Jo Chuang 5 лет назад
Родитель
Сommit
6ca5648bdc
91 измененных файлов с 6213 добавлено и 5132 удалено
  1. 7 15
      dashboard/src/components/Boilerplate.tsx
  2. 30 29
      dashboard/src/components/ConfirmOverlay.tsx
  3. 21 19
      dashboard/src/components/ExpandableResource.tsx
  4. 7 9
      dashboard/src/components/Loading.tsx
  5. 80 55
      dashboard/src/components/ResourceTab.tsx
  6. 39 36
      dashboard/src/components/SaveButton.tsx
  7. 90 59
      dashboard/src/components/Selector.tsx
  8. 51 37
      dashboard/src/components/StatusIndicator.tsx
  9. 18 27
      dashboard/src/components/TabRegion.tsx
  10. 38 41
      dashboard/src/components/TabSelector.tsx
  11. 20 14
      dashboard/src/components/TooltipParent.tsx
  12. 29 31
      dashboard/src/components/YamlEditor.tsx
  13. 20 23
      dashboard/src/components/forms/VeleroForm.tsx
  14. 134 109
      dashboard/src/components/image-selector/ImageSelector.tsx
  15. 51 41
      dashboard/src/components/image-selector/TagList.tsx
  16. 50 41
      dashboard/src/components/repo-selector/BranchList.tsx
  17. 87 77
      dashboard/src/components/repo-selector/ContentsList.tsx
  18. 43 39
      dashboard/src/components/repo-selector/NewGHAction.tsx
  19. 141 114
      dashboard/src/components/repo-selector/RepoSelector.tsx
  20. 27 22
      dashboard/src/components/values-form/Base64InputRow.tsx
  21. 18 19
      dashboard/src/components/values-form/CheckboxList.tsx
  22. 11 11
      dashboard/src/components/values-form/CheckboxRow.tsx
  23. 8 6
      dashboard/src/components/values-form/Heading.tsx
  24. 3 3
      dashboard/src/components/values-form/Helper.tsx
  25. 28 23
      dashboard/src/components/values-form/InputRow.tsx
  26. 9 14
      dashboard/src/components/values-form/MultiSelect.tsx
  27. 14 16
      dashboard/src/components/values-form/SelectRow.tsx
  28. 17 16
      dashboard/src/components/values-form/TextArea.tsx
  29. 62 72
      dashboard/src/components/values-form/ValuesForm.tsx
  30. 47 41
      dashboard/src/components/values-form/ValuesWrapper.tsx
  31. 378 292
      dashboard/src/main/home/Home.tsx
  32. 63 53
      dashboard/src/main/home/cluster-dashboard/ClusterDashboard.tsx
  33. 42 30
      dashboard/src/main/home/cluster-dashboard/NamespaceSelector.tsx
  34. 18 18
      dashboard/src/main/home/cluster-dashboard/SortSelector.tsx
  35. 40 38
      dashboard/src/main/home/cluster-dashboard/chart/Chart.tsx
  36. 186 144
      dashboard/src/main/home/cluster-dashboard/chart/ChartList.tsx
  37. 385 309
      dashboard/src/main/home/cluster-dashboard/expanded-chart/ExpandedChart.tsx
  38. 16 20
      dashboard/src/main/home/cluster-dashboard/expanded-chart/GraphSection.tsx
  39. 44 38
      dashboard/src/main/home/cluster-dashboard/expanded-chart/ListSection.tsx
  40. 140 95
      dashboard/src/main/home/cluster-dashboard/expanded-chart/RevisionSection.tsx
  41. 167 118
      dashboard/src/main/home/cluster-dashboard/expanded-chart/SettingsSection.tsx
  42. 41 36
      dashboard/src/main/home/cluster-dashboard/expanded-chart/ValuesYaml.tsx
  43. 25 32
      dashboard/src/main/home/cluster-dashboard/expanded-chart/deploy/DeploySection.tsx
  44. 12 19
      dashboard/src/main/home/cluster-dashboard/expanded-chart/deploy/EventTab.tsx
  45. 36 31
      dashboard/src/main/home/cluster-dashboard/expanded-chart/graph/Edge.tsx
  46. 275 162
      dashboard/src/main/home/cluster-dashboard/expanded-chart/graph/GraphDisplay.tsx
  47. 52 48
      dashboard/src/main/home/cluster-dashboard/expanded-chart/graph/InfoPanel.tsx
  48. 34 38
      dashboard/src/main/home/cluster-dashboard/expanded-chart/graph/Node.tsx
  49. 21 21
      dashboard/src/main/home/cluster-dashboard/expanded-chart/graph/SelectRegion.tsx
  50. 12 16
      dashboard/src/main/home/cluster-dashboard/expanded-chart/graph/ZoomPanel.tsx
  51. 140 113
      dashboard/src/main/home/cluster-dashboard/expanded-chart/status/ControllerTab.tsx
  52. 68 55
      dashboard/src/main/home/cluster-dashboard/expanded-chart/status/Logs.tsx
  53. 64 60
      dashboard/src/main/home/cluster-dashboard/expanded-chart/status/StatusSection.tsx
  54. 46 39
      dashboard/src/main/home/dashboard/ClusterList.tsx
  55. 21 19
      dashboard/src/main/home/dashboard/ClusterPlaceholder.tsx
  56. 11 15
      dashboard/src/main/home/dashboard/ClusterPlaceholderContainer.tsx
  57. 56 59
      dashboard/src/main/home/dashboard/Dashboard.tsx
  58. 8 15
      dashboard/src/main/home/dashboard/PipelinesSection.tsx
  59. 41 38
      dashboard/src/main/home/integrations/IntegrationList.tsx
  60. 136 93
      dashboard/src/main/home/integrations/Integrations.tsx
  61. 47 40
      dashboard/src/main/home/integrations/integration-form/DockerHubForm.tsx
  62. 77 61
      dashboard/src/main/home/integrations/integration-form/ECRForm.tsx
  63. 57 49
      dashboard/src/main/home/integrations/integration-form/EKSForm.tsx
  64. 61 54
      dashboard/src/main/home/integrations/integration-form/GCRForm.tsx
  65. 48 41
      dashboard/src/main/home/integrations/integration-form/GKEForm.tsx
  66. 16 19
      dashboard/src/main/home/integrations/integration-form/IntegrationForm.tsx
  67. 74 43
      dashboard/src/main/home/modals/ClusterInstructionsModal.tsx
  68. 40 39
      dashboard/src/main/home/modals/IntegrationsInstructionsModal.tsx
  69. 38 30
      dashboard/src/main/home/modals/IntegrationsModal.tsx
  70. 43 25
      dashboard/src/main/home/modals/Modal.tsx
  71. 134 110
      dashboard/src/main/home/modals/UpdateClusterModal.tsx
  72. 57 35
      dashboard/src/main/home/new-project/NewProject.tsx
  73. 164 136
      dashboard/src/main/home/project-settings/InviteList.tsx
  74. 30 25
      dashboard/src/main/home/project-settings/ProjectSettings.tsx
  75. 196 184
      dashboard/src/main/home/provisioner/AWSFormSection.tsx
  76. 101 97
      dashboard/src/main/home/provisioner/DOFormSection.tsx
  77. 48 40
      dashboard/src/main/home/provisioner/ExistingClusterSection.tsx
  78. 192 185
      dashboard/src/main/home/provisioner/GCPFormSection.tsx
  79. 21 19
      dashboard/src/main/home/provisioner/InfraStatuses.tsx
  80. 83 81
      dashboard/src/main/home/provisioner/ProvisionerSettings.tsx
  81. 203 150
      dashboard/src/main/home/provisioner/ProvisionerStatus.tsx
  82. 91 74
      dashboard/src/main/home/sidebar/ClusterSection.tsx
  83. 47 29
      dashboard/src/main/home/sidebar/Drawer.tsx
  84. 26 21
      dashboard/src/main/home/sidebar/ProjectSection.tsx
  85. 11 10
      dashboard/src/main/home/sidebar/ProjectSectionContainer.tsx
  86. 100 77
      dashboard/src/main/home/sidebar/Sidebar.tsx
  87. 72 52
      dashboard/src/main/home/templates/Templates.tsx
  88. 56 38
      dashboard/src/main/home/templates/expanded-template/ExpandedTemplate.tsx
  89. 238 184
      dashboard/src/main/home/templates/expanded-template/LaunchTemplate.tsx
  90. 54 51
      dashboard/src/main/home/templates/expanded-template/TemplateInfo.tsx
  91. 10 10
      dashboard/src/main/home/templates/hardcodedNameDict.tsx

+ 7 - 15
dashboard/src/components/Boilerplate.tsx

@@ -1,24 +1,16 @@
-import React, { Component } from 'react';
-import styled from 'styled-components';
+import React, { Component } from "react";
+import styled from "styled-components";
 
 
-type PropsType = {
-};
+type PropsType = {};
 
 
-type StateType = {
-};
+type StateType = {};
 
 
 export default class Boilerplate extends Component<PropsType, StateType> {
 export default class Boilerplate extends Component<PropsType, StateType> {
-  state = {
-  }
+  state = {};
 
 
   render() {
   render() {
-    return (
-      <StyledBoilerplate>
-        boilerplate
-      </StyledBoilerplate>
-    );
+    return <StyledBoilerplate>boilerplate</StyledBoilerplate>;
   }
   }
 }
 }
 
 
-const StyledBoilerplate = styled.div`
-`;
+const StyledBoilerplate = styled.div``;

+ 30 - 29
dashboard/src/components/ConfirmOverlay.tsx

@@ -1,15 +1,14 @@
-import React, { Component } from 'react';
-import styled from 'styled-components';
+import React, { Component } from "react";
+import styled from "styled-components";
 
 
 type PropsType = {
 type PropsType = {
-  message: string,
-  show: boolean,
-  onYes: () => void,
-  onNo: () => void
+  message: string;
+  show: boolean;
+  onYes: () => void;
+  onNo: () => void;
 };
 };
 
 
-type StateType = {
-};
+type StateType = {};
 
 
 export default class ConfirmOverlay extends Component<PropsType, StateType> {
 export default class ConfirmOverlay extends Component<PropsType, StateType> {
   render() {
   render() {
@@ -18,16 +17,8 @@ export default class ConfirmOverlay extends Component<PropsType, StateType> {
         <StyledConfirmOverlay>
         <StyledConfirmOverlay>
           {this.props.message}
           {this.props.message}
           <ButtonRow>
           <ButtonRow>
-            <ConfirmButton
-              onClick={this.props.onYes}
-            >
-              Yes
-          </ConfirmButton>
-            <ConfirmButton
-              onClick={this.props.onNo}
-            >
-              No
-          </ConfirmButton>
+            <ConfirmButton onClick={this.props.onYes}>Yes</ConfirmButton>
+            <ConfirmButton onClick={this.props.onNo}>No</ConfirmButton>
           </ButtonRow>
           </ButtonRow>
         </StyledConfirmOverlay>
         </StyledConfirmOverlay>
       );
       );
@@ -48,19 +39,23 @@ const StyledConfirmOverlay = styled.div`
   padding-bottom: 30px;
   padding-bottom: 30px;
   align-items: center;
   align-items: center;
   justify-content: center;
   justify-content: center;
-  font-family: 'Work Sans', sans-serif;
+  font-family: "Work Sans", sans-serif;
   font-size: 18px;
   font-size: 18px;
   font-weight: 500;
   font-weight: 500;
   color: white;
   color: white;
   flex-direction: column;
   flex-direction: column;
-  background: rgb(0,0,0,0.73);
+  background: rgb(0, 0, 0, 0.73);
   opacity: 0;
   opacity: 0;
   animation: lindEnter 0.2s;
   animation: lindEnter 0.2s;
   animation-fill-mode: forwards;
   animation-fill-mode: forwards;
 
 
   @keyframes lindEnter {
   @keyframes lindEnter {
-    from { opacity: 0; }
-    to   { opacity: 1; }
+    from {
+      opacity: 0;
+    }
+    to {
+      opacity: 1;
+    }
   }
   }
 `;
 `;
 
 
@@ -75,24 +70,30 @@ const ButtonRow = styled.div`
 const ConfirmButton = styled.div`
 const ConfirmButton = styled.div`
   font-size: 18px;
   font-size: 18px;
   padding: 10px 15px;
   padding: 10px 15px;
-  outline: none; 
+  outline: none;
   border: 1px solid white;
   border: 1px solid white;
-  border-radius: 10px; 
-  text-align: center; 
+  border-radius: 10px;
+  text-align: center;
   width: 80px;
   width: 80px;
   cursor: pointer;
   cursor: pointer;
   opacity: 0;
   opacity: 0;
-  font-family: 'Work Sans', sans-serif;
+  font-family: "Work Sans", sans-serif;
   font-size: 18px;
   font-size: 18px;
   font-weight: 500;
   font-weight: 500;
   animation: linEnter 0.3s 0.1s;
   animation: linEnter 0.3s 0.1s;
   animation-fill-mode: forwards;
   animation-fill-mode: forwards;
   @keyframes linEnter {
   @keyframes linEnter {
-    from { transform: translateY(20px); opacity: 0; }
-    to   { transform: translateY(0px); opacity: 1; }
+    from {
+      transform: translateY(20px);
+      opacity: 0;
+    }
+    to {
+      transform: translateY(0px);
+      opacity: 1;
+    }
   }
   }
   :hover {
   :hover {
     background: white;
     background: white;
     color: #232323;
     color: #232323;
   }
   }
-`;
+`;

+ 21 - 19
dashboard/src/components/ExpandableResource.tsx

@@ -1,21 +1,23 @@
-import React, { Component } from 'react';
-import styled from 'styled-components';
-import api from 'shared/api';
-import { Context } from 'shared/Context';
+import React, { Component } from "react";
+import styled from "styled-components";
+import { Context } from "shared/Context";
 
 
-import ResourceTab from './ResourceTab';
+import ResourceTab from "./ResourceTab";
 
 
 type PropsType = {
 type PropsType = {
-  resource: any,
-  handleClick?: () => void,
-  selected?: boolean,
-  isLast?: boolean,
-  roundAllCorners?: boolean,
+  resource: any;
+  handleClick?: () => void;
+  selected?: boolean;
+  isLast?: boolean;
+  roundAllCorners?: boolean;
 };
 };
 
 
 type StateType = any;
 type StateType = any;
 
 
-export default class ExpandableResource extends Component<PropsType, StateType> {
+export default class ExpandableResource extends Component<
+  PropsType,
+  StateType
+> {
   render() {
   render() {
     let { resource } = this.props;
     let { resource } = this.props;
     return (
     return (
@@ -34,16 +36,16 @@ export default class ExpandableResource extends Component<PropsType, StateType>
             </StatusHeader>
             </StatusHeader>
             {resource.message}
             {resource.message}
           </StatusSection>
           </StatusSection>
-          {
-            Object.keys(this.props.resource.data).map((key: string, i: number) => {
+          {Object.keys(this.props.resource.data).map(
+            (key: string, i: number) => {
               return (
               return (
                 <Pair key={i}>
                 <Pair key={i}>
-                  <Key>{key}:</Key> 
+                  <Key>{key}:</Key>
                   {this.props.resource.data[key]}
                   {this.props.resource.data[key]}
                 </Pair>
                 </Pair>
-              )
-            })
-          }
+              );
+            }
+          )}
         </ExpandedWrapper>
         </ExpandedWrapper>
       </ResourceTab>
       </ResourceTab>
     );
     );
@@ -81,7 +83,7 @@ const ExpandedWrapper = styled.div`
   padding: 20px 20px 25px;
   padding: 20px 20px 25px;
 `;
 `;
 
 
-const Pair = styled.div`  
+const Pair = styled.div`
   margin-top: 20px;
   margin-top: 20px;
   font-size: 13px;
   font-size: 13px;
   padding: 0 5px;
   padding: 0 5px;
@@ -94,4 +96,4 @@ const Key = styled.div`
   font-weight: bold;
   font-weight: bold;
   color: #ffffff;
   color: #ffffff;
   margin-right: 8px;
   margin-right: 8px;
-`;
+`;

+ 7 - 9
dashboard/src/components/Loading.tsx

@@ -1,17 +1,15 @@
-import React, { Component } from 'react';
-import styled from 'styled-components';
-import loading from 'assets/loading.gif';
+import React, { Component } from "react";
+import styled from "styled-components";
+import loading from "assets/loading.gif";
 
 
 type PropsType = {
 type PropsType = {
-  offset?: string
+  offset?: string;
 };
 };
 
 
-type StateType = {
-};
+type StateType = {};
 
 
 export default class Loading extends Component<PropsType, StateType> {
 export default class Loading extends Component<PropsType, StateType> {
-  state = {
-  }
+  state = {};
 
 
   render() {
   render() {
     return (
     return (
@@ -33,4 +31,4 @@ const StyledLoading = styled.div`
   align-items: center;
   align-items: center;
   justify-content: center;
   justify-content: center;
   margin-top: ${(props: { offset?: string }) => props.offset};
   margin-top: ${(props: { offset?: string }) => props.offset};
-`;
+`;

+ 80 - 55
dashboard/src/components/ResourceTab.tsx

@@ -1,33 +1,33 @@
-import React, { Component } from 'react';
-import styled from 'styled-components';
+import React, { Component } from "react";
+import styled from "styled-components";
 
 
-import { kindToIcon } from 'shared/rosettaStone';
+import { kindToIcon } from "shared/rosettaStone";
 
 
 type PropsType = {
 type PropsType = {
-  label: string,
-  name: string,
-  handleClick?: () => void,
-  selected?: boolean,
-  isLast?: boolean,
-  roundAllCorners?: boolean,
+  label: string;
+  name: string;
+  handleClick?: () => void;
+  selected?: boolean;
+  isLast?: boolean;
+  roundAllCorners?: boolean;
   status?: {
   status?: {
-    label: string,
-    available?: number,
-    total?: number,
-  } | null
-  expanded?: boolean,
+    label: string;
+    available?: number;
+    total?: number;
+  } | null;
+  expanded?: boolean;
 };
 };
 
 
 type StateType = {
 type StateType = {
-  expanded: boolean,
-  showTooltip: boolean,
+  expanded: boolean;
+  showTooltip: boolean;
 };
 };
 
 
 export default class ResourceTab extends Component<PropsType, StateType> {
 export default class ResourceTab extends Component<PropsType, StateType> {
   state = {
   state = {
     expanded: this.props.expanded || false,
     expanded: this.props.expanded || false,
     showTooltip: false,
     showTooltip: false,
-  }
+  };
 
 
   renderDropdownIcon = () => {
   renderDropdownIcon = () => {
     if (this.props.children) {
     if (this.props.children) {
@@ -37,28 +37,26 @@ export default class ResourceTab extends Component<PropsType, StateType> {
         </DropdownIcon>
         </DropdownIcon>
       );
       );
     }
     }
-  }
+  };
 
 
   renderIcon = (kind: string) => {
   renderIcon = (kind: string) => {
-    let icon = 'tonality';
+    let icon = "tonality";
     if (Object.keys(kindToIcon).includes(kind)) {
     if (Object.keys(kindToIcon).includes(kind)) {
-      icon = kindToIcon[kind]; 
+      icon = kindToIcon[kind];
     }
     }
-    
+
     return (
     return (
       <IconWrapper>
       <IconWrapper>
         <i className="material-icons">{icon}</i>
         <i className="material-icons">{icon}</i>
       </IconWrapper>
       </IconWrapper>
     );
     );
-  }
+  };
 
 
   renderTooltip = (x: string): JSX.Element | undefined => {
   renderTooltip = (x: string): JSX.Element | undefined => {
     if (this.state.showTooltip) {
     if (this.state.showTooltip) {
-      return (
-        <Tooltip>{x}</Tooltip>
-      );
+      return <Tooltip>{x}</Tooltip>;
     }
     }
-  }
+  };
 
 
   getStatusText = () => {
   getStatusText = () => {
     let { status } = this.props;
     let { status } = this.props;
@@ -67,7 +65,7 @@ export default class ResourceTab extends Component<PropsType, StateType> {
     } else if (status.label) {
     } else if (status.label) {
       return status.label;
       return status.label;
     }
     }
-  }
+  };
 
 
   renderStatus = () => {
   renderStatus = () => {
     let { status } = this.props;
     let { status } = this.props;
@@ -80,20 +78,16 @@ export default class ResourceTab extends Component<PropsType, StateType> {
         </Status>
         </Status>
       );
       );
     }
     }
-  }
+  };
 
 
   renderExpanded = () => {
   renderExpanded = () => {
     if (this.props.children && this.state.expanded) {
     if (this.props.children && this.state.expanded) {
-      return (
-        <ExpandWrapper>
-          {this.props.children}
-        </ExpandWrapper>
-      );
+      return <ExpandWrapper>{this.props.children}</ExpandWrapper>;
     }
     }
-  }
+  };
 
 
   render() {
   render() {
-    let { 
+    let {
       label,
       label,
       name,
       name,
       children,
       children,
@@ -104,7 +98,7 @@ export default class ResourceTab extends Component<PropsType, StateType> {
       roundAllCorners,
       roundAllCorners,
     } = this.props;
     } = this.props;
     return (
     return (
-      <StyledResourceTab 
+      <StyledResourceTab
         isLast={isLast}
         isLast={isLast}
         onClick={() => handleClick && handleClick()}
         onClick={() => handleClick && handleClick()}
         roundAllCorners={roundAllCorners}
         roundAllCorners={roundAllCorners}
@@ -125,8 +119,12 @@ export default class ResourceTab extends Component<PropsType, StateType> {
               {label}
               {label}
               <ResourceName
               <ResourceName
                 showKindLabels={true}
                 showKindLabels={true}
-                onMouseOver={() => { this.setState({ showTooltip: true }) }}
-                onMouseOut={() => { this.setState({ showTooltip: false }) }}
+                onMouseOver={() => {
+                  this.setState({ showTooltip: true });
+                }}
+                onMouseOut={() => {
+                  this.setState({ showTooltip: false });
+                }}
               >
               >
                 {name}
                 {name}
               </ResourceName>
               </ResourceName>
@@ -145,8 +143,14 @@ const StyledResourceTab = styled.div`
   width: 100%;
   width: 100%;
   margin-bottom: 2px;
   margin-bottom: 2px;
   background: #ffffff11;
   background: #ffffff11;
-  border-bottom-left-radius: ${(props: { isLast: boolean, roundAllCorners: boolean }) => props.isLast ? '5px' : ''};
-  border-bottom-right-radius: ${(props: { isLast: boolean, roundAllCorners: boolean }) => props.roundAllCorners && props.isLast ? '5px' : ''};
+  border-bottom-left-radius: ${(props: {
+    isLast: boolean;
+    roundAllCorners: boolean;
+  }) => (props.isLast ? "5px" : "")};
+  border-bottom-right-radius: ${(props: {
+    isLast: boolean;
+    roundAllCorners: boolean;
+  }) => (props.roundAllCorners && props.isLast ? "5px" : "")};
 `;
 `;
 
 
 const Tooltip = styled.div`
 const Tooltip = styled.div`
@@ -170,13 +174,16 @@ const Tooltip = styled.div`
   animation: faded-in 0.2s 0.15s;
   animation: faded-in 0.2s 0.15s;
   animation-fill-mode: forwards;
   animation-fill-mode: forwards;
   @keyframes faded-in {
   @keyframes faded-in {
-    from { opacity: 0 }
-    to { opacity: 1 }
+    from {
+      opacity: 0;
+    }
+    to {
+      opacity: 1;
+    }
   }
   }
 `;
 `;
 
 
-const ExpandWrapper = styled.div`
-`;
+const ExpandWrapper = styled.div``;
 
 
 const ResourceHeader = styled.div`
 const ResourceHeader = styled.div`
   width: 100%;
   width: 100%;
@@ -188,9 +195,11 @@ const ResourceHeader = styled.div`
   color: #ffffff66;
   color: #ffffff66;
   user-select: none;
   user-select: none;
   padding: 8px 18px;
   padding: 8px 18px;
-  padding-left: ${(props: { expanded: boolean, hasChildren: boolean }) => props.hasChildren ? '10px' : '22px'};
+  padding-left: ${(props: { expanded: boolean; hasChildren: boolean }) =>
+    props.hasChildren ? "10px" : "22px"};
   cursor: pointer;
   cursor: pointer;
-  background: ${(props: { expanded: boolean, hasChildren: boolean }) => props.expanded ? '#ffffff11' : ''};
+  background: ${(props: { expanded: boolean; hasChildren: boolean }) =>
+    props.expanded ? "#ffffff11" : ""};
   :hover {
   :hover {
     background: #ffffff18;
     background: #ffffff18;
 
 
@@ -212,7 +221,8 @@ const Metadata = styled.div`
   display: flex;
   display: flex;
   align-items: center;
   align-items: center;
   position: relative;
   position: relative;
-  max-width: ${(props: { hasStatus: boolean }) => props.hasStatus ? 'calc(100% - 20px)' : '100%'};
+  max-width: ${(props: { hasStatus: boolean }) =>
+    props.hasStatus ? "calc(100% - 20px)" : "100%"};
 `;
 `;
 
 
 const Status = styled.div`
 const Status = styled.div`
@@ -236,13 +246,21 @@ const StatusColor = styled.div`
   width: 8px;
   width: 8px;
   min-width: 8px;
   min-width: 8px;
   height: 8px;
   height: 8px;
-  background: ${(props: { status: string }) => (props.status === 'running' || props.status === 'Ready' || props.status === 'Completed' ? '#4797ff' : props.status === 'failed' || props.status === 'FailedValidation' ? "#ed5f85" : "#f5cb42")};
+  background: ${(props: { status: string }) =>
+    props.status === "running" ||
+    props.status === "Ready" ||
+    props.status === "Completed"
+      ? "#4797ff"
+      : props.status === "failed" || props.status === "FailedValidation"
+      ? "#ed5f85"
+      : "#f5cb42"};
   border-radius: 20px;
   border-radius: 20px;
 `;
 `;
 
 
 const ResourceName = styled.div`
 const ResourceName = styled.div`
   color: #ffffff;
   color: #ffffff;
-  margin-left: ${(props: { showKindLabels: boolean }) => props.showKindLabels ? '10px' : ''};
+  margin-left: ${(props: { showKindLabels: boolean }) =>
+    props.showKindLabels ? "10px" : ""};
   text-transform: none;
   text-transform: none;
   white-space: nowrap;
   white-space: nowrap;
   overflow: hidden;
   overflow: hidden;
@@ -271,14 +289,21 @@ const DropdownIcon = styled.div`
     color: #ffffff66;
     color: #ffffff66;
     cursor: pointer;
     cursor: pointer;
     border-radius: 20px;
     border-radius: 20px;
-    background: ${(props: { expanded: boolean }) => props.expanded ? '#ffffff18' : ''};
-    transform: ${(props: { expanded: boolean }) => props.expanded ? 'rotate(180deg)' : ''};
-    animation: ${(props: { expanded: boolean }) => props.expanded ? 'quarterTurn 0.3s' : ''};
+    background: ${(props: { expanded: boolean }) =>
+      props.expanded ? "#ffffff18" : ""};
+    transform: ${(props: { expanded: boolean }) =>
+      props.expanded ? "rotate(180deg)" : ""};
+    animation: ${(props: { expanded: boolean }) =>
+      props.expanded ? "quarterTurn 0.3s" : ""};
     animation-fill-mode: forwards;
     animation-fill-mode: forwards;
 
 
     @keyframes quarterTurn {
     @keyframes quarterTurn {
-      from { transform: rotate(0deg) }
-      to { transform: rotate(90deg) }
+      from {
+        transform: rotate(0deg);
+      }
+      to {
+        transform: rotate(90deg);
+      }
     }
     }
   }
   }
-`;
+`;

+ 39 - 36
dashboard/src/components/SaveButton.tsx

@@ -1,39 +1,37 @@
-import React, { Component } from 'react';
-import styled from 'styled-components';
-import loading from 'assets/loading.gif';
+import React, { Component } from "react";
+import styled from "styled-components";
+import loading from "assets/loading.gif";
 
 
 type PropsType = {
 type PropsType = {
-  text: string,
-  onClick: () => void,
-  disabled?: boolean,
-  status?: string | null,
-  color?: string,
-  helper?: string | null,
+  text: string;
+  onClick: () => void;
+  disabled?: boolean;
+  status?: string | null;
+  color?: string;
+  helper?: string | null;
 
 
   // Makes flush with corner if not within a modal
   // Makes flush with corner if not within a modal
-  makeFlush?: boolean 
+  makeFlush?: boolean;
 };
 };
 
 
-type StateType = {
-};
+type StateType = {};
 
 
 export default class SaveButton extends Component<PropsType, StateType> {
 export default class SaveButton extends Component<PropsType, StateType> {
-
   renderStatus = () => {
   renderStatus = () => {
     if (this.props.status) {
     if (this.props.status) {
-      if (this.props.status === 'successful') {
+      if (this.props.status === "successful") {
         return (
         return (
           <StatusWrapper successful={true}>
           <StatusWrapper successful={true}>
             <i className="material-icons">done</i> Successfully updated
             <i className="material-icons">done</i> Successfully updated
           </StatusWrapper>
           </StatusWrapper>
         );
         );
-      } else if (this.props.status === 'loading') {
+      } else if (this.props.status === "loading") {
         return (
         return (
           <StatusWrapper successful={false}>
           <StatusWrapper successful={false}>
             <LoadingGif src={loading} /> Updating . . .
             <LoadingGif src={loading} /> Updating . . .
           </StatusWrapper>
           </StatusWrapper>
         );
         );
-      } else if (this.props.status === 'error') {
+      } else if (this.props.status === "error") {
         return (
         return (
           <StatusWrapper successful={false}>
           <StatusWrapper successful={false}>
             <i className="material-icons">error_outline</i> Could not update
             <i className="material-icons">error_outline</i> Could not update
@@ -51,16 +49,16 @@ export default class SaveButton extends Component<PropsType, StateType> {
         <StatusWrapper successful={true}>{this.props.helper}</StatusWrapper>
         <StatusWrapper successful={true}>{this.props.helper}</StatusWrapper>
       );
       );
     }
     }
-  }
+  };
 
 
   render() {
   render() {
     return (
     return (
       <ButtonWrapper makeFlush={this.props.makeFlush}>
       <ButtonWrapper makeFlush={this.props.makeFlush}>
         {this.renderStatus()}
         {this.renderStatus()}
-        <Button 
+        <Button
           disabled={this.props.disabled}
           disabled={this.props.disabled}
           onClick={this.props.onClick}
           onClick={this.props.onClick}
-          color={this.props.color || '#616FEEcc'}
+          color={this.props.color || "#616FEEcc"}
         >
         >
           {this.props.text}
           {this.props.text}
         </Button>
         </Button>
@@ -79,7 +77,7 @@ const LoadingGif = styled.img`
 const StatusWrapper = styled.div`
 const StatusWrapper = styled.div`
   display: flex;
   display: flex;
   align-items: center;
   align-items: center;
-  font-family: 'Work Sans', sans-serif;
+  font-family: "Work Sans", sans-serif;
   font-size: 13px;
   font-size: 13px;
   color: #ffffff55;
   color: #ffffff55;
   margin-right: 25px;
   margin-right: 25px;
@@ -87,7 +85,8 @@ const StatusWrapper = styled.div`
   > i {
   > i {
     font-size: 18px;
     font-size: 18px;
     margin-right: 10px;
     margin-right: 10px;
-    color: ${(props: { successful: boolean }) => props.successful ? '#4797ff' : '#fcba03'};
+    color: ${(props: { successful: boolean }) =>
+      props.successful ? "#4797ff" : "#fcba03"};
   }
   }
 
 
   animation: statusFloatIn 0.5s;
   animation: statusFloatIn 0.5s;
@@ -95,10 +94,12 @@ const StatusWrapper = styled.div`
 
 
   @keyframes statusFloatIn {
   @keyframes statusFloatIn {
     from {
     from {
-      opacity: 0; transform: translateY(10px);
+      opacity: 0;
+      transform: translateY(10px);
     }
     }
     to {
     to {
-      opacity: 1; transform: translateY(0px);
+      opacity: 1;
+      transform: translateY(0px);
     }
     }
   }
   }
 `;
 `;
@@ -109,35 +110,37 @@ const ButtonWrapper = styled.div`
   position: absolute;
   position: absolute;
   ${(props: { makeFlush: boolean }) => {
   ${(props: { makeFlush: boolean }) => {
     if (!props.makeFlush) {
     if (!props.makeFlush) {
-      return (`
+      return `
         bottom: 25px;
         bottom: 25px;
         right: 27px;
         right: 27px;
-      `);
-    } 
-    return (`
+      `;
+    }
+    return `
       bottom: 0;
       bottom: 0;
       right: 0;
       right: 0;
-    `);
+    `;
   }}
   }}
-
 `;
 `;
 
 
 const Button = styled.button`
 const Button = styled.button`
   height: 35px;
   height: 35px;
   font-size: 13px;
   font-size: 13px;
   font-weight: 500;
   font-weight: 500;
-  font-family: 'Work Sans', sans-serif;
+  font-family: "Work Sans", sans-serif;
   color: white;
   color: white;
   padding: 6px 20px 7px 20px;
   padding: 6px 20px 7px 20px;
   text-align: left;
   text-align: left;
   border: 0;
   border: 0;
   border-radius: 5px;
   border-radius: 5px;
-  background: ${(props) => (!props.disabled ? props.color : '#aaaabb')};
-  box-shadow: ${(props) => (!props.disabled ? '0 2px 5px 0 #00000030' : 'none')};
-  cursor: ${(props) => (!props.disabled ? 'pointer' : 'default')};
+  background: ${(props) => (!props.disabled ? props.color : "#aaaabb")};
+  box-shadow: ${(props) =>
+    !props.disabled ? "0 2px 5px 0 #00000030" : "none"};
+  cursor: ${(props) => (!props.disabled ? "pointer" : "default")};
   user-select: none;
   user-select: none;
-  :focus { outline: 0 }
+  :focus {
+    outline: 0;
+  }
   :hover {
   :hover {
-    filter: ${(props) => (!props.disabled ? 'brightness(120%)' : '')};
+    filter: ${(props) => (!props.disabled ? "brightness(120%)" : "")};
   }
   }
-`;
+`;

+ 90 - 59
dashboard/src/components/Selector.tsx

@@ -1,97 +1,109 @@
-import React, { Component } from 'react';
-import styled from 'styled-components';
+import React, { Component } from "react";
+import styled from "styled-components";
 
 
 type PropsType = {
 type PropsType = {
-  activeValue: string,
-  options: { value: string, label: string }[],
-  setActiveValue: (x: string) => void,
-  width: string,
-  height?: string,
-  dropdownLabel?: string,
-  dropdownWidth?: string,
-  dropdownMaxHeight?: string,
-  closeOverlay?: boolean
+  activeValue: string;
+  options: { value: string; label: string }[];
+  setActiveValue: (x: string) => void;
+  width: string;
+  height?: string;
+  dropdownLabel?: string;
+  dropdownWidth?: string;
+  dropdownMaxHeight?: string;
+  closeOverlay?: boolean;
 };
 };
 
 
-type StateType = {
-};
+type StateType = {};
 
 
 export default class Selector extends Component<PropsType, StateType> {
 export default class Selector extends Component<PropsType, StateType> {
   state = {
   state = {
-    expanded: false
-  }
+    expanded: false,
+  };
 
 
   wrapperRef: any = React.createRef();
   wrapperRef: any = React.createRef();
   parentRef: any = React.createRef();
   parentRef: any = React.createRef();
 
 
   componentDidMount() {
   componentDidMount() {
-    document.addEventListener('mousedown', this.handleClickOutside.bind(this));
+    document.addEventListener("mousedown", this.handleClickOutside.bind(this));
   }
   }
 
 
   componentWillUnmount() {
   componentWillUnmount() {
-    document.removeEventListener('mousedown', this.handleClickOutside.bind(this));
+    document.removeEventListener(
+      "mousedown",
+      this.handleClickOutside.bind(this)
+    );
   }
   }
 
 
   handleClickOutside = (event: any) => {
   handleClickOutside = (event: any) => {
     if (
     if (
-      (this.wrapperRef && this.wrapperRef.current && !this.wrapperRef.current.contains(event.target)) &&
-      (this.parentRef && this.parentRef.current && !this.parentRef.current.contains(event.target))
+      this.wrapperRef &&
+      this.wrapperRef.current &&
+      !this.wrapperRef.current.contains(event.target) &&
+      this.parentRef &&
+      this.parentRef.current &&
+      !this.parentRef.current.contains(event.target)
     ) {
     ) {
-      this.setState({ expanded: false })
+      this.setState({ expanded: false });
     }
     }
-  }
+  };
 
 
-  handleOptionClick = (option: { value: string, label: string }) => {
+  handleOptionClick = (option: { value: string; label: string }) => {
     this.props.setActiveValue(option.value);
     this.props.setActiveValue(option.value);
     this.props.closeOverlay ? null : this.setState({ expanded: false });
     this.props.closeOverlay ? null : this.setState({ expanded: false });
-  }
+  };
 
 
   renderOptionList = () => {
   renderOptionList = () => {
     let { options, activeValue } = this.props;
     let { options, activeValue } = this.props;
-    return options.map((option: { value: string, label: string }, i: number) => {
-      return (
-        <Option
-          key={i}
-          selected={option.value === activeValue}
-          onClick={() => this.handleOptionClick(option)}
-          lastItem={i === options.length - 1}
-        >
-          {option.label}
-        </Option>
-      );
-    });
-  }
+    return options.map(
+      (option: { value: string; label: string }, i: number) => {
+        return (
+          <Option
+            key={i}
+            selected={option.value === activeValue}
+            onClick={() => this.handleOptionClick(option)}
+            lastItem={i === options.length - 1}
+          >
+            {option.label}
+          </Option>
+        );
+      }
+    );
+  };
 
 
   renderDropdownLabel = () => {
   renderDropdownLabel = () => {
-    if (this.props.dropdownLabel && this.props.dropdownLabel !== '') {
-      return (
-        <DropdownLabel>{this.props.dropdownLabel}</DropdownLabel>
-      );
+    if (this.props.dropdownLabel && this.props.dropdownLabel !== "") {
+      return <DropdownLabel>{this.props.dropdownLabel}</DropdownLabel>;
     }
     }
-  }
+  };
 
 
   renderDropdown = () => {
   renderDropdown = () => {
     if (this.state.expanded) {
     if (this.state.expanded) {
       return (
       return (
         <Dropdown
         <Dropdown
           ref={this.wrapperRef}
           ref={this.wrapperRef}
-          dropdownWidth={this.props.dropdownWidth ? this.props.dropdownWidth : this.props.width}
+          dropdownWidth={
+            this.props.dropdownWidth
+              ? this.props.dropdownWidth
+              : this.props.width
+          }
           dropdownMaxHeight={this.props.dropdownMaxHeight}
           dropdownMaxHeight={this.props.dropdownMaxHeight}
           onClick={() => this.setState({ expanded: false })}
           onClick={() => this.setState({ expanded: false })}
         >
         >
           {this.renderDropdownLabel()}
           {this.renderDropdownLabel()}
           {this.renderOptionList()}
           {this.renderOptionList()}
         </Dropdown>
         </Dropdown>
-      )
+      );
     }
     }
-  }
+  };
 
 
   getLabel = (value: string): any => {
   getLabel = (value: string): any => {
-    let tgt = this.props.options.find((element: { value: string, label: string }) => element.value === value);
+    let tgt = this.props.options.find(
+      (element: { value: string; label: string }) => element.value === value
+    );
     if (tgt) {
     if (tgt) {
       return tgt.label;
       return tgt.label;
     }
     }
-  }
+  };
 
 
   render() {
   render() {
     let { activeValue } = this.props;
     let { activeValue } = this.props;
@@ -105,7 +117,7 @@ export default class Selector extends Component<PropsType, StateType> {
           height={this.props.height}
           height={this.props.height}
         >
         >
           <TextWrap>
           <TextWrap>
-            {activeValue === '' ? 'All' : this.getLabel(activeValue)}
+            {activeValue === "" ? "All" : this.getLabel(activeValue)}
           </TextWrap>
           </TextWrap>
           <i className="material-icons">arrow_drop_down</i>
           <i className="material-icons">arrow_drop_down</i>
         </MainSelector>
         </MainSelector>
@@ -129,10 +141,12 @@ const DropdownLabel = styled.div`
   margin: 10px 13px;
   margin: 10px 13px;
 `;
 `;
 
 
-const Option = styled.div` 
+const Option = styled.div`
   width: 100%;
   width: 100%;
   border-top: 1px solid #00000000;
   border-top: 1px solid #00000000;
-  border-bottom: 1px solid ${(props: { selected: boolean, lastItem: boolean }) => props.lastItem ? '#ffffff00' : '#ffffff15'};
+  border-bottom: 1px solid
+    ${(props: { selected: boolean; lastItem: boolean }) =>
+      props.lastItem ? "#ffffff00" : "#ffffff15"};
   height: 37px;
   height: 37px;
   font-size: 13px;
   font-size: 13px;
   padding-top: 9px;
   padding-top: 9px;
@@ -143,7 +157,8 @@ const Option = styled.div`
   white-space: nowrap;
   white-space: nowrap;
   overflow: hidden;
   overflow: hidden;
   text-overflow: ellipsis;
   text-overflow: ellipsis;
-  background: ${(props: { selected: boolean, lastItem: boolean }) => props.selected ? '#ffffff11' : ''};
+  background: ${(props: { selected: boolean; lastItem: boolean }) =>
+    props.selected ? "#ffffff11" : ""};
 
 
   :hover {
   :hover {
     background: #ffffff22;
     background: #ffffff22;
@@ -164,8 +179,10 @@ const Dropdown = styled.div`
   right: 0;
   right: 0;
   top: calc(100% + 5px);
   top: calc(100% + 5px);
   background: #26282f;
   background: #26282f;
-  width: ${(props: { dropdownWidth: string, dropdownMaxHeight: string }) => props.dropdownWidth};
-  max-height: ${(props: { dropdownWidth: string, dropdownMaxHeight: string }) => props.dropdownMaxHeight || '300px'};
+  width: ${(props: { dropdownWidth: string; dropdownMaxHeight: string }) =>
+    props.dropdownWidth};
+  max-height: ${(props: { dropdownWidth: string; dropdownMaxHeight: string }) =>
+    props.dropdownMaxHeight || "300px"};
   border-radius: 3px;
   border-radius: 3px;
   z-index: 999;
   z-index: 999;
   overflow-y: auto;
   overflow-y: auto;
@@ -175,12 +192,14 @@ const Dropdown = styled.div`
 
 
 const StyledSelector = styled.div<{ width: string }>`
 const StyledSelector = styled.div<{ width: string }>`
   position: relative;
   position: relative;
-  width: ${props => props.width};
+  width: ${(props) => props.width};
 `;
 `;
 
 
 const MainSelector = styled.div`
 const MainSelector = styled.div`
-  width: ${(props: { expanded: boolean, width: string, height?: string }) => props.width};
-  height: ${(props: { expanded: boolean, width: string, height?: string }) => props.height ? props.height : '35px'};
+  width: ${(props: { expanded: boolean; width: string; height?: string }) =>
+    props.width};
+  height: ${(props: { expanded: boolean; width: string; height?: string }) =>
+    props.height ? props.height : "35px"};
   border: 1px solid #ffffff55;
   border: 1px solid #ffffff55;
   font-size: 13px;
   font-size: 13px;
   padding: 5px 10px;
   padding: 5px 10px;
@@ -190,13 +209,25 @@ const MainSelector = styled.div`
   align-items: center;
   align-items: center;
   justify-content: space-between;
   justify-content: space-between;
   cursor: pointer;
   cursor: pointer;
-  background: ${(props: { expanded: boolean, width: string, height?: string }) => props.expanded ? '#ffffff33' : '#ffffff11'};
+  background: ${(props: {
+    expanded: boolean;
+    width: string;
+    height?: string;
+  }) => (props.expanded ? "#ffffff33" : "#ffffff11")};
   :hover {
   :hover {
-    background: ${(props: { expanded: boolean, width: string, height?: string }) => props.expanded ? '#ffffff33' : '#ffffff22'};
+    background: ${(props: {
+      expanded: boolean;
+      width: string;
+      height?: string;
+    }) => (props.expanded ? "#ffffff33" : "#ffffff22")};
   }
   }
 
 
   > i {
   > i {
     font-size: 20px;
     font-size: 20px;
-    transform: ${(props: { expanded: boolean, width: string, height?: string }) => props.expanded ? 'rotate(180deg)' : ''};
+    transform: ${(props: {
+      expanded: boolean;
+      width: string;
+      height?: string;
+    }) => (props.expanded ? "rotate(180deg)" : "")};
   }
   }
-`;
+`;

+ 51 - 37
dashboard/src/components/StatusIndicator.tsx

@@ -1,11 +1,11 @@
-import React, { Component } from 'react';
-import styled from 'styled-components';
-import loading from 'assets/loading.gif';
+import React, { Component } from "react";
+import styled from "styled-components";
+import loading from "assets/loading.gif";
 
 
 type PropsType = {
 type PropsType = {
-    status: string,
-    controllers: Record<string, Record<string, any>>,
-    margin_left: string,
+  status: string;
+  controllers: Record<string, Record<string, any>>;
+  margin_left: string;
 };
 };
 
 
 type StateType = {};
 type StateType = {};
@@ -13,65 +13,70 @@ type StateType = {};
 // Manages a tab selector and renders the associated view
 // Manages a tab selector and renders the associated view
 export default class StatusIndicator extends Component<PropsType, StateType> {
 export default class StatusIndicator extends Component<PropsType, StateType> {
   renderStatus = (status: string) => {
   renderStatus = (status: string) => {
-    if (status == 'loading') {
+    if (status == "loading") {
       return (
       return (
         <div>
         <div>
           <Spinner src={loading} />
           <Spinner src={loading} />
         </div>
         </div>
-      )
+      );
     }
     }
 
 
     return (
     return (
       <div>
       <div>
         <StatusColor status={status} />
         <StatusColor status={status} />
       </div>
       </div>
-    )
-  }
+    );
+  };
 
 
   getChartStatus = (chartStatus: string) => {
   getChartStatus = (chartStatus: string) => {
-    if (chartStatus === 'deployed') {
+    if (chartStatus === "deployed") {
       for (var uid in this.props.controllers) {
       for (var uid in this.props.controllers) {
-        let value = this.props.controllers[uid]
-        let available = this.getAvailability(value.metadata.kind, value)
-        let progressing = true
+        let value = this.props.controllers[uid];
+        let available = this.getAvailability(value.metadata.kind, value);
+        let progressing = true;
 
 
-        this.props.controllers[uid]?.status?.conditions?.forEach((condition: any) => {
-          if (condition.type == "Progressing" && condition.status == "False"
-              && condition.reason == "ProgressDeadlineExceeded") {
-            progressing = false
+        this.props.controllers[uid]?.status?.conditions?.forEach(
+          (condition: any) => {
+            if (
+              condition.type == "Progressing" &&
+              condition.status == "False" &&
+              condition.reason == "ProgressDeadlineExceeded"
+            ) {
+              progressing = false;
+            }
           }
           }
-        })
+        );
 
 
         if (!available && progressing) {
         if (!available && progressing) {
-          return 'loading'
+          return "loading";
         } else if (!available && !progressing) {
         } else if (!available && !progressing) {
-          return 'failed'
+          return "failed";
         }
         }
       }
       }
-      return 'deployed'
+      return "deployed";
     }
     }
-    return chartStatus
-  }
+    return chartStatus;
+  };
 
 
   getAvailability = (kind: string, c: any) => {
   getAvailability = (kind: string, c: any) => {
     switch (kind?.toLowerCase()) {
     switch (kind?.toLowerCase()) {
       case "deployment":
       case "deployment":
       case "replicaset":
       case "replicaset":
-        return (c.status.availableReplicas == c.status.replicas)
+        return c.status.availableReplicas == c.status.replicas;
       case "statefulset":
       case "statefulset":
-       return (c.status.readyReplicas == c.status.replicas)
+        return c.status.readyReplicas == c.status.replicas;
       case "daemonset":
       case "daemonset":
-        return (c.status.numberAvailable == c.status.desiredNumberScheduled)
-      }
-  }
+        return c.status.numberAvailable == c.status.desiredNumberScheduled;
+    }
+  };
 
 
   render() {
   render() {
-    let status = this.getChartStatus(this.props.status)
+    let status = this.getChartStatus(this.props.status);
     return (
     return (
-    <Status margin_left={this.props.margin_left}>
+      <Status margin_left={this.props.margin_left}>
         {this.renderStatus(status)}
         {this.renderStatus(status)}
         {status}
         {status}
-    </Status>
+      </Status>
     );
     );
   }
   }
 }
 }
@@ -87,7 +92,12 @@ const StatusColor = styled.div`
   margin-bottom: 1px;
   margin-bottom: 1px;
   width: 8px;
   width: 8px;
   height: 8px;
   height: 8px;
-  background: ${(props: { status: string }) => (props.status === 'deployed' ? '#4797ff' : props.status === 'failed' ? "#ed5f85" : "#f5cb42")};
+  background: ${(props: { status: string }) =>
+    props.status === "deployed"
+      ? "#4797ff"
+      : props.status === "failed"
+      ? "#ed5f85"
+      : "#f5cb42"};
   border-radius: 20px;
   border-radius: 20px;
   margin-right: 16px;
   margin-right: 16px;
 `;
 `;
@@ -99,13 +109,17 @@ const Status = styled.div`
   flex-direction: row;
   flex-direction: row;
   text-transform: capitalize;
   text-transform: capitalize;
   align-items: center;
   align-items: center;
-  font-family: 'Hind Siliguri', sans-serif;
+  font-family: "Hind Siliguri", sans-serif;
   color: #aaaabb;
   color: #aaaabb;
   animation: fadeIn 0.5s;
   animation: fadeIn 0.5s;
-  margin-left: ${(props: { margin_left: string}) => props.margin_left};
+  margin-left: ${(props: { margin_left: string }) => props.margin_left};
 
 
   @keyframes fadeIn {
   @keyframes fadeIn {
-    from { opacity: 0 }
-    to { opacity: 1 }
+    from {
+      opacity: 0;
+    }
+    to {
+      opacity: 1;
+    }
   }
   }
 `;
 `;

+ 18 - 27
dashboard/src/components/TabRegion.tsx

@@ -1,20 +1,19 @@
-import React, { Component } from 'react';
-import styled from 'styled-components';
+import React, { Component } from "react";
+import styled from "styled-components";
 
 
-import TabSelector from './TabSelector';
-import Loading from './Loading';
+import TabSelector from "./TabSelector";
+import Loading from "./Loading";
 
 
 type PropsType = {
 type PropsType = {
-  options: { label: string, value: string }[],
-  currentTab: string,
-  setCurrentTab: (x: string) => void,
-  defaultTab?: string,
-  addendum?: any,
-  color?: string | null,
+  options: { label: string; value: string }[];
+  currentTab: string;
+  setCurrentTab: (x: string) => void;
+  defaultTab?: string;
+  addendum?: any;
+  color?: string | null;
 };
 };
 
 
-type StateType = {
-};
+type StateType = {};
 
 
 // Manages a tab selector and renders the associated view
 // Manages a tab selector and renders the associated view
 export default class TabRegion extends Component<PropsType, StateType> {
 export default class TabRegion extends Component<PropsType, StateType> {
@@ -22,7 +21,7 @@ export default class TabRegion extends Component<PropsType, StateType> {
     if (!this.props.defaultTab && this.props.options[0]) {
     if (!this.props.defaultTab && this.props.options[0]) {
       this.props.setCurrentTab(this.props.options[0].value);
       this.props.setCurrentTab(this.props.options[0].value);
     }
     }
-  }
+  };
 
 
   componentDidMount() {
   componentDidMount() {
     this.setDefaultTab();
     this.setDefaultTab();
@@ -31,7 +30,7 @@ export default class TabRegion extends Component<PropsType, StateType> {
   componentDidUpdate(prevProps: PropsType) {
   componentDidUpdate(prevProps: PropsType) {
     let { options, currentTab } = this.props;
     let { options, currentTab } = this.props;
     if (prevProps.options !== options) {
     if (prevProps.options !== options) {
-      if (options.filter(x => x.value === currentTab).length === 0) {
+      if (options.filter((x) => x.value === currentTab).length === 0) {
         this.setDefaultTab();
         this.setDefaultTab();
       }
       }
     }
     }
@@ -39,9 +38,7 @@ export default class TabRegion extends Component<PropsType, StateType> {
 
 
   renderContents = () => {
   renderContents = () => {
     if (!this.props.currentTab) {
     if (!this.props.currentTab) {
-      return (
-        <Loading />
-      );
+      return <Loading />;
     }
     }
 
 
     return (
     return (
@@ -54,19 +51,13 @@ export default class TabRegion extends Component<PropsType, StateType> {
           addendum={this.props.addendum}
           addendum={this.props.addendum}
         />
         />
         <Gap />
         <Gap />
-        <TabContents>
-          {this.props.children}
-        </TabContents>
+        <TabContents>{this.props.children}</TabContents>
       </Div>
       </Div>
     );
     );
-  }
+  };
 
 
   render() {
   render() {
-    return (
-      <StyledTabRegion>
-        {this.renderContents()}
-      </StyledTabRegion>
-    );
+    return <StyledTabRegion>{this.renderContents()}</StyledTabRegion>;
   }
   }
 }
 }
 
 
@@ -103,4 +94,4 @@ const StyledTabRegion = styled.div`
   height: 100%;
   height: 100%;
   position: relative;
   position: relative;
   overflow-y: auto;
   overflow-y: auto;
-`;
+`;

+ 38 - 41
dashboard/src/components/TabSelector.tsx

@@ -1,58 +1,50 @@
-import React, { Component } from 'react';
-import styled from 'styled-components';
+import React, { Component } from "react";
+import styled from "styled-components";
 
 
 export interface selectOption {
 export interface selectOption {
-  value: string,
-  label: string
+  value: string;
+  label: string;
 }
 }
 
 
 type PropsType = {
 type PropsType = {
-  currentTab: string,
-  options: selectOption[],
-  setCurrentTab: (value: string) => void,
-  addendum?: any,
-  color?: string
+  currentTab: string;
+  options: selectOption[];
+  setCurrentTab: (value: string) => void;
+  addendum?: any;
+  color?: string;
 };
 };
 
 
-type StateType = {
-};
+type StateType = {};
 
 
 export default class TabSelector extends Component<PropsType, StateType> {
 export default class TabSelector extends Component<PropsType, StateType> {
   handleTabClick = (value: string) => {
   handleTabClick = (value: string) => {
     this.props.setCurrentTab(value);
     this.props.setCurrentTab(value);
-  }
+  };
 
 
   renderTabList = () => {
   renderTabList = () => {
-    let color = this.props.color || '#949effff';
-    return (
-      this.props.options.map((option: selectOption, i: number) => {
-        return (
-          <Tab
-            key={i}
-            onClick={() => this.handleTabClick(option.value)}
-            lastItem={i === this.props.options.length - 1}
-            highlight={option.value === this.props.currentTab ? color : null}
-          >
-            {option.label}
-          </Tab>
-        );
-      })
-    );
-  }
-
-  renderAddendumBuffer = () => {
+    let color = this.props.color || "#949effff";
+    return this.props.options.map((option: selectOption, i: number) => {
+      return (
+        <Tab
+          key={i}
+          onClick={() => this.handleTabClick(option.value)}
+          lastItem={i === this.props.options.length - 1}
+          highlight={option.value === this.props.currentTab ? color : null}
+        >
+          {option.label}
+        </Tab>
+      );
+    });
+  };
 
 
-  }
+  renderAddendumBuffer = () => {};
 
 
   render() {
   render() {
     return (
     return (
       <StyledTabSelector>
       <StyledTabSelector>
         <TabWrapper>
         <TabWrapper>
           {this.renderTabList()}
           {this.renderTabList()}
-          <Tab
-            lastItem={true}
-            highlight={null}
-          >
+          <Tab lastItem={true} highlight={null}>
             <Buffer />
             <Buffer />
           </Tab>
           </Tab>
         </TabWrapper>
         </TabWrapper>
@@ -77,12 +69,14 @@ const TabWrapper = styled.div`
 
 
 const Tab = styled.div`
 const Tab = styled.div`
   height: 30px;
   height: 30px;
-  margin-right: ${(props: { lastItem: boolean, highlight: string }) => props.lastItem ? '' : '30px'};
+  margin-right: ${(props: { lastItem: boolean; highlight: string }) =>
+    props.lastItem ? "" : "30px"};
   display: flex;
   display: flex;
-  font-family: 'Work Sans', sans-serif;
+  font-family: "Work Sans", sans-serif;
   font-size: 13px;
   font-size: 13px;
   user-select: none;
   user-select: none;
-  color: ${(props: { lastItem: boolean, highlight: string }) => props.highlight ? props.highlight : '#aaaabb55'};
+  color: ${(props: { lastItem: boolean; highlight: string }) =>
+    props.highlight ? props.highlight : "#aaaabb55"};
   flex-direction: column;
   flex-direction: column;
   padding-top: 7px;
   padding-top: 7px;
   padding-bottom: 2px;
   padding-bottom: 2px;
@@ -90,9 +84,12 @@ const Tab = styled.div`
   align-items: center;
   align-items: center;
   cursor: pointer;
   cursor: pointer;
   white-space: nowrap;
   white-space: nowrap;
-  border-bottom: 1px solid ${(props: { lastItem: boolean, highlight: string }) => props.highlight ? props.highlight : 'none'};
+  border-bottom: 1px solid
+    ${(props: { lastItem: boolean; highlight: string }) =>
+      props.highlight ? props.highlight : "none"};
   :hover {
   :hover {
-    color: ${(props: { lastItem: boolean, highlight: string }) => props.highlight ? '' : '#aaaabb'};
+    color: ${(props: { lastItem: boolean; highlight: string }) =>
+      props.highlight ? "" : "#aaaabb"};
   }
   }
 `;
 `;
 
 
@@ -104,4 +101,4 @@ const StyledTabSelector = styled.div`
   padding-bottom: 1px;
   padding-bottom: 1px;
   margin-left: 2px;
   margin-left: 2px;
   position: relative;
   position: relative;
-`;
+`;

+ 20 - 14
dashboard/src/components/TooltipParent.tsx

@@ -1,32 +1,34 @@
-import React, { Component } from 'react';
-import styled from 'styled-components';
+import React, { Component } from "react";
+import styled from "styled-components";
 
 
 type PropsType = {
 type PropsType = {
-  tooltipText: string
+  tooltipText: string;
 };
 };
 
 
 type StateType = {
 type StateType = {
-  showTooltip: boolean,
+  showTooltip: boolean;
 };
 };
 
 
 export default class TooltipParent extends Component<PropsType, StateType> {
 export default class TooltipParent extends Component<PropsType, StateType> {
   state = {
   state = {
     showTooltip: false,
     showTooltip: false,
-  }
+  };
 
 
   renderTooltip = (): JSX.Element | undefined => {
   renderTooltip = (): JSX.Element | undefined => {
     if (this.state.showTooltip) {
     if (this.state.showTooltip) {
-      return (
-        <Tooltip>{this.props.tooltipText}</Tooltip>
-      );
+      return <Tooltip>{this.props.tooltipText}</Tooltip>;
     }
     }
-  }
+  };
 
 
   render() {
   render() {
     return (
     return (
       <StyledTooltipParent
       <StyledTooltipParent
-        onMouseOver={() => { this.setState({ showTooltip: true }) }}
-        onMouseOut={() => { this.setState({ showTooltip: false }) }}
+        onMouseOver={() => {
+          this.setState({ showTooltip: true });
+        }}
+        onMouseOut={() => {
+          this.setState({ showTooltip: false });
+        }}
       >
       >
         {this.props.children}
         {this.props.children}
         {this.renderTooltip()}
         {this.renderTooltip()}
@@ -54,11 +56,15 @@ const Tooltip = styled.div`
   animation: faded-in 0.2s 0.15s;
   animation: faded-in 0.2s 0.15s;
   animation-fill-mode: forwards;
   animation-fill-mode: forwards;
   @keyframes faded-in {
   @keyframes faded-in {
-    from { opacity: 0 }
-    to { opacity: 1 }
+    from {
+      opacity: 0;
+    }
+    to {
+      opacity: 1;
+    }
   }
   }
 `;
 `;
 
 
 const StyledTooltipParent = styled.div`
 const StyledTooltipParent = styled.div`
   position: relative;
   position: relative;
-`;
+`;

+ 29 - 31
dashboard/src/components/YamlEditor.tsx

@@ -1,23 +1,21 @@
-import React, { Component } from 'react';
-import styled from 'styled-components';
-import AceEditor from 'react-ace';
+import React, { Component } from "react";
+import styled from "styled-components";
+import AceEditor from "react-ace";
 
 
-import 'ace-builds/src-noconflict/mode-yaml';
-import 'ace-builds/src-noconflict/theme-terminal';
+import "ace-builds/src-noconflict/mode-yaml";
+import "ace-builds/src-noconflict/theme-terminal";
 
 
 type PropsType = {
 type PropsType = {
-  value: string,
-  onChange?: (e: any) => void, // Might be read-only
-  height?: string,
-  border?: boolean,
-  readOnly?: boolean
-}
+  value: string;
+  onChange?: (e: any) => void; // Might be read-only
+  height?: string;
+  border?: boolean;
+  readOnly?: boolean;
+};
 
 
-type StateType = {
-}
+type StateType = {};
 
 
 class YamlEditor extends Component<PropsType, StateType> {
 class YamlEditor extends Component<PropsType, StateType> {
-
   // Uses the yaml-lint library to determine if a given string is valid yaml.
   // Uses the yaml-lint library to determine if a given string is valid yaml.
   // If the code is invalid, it returns an error message detailing what went wrong.
   // If the code is invalid, it returns an error message detailing what went wrong.
   checkYaml = () => {
   checkYaml = () => {
@@ -28,36 +26,33 @@ class YamlEditor extends Component<PropsType, StateType> {
       alert(error.message);
       alert(error.message);
     });
     });
     */
     */
-  }
+  };
 
 
   // Calls checkYaml and passes in the value from the textarea
   // Calls checkYaml and passes in the value from the textarea
   handleChange = (e: any) => {
   handleChange = (e: any) => {
     this.setState({ yaml: e });
     this.setState({ yaml: e });
-  }
+  };
 
 
   handleSubmit = (e: any) => {
   handleSubmit = (e: any) => {
     this.checkYaml();
     this.checkYaml();
     e.preventDefault();
     e.preventDefault();
-  }
+  };
 
 
   render() {
   render() {
     return (
     return (
       <Holder>
       <Holder>
-        <Editor
-          onSubmit={this.handleSubmit}
-          border={this.props.border}
-        >
+        <Editor onSubmit={this.handleSubmit} border={this.props.border}>
           <AceEditor
           <AceEditor
-            mode='yaml'
+            mode="yaml"
             value={this.props.value}
             value={this.props.value}
-            theme='terminal'
+            theme="terminal"
             onChange={this.props.onChange}
             onChange={this.props.onChange}
-            name='codeEditor'
+            name="codeEditor"
             readOnly={this.props.readOnly}
             readOnly={this.props.readOnly}
             editorProps={{ $blockScrolling: true }}
             editorProps={{ $blockScrolling: true }}
             height={this.props.height}
             height={this.props.height}
-            width='100%'
-            style={{ borderRadius: '5px' }}
+            width="100%"
+            style={{ borderRadius: "5px" }}
           />
           />
         </Editor>
         </Editor>
       </Holder>
       </Holder>
@@ -68,18 +63,21 @@ class YamlEditor extends Component<PropsType, StateType> {
 export default YamlEditor;
 export default YamlEditor;
 
 
 const Editor = styled.form`
 const Editor = styled.form`
-  border-radius: ${(props: { border: boolean }) => props.border ? '5px' : ''};
-  border: ${(props: { border: boolean }) => props.border ? '1px solid #ffffff22' : ''};
+  border-radius: ${(props: { border: boolean }) => (props.border ? "5px" : "")};
+  border: ${(props: { border: boolean }) =>
+    props.border ? "1px solid #ffffff22" : ""};
 `;
 `;
 
 
 const Holder = styled.div`
 const Holder = styled.div`
   .ace_scrollbar {
   .ace_scrollbar {
     display: none;
     display: none;
   }
   }
-  .ace_editor, .ace_editor * {
-    font-family: "Monaco", "Menlo", "Ubuntu Mono", "Droid Sans Mono", "Consolas", monospace !important;
+  .ace_editor,
+  .ace_editor * {
+    font-family: "Monaco", "Menlo", "Ubuntu Mono", "Droid Sans Mono", "Consolas",
+      monospace !important;
     font-size: 12px !important;
     font-size: 12px !important;
     font-weight: 400 !important;
     font-weight: 400 !important;
     letter-spacing: 0 !important;
     letter-spacing: 0 !important;
   }
   }
-`;
+`;

+ 20 - 23
dashboard/src/components/forms/VeleroForm.tsx

@@ -1,49 +1,46 @@
-import React, { Component } from 'react';
-import styled from 'styled-components';
+import React, { Component } from "react";
 
 
-import Heading from '../values-form/Heading';
-import Helper from '../values-form/Helper';
-import InputRow from '../values-form/InputRow';
-import MultiSelect from '../values-form/MultiSelect';
+import Heading from "../values-form/Heading";
+import InputRow from "../values-form/InputRow";
+import MultiSelect from "../values-form/MultiSelect";
 
 
-type PropsType = {
-};
+type PropsType = {};
 
 
 type StateType = {
 type StateType = {
-  name: string,
-  excludeNamespaces: string[],
-  excludeResources: string[],
-  includeNamespaces: string[],
-  includeResources: string[],
-  storageLocation: string,
-  volumeSnapshotLocations: string[],
+  name: string;
+  excludeNamespaces: string[];
+  excludeResources: string[];
+  includeNamespaces: string[];
+  includeResources: string[];
+  storageLocation: string;
+  volumeSnapshotLocations: string[];
 };
 };
 
 
 export default class VeleroForm extends Component<PropsType, StateType> {
 export default class VeleroForm extends Component<PropsType, StateType> {
   state = {
   state = {
-    name: '',
+    name: "",
     excludeNamespaces: [] as string[],
     excludeNamespaces: [] as string[],
     excludeResources: [] as string[],
     excludeResources: [] as string[],
     includeNamespaces: [] as string[],
     includeNamespaces: [] as string[],
     includeResources: [] as string[],
     includeResources: [] as string[],
-    storageLocation: '',
+    storageLocation: "",
     volumeSnapshotLocations: [] as string[],
     volumeSnapshotLocations: [] as string[],
-  }
+  };
 
 
   render() {
   render() {
     return (
     return (
       <>
       <>
         <Heading>Create a Bakup</Heading>
         <Heading>Create a Bakup</Heading>
         <InputRow
         <InputRow
-          placeholder='ex: my-backup'
-          type='text'
-          width='300px'
+          placeholder="ex: my-backup"
+          type="text"
+          width="300px"
           value={this.state.name}
           value={this.state.name}
           setValue={(x: string) => this.setState({ name: x })}
           setValue={(x: string) => this.setState({ name: x })}
-          label='Name'
+          label="Name"
         />
         />
         <MultiSelect />
         <MultiSelect />
       </>
       </>
     );
     );
   }
   }
-}
+}

+ 134 - 109
dashboard/src/components/image-selector/ImageSelector.tsx

@@ -1,30 +1,30 @@
-import React, { Component } from 'react';
-import styled from 'styled-components';
-import info from 'assets/info.svg';
-import edit from 'assets/edit.svg';
+import React, { Component } from "react";
+import styled from "styled-components";
+import info from "assets/info.svg";
+import edit from "assets/edit.svg";
 
 
-import api from 'shared/api';
-import { integrationList } from 'shared/common';
-import { Context } from 'shared/Context';
-import { ImageType } from 'shared/types';
+import api from "shared/api";
+import { integrationList } from "shared/common";
+import { Context } from "shared/Context";
+import { ImageType } from "shared/types";
 
 
-import Loading from '../Loading';
-import TagList from './TagList';
+import Loading from "../Loading";
+import TagList from "./TagList";
 
 
 type PropsType = {
 type PropsType = {
-  forceExpanded?: boolean,
-  selectedImageUrl: string | null,
-  selectedTag: string | null,
-  setSelectedImageUrl: (x: string) => void,
-  setSelectedTag: (x: string) => void,
+  forceExpanded?: boolean;
+  selectedImageUrl: string | null;
+  selectedTag: string | null;
+  setSelectedImageUrl: (x: string) => void;
+  setSelectedTag: (x: string) => void;
 };
 };
 
 
 type StateType = {
 type StateType = {
-  isExpanded: boolean,
-  loading: boolean,
-  error: boolean,
-  images: ImageType[],
-  clickedImage: ImageType | null,
+  isExpanded: boolean;
+  loading: boolean;
+  error: boolean;
+  images: ImageType[];
+  clickedImage: ImageType | null;
 };
 };
 
 
 export default class ImageSelector extends Component<PropsType, StateType> {
 export default class ImageSelector extends Component<PropsType, StateType> {
@@ -34,75 +34,89 @@ export default class ImageSelector extends Component<PropsType, StateType> {
     error: false,
     error: false,
     images: [] as ImageType[],
     images: [] as ImageType[],
     clickedImage: null as ImageType | null,
     clickedImage: null as ImageType | null,
-  }
+  };
 
 
   componentDidMount() {
   componentDidMount() {
     const { currentProject, setCurrentError } = this.context;
     const { currentProject, setCurrentError } = this.context;
-    let images = [] as ImageType[]
-    let errors = [] as number[]
-    api.getProjectRegistries('<token>', {}, { id: currentProject.id }, async (err: any, res: any) => {
-      if (err) {
-        console.log(err);
-        this.setState({ error: true });
-      } else {
-        let registries = res.data;
-        if (registries.length === 0) {
-          this.setState({ loading: false });
-        }
-
-        // Loop over connected image registries
-        registries.forEach(async (registry: any, i: number) => {
-          await new Promise((nextController: (res?: any) => void) => {           
-            api.getImageRepos('<token>', {}, 
-              { 
-                project_id: currentProject.id,
-                registry_id: registry.id,
-              }, (err: any, res: any) => {
-              if (err) {
-                errors.push(1);
-              } else {
-                res.data.sort((a: any, b: any) => (a.name > b.name) ? 1 : -1);
-                // Loop over found image repositories
-                let newImg = res.data.map((img: any) => {
-                  if (this.props.selectedImageUrl === img.uri) {
-                    this.setState({ 
-                      clickedImage: {
+    let images = [] as ImageType[];
+    let errors = [] as number[];
+    api.getProjectRegistries(
+      "<token>",
+      {},
+      { id: currentProject.id },
+      async (err: any, res: any) => {
+        if (err) {
+          console.log(err);
+          this.setState({ error: true });
+        } else {
+          let registries = res.data;
+          if (registries.length === 0) {
+            this.setState({ loading: false });
+          }
+
+          // Loop over connected image registries
+          registries.forEach(async (registry: any, i: number) => {
+            await new Promise((nextController: (res?: any) => void) => {
+              api.getImageRepos(
+                "<token>",
+                {},
+                {
+                  project_id: currentProject.id,
+                  registry_id: registry.id,
+                },
+                (err: any, res: any) => {
+                  if (err) {
+                    errors.push(1);
+                  } else {
+                    res.data.sort((a: any, b: any) =>
+                      a.name > b.name ? 1 : -1
+                    );
+                    // Loop over found image repositories
+                    let newImg = res.data.map((img: any) => {
+                      if (this.props.selectedImageUrl === img.uri) {
+                        this.setState({
+                          clickedImage: {
+                            kind: registry.service,
+                            source: img.uri,
+                            name: img.name,
+                            registryId: registry.id,
+                          },
+                        });
+                      }
+                      return {
                         kind: registry.service,
                         kind: registry.service,
                         source: img.uri,
                         source: img.uri,
                         name: img.name,
                         name: img.name,
                         registryId: registry.id,
                         registryId: registry.id,
-                      }
+                      };
                     });
                     });
+                    images.push(...newImg);
+                    errors.push(0);
                   }
                   }
-                  return {
-                    kind: registry.service, 
-                    source: img.uri,
-                    name: img.name,
-                    registryId: registry.id,
+
+                  if (i == registries.length - 1) {
+                    let error =
+                      errors.reduce((a, b) => {
+                        return a + b;
+                      }) == registries.length
+                        ? true
+                        : false;
+
+                    this.setState({
+                      images,
+                      loading: false,
+                      error,
+                    });
                   }
                   }
-                })
-                images.push(...newImg)
-                errors.push(0);
-              }
-              
-              if (i == registries.length - 1) {
-                let error = errors.reduce((a, b) => {
-                  return a + b;
-                }) == registries.length ? true : false; 
-
-                this.setState({
-                  images,
-                  loading: false,
-                  error,
-                });
-              }
-
-              nextController()
-            });    
-          })
-        });
+
+                  nextController();
+                }
+              );
+            });
+          });
+        }
       }
       }
-    });
+    );
   }
   }
 
 
   /*
   /*
@@ -113,46 +127,48 @@ export default class ImageSelector extends Component<PropsType, StateType> {
   renderImageList = () => {
   renderImageList = () => {
     let { images, loading, error } = this.state;
     let { images, loading, error } = this.state;
     if (loading) {
     if (loading) {
-      return <LoadingWrapper><Loading /></LoadingWrapper>
-    } else if (error || !images) {
-      return <LoadingWrapper>Error loading repos</LoadingWrapper>
-    } else if (images.length === 0) {
       return (
       return (
         <LoadingWrapper>
         <LoadingWrapper>
-          No registries found. 
+          <Loading />
         </LoadingWrapper>
         </LoadingWrapper>
       );
       );
+    } else if (error || !images) {
+      return <LoadingWrapper>Error loading repos</LoadingWrapper>;
+    } else if (images.length === 0) {
+      return <LoadingWrapper>No registries found.</LoadingWrapper>;
     }
     }
 
 
     return images.map((image: ImageType, i: number) => {
     return images.map((image: ImageType, i: number) => {
-      let icon = integrationList[image.kind] && integrationList[image.kind].icon;
+      let icon =
+        integrationList[image.kind] && integrationList[image.kind].icon;
       if (!icon) {
       if (!icon) {
-        icon = integrationList['docker'].icon;
+        icon = integrationList["docker"].icon;
       }
       }
       return (
       return (
         <ImageItem
         <ImageItem
           key={i}
           key={i}
           isSelected={image.source === this.props.selectedImageUrl}
           isSelected={image.source === this.props.selectedImageUrl}
           lastItem={i === images.length - 1}
           lastItem={i === images.length - 1}
-          onClick={() => { 
+          onClick={() => {
             this.props.setSelectedImageUrl(image.source);
             this.props.setSelectedImageUrl(image.source);
             this.setState({ clickedImage: image });
             this.setState({ clickedImage: image });
           }}
           }}
         >
         >
-          <img src={icon && icon} />{image.source}
+          <img src={icon && icon} />
+          {image.source}
         </ImageItem>
         </ImageItem>
       );
       );
     });
     });
-  }
+  };
 
 
   renderBackButton = () => {
   renderBackButton = () => {
     let { setSelectedImageUrl } = this.props;
     let { setSelectedImageUrl } = this.props;
     if (this.state.clickedImage) {
     if (this.state.clickedImage) {
       return (
       return (
         <BackButton
         <BackButton
-          width='175px'
+          width="175px"
           onClick={() => {
           onClick={() => {
-            setSelectedImageUrl('');
+            setSelectedImageUrl("");
             this.setState({ clickedImage: null });
             this.setState({ clickedImage: null });
           }}
           }}
         >
         >
@@ -161,16 +177,14 @@ export default class ImageSelector extends Component<PropsType, StateType> {
         </BackButton>
         </BackButton>
       );
       );
     }
     }
-  }
+  };
 
 
   renderExpanded = () => {
   renderExpanded = () => {
     let { selectedTag, selectedImageUrl, setSelectedTag } = this.props;
     let { selectedTag, selectedImageUrl, setSelectedTag } = this.props;
     if (!this.state.clickedImage) {
     if (!this.state.clickedImage) {
       return (
       return (
         <div>
         <div>
-          <ExpandedWrapper>
-            {this.renderImageList()}
-          </ExpandedWrapper>
+          <ExpandedWrapper>{this.renderImageList()}</ExpandedWrapper>
           {this.renderBackButton()}
           {this.renderBackButton()}
         </div>
         </div>
       );
       );
@@ -189,7 +203,7 @@ export default class ImageSelector extends Component<PropsType, StateType> {
         </div>
         </div>
       );
       );
     }
     }
-  }
+  };
 
 
   renderSelected = () => {
   renderSelected = () => {
     let { selectedImageUrl, setSelectedImageUrl } = this.props;
     let { selectedImageUrl, setSelectedImageUrl } = this.props;
@@ -197,11 +211,13 @@ export default class ImageSelector extends Component<PropsType, StateType> {
     let icon = info;
     let icon = info;
     if (clickedImage) {
     if (clickedImage) {
       icon = clickedImage.kind;
       icon = clickedImage.kind;
-      icon = integrationList[clickedImage.kind] && integrationList[clickedImage.kind].icon;
+      icon =
+        integrationList[clickedImage.kind] &&
+        integrationList[clickedImage.kind].icon;
       if (!icon) {
       if (!icon) {
-        icon = integrationList['docker'].icon;
+        icon = integrationList["docker"].icon;
       }
       }
-    } else if (selectedImageUrl && selectedImageUrl !== '') {
+    } else if (selectedImageUrl && selectedImageUrl !== "") {
       icon = edit;
       icon = edit;
     }
     }
     return (
     return (
@@ -210,21 +226,21 @@ export default class ImageSelector extends Component<PropsType, StateType> {
         <Input
         <Input
           onClick={(e: any) => e.stopPropagation()}
           onClick={(e: any) => e.stopPropagation()}
           value={selectedImageUrl}
           value={selectedImageUrl}
-          onChange={(e: any) => { 
-            setSelectedImageUrl(e.target.value); 
+          onChange={(e: any) => {
+            setSelectedImageUrl(e.target.value);
             this.setState({ clickedImage: null });
             this.setState({ clickedImage: null });
           }}
           }}
-          placeholder='Enter or select your container image URL'
+          placeholder="Enter or select your container image URL"
         />
         />
       </Label>
       </Label>
     );
     );
-  }
+  };
 
 
   handleClick = () => {
   handleClick = () => {
     if (!this.props.forceExpanded) {
     if (!this.props.forceExpanded) {
       this.setState({ isExpanded: !this.state.isExpanded });
       this.setState({ isExpanded: !this.state.isExpanded });
     }
     }
-  }
+  };
 
 
   render() {
   render() {
     return (
     return (
@@ -235,7 +251,11 @@ export default class ImageSelector extends Component<PropsType, StateType> {
           forceExpanded={this.props.forceExpanded}
           forceExpanded={this.props.forceExpanded}
         >
         >
           {this.renderSelected()}
           {this.renderSelected()}
-          {this.props.forceExpanded ? null : <i className="material-icons">{this.state.isExpanded ? 'close' : 'build'}</i>}
+          {this.props.forceExpanded ? null : (
+            <i className="material-icons">
+              {this.state.isExpanded ? "close" : "build"}
+            </i>
+          )}
         </StyledImageSelector>
         </StyledImageSelector>
 
 
         {this.state.isExpanded ? this.renderExpanded() : null}
         {this.state.isExpanded ? this.renderExpanded() : null}
@@ -292,13 +312,16 @@ const ImageItem = styled.div`
   display: flex;
   display: flex;
   width: 100%;
   width: 100%;
   font-size: 13px;
   font-size: 13px;
-  border-bottom: 1px solid ${(props: { lastItem: boolean, isSelected: boolean }) => props.lastItem ? '#00000000' : '#606166'};
+  border-bottom: 1px solid
+    ${(props: { lastItem: boolean; isSelected: boolean }) =>
+      props.lastItem ? "#00000000" : "#606166"};
   color: #ffffff;
   color: #ffffff;
   user-select: none;
   user-select: none;
   align-items: center;
   align-items: center;
   padding: 10px 0px;
   padding: 10px 0px;
   cursor: pointer;
   cursor: pointer;
-  background: ${(props: { isSelected: boolean, lastItem: boolean }) => props.isSelected ? '#ffffff11' : ''};
+  background: ${(props: { isSelected: boolean; lastItem: boolean }) =>
+    props.isSelected ? "#ffffff11" : ""};
   :hover {
   :hover {
     background: #ffffff22;
     background: #ffffff22;
 
 
@@ -352,7 +375,8 @@ const StyledImageSelector = styled.div`
   width: 100%;
   width: 100%;
   margin-top: 22px;
   margin-top: 22px;
   border: 1px solid #ffffff55;
   border: 1px solid #ffffff55;
-  background: ${(props: { isExpanded: boolean, forceExpanded: boolean }) => props.isExpanded ? '#ffffff11' : ''};
+  background: ${(props: { isExpanded: boolean; forceExpanded: boolean }) =>
+    props.isExpanded ? "#ffffff11" : ""};
   border-radius: 3px;
   border-radius: 3px;
   user-select: none;
   user-select: none;
   height: 40px;
   height: 40px;
@@ -361,7 +385,8 @@ const StyledImageSelector = styled.div`
   display: flex;
   display: flex;
   align-items: center;
   align-items: center;
   justify-content: space-between;
   justify-content: space-between;
-  cursor: ${(props: { isExpanded: boolean, forceExpanded: boolean }) => props.forceExpanded ? '' : 'pointer'};
+  cursor: ${(props: { isExpanded: boolean; forceExpanded: boolean }) =>
+    props.forceExpanded ? "" : "pointer"};
   :hover {
   :hover {
     background: #ffffff11;
     background: #ffffff11;
 
 
@@ -380,4 +405,4 @@ const StyledImageSelector = styled.div`
     border-radius: 20px;
     border-radius: 20px;
     padding: 4px;
     padding: 4px;
   }
   }
-`;
+`;

+ 51 - 41
dashboard/src/components/image-selector/TagList.tsx

@@ -1,25 +1,25 @@
-import React, { Component } from 'react';
-import styled from 'styled-components';
-import tag_icon from 'assets/tag.png';
-import info from 'assets/info.svg';
+import React, { Component } from "react";
+import styled from "styled-components";
+import tag_icon from "assets/tag.png";
+import info from "assets/info.svg";
 
 
-import api from 'shared/api';
-import { Context } from 'shared/Context';
+import api from "shared/api";
+import { Context } from "shared/Context";
 
 
-import Loading from '../Loading';
+import Loading from "../Loading";
 
 
 type PropsType = {
 type PropsType = {
-  setSelectedTag: (x: string) => void,
-  selectedTag: string,
-  selectedImageUrl: string,
-  registryId: number,
+  setSelectedTag: (x: string) => void;
+  selectedTag: string;
+  selectedImageUrl: string;
+  registryId: number;
 };
 };
 
 
 type StateType = {
 type StateType = {
-  loading: boolean,
-  error: boolean,
-  tags: string[],
-  currentTag: string | null,
+  loading: boolean;
+  error: boolean;
+  tags: string[];
+  currentTag: string | null;
 };
 };
 
 
 export default class TagList extends Component<PropsType, StateType> {
 export default class TagList extends Component<PropsType, StateType> {
@@ -28,42 +28,50 @@ export default class TagList extends Component<PropsType, StateType> {
     error: false,
     error: false,
     tags: [] as string[],
     tags: [] as string[],
     currentTag: this.props.selectedTag,
     currentTag: this.props.selectedTag,
-  }
+  };
 
 
   componentDidMount() {
   componentDidMount() {
     const { currentProject } = this.context;
     const { currentProject } = this.context;
-    let splits = this.props.selectedImageUrl.split('/');
+    let splits = this.props.selectedImageUrl.split("/");
     let repoName = splits[splits.length - 1];
     let repoName = splits[splits.length - 1];
-    api.getImageTags('<token>', {}, 
-      { 
+    api.getImageTags(
+      "<token>",
+      {},
+      {
         project_id: currentProject.id,
         project_id: currentProject.id,
         registry_id: this.props.registryId,
         registry_id: this.props.registryId,
         repo_name: repoName,
         repo_name: repoName,
-      }, (err: any, res: any) => {
-      if (err) {
-        console.log(err)
-        this.setState({ loading: false, error: true });
-      } else {
-        let tags = res.data.map((tag: any, i: number) => {
-          return tag.tag;
-        });
-        this.setState({ tags, loading: false });
+      },
+      (err: any, res: any) => {
+        if (err) {
+          console.log(err);
+          this.setState({ loading: false, error: true });
+        } else {
+          let tags = res.data.map((tag: any, i: number) => {
+            return tag.tag;
+          });
+          this.setState({ tags, loading: false });
+        }
       }
       }
-    });
+    );
   }
   }
 
 
   setTag = (tag: string) => {
   setTag = (tag: string) => {
     let { selectedTag, setSelectedTag } = this.props;
     let { selectedTag, setSelectedTag } = this.props;
     setSelectedTag(tag);
     setSelectedTag(tag);
     this.setState({ currentTag: tag });
     this.setState({ currentTag: tag });
-  }
+  };
 
 
   renderTagList = () => {
   renderTagList = () => {
     let { tags, loading, error } = this.state;
     let { tags, loading, error } = this.state;
     if (loading) {
     if (loading) {
-      return <LoadingWrapper><Loading /></LoadingWrapper>
+      return (
+        <LoadingWrapper>
+          <Loading />
+        </LoadingWrapper>
+      );
     } else if (error || !tags) {
     } else if (error || !tags) {
-      return <LoadingWrapper>Error loading tags</LoadingWrapper>
+      return <LoadingWrapper>Error loading tags</LoadingWrapper>;
     }
     }
 
 
     return tags.map((tag: string, i: number) => {
     return tags.map((tag: string, i: number) => {
@@ -74,21 +82,20 @@ export default class TagList extends Component<PropsType, StateType> {
           lastItem={i === tags.length - 1}
           lastItem={i === tags.length - 1}
           onClick={() => this.setTag(tag)}
           onClick={() => this.setTag(tag)}
         >
         >
-          <img src={tag_icon} />{tag}
+          <img src={tag_icon} />
+          {tag}
         </TagName>
         </TagName>
       );
       );
     });
     });
-  }
+  };
 
 
   render() {
   render() {
     return (
     return (
-<>
+      <>
         <TagNameAlt>
         <TagNameAlt>
           <img src={info} /> Select Image Tag
           <img src={info} /> Select Image Tag
         </TagNameAlt>
         </TagNameAlt>
-              <StyledTagList>
-        {this.renderTagList()}
-      </StyledTagList>
+        <StyledTagList>{this.renderTagList()}</StyledTagList>
       </>
       </>
     );
     );
   }
   }
@@ -106,13 +113,16 @@ const TagName = styled.div`
   display: flex;
   display: flex;
   width: 100%;
   width: 100%;
   font-size: 13px;
   font-size: 13px;
-  border-bottom: 1px solid ${(props: { lastItem?: boolean, isSelected?: boolean }) => props.lastItem ? '#00000000' : '#606166'};
+  border-bottom: 1px solid
+    ${(props: { lastItem?: boolean; isSelected?: boolean }) =>
+      props.lastItem ? "#00000000" : "#606166"};
   color: #ffffff;
   color: #ffffff;
   user-select: none;
   user-select: none;
   align-items: center;
   align-items: center;
   padding: 10px 0px;
   padding: 10px 0px;
   cursor: pointer;
   cursor: pointer;
-  background: ${(props: { isSelected?: boolean, lastItem?: boolean }) => props.isSelected ? '#ffffff11' : ''};
+  background: ${(props: { isSelected?: boolean; lastItem?: boolean }) =>
+    props.isSelected ? "#ffffff11" : ""};
   :hover {
   :hover {
     background: #ffffff22;
     background: #ffffff22;
 
 
@@ -147,4 +157,4 @@ const LoadingWrapper = styled.div`
   justify-content: center;
   justify-content: center;
   font-size: 13px;
   font-size: 13px;
   color: #ffffff44;
   color: #ffffff44;
-`;
+`;

+ 50 - 41
dashboard/src/components/repo-selector/BranchList.tsx

@@ -1,59 +1,68 @@
-import React, { Component } from 'react';
-import styled from 'styled-components';
-import branch_icon from 'assets/branch.png';
+import React, { Component } from "react";
+import styled from "styled-components";
+import branch_icon from "assets/branch.png";
 
 
-import api from 'shared/api';
-import { Context } from 'shared/Context';
+import api from "shared/api";
+import { Context } from "shared/Context";
 
 
-import Loading from '../Loading';
+import Loading from "../Loading";
 
 
 type PropsType = {
 type PropsType = {
-  grid: number,
-  repoName: string,
-  owner: string,
-  setSelectedBranch: (x: string) => void,
-  selectedBranch: string
+  grid: number;
+  repoName: string;
+  owner: string;
+  setSelectedBranch: (x: string) => void;
+  selectedBranch: string;
 };
 };
 
 
 type StateType = {
 type StateType = {
-  loading: boolean,
-  error: boolean,
-  branches: string[]
+  loading: boolean;
+  error: boolean;
+  branches: string[];
 };
 };
 
 
 export default class BranchList extends Component<PropsType, StateType> {
 export default class BranchList extends Component<PropsType, StateType> {
   state = {
   state = {
     loading: true,
     loading: true,
     error: false,
     error: false,
-    branches: [] as string[]
-  }
+    branches: [] as string[],
+  };
 
 
   componentDidMount() {
   componentDidMount() {
     let { currentProject } = this.context;
     let { currentProject } = this.context;
 
 
     // Get branches
     // Get branches
-    api.getBranches('<token>', {}, {
-      project_id: currentProject.id,
-      git_repo_id: this.props.grid,
-      kind: 'github',
-      owner: this.props.owner,
-      name: this.props.repoName,
-    }, (err: any, res: any) => {
-      if (err) {
-        console.log(err);
-        this.setState({ loading: false, error: true });
-      } else {
-        this.setState({ branches: res.data, loading: false, error: false });
+    api.getBranches(
+      "<token>",
+      {},
+      {
+        project_id: currentProject.id,
+        git_repo_id: this.props.grid,
+        kind: "github",
+        owner: this.props.owner,
+        name: this.props.repoName,
+      },
+      (err: any, res: any) => {
+        if (err) {
+          console.log(err);
+          this.setState({ loading: false, error: true });
+        } else {
+          this.setState({ branches: res.data, loading: false, error: false });
+        }
       }
       }
-    });
+    );
   }
   }
 
 
   renderBranchList = () => {
   renderBranchList = () => {
     let { branches, loading, error } = this.state;
     let { branches, loading, error } = this.state;
     if (loading) {
     if (loading) {
-      return <LoadingWrapper><Loading /></LoadingWrapper>
+      return (
+        <LoadingWrapper>
+          <Loading />
+        </LoadingWrapper>
+      );
     } else if (error || !branches) {
     } else if (error || !branches) {
-      return <LoadingWrapper>Error loading branches</LoadingWrapper>
+      return <LoadingWrapper>Error loading branches</LoadingWrapper>;
     }
     }
 
 
     return branches.map((branch: string, i: number) => {
     return branches.map((branch: string, i: number) => {
@@ -64,18 +73,15 @@ export default class BranchList extends Component<PropsType, StateType> {
           lastItem={i === branches.length - 1}
           lastItem={i === branches.length - 1}
           onClick={() => this.props.setSelectedBranch(branch)}
           onClick={() => this.props.setSelectedBranch(branch)}
         >
         >
-          <img src={branch_icon} />{branch}
+          <img src={branch_icon} />
+          {branch}
         </BranchName>
         </BranchName>
       );
       );
     });
     });
-  }
+  };
 
 
   render() {
   render() {
-    return (
-      <div>
-        {this.renderBranchList()}
-      </div>
-    );
+    return <div>{this.renderBranchList()}</div>;
   }
   }
 }
 }
 
 
@@ -85,13 +91,16 @@ const BranchName = styled.div`
   display: flex;
   display: flex;
   width: 100%;
   width: 100%;
   font-size: 13px;
   font-size: 13px;
-  border-bottom: 1px solid ${(props: { lastItem: boolean, isSelected: boolean }) => props.lastItem ? '#00000000' : '#606166'};
+  border-bottom: 1px solid
+    ${(props: { lastItem: boolean; isSelected: boolean }) =>
+      props.lastItem ? "#00000000" : "#606166"};
   color: #ffffff;
   color: #ffffff;
   user-select: none;
   user-select: none;
   align-items: center;
   align-items: center;
   padding: 10px 0px;
   padding: 10px 0px;
   cursor: pointer;
   cursor: pointer;
-  background: ${(props: { isSelected: boolean, lastItem: boolean }) => props.isSelected ? '#ffffff22' : '#ffffff11'};
+  background: ${(props: { isSelected: boolean; lastItem: boolean }) =>
+    props.isSelected ? "#ffffff22" : "#ffffff11"};
   :hover {
   :hover {
     background: #ffffff22;
     background: #ffffff22;
 
 
@@ -116,4 +125,4 @@ const LoadingWrapper = styled.div`
   justify-content: center;
   justify-content: center;
   font-size: 13px;
   font-size: 13px;
   color: #ffffff44;
   color: #ffffff44;
-`;
+`;

+ 87 - 77
dashboard/src/components/repo-selector/ContentsList.tsx

@@ -1,69 +1,77 @@
-import { stringify } from 'querystring';
-import React, { Component } from 'react';
-import styled from 'styled-components';
-import file from 'assets/file.svg';
-import folder from 'assets/folder.svg';
-import info from 'assets/info.svg';
+import React, { Component } from "react";
+import styled from "styled-components";
+import file from "assets/file.svg";
+import folder from "assets/folder.svg";
+import info from "assets/info.svg";
 
 
-import api from 'shared/api';
-import { Context } from 'shared/Context';
-import { FileType } from 'shared/types';
+import api from "shared/api";
+import { Context } from "shared/Context";
+import { FileType } from "shared/types";
 
 
-import Loading from '../Loading';
+import Loading from "../Loading";
 
 
 type PropsType = {
 type PropsType = {
-  grid: number,
-  repoName: string,
-  owner: string,
-  selectedBranch: string,
-  subdirectory: string,
-  setSubdirectory: (x: string) => void,
-  setDockerfile: () => void,
+  grid: number;
+  repoName: string;
+  owner: string;
+  selectedBranch: string;
+  subdirectory: string;
+  setSubdirectory: (x: string) => void;
+  setDockerfile: () => void;
 };
 };
 
 
 type StateType = {
 type StateType = {
-  loading: boolean,
-  error: boolean,
-  contents: FileType[]
+  loading: boolean;
+  error: boolean;
+  contents: FileType[];
 };
 };
 
 
 export default class ContentsList extends Component<PropsType, StateType> {
 export default class ContentsList extends Component<PropsType, StateType> {
   state = {
   state = {
     loading: true,
     loading: true,
     error: false,
     error: false,
-    contents: [] as FileType[]
-  }
+    contents: [] as FileType[],
+  };
 
 
   updateContents = () => {
   updateContents = () => {
     let { currentProject } = this.context;
     let { currentProject } = this.context;
 
 
     // Get branch contents
     // Get branch contents
-    api.getBranchContents('<token>', { dir: this.props.subdirectory }, {
-      project_id: currentProject.id,
-      git_repo_id: this.props.grid,
-      kind: 'github',
-      owner: this.props.owner,
-      name: this.props.repoName,
-      branch: this.props.selectedBranch
-    }, (err: any, res: any) => {
-      if (err) {
-        console.log(err);
-        this.setState({ loading: false, error: true });
-      } else {
-        let files = [] as FileType[];
-        let folders = [] as FileType[];
-        res.data.map((x: FileType, i: number) => {
-          x.Type === 'dir' ? folders.push(x) : files.push(x);
-        });
-
-        folders.sort((a: FileType, b: FileType) => { return a.Path < b.Path ? 1 : 0 });
-        files.sort((a: FileType, b: FileType) => { return a.Path < b.Path ? 1 : 0 });
-        let contents = folders.concat(files);
-        
-        this.setState({ contents, loading: false, error: false });
+    api.getBranchContents(
+      "<token>",
+      { dir: this.props.subdirectory },
+      {
+        project_id: currentProject.id,
+        git_repo_id: this.props.grid,
+        kind: "github",
+        owner: this.props.owner,
+        name: this.props.repoName,
+        branch: this.props.selectedBranch,
+      },
+      (err: any, res: any) => {
+        if (err) {
+          console.log(err);
+          this.setState({ loading: false, error: true });
+        } else {
+          let files = [] as FileType[];
+          let folders = [] as FileType[];
+          res.data.map((x: FileType, i: number) => {
+            x.Type === "dir" ? folders.push(x) : files.push(x);
+          });
+
+          folders.sort((a: FileType, b: FileType) => {
+            return a.Path < b.Path ? 1 : 0;
+          });
+          files.sort((a: FileType, b: FileType) => {
+            return a.Path < b.Path ? 1 : 0;
+          });
+          let contents = folders.concat(files);
+
+          this.setState({ contents, loading: false, error: false });
+        }
       }
       }
-    });
-  }
+    );
+  };
 
 
   componentDidMount() {
   componentDidMount() {
     this.updateContents();
     this.updateContents();
@@ -71,22 +79,26 @@ export default class ContentsList extends Component<PropsType, StateType> {
 
 
   componentDidUpdate(prevProps: PropsType) {
   componentDidUpdate(prevProps: PropsType) {
     if (this.props.subdirectory !== prevProps.subdirectory) {
     if (this.props.subdirectory !== prevProps.subdirectory) {
-      this.updateContents();  
+      this.updateContents();
     }
     }
   }
   }
 
 
   renderContentList = () => {
   renderContentList = () => {
     let { contents, loading, error } = this.state;
     let { contents, loading, error } = this.state;
     if (loading) {
     if (loading) {
-      return <LoadingWrapper><Loading /></LoadingWrapper>
+      return (
+        <LoadingWrapper>
+          <Loading />
+        </LoadingWrapper>
+      );
     } else if (error || !contents) {
     } else if (error || !contents) {
-      return <LoadingWrapper>Error loading repo contents.</LoadingWrapper>
+      return <LoadingWrapper>Error loading repo contents.</LoadingWrapper>;
     }
     }
 
 
     return contents.map((item: FileType, i: number) => {
     return contents.map((item: FileType, i: number) => {
-      let splits = item.Path.split('/');
+      let splits = item.Path.split("/");
       let fileName = splits[splits.length - 1];
       let fileName = splits[splits.length - 1];
-      if (item.Type === 'dir') {
+      if (item.Type === "dir") {
         return (
         return (
           <Item
           <Item
             key={i}
             key={i}
@@ -100,7 +112,7 @@ export default class ContentsList extends Component<PropsType, StateType> {
         );
         );
       }
       }
 
 
-      if (fileName === 'Dockerfile') {
+      if (fileName === "Dockerfile") {
         return (
         return (
           <FileItem
           <FileItem
             key={i}
             key={i}
@@ -114,48 +126,40 @@ export default class ContentsList extends Component<PropsType, StateType> {
         );
         );
       }
       }
       return (
       return (
-        <FileItem
-          key={i}
-          lastItem={i === contents.length - 1}
-        >
+        <FileItem key={i} lastItem={i === contents.length - 1}>
           <img src={file} />
           <img src={file} />
           {fileName}
           {fileName}
         </FileItem>
         </FileItem>
       );
       );
     });
     });
-  }
+  };
 
 
   renderJumpToParent = () => {
   renderJumpToParent = () => {
     let { subdirectory, setSubdirectory } = this.props;
     let { subdirectory, setSubdirectory } = this.props;
-    if (subdirectory !== '') {
-      let splits = subdirectory.split('/');
-      let subdir = '';
+    if (subdirectory !== "") {
+      let splits = subdirectory.split("/");
+      let subdir = "";
       if (splits.length !== 1) {
       if (splits.length !== 1) {
-        subdir = subdirectory.replace(splits[splits.length - 1], '');
-        if (subdir.charAt(subdir.length - 1) === '/') {
+        subdir = subdirectory.replace(splits[splits.length - 1], "");
+        if (subdir.charAt(subdir.length - 1) === "/") {
           subdir = subdir.slice(0, subdir.length - 1);
           subdir = subdir.slice(0, subdir.length - 1);
         }
         }
       }
       }
 
 
       return (
       return (
-        <Item
-          lastItem={false}
-          onClick={() => setSubdirectory(subdir)}
-        >
+        <Item lastItem={false} onClick={() => setSubdirectory(subdir)}>
           <BackLabel>..</BackLabel>
           <BackLabel>..</BackLabel>
         </Item>
         </Item>
       );
       );
     }
     }
 
 
     return (
     return (
-      <FileItem
-        lastItem={false}
-      >
+      <FileItem lastItem={false}>
         <img src={info} />
         <img src={info} />
         Select subfolder (Optional)
         Select subfolder (Optional)
       </FileItem>
       </FileItem>
     );
     );
-  }
+  };
 
 
   render() {
   render() {
     return (
     return (
@@ -180,13 +184,16 @@ const Item = styled.div`
   display: flex;
   display: flex;
   width: 100%;
   width: 100%;
   font-size: 13px;
   font-size: 13px;
-  border-bottom: 1px solid ${(props: { lastItem: boolean, isSelected?: boolean }) => props.lastItem ? '#00000000' : '#606166'};
+  border-bottom: 1px solid
+    ${(props: { lastItem: boolean; isSelected?: boolean }) =>
+      props.lastItem ? "#00000000" : "#606166"};
   color: #ffffff;
   color: #ffffff;
   user-select: none;
   user-select: none;
   align-items: center;
   align-items: center;
   padding: 10px 0px;
   padding: 10px 0px;
   cursor: pointer;
   cursor: pointer;
-  background: ${(props: { isSelected?: boolean, lastItem: boolean }) => props.isSelected ? '#ffffff22' : '#ffffff11'};
+  background: ${(props: { isSelected?: boolean; lastItem: boolean }) =>
+    props.isSelected ? "#ffffff22" : "#ffffff11"};
   :hover {
   :hover {
     background: #ffffff22;
     background: #ffffff22;
 
 
@@ -204,10 +211,13 @@ const Item = styled.div`
 `;
 `;
 
 
 const FileItem = styled(Item)`
 const FileItem = styled(Item)`
-  cursor: ${(props: {isADocker?: boolean}) => props.isADocker ? 'pointer' : 'default'};
-  color: ${(props: {isADocker?: boolean}) => props.isADocker ? '#fff' : '#ffffff55'};
+  cursor: ${(props: { isADocker?: boolean }) =>
+    props.isADocker ? "pointer" : "default"};
+  color: ${(props: { isADocker?: boolean }) =>
+    props.isADocker ? "#fff" : "#ffffff55"};
   :hover {
   :hover {
-    background: ${(props: {isADocker?: boolean}) => props.isADocker ? '#ffffff22' : '#ffffff11'};
+    background: ${(props: { isADocker?: boolean }) =>
+      props.isADocker ? "#ffffff22" : "#ffffff11"};
   }
   }
 `;
 `;
 
 
@@ -219,4 +229,4 @@ const LoadingWrapper = styled.div`
   align-items: center;
   align-items: center;
   justify-content: center;
   justify-content: center;
   color: #ffffff44;
   color: #ffffff44;
-`;
+`;

+ 43 - 39
dashboard/src/components/repo-selector/NewGHAction.tsx

@@ -1,84 +1,88 @@
-import React, { Component } from 'react';
-import styled from 'styled-components';
+import React, { Component } from "react";
+import styled from "styled-components";
 
 
-import { ChartType } from 'shared/types';
-import api from 'shared/api';
-import { Context } from 'shared/Context';
-import InputRow from 'components/values-form/InputRow';
+import { ChartType } from "shared/types";
+import { Context } from "shared/Context";
+import InputRow from "components/values-form/InputRow";
 
 
-import Loading from '../Loading';
+import Loading from "../Loading";
 
 
 type PropsType = {
 type PropsType = {
-  repoName: string,
-  dockerPath: string,
-  grid: number,
-  chart: ChartType,
-  imgURL: string,
-  setURL: (x: string) => void,
+  repoName: string;
+  dockerPath: string;
+  grid: number;
+  chart: ChartType;
+  imgURL: string;
+  setURL: (x: string) => void;
 };
 };
 
 
 type StateType = {
 type StateType = {
-  trueDockerPath: string,
-  loading: boolean,
-  error: boolean,
+  trueDockerPath: string;
+  loading: boolean;
+  error: boolean;
 };
 };
 
 
 export default class NewGHAction extends Component<PropsType, StateType> {
 export default class NewGHAction extends Component<PropsType, StateType> {
   state = {
   state = {
-    dockerRepo: '',
+    dockerRepo: "",
     trueDockerPath: this.props.dockerPath,
     trueDockerPath: this.props.dockerPath,
     loading: false,
     loading: false,
     error: false,
     error: false,
-  }
+  };
 
 
   componentDidMount() {
   componentDidMount() {
-    if (this.props.dockerPath[0] === '/') {
-      this.setState({ trueDockerPath: this.props.dockerPath.substring(1, this.props.dockerPath.length) });
+    if (this.props.dockerPath[0] === "/") {
+      this.setState({
+        trueDockerPath: this.props.dockerPath.substring(
+          1,
+          this.props.dockerPath.length
+        ),
+      });
     }
     }
   }
   }
 
 
   renderConfirmation = () => {
   renderConfirmation = () => {
     let { loading } = this.state;
     let { loading } = this.state;
     if (loading) {
     if (loading) {
-      return <LoadingWrapper><Loading /></LoadingWrapper>
+      return (
+        <LoadingWrapper>
+          <Loading />
+        </LoadingWrapper>
+      );
     }
     }
 
 
     return (
     return (
       <Holder>
       <Holder>
         <InputRow
         <InputRow
           disabled={true}
           disabled={true}
-          label='Git Repository'
-          type='text'
-          width='100%'
+          label="Git Repository"
+          type="text"
+          width="100%"
           value={this.props.repoName}
           value={this.props.repoName}
           setValue={(x: string) => console.log(x)}
           setValue={(x: string) => console.log(x)}
         />
         />
         <InputRow
         <InputRow
           disabled={true}
           disabled={true}
-          label='Dockerfile Path'
-          type='text'
-          width='100%'
+          label="Dockerfile Path"
+          type="text"
+          width="100%"
           value={this.state.trueDockerPath}
           value={this.state.trueDockerPath}
           setValue={(x: string) => console.log(x)}
           setValue={(x: string) => console.log(x)}
         />
         />
         <InputRow
         <InputRow
-          label='Docker Image Repository'
-          placeholder='Image Repo URL (ex. gcr.io/porter/mr-p)'
-          type='text'
-          width='100%'
+          label="Docker Image Repository"
+          placeholder="Image Repo URL (ex. gcr.io/porter/mr-p)"
+          type="text"
+          width="100%"
           value={this.props.imgURL}
           value={this.props.imgURL}
           setValue={(x: string) => this.props.setURL(x)}
           setValue={(x: string) => this.props.setURL(x)}
         />
         />
       </Holder>
       </Holder>
-    )
-  }
+    );
+  };
 
 
   render() {
   render() {
-    return (
-      <div>
-        {this.renderConfirmation()}
-      </div>
-    );
+    return <div>{this.renderConfirmation()}</div>;
   }
   }
 }
 }
 
 
@@ -96,4 +100,4 @@ const LoadingWrapper = styled.div`
   justify-content: center;
   justify-content: center;
   font-size: 13px;
   font-size: 13px;
   color: #ffffff44;
   color: #ffffff44;
-`;
+`;

+ 141 - 114
dashboard/src/components/repo-selector/RepoSelector.tsx

@@ -1,36 +1,36 @@
-import React, { Component } from 'react';
-import styled from 'styled-components';
-import github from 'assets/github.png';
-import info from 'assets/info.svg';
+import React, { Component } from "react";
+import styled from "styled-components";
+import github from "assets/github.png";
+import info from "assets/info.svg";
 
 
-import api from 'shared/api';
-import { RepoType, ChartType } from 'shared/types';
-import { Context } from 'shared/Context';
+import api from "shared/api";
+import { RepoType, ChartType } from "shared/types";
+import { Context } from "shared/Context";
 
 
-import Loading from 'components/Loading';
-import BranchList from './BranchList';
-import ContentsList from './ContentsList';
-import NewGHAction from './NewGHAction';
+import Loading from "components/Loading";
+import BranchList from "./BranchList";
+import ContentsList from "./ContentsList";
+import NewGHAction from "./NewGHAction";
 
 
 type PropsType = {
 type PropsType = {
-  chart: ChartType | null,
-  forceExpanded?: boolean,
-  selectedRepo: RepoType | null,
-  selectedBranch: string,
-  subdirectory: string,
-  setSelectedRepo: (x: RepoType) => void,
-  setSelectedBranch: (x: string) => void,
-  setSubdirectory: (x: string) => void
+  chart: ChartType | null;
+  forceExpanded?: boolean;
+  selectedRepo: RepoType | null;
+  selectedBranch: string;
+  subdirectory: string;
+  setSelectedRepo: (x: RepoType) => void;
+  setSelectedBranch: (x: string) => void;
+  setSubdirectory: (x: string) => void;
 };
 };
 
 
 type StateType = {
 type StateType = {
-  isExpanded: boolean,
-  loading: boolean,
-  error: boolean,
-  repos: RepoType[]
-  branchGrID: number,
-  dockerfileSelected: boolean,
-  imageURL: string,
+  isExpanded: boolean;
+  loading: boolean;
+  error: boolean;
+  repos: RepoType[];
+  branchGrID: number;
+  dockerfileSelected: boolean;
+  imageURL: string;
 };
 };
 
 
 export default class RepoSelector extends Component<PropsType, StateType> {
 export default class RepoSelector extends Component<PropsType, StateType> {
@@ -42,73 +42,95 @@ export default class RepoSelector extends Component<PropsType, StateType> {
     branchGrID: null as number,
     branchGrID: null as number,
     dockerfileSelected: false,
     dockerfileSelected: false,
     imageURL: null as string,
     imageURL: null as string,
-  }
+  };
 
 
   componentDidMount() {
   componentDidMount() {
     let { currentProject } = this.context;
     let { currentProject } = this.context;
 
 
     // Get repos
     // Get repos
-    api.getGitRepos('<token>', {
-    }, { project_id: currentProject.id }, (err: any, res: any) => {
-      if (err) {
-        this.setState({ loading: false, error: true });
-      } else {
-        var allRepos: any = [];
-        let counter = 0;
-        for (let i = 0; i < res.data.length; i++) {
-          var grid = res.data[i].id;
-          api.getGitRepoList('<token>', {}, { project_id: currentProject.id, git_repo_id: grid }, (err: any, res: any) => {
-            if (err) {
-              console.log(err);
-              this.setState({ loading: false, error: true });
-            } else {
-              res.data.forEach((repo: any, id: number) => {
-                repo.GHRepoID = grid;
-              })
-              allRepos = allRepos.concat(res.data);
-              this.setState({ repos: allRepos, loading: false, error: false });
-            }
-          })
+    api.getGitRepos(
+      "<token>",
+      {},
+      { project_id: currentProject.id },
+      (err: any, res: any) => {
+        if (err) {
+          this.setState({ loading: false, error: true });
+        } else {
+          var allRepos: any = [];
+          let counter = 0;
+          for (let i = 0; i < res.data.length; i++) {
+            var grid = res.data[i].id;
+            api.getGitRepoList(
+              "<token>",
+              {},
+              { project_id: currentProject.id, git_repo_id: grid },
+              (err: any, res: any) => {
+                if (err) {
+                  console.log(err);
+                  this.setState({ loading: false, error: true });
+                } else {
+                  res.data.forEach((repo: any, id: number) => {
+                    repo.GHRepoID = grid;
+                  });
+                  allRepos = allRepos.concat(res.data);
+                  this.setState({
+                    repos: allRepos,
+                    loading: false,
+                    error: false,
+                  });
+                }
+              }
+            );
+          }
         }
         }
       }
       }
-    });
+    );
   }
   }
 
 
   createGHAction = () => {
   createGHAction = () => {
     let { currentProject, currentCluster } = this.context;
     let { currentProject, currentCluster } = this.context;
-    let path = this.props.subdirectory + '/Dockerfile';
-    if (path[0] === '/') {
+    let path = this.props.subdirectory + "/Dockerfile";
+    if (path[0] === "/") {
       path = path.substring(1, path.length);
       path = path.substring(1, path.length);
     }
     }
 
 
-    api.createGHAction('<token>', {
-      git_repo: this.props.selectedRepo.FullName,
-      image_repo_uri: this.state.imageURL,
-      dockerfile_path: path,
-      git_repo_id: this.props.selectedRepo.GHRepoID,
-    }, {
-      project_id: currentProject.id,
-      CLUSTER_ID: currentCluster.id,
-      RELEASE_NAME: this.props.chart.name,
-      RELEASE_NAMESPACE: this.props.chart.namespace,
-    }, (err: any, res: any) => {
-      if (err) {
-        console.log(err);
-        this.setState({ error: true });
-      } else {
-        console.log(res.data);
+    api.createGHAction(
+      "<token>",
+      {
+        git_repo: this.props.selectedRepo.FullName,
+        image_repo_uri: this.state.imageURL,
+        dockerfile_path: path,
+        git_repo_id: this.props.selectedRepo.GHRepoID,
+      },
+      {
+        project_id: currentProject.id,
+        CLUSTER_ID: currentCluster.id,
+        RELEASE_NAME: this.props.chart.name,
+        RELEASE_NAMESPACE: this.props.chart.namespace,
+      },
+      (err: any, res: any) => {
+        if (err) {
+          console.log(err);
+          this.setState({ error: true });
+        } else {
+          console.log(res.data);
+        }
       }
       }
-    });
-  }
+    );
+  };
 
 
   renderRepoList = () => {
   renderRepoList = () => {
     let { repos, loading, error } = this.state;
     let { repos, loading, error } = this.state;
     if (loading) {
     if (loading) {
-      return <LoadingWrapper><Loading /></LoadingWrapper>
+      return (
+        <LoadingWrapper>
+          <Loading />
+        </LoadingWrapper>
+      );
     } else if (error || !repos) {
     } else if (error || !repos) {
-      return <LoadingWrapper>Error loading repos.</LoadingWrapper>
+      return <LoadingWrapper>Error loading repos.</LoadingWrapper>;
     } else if (repos.length == 0) {
     } else if (repos.length == 0) {
-      return <LoadingWrapper>No connected repos found.</LoadingWrapper>
+      return <LoadingWrapper>No connected repos found.</LoadingWrapper>;
     }
     }
 
 
     return repos.map((repo: RepoType, i: number) => {
     return repos.map((repo: RepoType, i: number) => {
@@ -119,11 +141,12 @@ export default class RepoSelector extends Component<PropsType, StateType> {
           lastItem={i === repos.length - 1}
           lastItem={i === repos.length - 1}
           onClick={() => this.props.setSelectedRepo(repo)}
           onClick={() => this.props.setSelectedRepo(repo)}
         >
         >
-          <img src={github} />{repo.FullName}
+          <img src={github} />
+          {repo.FullName}
         </RepoName>
         </RepoName>
       );
       );
     });
     });
-  }
+  };
 
 
   renderExpanded = () => {
   renderExpanded = () => {
     let {
     let {
@@ -132,16 +155,12 @@ export default class RepoSelector extends Component<PropsType, StateType> {
       subdirectory,
       subdirectory,
       setSelectedRepo,
       setSelectedRepo,
       setSelectedBranch,
       setSelectedBranch,
-      setSubdirectory
+      setSubdirectory,
     } = this.props;
     } = this.props;
 
 
     if (!selectedRepo) {
     if (!selectedRepo) {
-      return (
-        <ExpandedWrapper>
-          {this.renderRepoList()}
-        </ExpandedWrapper>
-      );
-    } else if (selectedBranch === '') {
+      return <ExpandedWrapper>{this.renderRepoList()}</ExpandedWrapper>;
+    } else if (selectedBranch === "") {
       return (
       return (
         <div>
         <div>
           <ExpandedWrapperAlt>
           <ExpandedWrapperAlt>
@@ -151,16 +170,13 @@ export default class RepoSelector extends Component<PropsType, StateType> {
                 this.setState({ branchGrID: selectedRepo.GHRepoID });
                 this.setState({ branchGrID: selectedRepo.GHRepoID });
                 setSelectedBranch(branch);
                 setSelectedBranch(branch);
               }}
               }}
-              repoName={selectedRepo.FullName.split('/')[1]}
-              owner={selectedRepo.FullName.split('/')[0]}
+              repoName={selectedRepo.FullName.split("/")[1]}
+              owner={selectedRepo.FullName.split("/")[0]}
               selectedBranch={selectedBranch}
               selectedBranch={selectedBranch}
             />
             />
           </ExpandedWrapperAlt>
           </ExpandedWrapperAlt>
           <ButtonTray>
           <ButtonTray>
-            <BackButton
-              width='130px'
-              onClick={() => setSelectedRepo(null)}
-            >
+            <BackButton width="130px" onClick={() => setSelectedRepo(null)}>
               <i className="material-icons">keyboard_backspace</i>
               <i className="material-icons">keyboard_backspace</i>
               Select Repo
               Select Repo
             </BackButton>
             </BackButton>
@@ -173,7 +189,7 @@ export default class RepoSelector extends Component<PropsType, StateType> {
           <ExpandedWrapperAlt>
           <ExpandedWrapperAlt>
             <NewGHAction
             <NewGHAction
               repoName={selectedRepo.FullName}
               repoName={selectedRepo.FullName}
-              dockerPath={subdirectory + '/Dockerfile'}
+              dockerPath={subdirectory + "/Dockerfile"}
               grid={this.state.branchGrID}
               grid={this.state.branchGrID}
               chart={this.props.chart}
               chart={this.props.chart}
               imgURL={this.state.imageURL}
               imgURL={this.state.imageURL}
@@ -182,31 +198,30 @@ export default class RepoSelector extends Component<PropsType, StateType> {
           </ExpandedWrapperAlt>
           </ExpandedWrapperAlt>
           <ButtonTray>
           <ButtonTray>
             <BackButton
             <BackButton
-              width='130px'
+              width="130px"
               onClick={() => this.setState({ dockerfileSelected: false })}
               onClick={() => this.setState({ dockerfileSelected: false })}
             >
             >
-              <i className='material-icons'>keyboard_backspace</i>
+              <i className="material-icons">keyboard_backspace</i>
               Select Dockerfile
               Select Dockerfile
             </BackButton>
             </BackButton>
-            <BackButton
-              width='146px'
-              onClick={() => this.createGHAction()}
-            >
-              <i className='material-icons'>play_circle_outline</i>
+            <BackButton width="146px" onClick={() => this.createGHAction()}>
+              <i className="material-icons">play_circle_outline</i>
               Create Github Action
               Create Github Action
             </BackButton>
             </BackButton>
           </ButtonTray>
           </ButtonTray>
         </div>
         </div>
-      )
+      );
     }
     }
     return (
     return (
       <div>
       <div>
         <ExpandedWrapperAlt>
         <ExpandedWrapperAlt>
           <ContentsList
           <ContentsList
             grid={this.state.branchGrID}
             grid={this.state.branchGrID}
-            setSubdirectory={(subdirectory: string) => setSubdirectory(subdirectory)}
-            repoName={selectedRepo.FullName.split('/')[1]}
-            owner={selectedRepo.FullName.split('/')[0]}
+            setSubdirectory={(subdirectory: string) =>
+              setSubdirectory(subdirectory)
+            }
+            repoName={selectedRepo.FullName.split("/")[1]}
+            owner={selectedRepo.FullName.split("/")[0]}
             selectedBranch={selectedBranch}
             selectedBranch={selectedBranch}
             subdirectory={subdirectory}
             subdirectory={subdirectory}
             setDockerfile={() => this.setState({ dockerfileSelected: true })}
             setDockerfile={() => this.setState({ dockerfileSelected: true })}
@@ -214,8 +229,12 @@ export default class RepoSelector extends Component<PropsType, StateType> {
         </ExpandedWrapperAlt>
         </ExpandedWrapperAlt>
         <ButtonTray>
         <ButtonTray>
           <BackButton
           <BackButton
-            onClick={() => {setSelectedBranch(''); setSubdirectory(''); this.setState({ imageURL: '' })}}
-            width='140px'
+            onClick={() => {
+              setSelectedBranch("");
+              setSubdirectory("");
+              this.setState({ imageURL: "" });
+            }}
+            width="140px"
           >
           >
             <i className="material-icons">keyboard_backspace</i>
             <i className="material-icons">keyboard_backspace</i>
             Select Branch
             Select Branch
@@ -223,18 +242,18 @@ export default class RepoSelector extends Component<PropsType, StateType> {
         </ButtonTray>
         </ButtonTray>
       </div>
       </div>
     );
     );
-  }
+  };
 
 
   renderSelected = () => {
   renderSelected = () => {
     let { selectedRepo, subdirectory, selectedBranch } = this.props;
     let { selectedRepo, subdirectory, selectedBranch } = this.props;
     if (selectedRepo) {
     if (selectedRepo) {
-      let subdir = subdirectory === '' ? '' : '/' + subdirectory;
+      let subdir = subdirectory === "" ? "" : "/" + subdirectory;
       return (
       return (
         <RepoLabel>
         <RepoLabel>
           <img src={github} />
           <img src={github} />
           {selectedRepo.FullName + subdir}
           {selectedRepo.FullName + subdir}
           <SelectedBranch>
           <SelectedBranch>
-            {!selectedBranch ? '(Select Branch)' : selectedBranch}
+            {!selectedBranch ? "(Select Branch)" : selectedBranch}
           </SelectedBranch>
           </SelectedBranch>
         </RepoLabel>
         </RepoLabel>
       );
       );
@@ -245,13 +264,13 @@ export default class RepoSelector extends Component<PropsType, StateType> {
         No source selected
         No source selected
       </RepoLabel>
       </RepoLabel>
     );
     );
-  }
+  };
 
 
   handleClick = () => {
   handleClick = () => {
     if (!this.props.forceExpanded) {
     if (!this.props.forceExpanded) {
       this.setState({ isExpanded: !this.state.isExpanded });
       this.setState({ isExpanded: !this.state.isExpanded });
     }
     }
-  }
+  };
 
 
   render() {
   render() {
     return (
     return (
@@ -262,7 +281,11 @@ export default class RepoSelector extends Component<PropsType, StateType> {
           forceExpanded={this.props.forceExpanded}
           forceExpanded={this.props.forceExpanded}
         >
         >
           {this.renderSelected()}
           {this.renderSelected()}
-          {this.props.forceExpanded ? null : <i className="material-icons">{this.state.isExpanded ? 'close' : 'build'}</i>}
+          {this.props.forceExpanded ? null : (
+            <i className="material-icons">
+              {this.state.isExpanded ? "close" : "build"}
+            </i>
+          )}
         </StyledRepoSelector>
         </StyledRepoSelector>
 
 
         {this.state.isExpanded ? this.renderExpanded() : null}
         {this.state.isExpanded ? this.renderExpanded() : null}
@@ -314,13 +337,16 @@ const RepoName = styled.div`
   display: flex;
   display: flex;
   width: 100%;
   width: 100%;
   font-size: 13px;
   font-size: 13px;
-  border-bottom: 1px solid ${(props: { lastItem: boolean, isSelected: boolean }) => props.lastItem ? '#00000000' : '#606166'};
+  border-bottom: 1px solid
+    ${(props: { lastItem: boolean; isSelected: boolean }) =>
+      props.lastItem ? "#00000000" : "#606166"};
   color: #ffffff;
   color: #ffffff;
   user-select: none;
   user-select: none;
   align-items: center;
   align-items: center;
   padding: 10px 0px;
   padding: 10px 0px;
   cursor: pointer;
   cursor: pointer;
-  background: ${(props: { isSelected: boolean, lastItem: boolean }) => props.isSelected ? '#ffffff22' : '#ffffff11'};
+  background: ${(props: { isSelected: boolean; lastItem: boolean }) =>
+    props.isSelected ? "#ffffff22" : "#ffffff11"};
   :hover {
   :hover {
     background: #ffffff22;
     background: #ffffff22;
 
 
@@ -356,8 +382,7 @@ const ExpandedWrapper = styled.div`
   overflow-y: auto;
   overflow-y: auto;
 `;
 `;
 
 
-const ExpandedWrapperAlt = styled(ExpandedWrapper)`
-`;
+const ExpandedWrapperAlt = styled(ExpandedWrapper)``;
 
 
 const RepoLabel = styled.div`
 const RepoLabel = styled.div`
   display: flex;
   display: flex;
@@ -375,7 +400,8 @@ const StyledRepoSelector = styled.div`
   width: 100%;
   width: 100%;
   margin-top: 22px;
   margin-top: 22px;
   border: 1px solid #ffffff55;
   border: 1px solid #ffffff55;
-  background: ${(props: { isExpanded: boolean, forceExpanded: boolean }) => props.isExpanded ? '#ffffff11' : ''};
+  background: ${(props: { isExpanded: boolean; forceExpanded: boolean }) =>
+    props.isExpanded ? "#ffffff11" : ""};
   border-radius: 3px;
   border-radius: 3px;
   user-select: none;
   user-select: none;
   height: 40px;
   height: 40px;
@@ -384,7 +410,8 @@ const StyledRepoSelector = styled.div`
   display: flex;
   display: flex;
   align-items: center;
   align-items: center;
   justify-content: space-between;
   justify-content: space-between;
-  cursor: ${(props: { isExpanded: boolean, forceExpanded: boolean }) => props.forceExpanded ? '' : 'pointer'};
+  cursor: ${(props: { isExpanded: boolean; forceExpanded: boolean }) =>
+    props.forceExpanded ? "" : "pointer"};
   :hover {
   :hover {
     background: #ffffff11;
     background: #ffffff11;
 
 
@@ -403,4 +430,4 @@ const StyledRepoSelector = styled.div`
     border-radius: 20px;
     border-radius: 20px;
     padding: 4px;
     padding: 4px;
   }
   }
-`;
+`;

+ 27 - 22
dashboard/src/components/values-form/Base64InputRow.tsx

@@ -1,41 +1,44 @@
-import React, { ChangeEvent, Component } from 'react';
-import styled from 'styled-components';
+import React, { ChangeEvent, Component } from "react";
+import styled from "styled-components";
 
 
 type PropsType = {
 type PropsType = {
-  label?: string,
-  type: string,
-  value: string | number,
-  setValue: (x: string | number) => void,
-  unit?: string,
-  placeholder?: string,
-  width?: string,
-  disabled?: boolean,
-  isRequired?: boolean,
+  label?: string;
+  type: string;
+  value: string | number;
+  setValue: (x: string | number) => void;
+  unit?: string;
+  placeholder?: string;
+  width?: string;
+  disabled?: boolean;
+  isRequired?: boolean;
 };
 };
 
 
 type StateType = {
 type StateType = {
-  readOnly: boolean
+  readOnly: boolean;
 };
 };
 
 
 export default class InputRow extends Component<PropsType, StateType> {
 export default class InputRow extends Component<PropsType, StateType> {
   state = {
   state = {
-    readOnly: true
-  }
+    readOnly: true,
+  };
 
 
   handleChange = (e: ChangeEvent<HTMLInputElement>) => {
   handleChange = (e: ChangeEvent<HTMLInputElement>) => {
     this.props.setValue(e.target.value);
     this.props.setValue(e.target.value);
-  }
-  
+  };
+
   render() {
   render() {
     let { label, value, type, unit, placeholder, width } = this.props;
     let { label, value, type, unit, placeholder, width } = this.props;
     value = value.toString();
     value = value.toString();
     value = atob(value);
     value = atob(value);
     return (
     return (
       <StyledInputRow>
       <StyledInputRow>
-        <Label>{label} <Required>{this.props.isRequired ? ' *' : null}</Required></Label>
+        <Label>
+          {label} <Required>{this.props.isRequired ? " *" : null}</Required>
+        </Label>
         <InputWrapper>
         <InputWrapper>
           <Input
           <Input
-            readOnly={this.state.readOnly} onFocus={() => this.setState({ readOnly: false })}
+            readOnly={this.state.readOnly}
+            onFocus={() => this.setState({ readOnly: false })}
             disabled={this.props.disabled}
             disabled={this.props.disabled}
             placeholder={placeholder}
             placeholder={placeholder}
             width={width}
             width={width}
@@ -72,8 +75,10 @@ const Input = styled.input`
   background: #ffffff11;
   background: #ffffff11;
   border: 1px solid #ffffff55;
   border: 1px solid #ffffff55;
   border-radius: 3px;
   border-radius: 3px;
-  width: ${(props: { disabled: boolean, width: string }) => props.width ? props.width : '270px'};
-  color: ${(props: { disabled: boolean, width: string }) => props.disabled ? '#ffffff44' : 'white'};
+  width: ${(props: { disabled: boolean; width: string }) =>
+    props.width ? props.width : "270px"};
+  color: ${(props: { disabled: boolean; width: string }) =>
+    props.disabled ? "#ffffff44" : "white"};
   padding: 5px 10px;
   padding: 5px 10px;
   margin-right: 8px;
   margin-right: 8px;
   height: 30px;
   height: 30px;
@@ -85,10 +90,10 @@ const Label = styled.div`
   display: flex;
   display: flex;
   align-items: center;
   align-items: center;
   font-size: 13px;
   font-size: 13px;
-  font-family: 'Work Sans', sans-serif;
+  font-family: "Work Sans", sans-serif;
 `;
 `;
 
 
 const StyledInputRow = styled.div`
 const StyledInputRow = styled.div`
   margin-bottom: 15px;
   margin-bottom: 15px;
   margin-top: 20px;
   margin-top: 20px;
-`;
+`;

+ 18 - 19
dashboard/src/components/values-form/CheckboxList.tsx

@@ -1,17 +1,15 @@
-import React from 'react';
-import styled from 'styled-components';
+import React from "react";
+import styled from "styled-components";
 
 
 type PropsType = {
 type PropsType = {
-  label?: string,
-  options: { disabled?: boolean, value: string, label: string }[],
-  selected: { value: string, label: string }[],
-  setSelected: (x: { value: string, label: string }[]) => void,
+  label?: string;
+  options: { disabled?: boolean; value: string; label: string }[];
+  selected: { value: string; label: string }[];
+  setSelected: (x: { value: string; label: string }[]) => void;
 };
 };
 
 
-const CheckboxList = ({ 
-  label, options, selected, setSelected,
-}: PropsType) => {
-  let onSelectOption = (option: { value: string, label: string }) => {
+const CheckboxList = ({ label, options, selected, setSelected }: PropsType) => {
+  let onSelectOption = (option: { value: string; label: string }) => {
     if (!selected.includes(option)) {
     if (!selected.includes(option)) {
       selected.push(option);
       selected.push(option);
       setSelected(selected);
       setSelected(selected);
@@ -19,14 +17,14 @@ const CheckboxList = ({
       selected.splice(selected.indexOf(option), 1);
       selected.splice(selected.indexOf(option), 1);
       setSelected(selected);
       setSelected(selected);
     }
     }
-  }
-  
+  };
+
   return (
   return (
     <StyledCheckboxList>
     <StyledCheckboxList>
       {label && <Label>{label}</Label>}
       {label && <Label>{label}</Label>}
-      {options.map((option: { value: string, label: string }, i: number) => {
+      {options.map((option: { value: string; label: string }, i: number) => {
         return (
         return (
-          <CheckboxOption 
+          <CheckboxOption
             isLast={i === options.length - 1}
             isLast={i === options.length - 1}
             onClick={() => onSelectOption(option)}
             onClick={() => onSelectOption(option)}
             key={i}
             key={i}
@@ -40,7 +38,7 @@ const CheckboxList = ({
       })}
       })}
     </StyledCheckboxList>
     </StyledCheckboxList>
   );
   );
-}
+};
 export default CheckboxList;
 export default CheckboxList;
 
 
 const Checkbox = styled.div`
 const Checkbox = styled.div`
@@ -49,7 +47,8 @@ const Checkbox = styled.div`
   border: 1px solid #ffffff55;
   border: 1px solid #ffffff55;
   margin: 1px 15px 0px 1px;
   margin: 1px 15px 0px 1px;
   border-radius: 3px;
   border-radius: 3px;
-  background: ${(props: { checked: boolean }) => props.checked ? '#ffffff22' : '#ffffff11'};
+  background: ${(props: { checked: boolean }) =>
+    props.checked ? "#ffffff22" : "#ffffff11"};
   display: flex;
   display: flex;
   align-items: center;
   align-items: center;
   justify-content: center;
   justify-content: center;
@@ -57,7 +56,7 @@ const Checkbox = styled.div`
   > i {
   > i {
     font-size: 12px;
     font-size: 12px;
     padding-left: 0px;
     padding-left: 0px;
-    display: ${(props: { checked: boolean }) => props.checked ? '' : 'none'};
+    display: ${(props: { checked: boolean }) => (props.checked ? "" : "none")};
   }
   }
 `;
 `;
 
 
@@ -68,7 +67,7 @@ const CheckboxOption = styled.div<{ isLast: boolean }>`
   display: flex;
   display: flex;
   cursor: pointer;
   cursor: pointer;
   align-items: center;
   align-items: center;
-  border-bottom: ${props => props.isLast ? '' : '1px solid #ffffff22'};
+  border-bottom: ${(props) => (props.isLast ? "" : "1px solid #ffffff22")};
   font-size: 13px;
   font-size: 13px;
 
 
   :hover {
   :hover {
@@ -88,4 +87,4 @@ const StyledCheckboxList = styled.div`
   background: #ffffff11;
   background: #ffffff11;
   margin-bottom: 15px;
   margin-bottom: 15px;
   margin-top: 20px;
   margin-top: 20px;
-`;
+`;

+ 11 - 11
dashboard/src/components/values-form/CheckboxRow.tsx

@@ -1,14 +1,13 @@
-import React, { Component } from 'react';
-import styled from 'styled-components';
+import React, { Component } from "react";
+import styled from "styled-components";
 
 
 type PropsType = {
 type PropsType = {
-  label: string,
-  checked: boolean,
-  toggle: () => void
+  label: string;
+  checked: boolean;
+  toggle: () => void;
 };
 };
 
 
-type StateType = {
-};
+type StateType = {};
 
 
 export default class CheckboxRow extends Component<PropsType, StateType> {
 export default class CheckboxRow extends Component<PropsType, StateType> {
   render() {
   render() {
@@ -24,7 +23,7 @@ export default class CheckboxRow extends Component<PropsType, StateType> {
     );
     );
   }
   }
 }
 }
-        
+
 const CheckboxWrapper = styled.div`
 const CheckboxWrapper = styled.div`
   display: flex;
   display: flex;
   align-items: center;
   align-items: center;
@@ -42,7 +41,8 @@ const Checkbox = styled.div`
   border: 1px solid #ffffff55;
   border: 1px solid #ffffff55;
   margin: 1px 10px 0px 1px;
   margin: 1px 10px 0px 1px;
   border-radius: 3px;
   border-radius: 3px;
-  background: ${(props: { checked: boolean }) => props.checked ? '#ffffff22' : '#ffffff11'};
+  background: ${(props: { checked: boolean }) =>
+    props.checked ? "#ffffff22" : "#ffffff11"};
   display: flex;
   display: flex;
   align-items: center;
   align-items: center;
   justify-content: center;
   justify-content: center;
@@ -50,7 +50,7 @@ const Checkbox = styled.div`
   > i {
   > i {
     font-size: 12px;
     font-size: 12px;
     padding-left: 0px;
     padding-left: 0px;
-    display: ${(props: { checked: boolean }) => props.checked ? '' : 'none'};
+    display: ${(props: { checked: boolean }) => (props.checked ? "" : "none")};
   }
   }
 `;
 `;
 
 
@@ -59,4 +59,4 @@ const StyledCheckboxRow = styled.div`
   align-items: center;
   align-items: center;
   margin-bottom: 15px;
   margin-bottom: 15px;
   margin-top: 20px;
   margin-top: 20px;
-`;
+`;

+ 8 - 6
dashboard/src/components/values-form/Heading.tsx

@@ -1,16 +1,18 @@
-import React from 'react';  
-import styled from 'styled-components';
+import React from "react";
+import styled from "styled-components";
 
 
-export default function Heading(props: { isAtTop?: boolean, children: any }) {
-  return <StyledHeading isAtTop={props.isAtTop}>{props.children}</StyledHeading>;
+export default function Heading(props: { isAtTop?: boolean; children: any }) {
+  return (
+    <StyledHeading isAtTop={props.isAtTop}>{props.children}</StyledHeading>
+  );
 }
 }
 
 
 const StyledHeading = styled.div<{ isAtTop: boolean }>`
 const StyledHeading = styled.div<{ isAtTop: boolean }>`
   color: white;
   color: white;
   font-weight: 500;
   font-weight: 500;
   font-size: 16px;
   font-size: 16px;
-  margin-top: ${props => props.isAtTop ? '0': '30px'};
+  margin-top: ${(props) => (props.isAtTop ? "0" : "30px")};
   margin-bottom: 5px;
   margin-bottom: 5px;
   display: flex;
   display: flex;
   align-items: center;
   align-items: center;
-`;
+`;

+ 3 - 3
dashboard/src/components/values-form/Helper.tsx

@@ -1,5 +1,5 @@
-import React from 'react';  
-import styled from 'styled-components';
+import React from "react";
+import styled from "styled-components";
 
 
 export default function Helper(props: { children: any }) {
 export default function Helper(props: { children: any }) {
   return <StyledHelper>{props.children}</StyledHelper>;
   return <StyledHelper>{props.children}</StyledHelper>;
@@ -13,4 +13,4 @@ const StyledHelper = styled.div`
   margin-top: 20px;
   margin-top: 20px;
   display: flex;
   display: flex;
   align-items: center;
   align-items: center;
-`;
+`;

+ 28 - 23
dashboard/src/components/values-form/InputRow.tsx

@@ -1,43 +1,46 @@
-import React, { ChangeEvent, Component } from 'react';
-import styled from 'styled-components';
+import React, { ChangeEvent, Component } from "react";
+import styled from "styled-components";
 
 
 type PropsType = {
 type PropsType = {
-  label?: string,
-  type: string,
-  value: string | number,
-  setValue: (x: string | number) => void,
-  unit?: string,
-  placeholder?: string,
-  width?: string,
-  disabled?: boolean,
-  isRequired?: boolean,
+  label?: string;
+  type: string;
+  value: string | number;
+  setValue: (x: string | number) => void;
+  unit?: string;
+  placeholder?: string;
+  width?: string;
+  disabled?: boolean;
+  isRequired?: boolean;
 };
 };
 
 
 type StateType = {
 type StateType = {
-  readOnly: boolean
+  readOnly: boolean;
 };
 };
 
 
 export default class InputRow extends Component<PropsType, StateType> {
 export default class InputRow extends Component<PropsType, StateType> {
   state = {
   state = {
-    readOnly: true
-  }
+    readOnly: true,
+  };
 
 
   handleChange = (e: ChangeEvent<HTMLInputElement>) => {
   handleChange = (e: ChangeEvent<HTMLInputElement>) => {
-    if (this.props.type === 'number') {
+    if (this.props.type === "number") {
       this.props.setValue(parseInt(e.target.value));
       this.props.setValue(parseInt(e.target.value));
     } else {
     } else {
       this.props.setValue(e.target.value);
       this.props.setValue(e.target.value);
     }
     }
-  }
-  
+  };
+
   render() {
   render() {
     let { label, value, type, unit, placeholder, width } = this.props;
     let { label, value, type, unit, placeholder, width } = this.props;
     return (
     return (
       <StyledInputRow>
       <StyledInputRow>
-        <Label>{label} <Required>{this.props.isRequired ? ' *' : null}</Required></Label>
+        <Label>
+          {label} <Required>{this.props.isRequired ? " *" : null}</Required>
+        </Label>
         <InputWrapper>
         <InputWrapper>
           <Input
           <Input
-            readOnly={this.state.readOnly} onFocus={() => this.setState({ readOnly: false })}
+            readOnly={this.state.readOnly}
+            onFocus={() => this.setState({ readOnly: false })}
             disabled={this.props.disabled}
             disabled={this.props.disabled}
             placeholder={placeholder}
             placeholder={placeholder}
             width={width}
             width={width}
@@ -74,8 +77,10 @@ const Input = styled.input`
   background: #ffffff11;
   background: #ffffff11;
   border: 1px solid #ffffff55;
   border: 1px solid #ffffff55;
   border-radius: 3px;
   border-radius: 3px;
-  width: ${(props: { disabled: boolean, width: string }) => props.width ? props.width : '270px'};
-  color: ${(props: { disabled: boolean, width: string }) => props.disabled ? '#ffffff44' : 'white'};
+  width: ${(props: { disabled: boolean; width: string }) =>
+    props.width ? props.width : "270px"};
+  color: ${(props: { disabled: boolean; width: string }) =>
+    props.disabled ? "#ffffff44" : "white"};
   padding: 5px 10px;
   padding: 5px 10px;
   height: 35px;
   height: 35px;
 `;
 `;
@@ -86,10 +91,10 @@ const Label = styled.div`
   display: flex;
   display: flex;
   align-items: center;
   align-items: center;
   font-size: 13px;
   font-size: 13px;
-  font-family: 'Work Sans', sans-serif;
+  font-family: "Work Sans", sans-serif;
 `;
 `;
 
 
 const StyledInputRow = styled.div`
 const StyledInputRow = styled.div`
   margin-bottom: 15px;
   margin-bottom: 15px;
   margin-top: 20px;
   margin-top: 20px;
-`;
+`;

+ 9 - 14
dashboard/src/components/values-form/MultiSelect.tsx

@@ -1,32 +1,27 @@
-import React, { Component } from 'react';
-import styled from 'styled-components';
+import React, { Component } from "react";
+import styled from "styled-components";
 
 
-type PropsType = {
-};
+type PropsType = {};
 
 
 type StateType = {
 type StateType = {
-  options: { label: string, value: string }[],
+  options: { label: string; value: string }[];
 };
 };
 
 
 export default class MultiSelect extends Component<PropsType, StateType> {
 export default class MultiSelect extends Component<PropsType, StateType> {
   state = {
   state = {
-    options: [] as { label: string, value: string }[],
-  }
+    options: [] as { label: string; value: string }[],
+  };
 
 
-  renderOptions = () => {
-    
-  }
+  renderOptions = () => {};
 
 
   render() {
   render() {
     return (
     return (
       <>
       <>
-        <StyledMultiSelect>
-        </StyledMultiSelect>
+        <StyledMultiSelect></StyledMultiSelect>
         boilerplate
         boilerplate
       </>
       </>
     );
     );
   }
   }
 }
 }
 
 
-const StyledMultiSelect = styled.div`
-`;
+const StyledMultiSelect = styled.div``;

+ 14 - 16
dashboard/src/components/values-form/SelectRow.tsx

@@ -1,20 +1,19 @@
-import React, { Component } from 'react';
-import styled from 'styled-components';
+import React, { Component } from "react";
+import styled from "styled-components";
 
 
-import Selector from '../Selector';
+import Selector from "../Selector";
 
 
 type PropsType = {
 type PropsType = {
-  label: string,
-  value: string,
-  setActiveValue: (x: string) => void,
-  options: { value: string, label: string }[],
-  dropdownLabel?: string,
-  width?: string,
-  dropdownMaxHeight?: string,
+  label: string;
+  value: string;
+  setActiveValue: (x: string) => void;
+  options: { value: string; label: string }[];
+  dropdownLabel?: string;
+  width?: string;
+  dropdownMaxHeight?: string;
 };
 };
 
 
-type StateType = {
-};
+type StateType = {};
 
 
 export default class SelectRow extends Component<PropsType, StateType> {
 export default class SelectRow extends Component<PropsType, StateType> {
   render() {
   render() {
@@ -27,7 +26,7 @@ export default class SelectRow extends Component<PropsType, StateType> {
             setActiveValue={this.props.setActiveValue}
             setActiveValue={this.props.setActiveValue}
             options={this.props.options}
             options={this.props.options}
             dropdownLabel={this.props.dropdownLabel}
             dropdownLabel={this.props.dropdownLabel}
-            width={this.props.width || '270px'}
+            width={this.props.width || "270px"}
             dropdownWidth={this.props.width}
             dropdownWidth={this.props.width}
             dropdownMaxHeight={this.props.dropdownMaxHeight}
             dropdownMaxHeight={this.props.dropdownMaxHeight}
           />
           />
@@ -37,8 +36,7 @@ export default class SelectRow extends Component<PropsType, StateType> {
   }
   }
 }
 }
 
 
-const SelectWrapper = styled.div`
-`;
+const SelectWrapper = styled.div``;
 
 
 const Label = styled.div`
 const Label = styled.div`
   color: #ffffff;
   color: #ffffff;
@@ -48,4 +46,4 @@ const Label = styled.div`
 const StyledSelectRow = styled.div`
 const StyledSelectRow = styled.div`
   margin-bottom: 15px;
   margin-bottom: 15px;
   margin-top: 20px;
   margin-top: 20px;
-`;
+`;

+ 17 - 16
dashboard/src/components/values-form/TextArea.tsx

@@ -1,22 +1,21 @@
-import React, { ChangeEvent, Component } from 'react';
-import styled from 'styled-components';
+import React, { Component } from "react";
+import styled from "styled-components";
 
 
 type PropsType = {
 type PropsType = {
-  label?: string,
-  value: string,
-  setValue: (x: string) => void,
-  placeholder?: string
-  width?: string
-  disabled?: boolean
+  label?: string;
+  value: string;
+  setValue: (x: string) => void;
+  placeholder?: string;
+  width?: string;
+  disabled?: boolean;
 };
 };
 
 
-type StateType = {
-};
+type StateType = {};
 
 
 export default class TextArea extends Component<PropsType, StateType> {
 export default class TextArea extends Component<PropsType, StateType> {
   handleChange = (e: any) => {
   handleChange = (e: any) => {
     this.props.setValue(e.target.value);
     this.props.setValue(e.target.value);
-  }
+  };
 
 
   render() {
   render() {
     let { label, value, placeholder, width } = this.props;
     let { label, value, placeholder, width } = this.props;
@@ -27,7 +26,7 @@ export default class TextArea extends Component<PropsType, StateType> {
           disabled={this.props.disabled}
           disabled={this.props.disabled}
           placeholder={placeholder}
           placeholder={placeholder}
           width={width}
           width={width}
-          value={value || ''}
+          value={value || ""}
           onChange={this.handleChange}
           onChange={this.handleChange}
         />
         />
       </StyledTextArea>
       </StyledTextArea>
@@ -43,8 +42,10 @@ const InputArea = styled.textarea`
   background: #ffffff11;
   background: #ffffff11;
   border: 1px solid #ffffff55;
   border: 1px solid #ffffff55;
   border-radius: 3px;
   border-radius: 3px;
-  width: ${(props: { disabled: boolean, width: string }) => props.width ? props.width : '270px'};
-  color: ${(props: { disabled: boolean, width: string }) => props.disabled ? '#ffffff44' : 'white'};
+  width: ${(props: { disabled: boolean; width: string }) =>
+    props.width ? props.width : "270px"};
+  color: ${(props: { disabled: boolean; width: string }) =>
+    props.disabled ? "#ffffff44" : "white"};
   padding: 5px 10px;
   padding: 5px 10px;
   margin-right: 8px;
   margin-right: 8px;
   height: 8em;
   height: 8em;
@@ -55,10 +56,10 @@ const Label = styled.div`
   color: #ffffff;
   color: #ffffff;
   margin-bottom: 10px;
   margin-bottom: 10px;
   font-size: 13px;
   font-size: 13px;
-  font-family: 'Work Sans', sans-serif;
+  font-family: "Work Sans", sans-serif;
 `;
 `;
 
 
 const StyledTextArea = styled.div`
 const StyledTextArea = styled.div`
   margin-bottom: 15px;
   margin-bottom: 15px;
   margin-top: 20px;
   margin-top: 20px;
-`;
+`;

+ 62 - 72
dashboard/src/components/values-form/ValuesForm.tsx

@@ -1,24 +1,22 @@
-import React, { Component } from 'react';
-import styled from 'styled-components';
-import _ from 'lodash';
+import React, { Component } from "react";
+import styled from "styled-components";
 
 
-import { Section, FormElement } from 'shared/types';
-import { Context } from 'shared/Context';
-import api from 'shared/api';
+import { Section, FormElement } from "shared/types";
+import { Context } from "shared/Context";
 
 
-import CheckboxRow from './CheckboxRow';
-import InputRow from './InputRow';
-import Base64InputRow from './Base64InputRow';
-import SelectRow from './SelectRow';
-import Helper from './Helper';
-import Heading from './Heading';
-import ExpandableResource from '../ExpandableResource';
-import VeleroForm from '../forms/VeleroForm';
+import CheckboxRow from "./CheckboxRow";
+import InputRow from "./InputRow";
+import Base64InputRow from "./Base64InputRow";
+import SelectRow from "./SelectRow";
+import Helper from "./Helper";
+import Heading from "./Heading";
+import ExpandableResource from "../ExpandableResource";
+import VeleroForm from "../forms/VeleroForm";
 
 
 type PropsType = {
 type PropsType = {
-  sections?: Section[],
-  metaState?: any,
-  setMetaState?: any,
+  sections?: Section[];
+  metaState?: any;
+  setMetaState?: any;
 };
 };
 
 
 type StateType = any;
 type StateType = any;
@@ -28,55 +26,54 @@ export default class ValuesForm extends Component<PropsType, StateType> {
     let key = item.name || item.variable;
     let key = item.name || item.variable;
     let value = this.props.metaState[key];
     let value = this.props.metaState[key];
     if (item.settings && item.settings.unit && value) {
     if (item.settings && item.settings.unit && value) {
-      value = value.split(item.settings.unit)[0]
+      value = value.split(item.settings.unit)[0];
     }
     }
     return value;
     return value;
-  }
+  };
 
 
   renderSection = (section: Section) => {
   renderSection = (section: Section) => {
     return section.contents.map((item: FormElement, i: number) => {
     return section.contents.map((item: FormElement, i: number) => {
-
       // If no name is assigned use values.yaml variable as identifier
       // If no name is assigned use values.yaml variable as identifier
       let key = item.name || item.variable;
       let key = item.name || item.variable;
       switch (item.type) {
       switch (item.type) {
-        case 'heading':
+        case "heading":
           return <Heading key={i}>{item.label}</Heading>;
           return <Heading key={i}>{item.label}</Heading>;
-        case 'subtitle':
+        case "subtitle":
           return <Helper key={i}>{item.label}</Helper>;
           return <Helper key={i}>{item.label}</Helper>;
-        case 'resource-list':
+        case "resource-list":
           if (Array.isArray(item.value)) {
           if (Array.isArray(item.value)) {
             return (
             return (
               <ResourceList>
               <ResourceList>
-                {
-                  item.value.map((resource: any, i: number) => {
-                    return (
-                      <ExpandableResource
-                        key={i}
-                        resource={resource}
-                        isLast={i === item.value.length - 1}
-                        roundAllCorners={true}
-                      />
-                    );
-                  })
-                }
+                {item.value.map((resource: any, i: number) => {
+                  return (
+                    <ExpandableResource
+                      key={i}
+                      resource={resource}
+                      isLast={i === item.value.length - 1}
+                      roundAllCorners={true}
+                    />
+                  );
+                })}
               </ResourceList>
               </ResourceList>
             );
             );
           }
           }
-        case 'checkbox':
+        case "checkbox":
           return (
           return (
             <CheckboxRow
             <CheckboxRow
               key={i}
               key={i}
               checked={this.props.metaState[key]}
               checked={this.props.metaState[key]}
-              toggle={() => this.props.setMetaState({ [key]: !this.props.metaState[key] })}
+              toggle={() =>
+                this.props.setMetaState({ [key]: !this.props.metaState[key] })
+              }
               label={item.label}
               label={item.label}
             />
             />
           );
           );
-        case 'array-input':
+        case "array-input":
           return (
           return (
             <InputRow
             <InputRow
               key={i}
               key={i}
               isRequired={item.required}
               isRequired={item.required}
-              type='text'
+              type="text"
               value={this.getInputValue(item)}
               value={this.getInputValue(item)}
               setValue={(x: string) => {
               setValue={(x: string) => {
                 this.props.setMetaState({ [key]: [x] });
                 this.props.setMetaState({ [key]: [x] });
@@ -85,15 +82,15 @@ export default class ValuesForm extends Component<PropsType, StateType> {
               unit={item.settings ? item.settings.unit : null}
               unit={item.settings ? item.settings.unit : null}
             />
             />
           );
           );
-        case 'string-input':
+        case "string-input":
           return (
           return (
             <InputRow
             <InputRow
               key={i}
               key={i}
               isRequired={item.required}
               isRequired={item.required}
-              type='text'
+              type="text"
               value={this.getInputValue(item)}
               value={this.getInputValue(item)}
               setValue={(x: string) => {
               setValue={(x: string) => {
-                if (item.settings && item.settings.unit && x !== '') {
+                if (item.settings && item.settings.unit && x !== "") {
                   x = x + item.settings.unit;
                   x = x + item.settings.unit;
                 }
                 }
                 this.props.setMetaState({ [key]: x });
                 this.props.setMetaState({ [key]: x });
@@ -102,17 +99,17 @@ export default class ValuesForm extends Component<PropsType, StateType> {
               unit={item.settings ? item.settings.unit : null}
               unit={item.settings ? item.settings.unit : null}
             />
             />
           );
           );
-        case 'number-input':
+        case "number-input":
           return (
           return (
             <InputRow
             <InputRow
               key={i}
               key={i}
               isRequired={item.required}
               isRequired={item.required}
-              type='number'
+              type="number"
               value={this.getInputValue(item)}
               value={this.getInputValue(item)}
               setValue={(x: number) => {
               setValue={(x: number) => {
                 let val: string | number = x;
                 let val: string | number = x;
                 if (Number.isNaN(x)) {
                 if (Number.isNaN(x)) {
-                  val = ''
+                  val = "";
                 }
                 }
 
 
                 // Convert to string if unit is set
                 // Convert to string if unit is set
@@ -120,53 +117,50 @@ export default class ValuesForm extends Component<PropsType, StateType> {
                   val = x.toString();
                   val = x.toString();
                   val = val + item.settings.unit;
                   val = val + item.settings.unit;
                 }
                 }
-                
+
                 this.props.setMetaState({ [key]: val });
                 this.props.setMetaState({ [key]: val });
               }}
               }}
               label={item.label}
               label={item.label}
               unit={item.settings ? item.settings.unit : null}
               unit={item.settings ? item.settings.unit : null}
             />
             />
           );
           );
-        case 'select':
+        case "select":
           return (
           return (
             <SelectRow
             <SelectRow
               key={i}
               key={i}
               value={this.props.metaState[key]}
               value={this.props.metaState[key]}
               setActiveValue={(val) => this.props.setMetaState({ [key]: val })}
               setActiveValue={(val) => this.props.setMetaState({ [key]: val })}
               options={item.settings.options}
               options={item.settings.options}
-              dropdownLabel=''
+              dropdownLabel=""
               label={item.label}
               label={item.label}
             />
             />
           );
           );
-        case 'provider-select':
+        case "provider-select":
           return (
           return (
             <SelectRow
             <SelectRow
               key={i}
               key={i}
               value={this.props.metaState[key]}
               value={this.props.metaState[key]}
               setActiveValue={(val) => this.props.setMetaState({ [key]: val })}
               setActiveValue={(val) => this.props.setMetaState({ [key]: val })}
               options={[
               options={[
-                { value: 'gcp', label: 'Google Cloud Platform (GCP)' },
-                { value: 'aws', label: 'Amazon Web Services (AWS)' },
-                { value: 'do', label: 'DigitalOcean' },
+                { value: "gcp", label: "Google Cloud Platform (GCP)" },
+                { value: "aws", label: "Amazon Web Services (AWS)" },
+                { value: "do", label: "DigitalOcean" },
               ]}
               ]}
-              dropdownLabel=''
+              dropdownLabel=""
               label={item.label}
               label={item.label}
             />
             />
           );
           );
-        case 'velero-create-backup':
-          return (
-            <VeleroForm
-            />
-          );
-        case 'base-64':
+        case "velero-create-backup":
+          return <VeleroForm />;
+        case "base-64":
           return (
           return (
             <Base64InputRow
             <Base64InputRow
               key={i}
               key={i}
               isRequired={item.required}
               isRequired={item.required}
-              type='text'
+              type="text"
               value={this.getInputValue(item)}
               value={this.getInputValue(item)}
               setValue={(x: string) => {
               setValue={(x: string) => {
-                if (item.settings && item.settings.unit && x !== '') {
+                if (item.settings && item.settings.unit && x !== "") {
                   x = x + item.settings.unit;
                   x = x + item.settings.unit;
                 }
                 }
                 this.props.setMetaState({ [key]: btoa(x) });
                 this.props.setMetaState({ [key]: btoa(x) });
@@ -175,15 +169,15 @@ export default class ValuesForm extends Component<PropsType, StateType> {
               unit={item.settings ? item.settings.unit : null}
               unit={item.settings ? item.settings.unit : null}
             />
             />
           );
           );
-        case 'base-64-password':
+        case "base-64-password":
           return (
           return (
             <Base64InputRow
             <Base64InputRow
               key={i}
               key={i}
               isRequired={item.required}
               isRequired={item.required}
-              type='password'
+              type="password"
               value={this.getInputValue(item)}
               value={this.getInputValue(item)}
               setValue={(x: string) => {
               setValue={(x: string) => {
-                if (item.settings && item.settings.unit && x !== '') {
+                if (item.settings && item.settings.unit && x !== "") {
                   x = x + item.settings.unit;
                   x = x + item.settings.unit;
                 }
                 }
                 this.props.setMetaState({ [key]: btoa(x) });
                 this.props.setMetaState({ [key]: btoa(x) });
@@ -195,7 +189,7 @@ export default class ValuesForm extends Component<PropsType, StateType> {
         default:
         default:
       }
       }
     });
     });
-  }
+  };
 
 
   renderFormContents = () => {
   renderFormContents = () => {
     if (this.props.metaState) {
     if (this.props.metaState) {
@@ -207,14 +201,10 @@ export default class ValuesForm extends Component<PropsType, StateType> {
           }
           }
         }
         }
 
 
-        return (
-          <div key={i}>
-            {this.renderSection(section)}
-          </div>
-        );
+        return <div key={i}>{this.renderSection(section)}</div>;
       });
       });
     }
     }
-  }
+  };
 
 
   render() {
   render() {
     return (
     return (
@@ -249,4 +239,4 @@ const StyledValuesForm = styled.div`
   border-radius: 5px;
   border-radius: 5px;
   font-size: 13px;
   font-size: 13px;
   overflow: auto;
   overflow: auto;
-`;
+`;

+ 47 - 41
dashboard/src/components/values-form/ValuesWrapper.tsx

@@ -1,46 +1,45 @@
-import React, { Component } from 'react';
-import styled from 'styled-components';
+import React, { Component } from "react";
+import styled from "styled-components";
 
 
-import { Section, FormElement } from '../../shared/types';
-import { Context } from '../../shared/Context';
+import { Section, FormElement } from "../../shared/types";
+import { Context } from "../../shared/Context";
 
 
-import SaveButton from '../SaveButton';
+import SaveButton from "../SaveButton";
 
 
 type PropsType = {
 type PropsType = {
-  formTabs: any,
-  onSubmit: (formValues: any) => void,
-  disabled?: boolean,
-  saveValuesStatus?: string | null,
-  isInModal?: boolean,
-  currentTab?: string, // For resetting state when flipping b/w tabs in ExpandedChart
+  formTabs: any;
+  onSubmit: (formValues: any) => void;
+  disabled?: boolean;
+  saveValuesStatus?: string | null;
+  isInModal?: boolean;
+  currentTab?: string; // For resetting state when flipping b/w tabs in ExpandedChart
 };
 };
 
 
 type StateType = any;
 type StateType = any;
 
 
 const providerMap: any = {
 const providerMap: any = {
-  'gke': 'gcp',
-  'eks': 'aws',
-  'doks': 'do',
+  gke: "gcp",
+  eks: "aws",
+  doks: "do",
 };
 };
 
 
 // Manages the consolidated state of all form tabs ("metastate")
 // Manages the consolidated state of all form tabs ("metastate")
 export default class ValuesWrapper extends Component<PropsType, StateType> {
 export default class ValuesWrapper extends Component<PropsType, StateType> {
-
   // No need to render, so OK to set as class variable outside of state
   // No need to render, so OK to set as class variable outside of state
   requiredFields: string[] = [];
   requiredFields: string[] = [];
 
 
   updateFormState() {
   updateFormState() {
     let metaState: any = {};
     let metaState: any = {};
     this.props.formTabs.forEach((tab: any, i: number) => {
     this.props.formTabs.forEach((tab: any, i: number) => {
-
       // TODO: reconcile tab.name and tab.value
       // TODO: reconcile tab.name and tab.value
-      if (tab.name || (tab.value && tab.value.includes('@'))) {
+      if (tab.name || (tab.value && tab.value.includes("@"))) {
         tab.sections.forEach((section: Section, i: number) => {
         tab.sections.forEach((section: Section, i: number) => {
           section.contents.forEach((item: FormElement, i: number) => {
           section.contents.forEach((item: FormElement, i: number) => {
-
             // If no name is assigned use values.yaml variable as identifier
             // If no name is assigned use values.yaml variable as identifier
             let key = item.name || item.variable;
             let key = item.name || item.variable;
-            let def = (item.value && item.value[0]) || (item.settings && item.settings.default);
+            let def =
+              (item.value && item.value[0]) ||
+              (item.settings && item.settings.default);
 
 
             // Handle add to list of required fields
             // Handle add to list of required fields
             if (item.required) {
             if (item.required) {
@@ -48,29 +47,29 @@ export default class ValuesWrapper extends Component<PropsType, StateType> {
             }
             }
 
 
             switch (item.type) {
             switch (item.type) {
-              case 'checkbox':
+              case "checkbox":
                 metaState[key] = def ? def : false;
                 metaState[key] = def ? def : false;
                 break;
                 break;
-              case 'string-input':
-                metaState[key] = def ? def : '';
+              case "string-input":
+                metaState[key] = def ? def : "";
                 break;
                 break;
-              case 'array-input':
+              case "array-input":
                 metaState[key] = def ? def : [];
                 metaState[key] = def ? def : [];
                 break;
                 break;
-              case 'number-input':
-                metaState[key] = def.toString() ? def : '';
+              case "number-input":
+                metaState[key] = def.toString() ? def : "";
                 break;
                 break;
-              case 'select':
+              case "select":
                 metaState[key] = def ? def : item.settings.options[0].value;
                 metaState[key] = def ? def : item.settings.options[0].value;
                 break;
                 break;
-              case 'provider-select':
+              case "provider-select":
                 def = providerMap[this.context.currentCluster.service];
                 def = providerMap[this.context.currentCluster.service];
-                metaState[key] = def ? def : 'aws';
+                metaState[key] = def ? def : "aws";
                 break;
                 break;
-              case 'base-64':
-                metaState[key] = def ? def : '';
-              case 'base-64-password':
-                metaState[key] = def ? def : '';
+              case "base-64":
+                metaState[key] = def ? def : "";
+              case "base-64-password":
+                metaState[key] = def ? def : "";
               default:
               default:
             }
             }
           });
           });
@@ -86,7 +85,8 @@ export default class ValuesWrapper extends Component<PropsType, StateType> {
   }
   }
 
 
   componentDidUpdate(prevProps: PropsType) {
   componentDidUpdate(prevProps: PropsType) {
-    if (this.props.formTabs !== prevProps.formTabs || 
+    if (
+      this.props.formTabs !== prevProps.formTabs ||
       this.props.currentTab !== prevProps.currentTab
       this.props.currentTab !== prevProps.currentTab
     ) {
     ) {
       this.updateFormState();
       this.updateFormState();
@@ -99,24 +99,30 @@ export default class ValuesWrapper extends Component<PropsType, StateType> {
     this.requiredFields.forEach((field: string, i: number) => {
     this.requiredFields.forEach((field: string, i: number) => {
       valueIndicators.push(this.state[field] && true);
       valueIndicators.push(this.state[field] && true);
     });
     });
-    return valueIndicators.includes(false) || valueIndicators.includes('')
-  }
+    return valueIndicators.includes(false) || valueIndicators.includes("");
+  };
 
 
   renderButton = () => {
   renderButton = () => {
     let { formTabs, currentTab } = this.props;
     let { formTabs, currentTab } = this.props;
-    let tab = formTabs.find((t: any) => t.name === currentTab || t.value === currentTab);
-    if (tab && tab.context && tab.context.type === 'helm/values') {
+    let tab = formTabs.find(
+      (t: any) => t.name === currentTab || t.value === currentTab
+    );
+    if (tab && tab.context && tab.context.type === "helm/values") {
       return (
       return (
         <SaveButton
         <SaveButton
           disabled={this.isDisabled() || this.props.disabled}
           disabled={this.isDisabled() || this.props.disabled}
-          text='Deploy'
+          text="Deploy"
           onClick={() => this.props.onSubmit(this.state)}
           onClick={() => this.props.onSubmit(this.state)}
-          status={this.isDisabled() ? 'Missing required fields' : this.props.saveValuesStatus}
+          status={
+            this.isDisabled()
+              ? "Missing required fields"
+              : this.props.saveValuesStatus
+          }
           makeFlush={true}
           makeFlush={true}
         />
         />
       );
       );
     }
     }
-  }
+  };
 
 
   render() {
   render() {
     let renderFunc: any = this.props.children;
     let renderFunc: any = this.props.children;
@@ -150,4 +156,4 @@ const StyledValuesWrapper = styled.div`
 const PaddedWrapper = styled.div`
 const PaddedWrapper = styled.div`
   padding-bottom: 65px;
   padding-bottom: 65px;
   position: relative;
   position: relative;
-`;
+`;

+ 378 - 292
dashboard/src/main/home/Home.tsx

@@ -1,49 +1,49 @@
-import React, { Component } from 'react';
-import posthog from 'posthog-js';
-import styled from 'styled-components';
-
-import { Context } from 'shared/Context';
-import api from 'shared/api';
-import { ClusterType, ProjectType } from 'shared/types';
-import { includesCompletedInfraSet } from 'shared/common';
-
-import Sidebar from './sidebar/Sidebar';
-import Dashboard from './dashboard/Dashboard';
-import ClusterDashboard from './cluster-dashboard/ClusterDashboard';
-import Loading from 'components/Loading';
-import Templates from './templates/Templates';
+import React, { Component } from "react";
+import posthog from "posthog-js";
+import styled from "styled-components";
+
+import { Context } from "shared/Context";
+import api from "shared/api";
+import { ClusterType, ProjectType } from "shared/types";
+import { includesCompletedInfraSet } from "shared/common";
+
+import Sidebar from "./sidebar/Sidebar";
+import Dashboard from "./dashboard/Dashboard";
+import ClusterDashboard from "./cluster-dashboard/ClusterDashboard";
+import Loading from "components/Loading";
+import Templates from "./templates/Templates";
 import Integrations from "./integrations/Integrations";
 import Integrations from "./integrations/Integrations";
-import UpdateClusterModal from './modals/UpdateClusterModal';
-import ClusterInstructionsModal from './modals/ClusterInstructionsModal';
-import IntegrationsModal from './modals/IntegrationsModal';
-import IntegrationsInstructionsModal from './modals/IntegrationsInstructionsModal';
-import NewProject from './new-project/NewProject';
-import Navbar from './navbar/Navbar';
-import ProvisionerStatus from './provisioner/ProvisionerStatus';
-import ProjectSettings from './project-settings/ProjectSettings';
-import ConfirmOverlay from 'components/ConfirmOverlay';
-import Modal from './modals/Modal';
-import * as FullStory from '@fullstory/browser';
-import { Redirect, RouteComponentProps, withRouter } from 'react-router';
-import {PorterUrls} from 'shared/urls';
+import UpdateClusterModal from "./modals/UpdateClusterModal";
+import ClusterInstructionsModal from "./modals/ClusterInstructionsModal";
+import IntegrationsModal from "./modals/IntegrationsModal";
+import IntegrationsInstructionsModal from "./modals/IntegrationsInstructionsModal";
+import NewProject from "./new-project/NewProject";
+import Navbar from "./navbar/Navbar";
+import ProvisionerStatus from "./provisioner/ProvisionerStatus";
+import ProjectSettings from "./project-settings/ProjectSettings";
+import ConfirmOverlay from "components/ConfirmOverlay";
+import Modal from "./modals/Modal";
+import * as FullStory from "@fullstory/browser";
+import { Redirect, RouteComponentProps, withRouter } from "react-router";
+import { PorterUrls } from "shared/urls";
 
 
 type PropsType = RouteComponentProps & {
 type PropsType = RouteComponentProps & {
-  logOut: () => void,
-  currentProject: ProjectType,
-  currentCluster: ClusterType,
-  currentRoute: PorterUrls,
+  logOut: () => void;
+  currentProject: ProjectType;
+  currentCluster: ClusterType;
+  currentRoute: PorterUrls;
 };
 };
 
 
 type StateType = {
 type StateType = {
-  forceSidebar: boolean,
-  showWelcome: boolean,
-  handleDO: boolean, // Trigger DO infra calls after oauth flow if needed
-  ghRedirect: boolean,
-  forceRefreshClusters: boolean, // For updating ClusterSection from modal on deletion
+  forceSidebar: boolean;
+  showWelcome: boolean;
+  handleDO: boolean; // Trigger DO infra calls after oauth flow if needed
+  ghRedirect: boolean;
+  forceRefreshClusters: boolean; // For updating ClusterSection from modal on deletion
 
 
   // Track last project id for refreshing clusters on project change
   // Track last project id for refreshing clusters on project change
-  prevProjectId: number | null,
-  sidebarReady: boolean, // Fixes error where ~1/3 times reloading to provisioner fails
+  prevProjectId: number | null;
+  sidebarReady: boolean; // Fixes error where ~1/3 times reloading to provisioner fails
 };
 };
 
 
 // TODO: Handle cluster connected but with some failed infras (no successful set)
 // TODO: Handle cluster connected but with some failed infras (no successful set)
@@ -56,173 +56,208 @@ class Home extends Component<PropsType, StateType> {
     sidebarReady: false,
     sidebarReady: false,
     handleDO: false,
     handleDO: false,
     ghRedirect: false,
     ghRedirect: false,
-  }
+  };
 
 
   // TODO: Refactor and prevent flash + multiple reload
   // TODO: Refactor and prevent flash + multiple reload
   initializeView = () => {
   initializeView = () => {
     let { currentProject } = this.props;
     let { currentProject } = this.props;
     let { currentCluster } = this.context;
     let { currentCluster } = this.context;
-    
+
     if (!currentProject) return;
     if (!currentProject) return;
 
 
     // Check if current project is provisioning
     // Check if current project is provisioning
-    api.getInfra('<token>', {}, { project_id: currentProject.id }, (err: any, res: any) => {
-      if (err) {
-        console.log(err);
-        return;
-      }
+    api.getInfra(
+      "<token>",
+      {},
+      { project_id: currentProject.id },
+      (err: any, res: any) => {
+        if (err) {
+          console.log(err);
+          return;
+        }
 
 
-      if (res.data.length > 0 && (!currentCluster && !includesCompletedInfraSet(res.data))) {
-        this.props.history.push("provisioner");
-        this.setState({ sidebarReady: true });
-      } else if (this.state.ghRedirect) {
-        this.props.history.push("integrations");
-        this.setState({ sidebarReady: true, ghRedirect: false });
-      } else {
-        // TODO: figure out when exactly in flow we need to send user to dashboard
-        // this.props.history.push("dashboard");
-        this.setState({ sidebarReady: true });
+        if (
+          res.data.length > 0 &&
+          !currentCluster &&
+          !includesCompletedInfraSet(res.data)
+        ) {
+          this.props.history.push("provisioner");
+          this.setState({ sidebarReady: true });
+        } else if (this.state.ghRedirect) {
+          this.props.history.push("integrations");
+          this.setState({ sidebarReady: true, ghRedirect: false });
+        } else {
+          // TODO: figure out when exactly in flow we need to send user to dashboard
+          // this.props.history.push("dashboard");
+          this.setState({ sidebarReady: true });
+        }
       }
       }
-    });
-  }
+    );
+  };
 
 
   getProjects = (id?: number) => {
   getProjects = (id?: number) => {
     let { user, setProjects } = this.context;
     let { user, setProjects } = this.context;
     let { currentProject } = this.props;
     let { currentProject } = this.props;
-    api.getProjects('<token>', {}, { id: user.userId }, (err: any, res: any) => {
-      if (err) {
-        console.log(err);
-      } else if (res.data) {
-        if (res.data.length === 0) {
-          this.props.history.push("new-project");
-        } else if (res.data.length > 0 && !currentProject) {
-          setProjects(res.data);
-
-          let foundProject = null;
-          if (id) {
-            res.data.forEach((project: ProjectType, i: number) => {
-              if (project.id === id) {
-                foundProject = project;
-              } 
-            });
-            this.context.setCurrentProject(foundProject);
-            <Redirect to="provisioner"></Redirect>
-          }
-
-          if (!foundProject) {
-            res.data.forEach((project: ProjectType, i: number) => {
-              if (project.id.toString() === localStorage.getItem('currentProject')) {
-                foundProject = project;
-              }
-            })
-            this.context.setCurrentProject(foundProject ? foundProject : res.data[0]);
-            this.initializeView();
+    api.getProjects(
+      "<token>",
+      {},
+      { id: user.userId },
+      (err: any, res: any) => {
+        if (err) {
+          console.log(err);
+        } else if (res.data) {
+          if (res.data.length === 0) {
+            this.props.history.push("new-project");
+          } else if (res.data.length > 0 && !currentProject) {
+            setProjects(res.data);
+
+            let foundProject = null;
+            if (id) {
+              res.data.forEach((project: ProjectType, i: number) => {
+                if (project.id === id) {
+                  foundProject = project;
+                }
+              });
+              this.context.setCurrentProject(foundProject);
+              <Redirect to="provisioner"></Redirect>;
+            }
+
+            if (!foundProject) {
+              res.data.forEach((project: ProjectType, i: number) => {
+                if (
+                  project.id.toString() ===
+                  localStorage.getItem("currentProject")
+                ) {
+                  foundProject = project;
+                }
+              });
+              this.context.setCurrentProject(
+                foundProject ? foundProject : res.data[0]
+              );
+              this.initializeView();
+            }
           }
           }
         }
         }
       }
       }
-    });
-  }
+    );
+  };
 
 
   provisionDOCR = (integrationId: number, tier: string, callback?: any) => {
   provisionDOCR = (integrationId: number, tier: string, callback?: any) => {
-    console.log('Provisioning DOCR...');
-    api.createDOCR('<token>', {
-      do_integration_id: integrationId,
-      docr_name: this.props.currentProject.name,
-      docr_subscription_tier: tier,
-    }, { 
-      project_id: this.props.currentProject.id
-    }, (err: any, res: any) => {
-      if (err) {
-        console.log(err);
-        return;
+    console.log("Provisioning DOCR...");
+    api.createDOCR(
+      "<token>",
+      {
+        do_integration_id: integrationId,
+        docr_name: this.props.currentProject.name,
+        docr_subscription_tier: tier,
+      },
+      {
+        project_id: this.props.currentProject.id,
+      },
+      (err: any, res: any) => {
+        if (err) {
+          console.log(err);
+          return;
+        }
+        callback && callback();
       }
       }
-      callback && callback();
-    });
-  }
+    );
+  };
 
 
   provisionDOKS = (integrationId: number, region: string) => {
   provisionDOKS = (integrationId: number, region: string) => {
-    console.log('Provisioning DOKS...');
-    api.createDOKS('<token>', {
-      do_integration_id: integrationId,
-      doks_name: this.props.currentProject.name,
-      do_region: region,
-    }, { 
-      project_id: this.props.currentProject.id
-    }, (err: any, res: any) => {
-      if (err) {
-        console.log(err);
-        return;
+    console.log("Provisioning DOKS...");
+    api.createDOKS(
+      "<token>",
+      {
+        do_integration_id: integrationId,
+        doks_name: this.props.currentProject.name,
+        do_region: region,
+      },
+      {
+        project_id: this.props.currentProject.id,
+      },
+      (err: any, res: any) => {
+        if (err) {
+          console.log(err);
+          return;
+        }
+        <Redirect to="provisioner"></Redirect>;
       }
       }
-      <Redirect to="provisioner"></Redirect>
-    });
-  }
+    );
+  };
 
 
   checkDO = () => {
   checkDO = () => {
     let { currentProject } = this.props;
     let { currentProject } = this.props;
     if (this.state.handleDO && currentProject?.id) {
     if (this.state.handleDO && currentProject?.id) {
-      api.getOAuthIds('<token>', {}, { 
-        project_id: currentProject.id
-      }, (err: any, res: any) => {
-        if (err) {
-          console.log(err);
-          return;
-        }
-        let tgtIntegration = res.data.find((integration: any) => {
-          return integration.client === 'do'
-        });
-        let queryString = window.location.search;
-        let urlParams = new URLSearchParams(queryString);
-        let tier = urlParams.get('tier');
-        let region = urlParams.get('region');
-        let infras = urlParams.getAll('infras');
-        if (infras.length === 2) {
-          this.provisionDOCR(tgtIntegration.id, tier, () => {
-            this.provisionDOKS(tgtIntegration.id, region);
-          });
-        } else if (infras[0] === 'docr') {
-          this.provisionDOCR(tgtIntegration.id, tier, () => {
-            <Redirect to="provisioner"></Redirect>
+      api.getOAuthIds(
+        "<token>",
+        {},
+        {
+          project_id: currentProject.id,
+        },
+        (err: any, res: any) => {
+          if (err) {
+            console.log(err);
+            return;
+          }
+          let tgtIntegration = res.data.find((integration: any) => {
+            return integration.client === "do";
           });
           });
-        } else {
-          this.provisionDOKS(tgtIntegration.id, region);
+          let queryString = window.location.search;
+          let urlParams = new URLSearchParams(queryString);
+          let tier = urlParams.get("tier");
+          let region = urlParams.get("region");
+          let infras = urlParams.getAll("infras");
+          if (infras.length === 2) {
+            this.provisionDOCR(tgtIntegration.id, tier, () => {
+              this.provisionDOKS(tgtIntegration.id, region);
+            });
+          } else if (infras[0] === "docr") {
+            this.provisionDOCR(tgtIntegration.id, tier, () => {
+              <Redirect to="provisioner"></Redirect>;
+            });
+          } else {
+            this.provisionDOKS(tgtIntegration.id, region);
+          }
         }
         }
-      });
+      );
       this.setState({ handleDO: false });
       this.setState({ handleDO: false });
     }
     }
-  }
+  };
 
 
   componentDidMount() {
   componentDidMount() {
     let { user } = this.context;
     let { user } = this.context;
-    FullStory.identify(user.email)
+    FullStory.identify(user.email);
 
 
     // Handle redirect from DO
     // Handle redirect from DO
     let queryString = window.location.search;
     let queryString = window.location.search;
     let urlParams = new URLSearchParams(queryString);
     let urlParams = new URLSearchParams(queryString);
 
 
-    let err = urlParams.get('error');
+    let err = urlParams.get("error");
     if (err) {
     if (err) {
       this.context.setCurrentError(err);
       this.context.setCurrentError(err);
     }
     }
 
 
-    let provision = urlParams.get('provision');
+    let provision = urlParams.get("provision");
     let defaultProjectId = null;
     let defaultProjectId = null;
-    if (provision === 'do') {
-      defaultProjectId = parseInt(urlParams.get('project_id'));
+    if (provision === "do") {
+      defaultProjectId = parseInt(urlParams.get("project_id"));
       this.setState({ handleDO: true });
       this.setState({ handleDO: true });
       this.checkDO();
       this.checkDO();
     }
     }
 
 
     // initialize posthog on non-localhosts. Gracefully fail when env vars are not set.
     // initialize posthog on non-localhosts. Gracefully fail when env vars are not set.
-    this.setState({ ghRedirect: urlParams.get('gh_oauth') !== null });
-    urlParams.delete('gh_oauth');
-    
-    window.location.href.indexOf('localhost') === -1 && posthog.init(process.env.POSTHOG_API_KEY || 'placeholder', {
-      api_host: process.env.POSTHOG_HOST || 'placeholder',
-      loaded: function(posthog: any) { 
-        posthog.identify(user.userId) 
-        posthog.people.set({ email: user.email })
-      }
-    })
+    this.setState({ ghRedirect: urlParams.get("gh_oauth") !== null });
+    urlParams.delete("gh_oauth");
+
+    window.location.href.indexOf("localhost") === -1 &&
+      posthog.init(process.env.POSTHOG_API_KEY || "placeholder", {
+        api_host: process.env.POSTHOG_HOST || "placeholder",
+        loaded: function (posthog: any) {
+          posthog.identify(user.userId);
+          posthog.people.set({ email: user.email });
+        },
+      });
 
 
     this.getProjects(defaultProjectId);
     this.getProjects(defaultProjectId);
   }
   }
@@ -233,8 +268,8 @@ class Home extends Component<PropsType, StateType> {
   // 3. Make sure initializing from URL (DO oauth) displays the appropriate initial view
   // 3. Make sure initializing from URL (DO oauth) displays the appropriate initial view
   componentDidUpdate(prevProps: PropsType) {
   componentDidUpdate(prevProps: PropsType) {
     if (
     if (
-      prevProps.currentProject !== this.props.currentProject
-      || (!prevProps.currentCluster && this.props.currentCluster)
+      prevProps.currentProject !== this.props.currentProject ||
+      (!prevProps.currentCluster && this.props.currentCluster)
     ) {
     ) {
       if (this.state.handleDO) {
       if (this.state.handleDO) {
         this.checkDO();
         this.checkDO();
@@ -251,19 +286,36 @@ class Home extends Component<PropsType, StateType> {
       return (
       return (
         <DashboardWrapper>
         <DashboardWrapper>
           <Placeholder>
           <Placeholder>
-            <Bold>Porter - Getting Started</Bold><br /><br />
-            1. Navigate to <A onClick={() => setCurrentModal('ClusterConfigModal')}>+ Add a Cluster</A> and provide a kubeconfig. *<br /><br />
-            2. Choose which contexts you would like to use from the <A onClick={() => {
-              setCurrentModal('ClusterConfigModal', { currentTab: 'select' });
-            }}>Select Clusters</A> tab.<br /><br />
-            3. For additional information, please refer to our <A>docs</A>.<br /><br /><br />
-
-            * Make sure all fields are explicitly declared (e.g., certs and keys).
+            <Bold>Porter - Getting Started</Bold>
+            <br />
+            <br />
+            1. Navigate to{" "}
+            <A onClick={() => setCurrentModal("ClusterConfigModal")}>
+              + Add a Cluster
+            </A>{" "}
+            and provide a kubeconfig. *<br />
+            <br />
+            2. Choose which contexts you would like to use from the{" "}
+            <A
+              onClick={() => {
+                setCurrentModal("ClusterConfigModal", { currentTab: "select" });
+              }}
+            >
+              Select Clusters
+            </A>{" "}
+            tab.
+            <br />
+            <br />
+            3. For additional information, please refer to our <A>docs</A>.
+            <br />
+            <br />
+            <br />* Make sure all fields are explicitly declared (e.g., certs
+            and keys).
           </Placeholder>
           </Placeholder>
         </DashboardWrapper>
         </DashboardWrapper>
       );
       );
     } else if (!currentCluster) {
     } else if (!currentCluster) {
-      return <Loading />
+      return <Loading />;
     }
     }
 
 
     return (
     return (
@@ -275,48 +327,40 @@ class Home extends Component<PropsType, StateType> {
         />
         />
       </DashboardWrapper>
       </DashboardWrapper>
     );
     );
-  }
+  };
 
 
   renderContents = () => {
   renderContents = () => {
     let currentView = this.props.currentRoute;
     let currentView = this.props.currentRoute;
-    if (this.context.currentProject && currentView !== 'new-project') {
-      if (currentView === 'cluster-dashboard') {
+    if (this.context.currentProject && currentView !== "new-project") {
+      if (currentView === "cluster-dashboard") {
         return this.renderDashboard();
         return this.renderDashboard();
-      } else if (currentView === 'dashboard') {
+      } else if (currentView === "dashboard") {
         return (
         return (
           <DashboardWrapper>
           <DashboardWrapper>
-            <Dashboard 
-              projectId={this.context.currentProject?.id}
-            />
+            <Dashboard projectId={this.context.currentProject?.id} />
           </DashboardWrapper>
           </DashboardWrapper>
         );
         );
-      } else if (currentView === 'integrations') {
+      } else if (currentView === "integrations") {
         return <Integrations />;
         return <Integrations />;
-      } else if (currentView === 'provisioner') {
-        return (
-          <ProvisionerStatus/>
-        );
-      } else if (currentView === 'project-settings') {
-        return (
-          <ProjectSettings />
-        )
+      } else if (currentView === "provisioner") {
+        return <ProvisionerStatus />;
+      } else if (currentView === "project-settings") {
+        return <ProjectSettings />;
       }
       }
 
 
-      return (
-        <Templates/>
-      );
-    } else if (currentView === 'new-project') {
-      return (
-        <NewProject/>
-      );
+      return <Templates />;
+    } else if (currentView === "new-project") {
+      return <NewProject />;
     }
     }
-  }
+  };
 
 
   renderSidebar = () => {
   renderSidebar = () => {
     if (this.context.projects.length > 0) {
     if (this.context.projects.length > 0) {
-
       // Force sidebar closed on first provision
       // Force sidebar closed on first provision
-      if (this.props.currentRoute === 'provisioner' && this.state.forceSidebar) {
+      if (
+        this.props.currentRoute === "provisioner" &&
+        this.state.forceSidebar
+      ) {
         this.setState({ forceSidebar: false });
         this.setState({ forceSidebar: false });
       } else {
       } else {
         return (
         return (
@@ -326,145 +370,180 @@ class Home extends Component<PropsType, StateType> {
             setWelcome={(x: boolean) => this.setState({ showWelcome: x })}
             setWelcome={(x: boolean) => this.setState({ showWelcome: x })}
             currentView={this.props.currentRoute}
             currentView={this.props.currentRoute}
             forceRefreshClusters={this.state.forceRefreshClusters}
             forceRefreshClusters={this.state.forceRefreshClusters}
-            setRefreshClusters={(x: boolean) => this.setState({ forceRefreshClusters: x })}
+            setRefreshClusters={(x: boolean) =>
+              this.setState({ forceRefreshClusters: x })
+            }
           />
           />
         );
         );
       }
       }
     }
     }
-  }
+  };
 
 
   projectOverlayCall = () => {
   projectOverlayCall = () => {
     let { user, setProjects } = this.context;
     let { user, setProjects } = this.context;
-    api.getProjects('<token>', {}, { id: user.userId }, (err: any, res: any) => {
-      if (err) {
-        console.log(err)
-      } else if (res.data) {
-        setProjects(res.data);
-        if (res.data.length > 0) {
-          this.context.setCurrentProject(res.data[0]);
-        } else {
-          this.context.setCurrentProject(null);
-          this.props.history.push("new-project");
+    api.getProjects(
+      "<token>",
+      {},
+      { id: user.userId },
+      (err: any, res: any) => {
+        if (err) {
+          console.log(err);
+        } else if (res.data) {
+          setProjects(res.data);
+          if (res.data.length > 0) {
+            this.context.setCurrentProject(res.data[0]);
+          } else {
+            this.context.setCurrentProject(null);
+            this.props.history.push("new-project");
+          }
+          this.context.setCurrentModal(null, null);
         }
         }
-        this.context.setCurrentModal(null, null);
       }
       }
-    });
-  }
+    );
+  };
 
 
   handleDelete = () => {
   handleDelete = () => {
     let { setCurrentModal, currentProject } = this.context;
     let { setCurrentModal, currentProject } = this.context;
-    localStorage.removeItem(currentProject.id + '-cluster');
-    api.deleteProject('<token>', {}, { id: currentProject.id }, (err: any, res: any) => {
-      if (err) {
-        console.log(err)
-      } else {
-        this.projectOverlayCall();
+    localStorage.removeItem(currentProject.id + "-cluster");
+    api.deleteProject(
+      "<token>",
+      {},
+      { id: currentProject.id },
+      (err: any, res: any) => {
+        if (err) {
+          console.log(err);
+        } else {
+          this.projectOverlayCall();
+        }
       }
       }
-    });
+    );
 
 
     // Loop through and delete infra of all clusters we've provisioned
     // Loop through and delete infra of all clusters we've provisioned
-    api.getClusters('<token>', {}, { id: currentProject.id }, (err: any, res: any) => {
+    api.getClusters(
+      "<token>",
+      {},
+      { id: currentProject.id },
+      (err: any, res: any) => {
+        if (err) {
+          console.log(err);
+          return;
+        }
 
 
-      if (err) { 
-        console.log(err); 
-        return; 
-      }
-      
-      for (var i = 0; i < res.data.length; i++) {
-        let cluster = res.data[i];
-        if (!cluster.infra_id) continue;
-
-        // Handle destroying infra we've provisioned
-        switch (cluster.service) {
-
-          case "eks":
-            api.destroyEKS('<token>', { eks_name: cluster.name }, { 
-              project_id: currentProject.id,
-              infra_id: cluster.infra_id,
-            }, (err: any, res: any) => {
-              if (err) {
-                console.log(err)
-              } else {
-                console.log('destroyed provisioned infra:', cluster.infra_id);
-              }
-            });
-            break;
-
-          case 'gke':
-            api.destroyGKE('<token>', { gke_name: cluster.name }, { 
-              project_id: currentProject.id,
-              infra_id: cluster.infra_id,
-            }, (err: any, res: any) => {
-              if (err) {
-                console.log(err)
-              } else {
-                console.log('destroyed provisioned infra.');
-              }
-            });
-            break;
-
-          case 'doks':
-            api.destroyDOKS('<token>', { doks_name: cluster.name }, { 
-              project_id: currentProject.id,
-              infra_id: cluster.infra_id,
-            }, (err: any, res: any) => {
-              if (err) {
-                console.log(err)
-              } else {
-                console.log('destroyed provisioned infra.');
-              }
-            });
-            break;
+        for (var i = 0; i < res.data.length; i++) {
+          let cluster = res.data[i];
+          if (!cluster.infra_id) continue;
+
+          // Handle destroying infra we've provisioned
+          switch (cluster.service) {
+            case "eks":
+              api.destroyEKS(
+                "<token>",
+                { eks_name: cluster.name },
+                {
+                  project_id: currentProject.id,
+                  infra_id: cluster.infra_id,
+                },
+                (err: any, res: any) => {
+                  if (err) {
+                    console.log(err);
+                  } else {
+                    console.log(
+                      "destroyed provisioned infra:",
+                      cluster.infra_id
+                    );
+                  }
+                }
+              );
+              break;
+
+            case "gke":
+              api.destroyGKE(
+                "<token>",
+                { gke_name: cluster.name },
+                {
+                  project_id: currentProject.id,
+                  infra_id: cluster.infra_id,
+                },
+                (err: any, res: any) => {
+                  if (err) {
+                    console.log(err);
+                  } else {
+                    console.log("destroyed provisioned infra.");
+                  }
+                }
+              );
+              break;
+
+            case "doks":
+              api.destroyDOKS(
+                "<token>",
+                { doks_name: cluster.name },
+                {
+                  project_id: currentProject.id,
+                  infra_id: cluster.infra_id,
+                },
+                (err: any, res: any) => {
+                  if (err) {
+                    console.log(err);
+                  } else {
+                    console.log("destroyed provisioned infra.");
+                  }
+                }
+              );
+              break;
+          }
         }
         }
       }
       }
-    });
+    );
     setCurrentModal(null, null);
     setCurrentModal(null, null);
     this.props.history.push("dashboard");
     this.props.history.push("dashboard");
-  }
+  };
 
 
   render() {
   render() {
     let { currentModal, setCurrentModal, currentProject } = this.context;
     let { currentModal, setCurrentModal, currentProject } = this.context;
 
 
     return (
     return (
       <StyledHome>
       <StyledHome>
-        {currentModal === 'ClusterInstructionsModal' &&
+        {currentModal === "ClusterInstructionsModal" && (
           <Modal
           <Modal
             onRequestClose={() => setCurrentModal(null, null)}
             onRequestClose={() => setCurrentModal(null, null)}
-            width='760px'
-            height='650px'
+            width="760px"
+            height="650px"
           >
           >
             <ClusterInstructionsModal />
             <ClusterInstructionsModal />
           </Modal>
           </Modal>
-        }
-        {currentModal === 'UpdateClusterModal' &&
+        )}
+        {currentModal === "UpdateClusterModal" && (
           <Modal
           <Modal
             onRequestClose={() => setCurrentModal(null, null)}
             onRequestClose={() => setCurrentModal(null, null)}
-            width='565px'
-            height='275px'
+            width="565px"
+            height="275px"
           >
           >
-            <UpdateClusterModal 
-              setRefreshClusters={(x: boolean) => this.setState({ forceRefreshClusters: x })} 
+            <UpdateClusterModal
+              setRefreshClusters={(x: boolean) =>
+                this.setState({ forceRefreshClusters: x })
+              }
             />
             />
           </Modal>
           </Modal>
-        }
-        {currentModal === 'IntegrationsModal' &&
+        )}
+        {currentModal === "IntegrationsModal" && (
           <Modal
           <Modal
             onRequestClose={() => setCurrentModal(null, null)}
             onRequestClose={() => setCurrentModal(null, null)}
-            width='760px'
-            height='725px'
+            width="760px"
+            height="725px"
           >
           >
             <IntegrationsModal />
             <IntegrationsModal />
           </Modal>
           </Modal>
-        }
-        {currentModal === 'IntegrationsInstructionsModal' &&
+        )}
+        {currentModal === "IntegrationsInstructionsModal" && (
           <Modal
           <Modal
             onRequestClose={() => setCurrentModal(null, null)}
             onRequestClose={() => setCurrentModal(null, null)}
-            width='760px'
-            height='650px'
+            width="760px"
+            height="650px"
           >
           >
             <IntegrationsInstructionsModal />
             <IntegrationsInstructionsModal />
           </Modal>
           </Modal>
-        }
+        )}
 
 
         {this.renderSidebar()}
         {this.renderSidebar()}
 
 
@@ -477,8 +556,12 @@ class Home extends Component<PropsType, StateType> {
         </ViewWrapper>
         </ViewWrapper>
 
 
         <ConfirmOverlay
         <ConfirmOverlay
-          show={currentModal === 'UpdateProjectModal'}
-          message={(currentProject) ? `Are you sure you want to delete ${currentProject.name}?` : ''}
+          show={currentModal === "UpdateProjectModal"}
+          message={
+            currentProject
+              ? `Are you sure you want to delete ${currentProject.name}?`
+              : ""
+          }
           onYes={this.handleDelete}
           onYes={this.handleDelete}
           onNo={() => setCurrentModal(null, null)}
           onNo={() => setCurrentModal(null, null)}
         />
         />
@@ -513,7 +596,8 @@ const DashboardWrapper = styled.div`
 const A = styled.a`
 const A = styled.a`
   color: #ffffff;
   color: #ffffff;
   text-decoration: underline;
   text-decoration: underline;
-  cursor: ${(props: { disabled?: boolean }) => props.disabled ? 'not-allowed' : 'pointer'};
+  cursor: ${(props: { disabled?: boolean }) =>
+    props.disabled ? "not-allowed" : "pointer"};
 `;
 `;
 
 
 const Placeholder = styled.div`
 const Placeholder = styled.div`
@@ -543,10 +627,12 @@ const StyledHome = styled.div`
 
 
   @keyframes floatInModal {
   @keyframes floatInModal {
     from {
     from {
-      opacity: 0; transform: translateY(30px);
+      opacity: 0;
+      transform: translateY(30px);
     }
     }
     to {
     to {
-      opacity: 1; transform: translateY(0px);
+      opacity: 1;
+      transform: translateY(0px);
     }
     }
   }
   }
-`;
+`;

+ 63 - 53
dashboard/src/main/home/cluster-dashboard/ClusterDashboard.tsx

@@ -1,42 +1,47 @@
-import React, { Component } from 'react';
-import styled from 'styled-components';
-import gradient from 'assets/gradient.jpg';
+import React, { Component } from "react";
+import styled from "styled-components";
+import gradient from "assets/gradient.jpg";
 
 
-import { Context } from 'shared/Context';
-import { ChartType, StorageType, ClusterType } from 'shared/types';
-import api from 'shared/api';
+import { Context } from "shared/Context";
+import { ChartType, ClusterType } from "shared/types";
 
 
-import ChartList from './chart/ChartList';
-import NamespaceSelector from './NamespaceSelector';
-import SortSelector from './SortSelector';
-import ExpandedChart from './expanded-chart/ExpandedChart';
-import { Redirect, RouteComponentProps, withRouter } from 'react-router';
+import ChartList from "./chart/ChartList";
+import NamespaceSelector from "./NamespaceSelector";
+import SortSelector from "./SortSelector";
+import ExpandedChart from "./expanded-chart/ExpandedChart";
+import { RouteComponentProps, withRouter } from "react-router";
 
 
 type PropsType = RouteComponentProps & {
 type PropsType = RouteComponentProps & {
-  currentCluster: ClusterType,
-  setSidebar: (x: boolean) => void,
+  currentCluster: ClusterType;
+  setSidebar: (x: boolean) => void;
 };
 };
 
 
 type StateType = {
 type StateType = {
-  namespace: string,
-  sortType: string,
-  currentChart: ChartType | null
+  namespace: string;
+  sortType: string;
+  currentChart: ChartType | null;
 };
 };
 
 
 class ClusterDashboard extends Component<PropsType, StateType> {
 class ClusterDashboard extends Component<PropsType, StateType> {
   state = {
   state = {
-    namespace: 'default',
-    sortType: (localStorage.getItem("SortType") ? localStorage.getItem('SortType') : 'Newest'),
-    currentChart: null as (ChartType | null)
-  }
+    namespace: "default",
+    sortType: localStorage.getItem("SortType")
+      ? localStorage.getItem("SortType")
+      : "Newest",
+    currentChart: null as ChartType | null,
+  };
 
 
   componentDidUpdate(prevProps: PropsType) {
   componentDidUpdate(prevProps: PropsType) {
     localStorage.setItem("SortType", this.state.sortType);
     localStorage.setItem("SortType", this.state.sortType);
     // Reset namespace filter and close expanded chart on cluster change
     // Reset namespace filter and close expanded chart on cluster change
     if (prevProps.currentCluster !== this.props.currentCluster) {
     if (prevProps.currentCluster !== this.props.currentCluster) {
-      this.setState({ namespace: 'default', sortType: (
-        localStorage.getItem("SortType") ? localStorage.getItem('SortType') : 'Newest'
-      ), currentChart: null });
+      this.setState({
+        namespace: "default",
+        sortType: localStorage.getItem("SortType")
+          ? localStorage.getItem("SortType")
+          : "Newest",
+        currentChart: null,
+      });
     }
     }
   }
   }
 
 
@@ -46,7 +51,9 @@ class ClusterDashboard extends Component<PropsType, StateType> {
       return (
       return (
         <DashboardIcon>
         <DashboardIcon>
           <DashboardImage src={gradient} />
           <DashboardImage src={gradient} />
-          <Overlay>{currentCluster && currentCluster.name[0].toUpperCase()}</Overlay>
+          <Overlay>
+            {currentCluster && currentCluster.name[0].toUpperCase()}
+          </Overlay>
         </DashboardIcon>
         </DashboardIcon>
       );
       );
     }
     }
@@ -56,18 +63,20 @@ class ClusterDashboard extends Component<PropsType, StateType> {
         <i className="material-icons">device_hub</i>
         <i className="material-icons">device_hub</i>
       </DashboardIcon>
       </DashboardIcon>
     );
     );
-  }
+  };
 
 
   renderContents = () => {
   renderContents = () => {
     let { currentCluster, setSidebar } = this.props;
     let { currentCluster, setSidebar } = this.props;
-    
+
     if (this.state.currentChart) {
     if (this.state.currentChart) {
       return (
       return (
         <ExpandedChart
         <ExpandedChart
           namespace={this.state.namespace}
           namespace={this.state.namespace}
           currentCluster={this.props.currentCluster}
           currentCluster={this.props.currentCluster}
           currentChart={this.state.currentChart}
           currentChart={this.state.currentChart}
-          setCurrentChart={(x: ChartType | null) => this.setState({ currentChart: x })}
+          setCurrentChart={(x: ChartType | null) =>
+            this.setState({ currentChart: x })
+          }
           setSidebar={setSidebar}
           setSidebar={setSidebar}
         />
         />
       );
       );
@@ -78,9 +87,9 @@ class ClusterDashboard extends Component<PropsType, StateType> {
         <TitleSection>
         <TitleSection>
           {this.renderDashboardIcon()}
           {this.renderDashboardIcon()}
           <Title>{currentCluster.name}</Title>
           <Title>{currentCluster.name}</Title>
-          <i 
+          <i
             className="material-icons"
             className="material-icons"
-            onClick={() => this.context.setCurrentModal('UpdateClusterModal')}
+            onClick={() => this.context.setCurrentModal("UpdateClusterModal")}
           >
           >
             more_vert
             more_vert
           </i>
           </i>
@@ -92,15 +101,15 @@ class ClusterDashboard extends Component<PropsType, StateType> {
               <i className="material-icons">info</i> Info
               <i className="material-icons">info</i> Info
             </InfoLabel>
             </InfoLabel>
           </TopRow>
           </TopRow>
-          <Description>Cluster dashboard for {currentCluster.name}.</Description>
+          <Description>
+            Cluster dashboard for {currentCluster.name}.
+          </Description>
         </InfoSection>
         </InfoSection>
 
 
         <LineBreak />
         <LineBreak />
-        
+
         <ControlRow>
         <ControlRow>
-          <Button
-            onClick={() => this.props.history.push("templates")}
-          >
+          <Button onClick={() => this.props.history.push("templates")}>
             <i className="material-icons">add</i> Deploy Template
             <i className="material-icons">add</i> Deploy Template
           </Button>
           </Button>
           <SortFilterWrapper>
           <SortFilterWrapper>
@@ -119,18 +128,16 @@ class ClusterDashboard extends Component<PropsType, StateType> {
           currentCluster={currentCluster}
           currentCluster={currentCluster}
           namespace={this.state.namespace}
           namespace={this.state.namespace}
           sortType={this.state.sortType}
           sortType={this.state.sortType}
-          setCurrentChart={(x: ChartType | null) => this.setState({ currentChart: x })}
+          setCurrentChart={(x: ChartType | null) =>
+            this.setState({ currentChart: x })
+          }
         />
         />
       </div>
       </div>
     );
     );
-  }
+  };
 
 
   render() {
   render() {
-    return (
-      <div>
-        {this.renderContents()}
-      </div>
-    );
+    return <div>{this.renderContents()}</div>;
   }
   }
 }
 }
 
 
@@ -163,10 +170,10 @@ const InfoLabel = styled.div`
   height: 20px;
   height: 20px;
   display: flex;
   display: flex;
   align-items: center;
   align-items: center;
-  color: #7A838F;
+  color: #7a838f;
   font-size: 13px;
   font-size: 13px;
   > i {
   > i {
-    color: #8B949F;
+    color: #8b949f;
     font-size: 18px;
     font-size: 18px;
     margin-right: 5px;
     margin-right: 5px;
   }
   }
@@ -174,7 +181,7 @@ const InfoLabel = styled.div`
 
 
 const InfoSection = styled.div`
 const InfoSection = styled.div`
   margin-top: 20px;
   margin-top: 20px;
-  font-family: 'Work Sans', sans-serif;
+  font-family: "Work Sans", sans-serif;
   margin-left: 0px;
   margin-left: 0px;
   margin-bottom: 35px;
   margin-bottom: 35px;
 `;
 `;
@@ -186,7 +193,7 @@ const Button = styled.div`
   justify-content: space-between;
   justify-content: space-between;
   font-size: 13px;
   font-size: 13px;
   cursor: pointer;
   cursor: pointer;
-  font-family: 'Work Sans', sans-serif;
+  font-family: "Work Sans", sans-serif;
   border-radius: 20px;
   border-radius: 20px;
   color: white;
   color: white;
   height: 35px;
   height: 35px;
@@ -199,11 +206,14 @@ const Button = styled.div`
   white-space: nowrap;
   white-space: nowrap;
   text-overflow: ellipsis;
   text-overflow: ellipsis;
   box-shadow: 0 5px 8px 0px #00000010;
   box-shadow: 0 5px 8px 0px #00000010;
-  cursor: ${(props: { disabled?: boolean }) => props.disabled ? 'not-allowed' : 'pointer'};
+  cursor: ${(props: { disabled?: boolean }) =>
+    props.disabled ? "not-allowed" : "pointer"};
 
 
-  background: ${(props: { disabled?: boolean }) => props.disabled ? '#aaaabbee' : '#616FEEcc'};
+  background: ${(props: { disabled?: boolean }) =>
+    props.disabled ? "#aaaabbee" : "#616FEEcc"};
   :hover {
   :hover {
-    background: ${(props: { disabled?: boolean }) => props.disabled ? '' : '#505edddd'};
+    background: ${(props: { disabled?: boolean }) =>
+      props.disabled ? "" : "#505edddd"};
   }
   }
 
 
   > i {
   > i {
@@ -223,7 +233,7 @@ const Button = styled.div`
 const ButtonAlt = styled(Button)`
 const ButtonAlt = styled(Button)`
   min-width: 150px;
   min-width: 150px;
   max-width: 150px;
   max-width: 150px;
-  background: #7A838Fdd;
+  background: #7a838fdd;
 
 
   :hover {
   :hover {
     background: #69727eee;
     background: #69727eee;
@@ -250,7 +260,7 @@ const Overlay = styled.div`
   justify-content: center;
   justify-content: center;
   font-size: 24px;
   font-size: 24px;
   font-weight: 500;
   font-weight: 500;
-  font-family: 'Work Sans', sans-serif;
+  font-family: "Work Sans", sans-serif;
   color: white;
   color: white;
 `;
 `;
 
 
@@ -269,7 +279,7 @@ const DashboardIcon = styled.div`
   display: flex;
   display: flex;
   align-items: center;
   align-items: center;
   justify-content: center;
   justify-content: center;
-  background: #676C7C;
+  background: #676c7c;
   border: 2px solid #8e94aa;
   border: 2px solid #8e94aa;
 
 
   > i {
   > i {
@@ -280,7 +290,7 @@ const DashboardIcon = styled.div`
 const Title = styled.div`
 const Title = styled.div`
   font-size: 20px;
   font-size: 20px;
   font-weight: 500;
   font-weight: 500;
-  font-family: 'Work Sans', sans-serif;
+  font-family: "Work Sans", sans-serif;
   margin-left: 18px;
   margin-left: 18px;
   color: #ffffff;
   color: #ffffff;
   white-space: nowrap;
   white-space: nowrap;
@@ -315,4 +325,4 @@ const SortFilterWrapper = styled.div`
   width: 468px;
   width: 468px;
   display: flex;
   display: flex;
   justify-content: space-between;
   justify-content: space-between;
-`;
+`;

+ 42 - 30
dashboard/src/main/home/cluster-dashboard/NamespaceSelector.tsx

@@ -1,46 +1,58 @@
-import React, { Component } from 'react';
-import styled from 'styled-components';
+import React, { Component } from "react";
+import styled from "styled-components";
 
 
-import { Context } from 'shared/Context';
-import api from 'shared/api';
+import { Context } from "shared/Context";
+import api from "shared/api";
 
 
-import Selector from 'components/Selector';
+import Selector from "components/Selector";
 
 
 type PropsType = {
 type PropsType = {
-  setNamespace: (x: string) => void,
-  namespace: string
+  setNamespace: (x: string) => void;
+  namespace: string;
 };
 };
 
 
 type StateType = {
 type StateType = {
-  namespaceOptions: { label: string, value: string }[]
+  namespaceOptions: { label: string; value: string }[];
 };
 };
 
 
-// TODO: fix update to unmounted component 
+// TODO: fix update to unmounted component
 export default class NamespaceSelector extends Component<PropsType, StateType> {
 export default class NamespaceSelector extends Component<PropsType, StateType> {
   _isMounted = false;
   _isMounted = false;
 
 
   state = {
   state = {
-    namespaceOptions: [] as { label: string, value: string }[]
-  }
+    namespaceOptions: [] as { label: string; value: string }[],
+  };
 
 
   updateOptions = () => {
   updateOptions = () => {
     let { currentCluster, currentProject } = this.context;
     let { currentCluster, currentProject } = this.context;
 
 
-    api.getNamespaces('<token>', {
-      cluster_id: currentCluster.id,
-    }, { id: currentProject.id }, (err: any, res: any) => {
-      if (err && this._isMounted) {
-        // setCurrentError('Could not read clusters: ' + JSON.stringify(err));
-        this.setState({ namespaceOptions: [{ label: 'All', value: '' }] });
-      } else if (this._isMounted) {
-        let namespaceOptions: { label: string, value: string }[] = [{ label: 'All', value: '' }];
-        res.data.items.forEach((x: { metadata: { name: string }}, i: number) => {
-          namespaceOptions.push({ label: x.metadata.name, value: x.metadata.name });
-        })
-        this.setState({ namespaceOptions });
+    api.getNamespaces(
+      "<token>",
+      {
+        cluster_id: currentCluster.id,
+      },
+      { id: currentProject.id },
+      (err: any, res: any) => {
+        if (err && this._isMounted) {
+          // setCurrentError('Could not read clusters: ' + JSON.stringify(err));
+          this.setState({ namespaceOptions: [{ label: "All", value: "" }] });
+        } else if (this._isMounted) {
+          let namespaceOptions: { label: string; value: string }[] = [
+            { label: "All", value: "" },
+          ];
+          res.data.items.forEach(
+            (x: { metadata: { name: string } }, i: number) => {
+              namespaceOptions.push({
+                label: x.metadata.name,
+                value: x.metadata.name,
+              });
+            }
+          );
+          this.setState({ namespaceOptions });
+        }
       }
       }
-    });
-  }
+    );
+  };
 
 
   componentDidMount() {
   componentDidMount() {
     this._isMounted = true;
     this._isMounted = true;
@@ -58,7 +70,7 @@ export default class NamespaceSelector extends Component<PropsType, StateType> {
   }
   }
 
 
   render() {
   render() {
-    return ( 
+    return (
       <StyledNamespaceSelector>
       <StyledNamespaceSelector>
         <Label>
         <Label>
           <i className="material-icons">filter_alt</i> Filter
           <i className="material-icons">filter_alt</i> Filter
@@ -67,9 +79,9 @@ export default class NamespaceSelector extends Component<PropsType, StateType> {
           activeValue={this.props.namespace}
           activeValue={this.props.namespace}
           setActiveValue={(namespace) => this.props.setNamespace(namespace)}
           setActiveValue={(namespace) => this.props.setNamespace(namespace)}
           options={this.state.namespaceOptions}
           options={this.state.namespaceOptions}
-          dropdownLabel='Namespace'
-          width='150px'
-          dropdownWidth='230px'
+          dropdownLabel="Namespace"
+          width="150px"
+          dropdownWidth="230px"
           closeOverlay={true}
           closeOverlay={true}
         />
         />
       </StyledNamespaceSelector>
       </StyledNamespaceSelector>
@@ -94,4 +106,4 @@ const StyledNamespaceSelector = styled.div`
   display: flex;
   display: flex;
   align-items: center;
   align-items: center;
   font-size: 13px;
   font-size: 13px;
-`;
+`;

+ 18 - 18
dashboard/src/main/home/cluster-dashboard/SortSelector.tsx

@@ -1,31 +1,31 @@
-import React, { Component } from 'react';
-import styled from 'styled-components';
+import React, { Component } from "react";
+import styled from "styled-components";
 
 
-import { Context } from 'shared/Context';
+import { Context } from "shared/Context";
 
 
-import Selector from 'components/Selector';
+import Selector from "components/Selector";
 
 
 type PropsType = {
 type PropsType = {
-  setSortType: (x: string) => void,
-  sortType: string
+  setSortType: (x: string) => void;
+  sortType: string;
 };
 };
 
 
 type StateType = {
 type StateType = {
-  sortOptions: { label: string, value: string }[]
+  sortOptions: { label: string; value: string }[];
 };
 };
 
 
-// TODO: fix update to unmounted component 
+// TODO: fix update to unmounted component
 export default class SortSelector extends Component<PropsType, StateType> {
 export default class SortSelector extends Component<PropsType, StateType> {
   state = {
   state = {
     sortOptions: [
     sortOptions: [
-      { label: 'Newest', value: 'Newest' },
-      { label: 'Oldest', value: 'Oldest' },
-      { label: 'Alphabetical', value: 'Alphabetical' }
-    ] as {label: string, value: string}[]
-  }
+      { label: "Newest", value: "Newest" },
+      { label: "Oldest", value: "Oldest" },
+      { label: "Alphabetical", value: "Alphabetical" },
+    ] as { label: string; value: string }[],
+  };
 
 
   render() {
   render() {
-    return ( 
+    return (
       <StyledSortSelector>
       <StyledSortSelector>
         <Label>
         <Label>
           <i className="material-icons">sort</i> Sort
           <i className="material-icons">sort</i> Sort
@@ -34,9 +34,9 @@ export default class SortSelector extends Component<PropsType, StateType> {
           activeValue={this.props.sortType}
           activeValue={this.props.sortType}
           setActiveValue={(sortType) => this.props.setSortType(sortType)}
           setActiveValue={(sortType) => this.props.setSortType(sortType)}
           options={this.state.sortOptions}
           options={this.state.sortOptions}
-          dropdownLabel='Sort By'
-          width='150px'
-          dropdownWidth='230px'
+          dropdownLabel="Sort By"
+          width="150px"
+          dropdownWidth="230px"
           closeOverlay={true}
           closeOverlay={true}
         />
         />
       </StyledSortSelector>
       </StyledSortSelector>
@@ -61,4 +61,4 @@ const StyledSortSelector = styled.div`
   display: flex;
   display: flex;
   align-items: center;
   align-items: center;
   font-size: 13px;
   font-size: 13px;
-`;
+`;

+ 40 - 38
dashboard/src/main/home/cluster-dashboard/chart/Chart.tsx

@@ -1,48 +1,51 @@
-import React, { Component } from 'react';
-import styled from 'styled-components';
+import React, { Component } from "react";
+import styled from "styled-components";
 
 
-import { ChartType, StorageType } from '../../../../shared/types';
-import { Context } from '../../../../shared/Context';
-import StatusIndicator from '../../../../components/StatusIndicator';
+import { ChartType, StorageType } from "shared/types";
+import { Context } from "shared/Context";
+import StatusIndicator from "components/StatusIndicator";
 
 
 type PropsType = {
 type PropsType = {
-  chart: ChartType,
-  setCurrentChart: (c: ChartType) => void,
-  controllers: Record<string, any>,
+  chart: ChartType;
+  setCurrentChart: (c: ChartType) => void;
+  controllers: Record<string, any>;
 };
 };
 
 
 type StateType = {
 type StateType = {
-  expand: boolean,
-  update: any[],
+  expand: boolean;
+  update: any[];
 };
 };
 
 
 export default class Chart extends Component<PropsType, StateType> {
 export default class Chart extends Component<PropsType, StateType> {
   state = {
   state = {
     expand: false,
     expand: false,
     update: [] as any[],
     update: [] as any[],
-  }
+  };
 
 
   renderIcon = () => {
   renderIcon = () => {
     let { chart } = this.props;
     let { chart } = this.props;
 
 
-    if (chart.chart.metadata.icon && chart.chart.metadata.icon !== '') {
-      return <Icon src={chart.chart.metadata.icon} />
+    if (chart.chart.metadata.icon && chart.chart.metadata.icon !== "") {
+      return <Icon src={chart.chart.metadata.icon} />;
     } else {
     } else {
-      return <i className="material-icons">tonality</i>
+      return <i className="material-icons">tonality</i>;
     }
     }
-  }
+  };
 
 
   readableDate = (s: string) => {
   readableDate = (s: string) => {
     let ts = new Date(s);
     let ts = new Date(s);
     let date = ts.toLocaleDateString();
     let date = ts.toLocaleDateString();
-    let time = ts.toLocaleTimeString([], { hour: 'numeric', minute: '2-digit' });
+    let time = ts.toLocaleTimeString([], {
+      hour: "numeric",
+      minute: "2-digit",
+    });
     return `${time} on ${date}`;
     return `${time} on ${date}`;
-  }
+  };
 
 
   render() {
   render() {
     let { chart, setCurrentChart } = this.props;
     let { chart, setCurrentChart } = this.props;
 
 
-    return ( 
+    return (
       <StyledChart
       <StyledChart
         onMouseEnter={() => this.setState({ expand: true })}
         onMouseEnter={() => this.setState({ expand: true })}
         onMouseLeave={() => this.setState({ expand: false })}
         onMouseLeave={() => this.setState({ expand: false })}
@@ -50,29 +53,26 @@ export default class Chart extends Component<PropsType, StateType> {
         onClick={() => setCurrentChart(chart)}
         onClick={() => setCurrentChart(chart)}
       >
       >
         <Title>
         <Title>
-          <IconWrapper>
-            {this.renderIcon()}
-          </IconWrapper>
+          <IconWrapper>{this.renderIcon()}</IconWrapper>
           {chart.name}
           {chart.name}
         </Title>
         </Title>
 
 
         <BottomWrapper>
         <BottomWrapper>
           <InfoWrapper>
           <InfoWrapper>
             <StatusIndicator
             <StatusIndicator
-              controllers={this.props.controllers} 
+              controllers={this.props.controllers}
               status={chart.info.status}
               status={chart.info.status}
-              margin_left={'17px'}
+              margin_left={"17px"}
             />
             />
             <LastDeployed>
             <LastDeployed>
-              <Dot>•</Dot> Last deployed {this.readableDate(chart.info.last_deployed)}
+              <Dot>•</Dot> Last deployed{" "}
+              {this.readableDate(chart.info.last_deployed)}
             </LastDeployed>
             </LastDeployed>
           </InfoWrapper>
           </InfoWrapper>
 
 
           <TagWrapper>
           <TagWrapper>
             Namespace
             Namespace
-            <NamespaceTag>
-              {chart.namespace}
-            </NamespaceTag>
+            <NamespaceTag>{chart.namespace}</NamespaceTag>
           </TagWrapper>
           </TagWrapper>
         </BottomWrapper>
         </BottomWrapper>
 
 
@@ -182,7 +182,7 @@ const Title = styled.div`
   text-decoration: none;
   text-decoration: none;
   padding: 12px 35px 12px 45px;
   padding: 12px 35px 12px 45px;
   font-size: 14px;
   font-size: 14px;
-  font-family: 'Work Sans', sans-serif;
+  font-family: "Work Sans", sans-serif;
   font-weight: 500;
   font-weight: 500;
   color: #ffffff;
   color: #ffffff;
   width: 80%;
   width: 80%;
@@ -191,7 +191,7 @@ const Title = styled.div`
   text-overflow: ellipsis;
   text-overflow: ellipsis;
   animation: fadeIn 0.5s;
   animation: fadeIn 0.5s;
 
 
-  >img {
+  > img {
     background: none;
     background: none;
     top: 12px;
     top: 12px;
     left: 13px;
     left: 13px;
@@ -210,17 +210,19 @@ const StyledChart = styled.div`
   border-radius: 5px;
   border-radius: 5px;
   box-shadow: 0 5px 8px 0px #00000033;
   box-shadow: 0 5px 8px 0px #00000033;
   position: relative;
   position: relative;
-  border: 2px solid #9EB4FF00;
+  border: 2px solid #9eb4ff00;
   width: calc(100% + 2px);
   width: calc(100% + 2px);
   height: calc(100% + 2px);
   height: calc(100% + 2px);
 
 
-  animation: ${(props: { expand: boolean }) => props.expand ? 'expand' : 'shrink'} 0.12s;
+  animation: ${(props: { expand: boolean }) =>
+      props.expand ? "expand" : "shrink"}
+    0.12s;
   animation-fill-mode: forwards;
   animation-fill-mode: forwards;
   animation-timing-function: ease-out;
   animation-timing-function: ease-out;
 
 
   @keyframes expand {
   @keyframes expand {
-    from { 
-      width: calc(100% + 2px); 
+    from {
+      width: calc(100% + 2px);
       padding-top: 4px;
       padding-top: 4px;
       padding-bottom: 14px;
       padding-bottom: 14px;
       margin-left: 0px;
       margin-left: 0px;
@@ -233,7 +235,7 @@ const StyledChart = styled.div`
       width: calc(100% + 22px);
       width: calc(100% + 22px);
       padding-top: 7px;
       padding-top: 7px;
       padding-bottom: 20px;
       padding-bottom: 20px;
-      margin-left: -10px; 
+      margin-left: -10px;
       box-shadow: 0 8px 20px 0px #00000030;
       box-shadow: 0 8px 20px 0px #00000030;
       padding-left: 5px;
       padding-left: 5px;
       margin-bottom: 21px;
       margin-bottom: 21px;
@@ -242,21 +244,21 @@ const StyledChart = styled.div`
   }
   }
 
 
   @keyframes shrink {
   @keyframes shrink {
-    from { 
+    from {
       width: calc(100% + 22px);
       width: calc(100% + 22px);
       padding-top: 7px;
       padding-top: 7px;
       padding-bottom: 20px;
       padding-bottom: 20px;
-      margin-left: -10px; 
+      margin-left: -10px;
       box-shadow: 0 8px 20px 0px #00000030;
       box-shadow: 0 8px 20px 0px #00000030;
       padding-left: 5px;
       padding-left: 5px;
       margin-bottom: 21px;
       margin-bottom: 21px;
       margin-top: -4px;
       margin-top: -4px;
     }
     }
     to {
     to {
-      width: calc(100% + 2px); 
+      width: calc(100% + 2px);
       padding-top: 4px;
       padding-top: 4px;
       padding-bottom: 14px;
       padding-bottom: 14px;
-      margin-left: 0px; 
+      margin-left: 0px;
       box-shadow: 0 5px 8px 0px #00000033;
       box-shadow: 0 5px 8px 0px #00000033;
       padding-left: 1px;
       padding-left: 1px;
       margin-bottom: 25px;
       margin-bottom: 25px;

+ 186 - 144
dashboard/src/main/home/cluster-dashboard/chart/ChartList.tsx

@@ -1,27 +1,27 @@
-import React, { Component } from 'react';
-import styled from 'styled-components';
+import React, { Component } from "react";
+import styled from "styled-components";
 
 
-import { Context } from '../../../../shared/Context';
-import api from '../../../../shared/api';
-import { ChartType, StorageType, ClusterType } from '../../../../shared/types';
+import { Context } from "shared/Context";
+import api from "shared/api";
+import { ChartType, StorageType, ClusterType } from "shared/types";
 
 
-import Chart from './Chart';
-import Loading from '../../../../components/Loading';
+import Chart from "./Chart";
+import Loading from "components/Loading";
 
 
 type PropsType = {
 type PropsType = {
-  currentCluster: ClusterType,
-  namespace: string,
-  sortType: string,
-  setCurrentChart: (c: ChartType) => void
+  currentCluster: ClusterType;
+  namespace: string;
+  sortType: string;
+  setCurrentChart: (c: ChartType) => void;
 };
 };
 
 
 type StateType = {
 type StateType = {
-  charts: ChartType[],
-  chartLookupTable: Record<string, string>,
-  controllers: Record<string, Record<string, any>>,
-  loading: boolean,
-  error: boolean,
-  websockets: Record<string, any>,
+  charts: ChartType[];
+  chartLookupTable: Record<string, string>;
+  controllers: Record<string, Record<string, any>>;
+  loading: boolean;
+  error: boolean;
+  websockets: Record<string, any>;
 };
 };
 
 
 export default class ChartList extends Component<PropsType, StateType> {
 export default class ChartList extends Component<PropsType, StateType> {
@@ -31,161 +31,200 @@ export default class ChartList extends Component<PropsType, StateType> {
     controllers: {} as Record<string, Record<string, any>>,
     controllers: {} as Record<string, Record<string, any>>,
     loading: false,
     loading: false,
     error: false,
     error: false,
-    websockets : {} as Record<string, any>,
-  }
+    websockets: {} as Record<string, any>,
+  };
 
 
   updateCharts = (callback: Function) => {
   updateCharts = (callback: Function) => {
     let { currentCluster, currentProject, setCurrentError } = this.context;
     let { currentCluster, currentProject, setCurrentError } = this.context;
     this.setState({ loading: true });
     this.setState({ loading: true });
 
 
-    api.getCharts('<token>', {
-      namespace: this.props.namespace,
-      cluster_id: currentCluster.id,
-      storage: StorageType.Secret,
-      limit: 20,
-      skip: 0,
-      byDate: false,
-      statusFilter: ['deployed', 'uninstalled', 'pending', 'pending_upgrade',
-        'pending_rollback','superseded','failed']
-    }, { id: currentProject.id }, (err: any, res: any) => {
-      if (err) {
-        console.log(err)
-        setCurrentError(JSON.stringify(err));
-        this.setState({ loading: false, error: true });
-      } else {
-        let charts = res.data || [];
-        if (this.props.sortType == "Newest") {
-          charts.sort((a: any, b: any) => (Date.parse(a.info.last_deployed) > Date.parse(b.info.last_deployed)) ? -1 : 1);
-        } else if (this.props.sortType == "Oldest") {
-          charts.sort((a: any, b: any) => (Date.parse(a.info.last_deployed) > Date.parse(b.info.last_deployed)) ? 1 : -1);
-        } else if (this.props.sortType == "Alphabetical") {
-          charts.sort((a: any, b: any) => (a.name > b.name) ? 1 : -1);
+    api.getCharts(
+      "<token>",
+      {
+        namespace: this.props.namespace,
+        cluster_id: currentCluster.id,
+        storage: StorageType.Secret,
+        limit: 20,
+        skip: 0,
+        byDate: false,
+        statusFilter: [
+          "deployed",
+          "uninstalled",
+          "pending",
+          "pending_upgrade",
+          "pending_rollback",
+          "superseded",
+          "failed",
+        ],
+      },
+      { id: currentProject.id },
+      (err: any, res: any) => {
+        if (err) {
+          console.log(err);
+          setCurrentError(JSON.stringify(err));
+          this.setState({ loading: false, error: true });
+        } else {
+          let charts = res.data || [];
+          if (this.props.sortType == "Newest") {
+            charts.sort((a: any, b: any) =>
+              Date.parse(a.info.last_deployed) >
+              Date.parse(b.info.last_deployed)
+                ? -1
+                : 1
+            );
+          } else if (this.props.sortType == "Oldest") {
+            charts.sort((a: any, b: any) =>
+              Date.parse(a.info.last_deployed) >
+              Date.parse(b.info.last_deployed)
+                ? 1
+                : -1
+            );
+          } else if (this.props.sortType == "Alphabetical") {
+            charts.sort((a: any, b: any) => (a.name > b.name ? 1 : -1));
+          }
+          this.setState({ charts }, () => {
+            this.setState({ loading: false, error: false });
+          });
+          callback(charts);
         }
         }
-        this.setState({ charts }, () => {
-          this.setState({ loading: false, error: false });
-        });
-        callback(charts)
       }
       }
-    });
-  }
+    );
+  };
 
 
   setupWebsocket = (kind: string) => {
   setupWebsocket = (kind: string) => {
-      let { currentCluster, currentProject } = this.context;
-      let protocol = process.env.NODE_ENV == 'production' ? 'wss' : 'ws';
-      let ws = new WebSocket(`${protocol}://${process.env.API_SERVER}/api/projects/${currentProject.id}/k8s/${kind}/status?cluster_id=${currentCluster.id}`);
-      ws.onopen = () => {
-        console.log('connected to websocket');
-      }
-  
-      ws.onmessage = (evt: MessageEvent) => {
-        let event = JSON.parse(evt.data);
-        let object = event.Object;
-        object.metadata.kind = event.Kind
-        let chartKey = this.state.chartLookupTable[object.metadata.uid];
-
-        // ignore if updated object does not belong to any chart in the list.
-        if (!chartKey) {
-          return;
-        }
+    let { currentCluster, currentProject } = this.context;
+    let protocol = process.env.NODE_ENV == "production" ? "wss" : "ws";
+    let ws = new WebSocket(
+      `${protocol}://${process.env.API_SERVER}/api/projects/${currentProject.id}/k8s/${kind}/status?cluster_id=${currentCluster.id}`
+    );
+    ws.onopen = () => {
+      console.log("connected to websocket");
+    };
 
 
-        let chartControllers = this.state.controllers[chartKey];
-        chartControllers[object.metadata.uid] = object;
+    ws.onmessage = (evt: MessageEvent) => {
+      let event = JSON.parse(evt.data);
+      let object = event.Object;
+      object.metadata.kind = event.Kind;
+      let chartKey = this.state.chartLookupTable[object.metadata.uid];
 
 
-        this.setState({
-          controllers: {
-            ...this.state.controllers,
-            [chartKey] : chartControllers
-          }
-        });
-      }
-  
-      ws.onclose = () => {
-        console.log('closing websocket');
-      }
-  
-      ws.onerror = (err: ErrorEvent) => {
-        console.log(err);
-        ws.close();
+      // ignore if updated object does not belong to any chart in the list.
+      if (!chartKey) {
+        return;
       }
       }
 
 
-      return ws;
-  }
+      let chartControllers = this.state.controllers[chartKey];
+      chartControllers[object.metadata.uid] = object;
+
+      this.setState({
+        controllers: {
+          ...this.state.controllers,
+          [chartKey]: chartControllers,
+        },
+      });
+    };
+
+    ws.onclose = () => {
+      console.log("closing websocket");
+    };
+
+    ws.onerror = (err: ErrorEvent) => {
+      console.log(err);
+      ws.close();
+    };
+
+    return ws;
+  };
 
 
   setControllerWebsockets = (controllers: any[]) => {
   setControllerWebsockets = (controllers: any[]) => {
     let websockets = controllers.map((kind: string) => {
     let websockets = controllers.map((kind: string) => {
       return this.setupWebsocket(kind);
       return this.setupWebsocket(kind);
-    })
+    });
     this.setState({ websockets });
     this.setState({ websockets });
-  }
+  };
 
 
   getControllers = (charts: any[]) => {
   getControllers = (charts: any[]) => {
     let { currentCluster, currentProject, setCurrentError } = this.context;
     let { currentCluster, currentProject, setCurrentError } = this.context;
 
 
     charts.forEach(async (chart: any) => {
     charts.forEach(async (chart: any) => {
       // don't retrieve controllers for chart that failed to even deploy.
       // don't retrieve controllers for chart that failed to even deploy.
-      if (chart.info.status == 'failed') return;
+      if (chart.info.status == "failed") return;
 
 
       await new Promise((next: (res?: any) => void) => {
       await new Promise((next: (res?: any) => void) => {
-        api.getChartControllers('<token>', {
-          namespace: chart.namespace,
-          cluster_id: currentCluster.id,
-          storage: StorageType.Secret
-        }, {
-          id: currentProject.id,
-          name: chart.name,
-          revision: chart.version
-        }, (err: any, res: any) => {
-          if (err) {
-            setCurrentError(JSON.stringify(err));
-            return
+        api.getChartControllers(
+          "<token>",
+          {
+            namespace: chart.namespace,
+            cluster_id: currentCluster.id,
+            storage: StorageType.Secret,
+          },
+          {
+            id: currentProject.id,
+            name: chart.name,
+            revision: chart.version,
+          },
+          (err: any, res: any) => {
+            if (err) {
+              setCurrentError(JSON.stringify(err));
+              return;
+            }
+            // transform controller array into hash table for easy lookup during updates.
+            let chartControllers = {} as Record<string, Record<string, any>>;
+            res.data.forEach((c: any) => {
+              c.metadata.kind = c.kind;
+              chartControllers[c.metadata.uid] = c;
+            });
+
+            res.data.forEach(async (c: any) => {
+              await new Promise((nextController: (res?: any) => void) => {
+                this.setState(
+                  {
+                    chartLookupTable: {
+                      ...this.state.chartLookupTable,
+                      [c.metadata.uid]: `${chart.namespace}-${chart.name}`,
+                    },
+                    controllers: {
+                      ...this.state.controllers,
+                      [`${chart.namespace}-${chart.name}`]: chartControllers,
+                    },
+                  },
+                  () => {
+                    nextController();
+                  }
+                );
+              });
+            });
+            next();
           }
           }
-          // transform controller array into hash table for easy lookup during updates.
-          let chartControllers = {} as Record<string, Record<string, any>>
-          res.data.forEach((c: any) => {
-            c.metadata.kind = c.kind
-            chartControllers[c.metadata.uid] = c
-          })
-
-          res.data.forEach(async (c: any) => {
-            await new Promise((nextController: (res?: any) => void) => {
-              this.setState({
-                chartLookupTable: {
-                  ...this.state.chartLookupTable,
-                  [c.metadata.uid] : `${chart.namespace}-${chart.name}`
-                },
-                controllers: {
-                  ...this.state.controllers,
-                  [`${chart.namespace}-${chart.name}`] : chartControllers
-                }
-              }, () => {
-                nextController();
-              })
-            })
-          })
-          next();
-        });
-      })
-    })
-  }
+        );
+      });
+    });
+  };
 
 
   componentDidMount() {
   componentDidMount() {
     this.updateCharts(this.getControllers);
     this.updateCharts(this.getControllers);
-    this.setControllerWebsockets(["deployment", "statefulset", "daemonset", "replicaset"]);
+    this.setControllerWebsockets([
+      "deployment",
+      "statefulset",
+      "daemonset",
+      "replicaset",
+    ]);
   }
   }
 
 
   componentWillUnmount() {
   componentWillUnmount() {
     if (this.state.websockets) {
     if (this.state.websockets) {
       this.state.websockets.forEach((ws: WebSocket) => {
       this.state.websockets.forEach((ws: WebSocket) => {
-        ws.close()
-      })
+        ws.close();
+      });
     }
     }
   }
   }
 
 
   componentDidUpdate(prevProps: PropsType) {
   componentDidUpdate(prevProps: PropsType) {
     // Ret2: Prevents reload when opening ClusterConfigModal
     // Ret2: Prevents reload when opening ClusterConfigModal
-    if (prevProps.currentCluster !== this.props.currentCluster || 
+    if (
+      prevProps.currentCluster !== this.props.currentCluster ||
       prevProps.namespace !== this.props.namespace ||
       prevProps.namespace !== this.props.namespace ||
-      prevProps.sortType !== this.props.sortType) {
+      prevProps.sortType !== this.props.sortType
+    ) {
       this.updateCharts(this.getControllers);
       this.updateCharts(this.getControllers);
     }
     }
   }
   }
@@ -194,7 +233,11 @@ export default class ChartList extends Component<PropsType, StateType> {
     let { loading, error, charts } = this.state;
     let { loading, error, charts } = this.state;
 
 
     if (loading) {
     if (loading) {
-      return <LoadingWrapper><Loading /></LoadingWrapper>
+      return (
+        <LoadingWrapper>
+          <Loading />
+        </LoadingWrapper>
+      );
     } else if (error) {
     } else if (error) {
       return (
       return (
         <Placeholder>
         <Placeholder>
@@ -204,7 +247,8 @@ export default class ChartList extends Component<PropsType, StateType> {
     } else if (charts.length === 0) {
     } else if (charts.length === 0) {
       return (
       return (
         <Placeholder>
         <Placeholder>
-          <i className="material-icons">category</i> No charts found in this namespace.
+          <i className="material-icons">category</i> No charts found in this
+          namespace.
         </Placeholder>
         </Placeholder>
       );
       );
     }
     }
@@ -215,19 +259,17 @@ export default class ChartList extends Component<PropsType, StateType> {
           key={`${chart.namespace}-${chart.name}`}
           key={`${chart.namespace}-${chart.name}`}
           chart={chart}
           chart={chart}
           setCurrentChart={this.props.setCurrentChart}
           setCurrentChart={this.props.setCurrentChart}
-          controllers={this.state.controllers[`${chart.namespace}-${chart.name}`] || {} as Record<string, any>}
+          controllers={
+            this.state.controllers[`${chart.namespace}-${chart.name}`] ||
+            ({} as Record<string, any>)
+          }
         />
         />
-      )
-    })
-  }
-
+      );
+    });
+  };
 
 
   render() {
   render() {
-    return (
-      <StyledChartList>
-        {this.renderChartList()}
-      </StyledChartList>
-    );
+    return <StyledChartList>{this.renderChartList()}</StyledChartList>;
   }
   }
 }
 }
 
 
@@ -260,4 +302,4 @@ const LoadingWrapper = styled.div`
 
 
 const StyledChartList = styled.div`
 const StyledChartList = styled.div`
   padding-bottom: 85px;
   padding-bottom: 85px;
-`;
+`;

Разница между файлами не показана из-за своего большого размера
+ 385 - 309
dashboard/src/main/home/cluster-dashboard/expanded-chart/ExpandedChart.tsx


+ 16 - 20
dashboard/src/main/home/cluster-dashboard/expanded-chart/GraphSection.tsx

@@ -1,27 +1,27 @@
-import React, { Component } from 'react';
-import styled from 'styled-components';
+import React, { Component } from "react";
+import styled from "styled-components";
 
 
-import { Context } from 'shared/Context';
-import { ResourceType, ChartType } from 'shared/types';
+import { Context } from "shared/Context";
+import { ResourceType, ChartType } from "shared/types";
 
 
-import GraphDisplay from './graph/GraphDisplay';
-import Loading from 'components/Loading';
+import GraphDisplay from "./graph/GraphDisplay";
+import Loading from "components/Loading";
 
 
 type PropsType = {
 type PropsType = {
-  components: ResourceType[],
-  currentChart: ChartType,
-  setSidebar: (x: boolean) => void,
-  showRevisions: boolean
+  components: ResourceType[];
+  currentChart: ChartType;
+  setSidebar: (x: boolean) => void;
+  showRevisions: boolean;
 };
 };
 
 
 type StateType = {
 type StateType = {
-  isExpanded: boolean
+  isExpanded: boolean;
 };
 };
 
 
 export default class GraphSection extends Component<PropsType, StateType> {
 export default class GraphSection extends Component<PropsType, StateType> {
   state = {
   state = {
-    isExpanded: false
-  }
+    isExpanded: false,
+  };
 
 
   renderContents = () => {
   renderContents = () => {
     if (this.props.components && this.props.components.length > 0) {
     if (this.props.components && this.props.components.length > 0) {
@@ -36,15 +36,11 @@ export default class GraphSection extends Component<PropsType, StateType> {
       );
       );
     }
     }
 
 
-    return <Loading offset='-30px' />;
-  }
+    return <Loading offset="-30px" />;
+  };
 
 
   render() {
   render() {
-    return (
-      <StyledGraphSection>
-        {this.renderContents()}
-      </StyledGraphSection>
-    );
+    return <StyledGraphSection>{this.renderContents()}</StyledGraphSection>;
   }
   }
 }
 }
 
 

+ 44 - 38
dashboard/src/main/home/cluster-dashboard/expanded-chart/ListSection.tsx

@@ -1,34 +1,34 @@
-import React, { Component } from 'react';
-import styled from 'styled-components';
-import yaml from 'js-yaml';
+import React, { Component } from "react";
+import styled from "styled-components";
+import yaml from "js-yaml";
 
 
-import { Context } from 'shared/Context';
-import { ResourceType, ChartType } from 'shared/types';
+import { Context } from "shared/Context";
+import { ResourceType, ChartType } from "shared/types";
 
 
-import Loading from 'components/Loading';
-import ResourceTab from 'components/ResourceTab';
-import YamlEditor from 'components/YamlEditor';
+import Loading from "components/Loading";
+import ResourceTab from "components/ResourceTab";
+import YamlEditor from "components/YamlEditor";
 
 
 type PropsType = {
 type PropsType = {
-  currentChart: ChartType,
-  components: ResourceType[],
-  showRevisions: boolean,
+  currentChart: ChartType;
+  components: ResourceType[];
+  showRevisions: boolean;
 };
 };
 
 
 type StateType = {
 type StateType = {
-  showKindLabels: boolean,
-  yaml: string | null,
-  wrapperHeight: number,
-  selectedResource: { kind: string, name: string } | null,
+  showKindLabels: boolean;
+  yaml: string | null;
+  wrapperHeight: number;
+  selectedResource: { kind: string; name: string } | null;
 };
 };
 
 
 export default class ListSection extends Component<PropsType, StateType> {
 export default class ListSection extends Component<PropsType, StateType> {
   state = {
   state = {
     showKindLabels: true,
     showKindLabels: true,
-    yaml: '# Select a resource to view its manifest' as string | null,
+    yaml: "# Select a resource to view its manifest" as string | null,
     wrapperHeight: 0,
     wrapperHeight: 0,
-    selectedResource: null as { kind: string, name: string } | null,
-  }
+    selectedResource: null as { kind: string; name: string } | null,
+  };
 
 
   wrapperRef: any = React.createRef();
   wrapperRef: any = React.createRef();
 
 
@@ -37,23 +37,31 @@ export default class ListSection extends Component<PropsType, StateType> {
   }
   }
 
 
   componentDidUpdate(prevProps: PropsType) {
   componentDidUpdate(prevProps: PropsType) {
-
     // Adjust yaml wrapper height on revision toggle
     // Adjust yaml wrapper height on revision toggle
-    if ((prevProps.showRevisions !== this.props.showRevisions) && this.wrapperRef) {
+    if (
+      prevProps.showRevisions !== this.props.showRevisions &&
+      this.wrapperRef
+    ) {
       this.setState({ wrapperHeight: this.wrapperRef.offsetHeight });
       this.setState({ wrapperHeight: this.wrapperRef.offsetHeight });
     }
     }
 
 
-    if (prevProps.components !== this.props.components && this.state.selectedResource) {
+    if (
+      prevProps.components !== this.props.components &&
+      this.state.selectedResource
+    ) {
       let matchingResourceFound = false;
       let matchingResourceFound = false;
       this.props.components.forEach((resource: ResourceType) => {
       this.props.components.forEach((resource: ResourceType) => {
-        if (resource.Kind === this.state.selectedResource.kind && resource.Name === this.state.selectedResource.name) {
+        if (
+          resource.Kind === this.state.selectedResource.kind &&
+          resource.Name === this.state.selectedResource.name
+        ) {
           let rawYaml = yaml.dump(resource.RawYAML);
           let rawYaml = yaml.dump(resource.RawYAML);
           this.setState({ yaml: rawYaml });
           this.setState({ yaml: rawYaml });
           matchingResourceFound = true;
           matchingResourceFound = true;
         }
         }
       });
       });
       if (!matchingResourceFound) {
       if (!matchingResourceFound) {
-        this.setState({ yaml: '# Select a resource to view its manifest' });
+        this.setState({ yaml: "# Select a resource to view its manifest" });
       }
       }
     }
     }
   }
   }
@@ -64,10 +72,12 @@ export default class ListSection extends Component<PropsType, StateType> {
       return (
       return (
         <ResourceTab
         <ResourceTab
           key={i}
           key={i}
-          handleClick={() => this.setState({ 
-            yaml: rawYaml,
-            selectedResource: { kind: resource.Kind, name: resource.Name }
-          })}
+          handleClick={() =>
+            this.setState({
+              yaml: rawYaml,
+              selectedResource: { kind: resource.Kind, name: resource.Name },
+            })
+          }
           selected={this.state.yaml === rawYaml}
           selected={this.state.yaml === rawYaml}
           label={resource.Kind}
           label={resource.Kind}
           name={resource.Name}
           name={resource.Name}
@@ -75,30 +85,26 @@ export default class ListSection extends Component<PropsType, StateType> {
         />
         />
       );
       );
     });
     });
-  }
+  };
 
 
   renderTabs = () => {
   renderTabs = () => {
     if (this.props.components && this.props.components.length > 0) {
     if (this.props.components && this.props.components.length > 0) {
-      return (
-        <TabWrapper>
-          {this.renderResourceList()}
-        </TabWrapper>
-      );
+      return <TabWrapper>{this.renderResourceList()}</TabWrapper>;
     }
     }
 
 
-    return <Loading offset='-30px' />;
-  }
+    return <Loading offset="-30px" />;
+  };
 
 
   render() {
   render() {
     return (
     return (
       <StyledListSection>
       <StyledListSection>
         {this.renderTabs()}
         {this.renderTabs()}
-        <FlexWrapper ref={element => this.wrapperRef = element}>
+        <FlexWrapper ref={(element) => (this.wrapperRef = element)}>
           <YamlWrapper>
           <YamlWrapper>
             <YamlEditor
             <YamlEditor
               value={this.state.yaml}
               value={this.state.yaml}
               onChange={(e: any) => this.setState({ yaml: e })}
               onChange={(e: any) => this.setState({ yaml: e })}
-              height={this.state.wrapperHeight - 2 + 'px'}
+              height={this.state.wrapperHeight - 2 + "px"}
               border={true}
               border={true}
               readOnly={true}
               readOnly={true}
             />
             />
@@ -138,4 +144,4 @@ const StyledListSection = styled.div`
   font-size: 13px;
   font-size: 13px;
   border-radius: 5px;
   border-radius: 5px;
   overflow: hidden;
   overflow: hidden;
-`;
+`;

+ 140 - 95
dashboard/src/main/home/cluster-dashboard/expanded-chart/RevisionSection.tsx

@@ -1,57 +1,67 @@
-import React, { Component } from 'react';
-import styled from 'styled-components';
-import loading from 'assets/loading.gif';
+import React, { Component } from "react";
+import styled from "styled-components";
+import loading from "assets/loading.gif";
 
 
-import api from 'shared/api';
-import { Context } from 'shared/Context';
-import { ChartType, StorageType } from 'shared/types';
+import api from "shared/api";
+import { Context } from "shared/Context";
+import { ChartType, StorageType } from "shared/types";
 
 
-import ConfirmOverlay from 'components/ConfirmOverlay';
+import ConfirmOverlay from "components/ConfirmOverlay";
 
 
 type PropsType = {
 type PropsType = {
-  showRevisions: boolean,
-  toggleShowRevisions: () => void,
-  chart: ChartType,
-  refreshChart: () => void,
-  setRevision: (x: ChartType, isCurrent?: boolean) => void
-  forceRefreshRevisions: boolean,
-  refreshRevisionsOff: () => void,
-  status: string,
+  showRevisions: boolean;
+  toggleShowRevisions: () => void;
+  chart: ChartType;
+  refreshChart: () => void;
+  setRevision: (x: ChartType, isCurrent?: boolean) => void;
+  forceRefreshRevisions: boolean;
+  refreshRevisionsOff: () => void;
+  status: string;
 };
 };
 
 
 type StateType = {
 type StateType = {
-  revisions: ChartType[],
-  rollbackRevision: number | null,
-  loading: boolean,
-  maxVersion: number,
+  revisions: ChartType[];
+  rollbackRevision: number | null;
+  loading: boolean;
+  maxVersion: number;
 };
 };
 
 
 // TODO: handle refresh when new revision is generated from an old revision
 // TODO: handle refresh when new revision is generated from an old revision
 export default class RevisionSection extends Component<PropsType, StateType> {
 export default class RevisionSection extends Component<PropsType, StateType> {
   state = {
   state = {
     revisions: [] as ChartType[],
     revisions: [] as ChartType[],
-    rollbackRevision: null as (number | null),
+    rollbackRevision: null as number | null,
     loading: false,
     loading: false,
     maxVersion: 0, // Track most recent version even when previewing old revisions
     maxVersion: 0, // Track most recent version even when previewing old revisions
-  }
+  };
 
 
   refreshHistory = (callback?: () => void) => {
   refreshHistory = (callback?: () => void) => {
     let { chart } = this.props;
     let { chart } = this.props;
     let { currentCluster, currentProject } = this.context;
     let { currentCluster, currentProject } = this.context;
-    api.getRevisions('<token>', {
-      namespace: chart.namespace,
-      cluster_id: currentCluster.id,
-      storage: StorageType.Secret
-    }, { id: currentProject.id, name: chart.name }, (err: any, res: any) => {
-      if (err) {
-        console.log(err)
-      } else {
-        res.data.sort((a: ChartType, b: ChartType) => { return -(a.version - b.version) });
-        this.setState({ revisions: res.data, maxVersion: res.data[0].version });
-        callback && callback();
+    api.getRevisions(
+      "<token>",
+      {
+        namespace: chart.namespace,
+        cluster_id: currentCluster.id,
+        storage: StorageType.Secret,
+      },
+      { id: currentProject.id, name: chart.name },
+      (err: any, res: any) => {
+        if (err) {
+          console.log(err);
+        } else {
+          res.data.sort((a: ChartType, b: ChartType) => {
+            return -(a.version - b.version);
+          });
+          this.setState({
+            revisions: res.data,
+            maxVersion: res.data[0].version,
+          });
+          callback && callback();
+        }
       }
       }
-    });
-  }
+    );
+  };
 
 
   componentDidMount() {
   componentDidMount() {
     this.refreshHistory();
     this.refreshHistory();
@@ -74,9 +84,12 @@ export default class RevisionSection extends Component<PropsType, StateType> {
   readableDate = (s: string) => {
   readableDate = (s: string) => {
     let ts = new Date(s);
     let ts = new Date(s);
     let date = ts.toLocaleDateString();
     let date = ts.toLocaleDateString();
-    let time = ts.toLocaleTimeString([], { hour: 'numeric', minute: '2-digit' });
+    let time = ts.toLocaleTimeString([], {
+      hour: "numeric",
+      minute: "2-digit",
+    });
     return `${time} on ${date}`;
     return `${time} on ${date}`;
-  }
+  };
 
 
   handleRollback = () => {
   handleRollback = () => {
     let { setCurrentError, currentCluster, currentProject } = this.context;
     let { setCurrentError, currentCluster, currentProject } = this.context;
@@ -84,27 +97,32 @@ export default class RevisionSection extends Component<PropsType, StateType> {
     let revisionNumber = this.state.rollbackRevision;
     let revisionNumber = this.state.rollbackRevision;
     this.setState({ loading: true, rollbackRevision: null });
     this.setState({ loading: true, rollbackRevision: null });
 
 
-    api.rollbackChart('<token>', {
-      namespace: this.props.chart.namespace,
-      storage: StorageType.Secret,
-      revision: revisionNumber
-    }, {
-      id: currentProject.id,
-      name: this.props.chart.name,
-      cluster_id: currentCluster.id,
-    }, (err: any, res: any) => {
-      if (err) {
-        console.log(err);
-        setCurrentError(err.response.data);
-        this.setState({ loading: false });
-      } else {
-        this.setState({ loading: false });
-        this.refreshHistory(() => {
-          this.props.setRevision(this.state.revisions[0], true);
-        });
+    api.rollbackChart(
+      "<token>",
+      {
+        namespace: this.props.chart.namespace,
+        storage: StorageType.Secret,
+        revision: revisionNumber,
+      },
+      {
+        id: currentProject.id,
+        name: this.props.chart.name,
+        cluster_id: currentCluster.id,
+      },
+      (err: any, res: any) => {
+        if (err) {
+          console.log(err);
+          setCurrentError(err.response.data);
+          this.setState({ loading: false });
+        } else {
+          this.setState({ loading: false });
+          this.refreshHistory(() => {
+            this.props.setRevision(this.state.revisions[0], true);
+          });
+        }
       }
       }
-    });
-  }
+    );
+  };
 
 
   handleClickRevision = (revision: ChartType) => {
   handleClickRevision = (revision: ChartType) => {
     let isCurrent = revision.version === this.state.maxVersion;
     let isCurrent = revision.version === this.state.maxVersion;
@@ -113,21 +131,24 @@ export default class RevisionSection extends Component<PropsType, StateType> {
     } else {
     } else {
       this.props.setRevision(revision);
       this.props.setRevision(revision);
     }
     }
-  }
+  };
 
 
   renderStatus = (revision: ChartType) => {
   renderStatus = (revision: ChartType) => {
-    if (this.props.chart.version === revision.version && this.props.status == 'loading') {
+    if (
+      this.props.chart.version === revision.version &&
+      this.props.status == "loading"
+    ) {
       return (
       return (
         <div>
         <div>
           {this.props.status}
           {this.props.status}
-          <LoadingGif src={loading} revision={true}/>
+          <LoadingGif src={loading} revision={true} />
         </div>
         </div>
-      )
+      );
     } else if (this.props.chart.version === revision.version) {
     } else if (this.props.chart.version === revision.version) {
-      return this.props.status        
+      return this.props.status;
     }
     }
-    return revision.info.status    
-  }
+    return revision.info.status;
+  };
 
 
   renderRevisionList = () => {
   renderRevisionList = () => {
     return this.state.revisions.map((revision: ChartType, i: number) => {
     return this.state.revisions.map((revision: ChartType, i: number) => {
@@ -144,15 +165,17 @@ export default class RevisionSection extends Component<PropsType, StateType> {
           <Td>
           <Td>
             <RollbackButton
             <RollbackButton
               disabled={isCurrent}
               disabled={isCurrent}
-              onClick={() => this.setState({ rollbackRevision: revision.version })}
+              onClick={() =>
+                this.setState({ rollbackRevision: revision.version })
+              }
             >
             >
-              {isCurrent ? 'Current' : 'Revert'}
+              {isCurrent ? "Current" : "Revert"}
             </RollbackButton>
             </RollbackButton>
           </Td>
           </Td>
         </Tr>
         </Tr>
       );
       );
     });
     });
-  }
+  };
 
 
   renderExpanded = () => {
   renderExpanded = () => {
     if (this.props.showRevisions) {
     if (this.props.showRevisions) {
@@ -170,22 +193,24 @@ export default class RevisionSection extends Component<PropsType, StateType> {
             </tbody>
             </tbody>
           </RevisionsTable>
           </RevisionsTable>
         </TableWrapper>
         </TableWrapper>
-      )
+      );
     }
     }
-  }
+  };
 
 
   renderContents = () => {
   renderContents = () => {
     if (this.state.loading) {
     if (this.state.loading) {
       return (
       return (
         <LoadingPlaceholder>
         <LoadingPlaceholder>
           <StatusWrapper>
           <StatusWrapper>
-            <LoadingGif src={loading} revision={false}/> Updating . . .
+            <LoadingGif src={loading} revision={false} /> Updating . . .
           </StatusWrapper>
           </StatusWrapper>
         </LoadingPlaceholder>
         </LoadingPlaceholder>
-      )
+      );
     }
     }
 
 
-    let isCurrent = this.props.chart.version === this.state.maxVersion || this.state.maxVersion === 0;
+    let isCurrent =
+      this.props.chart.version === this.state.maxVersion ||
+      this.state.maxVersion === 0;
     return (
     return (
       <div>
       <div>
         <RevisionHeader
         <RevisionHeader
@@ -193,16 +218,17 @@ export default class RevisionSection extends Component<PropsType, StateType> {
           isCurrent={isCurrent}
           isCurrent={isCurrent}
           onClick={this.props.toggleShowRevisions}
           onClick={this.props.toggleShowRevisions}
         >
         >
-          {isCurrent ? `Current Revision` : `Previewing Revision (Not Deployed)`} - <Revision>No. {this.props.chart.version}</Revision>
+          {isCurrent
+            ? `Current Revision`
+            : `Previewing Revision (Not Deployed)`}{" "}
+          - <Revision>No. {this.props.chart.version}</Revision>
           <i className="material-icons">expand_more</i>
           <i className="material-icons">expand_more</i>
         </RevisionHeader>
         </RevisionHeader>
 
 
-        <RevisionList>
-          {this.renderExpanded()}
-        </RevisionList>
+        <RevisionList>{this.renderExpanded()}</RevisionList>
       </div>
       </div>
     );
     );
-  }
+  };
 
 
   render() {
   render() {
     return (
     return (
@@ -235,15 +261,18 @@ const LoadingPlaceholder = styled.div`
 const LoadingGif = styled.img`
 const LoadingGif = styled.img`
   width: 15px;
   width: 15px;
   height: 15px;
   height: 15px;
-  margin-right: ${(props: {revision: boolean}) => props.revision ? '0px' : '9px'};
-  margin-left: ${(props: {revision: boolean}) => props.revision ? '10px' : '0px'};
-  margin-bottom: ${(props: {revision: boolean }) => props.revision ? '-2px' : '0px'};
+  margin-right: ${(props: { revision: boolean }) =>
+    props.revision ? "0px" : "9px"};
+  margin-left: ${(props: { revision: boolean }) =>
+    props.revision ? "10px" : "0px"};
+  margin-bottom: ${(props: { revision: boolean }) =>
+    props.revision ? "-2px" : "0px"};
 `;
 `;
 
 
 const StatusWrapper = styled.div`
 const StatusWrapper = styled.div`
   display: flex;
   display: flex;
   align-items: center;
   align-items: center;
-  font-family: 'Work Sans', sans-serif;
+  font-family: "Work Sans", sans-serif;
   font-size: 13px;
   font-size: 13px;
   color: #ffffff55;
   color: #ffffff55;
   margin-right: 25px;
   margin-right: 25px;
@@ -255,7 +284,8 @@ const RevisionList = styled.div`
 `;
 `;
 
 
 const RollbackButton = styled.div`
 const RollbackButton = styled.div`
-  cursor: ${(props: { disabled: boolean }) => props.disabled ? 'not-allowed' :'pointer'};
+  cursor: ${(props: { disabled: boolean }) =>
+    props.disabled ? "not-allowed" : "pointer"};
   display: flex;
   display: flex;
   border-radius: 3px;
   border-radius: 3px;
   align-items: center;
   align-items: center;
@@ -264,18 +294,23 @@ const RollbackButton = styled.div`
   height: 21px;
   height: 21px;
   font-size: 13px;
   font-size: 13px;
   width: 70px;
   width: 70px;
-  background: ${(props: { disabled: boolean }) => props.disabled ? '#aaaabbee' :'#616FEEcc'};
+  background: ${(props: { disabled: boolean }) =>
+    props.disabled ? "#aaaabbee" : "#616FEEcc"};
   :hover {
   :hover {
-    background: ${(props: { disabled: boolean }) => props.disabled ? '' : '#405eddbb'};
+    background: ${(props: { disabled: boolean }) =>
+      props.disabled ? "" : "#405eddbb"};
   }
   }
 `;
 `;
 
 
 const Tr = styled.tr`
 const Tr = styled.tr`
   line-height: 2.2em;
   line-height: 2.2em;
-  cursor: ${(props: { disableHover?: boolean, selected?: boolean }) => props.disableHover ? '' : 'pointer'};
-  background: ${(props: { disableHover?: boolean, selected?: boolean  }) => props.selected ? '#ffffff11' : ''};
+  cursor: ${(props: { disableHover?: boolean; selected?: boolean }) =>
+    props.disableHover ? "" : "pointer"};
+  background: ${(props: { disableHover?: boolean; selected?: boolean }) =>
+    props.selected ? "#ffffff11" : ""};
   :hover {
   :hover {
-    background: ${(props: { disableHover?: boolean, selected?: boolean  }) => props.disableHover ? '' : '#ffffff22'};
+    background: ${(props: { disableHover?: boolean; selected?: boolean }) =>
+      props.disableHover ? "" : "#ffffff22"};
   }
   }
 `;
 `;
 
 
@@ -307,7 +342,8 @@ const Revision = styled.div`
 `;
 `;
 
 
 const RevisionHeader = styled.div`
 const RevisionHeader = styled.div`
-  color: ${(props: { showRevisions: boolean, isCurrent: boolean }) => props.isCurrent ? '#ffffff66' : '#f5cb42'};
+  color: ${(props: { showRevisions: boolean; isCurrent: boolean }) =>
+    props.isCurrent ? "#ffffff66" : "#f5cb42"};
   display: flex;
   display: flex;
   align-items: center;
   align-items: center;
   height: 40px;
   height: 40px;
@@ -315,7 +351,8 @@ const RevisionHeader = styled.div`
   width: 100%;
   width: 100%;
   padding-left: 15px;
   padding-left: 15px;
   cursor: pointer;
   cursor: pointer;
-  background: ${(props: { showRevisions: boolean, isCurrent: boolean }) => props.showRevisions ? '#ffffff11' : ''};
+  background: ${(props: { showRevisions: boolean; isCurrent: boolean }) =>
+    props.showRevisions ? "#ffffff11" : ""};
   :hover {
   :hover {
     background: #ffffff18;
     background: #ffffff18;
     > i {
     > i {
@@ -328,22 +365,30 @@ const RevisionHeader = styled.div`
     font-size: 20px;
     font-size: 20px;
     cursor: pointer;
     cursor: pointer;
     border-radius: 20px;
     border-radius: 20px;
-    background: ${(props: { showRevisions: boolean, isCurrent: boolean }) => props.showRevisions ? '#ffffff18' : ''};
-    transform: ${(props: { showRevisions: boolean, isCurrent: boolean }) => props.showRevisions ? 'rotate(180deg)' : ''};
+    background: ${(props: { showRevisions: boolean; isCurrent: boolean }) =>
+      props.showRevisions ? "#ffffff18" : ""};
+    transform: ${(props: { showRevisions: boolean; isCurrent: boolean }) =>
+      props.showRevisions ? "rotate(180deg)" : ""};
   }
   }
 `;
 `;
 
 
 const StyledRevisionSection = styled.div`
 const StyledRevisionSection = styled.div`
   width: 100%;
   width: 100%;
-  max-height: ${(props: { showRevisions: boolean }) => props.showRevisions ? '255px' : '40px'};
+  max-height: ${(props: { showRevisions: boolean }) =>
+    props.showRevisions ? "255px" : "40px"};
   background: #ffffff11;
   background: #ffffff11;
   margin: 25px 0px 18px;
   margin: 25px 0px 18px;
   overflow: hidden;
   overflow: hidden;
   border-radius: 5px;
   border-radius: 5px;
-  animation: ${(props: { showRevisions: boolean }) => props.showRevisions ? 'expandRevisions 0.3s' : ''};
+  animation: ${(props: { showRevisions: boolean }) =>
+    props.showRevisions ? "expandRevisions 0.3s" : ""};
   animation-timing-function: ease-out;
   animation-timing-function: ease-out;
   @keyframes expandRevisions {
   @keyframes expandRevisions {
-    from { max-height: 40px }
-    to { max-height: 250px }
+    from {
+      max-height: 40px;
+    }
+    to {
+      max-height: 250px;
+    }
   }
   }
-`;
+`;

+ 167 - 118
dashboard/src/main/home/cluster-dashboard/expanded-chart/SettingsSection.tsx

@@ -1,87 +1,100 @@
-import React, { Component } from 'react';
-import styled from 'styled-components';
-import api from 'shared/api';
-import yaml from 'js-yaml';
+import React, { Component } from "react";
+import styled from "styled-components";
+import api from "shared/api";
+import yaml from "js-yaml";
 
 
-import { ChartType, RepoType, StorageType, ActionConfigType } from 'shared/types';
-import { Context } from 'shared/Context';
+import {
+  ChartType,
+  RepoType,
+  StorageType,
+  ActionConfigType,
+} from "shared/types";
+import { Context } from "shared/Context";
 
 
-import ImageSelector from 'components/image-selector/ImageSelector';
-import RepoSelector from 'components/repo-selector/RepoSelector';
-import SaveButton from 'components/SaveButton';
-import Heading from 'components/values-form/Heading';
-import Helper from 'components/values-form/Helper';
-import InputRow from 'components/values-form/InputRow';
+import ImageSelector from "components/image-selector/ImageSelector";
+import RepoSelector from "components/repo-selector/RepoSelector";
+import SaveButton from "components/SaveButton";
+import Heading from "components/values-form/Heading";
+import Helper from "components/values-form/Helper";
+import InputRow from "components/values-form/InputRow";
 
 
 type PropsType = {
 type PropsType = {
-  currentChart: ChartType,
-  refreshChart: () => void,
-  setShowDeleteOverlay: (x: boolean) => void,
+  currentChart: ChartType;
+  refreshChart: () => void;
+  setShowDeleteOverlay: (x: boolean) => void;
 };
 };
 
 
 type StateType = {
 type StateType = {
-  sourceType: string,
-  selectedImageUrl: string | null,
-  selectedTag: string | null,
-  saveValuesStatus: string | null,
-  values: string,
-  selectedRepo: RepoType | null,
-  selectedBranch: string,
-  subdirectory: string,
-  webhookToken: string,
-  highlightCopyButton: boolean,
+  sourceType: string;
+  selectedImageUrl: string | null;
+  selectedTag: string | null;
+  saveValuesStatus: string | null;
+  values: string;
+  selectedRepo: RepoType | null;
+  selectedBranch: string;
+  subdirectory: string;
+  webhookToken: string;
+  highlightCopyButton: boolean;
   action: ActionConfigType;
   action: ActionConfigType;
 };
 };
 
 
 export default class SettingsSection extends Component<PropsType, StateType> {
 export default class SettingsSection extends Component<PropsType, StateType> {
   state = {
   state = {
-    sourceType: 'registry',
-    selectedImageUrl: '',
-    selectedTag: '',
-    values: '',
-    saveValuesStatus: null as (string | null),
+    sourceType: "registry",
+    selectedImageUrl: "",
+    selectedTag: "",
+    values: "",
+    saveValuesStatus: null as string | null,
     selectedRepo: null as RepoType | null,
     selectedRepo: null as RepoType | null,
-    selectedBranch: '',
-    subdirectory: '',
-    webhookToken: '',
+    selectedBranch: "",
+    subdirectory: "",
+    webhookToken: "",
     highlightCopyButton: false,
     highlightCopyButton: false,
     action: {
     action: {
-      git_repo: '',
-      image_repo_uri: '',
+      git_repo: "",
+      image_repo_uri: "",
       git_repo_id: 0,
       git_repo_id: 0,
-      dockerfile_path: '',
+      dockerfile_path: "",
     } as ActionConfigType,
     } as ActionConfigType,
-  }
+  };
 
 
   // TODO: read in set image from form context instead of config
   // TODO: read in set image from form context instead of config
   componentDidMount() {
   componentDidMount() {
     let { currentCluster, currentProject } = this.context;
     let { currentCluster, currentProject } = this.context;
 
 
     let image = this.props.currentChart.config?.image;
     let image = this.props.currentChart.config?.image;
-    this.setState({ 
-      selectedImageUrl: image?.repository, 
-      selectedTag: image?.tag 
+    this.setState({
+      selectedImageUrl: image?.repository,
+      selectedTag: image?.tag,
     });
     });
 
 
-    api.getReleaseToken('<token>', {
-      namespace: this.props.currentChart.namespace,
-      cluster_id: currentCluster.id,
-      storage: StorageType.Secret
-    }, { id: currentProject.id, name: this.props.currentChart.name }, (err: any, res: any) => {
-      if (err) {
-        console.log(err)
-      } else {
-        this.setState({ action: res.data.git_action_config, webhookToken: res.data.webhook_token });
+    api.getReleaseToken(
+      "<token>",
+      {
+        namespace: this.props.currentChart.namespace,
+        cluster_id: currentCluster.id,
+        storage: StorageType.Secret,
+      },
+      { id: currentProject.id, name: this.props.currentChart.name },
+      (err: any, res: any) => {
+        if (err) {
+          console.log(err);
+        } else {
+          this.setState({
+            action: res.data.git_action_config,
+            webhookToken: res.data.webhook_token,
+          });
+        }
       }
       }
-    });
+    );
   }
   }
 
 
   redeployWithNewImage = (img: string, tag: string) => {
   redeployWithNewImage = (img: string, tag: string) => {
-    this.setState({ saveValuesStatus: 'loading' });
+    this.setState({ saveValuesStatus: "loading" });
     let { currentCluster, currentProject } = this.context;
     let { currentCluster, currentProject } = this.context;
 
 
     // If tag is explicitly declared, parse tag
     // If tag is explicitly declared, parse tag
-    let imgSplits = img.split(':');
+    let imgSplits = img.split(":");
     let parsedTag = null;
     let parsedTag = null;
     if (imgSplits.length > 1) {
     if (imgSplits.length > 1) {
       img = imgSplits[0];
       img = imgSplits[0];
@@ -92,28 +105,33 @@ export default class SettingsSection extends Component<PropsType, StateType> {
       image: {
       image: {
         repository: img,
         repository: img,
         tag: parsedTag || tag,
         tag: parsedTag || tag,
-      }
-    }
+      },
+    };
 
 
     let values = yaml.dump(image);
     let values = yaml.dump(image);
-    api.upgradeChartValues('<token>', {
-      namespace: this.props.currentChart.namespace,
-      storage: StorageType.Secret,
-      values,
-    }, {
-      id: currentProject.id, 
-      name: this.props.currentChart.name,
-      cluster_id: currentCluster.id,
-    }, (err: any, res: any) => {
-      if (err) {
-        console.log(err)
-        this.setState({ saveValuesStatus: 'error' });
-      } else {
-        this.setState({ saveValuesStatus: 'successful' });
-        this.props.refreshChart();
+    api.upgradeChartValues(
+      "<token>",
+      {
+        namespace: this.props.currentChart.namespace,
+        storage: StorageType.Secret,
+        values,
+      },
+      {
+        id: currentProject.id,
+        name: this.props.currentChart.name,
+        cluster_id: currentCluster.id,
+      },
+      (err: any, res: any) => {
+        if (err) {
+          console.log(err);
+          this.setState({ saveValuesStatus: "error" });
+        } else {
+          this.setState({ saveValuesStatus: "successful" });
+          this.props.refreshChart();
+        }
       }
       }
-    });
-  }
+    );
+  };
 
 
   /*
   /*
     <Helper>
     <Helper>
@@ -124,17 +142,17 @@ export default class SettingsSection extends Component<PropsType, StateType> {
     </Helper>
     </Helper>
   */
   */
   renderSourceSection = () => {
   renderSourceSection = () => {
-    if (this.state.sourceType === 'registry') {
+    if (this.state.sourceType === "registry") {
       return (
       return (
         <>
         <>
           <Heading>Connected Source</Heading>
           <Heading>Connected Source</Heading>
-          <Helper>
-            Specify a container image and tag.
-          </Helper>
+          <Helper>Specify a container image and tag.</Helper>
           <ImageSelector
           <ImageSelector
             selectedImageUrl={this.state.selectedImageUrl}
             selectedImageUrl={this.state.selectedImageUrl}
             selectedTag={this.state.selectedTag}
             selectedTag={this.state.selectedTag}
-            setSelectedImageUrl={(x: string) => this.setState({ selectedImageUrl: x })}
+            setSelectedImageUrl={(x: string) =>
+              this.setState({ selectedImageUrl: x })
+            }
             setSelectedTag={(x: string) => this.setState({ selectedTag: x })}
             setSelectedTag={(x: string) => this.setState({ selectedTag: x })}
             forceExpanded={true}
             forceExpanded={true}
           />
           />
@@ -145,48 +163,54 @@ export default class SettingsSection extends Component<PropsType, StateType> {
     let { currentProject } = this.context;
     let { currentProject } = this.context;
     return (
     return (
       <>
       <>
-        {this.state.action.git_repo.length > 0
-          ?
+        {this.state.action.git_repo.length > 0 ? (
           <>
           <>
             <Heading>Connected Source</Heading>
             <Heading>Connected Source</Heading>
             <Holder>
             <Holder>
               <InputRow
               <InputRow
                 disabled={true}
                 disabled={true}
-                label='Git Repository'
-                type='text'
-                width='100%'
+                label="Git Repository"
+                type="text"
+                width="100%"
                 value={this.state.action.git_repo}
                 value={this.state.action.git_repo}
                 setValue={(x: string) => console.log(x)}
                 setValue={(x: string) => console.log(x)}
               />
               />
               <InputRow
               <InputRow
                 disabled={true}
                 disabled={true}
-                label='Dockerfile Path'
-                type='text'
-                width='100%'
+                label="Dockerfile Path"
+                type="text"
+                width="100%"
                 value={this.state.action.dockerfile_path}
                 value={this.state.action.dockerfile_path}
                 setValue={(x: string) => console.log(x)}
                 setValue={(x: string) => console.log(x)}
               />
               />
               <InputRow
               <InputRow
                 disabled={true}
                 disabled={true}
-                label='Docker Image Repository'
-                type='text'
-                width='100%'
+                label="Docker Image Repository"
+                type="text"
+                width="100%"
                 value={this.state.action.image_repo_uri}
                 value={this.state.action.image_repo_uri}
                 setValue={(x: string) => console.log(x)}
                 setValue={(x: string) => console.log(x)}
               />
               />
             </Holder>
             </Holder>
           </>
           </>
-          :
+        ) : (
           <>
           <>
             <Heading>Connect a Source</Heading>
             <Heading>Connect a Source</Heading>
             <Helper>
             <Helper>
-              Select a repo to connect to. You can 
-              <A padRight={true} href={`/api/oauth/projects/${currentProject.id}/github?redirected=true`}>
+              Select a repo to connect to. You can
+              <A
+                padRight={true}
+                href={`/api/oauth/projects/${currentProject.id}/github?redirected=true`}
+              >
                 log in with GitHub
                 log in with GitHub
-              </A> or
-              <Highlight onClick={() => this.setState({ sourceType: 'registry' })}>
+              </A>{" "}
+              or
+              <Highlight
+                onClick={() => this.setState({ sourceType: "registry" })}
+              >
                 link an image registry
                 link an image registry
-              </Highlight>.
+              </Highlight>
+              .
             </Helper>
             </Helper>
             <RepoSelector
             <RepoSelector
               chart={this.props.currentChart}
               chart={this.props.currentChart}
@@ -194,15 +218,21 @@ export default class SettingsSection extends Component<PropsType, StateType> {
               selectedRepo={this.state.selectedRepo}
               selectedRepo={this.state.selectedRepo}
               selectedBranch={this.state.selectedBranch}
               selectedBranch={this.state.selectedBranch}
               subdirectory={this.state.subdirectory}
               subdirectory={this.state.subdirectory}
-              setSelectedRepo={(x: RepoType) => this.setState({ selectedRepo: x })}
-              setSelectedBranch={(x: string) => this.setState({ selectedBranch: x })}
-              setSubdirectory={(x: string) => this.setState({ subdirectory: x })}
+              setSelectedRepo={(x: RepoType) =>
+                this.setState({ selectedRepo: x })
+              }
+              setSelectedBranch={(x: string) =>
+                this.setState({ selectedBranch: x })
+              }
+              setSubdirectory={(x: string) =>
+                this.setState({ subdirectory: x })
+              }
             />
             />
           </>
           </>
-        }
+        )}
       </>
       </>
     );
     );
-  }
+  };
 
 
   renderWebhookSection = () => {
   renderWebhookSection = () => {
     if (true || this.state.webhookToken) {
     if (true || this.state.webhookToken) {
@@ -210,12 +240,14 @@ export default class SettingsSection extends Component<PropsType, StateType> {
       return (
       return (
         <>
         <>
           <Heading>Redeploy Webhook</Heading>
           <Heading>Redeploy Webhook</Heading>
-          <Helper>Programmatically deploy by calling this secret webhook.</Helper>
+          <Helper>
+            Programmatically deploy by calling this secret webhook.
+          </Helper>
           <Webhook copiedToClipboard={this.state.highlightCopyButton}>
           <Webhook copiedToClipboard={this.state.highlightCopyButton}>
             <div>{webhookText}</div>
             <div>{webhookText}</div>
-            <i 
+            <i
               className="material-icons"
               className="material-icons"
-              onClick={() => { 
+              onClick={() => {
                 navigator.clipboard.writeText(webhookText);
                 navigator.clipboard.writeText(webhookText);
                 this.setState({ highlightCopyButton: true });
                 this.setState({ highlightCopyButton: true });
               }}
               }}
@@ -227,7 +259,7 @@ export default class SettingsSection extends Component<PropsType, StateType> {
         </>
         </>
       );
       );
     }
     }
-  }
+  };
 
 
   render() {
   render() {
     return (
     return (
@@ -236,16 +268,26 @@ export default class SettingsSection extends Component<PropsType, StateType> {
           {this.renderSourceSection()}
           {this.renderSourceSection()}
           {this.renderWebhookSection()}
           {this.renderWebhookSection()}
           <Heading>Additional Settings</Heading>
           <Heading>Additional Settings</Heading>
-          <Button color='#b91133' onClick={() => this.props.setShowDeleteOverlay(true)}>
+          <Button
+            color="#b91133"
+            onClick={() => this.props.setShowDeleteOverlay(true)}
+          >
             Delete {this.props.currentChart.name}
             Delete {this.props.currentChart.name}
           </Button>
           </Button>
         </StyledSettingsSection>
         </StyledSettingsSection>
         <SaveButton
         <SaveButton
-          text='Save Settings'
-          onClick={() => this.redeployWithNewImage(this.state.selectedImageUrl, this.state.selectedTag)}
+          text="Save Settings"
+          onClick={() =>
+            this.redeployWithNewImage(
+              this.state.selectedImageUrl,
+              this.state.selectedTag
+            )
+          }
           status={this.state.saveValuesStatus}
           status={this.state.saveValuesStatus}
           makeFlush={true}
           makeFlush={true}
-          disabled={this.state.selectedImageUrl && this.state.selectedTag ? false : true}
+          disabled={
+            this.state.selectedImageUrl && this.state.selectedTag ? false : true
+          }
         />
         />
       </Wrapper>
       </Wrapper>
     );
     );
@@ -260,19 +302,22 @@ const Button = styled.button`
   margin-top: 20px;
   margin-top: 20px;
   margin-bottom: 30px;
   margin-bottom: 30px;
   font-weight: 500;
   font-weight: 500;
-  font-family: 'Work Sans', sans-serif;
+  font-family: "Work Sans", sans-serif;
   color: white;
   color: white;
   padding: 6px 20px 7px 20px;
   padding: 6px 20px 7px 20px;
   text-align: left;
   text-align: left;
   border: 0;
   border: 0;
   border-radius: 5px;
   border-radius: 5px;
-  background: ${(props) => (!props.disabled ? props.color : '#aaaabb')};
-  box-shadow: ${(props) => (!props.disabled ? '0 2px 5px 0 #00000030' : 'none')};
-  cursor: ${(props) => (!props.disabled ? 'pointer' : 'default')};
+  background: ${(props) => (!props.disabled ? props.color : "#aaaabb")};
+  box-shadow: ${(props) =>
+    !props.disabled ? "0 2px 5px 0 #00000030" : "none"};
+  cursor: ${(props) => (!props.disabled ? "pointer" : "default")};
   user-select: none;
   user-select: none;
-  :focus { outline: 0 }
+  :focus {
+    outline: 0;
+  }
   :hover {
   :hover {
-    filter: ${(props) => (!props.disabled ? 'brightness(120%)' : '')};
+    filter: ${(props) => (!props.disabled ? "brightness(120%)" : "")};
   }
   }
 `;
 `;
 
 
@@ -293,10 +338,11 @@ const Webhook = styled.div`
   > div {
   > div {
     user-select: all;
     user-select: all;
   }
   }
-  
+
   > i {
   > i {
     padding: 5px;
     padding: 5px;
-    background: ${(props: { copiedToClipboard: boolean }) => props.copiedToClipboard ? '#616FEEcc' : '#ffffff22'};
+    background: ${(props: { copiedToClipboard: boolean }) =>
+      props.copiedToClipboard ? "#616FEEcc" : "#ffffff22"};
     border-radius: 5px;
     border-radius: 5px;
     position: absolute;
     position: absolute;
     right: 10px;
     right: 10px;
@@ -305,7 +351,8 @@ const Webhook = styled.div`
     color: #ffffff;
     color: #ffffff;
 
 
     :hover {
     :hover {
-      background: ${(props: { copiedToClipboard: boolean }) => props.copiedToClipboard ? '' : '#ffffff44'};;
+      background: ${(props: { copiedToClipboard: boolean }) =>
+        props.copiedToClipboard ? "" : "#ffffff44"};
     }
     }
   }
   }
 `;
 `;
@@ -315,7 +362,8 @@ const Highlight = styled.div`
   text-decoration: underline;
   text-decoration: underline;
   margin-left: 5px;
   margin-left: 5px;
   cursor: pointer;
   cursor: pointer;
-  padding-right: ${(props: { padRight?: boolean }) => props.padRight ? '5px' : ''};
+  padding-right: ${(props: { padRight?: boolean }) =>
+    props.padRight ? "5px" : ""};
 `;
 `;
 
 
 const A = styled.a`
 const A = styled.a`
@@ -323,7 +371,8 @@ const A = styled.a`
   text-decoration: underline;
   text-decoration: underline;
   margin-left: 5px;
   margin-left: 5px;
   cursor: pointer;
   cursor: pointer;
-  padding-right: ${(props: { padRight?: boolean }) => props.padRight ? '5px' : ''};
+  padding-right: ${(props: { padRight?: boolean }) =>
+    props.padRight ? "5px" : ""};
 `;
 `;
 
 
 const Wrapper = styled.div`
 const Wrapper = styled.div`
@@ -344,4 +393,4 @@ const StyledSettingsSection = styled.div`
 
 
 const Holder = styled.div`
 const Holder = styled.div`
   padding: 0px 12px;
   padding: 0px 12px;
-`;
+`;

+ 41 - 36
dashboard/src/main/home/cluster-dashboard/expanded-chart/ValuesYaml.tsx

@@ -1,33 +1,33 @@
-import React, { Component } from 'react';
-import styled from 'styled-components';
-import yaml from 'js-yaml';
+import React, { Component } from "react";
+import styled from "styled-components";
+import yaml from "js-yaml";
 
 
-import { ChartType, StorageType } from 'shared/types';
-import api from 'shared/api';
-import { Context } from 'shared/Context';
+import { ChartType, StorageType } from "shared/types";
+import api from "shared/api";
+import { Context } from "shared/Context";
 
 
-import YamlEditor from 'components/YamlEditor';
-import SaveButton from 'components/SaveButton';
+import YamlEditor from "components/YamlEditor";
+import SaveButton from "components/SaveButton";
 
 
 type PropsType = {
 type PropsType = {
-  currentChart: ChartType
-  refreshChart: () => void
+  currentChart: ChartType;
+  refreshChart: () => void;
 };
 };
 
 
 type StateType = {
 type StateType = {
-  values: string,
-  saveValuesStatus: string | null
+  values: string;
+  saveValuesStatus: string | null;
 };
 };
 
 
 // TODO: handle zoom out
 // TODO: handle zoom out
 export default class ValuesYaml extends Component<PropsType, StateType> {
 export default class ValuesYaml extends Component<PropsType, StateType> {
   state = {
   state = {
-    values: '',
-    saveValuesStatus: null as (string | null)
-  }
+    values: "",
+    saveValuesStatus: null as string | null,
+  };
 
 
   updateValues() {
   updateValues() {
-    let values = '# Nothing here yet';
+    let values = "# Nothing here yet";
     if (this.props.currentChart.config) {
     if (this.props.currentChart.config) {
       values = yaml.dump(this.props.currentChart.config);
       values = yaml.dump(this.props.currentChart.config);
     }
     }
@@ -46,26 +46,31 @@ export default class ValuesYaml extends Component<PropsType, StateType> {
 
 
   handleSaveValues = () => {
   handleSaveValues = () => {
     let { currentCluster, setCurrentError, currentProject } = this.context;
     let { currentCluster, setCurrentError, currentProject } = this.context;
-    this.setState({ saveValuesStatus: 'loading' });
+    this.setState({ saveValuesStatus: "loading" });
 
 
-    api.upgradeChartValues('<token>', {
-      namespace: this.props.currentChart.namespace,
-      storage: StorageType.Secret,
-      values: this.state.values
-    }, {
-      id: currentProject.id, 
-      name: this.props.currentChart.name,
-      cluster_id: currentCluster.id,
-    }, (err: any, res: any) => {
-      if (err) {
-        console.log(err)
-        this.setState({ saveValuesStatus: 'error' });
-      } else {
-        this.setState({ saveValuesStatus: 'successful' });
-        this.props.refreshChart();
+    api.upgradeChartValues(
+      "<token>",
+      {
+        namespace: this.props.currentChart.namespace,
+        storage: StorageType.Secret,
+        values: this.state.values,
+      },
+      {
+        id: currentProject.id,
+        name: this.props.currentChart.name,
+        cluster_id: currentCluster.id,
+      },
+      (err: any, res: any) => {
+        if (err) {
+          console.log(err);
+          this.setState({ saveValuesStatus: "error" });
+        } else {
+          this.setState({ saveValuesStatus: "successful" });
+          this.props.refreshChart();
+        }
       }
       }
-    });
-  }
+    );
+  };
 
 
   render() {
   render() {
     return (
     return (
@@ -77,7 +82,7 @@ export default class ValuesYaml extends Component<PropsType, StateType> {
           />
           />
         </Wrapper>
         </Wrapper>
         <SaveButton
         <SaveButton
-          text='Update Values'
+          text="Update Values"
           onClick={this.handleSaveValues}
           onClick={this.handleSaveValues}
           status={this.state.saveValuesStatus}
           status={this.state.saveValuesStatus}
           makeFlush={true}
           makeFlush={true}
@@ -101,4 +106,4 @@ const StyledValuesYaml = styled.div`
   flex-direction: column;
   flex-direction: column;
   width: 100%;
   width: 100%;
   height: 100%;
   height: 100%;
-`;
+`;

+ 25 - 32
dashboard/src/main/home/cluster-dashboard/expanded-chart/deploy/DeploySection.tsx

@@ -1,58 +1,53 @@
-import React, { Component } from 'react';
-import styled from 'styled-components';
+import React, { Component } from "react";
+import styled from "styled-components";
 
 
-import Loading from 'components/Loading';
-import { Context } from 'shared/Context';
-import { ChartType } from 'shared/types';
+import Loading from "components/Loading";
+import { Context } from "shared/Context";
+import { ChartType } from "shared/types";
 
 
-import EventTab from './EventTab';
+import EventTab from "./EventTab";
 
 
 type PropsType = {
 type PropsType = {
-  currentChart: ChartType,
+  currentChart: ChartType;
 };
 };
 
 
 type StateType = {
 type StateType = {
-  events: any[],
-  loading: boolean,
+  events: any[];
+  loading: boolean;
 };
 };
 
 
 export default class StatusSection extends Component<PropsType, StateType> {
 export default class StatusSection extends Component<PropsType, StateType> {
   state = {
   state = {
     events: [] as any[],
     events: [] as any[],
     loading: true,
     loading: true,
-  }
+  };
 
 
   renderTabs = () => {
   renderTabs = () => {
     return this.state.events.map((c, i) => {
     return this.state.events.map((c, i) => {
-      return (
-        <EventTab />
-      )
-    })
-  }
+      return <EventTab />;
+    });
+  };
 
 
   renderStatusSection = () => {
   renderStatusSection = () => {
     if (this.state.loading) {
     if (this.state.loading) {
       return (
       return (
-        <NoEvents> 
+        <NoEvents>
           <Loading />
           <Loading />
         </NoEvents>
         </NoEvents>
-      )
+      );
     }
     }
     if (this.state.events.length > 0) {
     if (this.state.events.length > 0) {
-      return (
-        <Wrapper>
-          {this.renderTabs()}
-        </Wrapper>
-      )
+      return <Wrapper>{this.renderTabs()}</Wrapper>;
     } else {
     } else {
       return (
       return (
-        <NoEvents> 
-          <i className="material-icons">category</i> 
-          No events to display. This might happen while your app is still deploying.
+        <NoEvents>
+          <i className="material-icons">category</i>
+          No events to display. This might happen while your app is still
+          deploying.
         </NoEvents>
         </NoEvents>
-      )
+      );
     }
     }
-  }
+  };
 
 
   componentDidMount() {
   componentDidMount() {
     const { currentChart } = this.props;
     const { currentChart } = this.props;
@@ -73,14 +68,12 @@ export default class StatusSection extends Component<PropsType, StateType> {
     //   }
     //   }
     //   this.setState({ controllers: res.data, loading: false })
     //   this.setState({ controllers: res.data, loading: false })
     // });
     // });
-    this.setState({events: [1, 2, 3], loading: false})
+    this.setState({ events: [1, 2, 3], loading: false });
   }
   }
 
 
   render() {
   render() {
     return (
     return (
-      <StyledDeploySection>
-        {this.renderStatusSection()}
-      </StyledDeploySection>
+      <StyledDeploySection>{this.renderStatusSection()}</StyledDeploySection>
     );
     );
   }
   }
 }
 }
@@ -119,4 +112,4 @@ const NoEvents = styled.div`
     font-size: 18px;
     font-size: 18px;
     margin-right: 12px;
     margin-right: 12px;
   }
   }
-`;
+`;

+ 12 - 19
dashboard/src/main/home/cluster-dashboard/expanded-chart/deploy/EventTab.tsx

@@ -1,28 +1,20 @@
+import React, { Component } from "react";
+import styled from "styled-components";
 
 
-import React, { Component } from 'react';
-import styled from 'styled-components';
+type PropsType = {};
 
 
-type PropsType = {
-};
-
-type StateType = {
-};
+type StateType = {};
 
 
 export default class EventTab extends Component<PropsType, StateType> {
 export default class EventTab extends Component<PropsType, StateType> {
-  state = {
-  }
+  state = {};
 
 
   render() {
   render() {
     return (
     return (
-      <StyledEventTab 
-        isLast={false}
-      >
+      <StyledEventTab isLast={false}>
         <EventHeader>
         <EventHeader>
-            <i className="material-icons">cloud_upload</i>
-            Deploy successful!
-            <div>
-                Dec 12 at 11:55AM
-            </div>
+          <i className="material-icons">cloud_upload</i>
+          Deploy successful!
+          <div>Dec 12 at 11:55AM</div>
         </EventHeader>
         </EventHeader>
       </StyledEventTab>
       </StyledEventTab>
     );
     );
@@ -33,7 +25,8 @@ const StyledEventTab = styled.div`
   width: 100%;
   width: 100%;
   margin-bottom: 2px;
   margin-bottom: 2px;
   background: #ffffff11;
   background: #ffffff11;
-  border-bottom-left-radius: ${(props: { isLast: boolean }) => props.isLast ? '5px' : ''};
+  border-bottom-left-radius: ${(props: { isLast: boolean }) =>
+    props.isLast ? "5px" : ""};
 `;
 `;
 
 
 const EventHeader = styled.div`
 const EventHeader = styled.div`
@@ -46,4 +39,4 @@ const EventHeader = styled.div`
   user-select: none;
   user-select: none;
   padding: 8px 18px;
   padding: 8px 18px;
   padding-left: 22px;
   padding-left: 22px;
-`;
+`;

+ 36 - 31
dashboard/src/main/home/cluster-dashboard/expanded-chart/graph/Edge.tsx

@@ -1,30 +1,30 @@
-import React, { Component } from 'react';
-import styled from 'styled-components';
+import React, { Component } from "react";
+import styled from "styled-components";
 
 
-import { edgeColors } from 'shared/rosettaStone';
-import { EdgeType } from 'shared/types';
+import { edgeColors } from "shared/rosettaStone";
+import { EdgeType } from "shared/types";
 
 
 const thickness = 12;
 const thickness = 12;
 
 
 type PropsType = {
 type PropsType = {
-  x1: number,
-  y1: number,
-  x2: number,
-  y2: number,
-  originX: number,
-  originY: number,
-  edge: EdgeType,
-  setCurrentEdge: (edge: EdgeType) => void
+  x1: number;
+  y1: number;
+  x2: number;
+  y2: number;
+  originX: number;
+  originY: number;
+  edge: EdgeType;
+  setCurrentEdge: (edge: EdgeType) => void;
 };
 };
 
 
 type StateType = {
 type StateType = {
-  showArrowHead: boolean
+  showArrowHead: boolean;
 };
 };
 
 
 export default class Edge extends Component<PropsType, StateType> {
 export default class Edge extends Component<PropsType, StateType> {
   state = {
   state = {
-    showArrowHead: true
-  }
+    showArrowHead: true,
+  };
 
 
   render() {
   render() {
     let { originX, originY, edge, setCurrentEdge } = this.props;
     let { originX, originY, edge, setCurrentEdge } = this.props;
@@ -32,13 +32,13 @@ export default class Edge extends Component<PropsType, StateType> {
     let x2 = Math.round(originX + this.props.x2);
     let x2 = Math.round(originX + this.props.x2);
     let y1 = Math.round(originY - this.props.y1);
     let y1 = Math.round(originY - this.props.y1);
     let y2 = Math.round(originY - this.props.y2);
     let y2 = Math.round(originY - this.props.y2);
-    
-    var length = Math.sqrt(((x2-x1) * (x2-x1)) + ((y2-y1) * (y2-y1)));
+
+    var length = Math.sqrt((x2 - x1) * (x2 - x1) + (y2 - y1) * (y2 - y1));
     // center
     // center
-    var cx = ((x1 + x2) / 2) - (length / 2);
-    var cy = ((y1 + y2) / 2) - (thickness / 2);
+    var cx = (x1 + x2) / 2 - length / 2;
+    var cy = (y1 + y2) / 2 - thickness / 2;
     // angle
     // angle
-    var angle = Math.atan2((y1 - y2), (x1 - x2)) * (180 / Math.PI);
+    var angle = Math.atan2(y1 - y2, x1 - x2) * (180 / Math.PI);
 
 
     return (
     return (
       <StyledEdge
       <StyledEdge
@@ -50,7 +50,9 @@ export default class Edge extends Component<PropsType, StateType> {
         onMouseLeave={() => setCurrentEdge(null)}
         onMouseLeave={() => setCurrentEdge(null)}
         type={edge.type}
         type={edge.type}
       >
       >
-        {this.state.showArrowHead ? <ArrowHead color={edgeColors[edge.type]} /> : null}
+        {this.state.showArrowHead ? (
+          <ArrowHead color={edgeColors[edge.type]} />
+        ) : null}
         <VisibleLine color={edgeColors[edge.type]} />
         <VisibleLine color={edgeColors[edge.type]} />
       </StyledEdge>
       </StyledEdge>
     );
     );
@@ -58,32 +60,35 @@ export default class Edge extends Component<PropsType, StateType> {
 }
 }
 
 
 const ArrowHead = styled.div`
 const ArrowHead = styled.div`
-  width: 0; 
+  width: 0;
   height: 0;
   height: 0;
   margin-left: 20px;
   margin-left: 20px;
   border-top: 5px solid transparent;
   border-top: 5px solid transparent;
-  border-bottom: 5px solid transparent; 
-  border-right: 10px solid ${(props: { color: string }) => props.color ? props.color : '#ffffff66'};
+  border-bottom: 5px solid transparent;
+  border-right: 10px solid
+    ${(props: { color: string }) => (props.color ? props.color : "#ffffff66")};
 `;
 `;
 
 
 const VisibleLine = styled.section`
 const VisibleLine = styled.section`
   height: 2px;
   height: 2px;
   width: 100%;
   width: 100%;
-  background: ${(props: { color: string }) => props.color ? props.color : '#ffffff66'};
+  background: ${(props: { color: string }) =>
+    props.color ? props.color : "#ffffff66"};
 `;
 `;
 
 
 const StyledEdge: any = styled.div.attrs((props: any) => ({
 const StyledEdge: any = styled.div.attrs((props: any) => ({
   style: {
   style: {
-    top: props.cy + 'px',
-    left: props.cx + 'px',
-    transform: 'rotate(' + props.angle + 'deg)',
-    width: props.length + 'px'
+    top: props.cy + "px",
+    left: props.cx + "px",
+    transform: "rotate(" + props.angle + "deg)",
+    width: props.length + "px",
   },
   },
 }))`
 }))`
   position: absolute;
   position: absolute;
   height: ${thickness}px;
   height: ${thickness}px;
   cursor: pointer;
   cursor: pointer;
-  z-index: ${(props: { type: string, color: string }) => props.type == 'ControlRel' ? '1' : '0'};
+  z-index: ${(props: { type: string; color: string }) =>
+    props.type == "ControlRel" ? "1" : "0"};
   display: flex;
   display: flex;
   align-items: center;
   align-items: center;
   justify-content: center;
   justify-content: center;
@@ -92,4 +97,4 @@ const StyledEdge: any = styled.div.attrs((props: any) => ({
       box-shadow: 0 0 10px #ffffff;
       box-shadow: 0 0 10px #ffffff;
     }
     }
   }
   }
-`;
+`;

+ 275 - 162
dashboard/src/main/home/cluster-dashboard/expanded-chart/graph/GraphDisplay.tsx

@@ -1,56 +1,56 @@
-import React, { Component } from 'react';
-import styled from 'styled-components';
+import React, { Component } from "react";
+import styled from "styled-components";
 
 
-import { ResourceType, NodeType, EdgeType, ChartType } from 'shared/types';
+import { ResourceType, NodeType, EdgeType, ChartType } from "shared/types";
 
 
-import Node from './Node';
-import Edge from './Edge';
-import InfoPanel from './InfoPanel';
-import ZoomPanel from './ZoomPanel';
-import SelectRegion from './SelectRegion';
+import Node from "./Node";
+import Edge from "./Edge";
+import InfoPanel from "./InfoPanel";
+import ZoomPanel from "./ZoomPanel";
+import SelectRegion from "./SelectRegion";
 
 
 const zoomConstant = 0.01;
 const zoomConstant = 0.01;
 const panConstant = 0.8;
 const panConstant = 0.8;
 
 
 type PropsType = {
 type PropsType = {
-  components: ResourceType[],
-  isExpanded: boolean,
-  setSidebar: (x: boolean) => void,
-  currentChart: ChartType,
+  components: ResourceType[];
+  isExpanded: boolean;
+  setSidebar: (x: boolean) => void;
+  currentChart: ChartType;
 
 
   // Handle revisions expansion for YAML wrapper
   // Handle revisions expansion for YAML wrapper
-  showRevisions: boolean
+  showRevisions: boolean;
 };
 };
 
 
 type StateType = {
 type StateType = {
-  nodes: NodeType[],
-  edges: EdgeType[],
-  activeIds: number[], // IDs of all currently selected nodes
-  originX: number | null,
-  originY: number | null,
-  cursorX: number | null,
-  cursorY: number | null,
-  deltaX: number | null, // Dragging bg x-displacement
-  deltaY: number | null, // Dragging y-displacement
-  panX: number | null, // Two-finger pan x-displacement 
-  panY: number | null, // Two-finger pan y-displacement
-  anchorX: number | null, // Initial cursorX during region select
-  anchorY: number | null, // Initial cursorY during region select
-  nodeClickX: number | null, // Initial cursorX during node click (drag vs click)
-  nodeClickY: number | null, // Initial cursorY during node click (drag vs click)
-  dragBg: boolean, // Boolean to track if all nodes should move with mouse (bg drag)
-  preventBgDrag: boolean, // Prevent bg drag when moving selected with mouse down
-  relocateAllowed: boolean, // Suppress movement of selected when drawing select region
-  scale: number,
-  btnZooming: boolean,
-  showKindLabels: boolean,
-  isExpanded: boolean,
-  currentNode: NodeType | null,
-  currentEdge: EdgeType | null,
-  openedNode: NodeType | null,
-  suppressCloseNode: boolean, // Still click should close opened unless on a node
-  suppressDisplay: boolean, // Ignore clicks + pan/zoom on InfoPanel or ButtonSection
-  version?: number // Track in localstorage for handling updates when unmounted
+  nodes: NodeType[];
+  edges: EdgeType[];
+  activeIds: number[]; // IDs of all currently selected nodes
+  originX: number | null;
+  originY: number | null;
+  cursorX: number | null;
+  cursorY: number | null;
+  deltaX: number | null; // Dragging bg x-displacement
+  deltaY: number | null; // Dragging y-displacement
+  panX: number | null; // Two-finger pan x-displacement
+  panY: number | null; // Two-finger pan y-displacement
+  anchorX: number | null; // Initial cursorX during region select
+  anchorY: number | null; // Initial cursorY during region select
+  nodeClickX: number | null; // Initial cursorX during node click (drag vs click)
+  nodeClickY: number | null; // Initial cursorY during node click (drag vs click)
+  dragBg: boolean; // Boolean to track if all nodes should move with mouse (bg drag)
+  preventBgDrag: boolean; // Prevent bg drag when moving selected with mouse down
+  relocateAllowed: boolean; // Suppress movement of selected when drawing select region
+  scale: number;
+  btnZooming: boolean;
+  showKindLabels: boolean;
+  isExpanded: boolean;
+  currentNode: NodeType | null;
+  currentEdge: EdgeType | null;
+  openedNode: NodeType | null;
+  suppressCloseNode: boolean; // Still click should close opened unless on a node
+  suppressDisplay: boolean; // Ignore clicks + pan/zoom on InfoPanel or ButtonSection
+  version?: number; // Track in localstorage for handling updates when unmounted
 };
 };
 
 
 // TODO: region-based unselect, shift-click, multi-region
 // TODO: region-based unselect, shift-click, multi-region
@@ -59,18 +59,18 @@ export default class GraphDisplay extends Component<PropsType, StateType> {
     nodes: [] as NodeType[],
     nodes: [] as NodeType[],
     edges: [] as EdgeType[],
     edges: [] as EdgeType[],
     activeIds: [] as number[],
     activeIds: [] as number[],
-    originX: null as (number | null),
-    originY: null as (number | null),
-    cursorX: null as (number | null),
-    cursorY: null as (number | null),
-    deltaX: null as (number | null),
-    deltaY: null as (number | null),
-    panX: null as (number | null),
-    panY: null as (number | null),
-    anchorX: null as (number | null),
-    anchorY: null as (number | null),
-    nodeClickX: null as (number | null),
-    nodeClickY: null as (number | null),
+    originX: null as number | null,
+    originY: null as number | null,
+    cursorX: null as number | null,
+    cursorY: null as number | null,
+    deltaX: null as number | null,
+    deltaY: null as number | null,
+    panX: null as number | null,
+    panY: null as number | null,
+    anchorX: null as number | null,
+    anchorY: null as number | null,
+    nodeClickX: null as number | null,
+    nodeClickY: null as number | null,
     dragBg: false,
     dragBg: false,
     preventBgDrag: false,
     preventBgDrag: false,
     relocateAllowed: false,
     relocateAllowed: false,
@@ -78,26 +78,28 @@ export default class GraphDisplay extends Component<PropsType, StateType> {
     btnZooming: false,
     btnZooming: false,
     showKindLabels: true,
     showKindLabels: true,
     isExpanded: false,
     isExpanded: false,
-    currentNode: null as (NodeType | null),
-    currentEdge: null as (EdgeType | null),
-    openedNode: null as (NodeType | null),
+    currentNode: null as NodeType | null,
+    currentEdge: null as EdgeType | null,
+    openedNode: null as NodeType | null,
     suppressCloseNode: false,
     suppressCloseNode: false,
-    suppressDisplay: false
-  }
+    suppressDisplay: false,
+  };
 
 
   spaceRef: any = React.createRef();
   spaceRef: any = React.createRef();
 
 
   getRandomIntBetweenRange = (min: number, max: number) => {
   getRandomIntBetweenRange = (min: number, max: number) => {
     min = Math.ceil(min);
     min = Math.ceil(min);
     max = Math.floor(max);
     max = Math.floor(max);
-    return Math.floor(Math.random() * (max - min) + min);  
-  }
+    return Math.floor(Math.random() * (max - min) + min);
+  };
 
 
   // Handle graph from localstorage
   // Handle graph from localstorage
   getChartGraph = () => {
   getChartGraph = () => {
     let { components, currentChart } = this.props;
     let { components, currentChart } = this.props;
 
 
-    let graph = localStorage.getItem(`charts.${currentChart.name}-${currentChart.version}`);
+    let graph = localStorage.getItem(
+      `charts.${currentChart.name}-${currentChart.version}`
+    );
     let nodes = [] as NodeType[];
     let nodes = [] as NodeType[];
     let edges = [] as EdgeType[];
     let edges = [] as EdgeType[];
     if (!graph) {
     if (!graph) {
@@ -105,26 +107,29 @@ export default class GraphDisplay extends Component<PropsType, StateType> {
       edges = this.createEdges(components);
       edges = this.createEdges(components);
       this.setState({ nodes, edges });
       this.setState({ nodes, edges });
     } else {
     } else {
-      let storedState = JSON.parse(localStorage.getItem(
-        `charts.${currentChart.name}-${currentChart.version}`
-      ));
+      let storedState = JSON.parse(
+        localStorage.getItem(
+          `charts.${currentChart.name}-${currentChart.version}`
+        )
+      );
       this.setState(storedState);
       this.setState(storedState);
     }
     }
-  }
+  };
 
 
   componentDidMount() {
   componentDidMount() {
-
     // Initialize origin
     // Initialize origin
     let height = this.spaceRef.offsetHeight;
     let height = this.spaceRef.offsetHeight;
     let width = this.spaceRef.offsetWidth;
     let width = this.spaceRef.offsetWidth;
     this.setState({
     this.setState({
       originX: Math.round(width / 2),
       originX: Math.round(width / 2),
-      originY: Math.round(height / 2)
+      originY: Math.round(height / 2),
     });
     });
 
 
     // Suppress trackpad gestures
     // Suppress trackpad gestures
     this.spaceRef.addEventListener("touchmove", (e: any) => e.preventDefault());
     this.spaceRef.addEventListener("touchmove", (e: any) => e.preventDefault());
-    this.spaceRef.addEventListener("mousewheel", (e: any) => e.preventDefault());
+    this.spaceRef.addEventListener("mousewheel", (e: any) =>
+      e.preventDefault()
+    );
 
 
     document.addEventListener("keydown", this.handleKeyDown);
     document.addEventListener("keydown", this.handleKeyDown);
     document.addEventListener("keyup", this.handleKeyUp);
     document.addEventListener("keyup", this.handleKeyUp);
@@ -133,7 +138,7 @@ export default class GraphDisplay extends Component<PropsType, StateType> {
 
 
     window.onbeforeunload = () => {
     window.onbeforeunload = () => {
       this.storeChartGraph();
       this.storeChartGraph();
-    }
+    };
   }
   }
 
 
   // Live update on rollback/upgrade
   // Live update on rollback/upgrade
@@ -146,48 +151,96 @@ export default class GraphDisplay extends Component<PropsType, StateType> {
 
 
   createNodes = (components: ResourceType[]) => {
   createNodes = (components: ResourceType[]) => {
     return components.map((c: ResourceType) => {
     return components.map((c: ResourceType) => {
-      switch(c.Kind) {
+      switch (c.Kind) {
         case "ClusterRoleBinding":
         case "ClusterRoleBinding":
         case "ClusterRole":
         case "ClusterRole":
         case "RoleBinding":
         case "RoleBinding":
         case "Role":
         case "Role":
-          return { id: c.ID, RawYAML: c.RawYAML, name: c.Name, kind: c.Kind, x: this.getRandomIntBetweenRange(-500, 0), y: this.getRandomIntBetweenRange(0, 250), w: 40, h: 40 };
+          return {
+            id: c.ID,
+            RawYAML: c.RawYAML,
+            name: c.Name,
+            kind: c.Kind,
+            x: this.getRandomIntBetweenRange(-500, 0),
+            y: this.getRandomIntBetweenRange(0, 250),
+            w: 40,
+            h: 40,
+          };
         case "Deployment":
         case "Deployment":
         case "StatefulSet":
         case "StatefulSet":
         case "Pod":
         case "Pod":
         case "ServiceAccount":
         case "ServiceAccount":
-          return { id: c.ID, RawYAML: c.RawYAML, name: c.Name, kind: c.Kind, x: this.getRandomIntBetweenRange(0, 500), y: this.getRandomIntBetweenRange(0, 250), w: 40, h: 40 };
+          return {
+            id: c.ID,
+            RawYAML: c.RawYAML,
+            name: c.Name,
+            kind: c.Kind,
+            x: this.getRandomIntBetweenRange(0, 500),
+            y: this.getRandomIntBetweenRange(0, 250),
+            w: 40,
+            h: 40,
+          };
         case "Service":
         case "Service":
         case "Ingress":
         case "Ingress":
         case "ServiceAccount":
         case "ServiceAccount":
-            return { id: c.ID, RawYAML: c.RawYAML, name: c.Name, kind: c.Kind, x: this.getRandomIntBetweenRange(0, 500), y: this.getRandomIntBetweenRange(-250, 0), w: 40, h: 40 };
+          return {
+            id: c.ID,
+            RawYAML: c.RawYAML,
+            name: c.Name,
+            kind: c.Kind,
+            x: this.getRandomIntBetweenRange(0, 500),
+            y: this.getRandomIntBetweenRange(-250, 0),
+            w: 40,
+            h: 40,
+          };
         default:
         default:
-          return { id: c.ID, RawYAML: c.RawYAML, name: c.Name, kind: c.Kind, x: this.getRandomIntBetweenRange(-400, 0), y: this.getRandomIntBetweenRange(-250, 0), w: 40, h: 40 };
-        }
+          return {
+            id: c.ID,
+            RawYAML: c.RawYAML,
+            name: c.Name,
+            kind: c.Kind,
+            x: this.getRandomIntBetweenRange(-400, 0),
+            y: this.getRandomIntBetweenRange(-250, 0),
+            w: 40,
+            h: 40,
+          };
+      }
     });
     });
-  }
+  };
 
 
   createEdges = (components: ResourceType[]) => {
   createEdges = (components: ResourceType[]) => {
     let edges = [] as EdgeType[];
     let edges = [] as EdgeType[];
     components.map((c: ResourceType) => {
     components.map((c: ResourceType) => {
       c.Relations?.ControlRels?.map((rel: any) => {
       c.Relations?.ControlRels?.map((rel: any) => {
         if (rel.Source == c.ID) {
         if (rel.Source == c.ID) {
-          edges.push({ type: "ControlRel", source: rel.Source, target: rel.Target });
+          edges.push({
+            type: "ControlRel",
+            source: rel.Source,
+            target: rel.Target,
+          });
         }
         }
-      })
+      });
       c.Relations?.LabelRels?.map((rel: any) => {
       c.Relations?.LabelRels?.map((rel: any) => {
         if (rel.Source == c.ID) {
         if (rel.Source == c.ID) {
-          edges.push({ type: "LabelRel", source: rel.Source, target: rel.Target });
+          edges.push({
+            type: "LabelRel",
+            source: rel.Source,
+            target: rel.Target,
+          });
         }
         }
-      })
+      });
       c.Relations?.SpecRels?.map((rel: any) => {
       c.Relations?.SpecRels?.map((rel: any) => {
         if (rel.Source == c.ID) {
         if (rel.Source == c.ID) {
-          edges.push({ type: "SpecRel", source: rel.Source, target: rel.Target });
+          edges.push({
+            type: "SpecRel",
+            source: rel.Source,
+            target: rel.Target,
+          });
         }
         }
-      })
+      });
     });
     });
     return edges;
     return edges;
-  }
+  };
 
 
   storeChartGraph = (props?: PropsType) => {
   storeChartGraph = (props?: PropsType) => {
     let useProps = props || this.props;
     let useProps = props || this.props;
@@ -207,20 +260,24 @@ export default class GraphDisplay extends Component<PropsType, StateType> {
       `charts.${currentChart.name}-${currentChart.version}`,
       `charts.${currentChart.name}-${currentChart.version}`,
       JSON.stringify(graph)
       JSON.stringify(graph)
     );
     );
-  }
+  };
 
 
   componentWillUnmount() {
   componentWillUnmount() {
     this.storeChartGraph();
     this.storeChartGraph();
-    
-    this.spaceRef.removeEventListener("touchmove", (e: any) => e.preventDefault());
-    this.spaceRef.removeEventListener("mousewheel", (e: any) => e.preventDefault());
+
+    this.spaceRef.removeEventListener("touchmove", (e: any) =>
+      e.preventDefault()
+    );
+    this.spaceRef.removeEventListener("mousewheel", (e: any) =>
+      e.preventDefault()
+    );
     document.removeEventListener("keydown", this.handleKeyDown);
     document.removeEventListener("keydown", this.handleKeyDown);
     document.removeEventListener("keyup", this.handleKeyUp);
     document.removeEventListener("keyup", this.handleKeyUp);
   }
   }
 
 
   // Handle shift key for multi-select
   // Handle shift key for multi-select
   handleKeyDown = (e: any) => {
   handleKeyDown = (e: any) => {
-    if (e.key === 'Shift') {
+    if (e.key === "Shift") {
       this.setState({
       this.setState({
         anchorX: this.state.cursorX,
         anchorX: this.state.cursorX,
         anchorY: this.state.cursorY,
         anchorY: this.state.cursorY,
@@ -230,13 +287,13 @@ export default class GraphDisplay extends Component<PropsType, StateType> {
         panX: null,
         panX: null,
         panY: null,
         panY: null,
         deltaX: null,
         deltaX: null,
-        deltaY: null
+        deltaY: null,
       });
       });
     }
     }
-  }
+  };
 
 
   handleKeyUp = (e: any) => {
   handleKeyUp = (e: any) => {
-    if (e.key === 'Shift') {
+    if (e.key === "Shift") {
       this.setState({
       this.setState({
         anchorX: null,
         anchorX: null,
         anchorY: null,
         anchorY: null,
@@ -245,16 +302,20 @@ export default class GraphDisplay extends Component<PropsType, StateType> {
         panX: null,
         panX: null,
         panY: null,
         panY: null,
         deltaX: null,
         deltaX: null,
-        deltaY: null
+        deltaY: null,
       });
       });
     }
     }
-  }
+  };
 
 
   handleClickNode = (clickedId: number) => {
   handleClickNode = (clickedId: number) => {
     let { cursorX, cursorY } = this.state;
     let { cursorX, cursorY } = this.state;
 
 
     // Store position for distinguishing click vs drag on release
     // Store position for distinguishing click vs drag on release
-    this.setState({ nodeClickX: cursorX, nodeClickY: cursorY, suppressCloseNode: true });
+    this.setState({
+      nodeClickX: cursorX,
+      nodeClickY: cursorY,
+      suppressCloseNode: true,
+    });
 
 
     // Push to activeIds if not already present
     // Push to activeIds if not already present
     let holding = this.state.activeIds;
     let holding = this.state.activeIds;
@@ -270,8 +331,12 @@ export default class GraphDisplay extends Component<PropsType, StateType> {
       }
       }
     });
     });
 
 
-    this.setState({ activeIds: holding, preventBgDrag: true, relocateAllowed: true });
-  }
+    this.setState({
+      activeIds: holding,
+      preventBgDrag: true,
+      relocateAllowed: true,
+    });
+  };
 
 
   handleReleaseNode = (node: NodeType) => {
   handleReleaseNode = (node: NodeType) => {
     let { cursorX, cursorY, nodeClickX, nodeClickY } = this.state;
     let { cursorX, cursorY, nodeClickX, nodeClickY } = this.state;
@@ -281,7 +346,7 @@ export default class GraphDisplay extends Component<PropsType, StateType> {
     if (cursorX === nodeClickX && cursorY === nodeClickY) {
     if (cursorX === nodeClickX && cursorY === nodeClickY) {
       this.setState({ openedNode: node });
       this.setState({ openedNode: node });
     }
     }
-  }
+  };
 
 
   handleMouseDown = () => {
   handleMouseDown = () => {
     let { cursorX, cursorY } = this.state;
     let { cursorX, cursorY } = this.state;
@@ -297,25 +362,48 @@ export default class GraphDisplay extends Component<PropsType, StateType> {
       deltaY: null,
       deltaY: null,
       panX: null,
       panX: null,
       panY: null,
       panY: null,
-      scale: 1
-    })
-  }
+      scale: 1,
+    });
+  };
 
 
   handleMouseUp = () => {
   handleMouseUp = () => {
-    let { cursorX, nodeClickX, cursorY, nodeClickY, suppressCloseNode } = this.state;
+    let {
+      cursorX,
+      nodeClickX,
+      cursorY,
+      nodeClickY,
+      suppressCloseNode,
+    } = this.state;
     this.setState({ dragBg: false, activeIds: [] });
     this.setState({ dragBg: false, activeIds: [] });
 
 
     // Distinguish bg click vs drag for setting closing opened node
     // Distinguish bg click vs drag for setting closing opened node
-    if (!suppressCloseNode && cursorX === nodeClickX && cursorY === nodeClickY) {
+    if (
+      !suppressCloseNode &&
+      cursorX === nodeClickX &&
+      cursorY === nodeClickY
+    ) {
       this.setState({ openedNode: null });
       this.setState({ openedNode: null });
     } else if (this.state.suppressCloseNode) {
     } else if (this.state.suppressCloseNode) {
       this.setState({ suppressCloseNode: false });
       this.setState({ suppressCloseNode: false });
     }
     }
-  }
+  };
 
 
   handleMouseMove = (e: any) => {
   handleMouseMove = (e: any) => {
-    let { originX, originY, dragBg, preventBgDrag, scale, panX, panY, anchorX, anchorY, nodes, activeIds, relocateAllowed } = this.state;
-    
+    let {
+      originX,
+      originY,
+      dragBg,
+      preventBgDrag,
+      scale,
+      panX,
+      panY,
+      anchorX,
+      anchorY,
+      nodes,
+      activeIds,
+      relocateAllowed,
+    } = this.state;
+
     // Suppress navigation gestures
     // Suppress navigation gestures
     if (scale !== 1 || panX !== 0 || panY !== 0) {
     if (scale !== 1 || panX !== 0 || panY !== 0) {
       this.setState({ scale: 1, panX: 0, panY: 0 });
       this.setState({ scale: 1, panX: 0, panY: 0 });
@@ -332,18 +420,21 @@ export default class GraphDisplay extends Component<PropsType, StateType> {
       this.setState({ deltaX: e.movementX, deltaY: e.movementY });
       this.setState({ deltaX: e.movementX, deltaY: e.movementY });
     }
     }
 
 
-    // Check if within select region 
+    // Check if within select region
     if (anchorX && anchorY) {
     if (anchorX && anchorY) {
       nodes.forEach((node: NodeType) => {
       nodes.forEach((node: NodeType) => {
-        if (node.x > Math.min(anchorX, cursorX) && node.x < Math.max(anchorX, cursorX)
-          && node.y > Math.min(anchorY, cursorY) && node.y < Math.max(anchorY, cursorY)
+        if (
+          node.x > Math.min(anchorX, cursorX) &&
+          node.x < Math.max(anchorX, cursorX) &&
+          node.y > Math.min(anchorY, cursorY) &&
+          node.y < Math.max(anchorY, cursorY)
         ) {
         ) {
           activeIds.push(node.id);
           activeIds.push(node.id);
           this.setState({ activeIds });
           this.setState({ activeIds });
         }
         }
       });
       });
-    } 
-  }
+    }
+  };
 
 
   // Handle pan XOR zoom (two-finger gestures count as onWheel)
   // Handle pan XOR zoom (two-finger gestures count as onWheel)
   handleWheel = (e: any) => {
   handleWheel = (e: any) => {
@@ -351,12 +442,11 @@ export default class GraphDisplay extends Component<PropsType, StateType> {
 
 
     // Prevent nav gestures if mouse is over InfoPanel or ButtonSection
     // Prevent nav gestures if mouse is over InfoPanel or ButtonSection
     if (!this.state.suppressDisplay) {
     if (!this.state.suppressDisplay) {
-
       // Pinch/zoom sets e.ctrlKey to true
       // Pinch/zoom sets e.ctrlKey to true
       if (e.ctrlKey) {
       if (e.ctrlKey) {
-  
         // Clip deltaY for extreme mousewheel values
         // Clip deltaY for extreme mousewheel values
-        let deltaY = e.deltaY >= 0 ? Math.min(40, e.deltaY) : Math.max(-40, e.deltaY);
+        let deltaY =
+          e.deltaY >= 0 ? Math.min(40, e.deltaY) : Math.max(-40, e.deltaY);
 
 
         let scale = 1;
         let scale = 1;
         scale -= deltaY * zoomConstant;
         scale -= deltaY * zoomConstant;
@@ -368,12 +458,12 @@ export default class GraphDisplay extends Component<PropsType, StateType> {
   };
   };
 
 
   btnZoomIn = () => {
   btnZoomIn = () => {
-    this.setState({ scale: 1.24, btnZooming: true});
-  }
+    this.setState({ scale: 1.24, btnZooming: true });
+  };
 
 
   btnZoomOut = () => {
   btnZoomOut = () => {
     this.setState({ scale: 0.76, btnZooming: true });
     this.setState({ scale: 0.76, btnZooming: true });
-  }
+  };
 
 
   toggleExpanded = () => {
   toggleExpanded = () => {
     this.setState({ isExpanded: !this.state.isExpanded }, () => {
     this.setState({ isExpanded: !this.state.isExpanded }, () => {
@@ -388,32 +478,48 @@ export default class GraphDisplay extends Component<PropsType, StateType> {
       }
       }
       this.setState({
       this.setState({
         originX: Math.round(width / 2) - nudge,
         originX: Math.round(width / 2) - nudge,
-        originY: Math.round(height / 2)
-      });  
+        originY: Math.round(height / 2),
+      });
     });
     });
-  }
+  };
 
 
   // Pass origin to node for offset
   // Pass origin to node for offset
   renderNodes = () => {
   renderNodes = () => {
-    let { activeIds, originX, originY, cursorX, cursorY, scale, panX, panY, anchorX, anchorY, relocateAllowed } = this.state;
+    let {
+      activeIds,
+      originX,
+      originY,
+      cursorX,
+      cursorY,
+      scale,
+      panX,
+      panY,
+      anchorX,
+      anchorY,
+      relocateAllowed,
+    } = this.state;
 
 
     let minX = 0;
     let minX = 0;
     let maxX = 0;
     let maxX = 0;
     let minY = 0;
     let minY = 0;
     let maxY = 0;
     let maxY = 0;
-    this.state.nodes.map((node: NodeType, i: number) => { 
-      if (node.x < minX) 
-      minX = (node.x < minX) ? node.x : minX;
-      maxX = (node.x > maxX) ? node.x : maxX;
-      minY = (node.y < minY) ? node.y : minY;
-      maxY = (node.y > maxY) ? node.y : maxY;
+    this.state.nodes.map((node: NodeType, i: number) => {
+      if (node.x < minX) minX = node.x < minX ? node.x : minX;
+      maxX = node.x > maxX ? node.x : maxX;
+      minY = node.y < minY ? node.y : minY;
+      maxY = node.y > maxY ? node.y : maxY;
     });
     });
-    let midX = (minX + maxX)/2;
-    let midY = (minY + maxY)/2;
+    let midX = (minX + maxX) / 2;
+    let midY = (minY + maxY) / 2;
 
 
     return this.state.nodes.map((node: NodeType, i: number) => {
     return this.state.nodes.map((node: NodeType, i: number) => {
       // Update position if not highlighting and active
       // Update position if not highlighting and active
-      if (activeIds.includes(node.id) && relocateAllowed && !anchorX && !anchorY) {
+      if (
+        activeIds.includes(node.id) &&
+        relocateAllowed &&
+        !anchorX &&
+        !anchorY
+      ) {
         node.x = cursorX + node.toCursorX;
         node.x = cursorX + node.toCursorX;
         node.y = cursorY + node.toCursorY;
         node.y = cursorY + node.toCursorY;
       }
       }
@@ -435,12 +541,12 @@ export default class GraphDisplay extends Component<PropsType, StateType> {
         }
         }
       }
       }
 
 
-      // Apply pan 
+      // Apply pan
       if (this.state.panX !== 0 || this.state.panY !== 0) {
       if (this.state.panX !== 0 || this.state.panY !== 0) {
         node.x -= panConstant * panX;
         node.x -= panConstant * panX;
         node.y += panConstant * panY;
         node.y += panConstant * panY;
       }
       }
-      
+
       return (
       return (
         <Node
         <Node
           key={i}
           key={i}
@@ -452,13 +558,14 @@ export default class GraphDisplay extends Component<PropsType, StateType> {
           isActive={activeIds.includes(node.id)}
           isActive={activeIds.includes(node.id)}
           showKindLabels={this.state.showKindLabels}
           showKindLabels={this.state.showKindLabels}
           isOpen={node === this.state.openedNode}
           isOpen={node === this.state.openedNode}
-
           // Parameterized to allow setting to null
           // Parameterized to allow setting to null
-          setCurrentNode={(node: NodeType) => this.setState({ currentNode: node })}
+          setCurrentNode={(node: NodeType) =>
+            this.setState({ currentNode: node })
+          }
         />
         />
       );
       );
     });
     });
-  }
+  };
 
 
   renderEdges = () => {
   renderEdges = () => {
     return this.state.edges.map((edge: EdgeType, i: number) => {
     return this.state.edges.map((edge: EdgeType, i: number) => {
@@ -472,11 +579,13 @@ export default class GraphDisplay extends Component<PropsType, StateType> {
           x2={this.state.nodes[edge.target].x}
           x2={this.state.nodes[edge.target].x}
           y2={this.state.nodes[edge.target].y}
           y2={this.state.nodes[edge.target].y}
           edge={edge}
           edge={edge}
-          setCurrentEdge={(edge: EdgeType) => this.setState({ currentEdge: edge })}
+          setCurrentEdge={(edge: EdgeType) =>
+            this.setState({ currentEdge: edge })
+          }
         />
         />
       );
       );
     });
     });
-  }
+  };
 
 
   renderSelectRegion = () => {
   renderSelectRegion = () => {
     if (this.state.anchorX && this.state.anchorY) {
     if (this.state.anchorX && this.state.anchorY) {
@@ -491,13 +600,13 @@ export default class GraphDisplay extends Component<PropsType, StateType> {
         />
         />
       );
       );
     }
     }
-  }
+  };
 
 
   render() {
   render() {
     return (
     return (
       <StyledGraphDisplay
       <StyledGraphDisplay
         isExpanded={this.state.isExpanded}
         isExpanded={this.state.isExpanded}
-        ref={element => this.spaceRef = element}
+        ref={(element) => (this.spaceRef = element)}
         onMouseMove={this.handleMouseMove}
         onMouseMove={this.handleMouseMove}
         onMouseDown={this.state.suppressDisplay ? null : this.handleMouseDown}
         onMouseDown={this.state.suppressDisplay ? null : this.handleMouseDown}
         onMouseUp={this.state.suppressDisplay ? null : this.handleMouseUp}
         onMouseUp={this.state.suppressDisplay ? null : this.handleMouseUp}
@@ -512,38 +621,37 @@ export default class GraphDisplay extends Component<PropsType, StateType> {
           onMouseLeave={() => this.setState({ suppressDisplay: false })}
           onMouseLeave={() => this.setState({ suppressDisplay: false })}
         >
         >
           <ToggleLabel
           <ToggleLabel
-            onClick={() => this.setState({ showKindLabels: !this.state.showKindLabels })}
+            onClick={() =>
+              this.setState({ showKindLabels: !this.state.showKindLabels })
+            }
           >
           >
             <Checkbox checked={this.state.showKindLabels}>
             <Checkbox checked={this.state.showKindLabels}>
-                <i className="material-icons">done</i>
+              <i className="material-icons">done</i>
             </Checkbox>
             </Checkbox>
             Show Type
             Show Type
           </ToggleLabel>
           </ToggleLabel>
-          <ExpandButton
-            onClick={this.toggleExpanded}
-          >
+          <ExpandButton onClick={this.toggleExpanded}>
             <i className="material-icons">
             <i className="material-icons">
-              {this.state.isExpanded ? 'close_fullscreen' : 'open_in_full'}
+              {this.state.isExpanded ? "close_fullscreen" : "open_in_full"}
             </i>
             </i>
           </ExpandButton>
           </ExpandButton>
         </ButtonSection>
         </ButtonSection>
         <InfoPanel
         <InfoPanel
-          setSuppressDisplay={(x: boolean) => this.setState({ suppressDisplay: x })}
+          setSuppressDisplay={(x: boolean) =>
+            this.setState({ suppressDisplay: x })
+          }
           currentNode={this.state.currentNode}
           currentNode={this.state.currentNode}
           currentEdge={this.state.currentEdge}
           currentEdge={this.state.currentEdge}
           openedNode={this.state.openedNode}
           openedNode={this.state.openedNode}
-
           // InfoPanel won't trigger onMouseLeave for unsuppressing if close is clicked
           // InfoPanel won't trigger onMouseLeave for unsuppressing if close is clicked
-          closeNode={() => this.setState({ openedNode: null, suppressDisplay: false })}
-
+          closeNode={() =>
+            this.setState({ openedNode: null, suppressDisplay: false })
+          }
           // For YAML wrapper to trigger resize
           // For YAML wrapper to trigger resize
           isExpanded={this.state.isExpanded}
           isExpanded={this.state.isExpanded}
           showRevisions={this.props.showRevisions}
           showRevisions={this.props.showRevisions}
         />
         />
-        <ZoomPanel
-          btnZoomIn={this.btnZoomIn}
-          btnZoomOut={this.btnZoomOut}
-        />
+        <ZoomPanel btnZoomIn={this.btnZoomIn} btnZoomOut={this.btnZoomOut} />
       </StyledGraphDisplay>
       </StyledGraphDisplay>
     );
     );
   }
   }
@@ -555,7 +663,8 @@ const Checkbox = styled.div`
   border: 1px solid #ffffff55;
   border: 1px solid #ffffff55;
   margin: 0px 8px 0px 3px;
   margin: 0px 8px 0px 3px;
   border-radius: 3px;
   border-radius: 3px;
-  background: ${(props: { checked: boolean }) => props.checked ? '#ffffff22' : ''};
+  background: ${(props: { checked: boolean }) =>
+    props.checked ? "#ffffff22" : ""};
   display: flex;
   display: flex;
   align-items: center;
   align-items: center;
   justify-content: center;
   justify-content: center;
@@ -564,12 +673,12 @@ const Checkbox = styled.div`
   > i {
   > i {
     font-size: 12px;
     font-size: 12px;
     padding-left: 0px;
     padding-left: 0px;
-    display: ${(props: { checked: boolean }) => props.checked ? '' : 'none'};
+    display: ${(props: { checked: boolean }) => (props.checked ? "" : "none")};
   }
   }
 `;
 `;
 
 
 const ToggleLabel = styled.div`
 const ToggleLabel = styled.div`
-  font: 12px 'Work Sans';
+  font: 12px "Work Sans";
   color: #ffffff;
   color: #ffffff;
   position: relative;
   position: relative;
   height: 24px;
   height: 24px;
@@ -611,7 +720,7 @@ const ExpandButton = styled.div`
   border: 1px solid #ffffff55;
   border: 1px solid #ffffff55;
 
 
   :hover {
   :hover {
-    background: #ffffff44; 
+    background: #ffffff44;
   }
   }
 
 
   > i {
   > i {
@@ -622,10 +731,14 @@ const ExpandButton = styled.div`
 const StyledGraphDisplay = styled.div`
 const StyledGraphDisplay = styled.div`
   overflow: hidden;
   overflow: hidden;
   cursor: move;
   cursor: move;
-  width: ${(props: { isExpanded: boolean }) => props.isExpanded ? '100vw' : '100%'};
-  height: ${(props: { isExpanded: boolean }) => props.isExpanded ? '100vh' : '100%'};
+  width: ${(props: { isExpanded: boolean }) =>
+    props.isExpanded ? "100vw" : "100%"};
+  height: ${(props: { isExpanded: boolean }) =>
+    props.isExpanded ? "100vh" : "100%"};
   background: #202227;
   background: #202227;
-  position: ${(props: { isExpanded: boolean }) => props.isExpanded ? 'fixed' : 'relative'};
-  top: ${(props: { isExpanded: boolean }) => props.isExpanded ? '-25px' : ''};
-  right: ${(props: { isExpanded: boolean }) => props.isExpanded ? '-25px' : ''};
-`;
+  position: ${(props: { isExpanded: boolean }) =>
+    props.isExpanded ? "fixed" : "relative"};
+  top: ${(props: { isExpanded: boolean }) => (props.isExpanded ? "-25px" : "")};
+  right: ${(props: { isExpanded: boolean }) =>
+    props.isExpanded ? "-25px" : ""};
+`;

+ 52 - 48
dashboard/src/main/home/cluster-dashboard/expanded-chart/graph/InfoPanel.tsx

@@ -1,48 +1,47 @@
-import React, { Component } from 'react';
-import styled from 'styled-components';
-import yaml from 'js-yaml';
+import React, { Component } from "react";
+import styled from "styled-components";
+import yaml from "js-yaml";
 
 
-import { kindToIcon, edgeColors } from 'shared/rosettaStone';
-import { NodeType, EdgeType } from 'shared/types';
+import { kindToIcon, edgeColors } from "shared/rosettaStone";
+import { NodeType, EdgeType } from "shared/types";
 
 
-import YamlEditor from 'components/YamlEditor';
+import YamlEditor from "components/YamlEditor";
 
 
 type PropsType = {
 type PropsType = {
-  currentNode: NodeType,
-  currentEdge: EdgeType,
-  openedNode: NodeType,
-  setSuppressDisplay: (x: boolean) => void,
-  closeNode: () => void,
-  isExpanded: boolean,
-  showRevisions: boolean
+  currentNode: NodeType;
+  currentEdge: EdgeType;
+  openedNode: NodeType;
+  setSuppressDisplay: (x: boolean) => void;
+  closeNode: () => void;
+  isExpanded: boolean;
+  showRevisions: boolean;
 };
 };
 
 
 type StateType = {
 type StateType = {
-  wrapperHeight: number
+  wrapperHeight: number;
 };
 };
 
 
 export default class InfoPanel extends Component<PropsType, StateType> {
 export default class InfoPanel extends Component<PropsType, StateType> {
   state = {
   state = {
-    wrapperHeight: 0
-  }
+    wrapperHeight: 0,
+  };
 
 
   renderIcon = (kind: string) => {
   renderIcon = (kind: string) => {
-
-    let icon = 'tonality';
+    let icon = "tonality";
     if (Object.keys(kindToIcon).includes(kind)) {
     if (Object.keys(kindToIcon).includes(kind)) {
-      icon = kindToIcon[kind]; 
+      icon = kindToIcon[kind];
     }
     }
-    
+
     return (
     return (
       <IconWrapper>
       <IconWrapper>
         <i className="material-icons">{icon}</i>
         <i className="material-icons">{icon}</i>
       </IconWrapper>
       </IconWrapper>
     );
     );
-  }
+  };
 
 
   renderColorBlock = (type: string) => {
   renderColorBlock = (type: string) => {
     return <ColorBlock color={edgeColors[type]} />;
     return <ColorBlock color={edgeColors[type]} />;
-  }
+  };
 
 
   wrapperRef: any = React.createRef();
   wrapperRef: any = React.createRef();
 
 
@@ -51,9 +50,11 @@ export default class InfoPanel extends Component<PropsType, StateType> {
   }
   }
 
 
   componentDidUpdate(prevProps: PropsType) {
   componentDidUpdate(prevProps: PropsType) {
-    if ((prevProps.openedNode !== this.props.openedNode 
-      || prevProps.isExpanded !== this.props.isExpanded
-      || prevProps.showRevisions !== this.props.showRevisions) && this.wrapperRef
+    if (
+      (prevProps.openedNode !== this.props.openedNode ||
+        prevProps.isExpanded !== this.props.isExpanded ||
+        prevProps.showRevisions !== this.props.showRevisions) &&
+      this.wrapperRef
     ) {
     ) {
       this.setState({ wrapperHeight: this.wrapperRef.offsetHeight });
       this.setState({ wrapperHeight: this.wrapperRef.offsetHeight });
     }
     }
@@ -67,27 +68,23 @@ export default class InfoPanel extends Component<PropsType, StateType> {
           <Div>
           <Div>
             {this.renderIcon(openedNode.kind)}
             {this.renderIcon(openedNode.kind)}
             {openedNode.kind}
             {openedNode.kind}
-            <ResourceName>
-              {openedNode.name}
-            </ResourceName>
+            <ResourceName>{openedNode.name}</ResourceName>
           </Div>
           </Div>
-          <YamlWrapper ref={element => this.wrapperRef = element}>
+          <YamlWrapper ref={(element) => (this.wrapperRef = element)}>
             <YamlEditor
             <YamlEditor
               value={yaml.dump(openedNode.RawYAML)}
               value={yaml.dump(openedNode.RawYAML)}
               readOnly={true}
               readOnly={true}
-              height={this.state.wrapperHeight + 'px'}
+              height={this.state.wrapperHeight + "px"}
             />
             />
           </YamlWrapper>
           </YamlWrapper>
         </Wrapped>
         </Wrapped>
-      )
+      );
     } else if (currentNode) {
     } else if (currentNode) {
       return (
       return (
         <Div>
         <Div>
           {this.renderIcon(currentNode.kind)}
           {this.renderIcon(currentNode.kind)}
           {currentNode.kind}
           {currentNode.kind}
-          <ResourceName>
-            {currentNode.name}
-          </ResourceName>
+          <ResourceName>{currentNode.name}</ResourceName>
         </Div>
         </Div>
       );
       );
     } else if (currentEdge) {
     } else if (currentEdge) {
@@ -96,7 +93,7 @@ export default class InfoPanel extends Component<PropsType, StateType> {
           {this.renderColorBlock(currentEdge.type)}
           {this.renderColorBlock(currentEdge.type)}
           {this.renderEdgeMessage(currentEdge)}
           {this.renderEdgeMessage(currentEdge)}
         </EdgeInfo>
         </EdgeInfo>
-      )
+      );
     }
     }
 
 
     return (
     return (
@@ -106,20 +103,20 @@ export default class InfoPanel extends Component<PropsType, StateType> {
         </IconWrapper>
         </IconWrapper>
         Hover over a node or edge to display info.
         Hover over a node or edge to display info.
       </Div>
       </Div>
-    )
-  }
+    );
+  };
 
 
   renderEdgeMessage = (edge: EdgeType) => {
   renderEdgeMessage = (edge: EdgeType) => {
     // TODO: render more information about edges (labels, spec property field)
     // TODO: render more information about edges (labels, spec property field)
-    switch(edge.type) {
+    switch (edge.type) {
       case "ControlRel":
       case "ControlRel":
-        return "Controller Relation"
+        return "Controller Relation";
       case "LabelRel":
       case "LabelRel":
-        return "Label Relation"
+        return "Label Relation";
       case "SpecRel":
       case "SpecRel":
-        return "Spec Relation"
+        return "Spec Relation";
     }
     }
-  }
+  };
 
 
   render() {
   render() {
     let { openedNode, closeNode, setSuppressDisplay } = this.props;
     let { openedNode, closeNode, setSuppressDisplay } = this.props;
@@ -133,7 +130,11 @@ export default class InfoPanel extends Component<PropsType, StateType> {
       >
       >
         {this.renderContents()}
         {this.renderContents()}
 
 
-        {openedNode ? <i onClick={closeNode} className="material-icons">close</i> : null}
+        {openedNode ? (
+          <i onClick={closeNode} className="material-icons">
+            close
+          </i>
+        ) : null}
       </StyledInfoPanel>
       </StyledInfoPanel>
     );
     );
   }
   }
@@ -163,7 +164,8 @@ const ColorBlock = styled.div`
   border-radius: 3px;
   border-radius: 3px;
   margin-left: -2px;
   margin-left: -2px;
   margin-right: 13px;
   margin-right: 13px;
-  background: ${(props: { color: string }) => props.color ? props.color : '#ffffff66'};
+  background: ${(props: { color: string }) =>
+    props.color ? props.color : "#ffffff66"};
 `;
 `;
 
 
 const Div = styled.div`
 const Div = styled.div`
@@ -209,11 +211,13 @@ const StyledInfoPanel = styled.div`
   right: 15px;
   right: 15px;
   bottom: 15px;
   bottom: 15px;
   color: #ffffff66;
   color: #ffffff66;
-  height: ${(props: { expanded: boolean }) => props.expanded ? 'calc(100% - 68px)' : '40px'};
-  width: ${(props: { expanded: boolean }) => props.expanded ? 'calc(50% - 68px)' : '400px'};
+  height: ${(props: { expanded: boolean }) =>
+    props.expanded ? "calc(100% - 68px)" : "40px"};
+  width: ${(props: { expanded: boolean }) =>
+    props.expanded ? "calc(50% - 68px)" : "400px"};
   max-width: 600px;
   max-width: 600px;
   min-width: 400px;
   min-width: 400px;
-  background: #34373Cdf;
+  background: #34373cdf;
   border-radius: 3px;
   border-radius: 3px;
   padding-left: 11px;
   padding-left: 11px;
   display: inline-block;
   display: inline-block;
@@ -238,4 +242,4 @@ const StyledInfoPanel = styled.div`
       background: #ffffff22;
       background: #ffffff22;
     }
     }
   }
   }
-`;
+`;

+ 34 - 38
dashboard/src/main/home/cluster-dashboard/expanded-chart/graph/Node.tsx

@@ -1,48 +1,44 @@
-import React, { Component } from 'react';
-import styled from 'styled-components';
+import React, { Component } from "react";
+import styled from "styled-components";
 
 
-import { kindToIcon } from 'shared/rosettaStone';
-import { NodeType } from 'shared/types';
+import { kindToIcon } from "shared/rosettaStone";
+import { NodeType } from "shared/types";
 
 
 type PropsType = {
 type PropsType = {
-  node: NodeType,
-  originX: number,
-  originY: number,
-  nodeMouseDown: () => void,
-  nodeMouseUp: () => void,
-  isActive: boolean,
-  showKindLabels: boolean,
-  setCurrentNode: (node: NodeType) => void,
-  isOpen: boolean
+  node: NodeType;
+  originX: number;
+  originY: number;
+  nodeMouseDown: () => void;
+  nodeMouseUp: () => void;
+  isActive: boolean;
+  showKindLabels: boolean;
+  setCurrentNode: (node: NodeType) => void;
+  isOpen: boolean;
 };
 };
 
 
-type StateType = {
-};
+type StateType = {};
 
 
 export default class Node extends Component<PropsType, StateType> {
 export default class Node extends Component<PropsType, StateType> {
-  state = {
-  }
+  state = {};
 
 
   render() {
   render() {
     let { x, y, w, h, name, kind } = this.props.node;
     let { x, y, w, h, name, kind } = this.props.node;
     let { originX, originY, nodeMouseDown, nodeMouseUp, isActive } = this.props;
     let { originX, originY, nodeMouseDown, nodeMouseUp, isActive } = this.props;
 
 
-    let icon = 'tonality';
+    let icon = "tonality";
     if (Object.keys(kindToIcon).includes(kind)) {
     if (Object.keys(kindToIcon).includes(kind)) {
-      icon = kindToIcon[kind]; 
+      icon = kindToIcon[kind];
     }
     }
 
 
     return (
     return (
       <StyledNode
       <StyledNode
-        x={Math.round(originX + x - (w / 2))}
-        y={Math.round(originY - y - (h / 2))}
+        x={Math.round(originX + x - w / 2)}
+        y={Math.round(originY - y - h / 2)}
         w={Math.round(w)}
         w={Math.round(w)}
         h={Math.round(h)}
         h={Math.round(h)}
       >
       >
-        <Kind>
-          {this.props.showKindLabels ? kind : null}
-        </Kind>
-        <NodeBlock 
+        <Kind>{this.props.showKindLabels ? kind : null}</Kind>
+        <NodeBlock
           onMouseDown={nodeMouseDown}
           onMouseDown={nodeMouseDown}
           onMouseUp={nodeMouseUp}
           onMouseUp={nodeMouseUp}
           onMouseEnter={() => this.props.setCurrentNode(this.props.node)}
           onMouseEnter={() => this.props.setCurrentNode(this.props.node)}
@@ -52,9 +48,7 @@ export default class Node extends Component<PropsType, StateType> {
         >
         >
           <i className="material-icons">{icon}</i>
           <i className="material-icons">{icon}</i>
         </NodeBlock>
         </NodeBlock>
-        <NodeLabel>
-          {name}
-        </NodeLabel>
+        <NodeLabel>{name}</NodeLabel>
       </StyledNode>
       </StyledNode>
     );
     );
   }
   }
@@ -70,7 +64,7 @@ const Kind = styled.div`
   min-width: 1px;
   min-width: 1px;
   height: 25px;
   height: 25px;
   font-size: 13px;
   font-size: 13px;
-  font-family: 'Work Sans', sans-serif;
+  font-family: "Work Sans", sans-serif;
   white-space: nowrap;
   white-space: nowrap;
   overflow: hidden;
   overflow: hidden;
   text-overflow: ellipsis;
   text-overflow: ellipsis;
@@ -84,7 +78,7 @@ const NodeLabel = styled.div`
   color: #aaaabb;
   color: #aaaabb;
   max-width: 140px;
   max-width: 140px;
   font-size: 13px;
   font-size: 13px;
-  font-family: 'Work Sans', sans-serif;
+  font-family: "Work Sans", sans-serif;
   text-align: center;
   text-align: center;
   white-space: nowrap;
   white-space: nowrap;
   overflow: hidden;
   overflow: hidden;
@@ -100,8 +94,10 @@ const NodeBlock = styled.div`
   align-items: center;
   align-items: center;
   justify-content: center;
   justify-content: center;
   border-radius: 100px;
   border-radius: 100px;
-  border: ${(props: { isActive: boolean, isOpen: boolean }) => props.isOpen ? '3px solid #ffffff' : ''};
-  box-shadow: ${(props: { isActive: boolean, isOpen: boolean }) => props.isActive ? '0 0 10px #ffffff66' : '0px 0px 10px 2px #00000022'};
+  border: ${(props: { isActive: boolean; isOpen: boolean }) =>
+    props.isOpen ? "3px solid #ffffff" : ""};
+  box-shadow: ${(props: { isActive: boolean; isOpen: boolean }) =>
+    props.isActive ? "0 0 10px #ffffff66" : "0px 0px 10px 2px #00000022"};
   z-index: 100;
   z-index: 100;
   cursor: pointer;
   cursor: pointer;
   :hover {
   :hover {
@@ -115,16 +111,16 @@ const NodeBlock = styled.div`
 
 
 const StyledNode: any = styled.div.attrs((props: NodeType) => ({
 const StyledNode: any = styled.div.attrs((props: NodeType) => ({
   style: {
   style: {
-    top: props.y + 'px',
-    left: props.x + 'px',
-    },
+    top: props.y + "px",
+    left: props.x + "px",
+  },
 }))`
 }))`
   position: absolute;
   position: absolute;
-  width: ${(props: NodeType) => props.w + 'px'};
-  height: ${(props: NodeType) => props.h + 'px'};
+  width: ${(props: NodeType) => props.w + "px"};
+  height: ${(props: NodeType) => props.h + "px"};
   color: #ffffff22;
   color: #ffffff22;
   border-radius: 100px;
   border-radius: 100px;
   display: flex;
   display: flex;
   flex-direction: column;
   flex-direction: column;
   align-items: center;
   align-items: center;
-`;
+`;

+ 21 - 21
dashboard/src/main/home/cluster-dashboard/expanded-chart/graph/SelectRegion.tsx

@@ -1,25 +1,23 @@
-import React, { Component } from 'react';
-import styled from 'styled-components';
+import React, { Component } from "react";
+import styled from "styled-components";
 
 
 type PropsType = {
 type PropsType = {
-  anchorX: number,
-  anchorY: number,
-  originX: number,
-  originY: number,
-  cursorX: number,
-  cursorY: number
+  anchorX: number;
+  anchorY: number;
+  originX: number;
+  originY: number;
+  cursorX: number;
+  cursorY: number;
 };
 };
 
 
-type StateType = {
-};
+type StateType = {};
 
 
 export default class SelectRegion extends Component<PropsType, StateType> {
 export default class SelectRegion extends Component<PropsType, StateType> {
-  state = {
-  }
+  state = {};
 
 
   render() {
   render() {
     let { cursorX, cursorY, anchorX, anchorY, originX, originY } = this.props;
     let { cursorX, cursorY, anchorX, anchorY, originX, originY } = this.props;
-    
+
     var x, y, w, h;
     var x, y, w, h;
     if (cursorY < anchorY) {
     if (cursorY < anchorY) {
       y = anchorY;
       y = anchorY;
@@ -46,15 +44,17 @@ export default class SelectRegion extends Component<PropsType, StateType> {
   }
   }
 }
 }
 
 
-const StyledSelectRegion: any = styled.div.attrs((props: { x: number, y: number, w: number, h: number }) => ({
-  style: {
-    top: props.y + 'px',
-    left: props.x + 'px',
-    width: props.w + 'px',
-    height: props.h + 'px'
+const StyledSelectRegion: any = styled.div.attrs(
+  (props: { x: number; y: number; w: number; h: number }) => ({
+    style: {
+      top: props.y + "px",
+      left: props.x + "px",
+      width: props.w + "px",
+      height: props.h + "px",
     },
     },
-}))`
+  })
+)`
   position: absolute;
   position: absolute;
   background: #ffffff22;
   background: #ffffff22;
   z-index: 1;
   z-index: 1;
-`;
+`;

+ 12 - 16
dashboard/src/main/home/cluster-dashboard/expanded-chart/graph/ZoomPanel.tsx

@@ -1,19 +1,19 @@
-import React, { Component } from 'react';
-import styled from 'styled-components';
+import React, { Component } from "react";
+import styled from "styled-components";
 
 
 type PropsType = {
 type PropsType = {
-    btnZoomIn: () => void,
-    btnZoomOut: () => void,
+  btnZoomIn: () => void;
+  btnZoomOut: () => void;
 };
 };
 
 
 type StateType = {
 type StateType = {
-  wrapperHeight: number
+  wrapperHeight: number;
 };
 };
 
 
 export default class ZoomPanel extends Component<PropsType, StateType> {
 export default class ZoomPanel extends Component<PropsType, StateType> {
   state = {
   state = {
-    wrapperHeight: 0
-  }
+    wrapperHeight: 0,
+  };
 
 
   wrapperRef: any = React.createRef();
   wrapperRef: any = React.createRef();
 
 
@@ -32,15 +32,11 @@ export default class ZoomPanel extends Component<PropsType, StateType> {
           <i className="material-icons">remove</i>
           <i className="material-icons">remove</i>
         </IconWrapper>
         </IconWrapper>
       </Div>
       </Div>
-    )
-  }
+    );
+  };
 
 
   render() {
   render() {
-    return (
-      <StyledZoomer>
-        {this.renderContents()}
-      </StyledZoomer>
-    );
+    return <StyledZoomer>{this.renderContents()}</StyledZoomer>;
   }
   }
 }
 }
 
 
@@ -75,7 +71,7 @@ const StyledZoomer = styled.div`
   color: #ffffff;
   color: #ffffff;
   height: 64px;
   height: 64px;
   width: 36px;
   width: 36px;
-  background: #34373Cdf;
+  background: #34373cdf;
   border-radius: 3px;
   border-radius: 3px;
   padding-left: 11px;
   padding-left: 11px;
   display: inline-block;
   display: inline-block;
@@ -92,4 +88,4 @@ const ZoomBreaker = styled.div`
   background: #ffffff20;
   background: #ffffff20;
   height: 1px;
   height: 1px;
   width: 22px;
   width: 22px;
-`;
+`;

+ 140 - 113
dashboard/src/main/home/cluster-dashboard/expanded-chart/status/ControllerTab.tsx

@@ -1,22 +1,22 @@
-import React, { Component } from 'react';
-import styled from 'styled-components';
-import api from 'shared/api';
-import { Context } from 'shared/Context';
+import React, { Component } from "react";
+import styled from "styled-components";
+import api from "shared/api";
+import { Context } from "shared/Context";
 
 
-import ResourceTab from 'components/ResourceTab';
+import ResourceTab from "components/ResourceTab";
 
 
 type PropsType = {
 type PropsType = {
-  controller: any,
-  selectedPod: any,
-  selectPod: Function,
-  isLast?: boolean,
-  isFirst?: boolean,
+  controller: any;
+  selectedPod: any;
+  selectPod: Function;
+  isLast?: boolean;
+  isFirst?: boolean;
 };
 };
 
 
 type StateType = {
 type StateType = {
-  pods: any[],
-  raw: any[],
-  showTooltip: boolean[],
+  pods: any[];
+  raw: any[];
+  showTooltip: boolean[];
 };
 };
 
 
 // Controller tab in log section that displays list of pods on click.
 // Controller tab in log section that displays list of pods on click.
@@ -25,54 +25,60 @@ export default class ControllerTab extends Component<PropsType, StateType> {
     pods: [] as any[],
     pods: [] as any[],
     raw: [] as any[],
     raw: [] as any[],
     showTooltip: [] as boolean[],
     showTooltip: [] as boolean[],
-  }
+  };
 
 
   componentDidMount() {
   componentDidMount() {
     let { currentCluster, currentProject, setCurrentError } = this.context;
     let { currentCluster, currentProject, setCurrentError } = this.context;
     let { controller, selectPod, isFirst } = this.props;
     let { controller, selectPod, isFirst } = this.props;
 
 
     let selectors = [] as string[];
     let selectors = [] as string[];
-    let ml = controller?.spec?.selector?.matchLabels || controller?.spec?.selector;
+    let ml =
+      controller?.spec?.selector?.matchLabels || controller?.spec?.selector;
     let i = 1;
     let i = 1;
-    let selector = '';
+    let selector = "";
     for (var key in ml) {
     for (var key in ml) {
-      selector += key + '=' + ml[key];
+      selector += key + "=" + ml[key];
       if (i != Object.keys(ml).length) {
       if (i != Object.keys(ml).length) {
-        selector += ',';
+        selector += ",";
       }
       }
       i += 1;
       i += 1;
     }
     }
     selectors.push(selector);
     selectors.push(selector);
-    
-    api.getMatchingPods('<token>', { 
-      cluster_id: currentCluster.id,
-      selectors,
-    }, {
-      id: currentProject.id
-    }, (err: any, res: any) => {
-      if (err) {
-        console.log(err);
-        setCurrentError(JSON.stringify(err));
-        return
-      }
-      let pods = res?.data?.map((pod: any) => {
-        return {
-          namespace: pod?.metadata?.namespace, 
-          name: pod?.metadata?.name,
-          phase: pod?.status?.phase,
+
+    api.getMatchingPods(
+      "<token>",
+      {
+        cluster_id: currentCluster.id,
+        selectors,
+      },
+      {
+        id: currentProject.id,
+      },
+      (err: any, res: any) => {
+        if (err) {
+          console.log(err);
+          setCurrentError(JSON.stringify(err));
+          return;
+        }
+        let pods = res?.data?.map((pod: any) => {
+          return {
+            namespace: pod?.metadata?.namespace,
+            name: pod?.metadata?.name,
+            phase: pod?.status?.phase,
+          };
+        });
+        let showTooltip = new Array(pods.length);
+        for (let j = 0; j < pods.length; j++) {
+          showTooltip[j] = false;
+        }
+
+        this.setState({ pods, raw: res.data, showTooltip });
+
+        if (isFirst) {
+          selectPod(res.data[0]);
         }
         }
-      });
-      let showTooltip = new Array(pods.length);
-      for (let j = 0; j < pods.length; j ++) {
-        showTooltip[j] = false;
-      }
-      
-      this.setState({ pods, raw: res.data, showTooltip });
-      
-      if (isFirst) {
-        selectPod(res.data[0])
       }
       }
-    })
+    );
   }
   }
 
 
   getAvailability = (kind: string, c: any) => {
   getAvailability = (kind: string, c: any) => {
@@ -80,50 +86,55 @@ export default class ControllerTab extends Component<PropsType, StateType> {
       case "deployment":
       case "deployment":
       case "replicaset":
       case "replicaset":
         return [
         return [
-          c.status?.availableReplicas || c.status?.replicas - c.status?.unavailableReplicas || 0, 
-          c.status?.replicas || 0
-        ]
+          c.status?.availableReplicas ||
+            c.status?.replicas - c.status?.unavailableReplicas ||
+            0,
+          c.status?.replicas || 0,
+        ];
       case "statefulset":
       case "statefulset":
-       return [c.status?.readyReplicas || 0, c.status?.replicas || 0]
+        return [c.status?.readyReplicas || 0, c.status?.replicas || 0];
       case "daemonset":
       case "daemonset":
-        return [c.status?.numberAvailable || 0, c.status?.desiredNumberScheduled || 0]
-      }
-  }
+        return [
+          c.status?.numberAvailable || 0,
+          c.status?.desiredNumberScheduled || 0,
+        ];
+    }
+  };
 
 
   getPodStatus = (status: any) => {
   getPodStatus = (status: any) => {
-    if (status?.phase == 'Pending' && status?.containerStatuses !== undefined) {
-      return status.containerStatuses[0].state.waiting.reason
+    if (status?.phase == "Pending" && status?.containerStatuses !== undefined) {
+      return status.containerStatuses[0].state.waiting.reason;
       // return 'waiting'
       // return 'waiting'
     }
     }
 
 
-    if (status?.phase == 'Failed') {
-      return 'failed'
+    if (status?.phase == "Failed") {
+      return "failed";
     }
     }
 
 
-    if (status?.phase == 'Running') {
-      let collatedStatus = 'running';
+    if (status?.phase == "Running") {
+      let collatedStatus = "running";
 
 
       status?.containerStatuses?.forEach((s: any) => {
       status?.containerStatuses?.forEach((s: any) => {
         if (s.state?.waiting) {
         if (s.state?.waiting) {
-          collatedStatus = 'waiting'
+          collatedStatus = "waiting";
         } else if (s.state?.terminated) {
         } else if (s.state?.terminated) {
-          collatedStatus = 'failed'
+          collatedStatus = "failed";
         }
         }
-      })
+      });
       return collatedStatus;
       return collatedStatus;
     }
     }
-  }
+  };
 
 
   renderTooltip = (x: string, ind: number): JSX.Element | undefined => {
   renderTooltip = (x: string, ind: number): JSX.Element | undefined => {
     if (this.state.showTooltip[ind]) {
     if (this.state.showTooltip[ind]) {
       return <Tooltip>{x}</Tooltip>;
       return <Tooltip>{x}</Tooltip>;
     }
     }
-  }
+  };
 
 
   render() {
   render() {
     let { controller, selectedPod, isLast, selectPod, isFirst } = this.props;
     let { controller, selectedPod, isLast, selectPod, isFirst } = this.props;
     let [available, total] = this.getAvailability(controller.kind, controller);
     let [available, total] = this.getAvailability(controller.kind, controller);
-    let status = (available == total) ? 'running' : 'waiting'
+    let status = available == total ? "running" : "waiting";
     return (
     return (
       <ResourceTab
       <ResourceTab
         label={controller.kind}
         label={controller.kind}
@@ -132,43 +143,43 @@ export default class ControllerTab extends Component<PropsType, StateType> {
         isLast={isLast}
         isLast={isLast}
         expanded={isFirst}
         expanded={isFirst}
       >
       >
-        {
-          this.state.raw.map((pod, i) => {
-            let status = this.getPodStatus(pod.status);
-            return (
-              <Tab 
-                key={pod.metadata?.name}
-                selected={selectedPod?.metadata?.name === pod?.metadata?.name}
-                onClick={() => { selectPod(pod)} }
-              > 
-                <Gutter>
-                  <Rail />
-                  <Circle />
-                  <Rail lastTab={i === this.state.raw.length - 1} />
-                </Gutter>
-                <Name
-                  onMouseOver={() => {
-                    let showTooltip = this.state.showTooltip;
-                    showTooltip[i] = true;
-                    this.setState({ showTooltip });
-                  }}
-                  onMouseOut={() => {
-                    let showTooltip = this.state.showTooltip;
-                    showTooltip[i] = false;
-                    this.setState({ showTooltip });
-                  }}
-                >
-                  {pod.metadata?.name}
-                </Name>
-                {this.renderTooltip(pod.metadata?.name, i)}
-                <Status>
-                  <StatusColor status={status} />
-                  {status}
-                </Status>
-              </Tab>
-            );
-          })
-        }
+        {this.state.raw.map((pod, i) => {
+          let status = this.getPodStatus(pod.status);
+          return (
+            <Tab
+              key={pod.metadata?.name}
+              selected={selectedPod?.metadata?.name === pod?.metadata?.name}
+              onClick={() => {
+                selectPod(pod);
+              }}
+            >
+              <Gutter>
+                <Rail />
+                <Circle />
+                <Rail lastTab={i === this.state.raw.length - 1} />
+              </Gutter>
+              <Name
+                onMouseOver={() => {
+                  let showTooltip = this.state.showTooltip;
+                  showTooltip[i] = true;
+                  this.setState({ showTooltip });
+                }}
+                onMouseOut={() => {
+                  let showTooltip = this.state.showTooltip;
+                  showTooltip[i] = false;
+                  this.setState({ showTooltip });
+                }}
+              >
+                {pod.metadata?.name}
+              </Name>
+              {this.renderTooltip(pod.metadata?.name, i)}
+              <Status>
+                <StatusColor status={status} />
+                {status}
+              </Status>
+            </Tab>
+          );
+        })}
       </ResourceTab>
       </ResourceTab>
     );
     );
   }
   }
@@ -178,7 +189,8 @@ ControllerTab.contextType = Context;
 
 
 const Rail = styled.div`
 const Rail = styled.div`
   width: 2px;
   width: 2px;
-  background: ${(props: { lastTab?: boolean }) => props.lastTab ? '' : '#52545D'};
+  background: ${(props: { lastTab?: boolean }) =>
+    props.lastTab ? "" : "#52545D"};
   height: 50%;
   height: 50%;
 `;
 `;
 
 
@@ -187,7 +199,7 @@ const Circle = styled.div`
   min-height: 2px;
   min-height: 2px;
   margin-bottom: -2px;
   margin-bottom: -2px;
   margin-left: 8px;
   margin-left: 8px;
-  background: #52545D;
+  background: #52545d;
 `;
 `;
 
 
 const Gutter = styled.div`
 const Gutter = styled.div`
@@ -209,12 +221,16 @@ const Status = styled.div`
   text-transform: capitalize;
   text-transform: capitalize;
   justify-content: flex-end;
   justify-content: flex-end;
   align-items: center;
   align-items: center;
-  font-family: 'Work Sans', sans-serif;
+  font-family: "Work Sans", sans-serif;
   color: #aaaabb;
   color: #aaaabb;
   animation: fadeIn 0.5s;
   animation: fadeIn 0.5s;
   @keyframes fadeIn {
   @keyframes fadeIn {
-    from { opacity: 0 }
-    to { opacity: 1 }
+    from {
+      opacity: 0;
+    }
+    to {
+      opacity: 1;
+    }
   }
   }
 `;
 `;
 
 
@@ -223,7 +239,12 @@ const StatusColor = styled.div`
   width: 7px;
   width: 7px;
   min-width: 7px;
   min-width: 7px;
   height: 7px;
   height: 7px;
-  background: ${(props: { status: string }) => (props.status === 'running' ? '#4797ff' : props.status === 'failed' ? "#ed5f85" : "#f5cb42")};
+  background: ${(props: { status: string }) =>
+    props.status === "running"
+      ? "#4797ff"
+      : props.status === "failed"
+      ? "#ed5f85"
+      : "#f5cb42"};
   border-radius: 20px;
   border-radius: 20px;
 `;
 `;
 
 
@@ -260,8 +281,12 @@ const Tooltip = styled.div`
   animation: faded-in 0.2s 0.15s;
   animation: faded-in 0.2s 0.15s;
   animation-fill-mode: forwards;
   animation-fill-mode: forwards;
   @keyframes faded-in {
   @keyframes faded-in {
-    from { opacity: 0 }
-    to { opacity: 1 }
+    from {
+      opacity: 0;
+    }
+    to {
+      opacity: 1;
+    }
   }
   }
 `;
 `;
 
 
@@ -272,8 +297,10 @@ const Tab = styled.div`
   display: flex;
   display: flex;
   align-items: center;
   align-items: center;
   justify-content: space-between;
   justify-content: space-between;
-  color: ${(props: {selected: boolean}) => props.selected ? 'white' : '#ffffff66'};
-  background: ${(props: {selected: boolean}) => props.selected ? '#ffffff18' : ''};
+  color: ${(props: { selected: boolean }) =>
+    props.selected ? "white" : "#ffffff66"};
+  background: ${(props: { selected: boolean }) =>
+    props.selected ? "#ffffff18" : ""};
   font-size: 13px;
   font-size: 13px;
   padding: 20px 19px 20px 42px;
   padding: 20px 19px 20px 42px;
   text-shadow: 0px 0px 8px none;
   text-shadow: 0px 0px 8px none;
@@ -283,4 +310,4 @@ const Tab = styled.div`
     color: white;
     color: white;
     background: #ffffff18;
     background: #ffffff18;
   }
   }
-`;
+`;

+ 68 - 55
dashboard/src/main/home/cluster-dashboard/expanded-chart/status/Logs.tsx

@@ -1,116 +1,129 @@
-import React, { Component } from 'react';
-import styled from 'styled-components';
-import { Context } from 'shared/Context';
+import React, { Component } from "react";
+import styled from "styled-components";
+import { Context } from "shared/Context";
 
 
 type PropsType = {
 type PropsType = {
-  selectedPod: any,
+  selectedPod: any;
 };
 };
 
 
 type StateType = {
 type StateType = {
-  logs: string[],
-  ws: any,
-  scroll: boolean,
+  logs: string[];
+  ws: any;
+  scroll: boolean;
 };
 };
 
 
 export default class Logs extends Component<PropsType, StateType> {
 export default class Logs extends Component<PropsType, StateType> {
-  
   state = {
   state = {
     logs: [] as string[],
     logs: [] as string[],
-    ws : null as any,
+    ws: null as any,
     scroll: true,
     scroll: true,
-  }
+  };
 
 
   ws = null as any;
   ws = null as any;
-  parentRef = React.createRef<HTMLDivElement>()
+  parentRef = React.createRef<HTMLDivElement>();
 
 
   scrollToBottom = (smooth: boolean) => {
   scrollToBottom = (smooth: boolean) => {
     if (smooth) {
     if (smooth) {
-      this.parentRef.current.lastElementChild.scrollIntoView({ behavior: "smooth" })
+      this.parentRef.current.lastElementChild.scrollIntoView({
+        behavior: "smooth",
+      });
     } else {
     } else {
-      this.parentRef.current.lastElementChild.scrollIntoView({ behavior: "auto" })
+      this.parentRef.current.lastElementChild.scrollIntoView({
+        behavior: "auto",
+      });
     }
     }
-  }
+  };
 
 
   renderLogs = () => {
   renderLogs = () => {
     let { selectedPod } = this.props;
     let { selectedPod } = this.props;
     if (!selectedPod?.metadata?.name) {
     if (!selectedPod?.metadata?.name) {
-      return <Message>Please select a pod to view its logs.</Message>
+      return <Message>Please select a pod to view its logs.</Message>;
     }
     }
     if (this.state.logs.length == 0) {
     if (this.state.logs.length == 0) {
-      return <Message>No logs to display from this pod.</Message>
+      return <Message>No logs to display from this pod.</Message>;
     }
     }
     return this.state.logs.map((log, i) => {
     return this.state.logs.map((log, i) => {
-        return <Log key={i}>{log}</Log>
-    })
-  }
+      return <Log key={i}>{log}</Log>;
+    });
+  };
 
 
-  setupWebsocket = () => {  
+  setupWebsocket = () => {
     let { currentCluster, currentProject } = this.context;
     let { currentCluster, currentProject } = this.context;
     let { selectedPod } = this.props;
     let { selectedPod } = this.props;
-    if (!selectedPod.metadata?.name) return
-    let protocol = process.env.NODE_ENV == 'production' ? 'wss' : 'ws'
-    this.ws = new WebSocket(`${protocol}://${process.env.API_SERVER}/api/projects/${currentProject.id}/k8s/${selectedPod?.metadata?.namespace}/pod/${selectedPod?.metadata?.name}/logs?cluster_id=${currentCluster.id}&service_account_id=${currentCluster.service_account_id}`)
+    if (!selectedPod.metadata?.name) return;
+    let protocol = process.env.NODE_ENV == "production" ? "wss" : "ws";
+    this.ws = new WebSocket(
+      `${protocol}://${process.env.API_SERVER}/api/projects/${currentProject.id}/k8s/${selectedPod?.metadata?.namespace}/pod/${selectedPod?.metadata?.name}/logs?cluster_id=${currentCluster.id}&service_account_id=${currentCluster.service_account_id}`
+    );
 
 
     this.ws.onopen = () => {
     this.ws.onopen = () => {
-      console.log('connected to websocket')
-    }
+      console.log("connected to websocket");
+    };
 
 
     this.ws.onmessage = (evt: MessageEvent) => {
     this.ws.onmessage = (evt: MessageEvent) => {
       this.setState({ logs: [...this.state.logs, evt.data] }, () => {
       this.setState({ logs: [...this.state.logs, evt.data] }, () => {
         if (this.state.scroll) {
         if (this.state.scroll) {
-          this.scrollToBottom(false)
+          this.scrollToBottom(false);
         }
         }
-      })
-    }
+      });
+    };
 
 
     this.ws.onerror = (err: ErrorEvent) => {
     this.ws.onerror = (err: ErrorEvent) => {
-      console.log("websocket error:", err)
-    }
+      console.log("websocket error:", err);
+    };
 
 
     this.ws.onclose = () => {
     this.ws.onclose = () => {
-      console.log("closing pod logs")
-    }
-  }
+      console.log("closing pod logs");
+    };
+  };
 
 
   refreshLogs = () => {
   refreshLogs = () => {
     if (this.ws) {
     if (this.ws) {
       this.ws.close();
       this.ws.close();
       this.ws = null;
       this.ws = null;
-      this.setState({logs: []})
+      this.setState({ logs: [] });
       this.setupWebsocket();
       this.setupWebsocket();
     }
     }
-  }
+  };
 
 
   componentDidMount() {
   componentDidMount() {
-    this.setupWebsocket()
+    this.setupWebsocket();
     this.scrollToBottom(false);
     this.scrollToBottom(false);
   }
   }
 
 
   componentWillUnmount() {
   componentWillUnmount() {
-    console.log('log unmount')
+    console.log("log unmount");
     if (this.ws) {
     if (this.ws) {
-      this.ws.close()
+      this.ws.close();
     }
     }
   }
   }
 
 
   render() {
   render() {
     return (
     return (
       <LogStream>
       <LogStream>
-        <Wrapper ref={this.parentRef}>
-          {this.renderLogs()}
-        </Wrapper>
+        <Wrapper ref={this.parentRef}>{this.renderLogs()}</Wrapper>
         <Options>
         <Options>
-          <Scroll onClick={()=> {
-            this.setState({scroll: !this.state.scroll}, () => {
-              if (this.state.scroll) {
-                this.scrollToBottom(true)
-              }
-            }); 
-          }}>
-            <input type="checkbox" checked={this.state.scroll} onChange={() => {}}/>
+          <Scroll
+            onClick={() => {
+              this.setState({ scroll: !this.state.scroll }, () => {
+                if (this.state.scroll) {
+                  this.scrollToBottom(true);
+                }
+              });
+            }}
+          >
+            <input
+              type="checkbox"
+              checked={this.state.scroll}
+              onChange={() => {}}
+            />
             Scroll to Bottom
             Scroll to Bottom
           </Scroll>
           </Scroll>
-          <Refresh onClick={() => {this.refreshLogs()}}>
+          <Refresh
+            onClick={() => {
+              this.refreshLogs();
+            }}
+          >
             <i className="material-icons">autorenew</i>
             <i className="material-icons">autorenew</i>
             Refresh
             Refresh
           </Refresh>
           </Refresh>
@@ -139,7 +152,7 @@ const Scroll = styled.div`
     margin-right: 6px;
     margin-right: 6px;
     pointer-events: none;
     pointer-events: none;
   }
   }
-`
+`;
 
 
 const Refresh = styled.div`
 const Refresh = styled.div`
   display: flex;
   display: flex;
@@ -158,7 +171,7 @@ const Refresh = styled.div`
   :hover {
   :hover {
     background: #2468d6;
     background: #2468d6;
   }
   }
-`
+`;
 
 
 const Options = styled.div`
 const Options = styled.div`
   width: 100%;
   width: 100%;
@@ -168,7 +181,7 @@ const Options = styled.div`
   flex-direction: row;
   flex-direction: row;
   align-items: center;
   align-items: center;
   justify-content: space-between;
   justify-content: space-between;
-`
+`;
 
 
 const Wrapper = styled.div`
 const Wrapper = styled.div`
   width: 100%;
   width: 100%;
@@ -187,7 +200,7 @@ const LogStream = styled.div`
   user-select: text;
   user-select: text;
   max-width: 65%;
   max-width: 65%;
   overflow-y: auto;
   overflow-y: auto;
-  overflow-wrap: break-word; 
+  overflow-wrap: break-word;
 `;
 `;
 
 
 const Message = styled.div`
 const Message = styled.div`
@@ -202,4 +215,4 @@ const Message = styled.div`
 
 
 const Log = styled.div`
 const Log = styled.div`
   font-family: monospace;
   font-family: monospace;
-`;
+`;

+ 64 - 60
dashboard/src/main/home/cluster-dashboard/expanded-chart/status/StatusSection.tsx

@@ -1,25 +1,25 @@
-import React, { Component } from 'react';
-import styled from 'styled-components';
+import React, { Component } from "react";
+import styled from "styled-components";
 
 
-import api from 'shared/api';
-import { Context } from 'shared/Context';
-import { ChartType, StorageType } from 'shared/types';
-import Loading from 'components/Loading';
+import api from "shared/api";
+import { Context } from "shared/Context";
+import { ChartType, StorageType } from "shared/types";
+import Loading from "components/Loading";
 
 
-import Logs from './Logs';
-import ControllerTab from './ControllerTab';
+import Logs from "./Logs";
+import ControllerTab from "./ControllerTab";
 
 
 type PropsType = {
 type PropsType = {
-  selectors: string[],
-  currentChart: ChartType,
+  selectors: string[];
+  currentChart: ChartType;
 };
 };
 
 
 type StateType = {
 type StateType = {
-  logs: string[]
-  pods: any[],
-  selectedPod: any,
-  controllers: any[],
-  loading: boolean,
+  logs: string[];
+  pods: any[];
+  selectedPod: any;
+  controllers: any[];
+  loading: boolean;
 };
 };
 
 
 export default class StatusSection extends Component<PropsType, StateType> {
 export default class StatusSection extends Component<PropsType, StateType> {
@@ -29,90 +29,94 @@ export default class StatusSection extends Component<PropsType, StateType> {
     selectedPod: {} as any,
     selectedPod: {} as any,
     controllers: [] as any[],
     controllers: [] as any[],
     loading: true,
     loading: true,
-  }
+  };
 
 
   renderLogs = () => {
   renderLogs = () => {
-    return <Logs 
-      key={this.state.selectedPod?.metadata?.name} 
-      selectedPod={this.state.selectedPod} 
-    />
-  }
+    return (
+      <Logs
+        key={this.state.selectedPod?.metadata?.name}
+        selectedPod={this.state.selectedPod}
+      />
+    );
+  };
 
 
   selectPod = (pod: any) => {
   selectPod = (pod: any) => {
     this.setState({
     this.setState({
-      selectedPod: pod
-    })
-  }
+      selectedPod: pod,
+    });
+  };
 
 
   renderTabs = () => {
   renderTabs = () => {
     return this.state.controllers.map((c, i) => {
     return this.state.controllers.map((c, i) => {
       return (
       return (
-        <ControllerTab 
-          key={c.metadata.uid} 
-          selectedPod={this.state.selectedPod} 
+        <ControllerTab
+          key={c.metadata.uid}
+          selectedPod={this.state.selectedPod}
           selectPod={this.selectPod.bind(this)}
           selectPod={this.selectPod.bind(this)}
           controller={c}
           controller={c}
           isLast={i === this.state.controllers.length - 1}
           isLast={i === this.state.controllers.length - 1}
           isFirst={i === 0}
           isFirst={i === 0}
         />
         />
-      )
-    })
-  }
+      );
+    });
+  };
 
 
   renderStatusSection = () => {
   renderStatusSection = () => {
     if (this.state.loading) {
     if (this.state.loading) {
       return (
       return (
-        <NoControllers> 
+        <NoControllers>
           <Loading />
           <Loading />
         </NoControllers>
         </NoControllers>
-      )
+      );
     }
     }
     if (this.state.controllers.length > 0) {
     if (this.state.controllers.length > 0) {
       return (
       return (
         <Wrapper>
         <Wrapper>
-          <TabWrapper>
-            {this.renderTabs()}
-          </TabWrapper>
+          <TabWrapper>{this.renderTabs()}</TabWrapper>
           {this.renderLogs()}
           {this.renderLogs()}
         </Wrapper>
         </Wrapper>
-      )
+      );
     } else {
     } else {
       return (
       return (
-        <NoControllers> 
-          <i className="material-icons">category</i> 
-          No objects to display. This might happen while your app is still deploying.
+        <NoControllers>
+          <i className="material-icons">category</i>
+          No objects to display. This might happen while your app is still
+          deploying.
         </NoControllers>
         </NoControllers>
-      )
+      );
     }
     }
-  }
+  };
 
 
   componentDidMount() {
   componentDidMount() {
     const { selectors, currentChart } = this.props;
     const { selectors, currentChart } = this.props;
     let { currentCluster, currentProject, setCurrentError } = this.context;
     let { currentCluster, currentProject, setCurrentError } = this.context;
 
 
-    api.getChartControllers('<token>', {
-      namespace: currentChart.namespace,
-      cluster_id: currentCluster.id,
-      storage: StorageType.Secret
-    }, {
-      id: currentProject.id,
-      name: currentChart.name,
-      revision: currentChart.version
-    }, (err: any, res: any) => {
-      if (err) {
-        setCurrentError(JSON.stringify(err));
-        this.setState({controllers: [], loading: false})
-        return
+    api.getChartControllers(
+      "<token>",
+      {
+        namespace: currentChart.namespace,
+        cluster_id: currentCluster.id,
+        storage: StorageType.Secret,
+      },
+      {
+        id: currentProject.id,
+        name: currentChart.name,
+        revision: currentChart.version,
+      },
+      (err: any, res: any) => {
+        if (err) {
+          setCurrentError(JSON.stringify(err));
+          this.setState({ controllers: [], loading: false });
+          return;
+        }
+        this.setState({ controllers: res.data, loading: false });
       }
       }
-      this.setState({ controllers: res.data, loading: false })
-    });
+    );
   }
   }
 
 
   render() {
   render() {
     return (
     return (
-      <StyledStatusSection>
-        {this.renderStatusSection()}
-      </StyledStatusSection>
+      <StyledStatusSection>{this.renderStatusSection()}</StyledStatusSection>
     );
     );
   }
   }
 }
 }
@@ -157,4 +161,4 @@ const NoControllers = styled.div`
     font-size: 18px;
     font-size: 18px;
     margin-right: 12px;
     margin-right: 12px;
   }
   }
-`;
+`;

+ 46 - 39
dashboard/src/main/home/dashboard/ClusterList.tsx

@@ -1,71 +1,74 @@
-import React, { Component } from 'react';
-import styled from 'styled-components';
+import React, { Component } from "react";
+import styled from "styled-components";
 
 
-import { Context } from 'shared/Context';
-import api from 'shared/api';
-import { ClusterType } from 'shared/types';
-import Helper from 'components/values-form/Helper';
+import { Context } from "shared/Context";
+import api from "shared/api";
+import { ClusterType } from "shared/types";
+import Helper from "components/values-form/Helper";
 
 
-import Loading from 'components/Loading';
-import { RouteComponentProps, withRouter } from 'react-router';
+import Loading from "components/Loading";
+import { RouteComponentProps, withRouter } from "react-router";
 
 
 type PropsType = RouteComponentProps;
 type PropsType = RouteComponentProps;
 
 
 type StateType = {
 type StateType = {
-  loading: boolean,
-  error: string,
-  clusters: ClusterType[],
+  loading: boolean;
+  error: string;
+  clusters: ClusterType[];
 };
 };
 
 
 class Templates extends Component<PropsType, StateType> {
 class Templates extends Component<PropsType, StateType> {
   state = {
   state = {
     loading: true,
     loading: true,
-    error: '',
+    error: "",
     clusters: [] as ClusterType[],
     clusters: [] as ClusterType[],
-  }
+  };
 
 
   componentDidMount() {
   componentDidMount() {
-    api.getClusters('<token>', {}, { id: this.context.currentProject.id }, (err: any, res: any) => {
-      if (res && res.data) {
-        this.setState({ clusters: res.data, loading: false, error: '' });
-      } else {
-        this.setState({ loading: false, error: err });
+    api.getClusters(
+      "<token>",
+      {},
+      { id: this.context.currentProject.id },
+      (err: any, res: any) => {
+        if (res && res.data) {
+          this.setState({ clusters: res.data, loading: false, error: "" });
+        } else {
+          this.setState({ loading: false, error: err });
+        }
       }
       }
-    });
+    );
   }
   }
 
 
   renderIcon = () => {
   renderIcon = () => {
     return (
     return (
-      <DashboardIcon><i className="material-icons">device_hub</i></DashboardIcon>
+      <DashboardIcon>
+        <i className="material-icons">device_hub</i>
+      </DashboardIcon>
     );
     );
-  }
+  };
 
 
   renderClusters = () => {
   renderClusters = () => {
     return this.state.clusters.map((cluster: ClusterType, i: number) => {
     return this.state.clusters.map((cluster: ClusterType, i: number) => {
       return (
       return (
-        <TemplateBlock 
-          onClick={() => { 
-            this.context.setCurrentCluster(cluster); 
-            this.props.history.push('cluster-dashboard');
+        <TemplateBlock
+          onClick={() => {
+            this.context.setCurrentCluster(cluster);
+            this.props.history.push("cluster-dashboard");
           }}
           }}
           key={i}
           key={i}
         >
         >
           {this.renderIcon()}
           {this.renderIcon()}
-          <TemplateTitle>
-            {cluster.name}
-          </TemplateTitle>
+          <TemplateTitle>{cluster.name}</TemplateTitle>
         </TemplateBlock>
         </TemplateBlock>
       );
       );
     });
     });
-  }
-  
+  };
+
   render() {
   render() {
     return (
     return (
       <>
       <>
         <Helper>Clusters connected to this project:</Helper>
         <Helper>Clusters connected to this project:</Helper>
-        <TemplateList>
-          {this.renderClusters()}
-        </TemplateList>
+        <TemplateList>{this.renderClusters()}</TemplateList>
       </>
       </>
     );
     );
   }
   }
@@ -73,7 +76,7 @@ class Templates extends Component<PropsType, StateType> {
 
 
 Templates.contextType = Context;
 Templates.contextType = Context;
 
 
-export default withRouter(Templates)
+export default withRouter(Templates);
 
 
 const DashboardIcon = styled.div`
 const DashboardIcon = styled.div`
   position: relative;
   position: relative;
@@ -84,7 +87,7 @@ const DashboardIcon = styled.div`
   display: flex;
   display: flex;
   align-items: center;
   align-items: center;
   justify-content: center;
   justify-content: center;
-  background: #676C7C;
+  background: #676c7c;
   border: 2px solid #8e94aa;
   border: 2px solid #8e94aa;
 
 
   > i {
   > i {
@@ -126,8 +129,12 @@ const TemplateBlock = styled.div`
 
 
   animation: fadeIn 0.3s 0s;
   animation: fadeIn 0.3s 0s;
   @keyframes fadeIn {
   @keyframes fadeIn {
-    from { opacity: 0 }
-    to { opacity: 1 }
+    from {
+      opacity: 0;
+    }
+    to {
+      opacity: 1;
+    }
   }
   }
 `;
 `;
 
 
@@ -144,7 +151,7 @@ const TemplateList = styled.div`
 const Title = styled.div`
 const Title = styled.div`
   font-size: 24px;
   font-size: 24px;
   font-weight: 600;
   font-weight: 600;
-  font-family: 'Work Sans', sans-serif;
+  font-family: "Work Sans", sans-serif;
   color: #ffffff;
   color: #ffffff;
   white-space: nowrap;
   white-space: nowrap;
   overflow: hidden;
   overflow: hidden;
@@ -164,7 +171,7 @@ const TitleSection = styled.div`
       margin-bottom: -2px;
       margin-bottom: -2px;
       font-size: 18px;
       font-size: 18px;
       margin-left: 18px;
       margin-left: 18px;
-      color: #858FAAaa;
+      color: #858faaaa;
       cursor: pointer;
       cursor: pointer;
       :hover {
       :hover {
         color: #aaaabb;
         color: #aaaabb;

+ 21 - 19
dashboard/src/main/home/dashboard/ClusterPlaceholder.tsx

@@ -1,25 +1,27 @@
-import React, { Component } from 'react';
-import styled from 'styled-components';
+import React, { Component } from "react";
+import styled from "styled-components";
 
 
-import api from 'shared/api';
-import { Context } from 'shared/Context';
-import { ClusterType } from 'shared/types';
+import { Context } from "shared/Context";
+import { ClusterType } from "shared/types";
 
 
-import ClusterList from './ClusterList';
-import Loading from 'components/Loading';
+import ClusterList from "./ClusterList";
+import Loading from "components/Loading";
 
 
 type PropsType = {
 type PropsType = {
-  currentCluster: ClusterType,
+  currentCluster: ClusterType;
 };
 };
 
 
 type StateType = {
 type StateType = {
-  loading: boolean,
+  loading: boolean;
 };
 };
 
 
-export default class ClusterPlaceholder extends Component<PropsType, StateType> {
+export default class ClusterPlaceholder extends Component<
+  PropsType,
+  StateType
+> {
   state = {
   state = {
     loading: true,
     loading: true,
-  }
+  };
 
 
   componentDidMount() {
   componentDidMount() {
     setTimeout(() => {
     setTimeout(() => {
@@ -48,18 +50,18 @@ export default class ClusterPlaceholder extends Component<PropsType, StateType>
             This project currently has no clusters connected.
             This project currently has no clusters connected.
           </Banner>
           </Banner>
           <StyledStatusPlaceholder>
           <StyledStatusPlaceholder>
-            <Highlight onClick={() => {
-              this.context.setCurrentModal('ClusterInstructionsModal', {});
-            }}>
+            <Highlight
+              onClick={() => {
+                this.context.setCurrentModal("ClusterInstructionsModal", {});
+              }}
+            >
               + Connect a Cluster
               + Connect a Cluster
             </Highlight>
             </Highlight>
           </StyledStatusPlaceholder>
           </StyledStatusPlaceholder>
         </>
         </>
       );
       );
     } else {
     } else {
-      return (
-        <ClusterList/>
-      );
+      return <ClusterList />;
     }
     }
   }
   }
 }
 }
@@ -111,6 +113,6 @@ const StyledStatusPlaceholder = styled.div`
   display: flex;
   display: flex;
   align-items: center;
   align-items: center;
   justify-content: center;
   justify-content: center;
-  font-family: 'Work Sans', sans-serif;
+  font-family: "Work Sans", sans-serif;
   user-select: text;
   user-select: text;
-`;
+`;

+ 11 - 15
dashboard/src/main/home/dashboard/ClusterPlaceholderContainer.tsx

@@ -1,26 +1,22 @@
-import React, { Component } from 'react';
-import styled from 'styled-components';
+import React, { Component } from "react";
 
 
-import { Context } from 'shared/Context';
-import ClusterPlaceholder from './ClusterPlaceholder';
+import { Context } from "shared/Context";
+import ClusterPlaceholder from "./ClusterPlaceholder";
 
 
 type PropsType = {};
 type PropsType = {};
 
 
-type StateType = {
-};
+type StateType = {};
 
 
 // Props in context to project section to trigger update on context change
 // Props in context to project section to trigger update on context change
-export default class ClusterPlaceholderContainer extends Component<PropsType, StateType> {
-  state = {
-  }
+export default class ClusterPlaceholderContainer extends Component<
+  PropsType,
+  StateType
+> {
+  state = {};
 
 
   render() {
   render() {
-    return (
-      <ClusterPlaceholder
-        currentCluster={this.context.currentCluster}
-      />
-    );
+    return <ClusterPlaceholder currentCluster={this.context.currentCluster} />;
   }
   }
 }
 }
 
 
-ClusterPlaceholderContainer.contextType = Context;
+ClusterPlaceholderContainer.contextType = Context;

+ 56 - 59
dashboard/src/main/home/dashboard/Dashboard.tsx

@@ -1,43 +1,47 @@
-import { render } from '@testing-library/react';
-import React, { Component } from 'react';
-import styled from 'styled-components';
+import React, { Component } from "react";
+import styled from "styled-components";
 
 
-import gradient from 'assets/gradient.jpg';
-import { Context } from 'shared/Context';
-import { InfraType } from 'shared/types';
-import api from 'shared/api';
+import gradient from "assets/gradient.jpg";
+import { Context } from "shared/Context";
+import { InfraType } from "shared/types";
+import api from "shared/api";
 
 
-import ProvisionerSettings from '../provisioner/ProvisionerSettings';
-import ClusterPlaceholderContainer from './ClusterPlaceholderContainer';
-import { Redirect, RouteComponentProps, withRouter } from 'react-router';
+import ProvisionerSettings from "../provisioner/ProvisionerSettings";
+import ClusterPlaceholderContainer from "./ClusterPlaceholderContainer";
+import { Redirect, RouteComponentProps, withRouter } from "react-router";
 
 
 type PropsType = RouteComponentProps & {
 type PropsType = RouteComponentProps & {
-  projectId: number | null,
+  projectId: number | null;
 };
 };
 
 
 type StateType = {
 type StateType = {
-  infras: InfraType[],
+  infras: InfraType[];
 };
 };
 
 
 class Dashboard extends Component<PropsType, StateType> {
 class Dashboard extends Component<PropsType, StateType> {
   state = {
   state = {
     infras: [] as InfraType[],
     infras: [] as InfraType[],
-  }
+  };
 
 
   refreshInfras = () => {
   refreshInfras = () => {
     if (this.props.projectId) {
     if (this.props.projectId) {
-      api.getInfra('<token>', {}, { 
-        project_id: this.props.projectId,
-      }, (err: any, res: any) => {
-        if (err) {
-          console.log(err);
-          return;
-        } 
-        this.setState({ infras: res.data });
-      });
+      api.getInfra(
+        "<token>",
+        {},
+        {
+          project_id: this.props.projectId,
+        },
+        (err: any, res: any) => {
+          if (err) {
+            console.log(err);
+            return;
+          }
+          this.setState({ infras: res.data });
+        }
+      );
     }
     }
-  }
-  
+  };
+
   componentDidMount() {
   componentDidMount() {
     this.refreshInfras();
     this.refreshInfras();
   }
   }
@@ -50,7 +54,7 @@ class Dashboard extends Component<PropsType, StateType> {
 
 
   onShowProjectSettings = () => {
   onShowProjectSettings = () => {
     this.props.history.push("project-settings");
     this.props.history.push("project-settings");
-  }
+  };
 
 
   render() {
   render() {
     let { currentProject, currentCluster } = this.context;
     let { currentProject, currentCluster } = this.context;
@@ -61,30 +65,27 @@ class Dashboard extends Component<PropsType, StateType> {
         {currentProject && (
         {currentProject && (
           <DashboardWrapper>
           <DashboardWrapper>
             <TitleSection>
             <TitleSection>
-            <DashboardIcon>
-              <DashboardImage src={gradient} />
-              <Overlay>
-                {currentProject && currentProject.name[0].toUpperCase()}
-              </Overlay>
-            </DashboardIcon>
+              <DashboardIcon>
+                <DashboardImage src={gradient} />
+                <Overlay>
+                  {currentProject && currentProject.name[0].toUpperCase()}
+                </Overlay>
+              </DashboardIcon>
               <Title>{currentProject && currentProject.name}</Title>
               <Title>{currentProject && currentProject.name}</Title>
               {this.context.currentProject.roles.filter((obj: any) => {
               {this.context.currentProject.roles.filter((obj: any) => {
                 return obj.user_id === this.context.user.userId;
                 return obj.user_id === this.context.user.userId;
-              })[0].kind === 'admin' &&
-                <i
-                  className="material-icons"
-                  onClick={onShowProjectSettings}
-                >
+              })[0].kind === "admin" && (
+                <i className="material-icons" onClick={onShowProjectSettings}>
                   more_vert
                   more_vert
                 </i>
                 </i>
-              }
+              )}
             </TitleSection>
             </TitleSection>
 
 
             <InfoSection>
             <InfoSection>
               <TopRow>
               <TopRow>
                 <InfoLabel>
                 <InfoLabel>
                   <i className="material-icons">info</i> Info
                   <i className="material-icons">info</i> Info
-              </InfoLabel>
+                </InfoLabel>
               </TopRow>
               </TopRow>
               <Description>
               <Description>
                 Project overview for {currentProject && currentProject.name}.
                 Project overview for {currentProject && currentProject.name}.
@@ -93,21 +94,17 @@ class Dashboard extends Component<PropsType, StateType> {
 
 
             <LineBreak />
             <LineBreak />
 
 
-            {!currentCluster 
-              ? (
-                <>
-                  <Banner>
-                    <i className="material-icons">error_outline</i>
-                    This project currently has no clusters connected.
-                    </Banner>
-                  <ProvisionerSettings 
-                    infras={infras}
-                  />
-                </>
-              ) : (
-                <ClusterPlaceholderContainer/>
-              )
-            }
+            {!currentCluster ? (
+              <>
+                <Banner>
+                  <i className="material-icons">error_outline</i>
+                  This project currently has no clusters connected.
+                </Banner>
+                <ProvisionerSettings infras={infras} />
+              </>
+            ) : (
+              <ClusterPlaceholderContainer />
+            )}
           </DashboardWrapper>
           </DashboardWrapper>
         )}
         )}
       </>
       </>
@@ -156,10 +153,10 @@ const InfoLabel = styled.div`
   height: 20px;
   height: 20px;
   display: flex;
   display: flex;
   align-items: center;
   align-items: center;
-  color: #7A838F;
+  color: #7a838f;
   font-size: 13px;
   font-size: 13px;
   > i {
   > i {
-    color: #8B949F;
+    color: #8b949f;
     font-size: 18px;
     font-size: 18px;
     margin-right: 5px;
     margin-right: 5px;
   }
   }
@@ -167,7 +164,7 @@ const InfoLabel = styled.div`
 
 
 const InfoSection = styled.div`
 const InfoSection = styled.div`
   margin-top: 20px;
   margin-top: 20px;
-  font-family: 'Work Sans', sans-serif;
+  font-family: "Work Sans", sans-serif;
   margin-left: 0px;
   margin-left: 0px;
   margin-bottom: 35px;
   margin-bottom: 35px;
 `;
 `;
@@ -192,7 +189,7 @@ const Overlay = styled.div`
   justify-content: center;
   justify-content: center;
   font-size: 24px;
   font-size: 24px;
   font-weight: 500;
   font-weight: 500;
-  font-family: 'Work Sans', sans-serif;
+  font-family: "Work Sans", sans-serif;
   color: white;
   color: white;
 `;
 `;
 
 
@@ -219,7 +216,7 @@ const DashboardIcon = styled.div`
 const Title = styled.div`
 const Title = styled.div`
   font-size: 20px;
   font-size: 20px;
   font-weight: 500;
   font-weight: 500;
-  font-family: 'Work Sans', sans-serif;
+  font-family: "Work Sans", sans-serif;
   margin-left: 18px;
   margin-left: 18px;
   color: #ffffff;
   color: #ffffff;
   white-space: nowrap;
   white-space: nowrap;
@@ -248,4 +245,4 @@ const TitleSection = styled.div`
     }
     }
     margin-bottom: -3px;
     margin-bottom: -3px;
   }
   }
-`;
+`;

+ 8 - 15
dashboard/src/main/home/dashboard/PipelinesSection.tsx

@@ -1,22 +1,15 @@
-import React, { Component } from 'react';
-import styled from 'styled-components';
+import React, { Component } from "react";
+import styled from "styled-components";
 
 
-type PropsType = {
-};
+type PropsType = {};
 
 
-type StateType = {
-};
+type StateType = {};
 
 
 export default class PipelinesSection extends Component<PropsType, StateType> {
 export default class PipelinesSection extends Component<PropsType, StateType> {
-  state = {
-  }
+  state = {};
 
 
   render() {
   render() {
-    return (
-      <StyledPipelinesSection>
-        
-      </StyledPipelinesSection>
-    );
+    return <StyledPipelinesSection></StyledPipelinesSection>;
   }
   }
 }
 }
 
 
@@ -31,5 +24,5 @@ const StyledPipelinesSection = styled.div`
   text-align: center;
   text-align: center;
   font-size: 13px;
   font-size: 13px;
   background: #ffffff08;
   background: #ffffff08;
-  font-family: 'Work Sans', sans-serif;
-`;
+  font-family: "Work Sans", sans-serif;
+`;

+ 41 - 38
dashboard/src/main/home/integrations/IntegrationList.tsx

@@ -1,19 +1,16 @@
-import React, { Component } from 'react';
-import styled from 'styled-components';
+import React, { Component } from "react";
+import styled from "styled-components";
 
 
-import { Context } from 'shared/Context';
-import { integrationList } from 'shared/common';
-import api from 'shared/api';
+import { integrationList } from "shared/common";
 
 
 type PropsType = {
 type PropsType = {
-  setCurrent: (x: any) => void,
-  integrations: string[],
-  titles?: string[],
-  isCategory?: boolean
+  setCurrent: (x: any) => void;
+  integrations: string[];
+  titles?: string[];
+  isCategory?: boolean;
 };
 };
 
 
-type StateType = {
-};
+type StateType = {};
 
 
 export default class IntegrationList extends Component<PropsType, StateType> {
 export default class IntegrationList extends Component<PropsType, StateType> {
   renderContents = () => {
   renderContents = () => {
@@ -22,14 +19,16 @@ export default class IntegrationList extends Component<PropsType, StateType> {
     console.log(`integrations: ${integrations}`);
     console.log(`integrations: ${integrations}`);
     if (titles && titles.length > 0) {
     if (titles && titles.length > 0) {
       return integrations.map((integration: string, i: number) => {
       return integrations.map((integration: string, i: number) => {
-        let icon = integrationList[integration] && integrationList[integration].icon;
-        let subtitle = integrationList[integration] && integrationList[integration].label;
+        let icon =
+          integrationList[integration] && integrationList[integration].icon;
+        let subtitle =
+          integrationList[integration] && integrationList[integration].label;
         let label = titles[i];
         let label = titles[i];
-        let disabled = integration === 'kubernetes' || integration === 'repo';
+        let disabled = integration === "kubernetes" || integration === "repo";
         return (
         return (
           <Integration
           <Integration
             key={i}
             key={i}
-            onClick={() => disabled ? null : setCurrent(integration)}
+            onClick={() => (disabled ? null : setCurrent(integration))}
             isCategory={isCategory}
             isCategory={isCategory}
             disabled={disabled}
             disabled={disabled}
           >
           >
@@ -40,19 +39,23 @@ export default class IntegrationList extends Component<PropsType, StateType> {
                 <Subtitle>{subtitle}</Subtitle>
                 <Subtitle>{subtitle}</Subtitle>
               </Description>
               </Description>
             </Flex>
             </Flex>
-            <i className="material-icons">{isCategory ? 'launch' : 'more_vert'}</i>
+            <i className="material-icons">
+              {isCategory ? "launch" : "more_vert"}
+            </i>
           </Integration>
           </Integration>
         );
         );
       });
       });
     } else if (integrations && integrations.length > 0) {
     } else if (integrations && integrations.length > 0) {
       return integrations.map((integration: string, i: number) => {
       return integrations.map((integration: string, i: number) => {
-        let icon = integrationList[integration] && integrationList[integration].icon;
-        let label = integrationList[integration] && integrationList[integration].label;
-        let disabled = integration === 'kubernetes' || integration === 'repo';
+        let icon =
+          integrationList[integration] && integrationList[integration].icon;
+        let label =
+          integrationList[integration] && integrationList[integration].label;
+        let disabled = integration === "kubernetes" || integration === "repo";
         return (
         return (
           <Integration
           <Integration
             key={i}
             key={i}
-            onClick={() => disabled ? null : setCurrent(integration)}
+            onClick={() => (disabled ? null : setCurrent(integration))}
             isCategory={isCategory}
             isCategory={isCategory}
             disabled={disabled}
             disabled={disabled}
           >
           >
@@ -60,23 +63,19 @@ export default class IntegrationList extends Component<PropsType, StateType> {
               <Icon src={icon && icon} />
               <Icon src={icon && icon} />
               <Label>{label}</Label>
               <Label>{label}</Label>
             </Flex>
             </Flex>
-            <i className="material-icons">{isCategory ? 'launch' : 'more_vert'}</i>
+            <i className="material-icons">
+              {isCategory ? "launch" : "more_vert"}
+            </i>
           </Integration>
           </Integration>
         );
         );
       });
       });
     }
     }
-    return (
-      <Placeholder>
-        No integrations set up yet.
-      </Placeholder>
-    );
-  }
-  
+    return <Placeholder>No integrations set up yet.</Placeholder>;
+  };
+
   render() {
   render() {
-    return ( 
-      <StyledIntegrationList>
-        {this.renderContents()}
-      </StyledIntegrationList>
+    return (
+      <StyledIntegrationList>{this.renderContents()}</StyledIntegrationList>
     );
     );
   }
   }
 }
 }
@@ -96,15 +95,18 @@ const Integration = styled.div`
   justify-content: space-between;
   justify-content: space-between;
   padding: 25px;
   padding: 25px;
   background: #26282f;
   background: #26282f;
-  cursor: ${(props: { isCategory: boolean, disabled: boolean }) => props.disabled ? 'not-allowed' : 'pointer'};
+  cursor: ${(props: { isCategory: boolean; disabled: boolean }) =>
+    props.disabled ? "not-allowed" : "pointer"};
   margin-bottom: 15px;
   margin-bottom: 15px;
   border-radius: 5px;
   border-radius: 5px;
   box-shadow: 0 5px 8px 0px #00000033;
   box-shadow: 0 5px 8px 0px #00000033;
   :hover {
   :hover {
-    background: ${(props: { isCategory: boolean, disabled: boolean }) => props.disabled ? '' : '#ffffff11'};
+    background: ${(props: { isCategory: boolean; disabled: boolean }) =>
+      props.disabled ? "" : "#ffffff11"};
 
 
     > i {
     > i {
-      background: ${(props: { isCategory: boolean, disabled: boolean }) => props.disabled ? '' : '#ffffff11'};
+      background: ${(props: { isCategory: boolean; disabled: boolean }) =>
+        props.disabled ? "" : "#ffffff11"};
     }
     }
   }
   }
 
 
@@ -112,7 +114,8 @@ const Integration = styled.div`
     border-radius: 20px;
     border-radius: 20px;
     font-size: 18px;
     font-size: 18px;
     padding: 5px;
     padding: 5px;
-    color: ${(props: { isCategory: boolean, disabled: boolean }) => props.isCategory ? '#616feecc' : '#ffffff44'};
+    color: ${(props: { isCategory: boolean; disabled: boolean }) =>
+      props.isCategory ? "#616feecc" : "#ffffff44"};
     margin-right: -7px;
     margin-right: -7px;
   }
   }
 `;
 `;
@@ -149,7 +152,7 @@ const Placeholder = styled.div`
   display: flex;
   display: flex;
   align-items: center;
   align-items: center;
   font-size: 13px;
   font-size: 13px;
-  font-family: 'Work Sans', sans-serif;
+  font-family: "Work Sans", sans-serif;
   justify-content: center;
   justify-content: center;
   margin-top: 30px;
   margin-top: 30px;
   background: #ffffff11;
   background: #ffffff11;
@@ -159,4 +162,4 @@ const Placeholder = styled.div`
 
 
 const StyledIntegrationList = styled.div`
 const StyledIntegrationList = styled.div`
   margin-top: 20px;
   margin-top: 20px;
-`;
+`;

+ 136 - 93
dashboard/src/main/home/integrations/Integrations.tsx

@@ -1,23 +1,21 @@
-import React, { Component } from 'react';
-import styled from 'styled-components';
+import React, { Component } from "react";
+import styled from "styled-components";
 
 
-import { Context } from 'shared/Context';
-import api from 'shared/api';
-import { integrationList } from 'shared/common';
-import { ChoiceType } from 'shared/types';
+import { Context } from "shared/Context";
+import api from "shared/api";
+import { integrationList } from "shared/common";
 
 
-import IntegrationList from './IntegrationList';
-import IntegrationForm from './integration-form/IntegrationForm';
+import IntegrationList from "./IntegrationList";
+import IntegrationForm from "./integration-form/IntegrationForm";
 
 
-type PropsType = {
-};
+type PropsType = {};
 
 
 type StateType = {
 type StateType = {
-  currentCategory: string | null,
-  currentIntegration: string | null,
-  currentOptions: any[],
-  currentTitles: any[],
-  currentIntegrationData: any[],
+  currentCategory: string | null;
+  currentIntegration: string | null;
+  currentOptions: any[];
+  currentTitles: any[];
+  currentIntegrationData: any[];
 };
 };
 
 
 export default class Integrations extends Component<PropsType, StateType> {
 export default class Integrations extends Component<PropsType, StateType> {
@@ -27,103 +25,139 @@ export default class Integrations extends Component<PropsType, StateType> {
     currentOptions: [] as any[],
     currentOptions: [] as any[],
     currentTitles: [] as any[],
     currentTitles: [] as any[],
     currentIntegrationData: [] as any[],
     currentIntegrationData: [] as any[],
-  }
+  };
 
 
   // TODO: implement once backend is restructured
   // TODO: implement once backend is restructured
   getIntegrations = (categoryType: string) => {
   getIntegrations = (categoryType: string) => {
     let { currentProject } = this.context;
     let { currentProject } = this.context;
-    this.setState({ currentOptions: [], currentTitles: [], currentIntegrationData: [] });
+    this.setState({
+      currentOptions: [],
+      currentTitles: [],
+      currentIntegrationData: [],
+    });
     switch (categoryType) {
     switch (categoryType) {
-      case 'kubernetes':
-        api.getProjectClusters('<token>', {}, { id: currentProject.id }, (err: any, res: any) => {
-          if (err) {
-            console.log(err);
-          } else {
-            // console.log(res.data)
+      case "kubernetes":
+        api.getProjectClusters(
+          "<token>",
+          {},
+          { id: currentProject.id },
+          (err: any, res: any) => {
+            if (err) {
+              console.log(err);
+            } else {
+              // console.log(res.data)
+            }
           }
           }
-        });
+        );
         break;
         break;
-      case 'registry':
-        api.getProjectRegistries('<token>', {}, { id: currentProject.id }, (err: any, res: any) => {
-          if (err) {
-            console.log(err);
-          } else {
-            // Sort res.data into service type and sort each service's registry alphabetically
-            let grouped: any = {}
-            let final: any = [];
-            for (let i = 0; i < res.data.length; i++) {
-              let p = res.data[i].service;
-              if (!grouped[p]) { grouped[p] = []; }
-              grouped[p].push(res.data[i]);
-            }
-            Object.values(grouped).forEach((val: any) => {
-              final = final.concat(val.sort((a: any, b: any) => (a.name > b.name) ? 1 : -1));
-            });
+      case "registry":
+        api.getProjectRegistries(
+          "<token>",
+          {},
+          { id: currentProject.id },
+          (err: any, res: any) => {
+            if (err) {
+              console.log(err);
+            } else {
+              // Sort res.data into service type and sort each service's registry alphabetically
+              let grouped: any = {};
+              let final: any = [];
+              for (let i = 0; i < res.data.length; i++) {
+                let p = res.data[i].service;
+                if (!grouped[p]) {
+                  grouped[p] = [];
+                }
+                grouped[p].push(res.data[i]);
+              }
+              Object.values(grouped).forEach((val: any) => {
+                final = final.concat(
+                  val.sort((a: any, b: any) => (a.name > b.name ? 1 : -1))
+                );
+              });
 
 
-            let currentOptions = [] as string[];
-            let currentTitles = [] as string[];
-            final.forEach((integration: any, i: number) => {
-              currentOptions.push(integration.service);
-              currentTitles.push(integration.name);
-            });
-            this.setState({ currentOptions, currentTitles, currentIntegrationData: res.data });
+              let currentOptions = [] as string[];
+              let currentTitles = [] as string[];
+              final.forEach((integration: any, i: number) => {
+                currentOptions.push(integration.service);
+                currentTitles.push(integration.name);
+              });
+              this.setState({
+                currentOptions,
+                currentTitles,
+                currentIntegrationData: res.data,
+              });
+            }
           }
           }
-        });
+        );
         break;
         break;
-      case 'repo':
-        api.getProjectRepos('<token>', {}, { id: currentProject.id }, (err: any, res: any) => {
-          if (err) {
-            console.log(err);
-          } else {
-            // console.log(res.data);
+      case "repo":
+        api.getProjectRepos(
+          "<token>",
+          {},
+          { id: currentProject.id },
+          (err: any, res: any) => {
+            if (err) {
+              console.log(err);
+            } else {
+              // console.log(res.data);
+            }
           }
           }
-        });
+        );
         break;
         break;
       default:
       default:
-        console.log('Unknown integration category.');
+        console.log("Unknown integration category.");
     }
     }
-  }
+  };
 
 
   componentDidUpdate(prevProps: PropsType, prevState: StateType) {
   componentDidUpdate(prevProps: PropsType, prevState: StateType) {
-    if (this.state.currentCategory && this.state.currentCategory !== prevState.currentCategory) {
+    if (
+      this.state.currentCategory &&
+      this.state.currentCategory !== prevState.currentCategory
+    ) {
       this.getIntegrations(this.state.currentCategory);
       this.getIntegrations(this.state.currentCategory);
     }
     }
   }
   }
 
 
   renderIntegrationContents = () => {
   renderIntegrationContents = () => {
     if (this.state.currentIntegrationData) {
     if (this.state.currentIntegrationData) {
-      let items = this.state.currentIntegrationData.filter(item => item.service === this.state.currentIntegration);
+      let items = this.state.currentIntegrationData.filter(
+        (item) => item.service === this.state.currentIntegration
+      );
       if (items.length > 0) {
       if (items.length > 0) {
         return (
         return (
           <div>
           <div>
             <Label>Existing Credentials</Label>
             <Label>Existing Credentials</Label>
-            {
-              items.map((item: any, i: number) => {
-                return (
-                  <Credential key={i}>
-                    <i className="material-icons">admin_panel_settings</i> {item.name}
-                  </Credential>
-                );
-              })
-            }
+            {items.map((item: any, i: number) => {
+              return (
+                <Credential key={i}>
+                  <i className="material-icons">admin_panel_settings</i>{" "}
+                  {item.name}
+                </Credential>
+              );
+            })}
             <br />
             <br />
           </div>
           </div>
         );
         );
       }
       }
     }
     }
-  }
+  };
 
 
   renderContents = () => {
   renderContents = () => {
     let { currentCategory, currentIntegration } = this.state;
     let { currentCategory, currentIntegration } = this.state;
 
 
     // TODO: Split integration page into separate component
     // TODO: Split integration page into separate component
     if (currentIntegration) {
     if (currentIntegration) {
-      let icon = integrationList[currentIntegration] && integrationList[currentIntegration].icon;
+      let icon =
+        integrationList[currentIntegration] &&
+        integrationList[currentIntegration].icon;
       return (
       return (
         <div>
         <div>
           <TitleSectionAlt>
           <TitleSectionAlt>
             <Flex>
             <Flex>
-              <i className="material-icons" onClick={() => this.setState({ currentIntegration: null })}>
+              <i
+                className="material-icons"
+                onClick={() => this.setState({ currentIntegration: null })}
+              >
                 keyboard_backspace
                 keyboard_backspace
               </i>
               </i>
               <Icon src={icon && icon} />
               <Icon src={icon && icon} />
@@ -131,7 +165,7 @@ export default class Integrations extends Component<PropsType, StateType> {
             </Flex>
             </Flex>
           </TitleSectionAlt>
           </TitleSectionAlt>
           {this.renderIntegrationContents()}
           {this.renderIntegrationContents()}
-          <IntegrationForm 
+          <IntegrationForm
             integrationName={currentIntegration}
             integrationName={currentIntegration}
             closeForm={() => {
             closeForm={() => {
               this.setState({ currentIntegration: null });
               this.setState({ currentIntegration: null });
@@ -142,25 +176,37 @@ export default class Integrations extends Component<PropsType, StateType> {
         </div>
         </div>
       );
       );
     } else if (currentCategory) {
     } else if (currentCategory) {
-      let icon = integrationList[currentCategory] && integrationList[currentCategory].icon;
-      let label = integrationList[currentCategory] && integrationList[currentCategory].label;
-      let buttonText = integrationList[currentCategory] && integrationList[currentCategory].buttonText;
+      let icon =
+        integrationList[currentCategory] &&
+        integrationList[currentCategory].icon;
+      let label =
+        integrationList[currentCategory] &&
+        integrationList[currentCategory].label;
+      let buttonText =
+        integrationList[currentCategory] &&
+        integrationList[currentCategory].buttonText;
       return (
       return (
         <div>
         <div>
           <TitleSectionAlt>
           <TitleSectionAlt>
             <Flex>
             <Flex>
-              <i className="material-icons" onClick={() => this.setState({ currentCategory: null })}>
+              <i
+                className="material-icons"
+                onClick={() => this.setState({ currentCategory: null })}
+              >
                 keyboard_backspace
                 keyboard_backspace
               </i>
               </i>
               <Icon src={icon && icon} />
               <Icon src={icon && icon} />
               <Title>{label}</Title>
               <Title>{label}</Title>
             </Flex>
             </Flex>
 
 
-            <Button 
-              onClick={() => this.context.setCurrentModal('IntegrationsModal', { 
-                category: currentCategory,
-                setCurrentIntegration: (x: string) => this.setState({ currentIntegration: x })
-              })}
+            <Button
+              onClick={() =>
+                this.context.setCurrentModal("IntegrationsModal", {
+                  category: currentCategory,
+                  setCurrentIntegration: (x: string) =>
+                    this.setState({ currentIntegration: x }),
+                })
+              }
             >
             >
               <i className="material-icons">add</i>
               <i className="material-icons">add</i>
               {buttonText}
               {buttonText}
@@ -184,20 +230,16 @@ export default class Integrations extends Component<PropsType, StateType> {
         </TitleSection>
         </TitleSection>
 
 
         <IntegrationList
         <IntegrationList
-          integrations={['kubernetes', 'registry', 'repo']}
+          integrations={["kubernetes", "registry", "repo"]}
           setCurrent={(x: any) => this.setState({ currentCategory: x })}
           setCurrent={(x: any) => this.setState({ currentCategory: x })}
           isCategory={true}
           isCategory={true}
         />
         />
       </div>
       </div>
     );
     );
-  }
-  
+  };
+
   render() {
   render() {
-    return ( 
-      <StyledIntegrations>
-        {this.renderContents()}
-      </StyledIntegrations>
-    );
+    return <StyledIntegrations>{this.renderContents()}</StyledIntegrations>;
   }
   }
 }
 }
 
 
@@ -221,7 +263,7 @@ const Credential = styled.div`
   border-radius: 5px;
   border-radius: 5px;
   background: #ffffff11;
   background: #ffffff11;
   margin-bottom: 5px;
   margin-bottom: 5px;
-  
+
   > i {
   > i {
     font-size: 22px;
     font-size: 22px;
     color: #ffffff44;
     color: #ffffff44;
@@ -274,7 +316,8 @@ const Button = styled.div`
   flex-direction: row;
   flex-direction: row;
   align-items: center;
   align-items: center;
 
 
-  > img, i {
+  > img,
+  i {
     width: 20px;
     width: 20px;
     height: 20px;
     height: 20px;
     font-size: 16px;
     font-size: 16px;
@@ -288,7 +331,7 @@ const Button = styled.div`
 const Title = styled.div`
 const Title = styled.div`
   font-size: 24px;
   font-size: 24px;
   font-weight: 600;
   font-weight: 600;
-  font-family: 'Work Sans', sans-serif;
+  font-family: "Work Sans", sans-serif;
   color: #ffffff;
   color: #ffffff;
   white-space: nowrap;
   white-space: nowrap;
   overflow: hidden;
   overflow: hidden;
@@ -320,4 +363,4 @@ const LineBreak = styled.div`
   height: 2px;
   height: 2px;
   background: #ffffff20;
   background: #ffffff20;
   margin: 32px 0px 24px;
   margin: 32px 0px 24px;
-`;
+`;

+ 47 - 40
dashboard/src/main/home/integrations/integration-form/DockerHubForm.tsx

@@ -1,82 +1,89 @@
-import React, { Component } from 'react';
-import styled from 'styled-components';
+import React, { Component } from "react";
+import styled from "styled-components";
 
 
-import { Context } from 'shared/Context';
-import api from 'shared/api';
-
-import InputRow from 'components/values-form/InputRow';
-import SaveButton from 'components/SaveButton';
+import InputRow from "components/values-form/InputRow";
+import SaveButton from "components/SaveButton";
 
 
 type PropsType = {
 type PropsType = {
-  closeForm: () => void,
+  closeForm: () => void;
 };
 };
 
 
 type StateType = {
 type StateType = {
-  registryURL: string,
-  dockerEmail: string,
-  dockerUsername: string,
-  dockerPassword: string
+  registryURL: string;
+  dockerEmail: string;
+  dockerUsername: string;
+  dockerPassword: string;
 };
 };
 
 
 export default class DockerHubForm extends Component<PropsType, StateType> {
 export default class DockerHubForm extends Component<PropsType, StateType> {
   state = {
   state = {
-    registryURL: '',
-    dockerEmail: '',
-    dockerUsername: '',
-    dockerPassword: ''
-  }
+    registryURL: "",
+    dockerEmail: "",
+    dockerUsername: "",
+    dockerPassword: "",
+  };
 
 
   isDisabled = (): boolean => {
   isDisabled = (): boolean => {
-    let { registryURL, dockerEmail, dockerUsername, dockerPassword } = this.state;
-    if (registryURL === '' || dockerEmail === '' || dockerUsername === '' || dockerPassword === '') {
+    let {
+      registryURL,
+      dockerEmail,
+      dockerUsername,
+      dockerPassword,
+    } = this.state;
+    if (
+      registryURL === "" ||
+      dockerEmail === "" ||
+      dockerUsername === "" ||
+      dockerPassword === ""
+    ) {
       return true;
       return true;
     }
     }
     return false;
     return false;
-  }
+  };
 
 
   handleSubmit = () => {
   handleSubmit = () => {
     // TODO: implement once api is restructured
     // TODO: implement once api is restructured
-  }
+  };
 
 
   render() {
   render() {
-    return ( 
+    return (
       <StyledForm>
       <StyledForm>
         <CredentialWrapper>
         <CredentialWrapper>
           <InputRow
           <InputRow
-            type='text'
+            type="text"
             value={this.state.registryURL}
             value={this.state.registryURL}
             setValue={(x: string) => this.setState({ registryURL: x })}
             setValue={(x: string) => this.setState({ registryURL: x })}
-            label='📦 Registry URL'
-            placeholder='ex: index.docker.io'
-            width='100%'
+            label="📦 Registry URL"
+            placeholder="ex: index.docker.io"
+            width="100%"
           />
           />
           <InputRow
           <InputRow
-            type='text'
+            type="text"
             value={this.state.dockerEmail}
             value={this.state.dockerEmail}
             setValue={(x: string) => this.setState({ dockerEmail: x })}
             setValue={(x: string) => this.setState({ dockerEmail: x })}
-            label='✉️ Docker Email'
-            placeholder='ex: captain@ahab.com'
-            width='100%'
+            label="✉️ Docker Email"
+            placeholder="ex: captain@ahab.com"
+            width="100%"
           />
           />
           <InputRow
           <InputRow
-            type='text'
+            type="text"
             value={this.state.dockerUsername}
             value={this.state.dockerUsername}
             setValue={(x: string) => this.setState({ dockerUsername: x })}
             setValue={(x: string) => this.setState({ dockerUsername: x })}
-            label='👤 Docker Username'
-            placeholder='ex: whale_watcher_2000'
-            width='100%'
+            label="👤 Docker Username"
+            placeholder="ex: whale_watcher_2000"
+            width="100%"
           />
           />
           <InputRow
           <InputRow
-            type='password'
+            type="password"
             value={this.state.dockerPassword}
             value={this.state.dockerPassword}
             setValue={(x: string) => this.setState({ dockerPassword: x })}
             setValue={(x: string) => this.setState({ dockerPassword: x })}
-            label='🔒 Docker Password'
-            placeholder='○ ○ ○ ○ ○ ○ ○ ○ ○'
-            width='100%'
+            label="🔒 Docker Password"
+            placeholder="○ ○ ○ ○ ○ ○ ○ ○ ○"
+            width="100%"
           />
           />
         </CredentialWrapper>
         </CredentialWrapper>
         <SaveButton
         <SaveButton
-          text='Save Settings'
+          text="Save Settings"
           makeFlush={true}
           makeFlush={true}
           disabled={this.isDisabled()}
           disabled={this.isDisabled()}
           onClick={this.isDisabled() ? null : this.handleSubmit}
           onClick={this.isDisabled() ? null : this.handleSubmit}
@@ -95,4 +102,4 @@ const CredentialWrapper = styled.div`
 const StyledForm = styled.div`
 const StyledForm = styled.div`
   position: relative;
   position: relative;
   padding-bottom: 75px;
   padding-bottom: 75px;
-`;
+`;

+ 77 - 61
dashboard/src/main/home/integrations/integration-form/ECRForm.tsx

@@ -1,111 +1,127 @@
-import React, { Component } from 'react';
-import styled from 'styled-components';
+import React, { Component } from "react";
+import styled from "styled-components";
 
 
-import { Context } from 'shared/Context';
-import api from 'shared/api';
+import { Context } from "shared/Context";
+import api from "shared/api";
 
 
-import InputRow from 'components/values-form/InputRow';
-import TextArea from 'components/values-form/TextArea';
-import SaveButton from 'components/SaveButton';
-import Heading from 'components/values-form/Heading';
-import Helper from 'components/values-form/Helper';
+import InputRow from "components/values-form/InputRow";
+import SaveButton from "components/SaveButton";
+import Heading from "components/values-form/Heading";
+import Helper from "components/values-form/Helper";
 
 
 type PropsType = {
 type PropsType = {
-  closeForm: () => void,
+  closeForm: () => void;
 };
 };
 
 
 type StateType = {
 type StateType = {
-  credentialsName: string,
-  awsRegion: string,
-  awsAccessId: string,
-  awsSecretKey: string,
+  credentialsName: string;
+  awsRegion: string;
+  awsAccessId: string;
+  awsSecretKey: string;
 };
 };
 
 
 export default class ECRForm extends Component<PropsType, StateType> {
 export default class ECRForm extends Component<PropsType, StateType> {
   state = {
   state = {
-    credentialsName: '',
-    awsRegion: '',
-    awsAccessId: '',
-    awsSecretKey: '',
-  }
+    credentialsName: "",
+    awsRegion: "",
+    awsAccessId: "",
+    awsSecretKey: "",
+  };
 
 
   isDisabled = (): boolean => {
   isDisabled = (): boolean => {
     let { awsRegion, awsAccessId, awsSecretKey, credentialsName } = this.state;
     let { awsRegion, awsAccessId, awsSecretKey, credentialsName } = this.state;
-    if (awsRegion === '' || awsAccessId === '' || awsSecretKey === '' || credentialsName === '') {
+    if (
+      awsRegion === "" ||
+      awsAccessId === "" ||
+      awsSecretKey === "" ||
+      credentialsName === ""
+    ) {
       return true;
       return true;
     }
     }
     return false;
     return false;
-  }
+  };
 
 
   handleSubmit = () => {
   handleSubmit = () => {
     let { awsRegion, awsAccessId, awsSecretKey, credentialsName } = this.state;
     let { awsRegion, awsAccessId, awsSecretKey, credentialsName } = this.state;
     let { currentProject } = this.context;
     let { currentProject } = this.context;
 
 
-    api.createAWSIntegration('<token>', {
-      aws_region: awsRegion,
-      aws_access_key_id: awsAccessId,
-      aws_secret_access_key: awsSecretKey,
-    }, { id: currentProject.id }, (err: any, res: any) => {
-      if (err) {
-        console.log(err);
-      } else {
-        api.createECR('<token>', {
-          name: credentialsName,
-          aws_integration_id: res.data.id,
-        }, { id: currentProject.id }, (err: any, res: any) => {
-          if (err) {
-            console.log(err);
-          } else {
-            this.props.closeForm();
-          }
-        });
+    api.createAWSIntegration(
+      "<token>",
+      {
+        aws_region: awsRegion,
+        aws_access_key_id: awsAccessId,
+        aws_secret_access_key: awsSecretKey,
+      },
+      { id: currentProject.id },
+      (err: any, res: any) => {
+        if (err) {
+          console.log(err);
+        } else {
+          api.createECR(
+            "<token>",
+            {
+              name: credentialsName,
+              aws_integration_id: res.data.id,
+            },
+            { id: currentProject.id },
+            (err: any, res: any) => {
+              if (err) {
+                console.log(err);
+              } else {
+                this.props.closeForm();
+              }
+            }
+          );
+        }
       }
       }
-    });
-  }
+    );
+  };
 
 
   render() {
   render() {
-    return ( 
+    return (
       <StyledForm>
       <StyledForm>
         <CredentialWrapper>
         <CredentialWrapper>
           <Heading>Porter Settings</Heading>
           <Heading>Porter Settings</Heading>
-          <Helper>Give a name to this set of registry credentials (just for Porter).</Helper>
+          <Helper>
+            Give a name to this set of registry credentials (just for Porter).
+          </Helper>
           <InputRow
           <InputRow
-            type='text'
+            type="text"
             value={this.state.credentialsName}
             value={this.state.credentialsName}
             setValue={(x: string) => this.setState({ credentialsName: x })}
             setValue={(x: string) => this.setState({ credentialsName: x })}
-            label='🏷️ Registry Name'
-            placeholder='ex: paper-straw'
-            width='100%'
+            label="🏷️ Registry Name"
+            placeholder="ex: paper-straw"
+            width="100%"
           />
           />
           <Heading>AWS Settings</Heading>
           <Heading>AWS Settings</Heading>
           <Helper>AWS access credentials.</Helper>
           <Helper>AWS access credentials.</Helper>
           <InputRow
           <InputRow
-            type='text'
+            type="text"
             value={this.state.awsRegion}
             value={this.state.awsRegion}
             setValue={(x: string) => this.setState({ awsRegion: x })}
             setValue={(x: string) => this.setState({ awsRegion: x })}
-            label='📍 AWS Region'
-            placeholder='ex: mars-north-12'
-            width='100%'
+            label="📍 AWS Region"
+            placeholder="ex: mars-north-12"
+            width="100%"
           />
           />
           <InputRow
           <InputRow
-            type='text'
+            type="text"
             value={this.state.awsAccessId}
             value={this.state.awsAccessId}
             setValue={(x: string) => this.setState({ awsAccessId: x })}
             setValue={(x: string) => this.setState({ awsAccessId: x })}
-            label='👤 AWS Access ID'
-            placeholder='ex: AKIAIOSFODNN7EXAMPLE'
-            width='100%'
+            label="👤 AWS Access ID"
+            placeholder="ex: AKIAIOSFODNN7EXAMPLE"
+            width="100%"
           />
           />
           <InputRow
           <InputRow
-            type='password'
+            type="password"
             value={this.state.awsSecretKey}
             value={this.state.awsSecretKey}
             setValue={(x: string) => this.setState({ awsSecretKey: x })}
             setValue={(x: string) => this.setState({ awsSecretKey: x })}
-            label='🔒 AWS Secret Key'
-            placeholder='○ ○ ○ ○ ○ ○ ○ ○ ○'
-            width='100%'
+            label="🔒 AWS Secret Key"
+            placeholder="○ ○ ○ ○ ○ ○ ○ ○ ○"
+            width="100%"
           />
           />
         </CredentialWrapper>
         </CredentialWrapper>
         <SaveButton
         <SaveButton
-          text='Save Settings'
+          text="Save Settings"
           makeFlush={true}
           makeFlush={true}
           disabled={this.isDisabled()}
           disabled={this.isDisabled()}
           onClick={this.isDisabled() ? null : this.handleSubmit}
           onClick={this.isDisabled() ? null : this.handleSubmit}
@@ -126,4 +142,4 @@ const CredentialWrapper = styled.div`
 const StyledForm = styled.div`
 const StyledForm = styled.div`
   position: relative;
   position: relative;
   padding-bottom: 75px;
   padding-bottom: 75px;
-`;
+`;

+ 57 - 49
dashboard/src/main/home/integrations/integration-form/EKSForm.tsx

@@ -1,100 +1,108 @@
-import React, { Component } from 'react';
-import styled from 'styled-components';
+import React, { Component } from "react";
+import styled from "styled-components";
 
 
-import { Context } from 'shared/Context';
-import api from 'shared/api';
-
-import InputRow from 'components/values-form/InputRow';
-import TextArea from 'components/values-form/TextArea';
-import SaveButton from 'components/SaveButton';
-import Heading from 'components/values-form/Heading';
-import Helper from 'components/values-form/Helper';
+import InputRow from "components/values-form/InputRow";
+import TextArea from "components/values-form/TextArea";
+import SaveButton from "components/SaveButton";
+import Heading from "components/values-form/Heading";
+import Helper from "components/values-form/Helper";
 
 
 type PropsType = {
 type PropsType = {
-  closeForm: () => void,
+  closeForm: () => void;
 };
 };
 
 
 type StateType = {
 type StateType = {
-  clusterName: string,
-  clusterEndpoint: string,
-  clusterCA: string,
-  awsAccessId: string,
-  awsSecretKey: string,
+  clusterName: string;
+  clusterEndpoint: string;
+  clusterCA: string;
+  awsAccessId: string;
+  awsSecretKey: string;
 };
 };
 
 
 export default class EKSForm extends Component<PropsType, StateType> {
 export default class EKSForm extends Component<PropsType, StateType> {
   state = {
   state = {
-    clusterName: '',
-    clusterEndpoint: '',
-    clusterCA: '',
-    awsAccessId: '',
-    awsSecretKey: '',
-  }
+    clusterName: "",
+    clusterEndpoint: "",
+    clusterCA: "",
+    awsAccessId: "",
+    awsSecretKey: "",
+  };
 
 
   isDisabled = (): boolean => {
   isDisabled = (): boolean => {
-    let { clusterName, clusterEndpoint, clusterCA, awsAccessId, awsSecretKey } = this.state;
-    if (clusterName === '' || clusterEndpoint === '' || clusterCA === '' 
-      || awsAccessId === '' || awsSecretKey === '') {
+    let {
+      clusterName,
+      clusterEndpoint,
+      clusterCA,
+      awsAccessId,
+      awsSecretKey,
+    } = this.state;
+    if (
+      clusterName === "" ||
+      clusterEndpoint === "" ||
+      clusterCA === "" ||
+      awsAccessId === "" ||
+      awsSecretKey === ""
+    ) {
       return true;
       return true;
     }
     }
     return false;
     return false;
-  }
+  };
 
 
   handleSubmit = () => {
   handleSubmit = () => {
     // TODO: implement once api is restructured
     // TODO: implement once api is restructured
-  }
+  };
 
 
   render() {
   render() {
-    return ( 
+    return (
       <StyledForm>
       <StyledForm>
         <CredentialWrapper>
         <CredentialWrapper>
           <Heading>Cluster Settings</Heading>
           <Heading>Cluster Settings</Heading>
           <Helper>Credentials for accessing your GKE cluster.</Helper>
           <Helper>Credentials for accessing your GKE cluster.</Helper>
           <InputRow
           <InputRow
-            type='text'
+            type="text"
             value={this.state.clusterName}
             value={this.state.clusterName}
             setValue={(x: string) => this.setState({ clusterName: x })}
             setValue={(x: string) => this.setState({ clusterName: x })}
-            label='🏷️ Cluster Name'
-            placeholder='ex: briny-pagelet'
-            width='100%'
+            label="🏷️ Cluster Name"
+            placeholder="ex: briny-pagelet"
+            width="100%"
           />
           />
           <InputRow
           <InputRow
-            type='text'
+            type="text"
             value={this.state.clusterEndpoint}
             value={this.state.clusterEndpoint}
             setValue={(x: string) => this.setState({ clusterEndpoint: x })}
             setValue={(x: string) => this.setState({ clusterEndpoint: x })}
-            label='🌐 Cluster Endpoint'
-            placeholder='ex: 00.00.000.00'
-            width='100%'
+            label="🌐 Cluster Endpoint"
+            placeholder="ex: 00.00.000.00"
+            width="100%"
           />
           />
           <TextArea
           <TextArea
             value={this.state.clusterCA}
             value={this.state.clusterCA}
             setValue={(x: string) => this.setState({ clusterCA: x })}
             setValue={(x: string) => this.setState({ clusterCA: x })}
-            label='🔏 Cluster Certificate'
-            placeholder='(Paste your certificate here)'
-            width='100%'
+            label="🔏 Cluster Certificate"
+            placeholder="(Paste your certificate here)"
+            width="100%"
           />
           />
 
 
           <Heading>AWS Settings</Heading>
           <Heading>AWS Settings</Heading>
           <Helper>AWS access credentials.</Helper>
           <Helper>AWS access credentials.</Helper>
           <InputRow
           <InputRow
-            type='text'
+            type="text"
             value={this.state.awsAccessId}
             value={this.state.awsAccessId}
             setValue={(x: string) => this.setState({ awsAccessId: x })}
             setValue={(x: string) => this.setState({ awsAccessId: x })}
-            label='👤 AWS Access ID'
-            placeholder='ex: AKIAIOSFODNN7EXAMPLE'
-            width='100%'
+            label="👤 AWS Access ID"
+            placeholder="ex: AKIAIOSFODNN7EXAMPLE"
+            width="100%"
           />
           />
           <InputRow
           <InputRow
-            type='password'
+            type="password"
             value={this.state.awsSecretKey}
             value={this.state.awsSecretKey}
             setValue={(x: string) => this.setState({ awsSecretKey: x })}
             setValue={(x: string) => this.setState({ awsSecretKey: x })}
-            label='🔒 AWS Secret Key'
-            placeholder='○ ○ ○ ○ ○ ○ ○ ○ ○'
-            width='100%'
+            label="🔒 AWS Secret Key"
+            placeholder="○ ○ ○ ○ ○ ○ ○ ○ ○"
+            width="100%"
           />
           />
         </CredentialWrapper>
         </CredentialWrapper>
         <SaveButton
         <SaveButton
-          text='Save Settings'
+          text="Save Settings"
           makeFlush={true}
           makeFlush={true}
           disabled={this.isDisabled()}
           disabled={this.isDisabled()}
           onClick={this.isDisabled() ? null : this.handleSubmit}
           onClick={this.isDisabled() ? null : this.handleSubmit}
@@ -113,4 +121,4 @@ const CredentialWrapper = styled.div`
 const StyledForm = styled.div`
 const StyledForm = styled.div`
   position: relative;
   position: relative;
   padding-bottom: 75px;
   padding-bottom: 75px;
-`;
+`;

+ 61 - 54
dashboard/src/main/home/integrations/integration-form/GCRForm.tsx

@@ -1,102 +1,109 @@
-import React, { Component } from 'react';
-import styled from 'styled-components';
+import React, { Component } from "react";
+import styled from "styled-components";
 
 
-import { Context } from 'shared/Context';
-import api from 'shared/api';
+import { Context } from "shared/Context";
+import api from "shared/api";
 
 
-import InputRow from 'components/values-form/InputRow';
-import TextArea from 'components/values-form/TextArea';
-import SaveButton from 'components/SaveButton';
-import Heading from 'components/values-form/Heading';
-import Helper from 'components/values-form/Helper';
+import InputRow from "components/values-form/InputRow";
+import TextArea from "components/values-form/TextArea";
+import SaveButton from "components/SaveButton";
+import Heading from "components/values-form/Heading";
+import Helper from "components/values-form/Helper";
 
 
 type PropsType = {
 type PropsType = {
-  closeForm: () => void,
+  closeForm: () => void;
 };
 };
 
 
 type StateType = {
 type StateType = {
-  credentialsName: string,
-  gcpRegion: string,
-  serviceAccountKey: string,
-  gcpProjectID: string,
+  credentialsName: string;
+  gcpRegion: string;
+  serviceAccountKey: string;
+  gcpProjectID: string;
 };
 };
 
 
 export default class GCRForm extends Component<PropsType, StateType> {
 export default class GCRForm extends Component<PropsType, StateType> {
   state = {
   state = {
-    credentialsName: '',
-    gcpRegion: '',
-    serviceAccountKey: '',
-    gcpProjectID: '',
-  }
+    credentialsName: "",
+    gcpRegion: "",
+    serviceAccountKey: "",
+    gcpProjectID: "",
+  };
 
 
   isDisabled = (): boolean => {
   isDisabled = (): boolean => {
     let { credentialsName, serviceAccountKey } = this.state;
     let { credentialsName, serviceAccountKey } = this.state;
-    if (credentialsName === '' || serviceAccountKey === '') {
+    if (credentialsName === "" || serviceAccountKey === "") {
       return true;
       return true;
     }
     }
     return false;
     return false;
-  }
-  
+  };
+
   handleSubmit = () => {
   handleSubmit = () => {
     let { currentProject } = this.context;
     let { currentProject } = this.context;
 
 
-    api.createGCPIntegration('<token>', {
-      gcp_region: this.state.gcpRegion,
-      gcp_key_data: this.state.serviceAccountKey,
-      gcp_project_id: this.state.gcpProjectID,
-    }, {
-      project_id: currentProject.id,
-    }, (err: any, res: any) => {
-      if (err) {
-        console.log(err);
-      } else {
-        console.log(res.data);
+    api.createGCPIntegration(
+      "<token>",
+      {
+        gcp_region: this.state.gcpRegion,
+        gcp_key_data: this.state.serviceAccountKey,
+        gcp_project_id: this.state.gcpProjectID,
+      },
+      {
+        project_id: currentProject.id,
+      },
+      (err: any, res: any) => {
+        if (err) {
+          console.log(err);
+        } else {
+          console.log(res.data);
+        }
       }
       }
-    })
-  }
+    );
+  };
 
 
   render() {
   render() {
-    return ( 
+    return (
       <StyledForm>
       <StyledForm>
         <CredentialWrapper>
         <CredentialWrapper>
           <Heading>Porter Settings</Heading>
           <Heading>Porter Settings</Heading>
-          <Helper>Give a name to this set of registry credentials (just for Porter).</Helper>
+          <Helper>
+            Give a name to this set of registry credentials (just for Porter).
+          </Helper>
           <InputRow
           <InputRow
-            type='text'
+            type="text"
             value={this.state.credentialsName}
             value={this.state.credentialsName}
             setValue={(x: string) => this.setState({ credentialsName: x })}
             setValue={(x: string) => this.setState({ credentialsName: x })}
-            label='🏷️ Registry Name'
-            placeholder='ex: paper-straw'
-            width='100%'
+            label="🏷️ Registry Name"
+            placeholder="ex: paper-straw"
+            width="100%"
           />
           />
           <Heading>GCP Settings</Heading>
           <Heading>GCP Settings</Heading>
           <Helper>Service account credentials for GCP permissions.</Helper>
           <Helper>Service account credentials for GCP permissions.</Helper>
           <InputRow
           <InputRow
-            type='text'
+            type="text"
             value={this.state.gcpRegion}
             value={this.state.gcpRegion}
             setValue={(x: string) => this.setState({ gcpRegion: x })}
             setValue={(x: string) => this.setState({ gcpRegion: x })}
-            label='📍 GCP Region'
-            placeholder='ex: uranus-north-12'
-            width='100%'
+            label="📍 GCP Region"
+            placeholder="ex: uranus-north-12"
+            width="100%"
           />
           />
           <TextArea
           <TextArea
             value={this.state.serviceAccountKey}
             value={this.state.serviceAccountKey}
             setValue={(x: string) => this.setState({ serviceAccountKey: x })}
             setValue={(x: string) => this.setState({ serviceAccountKey: x })}
-            label='🔑 Service Account Key (JSON)'
-            placeholder='(Paste your JSON service account key here)'
-            width='100%'
+            label="🔑 Service Account Key (JSON)"
+            placeholder="(Paste your JSON service account key here)"
+            width="100%"
           />
           />
           <InputRow
           <InputRow
-            type='text'
+            type="text"
             value={this.state.gcpProjectID}
             value={this.state.gcpProjectID}
             setValue={(x: string) => this.setState({ gcpProjectID: x })}
             setValue={(x: string) => this.setState({ gcpProjectID: x })}
-            label='GCP Project ID'
-            placeholder='ex: porter-dev-273614'
-            width='100%'
+            label="GCP Project ID"
+            placeholder="ex: porter-dev-273614"
+            width="100%"
           />
           />
         </CredentialWrapper>
         </CredentialWrapper>
         <SaveButton
         <SaveButton
-          text='Save Settings'
+          text="Save Settings"
           makeFlush={true}
           makeFlush={true}
           disabled={this.isDisabled()}
           disabled={this.isDisabled()}
           onClick={this.isDisabled() ? null : this.handleSubmit}
           onClick={this.isDisabled() ? null : this.handleSubmit}
@@ -117,4 +124,4 @@ const CredentialWrapper = styled.div`
 const StyledForm = styled.div`
 const StyledForm = styled.div`
   position: relative;
   position: relative;
   padding-bottom: 75px;
   padding-bottom: 75px;
-`;
+`;

+ 48 - 41
dashboard/src/main/home/integrations/integration-form/GKEForm.tsx

@@ -1,74 +1,81 @@
-import React, { Component } from 'react';
-import styled from 'styled-components';
+import React, { Component } from "react";
+import styled from "styled-components";
 
 
-import { Context } from 'shared/Context';
-import api from 'shared/api';
-
-import InputRow from 'components/values-form/InputRow';
-import TextArea from 'components/values-form/TextArea';
-import SaveButton from 'components/SaveButton';
-import Heading from 'components/values-form/Heading';
-import Helper from 'components/values-form/Helper';
+import InputRow from "components/values-form/InputRow";
+import TextArea from "components/values-form/TextArea";
+import SaveButton from "components/SaveButton";
+import Heading from "components/values-form/Heading";
+import Helper from "components/values-form/Helper";
 
 
 type PropsType = {
 type PropsType = {
-  closeForm: () => void,
+  closeForm: () => void;
 };
 };
 
 
 type StateType = {
 type StateType = {
-  clusterName: string,
-  clusterEndpoint: string,
-  clusterCA: string,
-  serviceAccountKey: string
+  clusterName: string;
+  clusterEndpoint: string;
+  clusterCA: string;
+  serviceAccountKey: string;
 };
 };
 
 
 export default class GKEForm extends Component<PropsType, StateType> {
 export default class GKEForm extends Component<PropsType, StateType> {
   state = {
   state = {
-    clusterName: '',
-    clusterEndpoint: '',
-    clusterCA: '',
-    serviceAccountKey: ''
-  }
+    clusterName: "",
+    clusterEndpoint: "",
+    clusterCA: "",
+    serviceAccountKey: "",
+  };
 
 
   isDisabled = (): boolean => {
   isDisabled = (): boolean => {
-    let { clusterName, clusterEndpoint, clusterCA, serviceAccountKey } = this.state;
-    if (clusterName === '' || clusterEndpoint === '' || clusterCA === '' || serviceAccountKey === '') {
+    let {
+      clusterName,
+      clusterEndpoint,
+      clusterCA,
+      serviceAccountKey,
+    } = this.state;
+    if (
+      clusterName === "" ||
+      clusterEndpoint === "" ||
+      clusterCA === "" ||
+      serviceAccountKey === ""
+    ) {
       return true;
       return true;
     }
     }
     return false;
     return false;
-  }
+  };
 
 
   handleSubmit = () => {
   handleSubmit = () => {
     // TODO: implement once api is restructured
     // TODO: implement once api is restructured
-  }
+  };
 
 
   render() {
   render() {
-    return ( 
+    return (
       <StyledForm>
       <StyledForm>
         <CredentialWrapper>
         <CredentialWrapper>
           <Heading>Cluster Settings</Heading>
           <Heading>Cluster Settings</Heading>
           <Helper>Credentials for accessing your GKE cluster.</Helper>
           <Helper>Credentials for accessing your GKE cluster.</Helper>
           <InputRow
           <InputRow
-            type='text'
+            type="text"
             value={this.state.clusterName}
             value={this.state.clusterName}
             setValue={(x: string) => this.setState({ clusterName: x })}
             setValue={(x: string) => this.setState({ clusterName: x })}
-            label='🏷️ Cluster Name'
-            placeholder='ex: briny-pagelet'
-            width='100%'
+            label="🏷️ Cluster Name"
+            placeholder="ex: briny-pagelet"
+            width="100%"
           />
           />
           <InputRow
           <InputRow
-            type='text'
+            type="text"
             value={this.state.clusterEndpoint}
             value={this.state.clusterEndpoint}
             setValue={(x: string) => this.setState({ clusterEndpoint: x })}
             setValue={(x: string) => this.setState({ clusterEndpoint: x })}
-            label='🌐 Cluster Endpoint'
-            placeholder='ex: 00.00.000.00'
-            width='100%'
+            label="🌐 Cluster Endpoint"
+            placeholder="ex: 00.00.000.00"
+            width="100%"
           />
           />
           <TextArea
           <TextArea
             value={this.state.clusterCA}
             value={this.state.clusterCA}
             setValue={(x: string) => this.setState({ clusterCA: x })}
             setValue={(x: string) => this.setState({ clusterCA: x })}
-            label='🔏 Cluster Certificate'
-            placeholder='(Paste your certificate here)'
-            width='100%'
+            label="🔏 Cluster Certificate"
+            placeholder="(Paste your certificate here)"
+            width="100%"
           />
           />
 
 
           <Heading>GCP Settings</Heading>
           <Heading>GCP Settings</Heading>
@@ -76,13 +83,13 @@ export default class GKEForm extends Component<PropsType, StateType> {
           <TextArea
           <TextArea
             value={this.state.serviceAccountKey}
             value={this.state.serviceAccountKey}
             setValue={(x: string) => this.setState({ serviceAccountKey: x })}
             setValue={(x: string) => this.setState({ serviceAccountKey: x })}
-            label='🔑 Service Account Key (JSON)'
-            placeholder='(Paste your JSON service account key here)'
-            width='100%'
+            label="🔑 Service Account Key (JSON)"
+            placeholder="(Paste your JSON service account key here)"
+            width="100%"
           />
           />
         </CredentialWrapper>
         </CredentialWrapper>
         <SaveButton
         <SaveButton
-          text='Save Settings'
+          text="Save Settings"
           makeFlush={true}
           makeFlush={true}
           disabled={this.isDisabled()}
           disabled={this.isDisabled()}
           onClick={this.isDisabled() ? null : this.handleSubmit}
           onClick={this.isDisabled() ? null : this.handleSubmit}
@@ -101,4 +108,4 @@ const CredentialWrapper = styled.div`
 const StyledForm = styled.div`
 const StyledForm = styled.div`
   position: relative;
   position: relative;
   padding-bottom: 75px;
   padding-bottom: 75px;
-`;
+`;

+ 16 - 19
dashboard/src/main/home/integrations/integration-form/IntegrationForm.tsx

@@ -1,39 +1,36 @@
-import React, { Component } from 'react';
-import styled from 'styled-components';
+import React, { Component } from "react";
 
 
-import DockerHubForm from './DockerHubForm';
-import GKEForm from './GKEForm';
-import EKSForm from './EKSForm';
-import GCRForm from './GCRForm';
-import ECRForm from './ECRForm';
+import DockerHubForm from "./DockerHubForm";
+import GKEForm from "./GKEForm";
+import EKSForm from "./EKSForm";
+import GCRForm from "./GCRForm";
+import ECRForm from "./ECRForm";
 
 
 type PropsType = {
 type PropsType = {
-  integrationName: string,
-  closeForm: () => void,
+  integrationName: string;
+  closeForm: () => void;
 };
 };
 
 
-type StateType = {
-};
+type StateType = {};
 
 
 export default class IntegrationForm extends Component<PropsType, StateType> {
 export default class IntegrationForm extends Component<PropsType, StateType> {
-  state = {
-  }
+  state = {};
 
 
   render() {
   render() {
     let { closeForm } = this.props;
     let { closeForm } = this.props;
     switch (this.props.integrationName) {
     switch (this.props.integrationName) {
-      case 'docker-hub':
+      case "docker-hub":
         return <DockerHubForm closeForm={closeForm} />;
         return <DockerHubForm closeForm={closeForm} />;
-      case 'gke':
+      case "gke":
         return <GKEForm closeForm={closeForm} />;
         return <GKEForm closeForm={closeForm} />;
-      case 'eks':
+      case "eks":
         return <EKSForm closeForm={closeForm} />;
         return <EKSForm closeForm={closeForm} />;
-      case 'ecr':
+      case "ecr":
         return <ECRForm closeForm={closeForm} />;
         return <ECRForm closeForm={closeForm} />;
-      case 'gcr':
+      case "gcr":
         return <GCRForm closeForm={closeForm} />;
         return <GCRForm closeForm={closeForm} />;
       default:
       default:
         return null;
         return null;
     }
     }
   }
   }
-}
+}

+ 74 - 43
dashboard/src/main/home/modals/ClusterInstructionsModal.tsx

@@ -1,27 +1,27 @@
-import React, { Component } from 'react';
-import styled from 'styled-components';
-import close from 'assets/close.png';
-import TabSelector from 'components/TabSelector';
+import React, { Component } from "react";
+import styled from "styled-components";
+import close from "assets/close.png";
+import TabSelector from "components/TabSelector";
 
 
-import { Context } from 'shared/Context';
+import { Context } from "shared/Context";
 
 
-type PropsType = {
-};
+type PropsType = {};
 
 
 type StateType = {
 type StateType = {
-  currentTab: string,
-  currentPage: number,
+  currentTab: string;
+  currentPage: number;
 };
 };
 
 
-const tabOptions = [
-  { label: 'MacOS', value: 'mac' }
-];
+const tabOptions = [{ label: "MacOS", value: "mac" }];
 
 
-export default class ClusterInstructionsModal extends Component<PropsType, StateType> {
+export default class ClusterInstructionsModal extends Component<
+  PropsType,
+  StateType
+> {
   state = {
   state = {
-    currentTab: 'mac',
+    currentTab: "mac",
     currentPage: 0,
     currentPage: 0,
-  }
+  };
 
 
   renderPage = () => {
   renderPage = () => {
     switch (this.state.currentPage) {
     switch (this.state.currentPage) {
@@ -30,27 +30,41 @@ export default class ClusterInstructionsModal extends Component<PropsType, State
           <Placeholder>
           <Placeholder>
             1. To install the Porter CLI, first retrieve the latest binary:
             1. To install the Porter CLI, first retrieve the latest binary:
             <Code>
             <Code>
-              &#123;<br />
-              name=$(curl -s https://api.github.com/repos/porter-dev/porter/releases/latest | grep "browser_download_url.*porter_.*_Darwin_x86_64\.zip" | cut -d ":" -f 2,3 | tr -d \")<br />
-              name=$(basename $name)<br />
-              curl -L https://github.com/porter-dev/porter/releases/latest/download/$name --output $name<br />
-              unzip -a $name<br />
-              rm $name<br />
+              &#123;
+              <br />
+              name=$(curl -s
+              https://api.github.com/repos/porter-dev/porter/releases/latest |
+              grep "browser_download_url.*porter_.*_Darwin_x86_64\.zip" | cut -d
+              ":" -f 2,3 | tr -d \")
+              <br />
+              name=$(basename $name)
+              <br />
+              curl -L
+              https://github.com/porter-dev/porter/releases/latest/download/$name
+              --output $name
+              <br />
+              unzip -a $name
+              <br />
+              rm $name
+              <br />
               &#125;
               &#125;
             </Code>
             </Code>
             2. Move the file into your bin:
             2. Move the file into your bin:
             <Code>
             <Code>
-              chmod +x ./porter<br />
+              chmod +x ./porter
+              <br />
               sudo mv ./porter /usr/local/bin/porter
               sudo mv ./porter /usr/local/bin/porter
             </Code>
             </Code>
             3. Log in to the Porter CLI:
             3. Log in to the Porter CLI:
             <Code>
             <Code>
-              porter config set-host {location.protocol + '//' + location.host}<br/>
+              porter config set-host {location.protocol + "//" + location.host}
+              <br />
               porter auth login
               porter auth login
             </Code>
             </Code>
             4. Configure the Porter CLI and link your current context:
             4. Configure the Porter CLI and link your current context:
             <Code>
             <Code>
-              porter config set-project {this.context.currentProject.id}<br/>
+              porter config set-project {this.context.currentProject.id}
+              <br />
               porter connect kubeconfig
               porter connect kubeconfig
             </Code>
             </Code>
           </Placeholder>
           </Placeholder>
@@ -64,24 +78,30 @@ export default class ClusterInstructionsModal extends Component<PropsType, State
               porter connect kubeconfig --kubeconfig path/to/kubeconfig
               porter connect kubeconfig --kubeconfig path/to/kubeconfig
             </Code>
             </Code>
             <Bold>Passing a context list</Bold>
             <Bold>Passing a context list</Bold>
-            You can initialize Porter with a set of contexts by passing a context list to start. The contexts that Porter will be able to access are the same as kubectl config get-contexts. For example, if there are two contexts named minikube and staging, you could connect both of them via:
+            You can initialize Porter with a set of contexts by passing a
+            context list to start. The contexts that Porter will be able to
+            access are the same as kubectl config get-contexts. For example, if
+            there are two contexts named minikube and staging, you could connect
+            both of them via:
             <Code>
             <Code>
               porter connect kubeconfig --contexts minikube --contexts staging
               porter connect kubeconfig --contexts minikube --contexts staging
             </Code>
             </Code>
           </Placeholder>
           </Placeholder>
         );
         );
       default:
       default:
-        return
+        return;
     }
     }
-  }
- 
+  };
+
   render() {
   render() {
     let { currentPage, currentTab } = this.state;
     let { currentPage, currentTab } = this.state;
     return (
     return (
       <StyledClusterInstructionsModal>
       <StyledClusterInstructionsModal>
-        <CloseButton onClick={() => {
-          this.context.setCurrentModal(null, null);
-        }}>
+        <CloseButton
+          onClick={() => {
+            this.context.setCurrentModal(null, null);
+          }}
+        >
           <CloseButtonImg src={close} />
           <CloseButtonImg src={close} />
         </CloseButton>
         </CloseButton>
 
 
@@ -90,21 +110,31 @@ export default class ClusterInstructionsModal extends Component<PropsType, State
         <TabSelector
         <TabSelector
           options={tabOptions}
           options={tabOptions}
           currentTab={currentTab}
           currentTab={currentTab}
-          setCurrentTab={(value: string) => this.setState({ currentTab: value })}
+          setCurrentTab={(value: string) =>
+            this.setState({ currentTab: value })
+          }
         />
         />
 
 
         {this.renderPage()}
         {this.renderPage()}
         <PageSection>
         <PageSection>
           <PageCount>{currentPage + 1}/2</PageCount>
           <PageCount>{currentPage + 1}/2</PageCount>
-          <i 
+          <i
             className="material-icons"
             className="material-icons"
-            onClick={() => currentPage > 0 ? this.setState({ currentPage: currentPage - 1 }) : null}
+            onClick={() =>
+              currentPage > 0
+                ? this.setState({ currentPage: currentPage - 1 })
+                : null
+            }
           >
           >
             arrow_back
             arrow_back
           </i>
           </i>
-          <i 
+          <i
             className="material-icons"
             className="material-icons"
-            onClick={() => currentPage < 1 ? this.setState({ currentPage: currentPage + 1 }) : null}
+            onClick={() =>
+              currentPage < 1
+                ? this.setState({ currentPage: currentPage + 1 })
+                : null
+            }
           >
           >
             arrow_forward
             arrow_forward
           </i>
           </i>
@@ -132,7 +162,7 @@ const PageSection = styled.div`
   color: #ffffff;
   color: #ffffff;
   justify-content: flex-end;
   justify-content: flex-end;
   user-select: none;
   user-select: none;
-  
+
   > i {
   > i {
     font-size: 18px;
     font-size: 18px;
     margin-left: 2px;
     margin-left: 2px;
@@ -146,7 +176,7 @@ const PageSection = styled.div`
 `;
 `;
 
 
 const Code = styled.div`
 const Code = styled.div`
-  background: #181B21;
+  background: #181b21;
   padding: 10px 15px;
   padding: 10px 15px;
   border: 1px solid #ffffff44;
   border: 1px solid #ffffff44;
   border-radius: 5px;
   border-radius: 5px;
@@ -161,7 +191,8 @@ const Code = styled.div`
 const A = styled.a`
 const A = styled.a`
   color: #ffffff;
   color: #ffffff;
   text-decoration: underline;
   text-decoration: underline;
-  cursor: ${(props: { disabled?: boolean }) => props.disabled ? 'not-allowed' : 'pointer'};
+  cursor: ${(props: { disabled?: boolean }) =>
+    props.disabled ? "not-allowed" : "pointer"};
 `;
 `;
 
 
 const Placeholder = styled.div`
 const Placeholder = styled.div`
@@ -180,7 +211,7 @@ const Bold = styled.div`
 
 
 const Subtitle = styled.div`
 const Subtitle = styled.div`
   padding: 17px 0px 25px;
   padding: 17px 0px 25px;
-  font-family: 'Work Sans', sans-serif;
+  font-family: "Work Sans", sans-serif;
   font-size: 13px;
   font-size: 13px;
   color: #aaaabb;
   color: #aaaabb;
   margin-top: 3px;
   margin-top: 3px;
@@ -193,7 +224,7 @@ const ModalTitle = styled.div`
   margin: 0px 0px 13px;
   margin: 0px 0px 13px;
   display: flex;
   display: flex;
   flex: 1;
   flex: 1;
-  font-family: 'Assistant';
+  font-family: "Assistant";
   font-size: 18px;
   font-size: 18px;
   color: #ffffff;
   color: #ffffff;
   user-select: none;
   user-select: none;
@@ -226,7 +257,7 @@ const CloseButtonImg = styled.img`
   margin: 0 auto;
   margin: 0 auto;
 `;
 `;
 
 
-const StyledClusterInstructionsModal= styled.div`
+const StyledClusterInstructionsModal = styled.div`
   width: 100%;
   width: 100%;
   position: absolute;
   position: absolute;
   left: 0;
   left: 0;
@@ -236,4 +267,4 @@ const StyledClusterInstructionsModal= styled.div`
   overflow: hidden;
   overflow: hidden;
   border-radius: 6px;
   border-radius: 6px;
   background: #202227;
   background: #202227;
-`;
+`;

+ 40 - 39
dashboard/src/main/home/modals/IntegrationsInstructionsModal.tsx

@@ -1,27 +1,27 @@
-import React, { Component } from 'react';
-import styled from 'styled-components';
-import close from 'assets/close.png';
-import TabSelector from 'components/TabSelector';
+import React, { Component } from "react";
+import styled from "styled-components";
+import close from "assets/close.png";
+import TabSelector from "components/TabSelector";
 
 
-import { Context } from 'shared/Context';
+import { Context } from "shared/Context";
 
 
-type PropsType = {
-};
+type PropsType = {};
 
 
 type StateType = {
 type StateType = {
-  currentTab: string,
-  currentPage: number,
+  currentTab: string;
+  currentPage: number;
 };
 };
 
 
-const tabOptions = [
-  { label: 'MacOS', value: 'mac' }
-];
+const tabOptions = [{ label: "MacOS", value: "mac" }];
 
 
-export default class ClusterInstructionsModal extends Component<PropsType, StateType> {
+export default class ClusterInstructionsModal extends Component<
+  PropsType,
+  StateType
+> {
   state = {
   state = {
-    currentTab: 'mac',
+    currentTab: "mac",
     currentPage: 0,
     currentPage: 0,
-  }
+  };
 
 
   renderPage = () => {
   renderPage = () => {
     switch (this.state.currentPage) {
     switch (this.state.currentPage) {
@@ -30,31 +30,29 @@ export default class ClusterInstructionsModal extends Component<PropsType, State
           <Placeholder>
           <Placeholder>
             <Bold>Elastic Container Registry (ECR):</Bold>
             <Bold>Elastic Container Registry (ECR):</Bold>
             1. Run the following command on the Porter CLI.
             1. Run the following command on the Porter CLI.
-            <Code>
-              porter connect ecr
-            </Code>
+            <Code>porter connect ecr</Code>
             2. Enter the region your ECR instance belongs to. For example:
             2. Enter the region your ECR instance belongs to. For example:
-            <Code>
-              AWS Region: us-west-2
-            </Code>
-            3. Porter will automatically set up an IAM user in your AWS account to grant ECR access. Once this is done, it will prompt you to enter a name for the registry. Here you may enter any name you'd like.
-            <Code>
-              Give this registry a name: my-awesome-registry
-            </Code>
+            <Code>AWS Region: us-west-2</Code>
+            3. Porter will automatically set up an IAM user in your AWS account
+            to grant ECR access. Once this is done, it will prompt you to enter
+            a name for the registry. Here you may enter any name you'd like.
+            <Code>Give this registry a name: my-awesome-registry</Code>
           </Placeholder>
           </Placeholder>
         );
         );
       default:
       default:
-        return
+        return;
     }
     }
-  }
- 
+  };
+
   render() {
   render() {
     let { currentPage, currentTab } = this.state;
     let { currentPage, currentTab } = this.state;
     return (
     return (
       <StyledClusterInstructionsModal>
       <StyledClusterInstructionsModal>
-        <CloseButton onClick={() => {
-          this.context.setCurrentModal(null, null);
-        }}>
+        <CloseButton
+          onClick={() => {
+            this.context.setCurrentModal(null, null);
+          }}
+        >
           <CloseButtonImg src={close} />
           <CloseButtonImg src={close} />
         </CloseButton>
         </CloseButton>
 
 
@@ -63,7 +61,9 @@ export default class ClusterInstructionsModal extends Component<PropsType, State
         <TabSelector
         <TabSelector
           options={tabOptions}
           options={tabOptions}
           currentTab={currentTab}
           currentTab={currentTab}
-          setCurrentTab={(value: string) => this.setState({ currentTab: value })}
+          setCurrentTab={(value: string) =>
+            this.setState({ currentTab: value })
+          }
         />
         />
 
 
         {this.renderPage()}
         {this.renderPage()}
@@ -90,7 +90,7 @@ const PageSection = styled.div`
   color: #ffffff;
   color: #ffffff;
   justify-content: flex-end;
   justify-content: flex-end;
   user-select: none;
   user-select: none;
-  
+
   > i {
   > i {
     font-size: 18px;
     font-size: 18px;
     margin-left: 2px;
     margin-left: 2px;
@@ -104,7 +104,7 @@ const PageSection = styled.div`
 `;
 `;
 
 
 const Code = styled.div`
 const Code = styled.div`
-  background: #181B21;
+  background: #181b21;
   padding: 10px 15px;
   padding: 10px 15px;
   border: 1px solid #ffffff44;
   border: 1px solid #ffffff44;
   border-radius: 5px;
   border-radius: 5px;
@@ -119,7 +119,8 @@ const Code = styled.div`
 const A = styled.a`
 const A = styled.a`
   color: #ffffff;
   color: #ffffff;
   text-decoration: underline;
   text-decoration: underline;
-  cursor: ${(props: { disabled?: boolean }) => props.disabled ? 'not-allowed' : 'pointer'};
+  cursor: ${(props: { disabled?: boolean }) =>
+    props.disabled ? "not-allowed" : "pointer"};
 `;
 `;
 
 
 const Placeholder = styled.div`
 const Placeholder = styled.div`
@@ -138,7 +139,7 @@ const Bold = styled.div`
 
 
 const Subtitle = styled.div`
 const Subtitle = styled.div`
   padding: 10px 0px 20px;
   padding: 10px 0px 20px;
-  font-family: 'Work Sans', sans-serif;
+  font-family: "Work Sans", sans-serif;
   font-size: 13px;
   font-size: 13px;
   color: #aaaabb;
   color: #aaaabb;
   margin-top: 3px;
   margin-top: 3px;
@@ -151,7 +152,7 @@ const ModalTitle = styled.div`
   margin: 0px 0px 13px;
   margin: 0px 0px 13px;
   display: flex;
   display: flex;
   flex: 1;
   flex: 1;
-  font-family: 'Assistant';
+  font-family: "Assistant";
   font-size: 18px;
   font-size: 18px;
   color: #ffffff;
   color: #ffffff;
   user-select: none;
   user-select: none;
@@ -184,7 +185,7 @@ const CloseButtonImg = styled.img`
   margin: 0 auto;
   margin: 0 auto;
 `;
 `;
 
 
-const StyledClusterInstructionsModal= styled.div`
+const StyledClusterInstructionsModal = styled.div`
   width: 100%;
   width: 100%;
   position: absolute;
   position: absolute;
   left: 0;
   left: 0;
@@ -194,4 +195,4 @@ const StyledClusterInstructionsModal= styled.div`
   overflow: hidden;
   overflow: hidden;
   border-radius: 6px;
   border-radius: 6px;
   background: #202227;
   background: #202227;
-`;
+`;

+ 38 - 30
dashboard/src/main/home/modals/IntegrationsModal.tsx

@@ -1,35 +1,34 @@
-import React, { Component } from 'react';
-import styled from 'styled-components';
-import close from 'assets/close.png';
+import React, { Component } from "react";
+import styled from "styled-components";
+import close from "assets/close.png";
 
 
-import { Context } from 'shared/Context';
-import api from 'shared/api';
-import { integrationList } from 'shared/common';
+import { Context } from "shared/Context";
+import api from "shared/api";
+import { integrationList } from "shared/common";
 
 
-type PropsType = {
-};
+type PropsType = {};
 
 
 type StateType = {
 type StateType = {
-  integrations: any[],
+  integrations: any[];
 };
 };
 
 
 export default class IntegrationsModal extends Component<PropsType, StateType> {
 export default class IntegrationsModal extends Component<PropsType, StateType> {
   state = {
   state = {
     integrations: [] as any[],
     integrations: [] as any[],
-  }
+  };
 
 
   componentDidMount() {
   componentDidMount() {
     let { category } = this.context.currentModalData;
     let { category } = this.context.currentModalData;
-    if (category === 'kubernetes') {
-      api.getClusterIntegrations('<token>', {}, {}, (err: any, res: any) => {
+    if (category === "kubernetes") {
+      api.getClusterIntegrations("<token>", {}, {}, (err: any, res: any) => {
         if (err) {
         if (err) {
           console.log(err);
           console.log(err);
         } else {
         } else {
           this.setState({ integrations: res.data });
           this.setState({ integrations: res.data });
         }
         }
       });
       });
-    } else if (category === 'registry') {
-      api.getRegistryIntegrations('<token>', {}, {}, (err: any, res: any) => {
+    } else if (category === "registry") {
+      api.getRegistryIntegrations("<token>", {}, {}, (err: any, res: any) => {
         if (err) {
         if (err) {
           console.log(err);
           console.log(err);
         } else {
         } else {
@@ -38,7 +37,7 @@ export default class IntegrationsModal extends Component<PropsType, StateType> {
         }
         }
       });
       });
     } else {
     } else {
-      api.getRepoIntegrations('<token>', {}, {}, (err: any, res: any) => {
+      api.getRepoIntegrations("<token>", {}, {}, (err: any, res: any) => {
         if (err) {
         if (err) {
           console.log(err);
           console.log(err);
         } else {
         } else {
@@ -52,10 +51,15 @@ export default class IntegrationsModal extends Component<PropsType, StateType> {
     if (this.context.currentModalData) {
     if (this.context.currentModalData) {
       let { setCurrentIntegration } = this.context.currentModalData;
       let { setCurrentIntegration } = this.context.currentModalData;
       return this.state.integrations.map((integration: any, i: number) => {
       return this.state.integrations.map((integration: any, i: number) => {
-        let icon = integrationList[integration.service] && integrationList[integration.service].icon;
-        let disabled = integration.service === 'kube' || integration.service === 'docker' || integration.service === 'gcr';
+        let icon =
+          integrationList[integration.service] &&
+          integrationList[integration.service].icon;
+        let disabled =
+          integration.service === "kube" ||
+          integration.service === "docker" ||
+          integration.service === "gcr";
         return (
         return (
-          <IntegrationOption 
+          <IntegrationOption
             key={i}
             key={i}
             disabled={disabled}
             disabled={disabled}
             onClick={() => {
             onClick={() => {
@@ -71,20 +75,22 @@ export default class IntegrationsModal extends Component<PropsType, StateType> {
         );
         );
       });
       });
     }
     }
-  }
- 
+  };
+
   render() {
   render() {
     return (
     return (
       <StyledIntegrationsModal>
       <StyledIntegrationsModal>
-        <CloseButton onClick={() => {
-          this.context.setCurrentModal(null, null);
-        }}>
+        <CloseButton
+          onClick={() => {
+            this.context.setCurrentModal(null, null);
+          }}
+        >
           <CloseButtonImg src={close} />
           <CloseButtonImg src={close} />
         </CloseButton>
         </CloseButton>
 
 
         <ModalTitle>Add a New Integration</ModalTitle>
         <ModalTitle>Add a New Integration</ModalTitle>
         <Subtitle>Select the service you would like to connect to.</Subtitle>
         <Subtitle>Select the service you would like to connect to.</Subtitle>
-       
+
         <IntegrationsCatalog>
         <IntegrationsCatalog>
           {this.renderIntegrationsCatalog()}
           {this.renderIntegrationsCatalog()}
         </IntegrationsCatalog>
         </IntegrationsCatalog>
@@ -114,9 +120,11 @@ const IntegrationOption = styled.div`
   display: flex;
   display: flex;
   align-items: center;
   align-items: center;
   padding: 20px;
   padding: 20px;
-  cursor: ${(props: { disabled: boolean }) => props.disabled ? 'not-allowed' : 'pointer'};
+  cursor: ${(props: { disabled: boolean }) =>
+    props.disabled ? "not-allowed" : "pointer"};
   :hover {
   :hover {
-    background: ${(props: { disabled: boolean }) => props.disabled ? '' : '#ffffff22'};
+    background: ${(props: { disabled: boolean }) =>
+      props.disabled ? "" : "#ffffff22"};
   }
   }
 `;
 `;
 
 
@@ -132,7 +140,7 @@ const IntegrationsCatalog = styled.div`
 
 
 const Subtitle = styled.div`
 const Subtitle = styled.div`
   padding: 10px 0px;
   padding: 10px 0px;
-  font-family: 'Work Sans', sans-serif;
+  font-family: "Work Sans", sans-serif;
   font-size: 13px;
   font-size: 13px;
   color: #aaaabb;
   color: #aaaabb;
   overflow: hidden;
   overflow: hidden;
@@ -144,7 +152,7 @@ const ModalTitle = styled.div`
   margin: 0px 0px 13px;
   margin: 0px 0px 13px;
   display: flex;
   display: flex;
   flex: 1;
   flex: 1;
-  font-family: 'Assistant';
+  font-family: "Assistant";
   font-size: 18px;
   font-size: 18px;
   color: #ffffff;
   color: #ffffff;
   user-select: none;
   user-select: none;
@@ -177,7 +185,7 @@ const CloseButtonImg = styled.img`
   margin: 0 auto;
   margin: 0 auto;
 `;
 `;
 
 
-const StyledIntegrationsModal= styled.div`
+const StyledIntegrationsModal = styled.div`
   width: 100%;
   width: 100%;
   position: absolute;
   position: absolute;
   left: 0;
   left: 0;
@@ -187,4 +195,4 @@ const StyledIntegrationsModal= styled.div`
   overflow: hidden;
   overflow: hidden;
   border-radius: 6px;
   border-radius: 6px;
   background: #202227;
   background: #202227;
-`;
+`;

+ 43 - 25
dashboard/src/main/home/modals/Modal.tsx

@@ -1,41 +1,43 @@
-import React, { Component } from 'react';
-import styled from 'styled-components';
+import React, { Component } from "react";
+import styled from "styled-components";
 
 
 type PropsType = {
 type PropsType = {
-  onRequestClose: () => void,
-  width?: string,
-  height?: string,
-}
+  onRequestClose: () => void;
+  width?: string;
+  height?: string;
+};
 
 
-type StateType = {
-}
+type StateType = {};
 
 
 export default class Modal extends Component<PropsType, StateType> {
 export default class Modal extends Component<PropsType, StateType> {
   wrapperRef: any = React.createRef();
   wrapperRef: any = React.createRef();
 
 
   componentDidMount() {
   componentDidMount() {
-    document.addEventListener('mousedown', this.handleClickOutside.bind(this));
+    document.addEventListener("mousedown", this.handleClickOutside.bind(this));
   }
   }
 
 
   componentWillUnmount() {
   componentWillUnmount() {
-    document.removeEventListener('mousedown', this.handleClickOutside.bind(this));
+    document.removeEventListener(
+      "mousedown",
+      this.handleClickOutside.bind(this)
+    );
   }
   }
 
 
   handleClickOutside = (event: any) => {
   handleClickOutside = (event: any) => {
-    if (this.wrapperRef && this.wrapperRef.current && !this.wrapperRef.current.contains(event.target)) {
+    if (
+      this.wrapperRef &&
+      this.wrapperRef.current &&
+      !this.wrapperRef.current.contains(event.target)
+    ) {
       this.props.onRequestClose();
       this.props.onRequestClose();
     }
     }
-  }
+  };
 
 
   render() {
   render() {
     let { width, height } = this.props;
     let { width, height } = this.props;
     return (
     return (
       <Overlay>
       <Overlay>
-        <StyledModal
-          ref={this.wrapperRef}
-          width={width}
-          height={height}
-        >
+        <StyledModal ref={this.wrapperRef} width={width} height={height}>
           {this.props.children}
           {this.props.children}
         </StyledModal>
         </StyledModal>
       </Overlay>
       </Overlay>
@@ -51,19 +53,33 @@ const Overlay = styled.div`
   left: 0;
   left: 0;
   width: 100%;
   width: 100%;
   height: 100%;
   height: 100%;
-  background-color: rgba(0,0,0,0.6);
+  background-color: rgba(0, 0, 0, 0.6);
   z-index: 3;
   z-index: 3;
 `;
 `;
 
 
 const StyledModal = styled.div`
 const StyledModal = styled.div`
   position: absolute;
   position: absolute;
-  top: calc(50% - (${(props: { width?: string, height?: string }) => props.height ? props.height : '425px'} / 2));
-  left: calc(50% - (${(props: { width?: string, height?: string }) => props.width ? props.width : '760px'} / 2));
+  top: calc(
+    50% -
+      (
+        ${(props: { width?: string; height?: string }) =>
+            props.height ? props.height : "425px"} / 2
+      )
+  );
+  left: calc(
+    50% -
+      (
+        ${(props: { width?: string; height?: string }) =>
+            props.width ? props.width : "760px"} / 2
+      )
+  );
   display: flex;
   display: flex;
   justify-content: center;
   justify-content: center;
-  width: ${(props: { width?: string, height?: string }) => props.width ? props.width : '760px'};
+  width: ${(props: { width?: string; height?: string }) =>
+    props.width ? props.width : "760px"};
   max-width: 80vw;
   max-width: 80vw;
-  height: ${(props: { width?: string, height?: string }) => props.height ? props.height : '425px'};
+  height: ${(props: { width?: string; height?: string }) =>
+    props.height ? props.height : "425px"};
   border-radius: 7px;
   border-radius: 7px;
   border: 0;
   border: 0;
   background-color: #202227;
   background-color: #202227;
@@ -72,10 +88,12 @@ const StyledModal = styled.div`
   animation: floatInModal 0.5s 0s;
   animation: floatInModal 0.5s 0s;
   @keyframes floatInModal {
   @keyframes floatInModal {
     from {
     from {
-      opacity: 0; transform: translateY(30px);
+      opacity: 0;
+      transform: translateY(30px);
     }
     }
     to {
     to {
-      opacity: 1; transform: translateY(0px);
+      opacity: 1;
+      transform: translateY(0px);
     }
     }
   }
   }
-`;
+`;

+ 134 - 110
dashboard/src/main/home/modals/UpdateClusterModal.tsx

@@ -1,24 +1,24 @@
-import React, { Component } from 'react';
-import styled from 'styled-components';
-import close from 'assets/close.png';
-import gradient from 'assets/gradient.jpg';
+import React, { Component } from "react";
+import styled from "styled-components";
+import close from "assets/close.png";
+import gradient from "assets/gradient.jpg";
 
 
-import api from 'shared/api';
-import { Context } from 'shared/Context';
+import api from "shared/api";
+import { Context } from "shared/Context";
 
 
-import SaveButton from 'components/SaveButton';
-import InputRow from 'components/values-form/InputRow';
-import ConfirmOverlay from 'components/ConfirmOverlay';
-import { RouteComponentProps, withRouter } from 'react-router';
+import SaveButton from "components/SaveButton";
+import InputRow from "components/values-form/InputRow";
+import ConfirmOverlay from "components/ConfirmOverlay";
+import { RouteComponentProps, withRouter } from "react-router";
 
 
 type PropsType = RouteComponentProps & {
 type PropsType = RouteComponentProps & {
-  setRefreshClusters: (x: boolean) => void,
+  setRefreshClusters: (x: boolean) => void;
 };
 };
 
 
 type StateType = {
 type StateType = {
-  clusterName: string,
-  status: string | null,
-  showDeleteOverlay: boolean
+  clusterName: string;
+  status: string | null;
+  showDeleteOverlay: boolean;
 };
 };
 
 
 class UpdateClusterModal extends Component<PropsType, StateType> {
 class UpdateClusterModal extends Component<PropsType, StateType> {
@@ -30,109 +30,132 @@ class UpdateClusterModal extends Component<PropsType, StateType> {
 
 
   handleDelete = () => {
   handleDelete = () => {
     let { currentProject, currentCluster } = this.context;
     let { currentProject, currentCluster } = this.context;
-    this.setState({ status: 'loading' });
-
-    api.deleteCluster('<token>', {}, { 
-      project_id: currentProject.id,
-      cluster_id: currentCluster.id,
-    }, (err: any, res: any) => {
-
-      if (err) {
-        this.setState({ status: 'error' });
-        console.log(err)
-        return;
-      }
+    this.setState({ status: "loading" });
+
+    api.deleteCluster(
+      "<token>",
+      {},
+      {
+        project_id: currentProject.id,
+        cluster_id: currentCluster.id,
+      },
+      (err: any, res: any) => {
+        if (err) {
+          this.setState({ status: "error" });
+          console.log(err);
+          return;
+        }
+
+        if (!currentCluster?.infra_id) {
+          // TODO: make this more declarative from the Home component
+          this.props.setRefreshClusters(true);
+          this.setState({ status: "successful", showDeleteOverlay: false });
+          this.context.setCurrentModal(null, null);
+          this.props.history.push("dashboard");
+          return;
+        }
+
+        // Handle destroying infra we've provisioned
+        switch (currentCluster.service) {
+          case "eks":
+            api.destroyEKS(
+              "<token>",
+              { eks_name: currentCluster.name },
+              {
+                project_id: currentProject.id,
+                infra_id: currentCluster.infra_id,
+              },
+              (err: any, res: any) => {
+                if (err) {
+                  this.setState({ status: "error" });
+                  console.log(err);
+                } else {
+                  console.log("destroyed provisioned infra.");
+                }
+              }
+            );
+            break;
+
+          case "gke":
+            api.destroyGKE(
+              "<token>",
+              { gke_name: currentCluster.name },
+              {
+                project_id: currentProject.id,
+                infra_id: currentCluster.infra_id,
+              },
+              (err: any, res: any) => {
+                if (err) {
+                  this.setState({ status: "error" });
+                  console.log(err);
+                } else {
+                  console.log("destroyed provisioned infra.");
+                }
+              }
+            );
+            break;
+
+          case "doks":
+            api.destroyDOKS(
+              "<token>",
+              { doks_name: currentCluster.name },
+              {
+                project_id: currentProject.id,
+                infra_id: currentCluster.infra_id,
+              },
+              (err: any, res: any) => {
+                if (err) {
+                  this.setState({ status: "error" });
+                  console.log(err);
+                } else {
+                  console.log("destroyed provisioned infra.");
+                }
+              }
+            );
+            break;
+        }
 
 
-      if (!currentCluster?.infra_id) {
-        // TODO: make this more declarative from the Home component
         this.props.setRefreshClusters(true);
         this.props.setRefreshClusters(true);
-        this.setState({ status: 'successful', showDeleteOverlay: false });
+        this.setState({ status: "successful", showDeleteOverlay: false });
         this.context.setCurrentModal(null, null);
         this.context.setCurrentModal(null, null);
-        this.props.history.push("dashboard");
-        return;
       }
       }
-
-      // Handle destroying infra we've provisioned
-      switch (currentCluster.service) {
-        case 'eks':
-          api.destroyEKS('<token>', { eks_name: currentCluster.name }, { 
-            project_id: currentProject.id,
-            infra_id: currentCluster.infra_id,
-          }, (err: any, res: any) => {
-            if (err) {
-              this.setState({ status: 'error' });
-              console.log(err)
-            } else {
-              console.log('destroyed provisioned infra.');
-            }
-          });
-          break;
-
-        case 'gke':
-          api.destroyGKE('<token>', { gke_name: currentCluster.name }, { 
-            project_id: currentProject.id,
-            infra_id: currentCluster.infra_id,
-          }, (err: any, res: any) => {
-            if (err) {
-              this.setState({ status: 'error' });
-              console.log(err)
-            } else {
-              console.log('destroyed provisioned infra.');
-            }
-          });
-          break;
-
-        case 'doks':
-          api.destroyDOKS('<token>', { doks_name : currentCluster.name }, { 
-            project_id: currentProject.id,
-            infra_id: currentCluster.infra_id,
-          }, (err: any, res: any) => {
-            if (err) {
-              this.setState({ status: 'error' });
-              console.log(err)
-            } else {
-              console.log('destroyed provisioned infra.');
-            }
-          });
-          break;
-      }
-        
-      this.props.setRefreshClusters(true);
-      this.setState({ status: 'successful', showDeleteOverlay: false });
-      this.context.setCurrentModal(null, null);
-    });
-  }
+    );
+  };
 
 
   renderWarning = () => {
   renderWarning = () => {
     let { currentCluster } = this.context;
     let { currentCluster } = this.context;
     if (!currentCluster?.infra_id || !currentCluster.service) {
     if (!currentCluster?.infra_id || !currentCluster.service) {
-      return(
+      return (
         <Warning highlight={true}>
         <Warning highlight={true}>
-          ⚠️ Since this cluster was not provisioned by Porter, deleting the cluster will only detach this cluster from your project. To delete the cluster itself, you must do so manually.
+          ⚠️ Since this cluster was not provisioned by Porter, deleting the
+          cluster will only detach this cluster from your project. To delete the
+          cluster itself, you must do so manually.
         </Warning>
         </Warning>
-      )    
+      );
     }
     }
 
 
-    return(
+    return (
       <Warning highlight={true}>
       <Warning highlight={true}>
-        ⚠️ Deletion may result in dangling resources. Please visit your cloud provider's console to ensure that all resources have been removed. Note that deleting the cluster does not delete your registries.
+        ⚠️ Deletion may result in dangling resources. Please visit your cloud
+        provider's console to ensure that all resources have been removed. Note
+        that deleting the cluster does not delete your registries.
       </Warning>
       </Warning>
-    )    
-  }
+    );
+  };
 
 
   render() {
   render() {
     return (
     return (
       <StyledUpdateProjectModal>
       <StyledUpdateProjectModal>
-        <CloseButton onClick={() => {
-          this.context.setCurrentModal(null, null);
-        }}>
+        <CloseButton
+          onClick={() => {
+            this.context.setCurrentModal(null, null);
+          }}
+        >
           <CloseButtonImg src={close} />
           <CloseButtonImg src={close} />
         </CloseButton>
         </CloseButton>
 
 
         <ModalTitle>Cluster Settings</ModalTitle>
         <ModalTitle>Cluster Settings</ModalTitle>
-        <Subtitle>
-          Cluster name
-        </Subtitle>
+        <Subtitle>Cluster name</Subtitle>
 
 
         <InputWrapper>
         <InputWrapper>
           <DashboardIcon>
           <DashboardIcon>
@@ -140,26 +163,26 @@ class UpdateClusterModal extends Component<PropsType, StateType> {
           </DashboardIcon>
           </DashboardIcon>
           <InputRow
           <InputRow
             disabled={true}
             disabled={true}
-            type='string'
+            type="string"
             value={this.state.clusterName}
             value={this.state.clusterName}
             setValue={(x: string) => this.setState({ clusterName: x })}
             setValue={(x: string) => this.setState({ clusterName: x })}
-            placeholder='ex: perspective-vortex'
-            width='470px'
+            placeholder="ex: perspective-vortex"
+            width="470px"
           />
           />
         </InputWrapper>
         </InputWrapper>
 
 
         {this.renderWarning()}
         {this.renderWarning()}
 
 
-        <Help 
-          href='https://docs.getporter.dev/docs/getting-started-with-porter-on-aws#deleting-provisioned-resources'
-          target='_blank'
+        <Help
+          href="https://docs.getporter.dev/docs/getting-started-with-porter-on-aws#deleting-provisioned-resources"
+          target="_blank"
         >
         >
           <i className="material-icons">help_outline</i> Help
           <i className="material-icons">help_outline</i> Help
         </Help>
         </Help>
 
 
         <SaveButton
         <SaveButton
-          text='Delete Cluster'
-          color='#b91133'
+          text="Delete Cluster"
+          color="#b91133"
           onClick={() => this.setState({ showDeleteOverlay: true })}
           onClick={() => this.setState({ showDeleteOverlay: true })}
           status={this.state.status}
           status={this.state.status}
         />
         />
@@ -171,7 +194,7 @@ class UpdateClusterModal extends Component<PropsType, StateType> {
           onNo={() => this.setState({ showDeleteOverlay: false })}
           onNo={() => this.setState({ showDeleteOverlay: false })}
         />
         />
       </StyledUpdateProjectModal>
       </StyledUpdateProjectModal>
-      );
+    );
   }
   }
 }
 }
 
 
@@ -212,7 +235,8 @@ const Warning = styled.div`
     margin-right: 10px;
     margin-right: 10px;
     font-size: 18px;
     font-size: 18px;
   }
   }
-  color: ${(props: { highlight: boolean, makeFlush?: boolean }) => props.highlight ? '#f5cb42' : ''};
+  color: ${(props: { highlight: boolean; makeFlush?: boolean }) =>
+    props.highlight ? "#f5cb42" : ""};
 `;
 `;
 
 
 const DashboardIcon = styled.div`
 const DashboardIcon = styled.div`
@@ -228,7 +252,7 @@ const DashboardIcon = styled.div`
   display: flex;
   display: flex;
   align-items: center;
   align-items: center;
   justify-content: center;
   justify-content: center;
-  background: #676C7C;
+  background: #676c7c;
   border: 2px solid #8e94aa;
   border: 2px solid #8e94aa;
   color: white;
   color: white;
 
 
@@ -244,7 +268,7 @@ const InputWrapper = styled.div`
 
 
 const Subtitle = styled.div`
 const Subtitle = styled.div`
   margin-top: 23px;
   margin-top: 23px;
-  font-family: 'Work Sans', sans-serif;
+  font-family: "Work Sans", sans-serif;
   font-size: 13px;
   font-size: 13px;
   color: #aaaabb;
   color: #aaaabb;
   overflow: hidden;
   overflow: hidden;
@@ -257,7 +281,7 @@ const ModalTitle = styled.div`
   margin: 0px 0px 13px;
   margin: 0px 0px 13px;
   display: flex;
   display: flex;
   flex: 1;
   flex: 1;
-  font-family: 'Assistant';
+  font-family: "Assistant";
   font-size: 18px;
   font-size: 18px;
   color: #ffffff;
   color: #ffffff;
   user-select: none;
   user-select: none;
@@ -290,7 +314,7 @@ const CloseButtonImg = styled.img`
   margin: 0 auto;
   margin: 0 auto;
 `;
 `;
 
 
-const StyledUpdateProjectModal= styled.div`
+const StyledUpdateProjectModal = styled.div`
   width: 100%;
   width: 100%;
   position: absolute;
   position: absolute;
   left: 0;
   left: 0;
@@ -300,4 +324,4 @@ const StyledUpdateProjectModal= styled.div`
   overflow: hidden;
   overflow: hidden;
   border-radius: 6px;
   border-radius: 6px;
   background: #202227;
   background: #202227;
-`;
+`;

+ 57 - 35
dashboard/src/main/home/new-project/NewProject.tsx

@@ -1,26 +1,26 @@
-import React, { Component } from 'react';
-import styled from 'styled-components';
+import React, { Component } from "react";
+import styled from "styled-components";
 
 
-import gradient from 'assets/gradient.jpg';
-import { Context } from 'shared/Context';
-import { isAlphanumeric } from 'shared/common';
+import gradient from "assets/gradient.jpg";
+import { Context } from "shared/Context";
+import { isAlphanumeric } from "shared/common";
 
 
-import InputRow from 'components/values-form/InputRow';
-import Helper from 'components/values-form/Helper';
-import ProvisionerSettings from '../provisioner/ProvisionerSettings';
+import InputRow from "components/values-form/InputRow";
+import Helper from "components/values-form/Helper";
+import ProvisionerSettings from "../provisioner/ProvisionerSettings";
 
 
 type PropsType = {};
 type PropsType = {};
 
 
 type StateType = {
 type StateType = {
-  projectName: string,
-  selectedProvider: string | null,
+  projectName: string;
+  selectedProvider: string | null;
 };
 };
 
 
 export default class NewProject extends Component<PropsType, StateType> {
 export default class NewProject extends Component<PropsType, StateType> {
   state = {
   state = {
-    projectName: '',
+    projectName: "",
     selectedProvider: null as string | null,
     selectedProvider: null as string | null,
-  }
+  };
 
 
   render() {
   render() {
     let { projectName } = this.state;
     let { projectName } = this.state;
@@ -31,7 +31,12 @@ export default class NewProject extends Component<PropsType, StateType> {
         </TitleSection>
         </TitleSection>
         <Helper>
         <Helper>
           Project name
           Project name
-          <Warning highlight={!isAlphanumeric(this.state.projectName) && this.state.projectName !== ''}>
+          <Warning
+            highlight={
+              !isAlphanumeric(this.state.projectName) &&
+              this.state.projectName !== ""
+            }
+          >
             (lowercase letters, numbers, and "-" only)
             (lowercase letters, numbers, and "-" only)
           </Warning>
           </Warning>
           <Required>*</Required>
           <Required>*</Required>
@@ -39,20 +44,21 @@ export default class NewProject extends Component<PropsType, StateType> {
         <InputWrapper>
         <InputWrapper>
           <ProjectIcon>
           <ProjectIcon>
             <ProjectImage src={gradient} />
             <ProjectImage src={gradient} />
-            <Letter>{this.state.projectName ? this.state.projectName[0].toUpperCase() : '-'}</Letter>
+            <Letter>
+              {this.state.projectName
+                ? this.state.projectName[0].toUpperCase()
+                : "-"}
+            </Letter>
           </ProjectIcon>
           </ProjectIcon>
           <InputRow
           <InputRow
-            type='string'
+            type="string"
             value={this.state.projectName}
             value={this.state.projectName}
             setValue={(x: string) => this.setState({ projectName: x })}
             setValue={(x: string) => this.setState({ projectName: x })}
-            placeholder='ex: perspective-vortex'
-            width='470px'
+            placeholder="ex: perspective-vortex"
+            width="470px"
           />
           />
         </InputWrapper>
         </InputWrapper>
-        <ProvisionerSettings 
-          isInNewProject={true}
-          projectName={projectName}
-        />
+        <ProvisionerSettings isInNewProject={true} projectName={projectName} />
         <Br />
         <Br />
       </StyledNewProject>
       </StyledNewProject>
     );
     );
@@ -197,7 +203,7 @@ const Letter = styled.div`
   justify-content: center;
   justify-content: center;
   font-size: 24px;
   font-size: 24px;
   font-weight: 500;
   font-weight: 500;
-  font-family: 'Work Sans', sans-serif;
+  font-family: "Work Sans", sans-serif;
 `;
 `;
 
 
 const ProjectImage = styled.img`
 const ProjectImage = styled.img`
@@ -224,15 +230,17 @@ const InputWrapper = styled.div`
 `;
 `;
 
 
 const Warning = styled.span`
 const Warning = styled.span`
-  color: ${(props: { highlight: boolean, makeFlush?: boolean }) => props.highlight ? '#f5cb42' : ''};
-  margin-left: ${(props: { highlight: boolean, makeFlush?: boolean }) => props.makeFlush ? '' : '5px'};
+  color: ${(props: { highlight: boolean; makeFlush?: boolean }) =>
+    props.highlight ? "#f5cb42" : ""};
+  margin-left: ${(props: { highlight: boolean; makeFlush?: boolean }) =>
+    props.makeFlush ? "" : "5px"};
 `;
 `;
 
 
 const Icon = styled.img`
 const Icon = styled.img`
   height: 42px;
   height: 42px;
   margin-top: 30px;
   margin-top: 30px;
   margin-bottom: 15px;
   margin-bottom: 15px;
-  filter: ${(props: { bw?: boolean }) => props.bw ? 'grayscale(1)' : ''};
+  filter: ${(props: { bw?: boolean }) => (props.bw ? "grayscale(1)" : "")};
 `;
 `;
 
 
 const BlockDescription = styled.div`
 const BlockDescription = styled.div`
@@ -247,7 +255,7 @@ const BlockDescription = styled.div`
   display: -webkit-box;
   display: -webkit-box;
   overflow: hidden;
   overflow: hidden;
   -webkit-line-clamp: 2;
   -webkit-line-clamp: 2;
-  -webkit-box-orient: vertical;  
+  -webkit-box-orient: vertical;
 `;
 `;
 
 
 const BlockTitle = styled.div`
 const BlockTitle = styled.div`
@@ -273,26 +281,40 @@ const Block = styled.div`
   align-item: center;
   align-item: center;
   justify-content: space-between;
   justify-content: space-between;
   height: 170px;
   height: 170px;
-  cursor: ${(props: { disabled?: boolean }) => props.disabled ? '' : 'pointer'};
+  cursor: ${(props: { disabled?: boolean }) =>
+    props.disabled ? "" : "pointer"};
   color: #ffffff;
   color: #ffffff;
   position: relative;
   position: relative;
   background: #26282f;
   background: #26282f;
   box-shadow: 0 3px 5px 0px #00000022;
   box-shadow: 0 3px 5px 0px #00000022;
   :hover {
   :hover {
-    background: ${(props: { disabled?: boolean }) => props.disabled ? '' : '#ffffff11'};
+    background: ${(props: { disabled?: boolean }) =>
+      props.disabled ? "" : "#ffffff11"};
   }
   }
 
 
   animation: fadeIn 0.3s 0s;
   animation: fadeIn 0.3s 0s;
   @keyframes fadeIn {
   @keyframes fadeIn {
-    from { opacity: 0 }
-    to { opacity: 1 }
+    from {
+      opacity: 0;
+    }
+    to {
+      opacity: 1;
+    }
   }
   }
 `;
 `;
 
 
 const ShinyBlock = styled(Block)`
 const ShinyBlock = styled(Block)`
-  background: linear-gradient(36deg, rgba(240,106,40,0.9) 0%, rgba(229,83,229,0.9) 100%);
+  background: linear-gradient(
+    36deg,
+    rgba(240, 106, 40, 0.9) 0%,
+    rgba(229, 83, 229, 0.9) 100%
+  );
   :hover {
   :hover {
-    background: linear-gradient(36deg, rgba(240,106,40,1) 0%, rgba(229,83,229,1) 100%);
+    background: linear-gradient(
+      36deg,
+      rgba(240, 106, 40, 1) 0%,
+      rgba(229, 83, 229, 1) 100%
+    );
   }
   }
 `;
 `;
 
 
@@ -309,7 +331,7 @@ const BlockList = styled.div`
 const Title = styled.div`
 const Title = styled.div`
   font-size: 24px;
   font-size: 24px;
   font-weight: 600;
   font-weight: 600;
-  font-family: 'Work Sans', sans-serif;
+  font-family: "Work Sans", sans-serif;
   color: #ffffff;
   color: #ffffff;
   white-space: nowrap;
   white-space: nowrap;
   overflow: hidden;
   overflow: hidden;
@@ -329,7 +351,7 @@ const TitleSection = styled.div`
       margin-bottom: -2px;
       margin-bottom: -2px;
       font-size: 18px;
       font-size: 18px;
       margin-left: 18px;
       margin-left: 18px;
-      color: #858FAAaa;
+      color: #858faaaa;
       cursor: pointer;
       cursor: pointer;
       :hover {
       :hover {
         color: #aaaabb;
         color: #aaaabb;
@@ -344,4 +366,4 @@ const StyledNewProject = styled.div`
   position: relative;
   position: relative;
   padding-top: 50px;
   padding-top: 50px;
   margin-top: calc(50vh - 340px);
   margin-top: calc(50vh - 340px);
-`;
+`;

+ 164 - 136
dashboard/src/main/home/project-settings/InviteList.tsx

@@ -1,25 +1,24 @@
-import React, { Component } from 'react';
-import styled from 'styled-components';
+import React, { Component } from "react";
+import styled from "styled-components";
 
 
-import { InviteType } from 'shared/types';
-import api from 'shared/api';
-import { Context } from 'shared/Context';
+import { InviteType } from "shared/types";
+import api from "shared/api";
+import { Context } from "shared/Context";
 
 
-import Loading from 'components/Loading';
-import InputRow from 'components/values-form/InputRow';
-import Helper from 'components/values-form/Helper';
-import Heading from 'components/values-form/Heading';
+import Loading from "components/Loading";
+import InputRow from "components/values-form/InputRow";
+import Helper from "components/values-form/Helper";
+import Heading from "components/values-form/Heading";
 
 
-type PropsType = {
-}
+type PropsType = {};
 
 
 type StateType = {
 type StateType = {
-  loading: boolean,
-  invites: InviteType[],
-  email: string,
-  invalidEmail: boolean,
-  isHTTPS: boolean,
-}
+  loading: boolean;
+  invites: InviteType[];
+  email: string;
+  invalidEmail: boolean;
+  isHTTPS: boolean;
+};
 
 
 const dummyInvites = [];
 const dummyInvites = [];
 
 
@@ -27,10 +26,10 @@ export default class InviteList extends Component<PropsType, StateType> {
   state = {
   state = {
     loading: true,
     loading: true,
     invites: [] as InviteType[],
     invites: [] as InviteType[],
-    email: '',
+    email: "",
     invalidEmail: false,
     invalidEmail: false,
-    isHTTPS: (process.env.API_SERVER === 'dashboard.getporter.dev'),
-  }
+    isHTTPS: process.env.API_SERVER === "dashboard.getporter.dev",
+  };
 
 
   componentDidMount() {
   componentDidMount() {
     this.getInviteData();
     this.getInviteData();
@@ -38,18 +37,23 @@ export default class InviteList extends Component<PropsType, StateType> {
 
 
   getInviteData = () => {
   getInviteData = () => {
     let { currentProject } = this.context;
     let { currentProject } = this.context;
-    
-    this.setState({ loading: true })
-    api.getInvites('<token>', {}, {
-      id: currentProject.id
-    }, (err: any, res: any) => {
-      if (err) {
-        console.log(err);
-      } else {
-        this.setState({ invites: res.data, loading: false });
+
+    this.setState({ loading: true });
+    api.getInvites(
+      "<token>",
+      {},
+      {
+        id: currentProject.id,
+      },
+      (err: any, res: any) => {
+        if (err) {
+          console.log(err);
+        } else {
+          this.setState({ invites: res.data, loading: false });
+        }
       }
       }
-    });
-  }
+    );
+  };
 
 
   validateEmail = () => {
   validateEmail = () => {
     var regex = /^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
     var regex = /^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
@@ -59,61 +63,91 @@ export default class InviteList extends Component<PropsType, StateType> {
     } else {
     } else {
       this.setState({ invalidEmail: true });
       this.setState({ invalidEmail: true });
     }
     }
-  }
+  };
 
 
   createInvite = () => {
   createInvite = () => {
     let { currentProject } = this.context;
     let { currentProject } = this.context;
-    api.createInvite('<token>', { email: this.state.email }, { id: currentProject.id }, (err: any, res: any) => {
-      if (err) {
-        console.log(err);
-      } else {
-        this.getInviteData();
-        this.setState({ email: '' });
+    api.createInvite(
+      "<token>",
+      { email: this.state.email },
+      { id: currentProject.id },
+      (err: any, res: any) => {
+        if (err) {
+          console.log(err);
+        } else {
+          this.getInviteData();
+          this.setState({ email: "" });
+        }
       }
       }
-    })
-  }
+    );
+  };
 
 
   deleteInvite = (index: number) => {
   deleteInvite = (index: number) => {
     let { currentProject } = this.context;
     let { currentProject } = this.context;
-    api.deleteInvite('<token>', {}, {
-      id: currentProject.id, invId: this.state.invites[index].id
-    }, (err: any, res: any) => {
-      if (err) {
-        console.log(err);
-      } else {
-        this.getInviteData();
+    api.deleteInvite(
+      "<token>",
+      {},
+      {
+        id: currentProject.id,
+        invId: this.state.invites[index].id,
+      },
+      (err: any, res: any) => {
+        if (err) {
+          console.log(err);
+        } else {
+          this.getInviteData();
+        }
       }
       }
-    })
-  }
+    );
+  };
 
 
   replaceInvite = (index: number) => {
   replaceInvite = (index: number) => {
     let { currentProject } = this.context;
     let { currentProject } = this.context;
-    api.createInvite('<token>', { email: this.state.invites[index].email }, { id: currentProject.id }, (err: any, res: any) => {
-      if (err) {
-        console.log(err);
-      } else {
-        api.deleteInvite('<token>', {}, {
-          id: currentProject.id, invId: this.state.invites[index].id
-        }, (err: any, res: any) => {
-          if (err) {
-            console.log(err);
-          } else {
-            this.getInviteData();
-          }
-        })
+    api.createInvite(
+      "<token>",
+      { email: this.state.invites[index].email },
+      { id: currentProject.id },
+      (err: any, res: any) => {
+        if (err) {
+          console.log(err);
+        } else {
+          api.deleteInvite(
+            "<token>",
+            {},
+            {
+              id: currentProject.id,
+              invId: this.state.invites[index].id,
+            },
+            (err: any, res: any) => {
+              if (err) {
+                console.log(err);
+              } else {
+                this.getInviteData();
+              }
+            }
+          );
+        }
       }
       }
-    })
-  }
+    );
+  };
 
 
   copyToClip = (index: number) => {
   copyToClip = (index: number) => {
     let { currentProject } = this.context;
     let { currentProject } = this.context;
-    navigator.clipboard.writeText(
-      `${this.state.isHTTPS ? 'https://' : ''}${process.env.API_SERVER}/api/projects/${currentProject.id}/invites/${this.state.invites[index].token}`
-    ).then(function() {
-    }, function() {
-      console.log("couldn't copy link to clipboard");
-    })
-  }
+    navigator.clipboard
+      .writeText(
+        `${this.state.isHTTPS ? "https://" : ""}${
+          process.env.API_SERVER
+        }/api/projects/${currentProject.id}/invites/${
+          this.state.invites[index].token
+        }`
+      )
+      .then(
+        function () {},
+        function () {
+          console.log("couldn't copy link to clipboard");
+        }
+      );
+  };
 
 
   renderInvitations = () => {
   renderInvitations = () => {
     let { currentProject } = this.context;
     let { currentProject } = this.context;
@@ -122,46 +156,35 @@ export default class InviteList extends Component<PropsType, StateType> {
     } else {
     } else {
       var invContent: any[] = [];
       var invContent: any[] = [];
       var collabList: any[] = [];
       var collabList: any[] = [];
-      this.state.invites.sort((a: any, b: any) => (a.email > b.email) ? 1 : -1);
-      this.state.invites.sort((a: any, b: any) => (a.accepted > b.accepted) ? 1 : -1);
+      this.state.invites.sort((a: any, b: any) => (a.email > b.email ? 1 : -1));
+      this.state.invites.sort((a: any, b: any) =>
+        a.accepted > b.accepted ? 1 : -1
+      );
       for (let i = 0; i < this.state.invites.length; i++) {
       for (let i = 0; i < this.state.invites.length; i++) {
         if (this.state.invites[i].accepted) {
         if (this.state.invites[i].accepted) {
           collabList.push(
           collabList.push(
             <Tr key={i}>
             <Tr key={i}>
-              <MailTd isTop={i === 0}>
-                {this.state.invites[i].email}
-              </MailTd>
-              <LinkTd isTop={i === 0}>
-              </LinkTd>
+              <MailTd isTop={i === 0}>{this.state.invites[i].email}</MailTd>
+              <LinkTd isTop={i === 0}></LinkTd>
               <Td isTop={i === 0}>
               <Td isTop={i === 0}>
-                <CopyButton
-                  invis={true}
-                >
-                  Remove
-                </CopyButton>
+                <CopyButton invis={true}>Remove</CopyButton>
               </Td>
               </Td>
             </Tr>
             </Tr>
           );
           );
         } else if (this.state.invites[i].expired) {
         } else if (this.state.invites[i].expired) {
           invContent.push(
           invContent.push(
             <Tr key={i}>
             <Tr key={i}>
-              <MailTd isTop={i === 0}>
-                {this.state.invites[i].email}
-              </MailTd>
+              <MailTd isTop={i === 0}>{this.state.invites[i].email}</MailTd>
               <LinkTd isTop={i === 0}>
               <LinkTd isTop={i === 0}>
                 <Rower>
                 <Rower>
                   Link Expired.
                   Link Expired.
-                  <NewLinkButton
-                    onClick={() => this.replaceInvite(i)}
-                  >
+                  <NewLinkButton onClick={() => this.replaceInvite(i)}>
                     <u>Generate a new link</u>
                     <u>Generate a new link</u>
                   </NewLinkButton>
                   </NewLinkButton>
                 </Rower>
                 </Rower>
               </LinkTd>
               </LinkTd>
               <Td isTop={i === 0}>
               <Td isTop={i === 0}>
-                <CopyButton
-                  onClick={() => this.deleteInvite(i)}
-                >
+                <CopyButton onClick={() => this.deleteInvite(i)}>
                   Delete Invite
                   Delete Invite
                 </CopyButton>
                 </CopyButton>
               </Td>
               </Td>
@@ -170,33 +193,31 @@ export default class InviteList extends Component<PropsType, StateType> {
         } else {
         } else {
           invContent.push(
           invContent.push(
             <Tr key={i}>
             <Tr key={i}>
-              <MailTd isTop={i === 0}>
-                {this.state.invites[i].email}
-              </MailTd>
+              <MailTd isTop={i === 0}>{this.state.invites[i].email}</MailTd>
               <LinkTd isTop={i === 0}>
               <LinkTd isTop={i === 0}>
                 <Rower>
                 <Rower>
                   <ShareLink
                   <ShareLink
                     disabled={true}
                     disabled={true}
-                    type='string'
-                    value={`${this.state.isHTTPS ? 'https://' : ''}${process.env.API_SERVER}/api/projects/${currentProject.id}/invites/${this.state.invites[i].token}`}
-                    placeholder='Unable to retrieve link'
+                    type="string"
+                    value={`${this.state.isHTTPS ? "https://" : ""}${
+                      process.env.API_SERVER
+                    }/api/projects/${currentProject.id}/invites/${
+                      this.state.invites[i].token
+                    }`}
+                    placeholder="Unable to retrieve link"
                   />
                   />
-                  <CopyButton
-                    onClick={() => this.copyToClip(i)}
-                  >
+                  <CopyButton onClick={() => this.copyToClip(i)}>
                     Copy Link
                     Copy Link
                   </CopyButton>
                   </CopyButton>
                 </Rower>
                 </Rower>
               </LinkTd>
               </LinkTd>
               <Td isTop={i === 0}>
               <Td isTop={i === 0}>
-                <CopyButton
-                  onClick={() => this.deleteInvite(i)}
-                >
+                <CopyButton onClick={() => this.deleteInvite(i)}>
                   Delete Invite
                   Delete Invite
                 </CopyButton>
                 </CopyButton>
               </Td>
               </Td>
             </Tr>
             </Tr>
-          )
+          );
         }
         }
       }
       }
 
 
@@ -204,14 +225,22 @@ export default class InviteList extends Component<PropsType, StateType> {
         <>
         <>
           <Heading>Invites & Collaborators</Heading>
           <Heading>Invites & Collaborators</Heading>
           <Helper>Manage pending invites and view collaborators.</Helper>
           <Helper>Manage pending invites and view collaborators.</Helper>
-          {((invContent.length > 0) || (collabList.length > 0))
-            ? <Table><tbody>{invContent}{collabList}</tbody></Table>
-            : <Placeholder>This project currently has no invites or collaborators.</Placeholder>
-          }
+          {invContent.length > 0 || collabList.length > 0 ? (
+            <Table>
+              <tbody>
+                {invContent}
+                {collabList}
+              </tbody>
+            </Table>
+          ) : (
+            <Placeholder>
+              This project currently has no invites or collaborators.
+            </Placeholder>
+          )}
         </>
         </>
-      )
+      );
     }
     }
-  }
+  };
 
 
   render() {
   render() {
     return (
     return (
@@ -221,27 +250,22 @@ export default class InviteList extends Component<PropsType, StateType> {
         <DarkMatter />
         <DarkMatter />
         <InputRow
         <InputRow
           value={this.state.email}
           value={this.state.email}
-          type='text'
+          type="text"
           setValue={(x: string) => this.setState({ email: x })}
           setValue={(x: string) => this.setState({ email: x })}
-          width='calc(100%)'
-          placeholder='ex: mrp@getporter.dev'
+          width="calc(100%)"
+          placeholder="ex: mrp@getporter.dev"
         />
         />
         <ButtonWrapper>
         <ButtonWrapper>
-          <InviteButton
-            disabled={false}
-            onClick={() => this.validateEmail()}
-          >
+          <InviteButton disabled={false} onClick={() => this.validateEmail()}>
             Create Invite
             Create Invite
           </InviteButton>
           </InviteButton>
-          {this.state.invalidEmail &&
-            <Invalid>
-              Invalid email address. Please try again.
-            </Invalid>
-          }
+          {this.state.invalidEmail && (
+            <Invalid>Invalid email address. Please try again.</Invalid>
+          )}
         </ButtonWrapper>
         </ButtonWrapper>
         {this.renderInvitations()}
         {this.renderInvitations()}
       </>
       </>
-    )
+    );
   }
   }
 }
 }
 
 
@@ -271,7 +295,8 @@ const DarkMatter = styled.div`
 `;
 `;
 
 
 const CopyButton = styled.div`
 const CopyButton = styled.div`
-  visibility: ${(props: { invis?: boolean }) => props.invis ? 'hidden' : 'visible'};
+  visibility: ${(props: { invis?: boolean }) =>
+    props.invis ? "hidden" : "visible"};
   color: #ffffff;
   color: #ffffff;
   font-weight: 400;
   font-weight: 400;
   font-size: 13px;
   font-size: 13px;
@@ -307,7 +332,7 @@ const InviteButton = styled.div<{ disabled: boolean }>`
   height: 35px;
   height: 35px;
   font-size: 13px;
   font-size: 13px;
   font-weight: 500;
   font-weight: 500;
-  font-family: 'Work Sans', sans-serif;
+  font-family: "Work Sans", sans-serif;
   color: white;
   color: white;
   display: flex;
   display: flex;
   align-items: center;
   align-items: center;
@@ -320,13 +345,16 @@ const InviteButton = styled.div<{ disabled: boolean }>`
   justify-content: center;
   justify-content: center;
   border: 0;
   border: 0;
   border-radius: 5px;
   border-radius: 5px;
-  background: ${props => !props.disabled ? '#616FEEcc' : '#aaaabb'};
-  box-shadow: ${props => !props.disabled ? '0 2px 5px 0 #00000030' : 'none'};
-  cursor: ${props => !props.disabled ? 'pointer' : 'default'};
+  background: ${(props) => (!props.disabled ? "#616FEEcc" : "#aaaabb")};
+  box-shadow: ${(props) =>
+    !props.disabled ? "0 2px 5px 0 #00000030" : "none"};
+  cursor: ${(props) => (!props.disabled ? "pointer" : "default")};
   user-select: none;
   user-select: none;
-  :focus { outline: 0 }
+  :focus {
+    outline: 0;
+  }
   :hover {
   :hover {
-    filter: ${props => !props.disabled ? 'brightness(120%)' : ''};
+    filter: ${(props) => (!props.disabled ? "brightness(120%)" : "")};
   }
   }
   margin-bottom: 10px;
   margin-bottom: 10px;
 `;
 `;
@@ -371,14 +399,14 @@ const Table = styled.table`
 const Td = styled.td`
 const Td = styled.td`
   white-space: nowrap;
   white-space: nowrap;
   padding: 6px 0px;
   padding: 6px 0px;
-  border-top: ${(props: { isTop: boolean }) => (props.isTop ? 'none' : '1px solid #ffffff55')};
+  border-top: ${(props: { isTop: boolean }) =>
+    props.isTop ? "none" : "1px solid #ffffff55"};
   &:last-child {
   &:last-child {
     padding-right: 16px;
     padding-right: 16px;
   }
   }
 `;
 `;
 
 
-const Tr = styled.tr`
-`;
+const Tr = styled.tr``;
 
 
 const MailTd = styled(Td)`
 const MailTd = styled(Td)`
   padding: 0 12px;
   padding: 0 12px;
@@ -397,5 +425,5 @@ const Invalid = styled.div`
   color: #f5cb42;
   color: #f5cb42;
   margin-left: 15px;
   margin-left: 15px;
   font-size: 13px;
   font-size: 13px;
-  font-family: 'Work Sans', sans-serif;
-`;
+  font-family: "Work Sans", sans-serif;
+`;

+ 30 - 25
dashboard/src/main/home/project-settings/ProjectSettings.tsx

@@ -1,30 +1,30 @@
-import React, { Component } from 'react';
-import styled from 'styled-components';
+import React, { Component } from "react";
+import styled from "styled-components";
 
 
-import { Context } from 'shared/Context';
+import { Context } from "shared/Context";
 
 
-import InviteList from './InviteList';
-import TabRegion from 'components/TabRegion';
-import Heading from 'components/values-form/Heading';
-import Helper from 'components/values-form/Helper';
+import InviteList from "./InviteList";
+import TabRegion from "components/TabRegion";
+import Heading from "components/values-form/Heading";
+import Helper from "components/values-form/Helper";
 
 
 type PropsType = {};
 type PropsType = {};
 
 
 type StateType = {
 type StateType = {
-  projectName: string,
-  currentTab: string,
-}
+  projectName: string;
+  currentTab: string;
+};
 
 
 const tabOptions = [
 const tabOptions = [
-  { value: 'manage-access', label: 'Manage Access' },
-  { value: 'additional-settings', label: 'Additional Settings' }
+  { value: "manage-access", label: "Manage Access" },
+  { value: "additional-settings", label: "Additional Settings" },
 ];
 ];
 
 
 export default class ProjectSettings extends Component<PropsType, StateType> {
 export default class ProjectSettings extends Component<PropsType, StateType> {
   state = {
   state = {
-    projectName: '',
-    currentTab: 'manage-access',
-  }
+    projectName: "",
+    currentTab: "manage-access",
+  };
 
 
   componentDidMount() {
   componentDidMount() {
     let { currentProject } = this.context;
     let { currentProject } = this.context;
@@ -32,19 +32,21 @@ export default class ProjectSettings extends Component<PropsType, StateType> {
   }
   }
 
 
   renderTabContents = () => {
   renderTabContents = () => {
-    if (this.state.currentTab === 'manage-access') {
+    if (this.state.currentTab === "manage-access") {
       return <InviteList />;
       return <InviteList />;
     } else {
     } else {
       return (
       return (
         <>
         <>
           <Heading isAtTop={true}>Delete Project</Heading>
           <Heading isAtTop={true}>Delete Project</Heading>
           <Helper>
           <Helper>
-            Permanently delete this project. This will destroy all clusters tied to this project that have been provisioned by Porter. Note that this will not delete the image registries provisioned by Porter. To delete the registries, please do so manually in your cloud console.
+            Permanently delete this project. This will destroy all clusters tied
+            to this project that have been provisioned by Porter. Note that this
+            will not delete the image registries provisioned by Porter. To
+            delete the registries, please do so manually in your cloud console.
           </Helper>
           </Helper>
 
 
           <Warning highlight={true}>This action cannot be undone.</Warning>
           <Warning highlight={true}>This action cannot be undone.</Warning>
 
 
-
           <DeleteButton
           <DeleteButton
             onClick={() => {
             onClick={() => {
               this.context.setCurrentModal("UpdateProjectModal", {
               this.context.setCurrentModal("UpdateProjectModal", {
@@ -57,9 +59,9 @@ export default class ProjectSettings extends Component<PropsType, StateType> {
         </>
         </>
       );
       );
     }
     }
-  }
+  };
 
 
-  render () {
+  render() {
     return (
     return (
       <StyledProjectSettings>
       <StyledProjectSettings>
         <TitleSection>
         <TitleSection>
@@ -81,14 +83,15 @@ ProjectSettings.contextType = Context;
 
 
 const Warning = styled.div`
 const Warning = styled.div`
   font-size: 13px;
   font-size: 13px;
-  color: ${(props: { highlight: boolean, makeFlush?: boolean }) => props.highlight ? '#f5cb42' : ''};
+  color: ${(props: { highlight: boolean; makeFlush?: boolean }) =>
+    props.highlight ? "#f5cb42" : ""};
   margin-bottom: 20px;
   margin-bottom: 20px;
 `;
 `;
 
 
 const Title = styled.div`
 const Title = styled.div`
   font-size: 24px;
   font-size: 24px;
   font-weight: 600;
   font-weight: 600;
-  font-family: 'Work Sans', sans-serif;
+  font-family: "Work Sans", sans-serif;
   color: #ffffff;
   color: #ffffff;
   white-space: nowrap;
   white-space: nowrap;
   overflow: hidden;
   overflow: hidden;
@@ -114,7 +117,7 @@ const DeleteButton = styled.div`
   height: 35px;
   height: 35px;
   font-size: 13px;
   font-size: 13px;
   font-weight: 500;
   font-weight: 500;
-  font-family: 'Work Sans', sans-serif;
+  font-family: "Work Sans", sans-serif;
   color: white;
   color: white;
   display: flex;
   display: flex;
   align-items: center;
   align-items: center;
@@ -129,7 +132,9 @@ const DeleteButton = styled.div`
   box-shadow: 0 2px 5px 0 #00000030;
   box-shadow: 0 2px 5px 0 #00000030;
   cursor: pointer;
   cursor: pointer;
   user-select: none;
   user-select: none;
-  :focus { outline: 0 }
+  :focus {
+    outline: 0;
+  }
   :hover {
   :hover {
     filter: brightness(120%);
     filter: brightness(120%);
   }
   }
@@ -138,4 +143,4 @@ const DeleteButton = styled.div`
   :hover {
   :hover {
     filter: brightness(120%);
     filter: brightness(120%);
   }
   }
-`;
+`;

+ 196 - 184
dashboard/src/main/home/provisioner/AWSFormSection.tsx

@@ -1,219 +1,234 @@
-import React, { Component } from 'react';
-import styled from 'styled-components';
-
-import close from 'assets/close.png';
-import { isAlphanumeric } from 'shared/common';
-import api from 'shared/api';
-import { Context } from 'shared/Context';
-import { ProjectType, InfraType } from 'shared/types';
-
-import SelectRow from 'components/values-form/SelectRow';
-import InputRow from 'components/values-form/InputRow';
-import Helper from 'components/values-form/Helper';
-import Heading from 'components/values-form/Heading';
-import SaveButton from 'components/SaveButton';
-import CheckboxList from 'components/values-form/CheckboxList';
-import { RouteComponentProps, withRouter } from 'react-router';
+import React, { Component } from "react";
+import styled from "styled-components";
+
+import close from "assets/close.png";
+import { isAlphanumeric } from "shared/common";
+import api from "shared/api";
+import { Context } from "shared/Context";
+import { InfraType } from "shared/types";
+
+import SelectRow from "components/values-form/SelectRow";
+import InputRow from "components/values-form/InputRow";
+import Helper from "components/values-form/Helper";
+import Heading from "components/values-form/Heading";
+import SaveButton from "components/SaveButton";
+import CheckboxList from "components/values-form/CheckboxList";
+import { RouteComponentProps, withRouter } from "react-router";
 
 
 type PropsType = RouteComponentProps & {
 type PropsType = RouteComponentProps & {
-  setSelectedProvisioner: (x: string | null) => void,
-  handleError: () => void,
-  projectName: string,
-  infras: InfraType[],
+  setSelectedProvisioner: (x: string | null) => void;
+  handleError: () => void;
+  projectName: string;
+  infras: InfraType[];
 };
 };
 
 
 type StateType = {
 type StateType = {
-  awsRegion: string,
-  awsAccessId: string,
-  awsSecretKey: string,
-  selectedInfras: { value: string, label: string }[],
-  buttonStatus: string,
+  awsRegion: string;
+  awsAccessId: string;
+  awsSecretKey: string;
+  selectedInfras: { value: string; label: string }[];
+  buttonStatus: string;
 };
 };
 
 
 const provisionOptions = [
 const provisionOptions = [
-  { value: 'ecr', label: 'Elastic Container Registry (ECR)' },
-  { value: 'eks', label: 'Elastic Kubernetes Service (EKS)' },
+  { value: "ecr", label: "Elastic Container Registry (ECR)" },
+  { value: "eks", label: "Elastic Kubernetes Service (EKS)" },
 ];
 ];
 
 
 const regionOptions = [
 const regionOptions = [
-  { value: 'us-east-1', label: 'US East (N. Virginia) us-east-1' },
-  { value: 'us-east-2', label: 'US East (Ohio) us-east-2' },
-  { value: 'us-west-1', label: 'US West (N. California) us-west-1' },
-  { value: 'us-west-2', label: 'US West (Oregon) us-west-2' },
-  { value: 'af-south-1', label: 'Africa (Cape Town) af-south-1' },
-  { value: 'ap-east-1', label: 'Asia Pacific (Hong Kong)ap-east-1' },
-  { value: 'ap-south-1', label: 'Asia Pacific (Mumbai) ap-south-1' },
-  { value: 'ap-northeast-2', label: 'Asia Pacific (Seoul) ap-northeast-2' },
-  { value: 'ap-southeast-1', label: 'Asia Pacific (Singapore) ap-southeast-1' },
-  { value: 'ap-southeast-2', label: 'Asia Pacific (Sydney) ap-southeast-2' },
-  { value: 'ap-northeast-1', label: 'Asia Pacific (Tokyo) ap-northeast-1' },
-  { value: 'ca-central-1', label: 'Canada (Central) ca-central-1' },
-  { value: 'eu-central-1', label: 'Europe (Frankfurt) eu-central-1' },
-  { value: 'eu-west-1', label: 'Europe (Ireland) eu-west-1' },
-  { value: 'eu-west-2', label: 'Europe (London) eu-west-2' },
-  { value: 'eu-south-1', label: 'Europe (Milan) eu-south-1' },
-  { value: 'eu-west-3', label: 'Europe (Paris) eu-west-3' },
-  { value: 'eu-north-1', label: 'Europe (Stockholm) eu-north-1' },
-  { value: 'me-south-1', label: 'Middle East (Bahrain) me-south-1' },
-  { value: 'sa-east-1', label: 'South America (São Paulo) sa-east-1' },
+  { value: "us-east-1", label: "US East (N. Virginia) us-east-1" },
+  { value: "us-east-2", label: "US East (Ohio) us-east-2" },
+  { value: "us-west-1", label: "US West (N. California) us-west-1" },
+  { value: "us-west-2", label: "US West (Oregon) us-west-2" },
+  { value: "af-south-1", label: "Africa (Cape Town) af-south-1" },
+  { value: "ap-east-1", label: "Asia Pacific (Hong Kong)ap-east-1" },
+  { value: "ap-south-1", label: "Asia Pacific (Mumbai) ap-south-1" },
+  { value: "ap-northeast-2", label: "Asia Pacific (Seoul) ap-northeast-2" },
+  { value: "ap-southeast-1", label: "Asia Pacific (Singapore) ap-southeast-1" },
+  { value: "ap-southeast-2", label: "Asia Pacific (Sydney) ap-southeast-2" },
+  { value: "ap-northeast-1", label: "Asia Pacific (Tokyo) ap-northeast-1" },
+  { value: "ca-central-1", label: "Canada (Central) ca-central-1" },
+  { value: "eu-central-1", label: "Europe (Frankfurt) eu-central-1" },
+  { value: "eu-west-1", label: "Europe (Ireland) eu-west-1" },
+  { value: "eu-west-2", label: "Europe (London) eu-west-2" },
+  { value: "eu-south-1", label: "Europe (Milan) eu-south-1" },
+  { value: "eu-west-3", label: "Europe (Paris) eu-west-3" },
+  { value: "eu-north-1", label: "Europe (Stockholm) eu-north-1" },
+  { value: "me-south-1", label: "Middle East (Bahrain) me-south-1" },
+  { value: "sa-east-1", label: "South America (São Paulo) sa-east-1" },
 ];
 ];
 
 
 // TODO: Consolidate across forms w/ HOC
 // TODO: Consolidate across forms w/ HOC
 class AWSFormSection extends Component<PropsType, StateType> {
 class AWSFormSection extends Component<PropsType, StateType> {
   state = {
   state = {
-    awsRegion: 'us-east-1',
-    awsAccessId: '',
-    awsSecretKey: '',
+    awsRegion: "us-east-1",
+    awsAccessId: "",
+    awsSecretKey: "",
     selectedInfras: [...provisionOptions],
     selectedInfras: [...provisionOptions],
-    buttonStatus: '',
-  }
+    buttonStatus: "",
+  };
 
 
   componentDidMount = () => {
   componentDidMount = () => {
     let { infras } = this.props;
     let { infras } = this.props;
     let { selectedInfras } = this.state;
     let { selectedInfras } = this.state;
 
 
     if (infras) {
     if (infras) {
-      
       // From the dashboard, only uncheck and disable if "creating" or "created"
       // From the dashboard, only uncheck and disable if "creating" or "created"
       let filtered = selectedInfras;
       let filtered = selectedInfras;
-      infras.forEach(
-        (infra: InfraType, i: number) => {
-          let { kind, status } = infra;
-          if (status === 'creating' || status === 'created') {
-            filtered = filtered.filter((item: any) => {
-              return item.value !== kind;
-            });
-          }
+      infras.forEach((infra: InfraType, i: number) => {
+        let { kind, status } = infra;
+        if (status === "creating" || status === "created") {
+          filtered = filtered.filter((item: any) => {
+            return item.value !== kind;
+          });
         }
         }
-      );
+      });
       this.setState({ selectedInfras: filtered });
       this.setState({ selectedInfras: filtered });
     }
     }
-  }
+  };
 
 
   checkFormDisabled = () => {
   checkFormDisabled = () => {
-    let { 
-      awsRegion,
-      awsAccessId, 
-      awsSecretKey, 
-      selectedInfras,
-    } = this.state;
+    let { awsRegion, awsAccessId, awsSecretKey, selectedInfras } = this.state;
     let { projectName } = this.props;
     let { projectName } = this.props;
-    if (projectName || projectName === '') {
+    if (projectName || projectName === "") {
       return (
       return (
-        !isAlphanumeric(projectName) 
-          || !(awsAccessId !== '' && awsSecretKey !== '' && awsRegion !== '')
-          || selectedInfras.length === 0
+        !isAlphanumeric(projectName) ||
+        !(awsAccessId !== "" && awsSecretKey !== "" && awsRegion !== "") ||
+        selectedInfras.length === 0
       );
       );
     } else {
     } else {
       return (
       return (
-        !(awsAccessId !== '' && awsSecretKey !== '' && awsRegion !== '')
-          || selectedInfras.length === 0
+        !(awsAccessId !== "" && awsSecretKey !== "" && awsRegion !== "") ||
+        selectedInfras.length === 0
       );
       );
     }
     }
-  }
+  };
 
 
   // Step 1: Create a project
   // Step 1: Create a project
   createProject = (callback?: any) => {
   createProject = (callback?: any) => {
-    console.log('Creating project');
+    console.log("Creating project");
     let { projectName, handleError } = this.props;
     let { projectName, handleError } = this.props;
-    let { 
-      user, 
-      setProjects, 
-      setCurrentProject, 
-      currentProject 
-    } = this.context;
-
-    api.createProject('<token>', { name: projectName }, {
-    }, (err: any, res: any) => {
-      if (err) {
-        console.log(err);
-        handleError();
-        return;
-      } else {
-        let proj = res.data;
-
-        // Need to set project list for dropdown
-        // TODO: consolidate into ProjectSection (case on exists in list on set)
-        api.getProjects('<token>', {}, { 
-          id: user.userId 
-        }, (err: any, res: any) => {
-          if (err) {
-            console.log(err);
-            handleError();
-            return;
-          }
-          setProjects(res.data);
-          setCurrentProject(proj, () => {
-            callback && callback()
-          });
-        });
+    let { user, setProjects, setCurrentProject, currentProject } = this.context;
+
+    api.createProject(
+      "<token>",
+      { name: projectName },
+      {},
+      (err: any, res: any) => {
+        if (err) {
+          console.log(err);
+          handleError();
+          return;
+        } else {
+          let proj = res.data;
+
+          // Need to set project list for dropdown
+          // TODO: consolidate into ProjectSection (case on exists in list on set)
+          api.getProjects(
+            "<token>",
+            {},
+            {
+              id: user.userId,
+            },
+            (err: any, res: any) => {
+              if (err) {
+                console.log(err);
+                handleError();
+                return;
+              }
+              setProjects(res.data);
+              setCurrentProject(proj, () => {
+                callback && callback();
+              });
+            }
+          );
+        }
       }
       }
-    });
-  }
+    );
+  };
 
 
   provisionECR = (callback?: any) => {
   provisionECR = (callback?: any) => {
-    console.log('Provisioning ECR');
+    console.log("Provisioning ECR");
     let { awsAccessId, awsSecretKey, awsRegion } = this.state;
     let { awsAccessId, awsSecretKey, awsRegion } = this.state;
     let { currentProject } = this.context;
     let { currentProject } = this.context;
     let { handleError } = this.props;
     let { handleError } = this.props;
 
 
-    api.createAWSIntegration('<token>', {
-      aws_region: awsRegion,
-      aws_access_key_id: awsAccessId,
-      aws_secret_access_key: awsSecretKey,
-    }, { id: currentProject.id }, (err: any, res: any) => {
-      if (err) {
-        console.log(err);
-        handleError();
-        return;
-      }
-
-      api.provisionECR('<token>', {
-        aws_integration_id: res.data.id,
-        ecr_name: `${currentProject.name}-registry`
-      }, {id: currentProject.id}, (err: any, res: any) => {
+    api.createAWSIntegration(
+      "<token>",
+      {
+        aws_region: awsRegion,
+        aws_access_key_id: awsAccessId,
+        aws_secret_access_key: awsSecretKey,
+      },
+      { id: currentProject.id },
+      (err: any, res: any) => {
         if (err) {
         if (err) {
           console.log(err);
           console.log(err);
           handleError();
           handleError();
           return;
           return;
         }
         }
-        callback && callback();
-      })
-      
-    });
-  }
+
+        api.provisionECR(
+          "<token>",
+          {
+            aws_integration_id: res.data.id,
+            ecr_name: `${currentProject.name}-registry`,
+          },
+          { id: currentProject.id },
+          (err: any, res: any) => {
+            if (err) {
+              console.log(err);
+              handleError();
+              return;
+            }
+            callback && callback();
+          }
+        );
+      }
+    );
+  };
 
 
   provisionEKS = () => {
   provisionEKS = () => {
-    console.log('Provisioning EKS');
+    console.log("Provisioning EKS");
     let { handleError } = this.props;
     let { handleError } = this.props;
     let { awsAccessId, awsSecretKey, awsRegion } = this.state;
     let { awsAccessId, awsSecretKey, awsRegion } = this.state;
     let { currentProject } = this.context;
     let { currentProject } = this.context;
 
 
-    let clusterName = `${currentProject.name}-cluster`
-    api.createAWSIntegration('<token>', {
-      aws_region: awsRegion,
-      aws_access_key_id: awsAccessId,
-      aws_secret_access_key: awsSecretKey,
-      aws_cluster_id: clusterName,
-    }, { id: currentProject.id }, (err: any, res: any) => {
-      if (err) {
-        console.log(err);
-        handleError();
-        return;
-      }
-      api.provisionEKS('<token>', {
-        aws_integration_id: res.data.id,
-        eks_name: clusterName,
-      }, { id: currentProject.id}, (err: any, eks: any) => {
+    let clusterName = `${currentProject.name}-cluster`;
+    api.createAWSIntegration(
+      "<token>",
+      {
+        aws_region: awsRegion,
+        aws_access_key_id: awsAccessId,
+        aws_secret_access_key: awsSecretKey,
+        aws_cluster_id: clusterName,
+      },
+      { id: currentProject.id },
+      (err: any, res: any) => {
         if (err) {
         if (err) {
           console.log(err);
           console.log(err);
           handleError();
           handleError();
           return;
           return;
         }
         }
-        this.props.history.push("provisioner");
-      })
-    })
-  }
+        api.provisionEKS(
+          "<token>",
+          {
+            aws_integration_id: res.data.id,
+            eks_name: clusterName,
+          },
+          { id: currentProject.id },
+          (err: any, eks: any) => {
+            if (err) {
+              console.log(err);
+              handleError();
+              return;
+            }
+            this.props.history.push("provisioner");
+          }
+        );
+      }
+    );
+  };
 
 
   // TODO: handle generically (with > 2 steps)
   // TODO: handle generically (with > 2 steps)
   onCreateAWS = () => {
   onCreateAWS = () => {
@@ -224,7 +239,7 @@ class AWSFormSection extends Component<PropsType, StateType> {
       if (selectedInfras.length === 2) {
       if (selectedInfras.length === 2) {
         // Case: project exists, provision ECR + EKS
         // Case: project exists, provision ECR + EKS
         this.provisionECR(this.provisionEKS);
         this.provisionECR(this.provisionEKS);
-      } else if (selectedInfras[0].value === 'ecr') {
+      } else if (selectedInfras[0].value === "ecr") {
         // Case: project exists, only provision ECR
         // Case: project exists, only provision ECR
         this.provisionECR(() => this.props.history.push("provisioner"));
         this.provisionECR(() => this.props.history.push("provisioner"));
       } else {
       } else {
@@ -233,28 +248,25 @@ class AWSFormSection extends Component<PropsType, StateType> {
       }
       }
     } else {
     } else {
       if (selectedInfras.length === 2) {
       if (selectedInfras.length === 2) {
-        // Case: project DNE, provision ECR + EKS 
+        // Case: project DNE, provision ECR + EKS
         this.createProject(() => this.provisionECR(this.provisionEKS));
         this.createProject(() => this.provisionECR(this.provisionEKS));
-      } else if (selectedInfras[0].value === 'ecr') {
+      } else if (selectedInfras[0].value === "ecr") {
         // Case: project DNE, only provision ECR
         // Case: project DNE, only provision ECR
-        this.createProject(() => this.provisionECR(() => {
-          this.props.history.push("provisioner");
-        }));
+        this.createProject(() =>
+          this.provisionECR(() => {
+            this.props.history.push("provisioner");
+          })
+        );
       } else {
       } else {
         // Case: project DNE, only provision EKS
         // Case: project DNE, only provision EKS
         this.createProject(this.provisionEKS);
         this.createProject(this.provisionEKS);
       }
       }
     }
     }
-  }
+  };
 
 
   render() {
   render() {
     let { setSelectedProvisioner } = this.props;
     let { setSelectedProvisioner } = this.props;
-    let {
-      awsRegion,
-      awsAccessId,
-      awsSecretKey,
-      selectedInfras,
-    } = this.state;
+    let { awsRegion, awsAccessId, awsSecretKey, selectedInfras } = this.state;
 
 
     return (
     return (
       <StyledAWSFormSection>
       <StyledAWSFormSection>
@@ -264,38 +276,38 @@ class AWSFormSection extends Component<PropsType, StateType> {
           </CloseButton>
           </CloseButton>
           <Heading isAtTop={true}>
           <Heading isAtTop={true}>
             AWS Credentials
             AWS Credentials
-            <GuideButton 
-              href='https://docs.getporter.dev/docs/getting-started-with-porter-on-aws' 
-              target='_blank'
+            <GuideButton
+              href="https://docs.getporter.dev/docs/getting-started-with-porter-on-aws"
+              target="_blank"
             >
             >
-              <i className="material-icons-outlined">help</i> 
+              <i className="material-icons-outlined">help</i>
               Guide
               Guide
             </GuideButton>
             </GuideButton>
           </Heading>
           </Heading>
           <SelectRow
           <SelectRow
             options={regionOptions}
             options={regionOptions}
-            width='100%'
+            width="100%"
             value={awsRegion}
             value={awsRegion}
-            dropdownMaxHeight='240px'
+            dropdownMaxHeight="240px"
             setActiveValue={(x: string) => this.setState({ awsRegion: x })}
             setActiveValue={(x: string) => this.setState({ awsRegion: x })}
-            label='📍 AWS Region'
+            label="📍 AWS Region"
           />
           />
           <InputRow
           <InputRow
-            type='text'
+            type="text"
             value={awsAccessId}
             value={awsAccessId}
             setValue={(x: string) => this.setState({ awsAccessId: x })}
             setValue={(x: string) => this.setState({ awsAccessId: x })}
-            label='👤 AWS Access ID'
-            placeholder='ex: AKIAIOSFODNN7EXAMPLE'
-            width='100%'
+            label="👤 AWS Access ID"
+            placeholder="ex: AKIAIOSFODNN7EXAMPLE"
+            width="100%"
             isRequired={true}
             isRequired={true}
           />
           />
           <InputRow
           <InputRow
-            type='password'
+            type="password"
             value={awsSecretKey}
             value={awsSecretKey}
             setValue={(x: string) => this.setState({ awsSecretKey: x })}
             setValue={(x: string) => this.setState({ awsSecretKey: x })}
-            label='🔒 AWS Secret Key'
-            placeholder='○ ○ ○ ○ ○ ○ ○ ○ ○'
-            width='100%'
+            label="🔒 AWS Secret Key"
+            placeholder="○ ○ ○ ○ ○ ○ ○ ○ ○"
+            width="100%"
             isRequired={true}
             isRequired={true}
           />
           />
           <Br />
           <Br />
@@ -304,18 +316,18 @@ class AWSFormSection extends Component<PropsType, StateType> {
           <CheckboxList
           <CheckboxList
             options={provisionOptions}
             options={provisionOptions}
             selected={selectedInfras}
             selected={selectedInfras}
-            setSelected={(x: { value: string, label: string }[]) => {
+            setSelected={(x: { value: string; label: string }[]) => {
               this.setState({ selectedInfras: x });
               this.setState({ selectedInfras: x });
             }}
             }}
           />
           />
         </FormSection>
         </FormSection>
         {this.props.children ? this.props.children : <Padding />}
         {this.props.children ? this.props.children : <Padding />}
         <SaveButton
         <SaveButton
-          text='Submit'
+          text="Submit"
           disabled={this.checkFormDisabled()}
           disabled={this.checkFormDisabled()}
           onClick={this.onCreateAWS}
           onClick={this.onCreateAWS}
           makeFlush={true}
           makeFlush={true}
-          helper='Note: Provisioning can take up to 15 minutes'
+          helper="Note: Provisioning can take up to 15 minutes"
         />
         />
       </StyledAWSFormSection>
       </StyledAWSFormSection>
     );
     );
@@ -402,4 +414,4 @@ const GuideButton = styled.a`
 const CloseButtonImg = styled.img`
 const CloseButtonImg = styled.img`
   width: 14px;
   width: 14px;
   margin: 0 auto;
   margin: 0 auto;
-`;
+`;

+ 101 - 97
dashboard/src/main/home/provisioner/DOFormSection.tsx

@@ -1,143 +1,143 @@
-import React, { Component } from 'react';
-import styled from 'styled-components';
+import React, { Component } from "react";
+import styled from "styled-components";
 
 
-import close from 'assets/close.png';
-import { isAlphanumeric } from 'shared/common';
-import api from 'shared/api';
-import { Context } from 'shared/Context';
-import { ProjectType, InfraType } from 'shared/types';
+import close from "assets/close.png";
+import { isAlphanumeric } from "shared/common";
+import api from "shared/api";
+import { Context } from "shared/Context";
+import { InfraType } from "shared/types";
 
 
-import SelectRow from 'components/values-form/SelectRow';
-import Helper from 'components/values-form/Helper';
-import Heading from 'components/values-form/Heading';
-import SaveButton from 'components/SaveButton';
-import CheckboxList from 'components/values-form/CheckboxList';
+import SelectRow from "components/values-form/SelectRow";
+import Helper from "components/values-form/Helper";
+import Heading from "components/values-form/Heading";
+import SaveButton from "components/SaveButton";
+import CheckboxList from "components/values-form/CheckboxList";
 
 
 type PropsType = {
 type PropsType = {
-  setSelectedProvisioner: (x: string | null) => void,
-  handleError: () => void,
-  projectName: string,
-  infras: InfraType[],
+  setSelectedProvisioner: (x: string | null) => void;
+  handleError: () => void;
+  projectName: string;
+  infras: InfraType[];
 };
 };
 
 
 type StateType = {
 type StateType = {
-  selectedInfras: { value: string, label: string }[],
-  subscriptionTier: string,
-  doRegion: string,
+  selectedInfras: { value: string; label: string }[];
+  subscriptionTier: string;
+  doRegion: string;
 };
 };
 
 
 const provisionOptions = [
 const provisionOptions = [
-  { value: 'docr', label: 'Digital Ocean Container Registry' },
-  { value: 'doks', label: 'Digital Ocean Kubernetes Service' },
+  { value: "docr", label: "Digital Ocean Container Registry" },
+  { value: "doks", label: "Digital Ocean Kubernetes Service" },
 ];
 ];
 
 
 const tierOptions = [
 const tierOptions = [
-  { value: 'basic', label: 'Basic' },
-  { value: 'starter', label: 'Starter' },
-  { value: 'professional', label: 'Professional' },
+  { value: "basic", label: "Basic" },
+  { value: "starter", label: "Starter" },
+  { value: "professional", label: "Professional" },
 ];
 ];
 
 
 const regionOptions = [
 const regionOptions = [
-  { value: 'ams3', label: 'Amsterdam 3' },
-  { value: 'blr1', label: 'Bangalore 1' },
-  { value: 'fra1', label: 'Frankfurt 1' },
-  { value: 'lon1', label: 'London 1' },
-  { value: 'nyc1', label: 'New York 1' },
-  { value: 'nyc3', label: 'New York 3' },
-  { value: 'sfo2', label: 'San Francisco 2' },
-  { value: 'sfo3', label: 'San Francisco 3' },
-  { value: 'sgp1', label: 'Singapore 1' },
-  { value: 'tor1', label: 'Toronto 1' },
+  { value: "ams3", label: "Amsterdam 3" },
+  { value: "blr1", label: "Bangalore 1" },
+  { value: "fra1", label: "Frankfurt 1" },
+  { value: "lon1", label: "London 1" },
+  { value: "nyc1", label: "New York 1" },
+  { value: "nyc3", label: "New York 3" },
+  { value: "sfo2", label: "San Francisco 2" },
+  { value: "sfo3", label: "San Francisco 3" },
+  { value: "sgp1", label: "Singapore 1" },
+  { value: "tor1", label: "Toronto 1" },
 ];
 ];
 
 
 // TODO: Consolidate across forms w/ HOC
 // TODO: Consolidate across forms w/ HOC
 export default class DOFormSection extends Component<PropsType, StateType> {
 export default class DOFormSection extends Component<PropsType, StateType> {
   state = {
   state = {
     selectedInfras: [...provisionOptions],
     selectedInfras: [...provisionOptions],
-    subscriptionTier: 'starter',
-    doRegion: 'nyc1',
-  }
+    subscriptionTier: "starter",
+    doRegion: "nyc1",
+  };
 
 
   componentDidMount = () => {
   componentDidMount = () => {
     let { infras } = this.props;
     let { infras } = this.props;
     let { selectedInfras } = this.state;
     let { selectedInfras } = this.state;
 
 
     if (infras) {
     if (infras) {
-      
       // From the dashboard, only uncheck and disable if "creating" or "created"
       // From the dashboard, only uncheck and disable if "creating" or "created"
       let filtered = selectedInfras;
       let filtered = selectedInfras;
-      infras.forEach(
-        (infra: InfraType, i: number) => {
-          let { kind, status } = infra;
-          if (status === 'creating' || status === 'created') {
-            filtered = filtered.filter((item: any) => {
-              return item.value !== kind;
-            });
-          }
+      infras.forEach((infra: InfraType, i: number) => {
+        let { kind, status } = infra;
+        if (status === "creating" || status === "created") {
+          filtered = filtered.filter((item: any) => {
+            return item.value !== kind;
+          });
         }
         }
-      );
+      });
       this.setState({ selectedInfras: filtered });
       this.setState({ selectedInfras: filtered });
     }
     }
-  }
+  };
 
 
   checkFormDisabled = () => {
   checkFormDisabled = () => {
-    let { 
-      selectedInfras,
-    } = this.state;
+    let { selectedInfras } = this.state;
     let { projectName } = this.props;
     let { projectName } = this.props;
-    if (projectName || projectName === '') {
+    if (projectName || projectName === "") {
       return !isAlphanumeric(projectName) || selectedInfras.length === 0;
       return !isAlphanumeric(projectName) || selectedInfras.length === 0;
     } else {
     } else {
       return selectedInfras.length === 0;
       return selectedInfras.length === 0;
     }
     }
-  }
+  };
 
 
   // Step 1: Create a project
   // Step 1: Create a project
   createProject = (callback?: any) => {
   createProject = (callback?: any) => {
-    console.log('Creating project');
+    console.log("Creating project");
     let { projectName, handleError } = this.props;
     let { projectName, handleError } = this.props;
-    let { 
-      user, 
-      setProjects, 
-      setCurrentProject, 
-    } = this.context;
+    let { user, setProjects, setCurrentProject } = this.context;
 
 
-    api.createProject('<token>', { name: projectName }, {
-    }, (err: any, res: any) => {
-      if (err) {
-        console.log(err);
-        handleError();
-        return;
-      } else {
-        let proj = res.data;
+    api.createProject(
+      "<token>",
+      { name: projectName },
+      {},
+      (err: any, res: any) => {
+        if (err) {
+          console.log(err);
+          handleError();
+          return;
+        } else {
+          let proj = res.data;
 
 
-        // Need to set project list for dropdown
-        // TODO: consolidate into ProjectSection (case on exists in list on set)
-        api.getProjects('<token>', {}, { 
-          id: user.userId 
-        }, (err: any, res: any) => {
-          if (err) {
-            console.log(err);
-            handleError();
-            return;
-          }
-          setProjects(res.data);
-          setCurrentProject(proj);
-          callback && callback(proj.id);
-        });
+          // Need to set project list for dropdown
+          // TODO: consolidate into ProjectSection (case on exists in list on set)
+          api.getProjects(
+            "<token>",
+            {},
+            {
+              id: user.userId,
+            },
+            (err: any, res: any) => {
+              if (err) {
+                console.log(err);
+                handleError();
+                return;
+              }
+              setProjects(res.data);
+              setCurrentProject(proj);
+              callback && callback(proj.id);
+            }
+          );
+        }
       }
       }
-    });
-  }
+    );
+  };
 
 
   doRedirect = (projectId: number) => {
   doRedirect = (projectId: number) => {
     let { subscriptionTier, doRegion, selectedInfras } = this.state;
     let { subscriptionTier, doRegion, selectedInfras } = this.state;
     let redirectUrl = `/api/oauth/projects/${projectId}/digitalocean?project_id=${projectId}&provision=do`;
     let redirectUrl = `/api/oauth/projects/${projectId}/digitalocean?project_id=${projectId}&provision=do`;
     redirectUrl += `&tier=${subscriptionTier}&region=${doRegion}`;
     redirectUrl += `&tier=${subscriptionTier}&region=${doRegion}`;
-    selectedInfras.forEach((option: { value: string, label: string }) => {
+    selectedInfras.forEach((option: { value: string; label: string }) => {
       redirectUrl += `&infras=${option.value}`;
       redirectUrl += `&infras=${option.value}`;
     });
     });
     window.location.href = redirectUrl;
     window.location.href = redirectUrl;
-  }
+  };
 
 
   // TODO: handle generically (with > 2 steps)
   // TODO: handle generically (with > 2 steps)
   onCreateDO = () => {
   onCreateDO = () => {
@@ -150,7 +150,7 @@ export default class DOFormSection extends Component<PropsType, StateType> {
     } else {
     } else {
       this.createProject((projectId: number) => this.doRedirect(projectId));
       this.createProject((projectId: number) => this.doRedirect(projectId));
     }
     }
-  }
+  };
 
 
   render() {
   render() {
     let { setSelectedProvisioner } = this.props;
     let { setSelectedProvisioner } = this.props;
@@ -165,37 +165,41 @@ export default class DOFormSection extends Component<PropsType, StateType> {
           <Heading isAtTop={true}>DigitalOcean Settings</Heading>
           <Heading isAtTop={true}>DigitalOcean Settings</Heading>
           <SelectRow
           <SelectRow
             options={tierOptions}
             options={tierOptions}
-            width='100%'
+            width="100%"
             value={subscriptionTier}
             value={subscriptionTier}
-            setActiveValue={(x: string) => this.setState({ subscriptionTier: x })}
-            label='💰 Subscription Tier'
+            setActiveValue={(x: string) =>
+              this.setState({ subscriptionTier: x })
+            }
+            label="💰 Subscription Tier"
           />
           />
           <SelectRow
           <SelectRow
             options={regionOptions}
             options={regionOptions}
-            width='100%'
-            dropdownMaxHeight='240px'
+            width="100%"
+            dropdownMaxHeight="240px"
             value={doRegion}
             value={doRegion}
             setActiveValue={(x: string) => this.setState({ doRegion: x })}
             setActiveValue={(x: string) => this.setState({ doRegion: x })}
-            label='📍 DigitalOcean Region'
+            label="📍 DigitalOcean Region"
           />
           />
           <Br />
           <Br />
           <Heading>DigitalOcean Resources</Heading>
           <Heading>DigitalOcean Resources</Heading>
-          <Helper>Porter will provision the following DigitalOcean resources</Helper>
+          <Helper>
+            Porter will provision the following DigitalOcean resources
+          </Helper>
           <CheckboxList
           <CheckboxList
             options={provisionOptions}
             options={provisionOptions}
             selected={selectedInfras}
             selected={selectedInfras}
-            setSelected={(x: { value: string, label: string }[]) => {
+            setSelected={(x: { value: string; label: string }[]) => {
               this.setState({ selectedInfras: x });
               this.setState({ selectedInfras: x });
             }}
             }}
           />
           />
         </FormSection>
         </FormSection>
         {this.props.children ? this.props.children : <Padding />}
         {this.props.children ? this.props.children : <Padding />}
         <SaveButton
         <SaveButton
-          text='Submit'
+          text="Submit"
           disabled={this.checkFormDisabled()}
           disabled={this.checkFormDisabled()}
           onClick={this.onCreateDO}
           onClick={this.onCreateDO}
           makeFlush={true}
           makeFlush={true}
-          helper='Note: Provisioning can take up to 15 minutes'
+          helper="Note: Provisioning can take up to 15 minutes"
         />
         />
       </StyledAWSFormSection>
       </StyledAWSFormSection>
     );
     );
@@ -280,4 +284,4 @@ const GuideButton = styled.a`
 const CloseButtonImg = styled.img`
 const CloseButtonImg = styled.img`
   width: 14px;
   width: 14px;
   margin: 0 auto;
   margin: 0 auto;
-`;
+`;

+ 48 - 40
dashboard/src/main/home/provisioner/ExistingClusterSection.tsx

@@ -1,58 +1,66 @@
-import React, { Component } from 'react';
-import styled from 'styled-components';
+import React, { Component } from "react";
+import styled from "styled-components";
 
 
-import api from 'shared/api';
-import { ProjectType } from 'shared/types';
-import { isAlphanumeric } from 'shared/common';
-import { Context } from 'shared/Context';
+import api from "shared/api";
+import { ProjectType } from "shared/types";
+import { isAlphanumeric } from "shared/common";
+import { Context } from "shared/Context";
 
 
-import SaveButton from 'components/SaveButton';
-import CheckboxList from 'components/values-form/CheckboxList';
-import { RouteComponentProps, withRouter } from 'react-router';
+import SaveButton from "components/SaveButton";
+import { RouteComponentProps, withRouter } from "react-router";
 
 
 type PropsType = RouteComponentProps & {
 type PropsType = RouteComponentProps & {
-  projectName: string,
+  projectName: string;
 };
 };
 
 
 type StateType = {
 type StateType = {
-  buttonStatus: string,
+  buttonStatus: string;
 };
 };
 
 
 class ExistingClusterSection extends Component<PropsType, StateType> {
 class ExistingClusterSection extends Component<PropsType, StateType> {
   state = {
   state = {
-    buttonStatus: '',
-  }
+    buttonStatus: "",
+  };
 
 
   onCreateProject = () => {
   onCreateProject = () => {
     let { projectName } = this.props;
     let { projectName } = this.props;
     let { user, setProjects, setCurrentProject } = this.context;
     let { user, setProjects, setCurrentProject } = this.context;
 
 
-    this.setState({ buttonStatus: 'loading' });
-    api.createProject('<token>', { name: projectName }, {
-    }, (err: any, res: any) => {
-      if (err) {
-        console.log(err);
-      } else {
-        api.getProjects('<token>', {}, { 
-          id: user.userId 
-        }, (err: any, res: any) => {
-          if (err) {
-            console.log(err)
-          } else if (res.data) {
-            setProjects(res.data);
-            if (res.data.length > 0) {
-              let proj = res.data.find((el: ProjectType) => {
-                return el.name === projectName;
-              });
-              setCurrentProject(proj);
+    this.setState({ buttonStatus: "loading" });
+    api.createProject(
+      "<token>",
+      { name: projectName },
+      {},
+      (err: any, res: any) => {
+        if (err) {
+          console.log(err);
+        } else {
+          api.getProjects(
+            "<token>",
+            {},
+            {
+              id: user.userId,
+            },
+            (err: any, res: any) => {
+              if (err) {
+                console.log(err);
+              } else if (res.data) {
+                setProjects(res.data);
+                if (res.data.length > 0) {
+                  let proj = res.data.find((el: ProjectType) => {
+                    return el.name === projectName;
+                  });
+                  setCurrentProject(proj);
 
 
-              this.props.history.push("dashboard")
-            } 
-          }
-        });
+                  this.props.history.push("dashboard");
+                }
+              }
+            }
+          );
+        }
       }
       }
-    });
-  }
+    );
+  };
 
 
   render() {
   render() {
     let { children, projectName } = this.props;
     let { children, projectName } = this.props;
@@ -65,12 +73,12 @@ class ExistingClusterSection extends Component<PropsType, StateType> {
         </Placeholder>
         </Placeholder>
         {children ? children : <Padding />}
         {children ? children : <Padding />}
         <SaveButton
         <SaveButton
-          text='Submit'
+          text="Submit"
           disabled={!isAlphanumeric(projectName)}
           disabled={!isAlphanumeric(projectName)}
           onClick={this.onCreateProject}
           onClick={this.onCreateProject}
           status={buttonStatus}
           status={buttonStatus}
           makeFlush={true}
           makeFlush={true}
-          helper='Note: Provisioning can take up to 15 minutes'
+          helper="Note: Provisioning can take up to 15 minutes"
         />
         />
       </StyledExistingClusterSection>
       </StyledExistingClusterSection>
     );
     );
@@ -101,4 +109,4 @@ const Placeholder = styled.div`
   justify-content: center;
   justify-content: center;
   color: #ffffff44;
   color: #ffffff44;
   font-size: 13px;
   font-size: 13px;
-`;
+`;

+ 192 - 185
dashboard/src/main/home/provisioner/GCPFormSection.tsx

@@ -1,220 +1,232 @@
-import React, { Component } from 'react';
-import styled from 'styled-components';
-
-import close from 'assets/close.png';
-import { isAlphanumeric } from 'shared/common';
-import api from 'shared/api';
-import { Context } from 'shared/Context';
-import { ProjectType, InfraType } from 'shared/types';
-
-import SelectRow from 'components/values-form/SelectRow';
-import InputRow from 'components/values-form/InputRow';
-import Helper from 'components/values-form/Helper';
-import Heading from 'components/values-form/Heading';
-import SaveButton from 'components/SaveButton';
-import CheckboxList from 'components/values-form/CheckboxList';
-import { RouteComponentProps, withRouter } from 'react-router';
+import React, { Component } from "react";
+import styled from "styled-components";
+
+import close from "assets/close.png";
+import { isAlphanumeric } from "shared/common";
+import api from "shared/api";
+import { Context } from "shared/Context";
+import { InfraType } from "shared/types";
+
+import SelectRow from "components/values-form/SelectRow";
+import InputRow from "components/values-form/InputRow";
+import Helper from "components/values-form/Helper";
+import Heading from "components/values-form/Heading";
+import SaveButton from "components/SaveButton";
+import CheckboxList from "components/values-form/CheckboxList";
+import { RouteComponentProps, withRouter } from "react-router";
 
 
 type PropsType = RouteComponentProps & {
 type PropsType = RouteComponentProps & {
-  setSelectedProvisioner: (x: string | null) => void,
-  handleError: () => void,
-  projectName: string,
-  infras: InfraType[],
+  setSelectedProvisioner: (x: string | null) => void;
+  handleError: () => void;
+  projectName: string;
+  infras: InfraType[];
 };
 };
 
 
 type StateType = {
 type StateType = {
-  gcpRegion: string,
-  gcpProjectId: string,
-  gcpKeyData: string,
-  selectedInfras: { value: string, label: string }[],
-  buttonStatus: string,
+  gcpRegion: string;
+  gcpProjectId: string;
+  gcpKeyData: string;
+  selectedInfras: { value: string; label: string }[];
+  buttonStatus: string;
 };
 };
 
 
 const provisionOptions = [
 const provisionOptions = [
-  { value: 'gcr', label: 'Google Container Registry (GCR)' },
-  { value: 'gke', label: 'Google Kubernetes Engine (GKE)' },
+  { value: "gcr", label: "Google Container Registry (GCR)" },
+  { value: "gke", label: "Google Kubernetes Engine (GKE)" },
 ];
 ];
 
 
 const regionOptions = [
 const regionOptions = [
-  { value: 'asia-east1', label: 'asia-east1' },
-  { value: 'asia-east2', label: 'asia-east2' },
-  { value: 'asia-northeast1', label: 'asia-northeast1' },
-  { value: 'asia-northeast2', label: 'asia-northeast2' },
-  { value: 'asia-northeast3', label: 'asia-northeast3' },
-  { value: 'asia-south1', label: 'asia-south1' },
-  { value: 'asia-southeast1', label: 'asia-southeast1' },
-  { value: 'asia-southeast2', label: 'asia-southeast2' },
-  { value: 'australia-southeast1', label: 'australia-southeast1' },
-  { value: 'europe-north1', label: 'europe-north1' },
-  { value: 'europe-west1', label: 'europe-west1' },
-  { value: 'europe-west2', label: 'europe-west2' },
-  { value: 'europe-west3', label: 'europe-west3' },
-  { value: 'europe-west4', label: 'europe-west4' },
-  { value: 'europe-west6', label: 'europe-west6' },
-  { value: 'northamerica-northeast1', label: 'northamerica-northeast1' },
-  { value: 'southamerica-east1', label: 'southamerica-east1' },
-  { value: 'us-central1', label: 'us-central1' },
-  { value: 'us-east1', label: 'us-east1' },
-  { value: 'us-east4', label: 'us-east4' },
-  { value: 'us-west1', label: 'us-west1' },
-  { value: 'us-west2', label: 'us-west2' },
-  { value: 'us-west3', label: 'us-west3' },
-  { value: 'us-west4', label: 'us-west4' },
-]
+  { value: "asia-east1", label: "asia-east1" },
+  { value: "asia-east2", label: "asia-east2" },
+  { value: "asia-northeast1", label: "asia-northeast1" },
+  { value: "asia-northeast2", label: "asia-northeast2" },
+  { value: "asia-northeast3", label: "asia-northeast3" },
+  { value: "asia-south1", label: "asia-south1" },
+  { value: "asia-southeast1", label: "asia-southeast1" },
+  { value: "asia-southeast2", label: "asia-southeast2" },
+  { value: "australia-southeast1", label: "australia-southeast1" },
+  { value: "europe-north1", label: "europe-north1" },
+  { value: "europe-west1", label: "europe-west1" },
+  { value: "europe-west2", label: "europe-west2" },
+  { value: "europe-west3", label: "europe-west3" },
+  { value: "europe-west4", label: "europe-west4" },
+  { value: "europe-west6", label: "europe-west6" },
+  { value: "northamerica-northeast1", label: "northamerica-northeast1" },
+  { value: "southamerica-east1", label: "southamerica-east1" },
+  { value: "us-central1", label: "us-central1" },
+  { value: "us-east1", label: "us-east1" },
+  { value: "us-east4", label: "us-east4" },
+  { value: "us-west1", label: "us-west1" },
+  { value: "us-west2", label: "us-west2" },
+  { value: "us-west3", label: "us-west3" },
+  { value: "us-west4", label: "us-west4" },
+];
 
 
 class GCPFormSection extends Component<PropsType, StateType> {
 class GCPFormSection extends Component<PropsType, StateType> {
   state = {
   state = {
-    gcpRegion: 'us-east1',
-    gcpProjectId: '',
-    gcpKeyData: '',
+    gcpRegion: "us-east1",
+    gcpProjectId: "",
+    gcpKeyData: "",
     selectedInfras: [...provisionOptions],
     selectedInfras: [...provisionOptions],
-    buttonStatus: '',
-  }
+    buttonStatus: "",
+  };
 
 
   componentDidMount = () => {
   componentDidMount = () => {
     let { infras } = this.props;
     let { infras } = this.props;
     let { selectedInfras } = this.state;
     let { selectedInfras } = this.state;
 
 
     if (infras) {
     if (infras) {
-      
       // From the dashboard, only uncheck and disable if "creating" or "created"
       // From the dashboard, only uncheck and disable if "creating" or "created"
       let filtered = selectedInfras;
       let filtered = selectedInfras;
-      infras.forEach(
-        (infra: InfraType, i: number) => {
-          let { kind, status } = infra;
-          if (status === 'creating' || status === 'created') {
-            filtered = filtered.filter((item: any) => {
-              return item.value !== kind;
-            });
-          }
+      infras.forEach((infra: InfraType, i: number) => {
+        let { kind, status } = infra;
+        if (status === "creating" || status === "created") {
+          filtered = filtered.filter((item: any) => {
+            return item.value !== kind;
+          });
         }
         }
-      );
+      });
       this.setState({ selectedInfras: filtered });
       this.setState({ selectedInfras: filtered });
     }
     }
-  }
+  };
 
 
   checkFormDisabled = () => {
   checkFormDisabled = () => {
-    let { 
-      gcpRegion,
-      gcpProjectId, 
-      gcpKeyData, 
-      selectedInfras,
-    } = this.state;
+    let { gcpRegion, gcpProjectId, gcpKeyData, selectedInfras } = this.state;
     let { projectName } = this.props;
     let { projectName } = this.props;
-    if (projectName || projectName === '') {
+    if (projectName || projectName === "") {
       return (
       return (
-        !isAlphanumeric(projectName) 
-          || !(gcpProjectId !== '' && gcpKeyData !== '' && gcpRegion !== '')
-          || selectedInfras.length === 0
+        !isAlphanumeric(projectName) ||
+        !(gcpProjectId !== "" && gcpKeyData !== "" && gcpRegion !== "") ||
+        selectedInfras.length === 0
       );
       );
     } else {
     } else {
       return (
       return (
-        !(gcpProjectId !== '' && gcpKeyData !== '' && gcpRegion !== '')
-          || selectedInfras.length === 0
+        !(gcpProjectId !== "" && gcpKeyData !== "" && gcpRegion !== "") ||
+        selectedInfras.length === 0
       );
       );
     }
     }
-  }
+  };
 
 
   // Step 1: Create a project
   // Step 1: Create a project
   createProject = (callback?: any) => {
   createProject = (callback?: any) => {
-    console.log('Creating project');
+    console.log("Creating project");
     let { projectName, handleError } = this.props;
     let { projectName, handleError } = this.props;
-    let { 
-      user, 
-      setProjects, 
-      setCurrentProject, 
-    } = this.context;
-
-    api.createProject('<token>', { name: projectName }, {
-    }, (err: any, res: any) => {
-      if (err) {
-        console.log(err);
-        handleError();
-        return;
-      } else {
-        let proj = res.data;
-
-        // Need to set project list for dropdown
-        // TODO: consolidate into ProjectSection (case on exists in list on set)
-        api.getProjects('<token>', {}, { 
-          id: user.userId 
-        }, (err: any, res: any) => {
-          if (err) {
-            console.log(err);
-            handleError();
-            return;
-          }
-          setProjects(res.data);
-          setCurrentProject(proj);
-          callback && callback();
-        });
+    let { user, setProjects, setCurrentProject } = this.context;
+
+    api.createProject(
+      "<token>",
+      { name: projectName },
+      {},
+      (err: any, res: any) => {
+        if (err) {
+          console.log(err);
+          handleError();
+          return;
+        } else {
+          let proj = res.data;
+
+          // Need to set project list for dropdown
+          // TODO: consolidate into ProjectSection (case on exists in list on set)
+          api.getProjects(
+            "<token>",
+            {},
+            {
+              id: user.userId,
+            },
+            (err: any, res: any) => {
+              if (err) {
+                console.log(err);
+                handleError();
+                return;
+              }
+              setProjects(res.data);
+              setCurrentProject(proj);
+              callback && callback();
+            }
+          );
+        }
       }
       }
-    });
-  }
+    );
+  };
 
 
   provisionGCR = (id: number, callback?: any) => {
   provisionGCR = (id: number, callback?: any) => {
-    console.log('Provisioning GCR')
+    console.log("Provisioning GCR");
     let { currentProject } = this.context;
     let { currentProject } = this.context;
     let { handleError } = this.props;
     let { handleError } = this.props;
 
 
-    api.createGCR('<token>', {
-      gcp_integration_id: id,
-    }, { project_id: currentProject.id }, (err: any, res: any) => {
-      if (err) {
-        console.log(err);
-        handleError();
-        return;
+    api.createGCR(
+      "<token>",
+      {
+        gcp_integration_id: id,
+      },
+      { project_id: currentProject.id },
+      (err: any, res: any) => {
+        if (err) {
+          console.log(err);
+          handleError();
+          return;
+        }
+        callback && callback();
       }
       }
-      callback && callback();
-    });
-  }
+    );
+  };
 
 
   provisionGKE = (id: number) => {
   provisionGKE = (id: number) => {
-    console.log('Provisioning GKE');
+    console.log("Provisioning GKE");
     let { handleError } = this.props;
     let { handleError } = this.props;
     let { currentProject } = this.context;
     let { currentProject } = this.context;
 
 
-    let clusterName = `${currentProject.name}-cluster`
-    api.createGKE('<token>', {
-      gke_name: clusterName,
-      gcp_integration_id: id,
-    }, { project_id: currentProject.id }, (err: any, res: any) => {
-      if (err) {
-        console.log(err);
-        handleError();
-        return;
+    let clusterName = `${currentProject.name}-cluster`;
+    api.createGKE(
+      "<token>",
+      {
+        gke_name: clusterName,
+        gcp_integration_id: id,
+      },
+      { project_id: currentProject.id },
+      (err: any, res: any) => {
+        if (err) {
+          console.log(err);
+          handleError();
+          return;
+        }
+        this.props.history.push("provisioner");
       }
       }
-      this.props.history.push("provisioner");
-    })
-  }
+    );
+  };
 
 
   handleCreateFlow = () => {
   handleCreateFlow = () => {
     let { selectedInfras, gcpKeyData, gcpProjectId, gcpRegion } = this.state;
     let { selectedInfras, gcpKeyData, gcpProjectId, gcpRegion } = this.state;
     let { currentProject } = this.context;
     let { currentProject } = this.context;
-    api.createGCPIntegration('<token>', {
-      gcp_region: gcpRegion,
-      gcp_key_data: gcpKeyData,
-      gcp_project_id: gcpProjectId,
-    }, { project_id: currentProject.id }, (err: any, res: any) => {
-      if (err) {
-        console.log(err);
-      } else if (res?.data) {
-        console.log('gcp provisioned with response: ', res.data);
-        let { id } = res.data;
-
-        if (selectedInfras.length === 2) {
-          // Case: project exists, provision GCR + GKE
-          this.provisionGCR(id, () => this.provisionGKE(id));
-        } else if (selectedInfras[0].value === 'gcr') {
-          // Case: project exists, only provision GCR
-          this.provisionGCR(id, () => this.props.history.push("provisioner"));
-        } else {
-          // Case: project exists, only provision GKE
-          this.provisionGKE(id);
+    api.createGCPIntegration(
+      "<token>",
+      {
+        gcp_region: gcpRegion,
+        gcp_key_data: gcpKeyData,
+        gcp_project_id: gcpProjectId,
+      },
+      { project_id: currentProject.id },
+      (err: any, res: any) => {
+        if (err) {
+          console.log(err);
+        } else if (res?.data) {
+          console.log("gcp provisioned with response: ", res.data);
+          let { id } = res.data;
+
+          if (selectedInfras.length === 2) {
+            // Case: project exists, provision GCR + GKE
+            this.provisionGCR(id, () => this.provisionGKE(id));
+          } else if (selectedInfras[0].value === "gcr") {
+            // Case: project exists, only provision GCR
+            this.provisionGCR(id, () => this.props.history.push("provisioner"));
+          } else {
+            // Case: project exists, only provision GKE
+            this.provisionGKE(id);
+          }
         }
         }
       }
       }
-    });
-  }
+    );
+  };
 
 
   // TODO: handle generically (with > 2 steps)
   // TODO: handle generically (with > 2 steps)
   onCreateGCP = () => {
   onCreateGCP = () => {
@@ -225,16 +237,11 @@ class GCPFormSection extends Component<PropsType, StateType> {
     } else {
     } else {
       this.createProject(this.handleCreateFlow);
       this.createProject(this.handleCreateFlow);
     }
     }
-  }
+  };
 
 
   render() {
   render() {
     let { setSelectedProvisioner } = this.props;
     let { setSelectedProvisioner } = this.props;
-    let {
-      gcpRegion,
-      gcpProjectId,
-      gcpKeyData,
-      selectedInfras,
-    } = this.state;
+    let { gcpRegion, gcpProjectId, gcpKeyData, selectedInfras } = this.state;
 
 
     return (
     return (
       <StyledGCPFormSection>
       <StyledGCPFormSection>
@@ -244,38 +251,38 @@ class GCPFormSection extends Component<PropsType, StateType> {
           </CloseButton>
           </CloseButton>
           <Heading isAtTop={true}>
           <Heading isAtTop={true}>
             GCP Credentials
             GCP Credentials
-            <GuideButton 
-              href='https://docs.getporter.dev/docs/getting-started-on-gcp'
-              target='_blank'
+            <GuideButton
+              href="https://docs.getporter.dev/docs/getting-started-on-gcp"
+              target="_blank"
             >
             >
-              <i className="material-icons-outlined">help</i> 
+              <i className="material-icons-outlined">help</i>
               Guide
               Guide
             </GuideButton>
             </GuideButton>
           </Heading>
           </Heading>
           <SelectRow
           <SelectRow
             options={regionOptions}
             options={regionOptions}
-            width='100%'
+            width="100%"
             value={gcpRegion}
             value={gcpRegion}
-            dropdownMaxHeight='240px'
+            dropdownMaxHeight="240px"
             setActiveValue={(x: string) => this.setState({ gcpRegion: x })}
             setActiveValue={(x: string) => this.setState({ gcpRegion: x })}
-            label='📍 GCP Region'
+            label="📍 GCP Region"
           />
           />
           <InputRow
           <InputRow
-            type='text'
+            type="text"
             value={gcpProjectId}
             value={gcpProjectId}
             setValue={(x: string) => this.setState({ gcpProjectId: x })}
             setValue={(x: string) => this.setState({ gcpProjectId: x })}
-            label='🏷️ GCP Project ID'
-            placeholder='ex: blindfold-ceiling-24601'
-            width='100%'
+            label="🏷️ GCP Project ID"
+            placeholder="ex: blindfold-ceiling-24601"
+            width="100%"
             isRequired={true}
             isRequired={true}
           />
           />
           <InputRow
           <InputRow
-            type='password'
+            type="password"
             value={gcpKeyData}
             value={gcpKeyData}
             setValue={(x: string) => this.setState({ gcpKeyData: x })}
             setValue={(x: string) => this.setState({ gcpKeyData: x })}
-            label='🔒 GCP Key Data (JSON)'
-            placeholder='○ ○ ○ ○ ○ ○ ○ ○ ○'
-            width='100%'
+            label="🔒 GCP Key Data (JSON)"
+            placeholder="○ ○ ○ ○ ○ ○ ○ ○ ○"
+            width="100%"
             isRequired={true}
             isRequired={true}
           />
           />
           <Br />
           <Br />
@@ -284,18 +291,18 @@ class GCPFormSection extends Component<PropsType, StateType> {
           <CheckboxList
           <CheckboxList
             options={provisionOptions}
             options={provisionOptions}
             selected={selectedInfras}
             selected={selectedInfras}
-            setSelected={(x: { value: string, label: string }[]) => {
+            setSelected={(x: { value: string; label: string }[]) => {
               this.setState({ selectedInfras: x });
               this.setState({ selectedInfras: x });
             }}
             }}
           />
           />
         </FormSection>
         </FormSection>
         {this.props.children ? this.props.children : <Padding />}
         {this.props.children ? this.props.children : <Padding />}
         <SaveButton
         <SaveButton
-          text='Submit'
+          text="Submit"
           disabled={this.checkFormDisabled()}
           disabled={this.checkFormDisabled()}
           onClick={this.onCreateGCP}
           onClick={this.onCreateGCP}
           makeFlush={true}
           makeFlush={true}
-          helper='Note: Provisioning can take up to 15 minutes'
+          helper="Note: Provisioning can take up to 15 minutes"
         />
         />
       </StyledGCPFormSection>
       </StyledGCPFormSection>
     );
     );
@@ -382,4 +389,4 @@ const GuideButton = styled.a`
 const CloseButtonImg = styled.img`
 const CloseButtonImg = styled.img`
   width: 14px;
   width: 14px;
   margin: 0 auto;
   margin: 0 auto;
-`;
+`;

+ 21 - 19
dashboard/src/main/home/provisioner/InfraStatuses.tsx

@@ -1,30 +1,32 @@
-import React, { Component } from 'react';
-import styled from 'styled-components';
+import React, { Component } from "react";
+import styled from "styled-components";
 
 
-import loadingDots from 'assets/loading-dots.gif';
-import { InfraType } from 'shared/types';
-import { infraNames } from 'shared/common';
+import loadingDots from "assets/loading-dots.gif";
+import { InfraType } from "shared/types";
+import { infraNames } from "shared/common";
 
 
 type PropsType = {
 type PropsType = {
-  infras: InfraType[],
+  infras: InfraType[];
 };
 };
 
 
-type StateType = {
-};
+type StateType = {};
 
 
 export default class InfraStatuses extends Component<PropsType, StateType> {
 export default class InfraStatuses extends Component<PropsType, StateType> {
-  state = {
-  }
+  state = {};
 
 
   renderStatusIcon = (status: string) => {
   renderStatusIcon = (status: string) => {
-    if (status === 'created') {
+    if (status === "created") {
       return <StatusIcon>✓</StatusIcon>;
       return <StatusIcon>✓</StatusIcon>;
-    } else if (status === 'creating') {
-      return <StatusIcon><img src={loadingDots} /></StatusIcon>
-    } else if (status === 'error') {
-      return <StatusIcon color='#e3366d'>✗</StatusIcon>
+    } else if (status === "creating") {
+      return (
+        <StatusIcon>
+          <img src={loadingDots} />
+        </StatusIcon>
+      );
+    } else if (status === "error") {
+      return <StatusIcon color="#e3366d">✗</StatusIcon>;
     }
     }
-  }
+  };
 
 
   render() {
   render() {
     return (
     return (
@@ -35,7 +37,7 @@ export default class InfraStatuses extends Component<PropsType, StateType> {
               {this.renderStatusIcon(infra.status)}
               {this.renderStatusIcon(infra.status)}
               {infraNames[infra.kind]}
               {infraNames[infra.kind]}
             </InfraRow>
             </InfraRow>
-          )
+          );
         })}
         })}
       </StyledInfraStatuses>
       </StyledInfraStatuses>
     );
     );
@@ -48,7 +50,7 @@ const StatusIcon = styled.div<{ color?: string }>`
   justify-content: center;
   justify-content: center;
   width: 20px;
   width: 20px;
   font-size: 16px;
   font-size: 16px;
-  color: ${props => props.color ? props.color : '#68c49c'};
+  color: ${(props) => (props.color ? props.color : "#68c49c")};
   margin-right: 10px;
   margin-right: 10px;
 `;
 `;
 
 
@@ -66,4 +68,4 @@ const InfraRow = styled.div`
 const StyledInfraStatuses = styled.div`
 const StyledInfraStatuses = styled.div`
   margin-top: 20px;
   margin-top: 20px;
   margin-bottom: 0;
   margin-bottom: 0;
-`;
+`;

+ 83 - 81
dashboard/src/main/home/provisioner/ProvisionerSettings.tsx

@@ -1,44 +1,46 @@
-import React, { Component } from 'react';
-import styled from 'styled-components';
+import React, { Component } from "react";
+import styled from "styled-components";
 
 
-import { Context } from 'shared/Context';
-import { integrationList } from 'shared/common';
-import { InfraType } from 'shared/types';
+import { Context } from "shared/Context";
+import { integrationList } from "shared/common";
+import { InfraType } from "shared/types";
 
 
-import Helper from 'components/values-form/Helper';
-import AWSFormSection from './AWSFormSection';
-import GCPFormSection from './GCPFormSection';
-import DOFormSection from './DOFormSection';
-import SaveButton from 'components/SaveButton';
-import ExistingClusterSection from './ExistingClusterSection';
-import { Redirect, RouteComponentProps, withRouter } from 'react-router';
+import Helper from "components/values-form/Helper";
+import AWSFormSection from "./AWSFormSection";
+import GCPFormSection from "./GCPFormSection";
+import DOFormSection from "./DOFormSection";
+import SaveButton from "components/SaveButton";
+import ExistingClusterSection from "./ExistingClusterSection";
+import { RouteComponentProps, withRouter } from "react-router";
 
 
 type PropsType = RouteComponentProps & {
 type PropsType = RouteComponentProps & {
-  isInNewProject?: boolean,
-  projectName?: string,
-  infras?: InfraType[],
+  isInNewProject?: boolean;
+  projectName?: string;
+  infras?: InfraType[];
 };
 };
 
 
 type StateType = {
 type StateType = {
-  selectedProvider: string | null,
-  infras: InfraType[],
+  selectedProvider: string | null;
+  infras: InfraType[];
 };
 };
 
 
-const providers = ['aws', 'gcp', 'do',];
+const providers = ["aws", "gcp", "do"];
 
 
 class NewProject extends Component<PropsType, StateType> {
 class NewProject extends Component<PropsType, StateType> {
   state = {
   state = {
     selectedProvider: null as string | null,
     selectedProvider: null as string | null,
     infras: [] as InfraType[],
     infras: [] as InfraType[],
-  }
+  };
 
 
   // Handle any submission (pre-status) error
   // Handle any submission (pre-status) error
   handleError = () => {
   handleError = () => {
     let { setCurrentError } = this.context;
     let { setCurrentError } = this.context;
     this.setState({ selectedProvider: null });
     this.setState({ selectedProvider: null });
-    setCurrentError('Provisioning failed. Check your credentials and try again.');
+    setCurrentError(
+      "Provisioning failed. Check your credentials and try again."
+    );
     this.props.history.push("dashboard");
     this.props.history.push("dashboard");
-  }
+  };
 
 
   renderSelectedProvider = () => {
   renderSelectedProvider = () => {
     let { selectedProvider } = this.state;
     let { selectedProvider } = this.state;
@@ -47,39 +49,39 @@ class NewProject extends Component<PropsType, StateType> {
     let renderSkipHelper = () => {
     let renderSkipHelper = () => {
       return (
       return (
         <>
         <>
-          {selectedProvider === 'skipped' 
-            ? (
+          {selectedProvider === "skipped" ? (
+            <Helper>
+              Don't have a Kubernetes cluster?
+              <Highlight
+                onClick={() => this.setState({ selectedProvider: null })}
+              >
+                Provision through Porter
+              </Highlight>
+            </Helper>
+          ) : (
+            <PositionWrapper selectedProvider={selectedProvider}>
               <Helper>
               <Helper>
-                Don't have a Kubernetes cluster?
-                <Highlight 
-                  onClick={() => this.setState({ selectedProvider: null })}
+                Already have a Kubernetes cluster?
+                <Highlight
+                  onClick={() =>
+                    this.setState({
+                      selectedProvider: "skipped",
+                    })
+                  }
                 >
                 >
-                  Provision through Porter
+                  Skip
                 </Highlight>
                 </Highlight>
               </Helper>
               </Helper>
-            ) : (
-              <PositionWrapper selectedProvider={selectedProvider}>
-                <Helper>
-                  Already have a Kubernetes cluster? 
-                  <Highlight 
-                    onClick={() => this.setState({ 
-                      selectedProvider: 'skipped' 
-                    })}
-                  >
-                    Skip
-                  </Highlight>
-                </Helper>
-              </PositionWrapper>
-            )
-          }
+            </PositionWrapper>
+          )}
         </>
         </>
       );
       );
-    }
+    };
 
 
     switch (selectedProvider) {
     switch (selectedProvider) {
-      case 'aws':
+      case "aws":
         return (
         return (
-          <AWSFormSection 
+          <AWSFormSection
             handleError={this.handleError}
             handleError={this.handleError}
             projectName={projectName}
             projectName={projectName}
             infras={infras}
             infras={infras}
@@ -90,9 +92,9 @@ class NewProject extends Component<PropsType, StateType> {
             {renderSkipHelper()}
             {renderSkipHelper()}
           </AWSFormSection>
           </AWSFormSection>
         );
         );
-      case 'gcp':
+      case "gcp":
         return (
         return (
-          <GCPFormSection 
+          <GCPFormSection
             handleError={this.handleError}
             handleError={this.handleError}
             projectName={projectName}
             projectName={projectName}
             infras={infras}
             infras={infras}
@@ -103,9 +105,9 @@ class NewProject extends Component<PropsType, StateType> {
             {renderSkipHelper()}
             {renderSkipHelper()}
           </GCPFormSection>
           </GCPFormSection>
         );
         );
-      case 'do':
+      case "do":
         return (
         return (
-          <DOFormSection 
+          <DOFormSection
             handleError={this.handleError}
             handleError={this.handleError}
             projectName={projectName}
             projectName={projectName}
             infras={infras}
             infras={infras}
@@ -113,28 +115,29 @@ class NewProject extends Component<PropsType, StateType> {
               this.setState({ selectedProvider: x });
               this.setState({ selectedProvider: x });
             }}
             }}
           />
           />
-        )
+        );
       default:
       default:
         return (
         return (
-          <ExistingClusterSection 
-            projectName={projectName}
-          >
+          <ExistingClusterSection projectName={projectName}>
             {renderSkipHelper()}
             {renderSkipHelper()}
           </ExistingClusterSection>
           </ExistingClusterSection>
         );
         );
     }
     }
-  }
-  
+  };
+
   render() {
   render() {
     let { selectedProvider } = this.state;
     let { selectedProvider } = this.state;
     let { isInNewProject } = this.props;
     let { isInNewProject } = this.props;
     return (
     return (
       <StyledProvisionerSettings>
       <StyledProvisionerSettings>
         <Helper>
         <Helper>
-          {isInNewProject 
-            ? <>Select your hosting backend:<Required>*</Required></>
-            : 'Need a cluster? Provision through Porter:'
-          }
+          {isInNewProject ? (
+            <>
+              Select your hosting backend:<Required>*</Required>
+            </>
+          ) : (
+            "Need a cluster? Provision through Porter:"
+          )}
         </Helper>
         </Helper>
         {!selectedProvider ? (
         {!selectedProvider ? (
           <BlockList>
           <BlockList>
@@ -142,18 +145,14 @@ class NewProject extends Component<PropsType, StateType> {
               let providerInfo = integrationList[provider];
               let providerInfo = integrationList[provider];
               return (
               return (
                 <Block
                 <Block
-                  key={i} 
+                  key={i}
                   onClick={() => {
                   onClick={() => {
                     this.setState({ selectedProvider: provider });
                     this.setState({ selectedProvider: provider });
                   }}
                   }}
                 >
                 >
                   <Icon src={providerInfo.icon} />
                   <Icon src={providerInfo.icon} />
-                  <BlockTitle>
-                    {providerInfo.label}
-                  </BlockTitle>
-                  <BlockDescription>
-                    Hosted in your own cloud.
-                  </BlockDescription>
+                  <BlockTitle>{providerInfo.label}</BlockTitle>
+                  <BlockDescription>Hosted in your own cloud.</BlockDescription>
                 </Block>
                 </Block>
               );
               );
             })}
             })}
@@ -161,23 +160,23 @@ class NewProject extends Component<PropsType, StateType> {
         ) : (
         ) : (
           <>{this.renderSelectedProvider()}</>
           <>{this.renderSelectedProvider()}</>
         )}
         )}
-        {(isInNewProject && !selectedProvider) && (
+        {isInNewProject && !selectedProvider && (
           <>
           <>
             <Helper>
             <Helper>
-              Already have a Kubernetes cluster? 
-              <Highlight 
-                onClick={() => this.setState({ selectedProvider: 'skipped' })}
+              Already have a Kubernetes cluster?
+              <Highlight
+                onClick={() => this.setState({ selectedProvider: "skipped" })}
               >
               >
                 Skip
                 Skip
               </Highlight>
               </Highlight>
             </Helper>
             </Helper>
             <Br />
             <Br />
             <SaveButton
             <SaveButton
-              text='Submit'
+              text="Submit"
               disabled={true}
               disabled={true}
               onClick={() => {}}
               onClick={() => {}}
               makeFlush={true}
               makeFlush={true}
-              helper='Note: Provisioning can take up to 15 minutes'
+              helper="Note: Provisioning can take up to 15 minutes"
             />
             />
           </>
           </>
         )}
         )}
@@ -199,8 +198,7 @@ const StyledProvisionerSettings = styled.div`
   position: relative;
   position: relative;
 `;
 `;
 
 
-const PositionWrapper = styled.div<{ selectedProvider: string | null}>`
-`;
+const PositionWrapper = styled.div<{ selectedProvider: string | null }>``;
 
 
 const Highlight = styled.div`
 const Highlight = styled.div`
   margin-left: 5px;
   margin-left: 5px;
@@ -227,7 +225,7 @@ const Icon = styled.img<{ bw?: boolean }>`
   height: 42px;
   height: 42px;
   margin-top: 30px;
   margin-top: 30px;
   margin-bottom: 15px;
   margin-bottom: 15px;
-  filter: ${props => props.bw ? 'grayscale(1)' : ''};
+  filter: ${(props) => (props.bw ? "grayscale(1)" : "")};
 `;
 `;
 
 
 const BlockDescription = styled.div`
 const BlockDescription = styled.div`
@@ -242,7 +240,7 @@ const BlockDescription = styled.div`
   display: -webkit-box;
   display: -webkit-box;
   overflow: hidden;
   overflow: hidden;
   -webkit-line-clamp: 2;
   -webkit-line-clamp: 2;
-  -webkit-box-orient: vertical;  
+  -webkit-box-orient: vertical;
 `;
 `;
 
 
 const BlockTitle = styled.div`
 const BlockTitle = styled.div`
@@ -268,18 +266,22 @@ const Block = styled.div<{ disabled?: boolean }>`
   align-item: center;
   align-item: center;
   justify-content: space-between;
   justify-content: space-between;
   height: 170px;
   height: 170px;
-  cursor: ${props => props.disabled ? '' : 'pointer'};
+  cursor: ${(props) => (props.disabled ? "" : "pointer")};
   color: #ffffff;
   color: #ffffff;
   position: relative;
   position: relative;
   background: #26282f;
   background: #26282f;
   box-shadow: 0 3px 5px 0px #00000022;
   box-shadow: 0 3px 5px 0px #00000022;
   :hover {
   :hover {
-    background: ${props => props.disabled ? '' : '#ffffff11'};
+    background: ${(props) => (props.disabled ? "" : "#ffffff11")};
   }
   }
 
 
   animation: fadeIn 0.3s 0s;
   animation: fadeIn 0.3s 0s;
   @keyframes fadeIn {
   @keyframes fadeIn {
-    from { opacity: 0 }
-    to { opacity: 1 }
+    from {
+      opacity: 0;
+    }
+    to {
+      opacity: 1;
+    }
   }
   }
-`;
+`;

+ 203 - 150
dashboard/src/main/home/provisioner/ProvisionerStatus.tsx

@@ -1,102 +1,121 @@
-import React, { Component } from 'react';
-import styled from 'styled-components';
-import posthog from 'posthog-js';
-
-import api from 'shared/api';
-import { Context } from 'shared/Context';
-import ansiparse from 'shared/ansiparser'
-import loading from 'assets/loading.gif';
-import warning from 'assets/warning.png';
-import { InfraType } from 'shared/types';
-import { filterOldInfras } from 'shared/common';
-
-import Helper from 'components/values-form/Helper';
-import InfraStatuses from './InfraStatuses';
+import React, { Component } from "react";
+import styled from "styled-components";
+import posthog from "posthog-js";
+
+import api from "shared/api";
+import { Context } from "shared/Context";
+import ansiparse from "shared/ansiparser";
+import loading from "assets/loading.gif";
+import warning from "assets/warning.png";
+import { InfraType } from "shared/types";
+import { filterOldInfras } from "shared/common";
+
+import Helper from "components/values-form/Helper";
+import InfraStatuses from "./InfraStatuses";
 import { RouteComponentProps, withRouter } from "react-router";
 import { RouteComponentProps, withRouter } from "react-router";
 import { Link } from "react-router-dom";
 import { Link } from "react-router-dom";
 
 
 type PropsType = RouteComponentProps & {};
 type PropsType = RouteComponentProps & {};
 
 
 type StateType = {
 type StateType = {
-  error: boolean,
-  logs: string[],
-  websockets: any[],
-  maxStep : Record<string, number>,
-  currentStep: Record<string, number>,
-  triggerEnd: boolean,
-  infras: InfraType[],
+  error: boolean;
+  logs: string[];
+  websockets: any[];
+  maxStep: Record<string, number>;
+  currentStep: Record<string, number>;
+  triggerEnd: boolean;
+  infras: InfraType[];
 };
 };
 
 
 const dummyInfras = [
 const dummyInfras = [
-  { kind: 'ecr', status: 'creating', id: 5, project_id: 1 }, 
-  { kind: 'eks', status: 'error', id: 3, project_id: 1 },
-  { kind: 'eks', status: 'error', id: 1, project_id: 1 },
-  { kind: 'eks', status: 'error', id: 4, project_id: 1 },
-  { kind: 'ecr', status: 'created', id: 2, project_id: 1 },
+  { kind: "ecr", status: "creating", id: 5, project_id: 1 },
+  { kind: "eks", status: "error", id: 3, project_id: 1 },
+  { kind: "eks", status: "error", id: 1, project_id: 1 },
+  { kind: "eks", status: "error", id: 4, project_id: 1 },
+  { kind: "ecr", status: "created", id: 2, project_id: 1 },
 ];
 ];
 
 
 class ProvisionerStatus extends Component<PropsType, StateType> {
 class ProvisionerStatus extends Component<PropsType, StateType> {
   state = {
   state = {
     error: false,
     error: false,
     logs: [] as string[],
     logs: [] as string[],
-    websockets : [] as any[],
+    websockets: [] as any[],
     maxStep: {} as Record<string, any>,
     maxStep: {} as Record<string, any>,
     currentStep: {} as Record<string, number>,
     currentStep: {} as Record<string, number>,
     triggerEnd: false,
     triggerEnd: false,
     infras: [] as InfraType[],
     infras: [] as InfraType[],
-  }
+  };
 
 
-  parentRef = React.createRef<HTMLDivElement>()
+  parentRef = React.createRef<HTMLDivElement>();
 
 
   scrollToBottom = (smooth: boolean) => {
   scrollToBottom = (smooth: boolean) => {
     if (smooth) {
     if (smooth) {
-      this.parentRef.current.lastElementChild.scrollIntoView({ behavior: "smooth" })
+      this.parentRef.current.lastElementChild.scrollIntoView({
+        behavior: "smooth",
+      });
     } else {
     } else {
-      this.parentRef.current.lastElementChild.scrollIntoView({ behavior: "auto" })
+      this.parentRef.current.lastElementChild.scrollIntoView({
+        behavior: "auto",
+      });
     }
     }
-  }
+  };
 
 
   componentDidMount() {
   componentDidMount() {
-    console.log('mounting provisioner')
+    console.log("mounting provisioner");
     let { currentProject } = this.context;
     let { currentProject } = this.context;
-    let protocol = process.env.NODE_ENV == 'production' ? 'wss' : 'ws'
+    let protocol = process.env.NODE_ENV == "production" ? "wss" : "ws";
 
 
     // Check if current project is provisioning
     // Check if current project is provisioning
-    api.getInfra('<token>', {}, { 
-      project_id: currentProject.id 
-    }, (err: any, res: any) => {
-      if (err) {
-        console.log(err);
-      } 
-      
-      let infras = filterOldInfras(res.data);
-      let error = false;
-
-      let maxStep = {} as Record<string, number>
-
-      infras.forEach((infra: InfraType, i: number) => {
-        maxStep[infra.kind] = null;
-        if (infra.status === 'error') {
-          error = true;
+    api.getInfra(
+      "<token>",
+      {},
+      {
+        project_id: currentProject.id,
+      },
+      (err: any, res: any) => {
+        if (err) {
+          console.log(err);
         }
         }
-      });
 
 
-      // Filter historical infras list for most current instances of each
-      let websockets = infras.map((infra: any) => {
-        let ws = new WebSocket(`${protocol}://${process.env.API_SERVER}/api/projects/${currentProject.id}/provision/${infra.kind}/${infra.id}/logs`)
-        return this.setupWebsocket(ws, infra)
-      });
-  
-      this.setState({ error, infras, websockets, maxStep, logs: ["Provisioning resources..."] });
-    });
+        let infras = filterOldInfras(res.data);
+        let error = false;
+
+        let maxStep = {} as Record<string, number>;
+
+        infras.forEach((infra: InfraType, i: number) => {
+          maxStep[infra.kind] = null;
+          if (infra.status === "error") {
+            error = true;
+          }
+        });
+
+        // Filter historical infras list for most current instances of each
+        let websockets = infras.map((infra: any) => {
+          let ws = new WebSocket(
+            `${protocol}://${process.env.API_SERVER}/api/projects/${currentProject.id}/provision/${infra.kind}/${infra.id}/logs`
+          );
+          return this.setupWebsocket(ws, infra);
+        });
+
+        this.setState({
+          error,
+          infras,
+          websockets,
+          maxStep,
+          logs: ["Provisioning resources..."],
+        });
+      }
+    );
   }
   }
 
 
   componentWillUnmount() {
   componentWillUnmount() {
-    if (this.state.websockets.length == 0) { return; }
+    if (this.state.websockets.length == 0) {
+      return;
+    }
 
 
     this.state.websockets.forEach((ws: any) => {
     this.state.websockets.forEach((ws: any) => {
-      ws.close()
-    })
+      ws.close();
+    });
   }
   }
 
 
   isJSON = (str: string) => {
   isJSON = (str: string) => {
@@ -106,21 +125,25 @@ class ProvisionerStatus extends Component<PropsType, StateType> {
       return false;
       return false;
     }
     }
     return true;
     return true;
-  }
+  };
 
 
   setupWebsocket = (ws: WebSocket, infra: any) => {
   setupWebsocket = (ws: WebSocket, infra: any) => {
     ws.onopen = () => {
     ws.onopen = () => {
-      console.log('connected to websocket')
-    }
+      console.log("connected to websocket");
+    };
 
 
     ws.onmessage = (evt: MessageEvent) => {
     ws.onmessage = (evt: MessageEvent) => {
       let event = JSON.parse(evt.data);
       let event = JSON.parse(evt.data);
       let validEvents = [] as any[];
       let validEvents = [] as any[];
       let err = null;
       let err = null;
-      
+
       for (var i = 0; i < event.length; i++) {
       for (var i = 0; i < event.length; i++) {
         let msg = event[i];
         let msg = event[i];
-        if (msg["Values"] && msg["Values"]["data"] && this.isJSON(msg["Values"]["data"])) { 
+        if (
+          msg["Values"] &&
+          msg["Values"]["data"] &&
+          this.isJSON(msg["Values"]["data"])
+        ) {
           let d = JSON.parse(msg["Values"]["data"]);
           let d = JSON.parse(msg["Values"]["data"]);
 
 
           if (d["kind"] == "error") {
           if (d["kind"] == "error") {
@@ -129,24 +152,32 @@ class ProvisionerStatus extends Component<PropsType, StateType> {
           }
           }
 
 
           // add only valid events
           // add only valid events
-          if (d["log"] != null && d["created_resources"] != null && d["total_resources"] != null) {
+          if (
+            d["log"] != null &&
+            d["created_resources"] != null &&
+            d["total_resources"] != null
+          ) {
             validEvents.push(d);
             validEvents.push(d);
           }
           }
         }
         }
       }
       }
 
 
       if (err) {
       if (err) {
-        posthog.capture('Provisioning Error', {error: err});
+        posthog.capture("Provisioning Error", { error: err });
 
 
         let e = ansiparse(err).map((el: any) => {
         let e = ansiparse(err).map((el: any) => {
           return el.text;
           return el.text;
-        })
+        });
 
 
-        let index = this.state.infras.findIndex(el => el.kind === infra.kind)
-        infra.status = "error"
-        let infras = this.state.infras
-        infras[index] = infra
-        this.setState({ logs: [...this.state.logs, ...e], error: true, infras });
+        let index = this.state.infras.findIndex((el) => el.kind === infra.kind);
+        infra.status = "error";
+        let infras = this.state.infras;
+        infras[index] = infra;
+        this.setState({
+          logs: [...this.state.logs, ...e],
+          error: true,
+          infras,
+        });
         return;
         return;
       }
       }
 
 
@@ -154,107 +185,123 @@ class ProvisionerStatus extends Component<PropsType, StateType> {
         return;
         return;
       }
       }
 
 
-      if (!this.state.maxStep[infra.kind] || !this.state.maxStep[infra.kind]["total_resources"]) {
+      if (
+        !this.state.maxStep[infra.kind] ||
+        !this.state.maxStep[infra.kind]["total_resources"]
+      ) {
         this.setState({
         this.setState({
           maxStep: {
           maxStep: {
             ...this.state.maxStep,
             ...this.state.maxStep,
-            [infra.kind] : validEvents[validEvents.length - 1]["total_resources"]
-          }
-        })
+            [infra.kind]:
+              validEvents[validEvents.length - 1]["total_resources"],
+          },
+        });
       }
       }
-      
-      let logs = [] as any[]
+
+      let logs = [] as any[];
       validEvents.forEach((e: any) => {
       validEvents.forEach((e: any) => {
-        logs.push(...ansiparse(e["log"]))
-      })
+        logs.push(...ansiparse(e["log"]));
+      });
 
 
       logs = logs.map((log: any) => {
       logs = logs.map((log: any) => {
-        return log.text
-      })
-
-      this.setState({ 
-        logs: [...this.state.logs, ...logs], 
-        currentStep: {
-          ...this.state.currentStep,
-          [infra.kind] : validEvents[validEvents.length - 1]["created_resources"]
+        return log.text;
+      });
+
+      this.setState(
+        {
+          logs: [...this.state.logs, ...logs],
+          currentStep: {
+            ...this.state.currentStep,
+            [infra.kind]:
+              validEvents[validEvents.length - 1]["created_resources"],
+          },
         },
         },
-      }, () => {
-        this.scrollToBottom(false)
-      })
-    }
+        () => {
+          this.scrollToBottom(false);
+        }
+      );
+    };
 
 
     ws.onerror = (err: ErrorEvent) => {
     ws.onerror = (err: ErrorEvent) => {
-      console.log('websocket err', err)
-    }
+      console.log("websocket err", err);
+    };
 
 
     ws.onclose = () => {
     ws.onclose = () => {
-      console.log('closing provisioner websocket')
-    }
+      console.log("closing provisioner websocket");
+    };
 
 
-    return ws
-  }
+    return ws;
+  };
 
 
   renderLogs = () => {
   renderLogs = () => {
     return this.state.logs.map((log, i) => {
     return this.state.logs.map((log, i) => {
       return <Log key={i}>{log}</Log>;
       return <Log key={i}>{log}</Log>;
     });
     });
-  }
+  };
 
 
   onEnd = () => {
   onEnd = () => {
     let myInterval = setInterval(() => {
     let myInterval = setInterval(() => {
-      api.getClusters('<token>', {}, { 
-        id: this.context.currentProject.id 
-      }, (err: any, res: any) => {
-        if (err) {
-          console.log(err);
-        } else if (res.data) {
-          let clusters = res.data;
-          if (clusters.length > 0) {
-            this.props.history.push("dashboard");
-            // console.log('provision end project: ', this.context.currentProject);
-            // console.log('provision end cluster: ', this.context.currentCluster);
-            clearInterval(myInterval);
-          } else {
-            // console.log('looped!')
-            // console.log('response :', res.data);
-            // console.log('provision end project: ', this.context.currentProject);
-            // console.log('provision end cluster: ', this.context.currentCluster);
+      api.getClusters(
+        "<token>",
+        {},
+        {
+          id: this.context.currentProject.id,
+        },
+        (err: any, res: any) => {
+          if (err) {
+            console.log(err);
+          } else if (res.data) {
+            let clusters = res.data;
+            if (clusters.length > 0) {
+              this.props.history.push("dashboard");
+              // console.log('provision end project: ', this.context.currentProject);
+              // console.log('provision end cluster: ', this.context.currentCluster);
+              clearInterval(myInterval);
+            } else {
+              // console.log('looped!')
+              // console.log('response :', res.data);
+              // console.log('provision end project: ', this.context.currentProject);
+              // console.log('provision end cluster: ', this.context.currentCluster);
+            }
           }
           }
         }
         }
-      });
+      );
     }, 1000);
     }, 1000);
-  }
+  };
 
 
   refreshLogs = () => {
   refreshLogs = () => {
-    if (this.state.websockets.length == 0) { return; }
+    if (this.state.websockets.length == 0) {
+      return;
+    }
     let { currentProject } = this.context;
     let { currentProject } = this.context;
-    let protocol = process.env.NODE_ENV == 'production' ? 'wss' : 'ws'
+    let protocol = process.env.NODE_ENV == "production" ? "wss" : "ws";
 
 
     this.state.websockets.forEach((ws: any) => {
     this.state.websockets.forEach((ws: any) => {
-      ws.close()
-    })
+      ws.close();
+    });
 
 
-    this.setState({ 
+    this.setState({
       websockets: [],
       websockets: [],
-      logs: []
-    })
+      logs: [],
+    });
 
 
     let websockets = this.state.infras.map((infra: any) => {
     let websockets = this.state.infras.map((infra: any) => {
-      let ws = new WebSocket(`${protocol}://${process.env.API_SERVER}/api/projects/${currentProject.id}/provision/${infra.kind}/${infra.infra_id}/logs`)
-      return this.setupWebsocket(ws, infra)
+      let ws = new WebSocket(
+        `${protocol}://${process.env.API_SERVER}/api/projects/${currentProject.id}/provision/${infra.kind}/${infra.infra_id}/logs`
+      );
+      return this.setupWebsocket(ws, infra);
     });
     });
 
 
     this.setState({ websockets, logs: ["Provisioning resources..."] });
     this.setState({ websockets, logs: ["Provisioning resources..."] });
-    
-  }
-  
+  };
+
   render() {
   render() {
     let { error, triggerEnd, infras } = this.state;
     let { error, triggerEnd, infras } = this.state;
-    
+
     let maxStep = 0;
     let maxStep = 0;
     let currentStep = 0;
     let currentStep = 0;
     let skip = false;
     let skip = false;
-    
+
     for (let i = 0; i < infras.length; i++) {
     for (let i = 0; i < infras.length; i++) {
       if (!this.state.maxStep[infras[i].kind]) {
       if (!this.state.maxStep[infras[i].kind]) {
         skip = true;
         skip = true;
@@ -263,14 +310,14 @@ class ProvisionerStatus extends Component<PropsType, StateType> {
 
 
     if (!skip) {
     if (!skip) {
       for (let key in this.state.maxStep) {
       for (let key in this.state.maxStep) {
-        maxStep += this.state.maxStep[key]
-        currentStep += this.state.currentStep[key]
-      }  
+        maxStep += this.state.maxStep[key];
+        currentStep += this.state.currentStep[key];
+      }
     }
     }
 
 
     if (maxStep !== 0 && currentStep === maxStep && !triggerEnd) {
     if (maxStep !== 0 && currentStep === maxStep && !triggerEnd) {
-      posthog.capture('Provisioning complete!')
-      this.onEnd()
+      posthog.capture("Provisioning complete!");
+      this.onEnd();
       this.setState({ triggerEnd: true });
       this.setState({ triggerEnd: true });
     }
     }
 
 
@@ -339,7 +386,7 @@ const Options = styled.div`
   flex-direction: row;
   flex-direction: row;
   align-items: center;
   align-items: center;
   justify-content: space-between;
   justify-content: space-between;
-`
+`;
 
 
 const Refresh = styled.div`
 const Refresh = styled.div`
   display: flex;
   display: flex;
@@ -358,7 +405,7 @@ const Refresh = styled.div`
   :hover {
   :hover {
     background: #2468d6;
     background: #2468d6;
   }
   }
-`
+`;
 
 
 // const Link = styled.a`
 // const Link = styled.a`
 //   cursor: pointer;
 //   cursor: pointer;
@@ -367,8 +414,10 @@ const Refresh = styled.div`
 // `;
 // `;
 
 
 const Warning = styled.span`
 const Warning = styled.span`
-  color: ${(props: { highlight: boolean, makeFlush?: boolean }) => props.highlight ? '#f5cb42' : ''};
-  margin-left: ${(props: { highlight: boolean, makeFlush?: boolean }) => props.makeFlush ? '' : '5px'};
+  color: ${(props: { highlight: boolean; makeFlush?: boolean }) =>
+    props.highlight ? "#f5cb42" : ""};
+  margin-left: ${(props: { highlight: boolean; makeFlush?: boolean }) =>
+    props.makeFlush ? "" : "5px"};
   margin-right: 5px;
   margin-right: 5px;
 `;
 `;
 
 
@@ -405,7 +454,7 @@ const Message = styled.div`
 `;
 `;
 
 
 const Loaded = styled.div<{ progress: string }>`
 const Loaded = styled.div<{ progress: string }>`
-  width: ${props => props.progress};
+  width: ${(props) => props.progress};
   height: 100%;
   height: 100%;
   background: linear-gradient(to right, #4f8aff, #8e7dff, #4f8aff);
   background: linear-gradient(to right, #4f8aff, #8e7dff, #4f8aff);
   background-size: 400% 400%;
   background-size: 400% 400%;
@@ -413,8 +462,12 @@ const Loaded = styled.div<{ progress: string }>`
   animation: linkLoad 2s infinite;
   animation: linkLoad 2s infinite;
 
 
   @keyframes linkLoad {
   @keyframes linkLoad {
-    0%{background-position:91% 100%}
-    100%{background-position:10% 0%}
+    0% {
+      background-position: 91% 100%;
+    }
+    100% {
+      background-position: 10% 0%;
+    }
   }
   }
 `;
 `;
 
 
@@ -430,7 +483,7 @@ const LoadingBar = styled.div`
 const Title = styled.div`
 const Title = styled.div`
   font-size: 24px;
   font-size: 24px;
   font-weight: 600;
   font-weight: 600;
-  font-family: 'Work Sans', sans-serif;
+  font-family: "Work Sans", sans-serif;
   color: #ffffff;
   color: #ffffff;
   white-space: nowrap;
   white-space: nowrap;
   overflow: hidden;
   overflow: hidden;
@@ -456,7 +509,7 @@ const TitleSection = styled.div`
       margin-bottom: -2px;
       margin-bottom: -2px;
       font-size: 18px;
       font-size: 18px;
       margin-left: 18px;
       margin-left: 18px;
-      color: #858FAAaa;
+      color: #858faaaa;
       cursor: pointer;
       cursor: pointer;
       :hover {
       :hover {
         color: #aaaabb;
         color: #aaaabb;
@@ -472,4 +525,4 @@ const StyledProvisioner = styled.div`
   position: relative;
   position: relative;
   padding-top: 50px;
   padding-top: 50px;
   margin-top: calc(50vh - 350px);
   margin-top: calc(50vh - 350px);
-`;
+`;

+ 91 - 74
dashboard/src/main/home/sidebar/ClusterSection.tsx

@@ -1,88 +1,93 @@
-import React, { Component } from 'react';
-import styled from 'styled-components';
-import drawerBg from 'assets/drawer-bg.png';
+import React, { Component } from "react";
+import styled from "styled-components";
+import drawerBg from "assets/drawer-bg.png";
 
 
-import api from 'shared/api';
-import { Context } from 'shared/Context';
-import { ClusterType } from 'shared/types';
+import api from "shared/api";
+import { Context } from "shared/Context";
+import { ClusterType } from "shared/types";
 
 
-import Drawer from './Drawer';
-import { RouteComponentProps, withRouter } from 'react-router';
+import Drawer from "./Drawer";
+import { RouteComponentProps, withRouter } from "react-router";
 
 
 type PropsType = RouteComponentProps & {
 type PropsType = RouteComponentProps & {
-  forceCloseDrawer: boolean,
-  releaseDrawer: () => void,
-  setWelcome: (x: boolean) => void,
-  currentView: string,
-  isSelected: boolean,
-  forceRefreshClusters: boolean,
-  setRefreshClusters: (x: boolean) => void,
+  forceCloseDrawer: boolean;
+  releaseDrawer: () => void;
+  setWelcome: (x: boolean) => void;
+  currentView: string;
+  isSelected: boolean;
+  forceRefreshClusters: boolean;
+  setRefreshClusters: (x: boolean) => void;
 };
 };
 
 
 type StateType = {
 type StateType = {
-  showDrawer: boolean,
-  initializedDrawer: boolean,
-  clusters: ClusterType[],
+  showDrawer: boolean;
+  initializedDrawer: boolean;
+  clusters: ClusterType[];
 
 
   // Track last project id for refreshing clusters on project change
   // Track last project id for refreshing clusters on project change
-  prevProjectId: number
+  prevProjectId: number;
 };
 };
 
 
 class ClusterSection extends Component<PropsType, StateType> {
 class ClusterSection extends Component<PropsType, StateType> {
-
   // Need to track initialized for animation mounting
   // Need to track initialized for animation mounting
   state = {
   state = {
     showDrawer: false,
     showDrawer: false,
     initializedDrawer: false,
     initializedDrawer: false,
     clusters: [] as ClusterType[],
     clusters: [] as ClusterType[],
-    prevProjectId: this.context.currentProject.id
+    prevProjectId: this.context.currentProject.id,
   };
   };
 
 
   updateClusters = () => {
   updateClusters = () => {
     let { currentProject, setCurrentCluster } = this.context;
     let { currentProject, setCurrentCluster } = this.context;
 
 
     // TODO: query with selected filter once implemented
     // TODO: query with selected filter once implemented
-    api.getClusters('<token>', {}, { id: currentProject.id }, (err: any, res: any) => {
-      if (err) {
-
-        // Assume intializing if no contexts
-        this.props.setWelcome(true);
-      } else {
-        this.props.setWelcome(false);
-        // TODO: handle uninitialized kubeconfig
-        if (res.data) {
-          let clusters = res.data;
-          clusters.sort((a: any, b: any) => a.id - b.id);
-          if (clusters.length > 0) {
-            this.setState({ clusters });
-            let saved = JSON.parse(localStorage.getItem(currentProject.id + '-cluster'));
-            if (saved !== 'null') {
-              setCurrentCluster(clusters[0]);
-              for (let i = 0; i < clusters.length; i++) {
-                if (
-                  clusters[i].id === saved.id &&
-                  clusters[i].project_id === saved.project_id && 
-                  clusters[i].name === saved.name
-                ) {
-                  setCurrentCluster(clusters[i]);
-                  break;
+    api.getClusters(
+      "<token>",
+      {},
+      { id: currentProject.id },
+      (err: any, res: any) => {
+        if (err) {
+          // Assume intializing if no contexts
+          this.props.setWelcome(true);
+        } else {
+          this.props.setWelcome(false);
+          // TODO: handle uninitialized kubeconfig
+          if (res.data) {
+            let clusters = res.data;
+            clusters.sort((a: any, b: any) => a.id - b.id);
+            if (clusters.length > 0) {
+              this.setState({ clusters });
+              let saved = JSON.parse(
+                localStorage.getItem(currentProject.id + "-cluster")
+              );
+              if (saved !== "null") {
+                setCurrentCluster(clusters[0]);
+                for (let i = 0; i < clusters.length; i++) {
+                  if (
+                    clusters[i].id === saved.id &&
+                    clusters[i].project_id === saved.project_id &&
+                    clusters[i].name === saved.name
+                  ) {
+                    setCurrentCluster(clusters[i]);
+                    break;
+                  }
                 }
                 }
+              } else {
+                setCurrentCluster(clusters[0]);
               }
               }
-            } else {
-              setCurrentCluster(clusters[0]);
+            } else if (
+              this.props.currentView !== "provisioner" &&
+              this.props.currentView !== "new-project"
+            ) {
+              this.setState({ clusters: [] });
+              setCurrentCluster(null);
+              // this.props.history.push("dashboard");
             }
             }
-          } else if (
-            this.props.currentView !== 'provisioner'
-            && this.props.currentView !== 'new-project'
-          ) {
-            this.setState({ clusters: [] });
-            setCurrentCluster(null);
-            // this.props.history.push("dashboard");
           }
           }
         }
         }
       }
       }
-    });
-  }
+    );
+  };
 
 
   componentDidMount() {
   componentDidMount() {
     this.updateClusters();
     this.updateClusters();
@@ -91,8 +96,7 @@ class ClusterSection extends Component<PropsType, StateType> {
   // Need to override showDrawer when the sidebar is closed
   // Need to override showDrawer when the sidebar is closed
   componentDidUpdate(prevProps: PropsType) {
   componentDidUpdate(prevProps: PropsType) {
     if (prevProps !== this.props) {
     if (prevProps !== this.props) {
-
-      // Refresh clusters on project change 
+      // Refresh clusters on project change
       if (this.state.prevProjectId !== this.context.currentProject.id) {
       if (this.state.prevProjectId !== this.context.currentProject.id) {
         this.updateClusters();
         this.updateClusters();
         this.setState({ prevProjectId: this.context.currentProject.id });
         this.setState({ prevProjectId: this.context.currentProject.id });
@@ -107,7 +111,7 @@ class ClusterSection extends Component<PropsType, StateType> {
       }
       }
     }
     }
   }
   }
-  
+
   toggleDrawer = (): void => {
   toggleDrawer = (): void => {
     if (!this.state.initializedDrawer) {
     if (!this.state.initializedDrawer) {
       this.setState({ initializedDrawer: true });
       this.setState({ initializedDrawer: true });
@@ -128,8 +132,10 @@ class ClusterSection extends Component<PropsType, StateType> {
   };
   };
 
 
   showClusterConfigModal = () => {
   showClusterConfigModal = () => {
-    this.context.setCurrentModal('ClusterConfigModal', { updateClusters: this.updateClusters });
-  }
+    this.context.setCurrentModal("ClusterConfigModal", {
+      updateClusters: this.updateClusters,
+    });
+  };
 
 
   renderContents = (): JSX.Element => {
   renderContents = (): JSX.Element => {
     let { clusters, showDrawer } = this.state;
     let { clusters, showDrawer } = this.state;
@@ -138,8 +144,12 @@ class ClusterSection extends Component<PropsType, StateType> {
     if (clusters.length > 0) {
     if (clusters.length > 0) {
       return (
       return (
         <ClusterSelector isSelected={this.props.isSelected}>
         <ClusterSelector isSelected={this.props.isSelected}>
-          <LinkWrapper onClick={() => this.props.history.push('cluster-dashboard')}>
-            <ClusterIcon><i className="material-icons">device_hub</i></ClusterIcon>
+          <LinkWrapper
+            onClick={() => this.props.history.push("cluster-dashboard")}
+          >
+            <ClusterIcon>
+              <i className="material-icons">device_hub</i>
+            </ClusterIcon>
             <ClusterName>{currentCluster && currentCluster.name}</ClusterName>
             <ClusterName>{currentCluster && currentCluster.name}</ClusterName>
           </LinkWrapper>
           </LinkWrapper>
           <DrawerButton onClick={this.toggleDrawer}>
           <DrawerButton onClick={this.toggleDrawer}>
@@ -154,11 +164,13 @@ class ClusterSection extends Component<PropsType, StateType> {
 
 
     return (
     return (
       <InitializeButton
       <InitializeButton
-        onClick={() => this.context.setCurrentModal('ClusterInstructionsModal', {})}
+        onClick={() =>
+          this.context.setCurrentModal("ClusterInstructionsModal", {})
+        }
       >
       >
         <Plus>+</Plus> Connect a Cluster
         <Plus>+</Plus> Connect a Cluster
       </InitializeButton>
       </InitializeButton>
-    )
+    );
   };
   };
 
 
   render() {
   render() {
@@ -203,7 +215,7 @@ const InitializeButton = styled.div`
 
 
 const BgAccent = styled.img`
 const BgAccent = styled.img`
   height: 42px;
   height: 42px;
-  background: #819BFD;
+  background: #819bfd;
   width: 30px;
   width: 30px;
   border-top-left-radius: 100px;
   border-top-left-radius: 100px;
   max-width: 30px;
   max-width: 30px;
@@ -240,14 +252,18 @@ const ClusterName = styled.div`
 
 
 const DropdownIcon = styled.span`
 const DropdownIcon = styled.span`
   position: absolute;
   position: absolute;
-  right: ${(props: { showDrawer: boolean }) => (props.showDrawer ? '-2px' : '2px')};
+  right: ${(props: { showDrawer: boolean }) =>
+    props.showDrawer ? "-2px" : "2px"};
   top: 10px;
   top: 10px;
   > i {
   > i {
     font-size: 18px;
     font-size: 18px;
   }
   }
-  -webkit-transform: ${(props: { showDrawer: boolean }) => (props.showDrawer ? 'rotate(-90deg)' : 'rotate(90deg)')};
-  transform: ${(props: { showDrawer: boolean }) => (props.showDrawer ? 'rotate(-90deg)' : 'rotate(90deg)')};
-  animation: ${(props: { showDrawer: boolean }) => (props.showDrawer ? 'rotateLeft 0.5s' : 'rotateRight 0.5s')};
+  -webkit-transform: ${(props: { showDrawer: boolean }) =>
+    props.showDrawer ? "rotate(-90deg)" : "rotate(90deg)"};
+  transform: ${(props: { showDrawer: boolean }) =>
+    props.showDrawer ? "rotate(-90deg)" : "rotate(90deg)"};
+  animation: ${(props: { showDrawer: boolean }) =>
+    props.showDrawer ? "rotateLeft 0.5s" : "rotateRight 0.5s"};
   animation-fill-mode: forwards;
   animation-fill-mode: forwards;
 
 
   @keyframes rotateLeft {
   @keyframes rotateLeft {
@@ -265,7 +281,6 @@ const DropdownIcon = styled.span`
       transform: rotate(-90deg);
       transform: rotate(-90deg);
     }
     }
   }
   }
-
 `;
 `;
 
 
 const ClusterIcon = styled.div`
 const ClusterIcon = styled.div`
@@ -298,10 +313,12 @@ const ClusterSelector = styled.div`
   font-weight: 500;
   font-weight: 500;
   color: white;
   color: white;
   cursor: pointer;
   cursor: pointer;
-  background: ${(props: { isSelected: boolean }) => props.isSelected ? '#ffffff11' : ''};
+  background: ${(props: { isSelected: boolean }) =>
+    props.isSelected ? "#ffffff11" : ""};
   z-index: 1;
   z-index: 1;
 
 
   :hover {
   :hover {
-    background: ${(props: { isSelected: boolean }) => props.isSelected ? '' : '#ffffff08'};
+    background: ${(props: { isSelected: boolean }) =>
+      props.isSelected ? "" : "#ffffff08"};
   }
   }
-`;
+`;

+ 47 - 29
dashboard/src/main/home/sidebar/Drawer.tsx

@@ -1,29 +1,27 @@
-import React, { Component } from 'react';
-import styled from 'styled-components';
-import close from 'assets/close.png';
+import React, { Component } from "react";
+import styled from "styled-components";
+import close from "assets/close.png";
 
 
-import { Context } from 'shared/Context';
-import { ClusterType } from 'shared/types';
-import { RouteComponentProps, withRouter } from 'react-router';
+import { Context } from "shared/Context";
+import { ClusterType } from "shared/types";
+import { RouteComponentProps, withRouter } from "react-router";
 
 
 type PropsType = RouteComponentProps & {
 type PropsType = RouteComponentProps & {
-  toggleDrawer: () => void,
-  showDrawer: boolean,
-  clusters: ClusterType[],
+  toggleDrawer: () => void;
+  showDrawer: boolean;
+  clusters: ClusterType[];
 };
 };
 
 
-type StateType = {
-};
+type StateType = {};
 
 
 class Drawer extends Component<PropsType, StateType> {
 class Drawer extends Component<PropsType, StateType> {
-
   renderClusterList = (): JSX.Element[] | JSX.Element => {
   renderClusterList = (): JSX.Element[] | JSX.Element => {
     let { clusters } = this.props;
     let { clusters } = this.props;
     let { currentCluster, setCurrentCluster } = this.context;
     let { currentCluster, setCurrentCluster } = this.context;
 
 
     if (clusters.length > 0 && currentCluster) {
     if (clusters.length > 0 && currentCluster) {
       clusters.sort((a, b) => a.id - b.id);
       clusters.sort((a, b) => a.id - b.id);
-      
+
       return clusters.map((cluster: ClusterType, i: number) => {
       return clusters.map((cluster: ClusterType, i: number) => {
         /*
         /*
         let active = this.context.activeProject &&
         let active = this.context.activeProject &&
@@ -34,23 +32,26 @@ class Drawer extends Component<PropsType, StateType> {
           <ClusterOption
           <ClusterOption
             key={i}
             key={i}
             active={cluster.name === currentCluster.name}
             active={cluster.name === currentCluster.name}
-            onClick={() => { setCurrentCluster(cluster); this.props.history.push('cluster-dashboard') }}
+            onClick={() => {
+              setCurrentCluster(cluster);
+              this.props.history.push("cluster-dashboard");
+            }}
           >
           >
-            <ClusterIcon><i className="material-icons">device_hub</i></ClusterIcon>
+            <ClusterIcon>
+              <i className="material-icons">device_hub</i>
+            </ClusterIcon>
             <ClusterName>{cluster.name}</ClusterName>
             <ClusterName>{cluster.name}</ClusterName>
           </ClusterOption>
           </ClusterOption>
         );
         );
       });
       });
     }
     }
 
 
-    return <Placeholder>No clusters selected</Placeholder>
+    return <Placeholder>No clusters selected</Placeholder>;
   };
   };
 
 
   renderCloseOverlay = (): JSX.Element | undefined => {
   renderCloseOverlay = (): JSX.Element | undefined => {
     if (this.props.showDrawer) {
     if (this.props.showDrawer) {
-      return (
-        <CloseOverlay onClick={this.props.toggleDrawer} />
-      );
+      return <CloseOverlay onClick={this.props.toggleDrawer} />;
     }
     }
   };
   };
 
 
@@ -65,9 +66,11 @@ class Drawer extends Component<PropsType, StateType> {
 
 
           {this.renderClusterList()}
           {this.renderClusterList()}
 
 
-          <InitializeButton onClick={() => {
-            this.context.setCurrentModal('ClusterInstructionsModal', {});
-          }}>
+          <InitializeButton
+            onClick={() => {
+              this.context.setCurrentModal("ClusterInstructionsModal", {});
+            }}
+          >
             <Plus>+</Plus> Add a Cluster
             <Plus>+</Plus> Add a Cluster
           </InitializeButton>
           </InitializeButton>
         </StyledDrawer>
         </StyledDrawer>
@@ -128,7 +131,8 @@ const ClusterOption = styled.div`
   overflow: hidden;
   overflow: hidden;
   text-overflow: ellipsis;
   text-overflow: ellipsis;
   cursor: pointer;
   cursor: pointer;
-  background: ${(props: { active?: boolean }) => props.active ? '#ffffff18' : ''};
+  background: ${(props: { active?: boolean }) =>
+    props.active ? "#ffffff18" : ""};
   :hover {
   :hover {
     background: #ffffff22;
     background: #ffffff22;
   }
   }
@@ -194,18 +198,32 @@ const StyledDrawer = styled.div`
   overflow-y: auto;
   overflow-y: auto;
   padding-bottom: 40px;
   padding-bottom: 40px;
   top: 0;
   top: 0;
-  left: ${(props: { showDrawer: boolean }) => (props.showDrawer ? '-30px' : '200px')};
+  left: ${(props: { showDrawer: boolean }) =>
+    props.showDrawer ? "-30px" : "200px"};
   z-index: -2;
   z-index: -2;
   background: #00000fd4;
   background: #00000fd4;
-  animation: ${(props: { showDrawer: boolean }) => (props.showDrawer ? 'slideDrawerRight 0.4s' : 'slideDrawerLeft 0.4s')};
+  animation: ${(props: { showDrawer: boolean }) =>
+    props.showDrawer ? "slideDrawerRight 0.4s" : "slideDrawerLeft 0.4s"};
   animation-fill-mode: forwards;
   animation-fill-mode: forwards;
   @keyframes slideDrawerRight {
   @keyframes slideDrawerRight {
-    from { left: -30px; opacity: 0; }
-    to { left: 200px; opacity: 1; }
+    from {
+      left: -30px;
+      opacity: 0;
+    }
+    to {
+      left: 200px;
+      opacity: 1;
+    }
   }
   }
   @keyframes slideDrawerLeft {
   @keyframes slideDrawerLeft {
-    from { left: 200px; opacity: 1; }
-    to { left: -30px; opacity: 0; }
+    from {
+      left: 200px;
+      opacity: 1;
+    }
+    to {
+      left: -30px;
+      opacity: 0;
+    }
   }
   }
 `;
 `;
 
 

+ 26 - 21
dashboard/src/main/home/sidebar/ProjectSection.tsx

@@ -1,18 +1,18 @@
-import React, { Component } from 'react';
-import styled from 'styled-components';
-import gradient from 'assets/gradient.jpg';
+import React, { Component } from "react";
+import styled from "styled-components";
+import gradient from "assets/gradient.jpg";
 
 
-import { Context } from 'shared/Context';
-import { ProjectType, InfraType } from 'shared/types';
-import { RouteComponentProps, withRouter } from 'react-router';
+import { Context } from "shared/Context";
+import { ProjectType } from "shared/types";
+import { RouteComponentProps, withRouter } from "react-router";
 
 
 type PropsType = RouteComponentProps & {
 type PropsType = RouteComponentProps & {
-  currentProject: ProjectType,
-  projects: ProjectType[],
+  currentProject: ProjectType;
+  projects: ProjectType[];
 };
 };
 
 
 type StateType = {
 type StateType = {
-  expanded: boolean
+  expanded: boolean;
 };
 };
 
 
 class ProjectSection extends Component<PropsType, StateType> {
 class ProjectSection extends Component<PropsType, StateType> {
@@ -38,7 +38,7 @@ class ProjectSection extends Component<PropsType, StateType> {
         </Option>
         </Option>
       );
       );
     });
     });
-  }
+  };
 
 
   renderDropdown = () => {
   renderDropdown = () => {
     if (this.state.expanded) {
     if (this.state.expanded) {
@@ -50,7 +50,7 @@ class ProjectSection extends Component<PropsType, StateType> {
             <Option
             <Option
               selected={false}
               selected={false}
               lastItem={true}
               lastItem={true}
-              onClick={() => this.props.history.push('new-project')}
+              onClick={() => this.props.history.push("new-project")}
             >
             >
               <ProjectIconAlt>+</ProjectIconAlt>
               <ProjectIconAlt>+</ProjectIconAlt>
               <ProjectLabel>Create a Project</ProjectLabel>
               <ProjectLabel>Create a Project</ProjectLabel>
@@ -59,11 +59,11 @@ class ProjectSection extends Component<PropsType, StateType> {
         </div>
         </div>
       );
       );
     }
     }
-  }
+  };
 
 
   handleExpand = () => {
   handleExpand = () => {
     this.setState({ expanded: !this.state.expanded });
     this.setState({ expanded: !this.state.expanded });
-  }
+  };
 
 
   render() {
   render() {
     let { currentProject } = this.props;
     let { currentProject } = this.props;
@@ -86,7 +86,7 @@ class ProjectSection extends Component<PropsType, StateType> {
       );
       );
     }
     }
     return (
     return (
-      <InitializeButton onClick={() => this.props.history.push('new-project')}>
+      <InitializeButton onClick={() => this.props.history.push("new-project")}>
         <Plus>+</Plus> Create a Project
         <Plus>+</Plus> Create a Project
       </InitializeButton>
       </InitializeButton>
     );
     );
@@ -136,10 +136,12 @@ const InitializeButton = styled.div`
   }
   }
 `;
 `;
 
 
-const Option = styled.div` 
+const Option = styled.div`
   width: 100%;
   width: 100%;
   border-top: 1px solid #00000000;
   border-top: 1px solid #00000000;
-  border-bottom: 1px solid ${(props: { selected: boolean, lastItem?: boolean }) => props.lastItem ? '#ffffff00' : '#ffffff15'};
+  border-bottom: 1px solid
+    ${(props: { selected: boolean; lastItem?: boolean }) =>
+      props.lastItem ? "#ffffff00" : "#ffffff15"};
   height: 45px;
   height: 45px;
   display: flex;
   display: flex;
   align-items: center;
   align-items: center;
@@ -148,9 +150,11 @@ const Option = styled.div`
   padding-left: 10px;
   padding-left: 10px;
   cursor: pointer;
   cursor: pointer;
   padding-right: 10px;
   padding-right: 10px;
-  background: ${(props: { selected: boolean, lastItem?: boolean }) => props.selected ? '#ffffff11' : ''};
+  background: ${(props: { selected: boolean; lastItem?: boolean }) =>
+    props.selected ? "#ffffff11" : ""};
   :hover {
   :hover {
-    background: ${(props: { selected: boolean, lastItem?: boolean }) => props.selected ? '' : '#ffffff22'};
+    background: ${(props: { selected: boolean; lastItem?: boolean }) =>
+      props.selected ? "" : "#ffffff22"};
   }
   }
 
 
   > i {
   > i {
@@ -234,7 +238,7 @@ const MainSelector = styled.div`
   align-items: center;
   align-items: center;
   margin: 10px 0 0;
   margin: 10px 0 0;
   font-size: 14px;
   font-size: 14px;
-  font-family: 'Work Sans', sans-serif;
+  font-family: "Work Sans", sans-serif;
   font-weight: 600;
   font-weight: 600;
   cursor: pointer;
   cursor: pointer;
   padding: 10px 0;
   padding: 10px 0;
@@ -253,6 +257,7 @@ const MainSelector = styled.div`
     align-items: center;
     align-items: center;
     justify-content: center;
     justify-content: center;
     border-radius: 20px;
     border-radius: 20px;
-    background: ${(props: { expanded: boolean }) => props.expanded ? '#ffffff22' : ''};
+    background: ${(props: { expanded: boolean }) =>
+      props.expanded ? "#ffffff22" : ""};
   }
   }
-`;
+`;

+ 11 - 10
dashboard/src/main/home/sidebar/ProjectSectionContainer.tsx

@@ -1,18 +1,19 @@
-import React, { Component } from 'react';
-import styled from 'styled-components';
+import React, { Component } from "react";
+import styled from "styled-components";
 
 
-import { Context } from 'shared/Context';
-import ProjectSection from './ProjectSection';
+import { Context } from "shared/Context";
+import ProjectSection from "./ProjectSection";
 
 
 type PropsType = {};
 type PropsType = {};
 
 
-type StateType = {
-};
+type StateType = {};
 
 
 // Props in context to project section to trigger update on context change
 // Props in context to project section to trigger update on context change
-export default class ProjectSectionContainer extends Component<PropsType, StateType> {
-  state = {
-  }
+export default class ProjectSectionContainer extends Component<
+  PropsType,
+  StateType
+> {
+  state = {};
 
 
   render() {
   render() {
     return (
     return (
@@ -24,4 +25,4 @@ export default class ProjectSectionContainer extends Component<PropsType, StateT
   }
   }
 }
 }
 
 
-ProjectSectionContainer.contextType = Context;
+ProjectSectionContainer.contextType = Context;

+ 100 - 77
dashboard/src/main/home/sidebar/Sidebar.tsx

@@ -1,36 +1,35 @@
-import React, { Component } from 'react';
-import styled from 'styled-components';
-import category from 'assets/category.svg';
-import integrations from 'assets/integrations.svg';
-import filter from 'assets/filter.svg';
-import settings from 'assets/settings.svg';
+import React, { Component } from "react";
+import styled from "styled-components";
+import category from "assets/category.svg";
+import integrations from "assets/integrations.svg";
+import filter from "assets/filter.svg";
+import settings from "assets/settings.svg";
 
 
-import { Context } from 'shared/Context';
+import { Context } from "shared/Context";
 
 
-import ClusterSection from './ClusterSection';
-import ProjectSectionContainer from './ProjectSectionContainer';
-import loading from 'assets/loading.gif';
-import posthog from 'posthog-js';
-import { RouteComponentProps, withRouter } from 'react-router';
+import ClusterSection from "./ClusterSection";
+import ProjectSectionContainer from "./ProjectSectionContainer";
+import loading from "assets/loading.gif";
+import posthog from "posthog-js";
+import { RouteComponentProps, withRouter } from "react-router";
 
 
 type PropsType = RouteComponentProps & {
 type PropsType = RouteComponentProps & {
-  forceSidebar: boolean,
-  setWelcome: (x: boolean) => void,
-  currentView: string,
-  forceRefreshClusters: boolean,
-  setRefreshClusters: (x: boolean) => void,
+  forceSidebar: boolean;
+  setWelcome: (x: boolean) => void;
+  currentView: string;
+  forceRefreshClusters: boolean;
+  setRefreshClusters: (x: boolean) => void;
 };
 };
 
 
 type StateType = {
 type StateType = {
-  showSidebar: boolean,
-  initializedSidebar: boolean,
-  pressingCtrl: boolean,
-  showTooltip: boolean,
-  forceCloseDrawer: boolean
+  showSidebar: boolean;
+  initializedSidebar: boolean;
+  pressingCtrl: boolean;
+  showTooltip: boolean;
+  forceCloseDrawer: boolean;
 };
 };
 
 
 class Sidebar extends Component<PropsType, StateType> {
 class Sidebar extends Component<PropsType, StateType> {
-
   // Need closeDrawer to hide drawer on sidebar close
   // Need closeDrawer to hide drawer on sidebar close
   state = {
   state = {
     showSidebar: true,
     showSidebar: true,
@@ -41,13 +40,13 @@ class Sidebar extends Component<PropsType, StateType> {
   };
   };
 
 
   componentDidMount() {
   componentDidMount() {
-    document.addEventListener('keydown', this.handleKeyDown);
-    document.addEventListener('keyup', this.handleKeyUp);
+    document.addEventListener("keydown", this.handleKeyDown);
+    document.addEventListener("keyup", this.handleKeyUp);
   }
   }
 
 
   componentWillUnmount() {
   componentWillUnmount() {
-    document.removeEventListener('keydown', this.handleKeyDown);
-    document.removeEventListener('keyup', this.handleKeyUp);
+    document.removeEventListener("keydown", this.handleKeyDown);
+    document.removeEventListener("keyup", this.handleKeyUp);
   }
   }
 
 
   // Need to override showDrawer when the sidebar is closed
   // Need to override showDrawer when the sidebar is closed
@@ -55,10 +54,10 @@ class Sidebar extends Component<PropsType, StateType> {
     if (prevProps.forceSidebar !== this.props.forceSidebar) {
     if (prevProps.forceSidebar !== this.props.forceSidebar) {
       this.setState({ showSidebar: this.props.forceSidebar });
       this.setState({ showSidebar: this.props.forceSidebar });
     }
     }
-  }  
+  }
 
 
   handleKeyDown = (e: KeyboardEvent): void => {
   handleKeyDown = (e: KeyboardEvent): void => {
-    if (e.key === 'Meta' || e.key === 'Control') {
+    if (e.key === "Meta" || e.key === "Control") {
       this.setState({ pressingCtrl: true });
       this.setState({ pressingCtrl: true });
     } else if (e.code === "Backslash" && this.state.pressingCtrl) {
     } else if (e.code === "Backslash" && this.state.pressingCtrl) {
       this.toggleSidebar();
       this.toggleSidebar();
@@ -66,13 +65,16 @@ class Sidebar extends Component<PropsType, StateType> {
   };
   };
 
 
   handleKeyUp = (e: KeyboardEvent): void => {
   handleKeyUp = (e: KeyboardEvent): void => {
-    if (e.key === 'Meta' || e.key === 'Control') {
+    if (e.key === "Meta" || e.key === "Control") {
       this.setState({ pressingCtrl: false });
       this.setState({ pressingCtrl: false });
     }
     }
   };
   };
 
 
   toggleSidebar = (): void => {
   toggleSidebar = (): void => {
-    this.setState({ showSidebar: !this.state.showSidebar, forceCloseDrawer: true });
+    this.setState({
+      showSidebar: !this.state.showSidebar,
+      forceCloseDrawer: true,
+    });
   };
   };
 
 
   renderPullTab = (): JSX.Element | undefined => {
   renderPullTab = (): JSX.Element | undefined => {
@@ -87,9 +89,7 @@ class Sidebar extends Component<PropsType, StateType> {
 
 
   renderTooltip = (): JSX.Element | undefined => {
   renderTooltip = (): JSX.Element | undefined => {
     if (this.state.showTooltip) {
     if (this.state.showTooltip) {
-      return (
-        <Tooltip>⌘/CTRL + \</Tooltip>
-      );
+      return <Tooltip>⌘/CTRL + \</Tooltip>;
     }
     }
   };
   };
 
 
@@ -101,26 +101,31 @@ class Sidebar extends Component<PropsType, StateType> {
         <>
         <>
           <SidebarLabel>Home</SidebarLabel>
           <SidebarLabel>Home</SidebarLabel>
           <NavButton
           <NavButton
-            onClick={() => (currentView !== 'provisioner') && this.props.history.push("dashboard")}
-            selected={currentView === 'dashboard' || currentView === 'provisioner'}
+            onClick={() =>
+              currentView !== "provisioner" &&
+              this.props.history.push("dashboard")
+            }
+            selected={
+              currentView === "dashboard" || currentView === "provisioner"
+            }
           >
           >
             <Img src={category} />
             <Img src={category} />
             Dashboard
             Dashboard
           </NavButton>
           </NavButton>
           <NavButton
           <NavButton
             onClick={() => this.props.history.push("templates")}
             onClick={() => this.props.history.push("templates")}
-            selected={currentView === 'templates'}
+            selected={currentView === "templates"}
           >
           >
             <Img src={filter} />
             <Img src={filter} />
             Templates
             Templates
           </NavButton>
           </NavButton>
           <NavButton
           <NavButton
-            selected={currentView === 'integrations'}
+            selected={currentView === "integrations"}
             //onClick={() => {
             //onClick={() => {
             //  setCurrentView('integrations')
             //  setCurrentView('integrations')
-           // }}
+            // }}
             onClick={() => {
             onClick={() => {
-              setCurrentModal('IntegrationsInstructionsModal', {})
+              setCurrentModal("IntegrationsInstructionsModal", {});
             }}
             }}
           >
           >
             <Img src={integrations} />
             <Img src={integrations} />
@@ -128,25 +133,25 @@ class Sidebar extends Component<PropsType, StateType> {
           </NavButton>
           </NavButton>
           {this.context.currentProject.roles.filter((obj: any) => {
           {this.context.currentProject.roles.filter((obj: any) => {
             return obj.user_id === this.context.user.userId;
             return obj.user_id === this.context.user.userId;
-          })[0].kind === 'admin' &&
+          })[0].kind === "admin" && (
             <NavButton
             <NavButton
               onClick={() => this.props.history.push("project-settings")}
               onClick={() => this.props.history.push("project-settings")}
-              selected={this.props.currentView === 'project-settings'}
+              selected={this.props.currentView === "project-settings"}
             >
             >
               <Img enlarge={true} src={settings} />
               <Img enlarge={true} src={settings} />
               Settings
               Settings
             </NavButton>
             </NavButton>
-          }
+          )}
 
 
           <br />
           <br />
 
 
           <SidebarLabel>Current Cluster</SidebarLabel>
           <SidebarLabel>Current Cluster</SidebarLabel>
-          <ClusterSection 
-            forceCloseDrawer={this.state.forceCloseDrawer} 
+          <ClusterSection
+            forceCloseDrawer={this.state.forceCloseDrawer}
             releaseDrawer={() => this.setState({ forceCloseDrawer: false })}
             releaseDrawer={() => this.setState({ forceCloseDrawer: false })}
             setWelcome={this.props.setWelcome}
             setWelcome={this.props.setWelcome}
             currentView={currentView}
             currentView={currentView}
-            isSelected={currentView === 'cluster-dashboard'}
+            isSelected={currentView === "cluster-dashboard"}
             forceRefreshClusters={this.props.forceRefreshClusters}
             forceRefreshClusters={this.props.forceRefreshClusters}
             setRefreshClusters={this.props.setRefreshClusters}
             setRefreshClusters={this.props.setRefreshClusters}
           />
           />
@@ -155,12 +160,8 @@ class Sidebar extends Component<PropsType, StateType> {
     }
     }
 
 
     // Render placeholder if no project exists
     // Render placeholder if no project exists
-    return (
-      <ProjectPlaceholder>
-        No projects found.
-      </ProjectPlaceholder>
-    );
-  }
+    return <ProjectPlaceholder>No projects found.</ProjectPlaceholder>;
+  };
 
 
   // SidebarBg is separate to cover retracted drawer
   // SidebarBg is separate to cover retracted drawer
   render() {
   render() {
@@ -169,10 +170,14 @@ class Sidebar extends Component<PropsType, StateType> {
         {this.renderPullTab()}
         {this.renderPullTab()}
         <StyledSidebar showSidebar={this.state.showSidebar}>
         <StyledSidebar showSidebar={this.state.showSidebar}>
           <SidebarBg />
           <SidebarBg />
-          <CollapseButton 
-            onClick={this.toggleSidebar} 
-            onMouseOver={() => { this.setState({ showTooltip: true }) }}
-            onMouseOut={() => { this.setState({ showTooltip: false }) }}
+          <CollapseButton
+            onClick={this.toggleSidebar}
+            onMouseOver={() => {
+              this.setState({ showTooltip: true });
+            }}
+            onMouseOut={() => {
+              this.setState({ showTooltip: false });
+            }}
           >
           >
             {this.renderTooltip()}
             {this.renderTooltip()}
             <i className="material-icons">double_arrow</i>
             <i className="material-icons">double_arrow</i>
@@ -202,7 +207,7 @@ const ProjectPlaceholder = styled.div`
   justify-content: center;
   justify-content: center;
   height: calc(100% - 100px);
   height: calc(100% - 100px);
   font-size: 13px;
   font-size: 13px;
-  font-family: 'Work Sans', sans-serif;
+  font-family: "Work Sans", sans-serif;
   color: #aaaabb;
   color: #aaaabb;
   padding-bottom: 80px;
   padding-bottom: 80px;
 
 
@@ -219,16 +224,19 @@ const NavButton = styled.div`
   height: 42px;
   height: 42px;
   padding: 12px 35px 1px 53px;
   padding: 12px 35px 1px 53px;
   font-size: 14px;
   font-size: 14px;
-  font-family: 'Work Sans', sans-serif;
+  font-family: "Work Sans", sans-serif;
   color: #ffffff;
   color: #ffffff;
   overflow: hidden;
   overflow: hidden;
   white-space: nowrap;
   white-space: nowrap;
   text-overflow: ellipsis;
   text-overflow: ellipsis;
-  background: ${(props: { disabled?: boolean, selected?: boolean }) => props.selected ? '#ffffff11' : ''};
-  cursor: ${(props: { disabled?: boolean, selected?: boolean }) => props.disabled ? 'not-allowed' : 'pointer'};
+  background: ${(props: { disabled?: boolean; selected?: boolean }) =>
+    props.selected ? "#ffffff11" : ""};
+  cursor: ${(props: { disabled?: boolean; selected?: boolean }) =>
+    props.disabled ? "not-allowed" : "pointer"};
 
 
   :hover {
   :hover {
-    background: ${(props: { disabled?: boolean, selected?: boolean }) => props.selected ? '' : '#ffffff08'};
+    background: ${(props: { disabled?: boolean; selected?: boolean }) =>
+      props.selected ? "" : "#ffffff08"};
   }
   }
 
 
   > i {
   > i {
@@ -246,11 +254,11 @@ const NavButton = styled.div`
 
 
 const Img = styled.img<{ enlarge?: boolean }>`
 const Img = styled.img<{ enlarge?: boolean }>`
   padding: 4px 4px;
   padding: 4px 4px;
-  height: ${props => props.enlarge ? '27px' : '23px'};
-  width: ${props => props.enlarge ? '27px' : '23px'};
+  height: ${(props) => (props.enlarge ? "27px" : "23px")};
+  width: ${(props) => (props.enlarge ? "27px" : "23px")};
   border-radius: 3px;
   border-radius: 3px;
   position: absolute;
   position: absolute;
-  left: ${props => props.enlarge ? '19px' : '20px'};
+  left: ${(props) => (props.enlarge ? "19px" : "20px")};
   top: 9px;
   top: 9px;
 `;
 `;
 
 
@@ -261,7 +269,7 @@ const BottomSection = styled.div`
 `;
 `;
 
 
 const LogOutButton = styled(NavButton)`
 const LogOutButton = styled(NavButton)`
-  width: calc(100% - 55px); 
+  width: calc(100% - 55px);
   border-top-right-radius: 3px;
   border-top-right-radius: 3px;
   border-bottom-right-radius: 3px;
   border-bottom-right-radius: 3px;
   margin-left: -1px;
   margin-left: -1px;
@@ -312,7 +320,9 @@ const UserSection = styled.div`
 const RingWrapper = styled.div`
 const RingWrapper = styled.div`
   width: 28px;
   width: 28px;
   border-radius: 30px;
   border-radius: 30px;
-  :focus { outline: 0 }
+  :focus {
+    outline: 0;
+  }
   height: 28px;
   height: 28px;
   padding: 3px;
   padding: 3px;
   border: 2px solid #ffffff44;
   border: 2px solid #ffffff44;
@@ -343,7 +353,7 @@ const PullTab = styled.div`
   position: fixed;
   position: fixed;
   width: 30px;
   width: 30px;
   height: 50px;
   height: 50px;
-  background: #7A838F77;
+  background: #7a838f77;
   top: calc(50vh - 60px);
   top: calc(50vh - 60px);
   left: 0;
   left: 0;
   z-index: 1;
   z-index: 1;
@@ -352,7 +362,7 @@ const PullTab = styled.div`
   cursor: pointer;
   cursor: pointer;
 
 
   :hover {
   :hover {
-    background: #99a5aF77;
+    background: #99a5af77;
   }
   }
 
 
   > i {
   > i {
@@ -384,8 +394,12 @@ const Tooltip = styled.div`
   animation: faded-in 0.2s 0.15s;
   animation: faded-in 0.2s 0.15s;
   animation-fill-mode: forwards;
   animation-fill-mode: forwards;
   @keyframes faded-in {
   @keyframes faded-in {
-    from { opacity: 0 }
-    to { opacity: 1 }
+    from {
+      opacity: 0;
+    }
+    to {
+      opacity: 1;
+    }
   }
   }
 `;
 `;
 
 
@@ -415,20 +429,29 @@ const CollapseButton = styled.div`
 `;
 `;
 
 
 const StyledSidebar = styled.section`
 const StyledSidebar = styled.section`
-  font-family: 'Work Sans', sans-serif;
+  font-family: "Work Sans", sans-serif;
   width: 200px;
   width: 200px;
   position: relative;
   position: relative;
   padding-top: 20px;
   padding-top: 20px;
   height: 100vh;
   height: 100vh;
   z-index: 2;
   z-index: 2;
-  animation: ${(props: { showSidebar: boolean }) => (props.showSidebar ? 'showSidebar 0.4s' : 'hideSidebar 0.4s')};
+  animation: ${(props: { showSidebar: boolean }) =>
+    props.showSidebar ? "showSidebar 0.4s" : "hideSidebar 0.4s"};
   animation-fill-mode: forwards;
   animation-fill-mode: forwards;
   @keyframes showSidebar {
   @keyframes showSidebar {
-    from { margin-left: -220px }
-    to { margin-left: 0px }
+    from {
+      margin-left: -220px;
+    }
+    to {
+      margin-left: 0px;
+    }
   }
   }
   @keyframes hideSidebar {
   @keyframes hideSidebar {
-    from { margin-left: 0px }
-    to { margin-left: -220px }
+    from {
+      margin-left: 0px;
+    }
+    to {
+      margin-left: -220px;
+    }
   }
   }
-`;
+`;

+ 72 - 52
dashboard/src/main/home/templates/Templates.tsx

@@ -1,47 +1,47 @@
-import React, { Component } from 'react';
-import styled from 'styled-components';
+import React, { Component } from "react";
+import styled from "styled-components";
 
 
-import { Context } from 'shared/Context';
-import api from 'shared/api';
-import { PorterTemplate } from 'shared/types';
+import { Context } from "shared/Context";
+import api from "shared/api";
+import { PorterTemplate } from "shared/types";
 
 
-import TabSelector from 'components/TabSelector';
-import ExpandedTemplate from './expanded-template/ExpandedTemplate';
-import Loading from 'components/Loading';
+import TabSelector from "components/TabSelector";
+import ExpandedTemplate from "./expanded-template/ExpandedTemplate";
+import Loading from "components/Loading";
 
 
-import hardcodedNames from './hardcodedNameDict';
+import hardcodedNames from "./hardcodedNameDict";
 
 
-const tabOptions = [
-  { label: 'Community Templates', value: 'community' }
-];
+const tabOptions = [{ label: "Community Templates", value: "community" }];
 
 
 type PropsType = {};
 type PropsType = {};
 
 
 type StateType = {
 type StateType = {
-  currentTemplate: PorterTemplate | null,
-  currentTab: string,
-  porterTemplates: PorterTemplate[],
-  loading: boolean,
-  error: boolean
+  currentTemplate: PorterTemplate | null;
+  currentTab: string;
+  porterTemplates: PorterTemplate[];
+  loading: boolean;
+  error: boolean;
 };
 };
 
 
 export default class Templates extends Component<PropsType, StateType> {
 export default class Templates extends Component<PropsType, StateType> {
   state = {
   state = {
-    currentTemplate: null as (PorterTemplate | null),
-    currentTab: 'community',
+    currentTemplate: null as PorterTemplate | null,
+    currentTab: "community",
     porterTemplates: [] as PorterTemplate[],
     porterTemplates: [] as PorterTemplate[],
     loading: true,
     loading: true,
     error: false,
     error: false,
-  }
+  };
 
 
   componentDidMount() {
   componentDidMount() {
-    api.getTemplates('<token>', {}, {}, (err: any, res: any) => {
+    api.getTemplates("<token>", {}, {}, (err: any, res: any) => {
       if (err) {
       if (err) {
         this.setState({ loading: false, error: true });
         this.setState({ loading: false, error: true });
       } else {
       } else {
         this.setState({ porterTemplates: res.data, error: false }, () => {
         this.setState({ porterTemplates: res.data, error: false }, () => {
-          this.state.porterTemplates.sort((a, b) => (a.name > b.name) ? 1 : -1);
-          this.state.porterTemplates.sort((a,b) => (a.name === 'docker') ? -1 : (b.name === 'docker') ? 1 : 0);
+          this.state.porterTemplates.sort((a, b) => (a.name > b.name ? 1 : -1));
+          this.state.porterTemplates.sort((a, b) =>
+            a.name === "docker" ? -1 : b.name === "docker" ? 1 : 0
+          );
           this.setState({ loading: false });
           this.setState({ loading: false });
         });
         });
       }
       }
@@ -54,15 +54,21 @@ export default class Templates extends Component<PropsType, StateType> {
     }
     }
 
 
     return (
     return (
-      <Polymer><i className="material-icons">layers</i></Polymer>
+      <Polymer>
+        <i className="material-icons">layers</i>
+      </Polymer>
     );
     );
-  }
+  };
 
 
   renderTemplateList = () => {
   renderTemplateList = () => {
     let { loading, error, porterTemplates } = this.state;
     let { loading, error, porterTemplates } = this.state;
 
 
     if (loading) {
     if (loading) {
-      return <LoadingWrapper><Loading /></LoadingWrapper>
+      return (
+        <LoadingWrapper>
+          <Loading />
+        </LoadingWrapper>
+      );
     } else if (error) {
     } else if (error) {
       return (
       return (
         <Placeholder>
         <Placeholder>
@@ -77,27 +83,34 @@ export default class Templates extends Component<PropsType, StateType> {
       );
       );
     }
     }
 
 
-    return this.state.porterTemplates.map((template: PorterTemplate, i: number) => {
-      let { name, icon, description } = template;
-      if (hardcodedNames[name]) {
-        name = hardcodedNames[name];
+    return this.state.porterTemplates.map(
+      (template: PorterTemplate, i: number) => {
+        let { name, icon, description } = template;
+        if (hardcodedNames[name]) {
+          name = hardcodedNames[name];
+        }
+        return (
+          <TemplateBlock
+            key={i}
+            onClick={() => this.setState({ currentTemplate: template })}
+          >
+            {this.renderIcon(icon)}
+            <TemplateTitle>{name}</TemplateTitle>
+            <TemplateDescription>{description}</TemplateDescription>
+          </TemplateBlock>
+        );
       }
       }
-      return (
-        <TemplateBlock key={i} onClick={() => this.setState({ currentTemplate: template })}>
-          {this.renderIcon(icon)}
-          <TemplateTitle>{name}</TemplateTitle>
-          <TemplateDescription>{description}</TemplateDescription>
-        </TemplateBlock>
-      )
-    });
-  }
+    );
+  };
 
 
   renderContents = () => {
   renderContents = () => {
     if (this.state.currentTemplate) {
     if (this.state.currentTemplate) {
       return (
       return (
         <ExpandedTemplate
         <ExpandedTemplate
           currentTemplate={this.state.currentTemplate}
           currentTemplate={this.state.currentTemplate}
-          setCurrentTemplate={(currentTemplate: PorterTemplate) => this.setState({ currentTemplate })}
+          setCurrentTemplate={(currentTemplate: PorterTemplate) =>
+            this.setState({ currentTemplate })
+          }
         />
         />
       );
       );
     }
     }
@@ -106,22 +119,25 @@ export default class Templates extends Component<PropsType, StateType> {
       <TemplatesWrapper>
       <TemplatesWrapper>
         <TitleSection>
         <TitleSection>
           <Title>Template Explorer</Title>
           <Title>Template Explorer</Title>
-          <a href='https://docs.getporter.dev/docs/porter-templates' target='_blank'>
+          <a
+            href="https://docs.getporter.dev/docs/porter-templates"
+            target="_blank"
+          >
             <i className="material-icons">help_outline</i>
             <i className="material-icons">help_outline</i>
           </a>
           </a>
         </TitleSection>
         </TitleSection>
         <TabSelector
         <TabSelector
           options={tabOptions}
           options={tabOptions}
           currentTab={this.state.currentTab}
           currentTab={this.state.currentTab}
-          setCurrentTab={(value: string) => this.setState({ currentTab: value })}
+          setCurrentTab={(value: string) =>
+            this.setState({ currentTab: value })
+          }
         />
         />
-        <TemplateList>
-          {this.renderTemplateList()}
-        </TemplateList>
+        <TemplateList>{this.renderTemplateList()}</TemplateList>
       </TemplatesWrapper>
       </TemplatesWrapper>
     );
     );
-  }
-  
+  };
+
   render() {
   render() {
     return this.renderContents();
     return this.renderContents();
   }
   }
@@ -173,7 +189,7 @@ const TemplateDescription = styled.div`
   display: -webkit-box;
   display: -webkit-box;
   overflow: hidden;
   overflow: hidden;
   -webkit-line-clamp: 2;
   -webkit-line-clamp: 2;
-  -webkit-box-orient: vertical;  
+  -webkit-box-orient: vertical;
 `;
 `;
 
 
 const TemplateTitle = styled.div`
 const TemplateTitle = styled.div`
@@ -210,8 +226,12 @@ const TemplateBlock = styled.div`
 
 
   animation: fadeIn 0.3s 0s;
   animation: fadeIn 0.3s 0s;
   @keyframes fadeIn {
   @keyframes fadeIn {
-    from { opacity: 0 }
-    to { opacity: 1 }
+    from {
+      opacity: 0;
+    }
+    to {
+      opacity: 1;
+    }
   }
   }
 `;
 `;
 
 
@@ -228,7 +248,7 @@ const TemplateList = styled.div`
 const Title = styled.div`
 const Title = styled.div`
   font-size: 24px;
   font-size: 24px;
   font-weight: 600;
   font-weight: 600;
-  font-family: 'Work Sans', sans-serif;
+  font-family: "Work Sans", sans-serif;
   color: #ffffff;
   color: #ffffff;
   white-space: nowrap;
   white-space: nowrap;
   overflow: hidden;
   overflow: hidden;
@@ -248,7 +268,7 @@ const TitleSection = styled.div`
       margin-bottom: -2px;
       margin-bottom: -2px;
       font-size: 18px;
       font-size: 18px;
       margin-left: 18px;
       margin-left: 18px;
-      color: #858FAAaa;
+      color: #858faaaa;
       cursor: pointer;
       cursor: pointer;
       :hover {
       :hover {
         color: #aaaabb;
         color: #aaaabb;

+ 56 - 38
dashboard/src/main/home/templates/expanded-template/ExpandedTemplate.tsx

@@ -1,26 +1,26 @@
-import React, { Component } from 'react';
-import styled from 'styled-components';
+import React, { Component } from "react";
+import styled from "styled-components";
 
 
-import { PorterTemplate } from 'shared/types';
-import api from 'shared/api';
+import { PorterTemplate } from "shared/types";
+import api from "shared/api";
 
 
-import TemplateInfo from './TemplateInfo';
-import LaunchTemplate from './LaunchTemplate';
-import Loading from 'components/Loading';
+import TemplateInfo from "./TemplateInfo";
+import LaunchTemplate from "./LaunchTemplate";
+import Loading from "components/Loading";
 
 
 type PropsType = {
 type PropsType = {
-  currentTemplate: PorterTemplate,
-  setCurrentTemplate: (x: PorterTemplate) => void,
+  currentTemplate: PorterTemplate;
+  setCurrentTemplate: (x: PorterTemplate) => void;
 };
 };
 
 
 type StateType = {
 type StateType = {
-  showLaunchTemplate: boolean,
-  form: any | null,
-  values: any | null,
-  loading: boolean,
-  error: boolean,
-  markdown: string | null,
-  keywords: string[],
+  showLaunchTemplate: boolean;
+  form: any | null;
+  values: any | null;
+  loading: boolean;
+  error: boolean;
+  markdown: string | null;
+  keywords: string[];
 };
 };
 
 
 export default class ExpandedTemplate extends Component<PropsType, StateType> {
 export default class ExpandedTemplate extends Component<PropsType, StateType> {
@@ -32,28 +32,44 @@ export default class ExpandedTemplate extends Component<PropsType, StateType> {
     error: false,
     error: false,
     markdown: null as string | null,
     markdown: null as string | null,
     keywords: [] as string[],
     keywords: [] as string[],
-  }
+  };
 
 
   componentDidMount() {
   componentDidMount() {
     this.setState({ loading: true });
     this.setState({ loading: true });
-    api.getTemplateInfo('<token>', {}, {
-      name: this.props.currentTemplate.name.toLowerCase().trim(),
-      version: 'latest',
-    }, (err: any, res: any) => {
-      if (err) {
-        this.setState({ loading: false, error: true });
-      } else {
-        let { form, values, markdown, metadata } = res.data;
-        let keywords = metadata.keywords;
-        this.setState({ form, values, markdown, keywords, loading: false, error: false });
+    api.getTemplateInfo(
+      "<token>",
+      {},
+      {
+        name: this.props.currentTemplate.name.toLowerCase().trim(),
+        version: "latest",
+      },
+      (err: any, res: any) => {
+        if (err) {
+          this.setState({ loading: false, error: true });
+        } else {
+          let { form, values, markdown, metadata } = res.data;
+          let keywords = metadata.keywords;
+          this.setState({
+            form,
+            values,
+            markdown,
+            keywords,
+            loading: false,
+            error: false,
+          });
+        }
       }
       }
-    });
+    );
   }
   }
 
 
   renderContents = () => {
   renderContents = () => {
-      if (this.state.loading) {
-        return <LoadingWrapper><Loading /></LoadingWrapper>;
-      }
+    if (this.state.loading) {
+      return (
+        <LoadingWrapper>
+          <Loading />
+        </LoadingWrapper>
+      );
+    }
     if (this.state.showLaunchTemplate) {
     if (this.state.showLaunchTemplate) {
       return (
       return (
         <LaunchTemplate
         <LaunchTemplate
@@ -76,13 +92,11 @@ export default class ExpandedTemplate extends Component<PropsType, StateType> {
         />
         />
       </FadeWrapper>
       </FadeWrapper>
     );
     );
-  }
+  };
 
 
   render() {
   render() {
     return (
     return (
-      <StyledExpandedTemplate>
-        {this.renderContents()}
-      </StyledExpandedTemplate>
+      <StyledExpandedTemplate>{this.renderContents()}</StyledExpandedTemplate>
     );
     );
   }
   }
 }
 }
@@ -90,8 +104,12 @@ export default class ExpandedTemplate extends Component<PropsType, StateType> {
 const FadeWrapper = styled.div`
 const FadeWrapper = styled.div`
   animation: fadeIn 0.2s;
   animation: fadeIn 0.2s;
   @keyframes fadeIn {
   @keyframes fadeIn {
-    from: { opacity: 0 }
-    to: { opacity: 1 }
+    from: {
+      opacity: 0;
+    }
+    to: {
+      opacity: 1;
+    }
   }
   }
 `;
 `;
 
 
@@ -104,4 +122,4 @@ const StyledExpandedTemplate = styled.div`
   width: calc(90% - 150px);
   width: calc(90% - 150px);
   min-width: 300px;
   min-width: 300px;
   padding-top: 75px;
   padding-top: 75px;
-`;
+`;

+ 238 - 184
dashboard/src/main/home/templates/expanded-template/LaunchTemplate.tsx

@@ -1,106 +1,118 @@
-import React, { Component } from 'react';
-import styled from 'styled-components';
-import randomWords from 'random-words';
-import posthog from 'posthog-js';
-import _ from 'lodash';
-import { Context } from 'shared/Context';
-import api from 'shared/api';
-
-import { PorterTemplate, ChoiceType, ClusterType, StorageType } from 'shared/types';
-import Selector from 'components/Selector';
-import ImageSelector from 'components/image-selector/ImageSelector';
-import TabRegion from 'components/TabRegion';
-import InputRow from 'components/values-form/InputRow';
-import SaveButton from 'components/SaveButton';
-import ValuesWrapper from 'components/values-form/ValuesWrapper';
-import ValuesForm from 'components/values-form/ValuesForm';
-import { isAlphanumeric } from 'shared/common';
-import { safeDump } from 'js-yaml';
+import React, { Component } from "react";
+import styled from "styled-components";
+import randomWords from "random-words";
+import posthog from "posthog-js";
+import _ from "lodash";
+import { Context } from "shared/Context";
+import api from "shared/api";
+
+import {
+  PorterTemplate,
+  ChoiceType,
+  ClusterType,
+  StorageType,
+} from "shared/types";
+import Selector from "components/Selector";
+import ImageSelector from "components/image-selector/ImageSelector";
+import TabRegion from "components/TabRegion";
+import InputRow from "components/values-form/InputRow";
+import SaveButton from "components/SaveButton";
+import ValuesWrapper from "components/values-form/ValuesWrapper";
+import ValuesForm from "components/values-form/ValuesForm";
+import { isAlphanumeric } from "shared/common";
+import { safeDump } from "js-yaml";
 
 
 type PropsType = {
 type PropsType = {
-  currentTemplate: any,
-  hideLaunch: () => void,
-  values: any,
-  form: any,
+  currentTemplate: any;
+  hideLaunch: () => void;
+  values: any;
+  form: any;
 };
 };
 
 
 type StateType = {
 type StateType = {
-  currentView: string,
-  clusterOptions: { label: string, value: string }[],
-  saveValuesStatus: string | null
-  selectedNamespace: string,
-  selectedCluster: string,
-  selectedImageUrl: string | null,
-  selectedTag: string | null,
-  templateName: string,
-  tabOptions: ChoiceType[],
-  currentTab: string | null,
-  tabContents: any
-  namespaceOptions: { label: string, value: string }[],
+  currentView: string;
+  clusterOptions: { label: string; value: string }[];
+  saveValuesStatus: string | null;
+  selectedNamespace: string;
+  selectedCluster: string;
+  selectedImageUrl: string | null;
+  selectedTag: string | null;
+  templateName: string;
+  tabOptions: ChoiceType[];
+  currentTab: string | null;
+  tabContents: any;
+  namespaceOptions: { label: string; value: string }[];
 };
 };
 
 
 export default class LaunchTemplate extends Component<PropsType, StateType> {
 export default class LaunchTemplate extends Component<PropsType, StateType> {
   state = {
   state = {
-    currentView: 'repo',
-    clusterOptions: [] as { label: string, value: string }[],
-    saveValuesStatus: 'No container image specified' as (string | null),
+    currentView: "repo",
+    clusterOptions: [] as { label: string; value: string }[],
+    saveValuesStatus: "No container image specified" as string | null,
     selectedCluster: this.context.currentCluster.name,
     selectedCluster: this.context.currentCluster.name,
     selectedNamespace: "default",
     selectedNamespace: "default",
-    selectedImageUrl: '' as string | null,
-    templateName: '',
-    selectedTag: '' as string | null,
+    selectedImageUrl: "" as string | null,
+    templateName: "",
+    selectedTag: "" as string | null,
     tabOptions: [] as ChoiceType[],
     tabOptions: [] as ChoiceType[],
     currentTab: null as string | null,
     currentTab: null as string | null,
     tabContents: [] as any,
     tabContents: [] as any,
-    namespaceOptions: [] as { label: string, value: string }[],
+    namespaceOptions: [] as { label: string; value: string }[],
   };
   };
 
 
   onSubmitAddon = (wildcard?: any) => {
   onSubmitAddon = (wildcard?: any) => {
     let { currentCluster, currentProject } = this.context;
     let { currentCluster, currentProject } = this.context;
-    let name = this.state.templateName || randomWords({ exactly: 3, join: '-' });
-    this.setState({ saveValuesStatus: 'loading' });
+    let name =
+      this.state.templateName || randomWords({ exactly: 3, join: "-" });
+    this.setState({ saveValuesStatus: "loading" });
 
 
     let values = {};
     let values = {};
     for (let key in wildcard) {
     for (let key in wildcard) {
       _.set(values, key, wildcard[key]);
       _.set(values, key, wildcard[key]);
     }
     }
 
 
-    api.deployTemplate('<token>', {
-      templateName: this.props.currentTemplate.name,
-      storage: StorageType.Secret,
-      formValues: values,
-      namespace: this.state.selectedNamespace,
-      name,
-    }, {
-      id: currentProject.id,
-      cluster_id: currentCluster.id,
-      name: this.props.currentTemplate.name.toLowerCase().trim(),
-      version: 'latest',
-    }, (err: any, res: any) => {
-      if (err) {
-        this.setState({ saveValuesStatus: 'error' });
-        posthog.capture('Failed to deploy template', {
-          name: this.props.currentTemplate.name,
-          namespace: this.state.selectedNamespace,
-          values: values,
-          error: err
-        })
-      } else {
-        // this.props.setCurrentView('cluster-dashboard');
-        this.setState({ saveValuesStatus: 'successful' });
-        posthog.capture('Deployed template', {
-          name: this.props.currentTemplate.name,
-          namespace: this.state.selectedNamespace,
-          values: values,
-        })
+    api.deployTemplate(
+      "<token>",
+      {
+        templateName: this.props.currentTemplate.name,
+        storage: StorageType.Secret,
+        formValues: values,
+        namespace: this.state.selectedNamespace,
+        name,
+      },
+      {
+        id: currentProject.id,
+        cluster_id: currentCluster.id,
+        name: this.props.currentTemplate.name.toLowerCase().trim(),
+        version: "latest",
+      },
+      (err: any, res: any) => {
+        if (err) {
+          this.setState({ saveValuesStatus: "error" });
+          posthog.capture("Failed to deploy template", {
+            name: this.props.currentTemplate.name,
+            namespace: this.state.selectedNamespace,
+            values: values,
+            error: err,
+          });
+        } else {
+          // this.props.setCurrentView('cluster-dashboard');
+          this.setState({ saveValuesStatus: "successful" });
+          posthog.capture("Deployed template", {
+            name: this.props.currentTemplate.name,
+            namespace: this.state.selectedNamespace,
+            values: values,
+          });
+        }
       }
       }
-    });
-  }
+    );
+  };
 
 
   onSubmit = (rawValues: any) => {
   onSubmit = (rawValues: any) => {
     let { currentCluster, currentProject } = this.context;
     let { currentCluster, currentProject } = this.context;
-    let name = this.state.templateName || randomWords({ exactly: 3, join: '-' });
-    this.setState({ saveValuesStatus: 'loading' });
+    let name =
+      this.state.templateName || randomWords({ exactly: 3, join: "-" });
+    this.setState({ saveValuesStatus: "loading" });
 
 
     // Convert dotted keys to nested objects
     // Convert dotted keys to nested objects
     let values = {};
     let values = {};
@@ -111,72 +123,81 @@ export default class LaunchTemplate extends Component<PropsType, StateType> {
     let imageUrl = this.state.selectedImageUrl;
     let imageUrl = this.state.selectedImageUrl;
     let tag = this.state.selectedTag;
     let tag = this.state.selectedTag;
 
 
-    if (this.state.selectedImageUrl.includes(':')) {
-      let splits = this.state.selectedImageUrl.split(':');
+    if (this.state.selectedImageUrl.includes(":")) {
+      let splits = this.state.selectedImageUrl.split(":");
       imageUrl = splits[0];
       imageUrl = splits[0];
       tag = splits[1];
       tag = splits[1];
     } else if (!tag) {
     } else if (!tag) {
-      tag = 'latest';
+      tag = "latest";
     }
     }
 
 
-    _.set(values, "image.repository", imageUrl)
-    _.set(values, "image.tag", tag)
-
-    api.deployTemplate('<token>', {
-      templateName: this.props.currentTemplate.name,
-      imageURL: this.state.selectedImageUrl,
-      storage: StorageType.Secret,
-      formValues: values,
-      namespace: this.state.selectedNamespace,
-      name,
-    }, {
-      id: currentProject.id,
-      cluster_id: currentCluster.id,
-      name: this.props.currentTemplate.name.toLowerCase().trim(),
-      version: 'latest',
-    }, (err: any, res: any) => {
-      if (err) {
-        this.setState({ saveValuesStatus: 'error' });
-        posthog.capture('Failed to deploy template', {
-          name: this.props.currentTemplate.name,
-          namespace: this.state.selectedNamespace,
-          values: values,
-          error: err
-        })
-      } else {
-        // this.props.setCurrentView('cluster-dashboard');
-        this.setState({ saveValuesStatus: 'successful' });
-        posthog.capture('Deployed template', {
-          name: this.props.currentTemplate.name,
-          namespace: this.state.selectedNamespace,
-          values: values,
-        })
+    _.set(values, "image.repository", imageUrl);
+    _.set(values, "image.tag", tag);
+
+    api.deployTemplate(
+      "<token>",
+      {
+        templateName: this.props.currentTemplate.name,
+        imageURL: this.state.selectedImageUrl,
+        storage: StorageType.Secret,
+        formValues: values,
+        namespace: this.state.selectedNamespace,
+        name,
+      },
+      {
+        id: currentProject.id,
+        cluster_id: currentCluster.id,
+        name: this.props.currentTemplate.name.toLowerCase().trim(),
+        version: "latest",
+      },
+      (err: any, res: any) => {
+        if (err) {
+          this.setState({ saveValuesStatus: "error" });
+          posthog.capture("Failed to deploy template", {
+            name: this.props.currentTemplate.name,
+            namespace: this.state.selectedNamespace,
+            values: values,
+            error: err,
+          });
+        } else {
+          // this.props.setCurrentView('cluster-dashboard');
+          this.setState({ saveValuesStatus: "successful" });
+          posthog.capture("Deployed template", {
+            name: this.props.currentTemplate.name,
+            namespace: this.state.selectedNamespace,
+            values: values,
+          });
+        }
       }
       }
-    });
-  }
+    );
+  };
 
 
   renderTabContents = () => {
   renderTabContents = () => {
     return (
     return (
       <ValuesWrapper
       <ValuesWrapper
         formTabs={this.props.form?.tabs}
         formTabs={this.props.form?.tabs}
-        onSubmit={this.props.currentTemplate.name === 'docker' ? this.onSubmit : this.onSubmitAddon}
+        onSubmit={
+          this.props.currentTemplate.name === "docker"
+            ? this.onSubmit
+            : this.onSubmitAddon
+        }
         saveValuesStatus={this.state.saveValuesStatus}
         saveValuesStatus={this.state.saveValuesStatus}
         disabled={
         disabled={
-          (this.state.templateName.length > 0 && !isAlphanumeric(this.state.templateName))
-          || (this.props.form?.hasSource ? !this.state.selectedImageUrl : false)
+          (this.state.templateName.length > 0 &&
+            !isAlphanumeric(this.state.templateName)) ||
+          (this.props.form?.hasSource ? !this.state.selectedImageUrl : false)
         }
         }
       >
       >
         {(metaState: any, setMetaState: any) => {
         {(metaState: any, setMetaState: any) => {
           return this.props.form?.tabs.map((tab: any, i: number) => {
           return this.props.form?.tabs.map((tab: any, i: number) => {
-
             // If tab is current, render
             // If tab is current, render
             if (tab.name === this.state.currentTab) {
             if (tab.name === this.state.currentTab) {
               return (
               return (
-                <ValuesForm 
+                <ValuesForm
                   metaState={metaState}
                   metaState={metaState}
                   setMetaState={setMetaState}
                   setMetaState={setMetaState}
                   key={tab.name}
                   key={tab.name}
-                  sections={tab.sections} 
+                  sections={tab.sections}
                 />
                 />
               );
               );
             }
             }
@@ -184,17 +205,17 @@ export default class LaunchTemplate extends Component<PropsType, StateType> {
         }}
         }}
       </ValuesWrapper>
       </ValuesWrapper>
     );
     );
-  }
+  };
 
 
   componentDidMount() {
   componentDidMount() {
-    if (this.props.currentTemplate.name !== 'docker') {
-      this.setState({ saveValuesStatus: '' });
+    if (this.props.currentTemplate.name !== "docker") {
+      this.setState({ saveValuesStatus: "" });
     }
     }
 
 
     // Retrieve tab options
     // Retrieve tab options
     let tabOptions = [] as ChoiceType[];
     let tabOptions = [] as ChoiceType[];
     this.props.form?.tabs.map((tab: any, i: number) => {
     this.props.form?.tabs.map((tab: any, i: number) => {
-      if (tab.context.type === 'helm/values') {
+      if (tab.context.type === "helm/values") {
         tabOptions.push({ value: tab.name, label: tab.label });
         tabOptions.push({ value: tab.name, label: tab.label });
       }
       }
     });
     });
@@ -202,57 +223,75 @@ export default class LaunchTemplate extends Component<PropsType, StateType> {
 
 
     // TODO: query with selected filter once implemented
     // TODO: query with selected filter once implemented
     let { currentProject, currentCluster } = this.context;
     let { currentProject, currentCluster } = this.context;
-    api.getClusters('<token>', {}, { id: currentProject.id }, (err: any, res: any) => {
-      if (err) {
-        // console.log(err)
-      } else if (res.data) {
-        let clusterOptions = res.data.map((x: ClusterType) => { return { label: x.name, value: x.name } });
-        if (res.data.length > 0) {
-          this.setState({ clusterOptions });
+    api.getClusters(
+      "<token>",
+      {},
+      { id: currentProject.id },
+      (err: any, res: any) => {
+        if (err) {
+          // console.log(err)
+        } else if (res.data) {
+          let clusterOptions = res.data.map((x: ClusterType) => {
+            return { label: x.name, value: x.name };
+          });
+          if (res.data.length > 0) {
+            this.setState({ clusterOptions });
+          }
         }
         }
       }
       }
-    });
+    );
 
 
-    api.getNamespaces('<token>', {
-      cluster_id: currentCluster.id,
-    }, { id: currentProject.id }, (err: any, res: any) => {
-      if (err) {
-        console.log(err)
-      } else if (res.data) {
-        let namespaceOptions = res.data.items.map((x: { metadata: {name: string}}) => { 
-          return { label: x.metadata.name, value: x.metadata.name } 
-        });
-        if (res.data.items.length > 0) {
-          this.setState({ namespaceOptions });
+    api.getNamespaces(
+      "<token>",
+      {
+        cluster_id: currentCluster.id,
+      },
+      { id: currentProject.id },
+      (err: any, res: any) => {
+        if (err) {
+          console.log(err);
+        } else if (res.data) {
+          let namespaceOptions = res.data.items.map(
+            (x: { metadata: { name: string } }) => {
+              return { label: x.metadata.name, value: x.metadata.name };
+            }
+          );
+          if (res.data.items.length > 0) {
+            this.setState({ namespaceOptions });
+          }
         }
         }
       }
       }
-    });
+    );
   }
   }
 
 
   setSelectedImageUrl = (x: string) => {
   setSelectedImageUrl = (x: string) => {
-    if (x === '') {
-      this.setState({ saveValuesStatus: 'No container image specified' });
+    if (x === "") {
+      this.setState({ saveValuesStatus: "No container image specified" });
     } else {
     } else {
-      this.setState({ saveValuesStatus: '' });
+      this.setState({ saveValuesStatus: "" });
     }
     }
     this.setState({ selectedImageUrl: x });
     this.setState({ selectedImageUrl: x });
-  }
+  };
 
 
   renderIcon = (icon: string) => {
   renderIcon = (icon: string) => {
     if (icon) {
     if (icon) {
-      return <Icon src={icon} />
+      return <Icon src={icon} />;
     }
     }
 
 
     return (
     return (
-      <Polymer><i className="material-icons">layers</i></Polymer>
+      <Polymer>
+        <i className="material-icons">layers</i>
+      </Polymer>
     );
     );
-  }
+  };
 
 
   renderTabRegion = () => {
   renderTabRegion = () => {
     if (this.state.tabOptions.length > 0) {
     if (this.state.tabOptions.length > 0) {
       return (
       return (
         <>
         <>
-          <Subtitle>Configure additional settings for this template. (Optional)</Subtitle>
+          <Subtitle>
+            Configure additional settings for this template. (Optional)
+          </Subtitle>
           <TabRegion
           <TabRegion
             options={this.state.tabOptions}
             options={this.state.tabOptions}
             currentTab={this.state.currentTab}
             currentTab={this.state.currentTab}
@@ -266,16 +305,17 @@ export default class LaunchTemplate extends Component<PropsType, StateType> {
       return (
       return (
         <Wrapper>
         <Wrapper>
           <Placeholder>
           <Placeholder>
-            To configure this chart through Porter, 
-            <Link 
-              target='_blank'
-              href='https://docs.getporter.dev/docs/porter-templates'
+            To configure this chart through Porter,
+            <Link
+              target="_blank"
+              href="https://docs.getporter.dev/docs/porter-templates"
             >
             >
               refer to our docs
               refer to our docs
-            </Link>.
+            </Link>
+            .
           </Placeholder>
           </Placeholder>
           <SaveButton
           <SaveButton
-            text='Deploy'
+            text="Deploy"
             onClick={() => this.onSubmitAddon()}
             onClick={() => this.onSubmitAddon()}
             status={this.state.saveValuesStatus}
             status={this.state.saveValuesStatus}
             makeFlush={true}
             makeFlush={true}
@@ -283,7 +323,7 @@ export default class LaunchTemplate extends Component<PropsType, StateType> {
         </Wrapper>
         </Wrapper>
       );
       );
     }
     }
-  }
+  };
 
 
   // Display if current template uses source (image or repo)
   // Display if current template uses source (image or repo)
   renderSourceSelector = () => {
   renderSourceSelector = () => {
@@ -291,7 +331,8 @@ export default class LaunchTemplate extends Component<PropsType, StateType> {
       return (
       return (
         <>
         <>
           <Subtitle>
           <Subtitle>
-            Select the container image you would like to connect to this template.
+            Select the container image you would like to connect to this
+            template.
             <Required>*</Required>
             <Required>*</Required>
           </Subtitle>
           </Subtitle>
           <DarkMatter />
           <DarkMatter />
@@ -306,7 +347,7 @@ export default class LaunchTemplate extends Component<PropsType, StateType> {
         </>
         </>
       );
       );
     }
     }
-  }
+  };
 
 
   render() {
   render() {
     let { name, icon } = this.props.currentTemplate;
     let { name, icon } = this.props.currentTemplate;
@@ -324,7 +365,9 @@ export default class LaunchTemplate extends Component<PropsType, StateType> {
         </TitleSection>
         </TitleSection>
         <ClusterSection>
         <ClusterSection>
           <Template>
           <Template>
-            {icon ? this.renderIcon(icon) : this.renderIcon(currentTemplate.icon)}
+            {icon
+              ? this.renderIcon(icon)
+              : this.renderIcon(currentTemplate.icon)}
             {name}
             {name}
           </Template>
           </Template>
           <i className="material-icons">arrow_right_alt</i>
           <i className="material-icons">arrow_right_alt</i>
@@ -333,36 +376,48 @@ export default class LaunchTemplate extends Component<PropsType, StateType> {
           </ClusterLabel>
           </ClusterLabel>
           <Selector
           <Selector
             activeValue={this.state.selectedCluster}
             activeValue={this.state.selectedCluster}
-            setActiveValue={(cluster: string) => this.setState({ selectedCluster: cluster })}
+            setActiveValue={(cluster: string) =>
+              this.setState({ selectedCluster: cluster })
+            }
             options={this.state.clusterOptions}
             options={this.state.clusterOptions}
-            width='250px'
-            dropdownWidth='335px'
+            width="250px"
+            dropdownWidth="335px"
             closeOverlay={true}
             closeOverlay={true}
           />
           />
           <NamespaceLabel>
           <NamespaceLabel>
             <i className="material-icons">view_list</i>Namespace
             <i className="material-icons">view_list</i>Namespace
           </NamespaceLabel>
           </NamespaceLabel>
           <Selector
           <Selector
-            key={'namespace'}
+            key={"namespace"}
             activeValue={this.state.selectedNamespace}
             activeValue={this.state.selectedNamespace}
-            setActiveValue={(namespace: string) => this.setState({ selectedNamespace: namespace })}
+            setActiveValue={(namespace: string) =>
+              this.setState({ selectedNamespace: namespace })
+            }
             options={this.state.namespaceOptions}
             options={this.state.namespaceOptions}
-            width='250px'
-            dropdownWidth='335px'
+            width="250px"
+            dropdownWidth="335px"
             closeOverlay={true}
             closeOverlay={true}
           />
           />
         </ClusterSection>
         </ClusterSection>
-        <Subtitle>Template name
-          <Warning highlight={!isAlphanumeric(this.state.templateName) && this.state.templateName !== ''}>
+        <Subtitle>
+          Template name
+          <Warning
+            highlight={
+              !isAlphanumeric(this.state.templateName) &&
+              this.state.templateName !== ""
+            }
+          >
             (lowercase letters, numbers, and "-" only)
             (lowercase letters, numbers, and "-" only)
-          </Warning>. (Optional)</Subtitle>
-        <DarkMatter antiHeight='-27px' />
+          </Warning>
+          . (Optional)
+        </Subtitle>
+        <DarkMatter antiHeight="-27px" />
         <InputRow
         <InputRow
-          type='text'
+          type="text"
           value={this.state.templateName}
           value={this.state.templateName}
           setValue={(x: string) => this.setState({ templateName: x })}
           setValue={(x: string) => this.setState({ templateName: x })}
-          placeholder='ex: doctor-scientist'
-          width='100%'
+          placeholder="ex: doctor-scientist"
+          width="100%"
         />
         />
         {this.renderSourceSelector()}
         {this.renderSourceSelector()}
         {this.renderTabRegion()}
         {this.renderTabRegion()}
@@ -373,9 +428,9 @@ export default class LaunchTemplate extends Component<PropsType, StateType> {
 
 
 LaunchTemplate.contextType = Context;
 LaunchTemplate.contextType = Context;
 
 
-const Warning = styled.span<{ highlight: boolean, makeFlush?: boolean }>`
-  color: ${props => props.highlight ? '#f5cb42' : ''};
-  margin-left: ${props => props.makeFlush ? '' : '5px'};
+const Warning = styled.span<{ highlight: boolean; makeFlush?: boolean }>`
+  color: ${(props) => (props.highlight ? "#f5cb42" : "")};
+  margin-left: ${(props) => (props.makeFlush ? "" : "5px")};
 `;
 `;
 
 
 const Required = styled.div`
 const Required = styled.div`
@@ -416,12 +471,12 @@ const Placeholder = styled.div`
 
 
 const DarkMatter = styled.div<{ antiHeight?: string }>`
 const DarkMatter = styled.div<{ antiHeight?: string }>`
   width: 100%;
   width: 100%;
-  margin-top: ${props => props.antiHeight || '-15px'};
+  margin-top: ${(props) => props.antiHeight || "-15px"};
 `;
 `;
 
 
 const Subtitle = styled.div`
 const Subtitle = styled.div`
   padding: 11px 0px 20px;
   padding: 11px 0px 20px;
-  font-family: 'Work Sans', sans-serif;
+  font-family: "Work Sans", sans-serif;
   font-size: 13px;
   font-size: 13px;
   color: #aaaabb;
   color: #aaaabb;
   line-height: 1.6em;
   line-height: 1.6em;
@@ -455,12 +510,11 @@ const Icon = styled.img`
   margin-right: 10px;
   margin-right: 10px;
 `;
 `;
 
 
-
 const Polymer = styled.div`
 const Polymer = styled.div`
   margin-bottom: -3px;
   margin-bottom: -3px;
 
 
   > i {
   > i {
-    color: ${props => props.theme.containerIcon};
+    color: ${(props) => props.theme.containerIcon};
     font-size: 18px;
     font-size: 18px;
     margin-right: 10px;
     margin-right: 10px;
   }
   }
@@ -476,7 +530,7 @@ const ClusterSection = styled.div`
   display: flex;
   display: flex;
   align-items: center;
   align-items: center;
   color: #ffffff;
   color: #ffffff;
-  font-family: 'Work Sans', sans-serif;
+  font-family: "Work Sans", sans-serif;
   font-size: 14px;
   font-size: 14px;
   font-weight: 500;
   font-weight: 500;
   margin-top: 20px;
   margin-top: 20px;
@@ -508,7 +562,7 @@ const Flex = styled.div`
 const Title = styled.div`
 const Title = styled.div`
   font-size: 24px;
   font-size: 24px;
   font-weight: 600;
   font-weight: 600;
-  font-family: 'Work Sans', sans-serif;
+  font-family: "Work Sans", sans-serif;
   margin-left: 11px;
   margin-left: 11px;
   border-radius: 2px;
   border-radius: 2px;
   color: #ffffff;
   color: #ffffff;
@@ -527,4 +581,4 @@ const TitleSection = styled.div`
 const StyledLaunchTemplate = styled.div`
 const StyledLaunchTemplate = styled.div`
   width: 100%;
   width: 100%;
   padding-bottom: 150px;
   padding-bottom: 150px;
-`;
+`;

+ 54 - 51
dashboard/src/main/home/templates/expanded-template/TemplateInfo.tsx

@@ -1,61 +1,55 @@
-import React, { Component } from 'react';
-import styled from 'styled-components';
-import rocket from 'assets/rocket.png';
-import Markdown from 'markdown-to-jsx';
+import React, { Component } from "react";
+import styled from "styled-components";
+import rocket from "assets/rocket.png";
+import Markdown from "markdown-to-jsx";
 
 
-import { Context } from 'shared/Context';
-import Loading from 'components/Loading';
+import { Context } from "shared/Context";
 
 
-import { PorterTemplate } from 'shared/types';
-import Helper from 'components/values-form/Helper';
+import { PorterTemplate } from "shared/types";
+import Helper from "components/values-form/Helper";
 
 
-import hardcodedNames from '../hardcodedNameDict';
+import hardcodedNames from "../hardcodedNameDict";
 
 
 type PropsType = {
 type PropsType = {
-  currentTemplate: any,
-  setCurrentTemplate: (x: PorterTemplate) => void,
-  launchTemplate: () => void,
-  markdown: string | null,
-  keywords: string[],
+  currentTemplate: any;
+  setCurrentTemplate: (x: PorterTemplate) => void;
+  launchTemplate: () => void;
+  markdown: string | null;
+  keywords: string[];
 };
 };
 
 
-type StateType = {
-};
+type StateType = {};
 
 
 export default class TemplateInfo extends Component<PropsType, StateType> {
 export default class TemplateInfo extends Component<PropsType, StateType> {
   renderIcon = (icon: string) => {
   renderIcon = (icon: string) => {
     if (icon) {
     if (icon) {
-      return <Icon src={icon} />
+      return <Icon src={icon} />;
     }
     }
 
 
     return (
     return (
-      <Polymer><i className="material-icons">layers</i></Polymer>
+      <Polymer>
+        <i className="material-icons">layers</i>
+      </Polymer>
     );
     );
-  }
+  };
 
 
   renderTagList = () => {
   renderTagList = () => {
     if (this.props.keywords) {
     if (this.props.keywords) {
       return this.props.keywords.map((tag: string, i: number) => {
       return this.props.keywords.map((tag: string, i: number) => {
-        return (
-          <Tag key={i}>{tag}</Tag>
-        )
+        return <Tag key={i}>{tag}</Tag>;
       });
       });
     }
     }
-  }
+  };
 
 
   renderMarkdown = () => {
   renderMarkdown = () => {
     let { currentTemplate, markdown } = this.props;
     let { currentTemplate, markdown } = this.props;
     if (markdown) {
     if (markdown) {
-      return (
-        <Markdown>{markdown}</Markdown>
-      );
+      return <Markdown>{markdown}</Markdown>;
     }
     }
     return currentTemplate.description;
     return currentTemplate.description;
-  }
-
+  };
 
 
   renderTagSection = () => {
   renderTagSection = () => {
-
     // Rendering doesn't make sense until search + clicking on tags is supported
     // Rendering doesn't make sense until search + clicking on tags is supported
     if (false && this.props.keywords && this.props.keywords.length > 0) {
     if (false && this.props.keywords && this.props.keywords.length > 0) {
       return (
       return (
@@ -65,7 +59,7 @@ export default class TemplateInfo extends Component<PropsType, StateType> {
         </TagSection>
         </TagSection>
       );
       );
     }
     }
-  }
+  };
 
 
   renderBanner = () => {
   renderBanner = () => {
     let { currentCluster } = this.context;
     let { currentCluster } = this.context;
@@ -79,40 +73,44 @@ export default class TemplateInfo extends Component<PropsType, StateType> {
           </Banner>
           </Banner>
         </>
         </>
       );
       );
-    } else if (this.props.currentTemplate.name.toLowerCase() === 'docker') {
+    } else if (this.props.currentTemplate.name.toLowerCase() === "docker") {
       return (
       return (
         <>
         <>
           <Br />
           <Br />
           <Banner>
           <Banner>
             <i className="material-icons-outlined">info</i>
             <i className="material-icons-outlined">info</i>
-            For instructions on connecting to your registry 
-            <Link 
+            For instructions on connecting to your registry
+            <Link
               target="_blank"
               target="_blank"
               href="https://docs.getporter.dev/docs/cli-documentation#pushing-docker-images-to-your-porter-image-registry"
               href="https://docs.getporter.dev/docs/cli-documentation#pushing-docker-images-to-your-porter-image-registry"
             >
             >
               refer to our docs
               refer to our docs
-            </Link>.
+            </Link>
+            .
           </Banner>
           </Banner>
         </>
         </>
       );
       );
-    } else if (this.props.currentTemplate.name.toLowerCase() === 'https-issuer') {
+    } else if (
+      this.props.currentTemplate.name.toLowerCase() === "https-issuer"
+    ) {
       return (
       return (
         <>
         <>
           <Br />
           <Br />
           <Banner>
           <Banner>
             <i className="material-icons-outlined">info</i>
             <i className="material-icons-outlined">info</i>
             To use this template you must first follow
             To use this template you must first follow
-            <Link 
+            <Link
               target="_blank"
               target="_blank"
               href="https://docs.getporter.dev/docs/https-and-custom-domains"
               href="https://docs.getporter.dev/docs/https-and-custom-domains"
             >
             >
               Porter's HTTPS setup guide
               Porter's HTTPS setup guide
-            </Link> (5 minutes).
+            </Link>{" "}
+            (5 minutes).
           </Banner>
           </Banner>
         </>
         </>
       );
       );
     }
     }
-  }
+  };
 
 
   render() {
   render() {
     let { currentCluster } = this.context;
     let { currentCluster } = this.context;
@@ -127,10 +125,15 @@ export default class TemplateInfo extends Component<PropsType, StateType> {
       <StyledExpandedTemplate>
       <StyledExpandedTemplate>
         <TitleSection>
         <TitleSection>
           <Flex>
           <Flex>
-            <i className="material-icons" onClick={() => this.props.setCurrentTemplate(null)}>
+            <i
+              className="material-icons"
+              onClick={() => this.props.setCurrentTemplate(null)}
+            >
               keyboard_backspace
               keyboard_backspace
             </i>
             </i>
-            {icon ? this.renderIcon(icon) : this.renderIcon(currentTemplate.icon)}
+            {icon
+              ? this.renderIcon(icon)
+              : this.renderIcon(currentTemplate.icon)}
             <Title>{name}</Title>
             <Title>{name}</Title>
           </Flex>
           </Flex>
           <Button
           <Button
@@ -145,9 +148,7 @@ export default class TemplateInfo extends Component<PropsType, StateType> {
         {this.renderTagSection()}
         {this.renderTagSection()}
         <LineBreak />
         <LineBreak />
         {this.renderBanner()}
         {this.renderBanner()}
-        <ContentSection>
-          {this.renderMarkdown()}
-        </ContentSection>
+        <ContentSection>{this.renderMarkdown()}</ContentSection>
       </StyledExpandedTemplate>
       </StyledExpandedTemplate>
     );
     );
   }
   }
@@ -211,7 +212,7 @@ const TagSection = styled.div`
   margin-top: 25px;
   margin-top: 25px;
   display: flex;
   display: flex;
   font-size: 13px;
   font-size: 13px;
-  font-family: 'Work Sans', sans-serif;
+  font-family: "Work Sans", sans-serif;
   align-items: center;
   align-items: center;
 
 
   > i {
   > i {
@@ -239,16 +240,19 @@ const Flex = styled.div`
 
 
 const Button = styled.div`
 const Button = styled.div`
   height: 35px;
   height: 35px;
-  background: ${(props: { isDisabled: boolean }) => (!props.isDisabled ? '#616feecc' : '#aaaabb')};
+  background: ${(props: { isDisabled: boolean }) =>
+    !props.isDisabled ? "#616feecc" : "#aaaabb"};
   :hover {
   :hover {
-    background: ${(props: { isDisabled: boolean }) => (!props.isDisabled ? '#505edddd' : '#aaaabb')};
+    background: ${(props: { isDisabled: boolean }) =>
+      !props.isDisabled ? "#505edddd" : "#aaaabb"};
   }
   }
   color: white;
   color: white;
   font-weight: 500;
   font-weight: 500;
   font-size: 13px;
   font-size: 13px;
   padding: 10px 15px;
   padding: 10px 15px;
   border-radius: 3px;
   border-radius: 3px;
-  cursor: ${(props: { isDisabled: boolean }) => (!props.isDisabled ? 'pointer' : 'default')};
+  cursor: ${(props: { isDisabled: boolean }) =>
+    !props.isDisabled ? "pointer" : "default"};
   box-shadow: 0 5px 8px 0px #00000010;
   box-shadow: 0 5px 8px 0px #00000010;
   display: flex;
   display: flex;
   flex-direction: row;
   flex-direction: row;
@@ -271,12 +275,11 @@ const Icon = styled.img`
   margin-bottom: -1px;
   margin-bottom: -1px;
 `;
 `;
 
 
-
 const Polymer = styled.div`
 const Polymer = styled.div`
   margin-bottom: -3px;
   margin-bottom: -3px;
 
 
   > i {
   > i {
-    color: ${props => props.theme.containerIcon};
+    color: ${(props) => props.theme.containerIcon};
     font-size: 24px;
     font-size: 24px;
     margin-left: 12px;
     margin-left: 12px;
     margin-right: 3px;
     margin-right: 3px;
@@ -286,7 +289,7 @@ const Polymer = styled.div`
 const Title = styled.div`
 const Title = styled.div`
   font-size: 24px;
   font-size: 24px;
   font-weight: 600;
   font-weight: 600;
-  font-family: 'Work Sans', sans-serif;
+  font-family: "Work Sans", sans-serif;
   margin-left: 10px;
   margin-left: 10px;
   border-radius: 2px;
   border-radius: 2px;
   color: #ffffff;
   color: #ffffff;
@@ -304,4 +307,4 @@ const TitleSection = styled.div`
 
 
 const StyledExpandedTemplate = styled.div`
 const StyledExpandedTemplate = styled.div`
   width: 100%;
   width: 100%;
-`;
+`;

+ 10 - 10
dashboard/src/main/home/templates/hardcodedNameDict.tsx

@@ -1,12 +1,12 @@
-const hardcodedNames: any = {
-  'docker': 'Docker',
-  'https-issuer': 'HTTPS Issuer',
-  'metabase': 'Metabase',
-  'mongodb': 'MongoDB',
-  'mysql': 'MySQL',
-  'postgresql': 'PostgreSQL',
-  'redis': 'Redis',
-  'ubuntu': 'Ubuntu',
+const hardcodedNames: { [key: string]: string } = {
+  docker: "Docker",
+  "https-issuer": "HTTPS Issuer",
+  metabase: "Metabase",
+  mongodb: "MongoDB",
+  mysql: "MySQL",
+  postgresql: "PostgreSQL",
+  redis: "Redis",
+  ubuntu: "Ubuntu",
 };
 };
 
 
-export default hardcodedNames;
+export default hardcodedNames;

Некоторые файлы не были показаны из-за большого количества измененных файлов