Răsfoiți Sursa

prettier final

Jo Chuang 5 ani în urmă
părinte
comite
6ca5648bdc
91 a modificat fișierele cu 6213 adăugiri și 5132 ștergeri
  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> {
-  state = {
-  }
+  state = {};
 
   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 = {
-  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> {
   render() {
@@ -18,16 +17,8 @@ export default class ConfirmOverlay extends Component<PropsType, StateType> {
         <StyledConfirmOverlay>
           {this.props.message}
           <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>
         </StyledConfirmOverlay>
       );
@@ -48,19 +39,23 @@ const StyledConfirmOverlay = styled.div`
   padding-bottom: 30px;
   align-items: center;
   justify-content: center;
-  font-family: 'Work Sans', sans-serif;
+  font-family: "Work Sans", sans-serif;
   font-size: 18px;
   font-weight: 500;
   color: white;
   flex-direction: column;
-  background: rgb(0,0,0,0.73);
+  background: rgb(0, 0, 0, 0.73);
   opacity: 0;
   animation: lindEnter 0.2s;
   animation-fill-mode: forwards;
 
   @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`
   font-size: 18px;
   padding: 10px 15px;
-  outline: none; 
+  outline: none;
   border: 1px solid white;
-  border-radius: 10px; 
-  text-align: center; 
+  border-radius: 10px;
+  text-align: center;
   width: 80px;
   cursor: pointer;
   opacity: 0;
-  font-family: 'Work Sans', sans-serif;
+  font-family: "Work Sans", sans-serif;
   font-size: 18px;
   font-weight: 500;
   animation: linEnter 0.3s 0.1s;
   animation-fill-mode: forwards;
   @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 {
     background: white;
     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 = {
-  resource: any,
-  handleClick?: () => void,
-  selected?: boolean,
-  isLast?: boolean,
-  roundAllCorners?: boolean,
+  resource: any;
+  handleClick?: () => void;
+  selected?: boolean;
+  isLast?: boolean;
+  roundAllCorners?: boolean;
 };
 
 type StateType = any;
 
-export default class ExpandableResource extends Component<PropsType, StateType> {
+export default class ExpandableResource extends Component<
+  PropsType,
+  StateType
+> {
   render() {
     let { resource } = this.props;
     return (
@@ -34,16 +36,16 @@ export default class ExpandableResource extends Component<PropsType, StateType>
             </StatusHeader>
             {resource.message}
           </StatusSection>
-          {
-            Object.keys(this.props.resource.data).map((key: string, i: number) => {
+          {Object.keys(this.props.resource.data).map(
+            (key: string, i: number) => {
               return (
                 <Pair key={i}>
-                  <Key>{key}:</Key> 
+                  <Key>{key}:</Key>
                   {this.props.resource.data[key]}
                 </Pair>
-              )
-            })
-          }
+              );
+            }
+          )}
         </ExpandedWrapper>
       </ResourceTab>
     );
@@ -81,7 +83,7 @@ const ExpandedWrapper = styled.div`
   padding: 20px 20px 25px;
 `;
 
-const Pair = styled.div`  
+const Pair = styled.div`
   margin-top: 20px;
   font-size: 13px;
   padding: 0 5px;
@@ -94,4 +96,4 @@ const Key = styled.div`
   font-weight: bold;
   color: #ffffff;
   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 = {
-  offset?: string
+  offset?: string;
 };
 
-type StateType = {
-};
+type StateType = {};
 
 export default class Loading extends Component<PropsType, StateType> {
-  state = {
-  }
+  state = {};
 
   render() {
     return (
@@ -33,4 +31,4 @@ const StyledLoading = styled.div`
   align-items: center;
   justify-content: center;
   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 = {
-  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?: {
-    label: string,
-    available?: number,
-    total?: number,
-  } | null
-  expanded?: boolean,
+    label: string;
+    available?: number;
+    total?: number;
+  } | null;
+  expanded?: boolean;
 };
 
 type StateType = {
-  expanded: boolean,
-  showTooltip: boolean,
+  expanded: boolean;
+  showTooltip: boolean;
 };
 
 export default class ResourceTab extends Component<PropsType, StateType> {
   state = {
     expanded: this.props.expanded || false,
     showTooltip: false,
-  }
+  };
 
   renderDropdownIcon = () => {
     if (this.props.children) {
@@ -37,28 +37,26 @@ export default class ResourceTab extends Component<PropsType, StateType> {
         </DropdownIcon>
       );
     }
-  }
+  };
 
   renderIcon = (kind: string) => {
-    let icon = 'tonality';
+    let icon = "tonality";
     if (Object.keys(kindToIcon).includes(kind)) {
-      icon = kindToIcon[kind]; 
+      icon = kindToIcon[kind];
     }
-    
+
     return (
       <IconWrapper>
         <i className="material-icons">{icon}</i>
       </IconWrapper>
     );
-  }
+  };
 
   renderTooltip = (x: string): JSX.Element | undefined => {
     if (this.state.showTooltip) {
-      return (
-        <Tooltip>{x}</Tooltip>
-      );
+      return <Tooltip>{x}</Tooltip>;
     }
-  }
+  };
 
   getStatusText = () => {
     let { status } = this.props;
@@ -67,7 +65,7 @@ export default class ResourceTab extends Component<PropsType, StateType> {
     } else if (status.label) {
       return status.label;
     }
-  }
+  };
 
   renderStatus = () => {
     let { status } = this.props;
@@ -80,20 +78,16 @@ export default class ResourceTab extends Component<PropsType, StateType> {
         </Status>
       );
     }
-  }
+  };
 
   renderExpanded = () => {
     if (this.props.children && this.state.expanded) {
-      return (
-        <ExpandWrapper>
-          {this.props.children}
-        </ExpandWrapper>
-      );
+      return <ExpandWrapper>{this.props.children}</ExpandWrapper>;
     }
-  }
+  };
 
   render() {
-    let { 
+    let {
       label,
       name,
       children,
@@ -104,7 +98,7 @@ export default class ResourceTab extends Component<PropsType, StateType> {
       roundAllCorners,
     } = this.props;
     return (
-      <StyledResourceTab 
+      <StyledResourceTab
         isLast={isLast}
         onClick={() => handleClick && handleClick()}
         roundAllCorners={roundAllCorners}
@@ -125,8 +119,12 @@ export default class ResourceTab extends Component<PropsType, StateType> {
               {label}
               <ResourceName
                 showKindLabels={true}
-                onMouseOver={() => { this.setState({ showTooltip: true }) }}
-                onMouseOut={() => { this.setState({ showTooltip: false }) }}
+                onMouseOver={() => {
+                  this.setState({ showTooltip: true });
+                }}
+                onMouseOut={() => {
+                  this.setState({ showTooltip: false });
+                }}
               >
                 {name}
               </ResourceName>
@@ -145,8 +143,14 @@ const StyledResourceTab = styled.div`
   width: 100%;
   margin-bottom: 2px;
   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`
@@ -170,13 +174,16 @@ const Tooltip = styled.div`
   animation: faded-in 0.2s 0.15s;
   animation-fill-mode: forwards;
   @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`
   width: 100%;
@@ -188,9 +195,11 @@ const ResourceHeader = styled.div`
   color: #ffffff66;
   user-select: none;
   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;
-  background: ${(props: { expanded: boolean, hasChildren: boolean }) => props.expanded ? '#ffffff11' : ''};
+  background: ${(props: { expanded: boolean; hasChildren: boolean }) =>
+    props.expanded ? "#ffffff11" : ""};
   :hover {
     background: #ffffff18;
 
@@ -212,7 +221,8 @@ const Metadata = styled.div`
   display: flex;
   align-items: center;
   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`
@@ -236,13 +246,21 @@ const StatusColor = styled.div`
   width: 8px;
   min-width: 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;
 `;
 
 const ResourceName = styled.div`
   color: #ffffff;
-  margin-left: ${(props: { showKindLabels: boolean }) => props.showKindLabels ? '10px' : ''};
+  margin-left: ${(props: { showKindLabels: boolean }) =>
+    props.showKindLabels ? "10px" : ""};
   text-transform: none;
   white-space: nowrap;
   overflow: hidden;
@@ -271,14 +289,21 @@ const DropdownIcon = styled.div`
     color: #ffffff66;
     cursor: pointer;
     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;
 
     @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 = {
-  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
-  makeFlush?: boolean 
+  makeFlush?: boolean;
 };
 
-type StateType = {
-};
+type StateType = {};
 
 export default class SaveButton extends Component<PropsType, StateType> {
-
   renderStatus = () => {
     if (this.props.status) {
-      if (this.props.status === 'successful') {
+      if (this.props.status === "successful") {
         return (
           <StatusWrapper successful={true}>
             <i className="material-icons">done</i> Successfully updated
           </StatusWrapper>
         );
-      } else if (this.props.status === 'loading') {
+      } else if (this.props.status === "loading") {
         return (
           <StatusWrapper successful={false}>
             <LoadingGif src={loading} /> Updating . . .
           </StatusWrapper>
         );
-      } else if (this.props.status === 'error') {
+      } else if (this.props.status === "error") {
         return (
           <StatusWrapper successful={false}>
             <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>
       );
     }
-  }
+  };
 
   render() {
     return (
       <ButtonWrapper makeFlush={this.props.makeFlush}>
         {this.renderStatus()}
-        <Button 
+        <Button
           disabled={this.props.disabled}
           onClick={this.props.onClick}
-          color={this.props.color || '#616FEEcc'}
+          color={this.props.color || "#616FEEcc"}
         >
           {this.props.text}
         </Button>
@@ -79,7 +77,7 @@ const LoadingGif = styled.img`
 const StatusWrapper = styled.div`
   display: flex;
   align-items: center;
-  font-family: 'Work Sans', sans-serif;
+  font-family: "Work Sans", sans-serif;
   font-size: 13px;
   color: #ffffff55;
   margin-right: 25px;
@@ -87,7 +85,8 @@ const StatusWrapper = styled.div`
   > i {
     font-size: 18px;
     margin-right: 10px;
-    color: ${(props: { successful: boolean }) => props.successful ? '#4797ff' : '#fcba03'};
+    color: ${(props: { successful: boolean }) =>
+      props.successful ? "#4797ff" : "#fcba03"};
   }
 
   animation: statusFloatIn 0.5s;
@@ -95,10 +94,12 @@ const StatusWrapper = styled.div`
 
   @keyframes statusFloatIn {
     from {
-      opacity: 0; transform: translateY(10px);
+      opacity: 0;
+      transform: translateY(10px);
     }
     to {
-      opacity: 1; transform: translateY(0px);
+      opacity: 1;
+      transform: translateY(0px);
     }
   }
 `;
@@ -109,35 +110,37 @@ const ButtonWrapper = styled.div`
   position: absolute;
   ${(props: { makeFlush: boolean }) => {
     if (!props.makeFlush) {
-      return (`
+      return `
         bottom: 25px;
         right: 27px;
-      `);
-    } 
-    return (`
+      `;
+    }
+    return `
       bottom: 0;
       right: 0;
-    `);
+    `;
   }}
-
 `;
 
 const Button = styled.button`
   height: 35px;
   font-size: 13px;
   font-weight: 500;
-  font-family: 'Work Sans', sans-serif;
+  font-family: "Work Sans", sans-serif;
   color: white;
   padding: 6px 20px 7px 20px;
   text-align: left;
   border: 0;
   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;
-  :focus { outline: 0 }
+  :focus {
+    outline: 0;
+  }
   :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 = {
-  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> {
   state = {
-    expanded: false
-  }
+    expanded: false,
+  };
 
   wrapperRef: any = React.createRef();
   parentRef: any = React.createRef();
 
   componentDidMount() {
-    document.addEventListener('mousedown', this.handleClickOutside.bind(this));
+    document.addEventListener("mousedown", this.handleClickOutside.bind(this));
   }
 
   componentWillUnmount() {
-    document.removeEventListener('mousedown', this.handleClickOutside.bind(this));
+    document.removeEventListener(
+      "mousedown",
+      this.handleClickOutside.bind(this)
+    );
   }
 
   handleClickOutside = (event: any) => {
     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.closeOverlay ? null : this.setState({ expanded: false });
-  }
+  };
 
   renderOptionList = () => {
     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 = () => {
-    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 = () => {
     if (this.state.expanded) {
       return (
         <Dropdown
           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}
           onClick={() => this.setState({ expanded: false })}
         >
           {this.renderDropdownLabel()}
           {this.renderOptionList()}
         </Dropdown>
-      )
+      );
     }
-  }
+  };
 
   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) {
       return tgt.label;
     }
-  }
+  };
 
   render() {
     let { activeValue } = this.props;
@@ -105,7 +117,7 @@ export default class Selector extends Component<PropsType, StateType> {
           height={this.props.height}
         >
           <TextWrap>
-            {activeValue === '' ? 'All' : this.getLabel(activeValue)}
+            {activeValue === "" ? "All" : this.getLabel(activeValue)}
           </TextWrap>
           <i className="material-icons">arrow_drop_down</i>
         </MainSelector>
@@ -129,10 +141,12 @@ const DropdownLabel = styled.div`
   margin: 10px 13px;
 `;
 
-const Option = styled.div` 
+const Option = styled.div`
   width: 100%;
   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;
   font-size: 13px;
   padding-top: 9px;
@@ -143,7 +157,8 @@ const Option = styled.div`
   white-space: nowrap;
   overflow: hidden;
   text-overflow: ellipsis;
-  background: ${(props: { selected: boolean, lastItem: boolean }) => props.selected ? '#ffffff11' : ''};
+  background: ${(props: { selected: boolean; lastItem: boolean }) =>
+    props.selected ? "#ffffff11" : ""};
 
   :hover {
     background: #ffffff22;
@@ -164,8 +179,10 @@ const Dropdown = styled.div`
   right: 0;
   top: calc(100% + 5px);
   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;
   z-index: 999;
   overflow-y: auto;
@@ -175,12 +192,14 @@ const Dropdown = styled.div`
 
 const StyledSelector = styled.div<{ width: string }>`
   position: relative;
-  width: ${props => props.width};
+  width: ${(props) => props.width};
 `;
 
 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;
   font-size: 13px;
   padding: 5px 10px;
@@ -190,13 +209,25 @@ const MainSelector = styled.div`
   align-items: center;
   justify-content: space-between;
   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 {
-    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 {
     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 = {
-    status: string,
-    controllers: Record<string, Record<string, any>>,
-    margin_left: string,
+  status: string;
+  controllers: Record<string, Record<string, any>>;
+  margin_left: string;
 };
 
 type StateType = {};
@@ -13,65 +13,70 @@ type StateType = {};
 // Manages a tab selector and renders the associated view
 export default class StatusIndicator extends Component<PropsType, StateType> {
   renderStatus = (status: string) => {
-    if (status == 'loading') {
+    if (status == "loading") {
       return (
         <div>
           <Spinner src={loading} />
         </div>
-      )
+      );
     }
 
     return (
       <div>
         <StatusColor status={status} />
       </div>
-    )
-  }
+    );
+  };
 
   getChartStatus = (chartStatus: string) => {
-    if (chartStatus === 'deployed') {
+    if (chartStatus === "deployed") {
       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) {
-          return 'loading'
+          return "loading";
         } else if (!available && !progressing) {
-          return 'failed'
+          return "failed";
         }
       }
-      return 'deployed'
+      return "deployed";
     }
-    return chartStatus
-  }
+    return chartStatus;
+  };
 
   getAvailability = (kind: string, c: any) => {
     switch (kind?.toLowerCase()) {
       case "deployment":
       case "replicaset":
-        return (c.status.availableReplicas == c.status.replicas)
+        return c.status.availableReplicas == c.status.replicas;
       case "statefulset":
-       return (c.status.readyReplicas == c.status.replicas)
+        return c.status.readyReplicas == c.status.replicas;
       case "daemonset":
-        return (c.status.numberAvailable == c.status.desiredNumberScheduled)
-      }
-  }
+        return c.status.numberAvailable == c.status.desiredNumberScheduled;
+    }
+  };
 
   render() {
-    let status = this.getChartStatus(this.props.status)
+    let status = this.getChartStatus(this.props.status);
     return (
-    <Status margin_left={this.props.margin_left}>
+      <Status margin_left={this.props.margin_left}>
         {this.renderStatus(status)}
         {status}
-    </Status>
+      </Status>
     );
   }
 }
@@ -87,7 +92,12 @@ const StatusColor = styled.div`
   margin-bottom: 1px;
   width: 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;
   margin-right: 16px;
 `;
@@ -99,13 +109,17 @@ const Status = styled.div`
   flex-direction: row;
   text-transform: capitalize;
   align-items: center;
-  font-family: 'Hind Siliguri', sans-serif;
+  font-family: "Hind Siliguri", sans-serif;
   color: #aaaabb;
   animation: fadeIn 0.5s;
-  margin-left: ${(props: { margin_left: string}) => props.margin_left};
+  margin-left: ${(props: { margin_left: string }) => props.margin_left};
 
   @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 = {
-  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
 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]) {
       this.props.setCurrentTab(this.props.options[0].value);
     }
-  }
+  };
 
   componentDidMount() {
     this.setDefaultTab();
@@ -31,7 +30,7 @@ export default class TabRegion extends Component<PropsType, StateType> {
   componentDidUpdate(prevProps: PropsType) {
     let { options, currentTab } = this.props;
     if (prevProps.options !== options) {
-      if (options.filter(x => x.value === currentTab).length === 0) {
+      if (options.filter((x) => x.value === currentTab).length === 0) {
         this.setDefaultTab();
       }
     }
@@ -39,9 +38,7 @@ export default class TabRegion extends Component<PropsType, StateType> {
 
   renderContents = () => {
     if (!this.props.currentTab) {
-      return (
-        <Loading />
-      );
+      return <Loading />;
     }
 
     return (
@@ -54,19 +51,13 @@ export default class TabRegion extends Component<PropsType, StateType> {
           addendum={this.props.addendum}
         />
         <Gap />
-        <TabContents>
-          {this.props.children}
-        </TabContents>
+        <TabContents>{this.props.children}</TabContents>
       </Div>
     );
-  }
+  };
 
   render() {
-    return (
-      <StyledTabRegion>
-        {this.renderContents()}
-      </StyledTabRegion>
-    );
+    return <StyledTabRegion>{this.renderContents()}</StyledTabRegion>;
   }
 }
 
@@ -103,4 +94,4 @@ const StyledTabRegion = styled.div`
   height: 100%;
   position: relative;
   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 {
-  value: string,
-  label: string
+  value: string;
+  label: string;
 }
 
 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> {
   handleTabClick = (value: string) => {
     this.props.setCurrentTab(value);
-  }
+  };
 
   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() {
     return (
       <StyledTabSelector>
         <TabWrapper>
           {this.renderTabList()}
-          <Tab
-            lastItem={true}
-            highlight={null}
-          >
+          <Tab lastItem={true} highlight={null}>
             <Buffer />
           </Tab>
         </TabWrapper>
@@ -77,12 +69,14 @@ const TabWrapper = styled.div`
 
 const Tab = styled.div`
   height: 30px;
-  margin-right: ${(props: { lastItem: boolean, highlight: string }) => props.lastItem ? '' : '30px'};
+  margin-right: ${(props: { lastItem: boolean; highlight: string }) =>
+    props.lastItem ? "" : "30px"};
   display: flex;
-  font-family: 'Work Sans', sans-serif;
+  font-family: "Work Sans", sans-serif;
   font-size: 13px;
   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;
   padding-top: 7px;
   padding-bottom: 2px;
@@ -90,9 +84,12 @@ const Tab = styled.div`
   align-items: center;
   cursor: pointer;
   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 {
-    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;
   margin-left: 2px;
   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 = {
-  tooltipText: string
+  tooltipText: string;
 };
 
 type StateType = {
-  showTooltip: boolean,
+  showTooltip: boolean;
 };
 
 export default class TooltipParent extends Component<PropsType, StateType> {
   state = {
     showTooltip: false,
-  }
+  };
 
   renderTooltip = (): JSX.Element | undefined => {
     if (this.state.showTooltip) {
-      return (
-        <Tooltip>{this.props.tooltipText}</Tooltip>
-      );
+      return <Tooltip>{this.props.tooltipText}</Tooltip>;
     }
-  }
+  };
 
   render() {
     return (
       <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.renderTooltip()}
@@ -54,11 +56,15 @@ const Tooltip = styled.div`
   animation: faded-in 0.2s 0.15s;
   animation-fill-mode: forwards;
   @keyframes faded-in {
-    from { opacity: 0 }
-    to { opacity: 1 }
+    from {
+      opacity: 0;
+    }
+    to {
+      opacity: 1;
+    }
   }
 `;
 
 const StyledTooltipParent = styled.div`
   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 = {
-  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> {
-
   // 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.
   checkYaml = () => {
@@ -28,36 +26,33 @@ class YamlEditor extends Component<PropsType, StateType> {
       alert(error.message);
     });
     */
-  }
+  };
 
   // Calls checkYaml and passes in the value from the textarea
   handleChange = (e: any) => {
     this.setState({ yaml: e });
-  }
+  };
 
   handleSubmit = (e: any) => {
     this.checkYaml();
     e.preventDefault();
-  }
+  };
 
   render() {
     return (
       <Holder>
-        <Editor
-          onSubmit={this.handleSubmit}
-          border={this.props.border}
-        >
+        <Editor onSubmit={this.handleSubmit} border={this.props.border}>
           <AceEditor
-            mode='yaml'
+            mode="yaml"
             value={this.props.value}
-            theme='terminal'
+            theme="terminal"
             onChange={this.props.onChange}
-            name='codeEditor'
+            name="codeEditor"
             readOnly={this.props.readOnly}
             editorProps={{ $blockScrolling: true }}
             height={this.props.height}
-            width='100%'
-            style={{ borderRadius: '5px' }}
+            width="100%"
+            style={{ borderRadius: "5px" }}
           />
         </Editor>
       </Holder>
@@ -68,18 +63,21 @@ class YamlEditor extends Component<PropsType, StateType> {
 export default YamlEditor;
 
 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`
   .ace_scrollbar {
     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-weight: 400 !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 = {
-  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> {
   state = {
-    name: '',
+    name: "",
     excludeNamespaces: [] as string[],
     excludeResources: [] as string[],
     includeNamespaces: [] as string[],
     includeResources: [] as string[],
-    storageLocation: '',
+    storageLocation: "",
     volumeSnapshotLocations: [] as string[],
-  }
+  };
 
   render() {
     return (
       <>
         <Heading>Create a Bakup</Heading>
         <InputRow
-          placeholder='ex: my-backup'
-          type='text'
-          width='300px'
+          placeholder="ex: my-backup"
+          type="text"
+          width="300px"
           value={this.state.name}
           setValue={(x: string) => this.setState({ name: x })}
-          label='Name'
+          label="Name"
         />
         <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 = {
-  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 = {
-  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> {
@@ -34,75 +34,89 @@ export default class ImageSelector extends Component<PropsType, StateType> {
     error: false,
     images: [] as ImageType[],
     clickedImage: null as ImageType | null,
-  }
+  };
 
   componentDidMount() {
     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,
                         source: img.uri,
                         name: img.name,
                         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 = () => {
     let { images, loading, error } = this.state;
     if (loading) {
-      return <LoadingWrapper><Loading /></LoadingWrapper>
-    } else if (error || !images) {
-      return <LoadingWrapper>Error loading repos</LoadingWrapper>
-    } else if (images.length === 0) {
       return (
         <LoadingWrapper>
-          No registries found. 
+          <Loading />
         </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) => {
-      let icon = integrationList[image.kind] && integrationList[image.kind].icon;
+      let icon =
+        integrationList[image.kind] && integrationList[image.kind].icon;
       if (!icon) {
-        icon = integrationList['docker'].icon;
+        icon = integrationList["docker"].icon;
       }
       return (
         <ImageItem
           key={i}
           isSelected={image.source === this.props.selectedImageUrl}
           lastItem={i === images.length - 1}
-          onClick={() => { 
+          onClick={() => {
             this.props.setSelectedImageUrl(image.source);
             this.setState({ clickedImage: image });
           }}
         >
-          <img src={icon && icon} />{image.source}
+          <img src={icon && icon} />
+          {image.source}
         </ImageItem>
       );
     });
-  }
+  };
 
   renderBackButton = () => {
     let { setSelectedImageUrl } = this.props;
     if (this.state.clickedImage) {
       return (
         <BackButton
-          width='175px'
+          width="175px"
           onClick={() => {
-            setSelectedImageUrl('');
+            setSelectedImageUrl("");
             this.setState({ clickedImage: null });
           }}
         >
@@ -161,16 +177,14 @@ export default class ImageSelector extends Component<PropsType, StateType> {
         </BackButton>
       );
     }
-  }
+  };
 
   renderExpanded = () => {
     let { selectedTag, selectedImageUrl, setSelectedTag } = this.props;
     if (!this.state.clickedImage) {
       return (
         <div>
-          <ExpandedWrapper>
-            {this.renderImageList()}
-          </ExpandedWrapper>
+          <ExpandedWrapper>{this.renderImageList()}</ExpandedWrapper>
           {this.renderBackButton()}
         </div>
       );
@@ -189,7 +203,7 @@ export default class ImageSelector extends Component<PropsType, StateType> {
         </div>
       );
     }
-  }
+  };
 
   renderSelected = () => {
     let { selectedImageUrl, setSelectedImageUrl } = this.props;
@@ -197,11 +211,13 @@ export default class ImageSelector extends Component<PropsType, StateType> {
     let icon = info;
     if (clickedImage) {
       icon = clickedImage.kind;
-      icon = integrationList[clickedImage.kind] && integrationList[clickedImage.kind].icon;
+      icon =
+        integrationList[clickedImage.kind] &&
+        integrationList[clickedImage.kind].icon;
       if (!icon) {
-        icon = integrationList['docker'].icon;
+        icon = integrationList["docker"].icon;
       }
-    } else if (selectedImageUrl && selectedImageUrl !== '') {
+    } else if (selectedImageUrl && selectedImageUrl !== "") {
       icon = edit;
     }
     return (
@@ -210,21 +226,21 @@ export default class ImageSelector extends Component<PropsType, StateType> {
         <Input
           onClick={(e: any) => e.stopPropagation()}
           value={selectedImageUrl}
-          onChange={(e: any) => { 
-            setSelectedImageUrl(e.target.value); 
+          onChange={(e: any) => {
+            setSelectedImageUrl(e.target.value);
             this.setState({ clickedImage: null });
           }}
-          placeholder='Enter or select your container image URL'
+          placeholder="Enter or select your container image URL"
         />
       </Label>
     );
-  }
+  };
 
   handleClick = () => {
     if (!this.props.forceExpanded) {
       this.setState({ isExpanded: !this.state.isExpanded });
     }
-  }
+  };
 
   render() {
     return (
@@ -235,7 +251,11 @@ export default class ImageSelector extends Component<PropsType, StateType> {
           forceExpanded={this.props.forceExpanded}
         >
           {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>
 
         {this.state.isExpanded ? this.renderExpanded() : null}
@@ -292,13 +312,16 @@ const ImageItem = styled.div`
   display: flex;
   width: 100%;
   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;
   user-select: none;
   align-items: center;
   padding: 10px 0px;
   cursor: pointer;
-  background: ${(props: { isSelected: boolean, lastItem: boolean }) => props.isSelected ? '#ffffff11' : ''};
+  background: ${(props: { isSelected: boolean; lastItem: boolean }) =>
+    props.isSelected ? "#ffffff11" : ""};
   :hover {
     background: #ffffff22;
 
@@ -352,7 +375,8 @@ const StyledImageSelector = styled.div`
   width: 100%;
   margin-top: 22px;
   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;
   user-select: none;
   height: 40px;
@@ -361,7 +385,8 @@ const StyledImageSelector = styled.div`
   display: flex;
   align-items: center;
   justify-content: space-between;
-  cursor: ${(props: { isExpanded: boolean, forceExpanded: boolean }) => props.forceExpanded ? '' : 'pointer'};
+  cursor: ${(props: { isExpanded: boolean; forceExpanded: boolean }) =>
+    props.forceExpanded ? "" : "pointer"};
   :hover {
     background: #ffffff11;
 
@@ -380,4 +405,4 @@ const StyledImageSelector = styled.div`
     border-radius: 20px;
     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 = {
-  setSelectedTag: (x: string) => void,
-  selectedTag: string,
-  selectedImageUrl: string,
-  registryId: number,
+  setSelectedTag: (x: string) => void;
+  selectedTag: string;
+  selectedImageUrl: string;
+  registryId: number;
 };
 
 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> {
@@ -28,42 +28,50 @@ export default class TagList extends Component<PropsType, StateType> {
     error: false,
     tags: [] as string[],
     currentTag: this.props.selectedTag,
-  }
+  };
 
   componentDidMount() {
     const { currentProject } = this.context;
-    let splits = this.props.selectedImageUrl.split('/');
+    let splits = this.props.selectedImageUrl.split("/");
     let repoName = splits[splits.length - 1];
-    api.getImageTags('<token>', {}, 
-      { 
+    api.getImageTags(
+      "<token>",
+      {},
+      {
         project_id: currentProject.id,
         registry_id: this.props.registryId,
         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) => {
     let { selectedTag, setSelectedTag } = this.props;
     setSelectedTag(tag);
     this.setState({ currentTag: tag });
-  }
+  };
 
   renderTagList = () => {
     let { tags, loading, error } = this.state;
     if (loading) {
-      return <LoadingWrapper><Loading /></LoadingWrapper>
+      return (
+        <LoadingWrapper>
+          <Loading />
+        </LoadingWrapper>
+      );
     } else if (error || !tags) {
-      return <LoadingWrapper>Error loading tags</LoadingWrapper>
+      return <LoadingWrapper>Error loading tags</LoadingWrapper>;
     }
 
     return tags.map((tag: string, i: number) => {
@@ -74,21 +82,20 @@ export default class TagList extends Component<PropsType, StateType> {
           lastItem={i === tags.length - 1}
           onClick={() => this.setTag(tag)}
         >
-          <img src={tag_icon} />{tag}
+          <img src={tag_icon} />
+          {tag}
         </TagName>
       );
     });
-  }
+  };
 
   render() {
     return (
-<>
+      <>
         <TagNameAlt>
           <img src={info} /> Select Image Tag
         </TagNameAlt>
-              <StyledTagList>
-        {this.renderTagList()}
-      </StyledTagList>
+        <StyledTagList>{this.renderTagList()}</StyledTagList>
       </>
     );
   }
@@ -106,13 +113,16 @@ const TagName = styled.div`
   display: flex;
   width: 100%;
   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;
   user-select: none;
   align-items: center;
   padding: 10px 0px;
   cursor: pointer;
-  background: ${(props: { isSelected?: boolean, lastItem?: boolean }) => props.isSelected ? '#ffffff11' : ''};
+  background: ${(props: { isSelected?: boolean; lastItem?: boolean }) =>
+    props.isSelected ? "#ffffff11" : ""};
   :hover {
     background: #ffffff22;
 
@@ -147,4 +157,4 @@ const LoadingWrapper = styled.div`
   justify-content: center;
   font-size: 13px;
   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 = {
-  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 = {
-  loading: boolean,
-  error: boolean,
-  branches: string[]
+  loading: boolean;
+  error: boolean;
+  branches: string[];
 };
 
 export default class BranchList extends Component<PropsType, StateType> {
   state = {
     loading: true,
     error: false,
-    branches: [] as string[]
-  }
+    branches: [] as string[],
+  };
 
   componentDidMount() {
     let { currentProject } = this.context;
 
     // 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 = () => {
     let { branches, loading, error } = this.state;
     if (loading) {
-      return <LoadingWrapper><Loading /></LoadingWrapper>
+      return (
+        <LoadingWrapper>
+          <Loading />
+        </LoadingWrapper>
+      );
     } else if (error || !branches) {
-      return <LoadingWrapper>Error loading branches</LoadingWrapper>
+      return <LoadingWrapper>Error loading branches</LoadingWrapper>;
     }
 
     return branches.map((branch: string, i: number) => {
@@ -64,18 +73,15 @@ export default class BranchList extends Component<PropsType, StateType> {
           lastItem={i === branches.length - 1}
           onClick={() => this.props.setSelectedBranch(branch)}
         >
-          <img src={branch_icon} />{branch}
+          <img src={branch_icon} />
+          {branch}
         </BranchName>
       );
     });
-  }
+  };
 
   render() {
-    return (
-      <div>
-        {this.renderBranchList()}
-      </div>
-    );
+    return <div>{this.renderBranchList()}</div>;
   }
 }
 
@@ -85,13 +91,16 @@ const BranchName = styled.div`
   display: flex;
   width: 100%;
   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;
   user-select: none;
   align-items: center;
   padding: 10px 0px;
   cursor: pointer;
-  background: ${(props: { isSelected: boolean, lastItem: boolean }) => props.isSelected ? '#ffffff22' : '#ffffff11'};
+  background: ${(props: { isSelected: boolean; lastItem: boolean }) =>
+    props.isSelected ? "#ffffff22" : "#ffffff11"};
   :hover {
     background: #ffffff22;
 
@@ -116,4 +125,4 @@ const LoadingWrapper = styled.div`
   justify-content: center;
   font-size: 13px;
   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 = {
-  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 = {
-  loading: boolean,
-  error: boolean,
-  contents: FileType[]
+  loading: boolean;
+  error: boolean;
+  contents: FileType[];
 };
 
 export default class ContentsList extends Component<PropsType, StateType> {
   state = {
     loading: true,
     error: false,
-    contents: [] as FileType[]
-  }
+    contents: [] as FileType[],
+  };
 
   updateContents = () => {
     let { currentProject } = this.context;
 
     // 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() {
     this.updateContents();
@@ -71,22 +79,26 @@ export default class ContentsList extends Component<PropsType, StateType> {
 
   componentDidUpdate(prevProps: PropsType) {
     if (this.props.subdirectory !== prevProps.subdirectory) {
-      this.updateContents();  
+      this.updateContents();
     }
   }
 
   renderContentList = () => {
     let { contents, loading, error } = this.state;
     if (loading) {
-      return <LoadingWrapper><Loading /></LoadingWrapper>
+      return (
+        <LoadingWrapper>
+          <Loading />
+        </LoadingWrapper>
+      );
     } 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) => {
-      let splits = item.Path.split('/');
+      let splits = item.Path.split("/");
       let fileName = splits[splits.length - 1];
-      if (item.Type === 'dir') {
+      if (item.Type === "dir") {
         return (
           <Item
             key={i}
@@ -100,7 +112,7 @@ export default class ContentsList extends Component<PropsType, StateType> {
         );
       }
 
-      if (fileName === 'Dockerfile') {
+      if (fileName === "Dockerfile") {
         return (
           <FileItem
             key={i}
@@ -114,48 +126,40 @@ export default class ContentsList extends Component<PropsType, StateType> {
         );
       }
       return (
-        <FileItem
-          key={i}
-          lastItem={i === contents.length - 1}
-        >
+        <FileItem key={i} lastItem={i === contents.length - 1}>
           <img src={file} />
           {fileName}
         </FileItem>
       );
     });
-  }
+  };
 
   renderJumpToParent = () => {
     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) {
-        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);
         }
       }
 
       return (
-        <Item
-          lastItem={false}
-          onClick={() => setSubdirectory(subdir)}
-        >
+        <Item lastItem={false} onClick={() => setSubdirectory(subdir)}>
           <BackLabel>..</BackLabel>
         </Item>
       );
     }
 
     return (
-      <FileItem
-        lastItem={false}
-      >
+      <FileItem lastItem={false}>
         <img src={info} />
         Select subfolder (Optional)
       </FileItem>
     );
-  }
+  };
 
   render() {
     return (
@@ -180,13 +184,16 @@ const Item = styled.div`
   display: flex;
   width: 100%;
   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;
   user-select: none;
   align-items: center;
   padding: 10px 0px;
   cursor: pointer;
-  background: ${(props: { isSelected?: boolean, lastItem: boolean }) => props.isSelected ? '#ffffff22' : '#ffffff11'};
+  background: ${(props: { isSelected?: boolean; lastItem: boolean }) =>
+    props.isSelected ? "#ffffff22" : "#ffffff11"};
   :hover {
     background: #ffffff22;
 
@@ -204,10 +211,13 @@ const Item = styled.div`
 `;
 
 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 {
-    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;
   justify-content: center;
   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 = {
-  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 = {
-  trueDockerPath: string,
-  loading: boolean,
-  error: boolean,
+  trueDockerPath: string;
+  loading: boolean;
+  error: boolean;
 };
 
 export default class NewGHAction extends Component<PropsType, StateType> {
   state = {
-    dockerRepo: '',
+    dockerRepo: "",
     trueDockerPath: this.props.dockerPath,
     loading: false,
     error: false,
-  }
+  };
 
   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 = () => {
     let { loading } = this.state;
     if (loading) {
-      return <LoadingWrapper><Loading /></LoadingWrapper>
+      return (
+        <LoadingWrapper>
+          <Loading />
+        </LoadingWrapper>
+      );
     }
 
     return (
       <Holder>
         <InputRow
           disabled={true}
-          label='Git Repository'
-          type='text'
-          width='100%'
+          label="Git Repository"
+          type="text"
+          width="100%"
           value={this.props.repoName}
           setValue={(x: string) => console.log(x)}
         />
         <InputRow
           disabled={true}
-          label='Dockerfile Path'
-          type='text'
-          width='100%'
+          label="Dockerfile Path"
+          type="text"
+          width="100%"
           value={this.state.trueDockerPath}
           setValue={(x: string) => console.log(x)}
         />
         <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}
           setValue={(x: string) => this.props.setURL(x)}
         />
       </Holder>
-    )
-  }
+    );
+  };
 
   render() {
-    return (
-      <div>
-        {this.renderConfirmation()}
-      </div>
-    );
+    return <div>{this.renderConfirmation()}</div>;
   }
 }
 
@@ -96,4 +100,4 @@ const LoadingWrapper = styled.div`
   justify-content: center;
   font-size: 13px;
   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 = {
-  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 = {
-  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> {
@@ -42,73 +42,95 @@ export default class RepoSelector extends Component<PropsType, StateType> {
     branchGrID: null as number,
     dockerfileSelected: false,
     imageURL: null as string,
-  }
+  };
 
   componentDidMount() {
     let { currentProject } = this.context;
 
     // 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 = () => {
     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);
     }
 
-    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 = () => {
     let { repos, loading, error } = this.state;
     if (loading) {
-      return <LoadingWrapper><Loading /></LoadingWrapper>
+      return (
+        <LoadingWrapper>
+          <Loading />
+        </LoadingWrapper>
+      );
     } else if (error || !repos) {
-      return <LoadingWrapper>Error loading repos.</LoadingWrapper>
+      return <LoadingWrapper>Error loading repos.</LoadingWrapper>;
     } 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) => {
@@ -119,11 +141,12 @@ export default class RepoSelector extends Component<PropsType, StateType> {
           lastItem={i === repos.length - 1}
           onClick={() => this.props.setSelectedRepo(repo)}
         >
-          <img src={github} />{repo.FullName}
+          <img src={github} />
+          {repo.FullName}
         </RepoName>
       );
     });
-  }
+  };
 
   renderExpanded = () => {
     let {
@@ -132,16 +155,12 @@ export default class RepoSelector extends Component<PropsType, StateType> {
       subdirectory,
       setSelectedRepo,
       setSelectedBranch,
-      setSubdirectory
+      setSubdirectory,
     } = this.props;
 
     if (!selectedRepo) {
-      return (
-        <ExpandedWrapper>
-          {this.renderRepoList()}
-        </ExpandedWrapper>
-      );
-    } else if (selectedBranch === '') {
+      return <ExpandedWrapper>{this.renderRepoList()}</ExpandedWrapper>;
+    } else if (selectedBranch === "") {
       return (
         <div>
           <ExpandedWrapperAlt>
@@ -151,16 +170,13 @@ export default class RepoSelector extends Component<PropsType, StateType> {
                 this.setState({ branchGrID: selectedRepo.GHRepoID });
                 setSelectedBranch(branch);
               }}
-              repoName={selectedRepo.FullName.split('/')[1]}
-              owner={selectedRepo.FullName.split('/')[0]}
+              repoName={selectedRepo.FullName.split("/")[1]}
+              owner={selectedRepo.FullName.split("/")[0]}
               selectedBranch={selectedBranch}
             />
           </ExpandedWrapperAlt>
           <ButtonTray>
-            <BackButton
-              width='130px'
-              onClick={() => setSelectedRepo(null)}
-            >
+            <BackButton width="130px" onClick={() => setSelectedRepo(null)}>
               <i className="material-icons">keyboard_backspace</i>
               Select Repo
             </BackButton>
@@ -173,7 +189,7 @@ export default class RepoSelector extends Component<PropsType, StateType> {
           <ExpandedWrapperAlt>
             <NewGHAction
               repoName={selectedRepo.FullName}
-              dockerPath={subdirectory + '/Dockerfile'}
+              dockerPath={subdirectory + "/Dockerfile"}
               grid={this.state.branchGrID}
               chart={this.props.chart}
               imgURL={this.state.imageURL}
@@ -182,31 +198,30 @@ export default class RepoSelector extends Component<PropsType, StateType> {
           </ExpandedWrapperAlt>
           <ButtonTray>
             <BackButton
-              width='130px'
+              width="130px"
               onClick={() => this.setState({ dockerfileSelected: false })}
             >
-              <i className='material-icons'>keyboard_backspace</i>
+              <i className="material-icons">keyboard_backspace</i>
               Select Dockerfile
             </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
             </BackButton>
           </ButtonTray>
         </div>
-      )
+      );
     }
     return (
       <div>
         <ExpandedWrapperAlt>
           <ContentsList
             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}
             subdirectory={subdirectory}
             setDockerfile={() => this.setState({ dockerfileSelected: true })}
@@ -214,8 +229,12 @@ export default class RepoSelector extends Component<PropsType, StateType> {
         </ExpandedWrapperAlt>
         <ButtonTray>
           <BackButton
-            onClick={() => {setSelectedBranch(''); setSubdirectory(''); this.setState({ imageURL: '' })}}
-            width='140px'
+            onClick={() => {
+              setSelectedBranch("");
+              setSubdirectory("");
+              this.setState({ imageURL: "" });
+            }}
+            width="140px"
           >
             <i className="material-icons">keyboard_backspace</i>
             Select Branch
@@ -223,18 +242,18 @@ export default class RepoSelector extends Component<PropsType, StateType> {
         </ButtonTray>
       </div>
     );
-  }
+  };
 
   renderSelected = () => {
     let { selectedRepo, subdirectory, selectedBranch } = this.props;
     if (selectedRepo) {
-      let subdir = subdirectory === '' ? '' : '/' + subdirectory;
+      let subdir = subdirectory === "" ? "" : "/" + subdirectory;
       return (
         <RepoLabel>
           <img src={github} />
           {selectedRepo.FullName + subdir}
           <SelectedBranch>
-            {!selectedBranch ? '(Select Branch)' : selectedBranch}
+            {!selectedBranch ? "(Select Branch)" : selectedBranch}
           </SelectedBranch>
         </RepoLabel>
       );
@@ -245,13 +264,13 @@ export default class RepoSelector extends Component<PropsType, StateType> {
         No source selected
       </RepoLabel>
     );
-  }
+  };
 
   handleClick = () => {
     if (!this.props.forceExpanded) {
       this.setState({ isExpanded: !this.state.isExpanded });
     }
-  }
+  };
 
   render() {
     return (
@@ -262,7 +281,11 @@ export default class RepoSelector extends Component<PropsType, StateType> {
           forceExpanded={this.props.forceExpanded}
         >
           {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>
 
         {this.state.isExpanded ? this.renderExpanded() : null}
@@ -314,13 +337,16 @@ const RepoName = styled.div`
   display: flex;
   width: 100%;
   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;
   user-select: none;
   align-items: center;
   padding: 10px 0px;
   cursor: pointer;
-  background: ${(props: { isSelected: boolean, lastItem: boolean }) => props.isSelected ? '#ffffff22' : '#ffffff11'};
+  background: ${(props: { isSelected: boolean; lastItem: boolean }) =>
+    props.isSelected ? "#ffffff22" : "#ffffff11"};
   :hover {
     background: #ffffff22;
 
@@ -356,8 +382,7 @@ const ExpandedWrapper = styled.div`
   overflow-y: auto;
 `;
 
-const ExpandedWrapperAlt = styled(ExpandedWrapper)`
-`;
+const ExpandedWrapperAlt = styled(ExpandedWrapper)``;
 
 const RepoLabel = styled.div`
   display: flex;
@@ -375,7 +400,8 @@ const StyledRepoSelector = styled.div`
   width: 100%;
   margin-top: 22px;
   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;
   user-select: none;
   height: 40px;
@@ -384,7 +410,8 @@ const StyledRepoSelector = styled.div`
   display: flex;
   align-items: center;
   justify-content: space-between;
-  cursor: ${(props: { isExpanded: boolean, forceExpanded: boolean }) => props.forceExpanded ? '' : 'pointer'};
+  cursor: ${(props: { isExpanded: boolean; forceExpanded: boolean }) =>
+    props.forceExpanded ? "" : "pointer"};
   :hover {
     background: #ffffff11;
 
@@ -403,4 +430,4 @@ const StyledRepoSelector = styled.div`
     border-radius: 20px;
     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 = {
-  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 = {
-  readOnly: boolean
+  readOnly: boolean;
 };
 
 export default class InputRow extends Component<PropsType, StateType> {
   state = {
-    readOnly: true
-  }
+    readOnly: true,
+  };
 
   handleChange = (e: ChangeEvent<HTMLInputElement>) => {
     this.props.setValue(e.target.value);
-  }
-  
+  };
+
   render() {
     let { label, value, type, unit, placeholder, width } = this.props;
     value = value.toString();
     value = atob(value);
     return (
       <StyledInputRow>
-        <Label>{label} <Required>{this.props.isRequired ? ' *' : null}</Required></Label>
+        <Label>
+          {label} <Required>{this.props.isRequired ? " *" : null}</Required>
+        </Label>
         <InputWrapper>
           <Input
-            readOnly={this.state.readOnly} onFocus={() => this.setState({ readOnly: false })}
+            readOnly={this.state.readOnly}
+            onFocus={() => this.setState({ readOnly: false })}
             disabled={this.props.disabled}
             placeholder={placeholder}
             width={width}
@@ -72,8 +75,10 @@ const Input = styled.input`
   background: #ffffff11;
   border: 1px solid #ffffff55;
   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;
   margin-right: 8px;
   height: 30px;
@@ -85,10 +90,10 @@ const Label = styled.div`
   display: flex;
   align-items: center;
   font-size: 13px;
-  font-family: 'Work Sans', sans-serif;
+  font-family: "Work Sans", sans-serif;
 `;
 
 const StyledInputRow = styled.div`
   margin-bottom: 15px;
   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 = {
-  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)) {
       selected.push(option);
       setSelected(selected);
@@ -19,14 +17,14 @@ const CheckboxList = ({
       selected.splice(selected.indexOf(option), 1);
       setSelected(selected);
     }
-  }
-  
+  };
+
   return (
     <StyledCheckboxList>
       {label && <Label>{label}</Label>}
-      {options.map((option: { value: string, label: string }, i: number) => {
+      {options.map((option: { value: string; label: string }, i: number) => {
         return (
-          <CheckboxOption 
+          <CheckboxOption
             isLast={i === options.length - 1}
             onClick={() => onSelectOption(option)}
             key={i}
@@ -40,7 +38,7 @@ const CheckboxList = ({
       })}
     </StyledCheckboxList>
   );
-}
+};
 export default CheckboxList;
 
 const Checkbox = styled.div`
@@ -49,7 +47,8 @@ const Checkbox = styled.div`
   border: 1px solid #ffffff55;
   margin: 1px 15px 0px 1px;
   border-radius: 3px;
-  background: ${(props: { checked: boolean }) => props.checked ? '#ffffff22' : '#ffffff11'};
+  background: ${(props: { checked: boolean }) =>
+    props.checked ? "#ffffff22" : "#ffffff11"};
   display: flex;
   align-items: center;
   justify-content: center;
@@ -57,7 +56,7 @@ const Checkbox = styled.div`
   > i {
     font-size: 12px;
     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;
   cursor: pointer;
   align-items: center;
-  border-bottom: ${props => props.isLast ? '' : '1px solid #ffffff22'};
+  border-bottom: ${(props) => (props.isLast ? "" : "1px solid #ffffff22")};
   font-size: 13px;
 
   :hover {
@@ -88,4 +87,4 @@ const StyledCheckboxList = styled.div`
   background: #ffffff11;
   margin-bottom: 15px;
   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 = {
-  label: string,
-  checked: boolean,
-  toggle: () => void
+  label: string;
+  checked: boolean;
+  toggle: () => void;
 };
 
-type StateType = {
-};
+type StateType = {};
 
 export default class CheckboxRow extends Component<PropsType, StateType> {
   render() {
@@ -24,7 +23,7 @@ export default class CheckboxRow extends Component<PropsType, StateType> {
     );
   }
 }
-        
+
 const CheckboxWrapper = styled.div`
   display: flex;
   align-items: center;
@@ -42,7 +41,8 @@ const Checkbox = styled.div`
   border: 1px solid #ffffff55;
   margin: 1px 10px 0px 1px;
   border-radius: 3px;
-  background: ${(props: { checked: boolean }) => props.checked ? '#ffffff22' : '#ffffff11'};
+  background: ${(props: { checked: boolean }) =>
+    props.checked ? "#ffffff22" : "#ffffff11"};
   display: flex;
   align-items: center;
   justify-content: center;
@@ -50,7 +50,7 @@ const Checkbox = styled.div`
   > i {
     font-size: 12px;
     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;
   margin-bottom: 15px;
   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 }>`
   color: white;
   font-weight: 500;
   font-size: 16px;
-  margin-top: ${props => props.isAtTop ? '0': '30px'};
+  margin-top: ${(props) => (props.isAtTop ? "0" : "30px")};
   margin-bottom: 5px;
   display: flex;
   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 }) {
   return <StyledHelper>{props.children}</StyledHelper>;
@@ -13,4 +13,4 @@ const StyledHelper = styled.div`
   margin-top: 20px;
   display: flex;
   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 = {
-  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 = {
-  readOnly: boolean
+  readOnly: boolean;
 };
 
 export default class InputRow extends Component<PropsType, StateType> {
   state = {
-    readOnly: true
-  }
+    readOnly: true,
+  };
 
   handleChange = (e: ChangeEvent<HTMLInputElement>) => {
-    if (this.props.type === 'number') {
+    if (this.props.type === "number") {
       this.props.setValue(parseInt(e.target.value));
     } else {
       this.props.setValue(e.target.value);
     }
-  }
-  
+  };
+
   render() {
     let { label, value, type, unit, placeholder, width } = this.props;
     return (
       <StyledInputRow>
-        <Label>{label} <Required>{this.props.isRequired ? ' *' : null}</Required></Label>
+        <Label>
+          {label} <Required>{this.props.isRequired ? " *" : null}</Required>
+        </Label>
         <InputWrapper>
           <Input
-            readOnly={this.state.readOnly} onFocus={() => this.setState({ readOnly: false })}
+            readOnly={this.state.readOnly}
+            onFocus={() => this.setState({ readOnly: false })}
             disabled={this.props.disabled}
             placeholder={placeholder}
             width={width}
@@ -74,8 +77,10 @@ const Input = styled.input`
   background: #ffffff11;
   border: 1px solid #ffffff55;
   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;
   height: 35px;
 `;
@@ -86,10 +91,10 @@ const Label = styled.div`
   display: flex;
   align-items: center;
   font-size: 13px;
-  font-family: 'Work Sans', sans-serif;
+  font-family: "Work Sans", sans-serif;
 `;
 
 const StyledInputRow = styled.div`
   margin-bottom: 15px;
   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 = {
-  options: { label: string, value: string }[],
+  options: { label: string; value: string }[];
 };
 
 export default class MultiSelect extends Component<PropsType, StateType> {
   state = {
-    options: [] as { label: string, value: string }[],
-  }
+    options: [] as { label: string; value: string }[],
+  };
 
-  renderOptions = () => {
-    
-  }
+  renderOptions = () => {};
 
   render() {
     return (
       <>
-        <StyledMultiSelect>
-        </StyledMultiSelect>
+        <StyledMultiSelect></StyledMultiSelect>
         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 = {
-  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> {
   render() {
@@ -27,7 +26,7 @@ export default class SelectRow extends Component<PropsType, StateType> {
             setActiveValue={this.props.setActiveValue}
             options={this.props.options}
             dropdownLabel={this.props.dropdownLabel}
-            width={this.props.width || '270px'}
+            width={this.props.width || "270px"}
             dropdownWidth={this.props.width}
             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`
   color: #ffffff;
@@ -48,4 +46,4 @@ const Label = styled.div`
 const StyledSelectRow = styled.div`
   margin-bottom: 15px;
   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 = {
-  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> {
   handleChange = (e: any) => {
     this.props.setValue(e.target.value);
-  }
+  };
 
   render() {
     let { label, value, placeholder, width } = this.props;
@@ -27,7 +26,7 @@ export default class TextArea extends Component<PropsType, StateType> {
           disabled={this.props.disabled}
           placeholder={placeholder}
           width={width}
-          value={value || ''}
+          value={value || ""}
           onChange={this.handleChange}
         />
       </StyledTextArea>
@@ -43,8 +42,10 @@ const InputArea = styled.textarea`
   background: #ffffff11;
   border: 1px solid #ffffff55;
   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;
   margin-right: 8px;
   height: 8em;
@@ -55,10 +56,10 @@ const Label = styled.div`
   color: #ffffff;
   margin-bottom: 10px;
   font-size: 13px;
-  font-family: 'Work Sans', sans-serif;
+  font-family: "Work Sans", sans-serif;
 `;
 
 const StyledTextArea = styled.div`
   margin-bottom: 15px;
   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 = {
-  sections?: Section[],
-  metaState?: any,
-  setMetaState?: any,
+  sections?: Section[];
+  metaState?: any;
+  setMetaState?: any;
 };
 
 type StateType = any;
@@ -28,55 +26,54 @@ export default class ValuesForm extends Component<PropsType, StateType> {
     let key = item.name || item.variable;
     let value = this.props.metaState[key];
     if (item.settings && item.settings.unit && value) {
-      value = value.split(item.settings.unit)[0]
+      value = value.split(item.settings.unit)[0];
     }
     return value;
-  }
+  };
 
   renderSection = (section: Section) => {
     return section.contents.map((item: FormElement, i: number) => {
-
       // If no name is assigned use values.yaml variable as identifier
       let key = item.name || item.variable;
       switch (item.type) {
-        case 'heading':
+        case "heading":
           return <Heading key={i}>{item.label}</Heading>;
-        case 'subtitle':
+        case "subtitle":
           return <Helper key={i}>{item.label}</Helper>;
-        case 'resource-list':
+        case "resource-list":
           if (Array.isArray(item.value)) {
             return (
               <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>
             );
           }
-        case 'checkbox':
+        case "checkbox":
           return (
             <CheckboxRow
               key={i}
               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}
             />
           );
-        case 'array-input':
+        case "array-input":
           return (
             <InputRow
               key={i}
               isRequired={item.required}
-              type='text'
+              type="text"
               value={this.getInputValue(item)}
               setValue={(x: string) => {
                 this.props.setMetaState({ [key]: [x] });
@@ -85,15 +82,15 @@ export default class ValuesForm extends Component<PropsType, StateType> {
               unit={item.settings ? item.settings.unit : null}
             />
           );
-        case 'string-input':
+        case "string-input":
           return (
             <InputRow
               key={i}
               isRequired={item.required}
-              type='text'
+              type="text"
               value={this.getInputValue(item)}
               setValue={(x: string) => {
-                if (item.settings && item.settings.unit && x !== '') {
+                if (item.settings && item.settings.unit && x !== "") {
                   x = x + item.settings.unit;
                 }
                 this.props.setMetaState({ [key]: x });
@@ -102,17 +99,17 @@ export default class ValuesForm extends Component<PropsType, StateType> {
               unit={item.settings ? item.settings.unit : null}
             />
           );
-        case 'number-input':
+        case "number-input":
           return (
             <InputRow
               key={i}
               isRequired={item.required}
-              type='number'
+              type="number"
               value={this.getInputValue(item)}
               setValue={(x: number) => {
                 let val: string | number = x;
                 if (Number.isNaN(x)) {
-                  val = ''
+                  val = "";
                 }
 
                 // Convert to string if unit is set
@@ -120,53 +117,50 @@ export default class ValuesForm extends Component<PropsType, StateType> {
                   val = x.toString();
                   val = val + item.settings.unit;
                 }
-                
+
                 this.props.setMetaState({ [key]: val });
               }}
               label={item.label}
               unit={item.settings ? item.settings.unit : null}
             />
           );
-        case 'select':
+        case "select":
           return (
             <SelectRow
               key={i}
               value={this.props.metaState[key]}
               setActiveValue={(val) => this.props.setMetaState({ [key]: val })}
               options={item.settings.options}
-              dropdownLabel=''
+              dropdownLabel=""
               label={item.label}
             />
           );
-        case 'provider-select':
+        case "provider-select":
           return (
             <SelectRow
               key={i}
               value={this.props.metaState[key]}
               setActiveValue={(val) => this.props.setMetaState({ [key]: val })}
               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}
             />
           );
-        case 'velero-create-backup':
-          return (
-            <VeleroForm
-            />
-          );
-        case 'base-64':
+        case "velero-create-backup":
+          return <VeleroForm />;
+        case "base-64":
           return (
             <Base64InputRow
               key={i}
               isRequired={item.required}
-              type='text'
+              type="text"
               value={this.getInputValue(item)}
               setValue={(x: string) => {
-                if (item.settings && item.settings.unit && x !== '') {
+                if (item.settings && item.settings.unit && x !== "") {
                   x = x + item.settings.unit;
                 }
                 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}
             />
           );
-        case 'base-64-password':
+        case "base-64-password":
           return (
             <Base64InputRow
               key={i}
               isRequired={item.required}
-              type='password'
+              type="password"
               value={this.getInputValue(item)}
               setValue={(x: string) => {
-                if (item.settings && item.settings.unit && x !== '') {
+                if (item.settings && item.settings.unit && x !== "") {
                   x = x + item.settings.unit;
                 }
                 this.props.setMetaState({ [key]: btoa(x) });
@@ -195,7 +189,7 @@ export default class ValuesForm extends Component<PropsType, StateType> {
         default:
       }
     });
-  }
+  };
 
   renderFormContents = () => {
     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() {
     return (
@@ -249,4 +239,4 @@ const StyledValuesForm = styled.div`
   border-radius: 5px;
   font-size: 13px;
   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 = {
-  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;
 
 const providerMap: any = {
-  'gke': 'gcp',
-  'eks': 'aws',
-  'doks': 'do',
+  gke: "gcp",
+  eks: "aws",
+  doks: "do",
 };
 
 // Manages the consolidated state of all form tabs ("metastate")
 export default class ValuesWrapper extends Component<PropsType, StateType> {
-
   // No need to render, so OK to set as class variable outside of state
   requiredFields: string[] = [];
 
   updateFormState() {
     let metaState: any = {};
     this.props.formTabs.forEach((tab: any, i: number) => {
-
       // 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) => {
           section.contents.forEach((item: FormElement, i: number) => {
-
             // If no name is assigned use values.yaml variable as identifier
             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
             if (item.required) {
@@ -48,29 +47,29 @@ export default class ValuesWrapper extends Component<PropsType, StateType> {
             }
 
             switch (item.type) {
-              case 'checkbox':
+              case "checkbox":
                 metaState[key] = def ? def : false;
                 break;
-              case 'string-input':
-                metaState[key] = def ? def : '';
+              case "string-input":
+                metaState[key] = def ? def : "";
                 break;
-              case 'array-input':
+              case "array-input":
                 metaState[key] = def ? def : [];
                 break;
-              case 'number-input':
-                metaState[key] = def.toString() ? def : '';
+              case "number-input":
+                metaState[key] = def.toString() ? def : "";
                 break;
-              case 'select':
+              case "select":
                 metaState[key] = def ? def : item.settings.options[0].value;
                 break;
-              case 'provider-select':
+              case "provider-select":
                 def = providerMap[this.context.currentCluster.service];
-                metaState[key] = def ? def : 'aws';
+                metaState[key] = def ? def : "aws";
                 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:
             }
           });
@@ -86,7 +85,8 @@ export default class ValuesWrapper extends Component<PropsType, StateType> {
   }
 
   componentDidUpdate(prevProps: PropsType) {
-    if (this.props.formTabs !== prevProps.formTabs || 
+    if (
+      this.props.formTabs !== prevProps.formTabs ||
       this.props.currentTab !== prevProps.currentTab
     ) {
       this.updateFormState();
@@ -99,24 +99,30 @@ export default class ValuesWrapper extends Component<PropsType, StateType> {
     this.requiredFields.forEach((field: string, i: number) => {
       valueIndicators.push(this.state[field] && true);
     });
-    return valueIndicators.includes(false) || valueIndicators.includes('')
-  }
+    return valueIndicators.includes(false) || valueIndicators.includes("");
+  };
 
   renderButton = () => {
     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 (
         <SaveButton
           disabled={this.isDisabled() || this.props.disabled}
-          text='Deploy'
+          text="Deploy"
           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}
         />
       );
     }
-  }
+  };
 
   render() {
     let renderFunc: any = this.props.children;
@@ -150,4 +156,4 @@ const StyledValuesWrapper = styled.div`
 const PaddedWrapper = styled.div`
   padding-bottom: 65px;
   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 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 & {
-  logOut: () => void,
-  currentProject: ProjectType,
-  currentCluster: ClusterType,
-  currentRoute: PorterUrls,
+  logOut: () => void;
+  currentProject: ProjectType;
+  currentCluster: ClusterType;
+  currentRoute: PorterUrls;
 };
 
 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
-  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)
@@ -56,173 +56,208 @@ class Home extends Component<PropsType, StateType> {
     sidebarReady: false,
     handleDO: false,
     ghRedirect: false,
-  }
+  };
 
   // TODO: Refactor and prevent flash + multiple reload
   initializeView = () => {
     let { currentProject } = this.props;
     let { currentCluster } = this.context;
-    
+
     if (!currentProject) return;
 
     // 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) => {
     let { user, setProjects } = this.context;
     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) => {
-    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) => {
-    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 = () => {
     let { currentProject } = this.props;
     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 });
     }
-  }
+  };
 
   componentDidMount() {
     let { user } = this.context;
-    FullStory.identify(user.email)
+    FullStory.identify(user.email);
 
     // Handle redirect from DO
     let queryString = window.location.search;
     let urlParams = new URLSearchParams(queryString);
 
-    let err = urlParams.get('error');
+    let err = urlParams.get("error");
     if (err) {
       this.context.setCurrentError(err);
     }
 
-    let provision = urlParams.get('provision');
+    let provision = urlParams.get("provision");
     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.checkDO();
     }
 
     // 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);
   }
@@ -233,8 +268,8 @@ class Home extends Component<PropsType, StateType> {
   // 3. Make sure initializing from URL (DO oauth) displays the appropriate initial view
   componentDidUpdate(prevProps: PropsType) {
     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) {
         this.checkDO();
@@ -251,19 +286,36 @@ class Home extends Component<PropsType, StateType> {
       return (
         <DashboardWrapper>
           <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>
         </DashboardWrapper>
       );
     } else if (!currentCluster) {
-      return <Loading />
+      return <Loading />;
     }
 
     return (
@@ -275,48 +327,40 @@ class Home extends Component<PropsType, StateType> {
         />
       </DashboardWrapper>
     );
-  }
+  };
 
   renderContents = () => {
     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();
-      } else if (currentView === 'dashboard') {
+      } else if (currentView === "dashboard") {
         return (
           <DashboardWrapper>
-            <Dashboard 
-              projectId={this.context.currentProject?.id}
-            />
+            <Dashboard projectId={this.context.currentProject?.id} />
           </DashboardWrapper>
         );
-      } else if (currentView === 'integrations') {
+      } else if (currentView === "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 = () => {
     if (this.context.projects.length > 0) {
-
       // 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 });
       } else {
         return (
@@ -326,145 +370,180 @@ class Home extends Component<PropsType, StateType> {
             setWelcome={(x: boolean) => this.setState({ showWelcome: x })}
             currentView={this.props.currentRoute}
             forceRefreshClusters={this.state.forceRefreshClusters}
-            setRefreshClusters={(x: boolean) => this.setState({ forceRefreshClusters: x })}
+            setRefreshClusters={(x: boolean) =>
+              this.setState({ forceRefreshClusters: x })
+            }
           />
         );
       }
     }
-  }
+  };
 
   projectOverlayCall = () => {
     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 = () => {
     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
-    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);
     this.props.history.push("dashboard");
-  }
+  };
 
   render() {
     let { currentModal, setCurrentModal, currentProject } = this.context;
 
     return (
       <StyledHome>
-        {currentModal === 'ClusterInstructionsModal' &&
+        {currentModal === "ClusterInstructionsModal" && (
           <Modal
             onRequestClose={() => setCurrentModal(null, null)}
-            width='760px'
-            height='650px'
+            width="760px"
+            height="650px"
           >
             <ClusterInstructionsModal />
           </Modal>
-        }
-        {currentModal === 'UpdateClusterModal' &&
+        )}
+        {currentModal === "UpdateClusterModal" && (
           <Modal
             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>
-        }
-        {currentModal === 'IntegrationsModal' &&
+        )}
+        {currentModal === "IntegrationsModal" && (
           <Modal
             onRequestClose={() => setCurrentModal(null, null)}
-            width='760px'
-            height='725px'
+            width="760px"
+            height="725px"
           >
             <IntegrationsModal />
           </Modal>
-        }
-        {currentModal === 'IntegrationsInstructionsModal' &&
+        )}
+        {currentModal === "IntegrationsInstructionsModal" && (
           <Modal
             onRequestClose={() => setCurrentModal(null, null)}
-            width='760px'
-            height='650px'
+            width="760px"
+            height="650px"
           >
             <IntegrationsInstructionsModal />
           </Modal>
-        }
+        )}
 
         {this.renderSidebar()}
 
@@ -477,8 +556,12 @@ class Home extends Component<PropsType, StateType> {
         </ViewWrapper>
 
         <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}
           onNo={() => setCurrentModal(null, null)}
         />
@@ -513,7 +596,8 @@ const DashboardWrapper = styled.div`
 const A = styled.a`
   color: #ffffff;
   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`
@@ -543,10 +627,12 @@ const StyledHome = styled.div`
 
   @keyframes floatInModal {
     from {
-      opacity: 0; transform: translateY(30px);
+      opacity: 0;
+      transform: translateY(30px);
     }
     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 & {
-  currentCluster: ClusterType,
-  setSidebar: (x: boolean) => void,
+  currentCluster: ClusterType;
+  setSidebar: (x: boolean) => void;
 };
 
 type StateType = {
-  namespace: string,
-  sortType: string,
-  currentChart: ChartType | null
+  namespace: string;
+  sortType: string;
+  currentChart: ChartType | null;
 };
 
 class ClusterDashboard extends Component<PropsType, StateType> {
   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) {
     localStorage.setItem("SortType", this.state.sortType);
     // Reset namespace filter and close expanded chart on cluster change
     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 (
         <DashboardIcon>
           <DashboardImage src={gradient} />
-          <Overlay>{currentCluster && currentCluster.name[0].toUpperCase()}</Overlay>
+          <Overlay>
+            {currentCluster && currentCluster.name[0].toUpperCase()}
+          </Overlay>
         </DashboardIcon>
       );
     }
@@ -56,18 +63,20 @@ class ClusterDashboard extends Component<PropsType, StateType> {
         <i className="material-icons">device_hub</i>
       </DashboardIcon>
     );
-  }
+  };
 
   renderContents = () => {
     let { currentCluster, setSidebar } = this.props;
-    
+
     if (this.state.currentChart) {
       return (
         <ExpandedChart
           namespace={this.state.namespace}
           currentCluster={this.props.currentCluster}
           currentChart={this.state.currentChart}
-          setCurrentChart={(x: ChartType | null) => this.setState({ currentChart: x })}
+          setCurrentChart={(x: ChartType | null) =>
+            this.setState({ currentChart: x })
+          }
           setSidebar={setSidebar}
         />
       );
@@ -78,9 +87,9 @@ class ClusterDashboard extends Component<PropsType, StateType> {
         <TitleSection>
           {this.renderDashboardIcon()}
           <Title>{currentCluster.name}</Title>
-          <i 
+          <i
             className="material-icons"
-            onClick={() => this.context.setCurrentModal('UpdateClusterModal')}
+            onClick={() => this.context.setCurrentModal("UpdateClusterModal")}
           >
             more_vert
           </i>
@@ -92,15 +101,15 @@ class ClusterDashboard extends Component<PropsType, StateType> {
               <i className="material-icons">info</i> Info
             </InfoLabel>
           </TopRow>
-          <Description>Cluster dashboard for {currentCluster.name}.</Description>
+          <Description>
+            Cluster dashboard for {currentCluster.name}.
+          </Description>
         </InfoSection>
 
         <LineBreak />
-        
+
         <ControlRow>
-          <Button
-            onClick={() => this.props.history.push("templates")}
-          >
+          <Button onClick={() => this.props.history.push("templates")}>
             <i className="material-icons">add</i> Deploy Template
           </Button>
           <SortFilterWrapper>
@@ -119,18 +128,16 @@ class ClusterDashboard extends Component<PropsType, StateType> {
           currentCluster={currentCluster}
           namespace={this.state.namespace}
           sortType={this.state.sortType}
-          setCurrentChart={(x: ChartType | null) => this.setState({ currentChart: x })}
+          setCurrentChart={(x: ChartType | null) =>
+            this.setState({ currentChart: x })
+          }
         />
       </div>
     );
-  }
+  };
 
   render() {
-    return (
-      <div>
-        {this.renderContents()}
-      </div>
-    );
+    return <div>{this.renderContents()}</div>;
   }
 }
 
@@ -163,10 +170,10 @@ const InfoLabel = styled.div`
   height: 20px;
   display: flex;
   align-items: center;
-  color: #7A838F;
+  color: #7a838f;
   font-size: 13px;
   > i {
-    color: #8B949F;
+    color: #8b949f;
     font-size: 18px;
     margin-right: 5px;
   }
@@ -174,7 +181,7 @@ const InfoLabel = styled.div`
 
 const InfoSection = styled.div`
   margin-top: 20px;
-  font-family: 'Work Sans', sans-serif;
+  font-family: "Work Sans", sans-serif;
   margin-left: 0px;
   margin-bottom: 35px;
 `;
@@ -186,7 +193,7 @@ const Button = styled.div`
   justify-content: space-between;
   font-size: 13px;
   cursor: pointer;
-  font-family: 'Work Sans', sans-serif;
+  font-family: "Work Sans", sans-serif;
   border-radius: 20px;
   color: white;
   height: 35px;
@@ -199,11 +206,14 @@ const Button = styled.div`
   white-space: nowrap;
   text-overflow: ellipsis;
   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 {
-    background: ${(props: { disabled?: boolean }) => props.disabled ? '' : '#505edddd'};
+    background: ${(props: { disabled?: boolean }) =>
+      props.disabled ? "" : "#505edddd"};
   }
 
   > i {
@@ -223,7 +233,7 @@ const Button = styled.div`
 const ButtonAlt = styled(Button)`
   min-width: 150px;
   max-width: 150px;
-  background: #7A838Fdd;
+  background: #7a838fdd;
 
   :hover {
     background: #69727eee;
@@ -250,7 +260,7 @@ const Overlay = styled.div`
   justify-content: center;
   font-size: 24px;
   font-weight: 500;
-  font-family: 'Work Sans', sans-serif;
+  font-family: "Work Sans", sans-serif;
   color: white;
 `;
 
@@ -269,7 +279,7 @@ const DashboardIcon = styled.div`
   display: flex;
   align-items: center;
   justify-content: center;
-  background: #676C7C;
+  background: #676c7c;
   border: 2px solid #8e94aa;
 
   > i {
@@ -280,7 +290,7 @@ const DashboardIcon = styled.div`
 const Title = styled.div`
   font-size: 20px;
   font-weight: 500;
-  font-family: 'Work Sans', sans-serif;
+  font-family: "Work Sans", sans-serif;
   margin-left: 18px;
   color: #ffffff;
   white-space: nowrap;
@@ -315,4 +325,4 @@ const SortFilterWrapper = styled.div`
   width: 468px;
   display: flex;
   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 = {
-  setNamespace: (x: string) => void,
-  namespace: string
+  setNamespace: (x: string) => void;
+  namespace: string;
 };
 
 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> {
   _isMounted = false;
 
   state = {
-    namespaceOptions: [] as { label: string, value: string }[]
-  }
+    namespaceOptions: [] as { label: string; value: string }[],
+  };
 
   updateOptions = () => {
     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() {
     this._isMounted = true;
@@ -58,7 +70,7 @@ export default class NamespaceSelector extends Component<PropsType, StateType> {
   }
 
   render() {
-    return ( 
+    return (
       <StyledNamespaceSelector>
         <Label>
           <i className="material-icons">filter_alt</i> Filter
@@ -67,9 +79,9 @@ export default class NamespaceSelector extends Component<PropsType, StateType> {
           activeValue={this.props.namespace}
           setActiveValue={(namespace) => this.props.setNamespace(namespace)}
           options={this.state.namespaceOptions}
-          dropdownLabel='Namespace'
-          width='150px'
-          dropdownWidth='230px'
+          dropdownLabel="Namespace"
+          width="150px"
+          dropdownWidth="230px"
           closeOverlay={true}
         />
       </StyledNamespaceSelector>
@@ -94,4 +106,4 @@ const StyledNamespaceSelector = styled.div`
   display: flex;
   align-items: center;
   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 = {
-  setSortType: (x: string) => void,
-  sortType: string
+  setSortType: (x: string) => void;
+  sortType: string;
 };
 
 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> {
   state = {
     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() {
-    return ( 
+    return (
       <StyledSortSelector>
         <Label>
           <i className="material-icons">sort</i> Sort
@@ -34,9 +34,9 @@ export default class SortSelector extends Component<PropsType, StateType> {
           activeValue={this.props.sortType}
           setActiveValue={(sortType) => this.props.setSortType(sortType)}
           options={this.state.sortOptions}
-          dropdownLabel='Sort By'
-          width='150px'
-          dropdownWidth='230px'
+          dropdownLabel="Sort By"
+          width="150px"
+          dropdownWidth="230px"
           closeOverlay={true}
         />
       </StyledSortSelector>
@@ -61,4 +61,4 @@ const StyledSortSelector = styled.div`
   display: flex;
   align-items: center;
   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 = {
-  chart: ChartType,
-  setCurrentChart: (c: ChartType) => void,
-  controllers: Record<string, any>,
+  chart: ChartType;
+  setCurrentChart: (c: ChartType) => void;
+  controllers: Record<string, any>;
 };
 
 type StateType = {
-  expand: boolean,
-  update: any[],
+  expand: boolean;
+  update: any[];
 };
 
 export default class Chart extends Component<PropsType, StateType> {
   state = {
     expand: false,
     update: [] as any[],
-  }
+  };
 
   renderIcon = () => {
     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 {
-      return <i className="material-icons">tonality</i>
+      return <i className="material-icons">tonality</i>;
     }
-  }
+  };
 
   readableDate = (s: string) => {
     let ts = new Date(s);
     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}`;
-  }
+  };
 
   render() {
     let { chart, setCurrentChart } = this.props;
 
-    return ( 
+    return (
       <StyledChart
         onMouseEnter={() => this.setState({ expand: true })}
         onMouseLeave={() => this.setState({ expand: false })}
@@ -50,29 +53,26 @@ export default class Chart extends Component<PropsType, StateType> {
         onClick={() => setCurrentChart(chart)}
       >
         <Title>
-          <IconWrapper>
-            {this.renderIcon()}
-          </IconWrapper>
+          <IconWrapper>{this.renderIcon()}</IconWrapper>
           {chart.name}
         </Title>
 
         <BottomWrapper>
           <InfoWrapper>
             <StatusIndicator
-              controllers={this.props.controllers} 
+              controllers={this.props.controllers}
               status={chart.info.status}
-              margin_left={'17px'}
+              margin_left={"17px"}
             />
             <LastDeployed>
-              <Dot>•</Dot> Last deployed {this.readableDate(chart.info.last_deployed)}
+              <Dot>•</Dot> Last deployed{" "}
+              {this.readableDate(chart.info.last_deployed)}
             </LastDeployed>
           </InfoWrapper>
 
           <TagWrapper>
             Namespace
-            <NamespaceTag>
-              {chart.namespace}
-            </NamespaceTag>
+            <NamespaceTag>{chart.namespace}</NamespaceTag>
           </TagWrapper>
         </BottomWrapper>
 
@@ -182,7 +182,7 @@ const Title = styled.div`
   text-decoration: none;
   padding: 12px 35px 12px 45px;
   font-size: 14px;
-  font-family: 'Work Sans', sans-serif;
+  font-family: "Work Sans", sans-serif;
   font-weight: 500;
   color: #ffffff;
   width: 80%;
@@ -191,7 +191,7 @@ const Title = styled.div`
   text-overflow: ellipsis;
   animation: fadeIn 0.5s;
 
-  >img {
+  > img {
     background: none;
     top: 12px;
     left: 13px;
@@ -210,17 +210,19 @@ const StyledChart = styled.div`
   border-radius: 5px;
   box-shadow: 0 5px 8px 0px #00000033;
   position: relative;
-  border: 2px solid #9EB4FF00;
+  border: 2px solid #9eb4ff00;
   width: 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-timing-function: ease-out;
 
   @keyframes expand {
-    from { 
-      width: calc(100% + 2px); 
+    from {
+      width: calc(100% + 2px);
       padding-top: 4px;
       padding-bottom: 14px;
       margin-left: 0px;
@@ -233,7 +235,7 @@ const StyledChart = styled.div`
       width: calc(100% + 22px);
       padding-top: 7px;
       padding-bottom: 20px;
-      margin-left: -10px; 
+      margin-left: -10px;
       box-shadow: 0 8px 20px 0px #00000030;
       padding-left: 5px;
       margin-bottom: 21px;
@@ -242,21 +244,21 @@ const StyledChart = styled.div`
   }
 
   @keyframes shrink {
-    from { 
+    from {
       width: calc(100% + 22px);
       padding-top: 7px;
       padding-bottom: 20px;
-      margin-left: -10px; 
+      margin-left: -10px;
       box-shadow: 0 8px 20px 0px #00000030;
       padding-left: 5px;
       margin-bottom: 21px;
       margin-top: -4px;
     }
     to {
-      width: calc(100% + 2px); 
+      width: calc(100% + 2px);
       padding-top: 4px;
       padding-bottom: 14px;
-      margin-left: 0px; 
+      margin-left: 0px;
       box-shadow: 0 5px 8px 0px #00000033;
       padding-left: 1px;
       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 = {
-  currentCluster: ClusterType,
-  namespace: string,
-  sortType: string,
-  setCurrentChart: (c: ChartType) => void
+  currentCluster: ClusterType;
+  namespace: string;
+  sortType: string;
+  setCurrentChart: (c: ChartType) => void;
 };
 
 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> {
@@ -31,161 +31,200 @@ export default class ChartList extends Component<PropsType, StateType> {
     controllers: {} as Record<string, Record<string, any>>,
     loading: false,
     error: false,
-    websockets : {} as Record<string, any>,
-  }
+    websockets: {} as Record<string, any>,
+  };
 
   updateCharts = (callback: Function) => {
     let { currentCluster, currentProject, setCurrentError } = this.context;
     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) => {
-      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[]) => {
     let websockets = controllers.map((kind: string) => {
       return this.setupWebsocket(kind);
-    })
+    });
     this.setState({ websockets });
-  }
+  };
 
   getControllers = (charts: any[]) => {
     let { currentCluster, currentProject, setCurrentError } = this.context;
 
     charts.forEach(async (chart: any) => {
       // 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) => {
-        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() {
     this.updateCharts(this.getControllers);
-    this.setControllerWebsockets(["deployment", "statefulset", "daemonset", "replicaset"]);
+    this.setControllerWebsockets([
+      "deployment",
+      "statefulset",
+      "daemonset",
+      "replicaset",
+    ]);
   }
 
   componentWillUnmount() {
     if (this.state.websockets) {
       this.state.websockets.forEach((ws: WebSocket) => {
-        ws.close()
-      })
+        ws.close();
+      });
     }
   }
 
   componentDidUpdate(prevProps: PropsType) {
     // Ret2: Prevents reload when opening ClusterConfigModal
-    if (prevProps.currentCluster !== this.props.currentCluster || 
+    if (
+      prevProps.currentCluster !== this.props.currentCluster ||
       prevProps.namespace !== this.props.namespace ||
-      prevProps.sortType !== this.props.sortType) {
+      prevProps.sortType !== this.props.sortType
+    ) {
       this.updateCharts(this.getControllers);
     }
   }
@@ -194,7 +233,11 @@ export default class ChartList extends Component<PropsType, StateType> {
     let { loading, error, charts } = this.state;
 
     if (loading) {
-      return <LoadingWrapper><Loading /></LoadingWrapper>
+      return (
+        <LoadingWrapper>
+          <Loading />
+        </LoadingWrapper>
+      );
     } else if (error) {
       return (
         <Placeholder>
@@ -204,7 +247,8 @@ export default class ChartList extends Component<PropsType, StateType> {
     } else if (charts.length === 0) {
       return (
         <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>
       );
     }
@@ -215,19 +259,17 @@ export default class ChartList extends Component<PropsType, StateType> {
           key={`${chart.namespace}-${chart.name}`}
           chart={chart}
           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() {
-    return (
-      <StyledChartList>
-        {this.renderChartList()}
-      </StyledChartList>
-    );
+    return <StyledChartList>{this.renderChartList()}</StyledChartList>;
   }
 }
 
@@ -260,4 +302,4 @@ const LoadingWrapper = styled.div`
 
 const StyledChartList = styled.div`
   padding-bottom: 85px;
-`;
+`;

Fișier diff suprimat deoarece este prea mare
+ 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 = {
-  components: ResourceType[],
-  currentChart: ChartType,
-  setSidebar: (x: boolean) => void,
-  showRevisions: boolean
+  components: ResourceType[];
+  currentChart: ChartType;
+  setSidebar: (x: boolean) => void;
+  showRevisions: boolean;
 };
 
 type StateType = {
-  isExpanded: boolean
+  isExpanded: boolean;
 };
 
 export default class GraphSection extends Component<PropsType, StateType> {
   state = {
-    isExpanded: false
-  }
+    isExpanded: false,
+  };
 
   renderContents = () => {
     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() {
-    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 = {
-  currentChart: ChartType,
-  components: ResourceType[],
-  showRevisions: boolean,
+  currentChart: ChartType;
+  components: ResourceType[];
+  showRevisions: boolean;
 };
 
 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> {
   state = {
     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,
-    selectedResource: null as { kind: string, name: string } | null,
-  }
+    selectedResource: null as { kind: string; name: string } | null,
+  };
 
   wrapperRef: any = React.createRef();
 
@@ -37,23 +37,31 @@ export default class ListSection extends Component<PropsType, StateType> {
   }
 
   componentDidUpdate(prevProps: PropsType) {
-
     // 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 });
     }
 
-    if (prevProps.components !== this.props.components && this.state.selectedResource) {
+    if (
+      prevProps.components !== this.props.components &&
+      this.state.selectedResource
+    ) {
       let matchingResourceFound = false;
       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);
           this.setState({ yaml: rawYaml });
           matchingResourceFound = true;
         }
       });
       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 (
         <ResourceTab
           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}
           label={resource.Kind}
           name={resource.Name}
@@ -75,30 +85,26 @@ export default class ListSection extends Component<PropsType, StateType> {
         />
       );
     });
-  }
+  };
 
   renderTabs = () => {
     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() {
     return (
       <StyledListSection>
         {this.renderTabs()}
-        <FlexWrapper ref={element => this.wrapperRef = element}>
+        <FlexWrapper ref={(element) => (this.wrapperRef = element)}>
           <YamlWrapper>
             <YamlEditor
               value={this.state.yaml}
               onChange={(e: any) => this.setState({ yaml: e })}
-              height={this.state.wrapperHeight - 2 + 'px'}
+              height={this.state.wrapperHeight - 2 + "px"}
               border={true}
               readOnly={true}
             />
@@ -138,4 +144,4 @@ const StyledListSection = styled.div`
   font-size: 13px;
   border-radius: 5px;
   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 = {
-  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 = {
-  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
 export default class RevisionSection extends Component<PropsType, StateType> {
   state = {
     revisions: [] as ChartType[],
-    rollbackRevision: null as (number | null),
+    rollbackRevision: null as number | null,
     loading: false,
     maxVersion: 0, // Track most recent version even when previewing old revisions
-  }
+  };
 
   refreshHistory = (callback?: () => void) => {
     let { chart } = this.props;
     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() {
     this.refreshHistory();
@@ -74,9 +84,12 @@ export default class RevisionSection extends Component<PropsType, StateType> {
   readableDate = (s: string) => {
     let ts = new Date(s);
     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}`;
-  }
+  };
 
   handleRollback = () => {
     let { setCurrentError, currentCluster, currentProject } = this.context;
@@ -84,27 +97,32 @@ export default class RevisionSection extends Component<PropsType, StateType> {
     let revisionNumber = this.state.rollbackRevision;
     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) => {
     let isCurrent = revision.version === this.state.maxVersion;
@@ -113,21 +131,24 @@ export default class RevisionSection extends Component<PropsType, StateType> {
     } else {
       this.props.setRevision(revision);
     }
-  }
+  };
 
   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 (
         <div>
           {this.props.status}
-          <LoadingGif src={loading} revision={true}/>
+          <LoadingGif src={loading} revision={true} />
         </div>
-      )
+      );
     } 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 = () => {
     return this.state.revisions.map((revision: ChartType, i: number) => {
@@ -144,15 +165,17 @@ export default class RevisionSection extends Component<PropsType, StateType> {
           <Td>
             <RollbackButton
               disabled={isCurrent}
-              onClick={() => this.setState({ rollbackRevision: revision.version })}
+              onClick={() =>
+                this.setState({ rollbackRevision: revision.version })
+              }
             >
-              {isCurrent ? 'Current' : 'Revert'}
+              {isCurrent ? "Current" : "Revert"}
             </RollbackButton>
           </Td>
         </Tr>
       );
     });
-  }
+  };
 
   renderExpanded = () => {
     if (this.props.showRevisions) {
@@ -170,22 +193,24 @@ export default class RevisionSection extends Component<PropsType, StateType> {
             </tbody>
           </RevisionsTable>
         </TableWrapper>
-      )
+      );
     }
-  }
+  };
 
   renderContents = () => {
     if (this.state.loading) {
       return (
         <LoadingPlaceholder>
           <StatusWrapper>
-            <LoadingGif src={loading} revision={false}/> Updating . . .
+            <LoadingGif src={loading} revision={false} /> Updating . . .
           </StatusWrapper>
         </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 (
       <div>
         <RevisionHeader
@@ -193,16 +218,17 @@ export default class RevisionSection extends Component<PropsType, StateType> {
           isCurrent={isCurrent}
           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>
         </RevisionHeader>
 
-        <RevisionList>
-          {this.renderExpanded()}
-        </RevisionList>
+        <RevisionList>{this.renderExpanded()}</RevisionList>
       </div>
     );
-  }
+  };
 
   render() {
     return (
@@ -235,15 +261,18 @@ const LoadingPlaceholder = styled.div`
 const LoadingGif = styled.img`
   width: 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`
   display: flex;
   align-items: center;
-  font-family: 'Work Sans', sans-serif;
+  font-family: "Work Sans", sans-serif;
   font-size: 13px;
   color: #ffffff55;
   margin-right: 25px;
@@ -255,7 +284,8 @@ const RevisionList = 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;
   border-radius: 3px;
   align-items: center;
@@ -264,18 +294,23 @@ const RollbackButton = styled.div`
   height: 21px;
   font-size: 13px;
   width: 70px;
-  background: ${(props: { disabled: boolean }) => props.disabled ? '#aaaabbee' :'#616FEEcc'};
+  background: ${(props: { disabled: boolean }) =>
+    props.disabled ? "#aaaabbee" : "#616FEEcc"};
   :hover {
-    background: ${(props: { disabled: boolean }) => props.disabled ? '' : '#405eddbb'};
+    background: ${(props: { disabled: boolean }) =>
+      props.disabled ? "" : "#405eddbb"};
   }
 `;
 
 const Tr = styled.tr`
   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 {
-    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`
-  color: ${(props: { showRevisions: boolean, isCurrent: boolean }) => props.isCurrent ? '#ffffff66' : '#f5cb42'};
+  color: ${(props: { showRevisions: boolean; isCurrent: boolean }) =>
+    props.isCurrent ? "#ffffff66" : "#f5cb42"};
   display: flex;
   align-items: center;
   height: 40px;
@@ -315,7 +351,8 @@ const RevisionHeader = styled.div`
   width: 100%;
   padding-left: 15px;
   cursor: pointer;
-  background: ${(props: { showRevisions: boolean, isCurrent: boolean }) => props.showRevisions ? '#ffffff11' : ''};
+  background: ${(props: { showRevisions: boolean; isCurrent: boolean }) =>
+    props.showRevisions ? "#ffffff11" : ""};
   :hover {
     background: #ffffff18;
     > i {
@@ -328,22 +365,30 @@ const RevisionHeader = styled.div`
     font-size: 20px;
     cursor: pointer;
     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`
   width: 100%;
-  max-height: ${(props: { showRevisions: boolean }) => props.showRevisions ? '255px' : '40px'};
+  max-height: ${(props: { showRevisions: boolean }) =>
+    props.showRevisions ? "255px" : "40px"};
   background: #ffffff11;
   margin: 25px 0px 18px;
   overflow: hidden;
   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;
   @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 = {
-  currentChart: ChartType,
-  refreshChart: () => void,
-  setShowDeleteOverlay: (x: boolean) => void,
+  currentChart: ChartType;
+  refreshChart: () => void;
+  setShowDeleteOverlay: (x: boolean) => void;
 };
 
 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;
 };
 
 export default class SettingsSection extends Component<PropsType, StateType> {
   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,
-    selectedBranch: '',
-    subdirectory: '',
-    webhookToken: '',
+    selectedBranch: "",
+    subdirectory: "",
+    webhookToken: "",
     highlightCopyButton: false,
     action: {
-      git_repo: '',
-      image_repo_uri: '',
+      git_repo: "",
+      image_repo_uri: "",
       git_repo_id: 0,
-      dockerfile_path: '',
+      dockerfile_path: "",
     } as ActionConfigType,
-  }
+  };
 
   // TODO: read in set image from form context instead of config
   componentDidMount() {
     let { currentCluster, currentProject } = this.context;
 
     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) => {
-    this.setState({ saveValuesStatus: 'loading' });
+    this.setState({ saveValuesStatus: "loading" });
     let { currentCluster, currentProject } = this.context;
 
     // If tag is explicitly declared, parse tag
-    let imgSplits = img.split(':');
+    let imgSplits = img.split(":");
     let parsedTag = null;
     if (imgSplits.length > 1) {
       img = imgSplits[0];
@@ -92,28 +105,33 @@ export default class SettingsSection extends Component<PropsType, StateType> {
       image: {
         repository: img,
         tag: parsedTag || tag,
-      }
-    }
+      },
+    };
 
     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>
@@ -124,17 +142,17 @@ export default class SettingsSection extends Component<PropsType, StateType> {
     </Helper>
   */
   renderSourceSection = () => {
-    if (this.state.sourceType === 'registry') {
+    if (this.state.sourceType === "registry") {
       return (
         <>
           <Heading>Connected Source</Heading>
-          <Helper>
-            Specify a container image and tag.
-          </Helper>
+          <Helper>Specify a container image and tag.</Helper>
           <ImageSelector
             selectedImageUrl={this.state.selectedImageUrl}
             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 })}
             forceExpanded={true}
           />
@@ -145,48 +163,54 @@ export default class SettingsSection extends Component<PropsType, StateType> {
     let { currentProject } = this.context;
     return (
       <>
-        {this.state.action.git_repo.length > 0
-          ?
+        {this.state.action.git_repo.length > 0 ? (
           <>
             <Heading>Connected Source</Heading>
             <Holder>
               <InputRow
                 disabled={true}
-                label='Git Repository'
-                type='text'
-                width='100%'
+                label="Git Repository"
+                type="text"
+                width="100%"
                 value={this.state.action.git_repo}
                 setValue={(x: string) => console.log(x)}
               />
               <InputRow
                 disabled={true}
-                label='Dockerfile Path'
-                type='text'
-                width='100%'
+                label="Dockerfile Path"
+                type="text"
+                width="100%"
                 value={this.state.action.dockerfile_path}
                 setValue={(x: string) => console.log(x)}
               />
               <InputRow
                 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}
                 setValue={(x: string) => console.log(x)}
               />
             </Holder>
           </>
-          :
+        ) : (
           <>
             <Heading>Connect a Source</Heading>
             <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
-              </A> or
-              <Highlight onClick={() => this.setState({ sourceType: 'registry' })}>
+              </A>{" "}
+              or
+              <Highlight
+                onClick={() => this.setState({ sourceType: "registry" })}
+              >
                 link an image registry
-              </Highlight>.
+              </Highlight>
+              .
             </Helper>
             <RepoSelector
               chart={this.props.currentChart}
@@ -194,15 +218,21 @@ export default class SettingsSection extends Component<PropsType, StateType> {
               selectedRepo={this.state.selectedRepo}
               selectedBranch={this.state.selectedBranch}
               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 = () => {
     if (true || this.state.webhookToken) {
@@ -210,12 +240,14 @@ export default class SettingsSection extends Component<PropsType, StateType> {
       return (
         <>
           <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}>
             <div>{webhookText}</div>
-            <i 
+            <i
               className="material-icons"
-              onClick={() => { 
+              onClick={() => {
                 navigator.clipboard.writeText(webhookText);
                 this.setState({ highlightCopyButton: true });
               }}
@@ -227,7 +259,7 @@ export default class SettingsSection extends Component<PropsType, StateType> {
         </>
       );
     }
-  }
+  };
 
   render() {
     return (
@@ -236,16 +268,26 @@ export default class SettingsSection extends Component<PropsType, StateType> {
           {this.renderSourceSection()}
           {this.renderWebhookSection()}
           <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}
           </Button>
         </StyledSettingsSection>
         <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}
           makeFlush={true}
-          disabled={this.state.selectedImageUrl && this.state.selectedTag ? false : true}
+          disabled={
+            this.state.selectedImageUrl && this.state.selectedTag ? false : true
+          }
         />
       </Wrapper>
     );
@@ -260,19 +302,22 @@ const Button = styled.button`
   margin-top: 20px;
   margin-bottom: 30px;
   font-weight: 500;
-  font-family: 'Work Sans', sans-serif;
+  font-family: "Work Sans", sans-serif;
   color: white;
   padding: 6px 20px 7px 20px;
   text-align: left;
   border: 0;
   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;
-  :focus { outline: 0 }
+  :focus {
+    outline: 0;
+  }
   :hover {
-    filter: ${(props) => (!props.disabled ? 'brightness(120%)' : '')};
+    filter: ${(props) => (!props.disabled ? "brightness(120%)" : "")};
   }
 `;
 
@@ -293,10 +338,11 @@ const Webhook = styled.div`
   > div {
     user-select: all;
   }
-  
+
   > i {
     padding: 5px;
-    background: ${(props: { copiedToClipboard: boolean }) => props.copiedToClipboard ? '#616FEEcc' : '#ffffff22'};
+    background: ${(props: { copiedToClipboard: boolean }) =>
+      props.copiedToClipboard ? "#616FEEcc" : "#ffffff22"};
     border-radius: 5px;
     position: absolute;
     right: 10px;
@@ -305,7 +351,8 @@ const Webhook = styled.div`
     color: #ffffff;
 
     :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;
   margin-left: 5px;
   cursor: pointer;
-  padding-right: ${(props: { padRight?: boolean }) => props.padRight ? '5px' : ''};
+  padding-right: ${(props: { padRight?: boolean }) =>
+    props.padRight ? "5px" : ""};
 `;
 
 const A = styled.a`
@@ -323,7 +371,8 @@ const A = styled.a`
   text-decoration: underline;
   margin-left: 5px;
   cursor: pointer;
-  padding-right: ${(props: { padRight?: boolean }) => props.padRight ? '5px' : ''};
+  padding-right: ${(props: { padRight?: boolean }) =>
+    props.padRight ? "5px" : ""};
 `;
 
 const Wrapper = styled.div`
@@ -344,4 +393,4 @@ const StyledSettingsSection = styled.div`
 
 const Holder = styled.div`
   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 = {
-  currentChart: ChartType
-  refreshChart: () => void
+  currentChart: ChartType;
+  refreshChart: () => void;
 };
 
 type StateType = {
-  values: string,
-  saveValuesStatus: string | null
+  values: string;
+  saveValuesStatus: string | null;
 };
 
 // TODO: handle zoom out
 export default class ValuesYaml extends Component<PropsType, StateType> {
   state = {
-    values: '',
-    saveValuesStatus: null as (string | null)
-  }
+    values: "",
+    saveValuesStatus: null as string | null,
+  };
 
   updateValues() {
-    let values = '# Nothing here yet';
+    let values = "# Nothing here yet";
     if (this.props.currentChart.config) {
       values = yaml.dump(this.props.currentChart.config);
     }
@@ -46,26 +46,31 @@ export default class ValuesYaml extends Component<PropsType, StateType> {
 
   handleSaveValues = () => {
     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() {
     return (
@@ -77,7 +82,7 @@ export default class ValuesYaml extends Component<PropsType, StateType> {
           />
         </Wrapper>
         <SaveButton
-          text='Update Values'
+          text="Update Values"
           onClick={this.handleSaveValues}
           status={this.state.saveValuesStatus}
           makeFlush={true}
@@ -101,4 +106,4 @@ const StyledValuesYaml = styled.div`
   flex-direction: column;
   width: 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 = {
-  currentChart: ChartType,
+  currentChart: ChartType;
 };
 
 type StateType = {
-  events: any[],
-  loading: boolean,
+  events: any[];
+  loading: boolean;
 };
 
 export default class StatusSection extends Component<PropsType, StateType> {
   state = {
     events: [] as any[],
     loading: true,
-  }
+  };
 
   renderTabs = () => {
     return this.state.events.map((c, i) => {
-      return (
-        <EventTab />
-      )
-    })
-  }
+      return <EventTab />;
+    });
+  };
 
   renderStatusSection = () => {
     if (this.state.loading) {
       return (
-        <NoEvents> 
+        <NoEvents>
           <Loading />
         </NoEvents>
-      )
+      );
     }
     if (this.state.events.length > 0) {
-      return (
-        <Wrapper>
-          {this.renderTabs()}
-        </Wrapper>
-      )
+      return <Wrapper>{this.renderTabs()}</Wrapper>;
     } else {
       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>
-      )
+      );
     }
-  }
+  };
 
   componentDidMount() {
     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({events: [1, 2, 3], loading: false})
+    this.setState({ events: [1, 2, 3], loading: false });
   }
 
   render() {
     return (
-      <StyledDeploySection>
-        {this.renderStatusSection()}
-      </StyledDeploySection>
+      <StyledDeploySection>{this.renderStatusSection()}</StyledDeploySection>
     );
   }
 }
@@ -119,4 +112,4 @@ const NoEvents = styled.div`
     font-size: 18px;
     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> {
-  state = {
-  }
+  state = {};
 
   render() {
     return (
-      <StyledEventTab 
-        isLast={false}
-      >
+      <StyledEventTab isLast={false}>
         <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>
       </StyledEventTab>
     );
@@ -33,7 +25,8 @@ const StyledEventTab = styled.div`
   width: 100%;
   margin-bottom: 2px;
   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`
@@ -46,4 +39,4 @@ const EventHeader = styled.div`
   user-select: none;
   padding: 8px 18px;
   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;
 
 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 = {
-  showArrowHead: boolean
+  showArrowHead: boolean;
 };
 
 export default class Edge extends Component<PropsType, StateType> {
   state = {
-    showArrowHead: true
-  }
+    showArrowHead: true,
+  };
 
   render() {
     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 y1 = Math.round(originY - this.props.y1);
     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
-    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
-    var angle = Math.atan2((y1 - y2), (x1 - x2)) * (180 / Math.PI);
+    var angle = Math.atan2(y1 - y2, x1 - x2) * (180 / Math.PI);
 
     return (
       <StyledEdge
@@ -50,7 +50,9 @@ export default class Edge extends Component<PropsType, StateType> {
         onMouseLeave={() => setCurrentEdge(null)}
         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]} />
       </StyledEdge>
     );
@@ -58,32 +60,35 @@ export default class Edge extends Component<PropsType, StateType> {
 }
 
 const ArrowHead = styled.div`
-  width: 0; 
+  width: 0;
   height: 0;
   margin-left: 20px;
   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`
   height: 2px;
   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) => ({
   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;
   height: ${thickness}px;
   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;
   align-items: center;
   justify-content: center;
@@ -92,4 +97,4 @@ const StyledEdge: any = styled.div.attrs((props: any) => ({
       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 panConstant = 0.8;
 
 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
-  showRevisions: boolean
+  showRevisions: boolean;
 };
 
 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
@@ -59,18 +59,18 @@ export default class GraphDisplay extends Component<PropsType, StateType> {
     nodes: [] as NodeType[],
     edges: [] as EdgeType[],
     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,
     preventBgDrag: false,
     relocateAllowed: false,
@@ -78,26 +78,28 @@ export default class GraphDisplay extends Component<PropsType, StateType> {
     btnZooming: false,
     showKindLabels: true,
     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,
-    suppressDisplay: false
-  }
+    suppressDisplay: false,
+  };
 
   spaceRef: any = React.createRef();
 
   getRandomIntBetweenRange = (min: number, max: number) => {
     min = Math.ceil(min);
     max = Math.floor(max);
-    return Math.floor(Math.random() * (max - min) + min);  
-  }
+    return Math.floor(Math.random() * (max - min) + min);
+  };
 
   // Handle graph from localstorage
   getChartGraph = () => {
     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 edges = [] as EdgeType[];
     if (!graph) {
@@ -105,26 +107,29 @@ export default class GraphDisplay extends Component<PropsType, StateType> {
       edges = this.createEdges(components);
       this.setState({ nodes, edges });
     } 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);
     }
-  }
+  };
 
   componentDidMount() {
-
     // Initialize origin
     let height = this.spaceRef.offsetHeight;
     let width = this.spaceRef.offsetWidth;
     this.setState({
       originX: Math.round(width / 2),
-      originY: Math.round(height / 2)
+      originY: Math.round(height / 2),
     });
 
     // Suppress trackpad gestures
     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("keyup", this.handleKeyUp);
@@ -133,7 +138,7 @@ export default class GraphDisplay extends Component<PropsType, StateType> {
 
     window.onbeforeunload = () => {
       this.storeChartGraph();
-    }
+    };
   }
 
   // Live update on rollback/upgrade
@@ -146,48 +151,96 @@ export default class GraphDisplay extends Component<PropsType, StateType> {
 
   createNodes = (components: ResourceType[]) => {
     return components.map((c: ResourceType) => {
-      switch(c.Kind) {
+      switch (c.Kind) {
         case "ClusterRoleBinding":
         case "ClusterRole":
         case "RoleBinding":
         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 "StatefulSet":
         case "Pod":
         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 "Ingress":
         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:
-          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[]) => {
     let edges = [] as EdgeType[];
     components.map((c: ResourceType) => {
       c.Relations?.ControlRels?.map((rel: any) => {
         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) => {
         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) => {
         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;
-  }
+  };
 
   storeChartGraph = (props?: PropsType) => {
     let useProps = props || this.props;
@@ -207,20 +260,24 @@ export default class GraphDisplay extends Component<PropsType, StateType> {
       `charts.${currentChart.name}-${currentChart.version}`,
       JSON.stringify(graph)
     );
-  }
+  };
 
   componentWillUnmount() {
     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("keyup", this.handleKeyUp);
   }
 
   // Handle shift key for multi-select
   handleKeyDown = (e: any) => {
-    if (e.key === 'Shift') {
+    if (e.key === "Shift") {
       this.setState({
         anchorX: this.state.cursorX,
         anchorY: this.state.cursorY,
@@ -230,13 +287,13 @@ export default class GraphDisplay extends Component<PropsType, StateType> {
         panX: null,
         panY: null,
         deltaX: null,
-        deltaY: null
+        deltaY: null,
       });
     }
-  }
+  };
 
   handleKeyUp = (e: any) => {
-    if (e.key === 'Shift') {
+    if (e.key === "Shift") {
       this.setState({
         anchorX: null,
         anchorY: null,
@@ -245,16 +302,20 @@ export default class GraphDisplay extends Component<PropsType, StateType> {
         panX: null,
         panY: null,
         deltaX: null,
-        deltaY: null
+        deltaY: null,
       });
     }
-  }
+  };
 
   handleClickNode = (clickedId: number) => {
     let { cursorX, cursorY } = this.state;
 
     // 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
     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) => {
     let { cursorX, cursorY, nodeClickX, nodeClickY } = this.state;
@@ -281,7 +346,7 @@ export default class GraphDisplay extends Component<PropsType, StateType> {
     if (cursorX === nodeClickX && cursorY === nodeClickY) {
       this.setState({ openedNode: node });
     }
-  }
+  };
 
   handleMouseDown = () => {
     let { cursorX, cursorY } = this.state;
@@ -297,25 +362,48 @@ export default class GraphDisplay extends Component<PropsType, StateType> {
       deltaY: null,
       panX: null,
       panY: null,
-      scale: 1
-    })
-  }
+      scale: 1,
+    });
+  };
 
   handleMouseUp = () => {
-    let { cursorX, nodeClickX, cursorY, nodeClickY, suppressCloseNode } = this.state;
+    let {
+      cursorX,
+      nodeClickX,
+      cursorY,
+      nodeClickY,
+      suppressCloseNode,
+    } = this.state;
     this.setState({ dragBg: false, activeIds: [] });
 
     // 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 });
     } else if (this.state.suppressCloseNode) {
       this.setState({ suppressCloseNode: false });
     }
-  }
+  };
 
   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
     if (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 });
     }
 
-    // Check if within select region 
+    // Check if within select region
     if (anchorX && anchorY) {
       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);
           this.setState({ activeIds });
         }
       });
-    } 
-  }
+    }
+  };
 
   // Handle pan XOR zoom (two-finger gestures count as onWheel)
   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
     if (!this.state.suppressDisplay) {
-
       // Pinch/zoom sets e.ctrlKey to true
       if (e.ctrlKey) {
-  
         // 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;
         scale -= deltaY * zoomConstant;
@@ -368,12 +458,12 @@ export default class GraphDisplay extends Component<PropsType, StateType> {
   };
 
   btnZoomIn = () => {
-    this.setState({ scale: 1.24, btnZooming: true});
-  }
+    this.setState({ scale: 1.24, btnZooming: true });
+  };
 
   btnZoomOut = () => {
     this.setState({ scale: 0.76, btnZooming: true });
-  }
+  };
 
   toggleExpanded = () => {
     this.setState({ isExpanded: !this.state.isExpanded }, () => {
@@ -388,32 +478,48 @@ export default class GraphDisplay extends Component<PropsType, StateType> {
       }
       this.setState({
         originX: Math.round(width / 2) - nudge,
-        originY: Math.round(height / 2)
-      });  
+        originY: Math.round(height / 2),
+      });
     });
-  }
+  };
 
   // Pass origin to node for offset
   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 maxX = 0;
     let minY = 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) => {
       // 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.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) {
         node.x -= panConstant * panX;
         node.y += panConstant * panY;
       }
-      
+
       return (
         <Node
           key={i}
@@ -452,13 +558,14 @@ export default class GraphDisplay extends Component<PropsType, StateType> {
           isActive={activeIds.includes(node.id)}
           showKindLabels={this.state.showKindLabels}
           isOpen={node === this.state.openedNode}
-
           // Parameterized to allow setting to null
-          setCurrentNode={(node: NodeType) => this.setState({ currentNode: node })}
+          setCurrentNode={(node: NodeType) =>
+            this.setState({ currentNode: node })
+          }
         />
       );
     });
-  }
+  };
 
   renderEdges = () => {
     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}
           y2={this.state.nodes[edge.target].y}
           edge={edge}
-          setCurrentEdge={(edge: EdgeType) => this.setState({ currentEdge: edge })}
+          setCurrentEdge={(edge: EdgeType) =>
+            this.setState({ currentEdge: edge })
+          }
         />
       );
     });
-  }
+  };
 
   renderSelectRegion = () => {
     if (this.state.anchorX && this.state.anchorY) {
@@ -491,13 +600,13 @@ export default class GraphDisplay extends Component<PropsType, StateType> {
         />
       );
     }
-  }
+  };
 
   render() {
     return (
       <StyledGraphDisplay
         isExpanded={this.state.isExpanded}
-        ref={element => this.spaceRef = element}
+        ref={(element) => (this.spaceRef = element)}
         onMouseMove={this.handleMouseMove}
         onMouseDown={this.state.suppressDisplay ? null : this.handleMouseDown}
         onMouseUp={this.state.suppressDisplay ? null : this.handleMouseUp}
@@ -512,38 +621,37 @@ export default class GraphDisplay extends Component<PropsType, StateType> {
           onMouseLeave={() => this.setState({ suppressDisplay: false })}
         >
           <ToggleLabel
-            onClick={() => this.setState({ showKindLabels: !this.state.showKindLabels })}
+            onClick={() =>
+              this.setState({ showKindLabels: !this.state.showKindLabels })
+            }
           >
             <Checkbox checked={this.state.showKindLabels}>
-                <i className="material-icons">done</i>
+              <i className="material-icons">done</i>
             </Checkbox>
             Show Type
           </ToggleLabel>
-          <ExpandButton
-            onClick={this.toggleExpanded}
-          >
+          <ExpandButton onClick={this.toggleExpanded}>
             <i className="material-icons">
-              {this.state.isExpanded ? 'close_fullscreen' : 'open_in_full'}
+              {this.state.isExpanded ? "close_fullscreen" : "open_in_full"}
             </i>
           </ExpandButton>
         </ButtonSection>
         <InfoPanel
-          setSuppressDisplay={(x: boolean) => this.setState({ suppressDisplay: x })}
+          setSuppressDisplay={(x: boolean) =>
+            this.setState({ suppressDisplay: x })
+          }
           currentNode={this.state.currentNode}
           currentEdge={this.state.currentEdge}
           openedNode={this.state.openedNode}
-
           // 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
           isExpanded={this.state.isExpanded}
           showRevisions={this.props.showRevisions}
         />
-        <ZoomPanel
-          btnZoomIn={this.btnZoomIn}
-          btnZoomOut={this.btnZoomOut}
-        />
+        <ZoomPanel btnZoomIn={this.btnZoomIn} btnZoomOut={this.btnZoomOut} />
       </StyledGraphDisplay>
     );
   }
@@ -555,7 +663,8 @@ const Checkbox = styled.div`
   border: 1px solid #ffffff55;
   margin: 0px 8px 0px 3px;
   border-radius: 3px;
-  background: ${(props: { checked: boolean }) => props.checked ? '#ffffff22' : ''};
+  background: ${(props: { checked: boolean }) =>
+    props.checked ? "#ffffff22" : ""};
   display: flex;
   align-items: center;
   justify-content: center;
@@ -564,12 +673,12 @@ const Checkbox = styled.div`
   > i {
     font-size: 12px;
     padding-left: 0px;
-    display: ${(props: { checked: boolean }) => props.checked ? '' : 'none'};
+    display: ${(props: { checked: boolean }) => (props.checked ? "" : "none")};
   }
 `;
 
 const ToggleLabel = styled.div`
-  font: 12px 'Work Sans';
+  font: 12px "Work Sans";
   color: #ffffff;
   position: relative;
   height: 24px;
@@ -611,7 +720,7 @@ const ExpandButton = styled.div`
   border: 1px solid #ffffff55;
 
   :hover {
-    background: #ffffff44; 
+    background: #ffffff44;
   }
 
   > i {
@@ -622,10 +731,14 @@ const ExpandButton = styled.div`
 const StyledGraphDisplay = styled.div`
   overflow: hidden;
   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;
-  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 = {
-  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 = {
-  wrapperHeight: number
+  wrapperHeight: number;
 };
 
 export default class InfoPanel extends Component<PropsType, StateType> {
   state = {
-    wrapperHeight: 0
-  }
+    wrapperHeight: 0,
+  };
 
   renderIcon = (kind: string) => {
-
-    let icon = 'tonality';
+    let icon = "tonality";
     if (Object.keys(kindToIcon).includes(kind)) {
-      icon = kindToIcon[kind]; 
+      icon = kindToIcon[kind];
     }
-    
+
     return (
       <IconWrapper>
         <i className="material-icons">{icon}</i>
       </IconWrapper>
     );
-  }
+  };
 
   renderColorBlock = (type: string) => {
     return <ColorBlock color={edgeColors[type]} />;
-  }
+  };
 
   wrapperRef: any = React.createRef();
 
@@ -51,9 +50,11 @@ export default class InfoPanel extends Component<PropsType, StateType> {
   }
 
   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 });
     }
@@ -67,27 +68,23 @@ export default class InfoPanel extends Component<PropsType, StateType> {
           <Div>
             {this.renderIcon(openedNode.kind)}
             {openedNode.kind}
-            <ResourceName>
-              {openedNode.name}
-            </ResourceName>
+            <ResourceName>{openedNode.name}</ResourceName>
           </Div>
-          <YamlWrapper ref={element => this.wrapperRef = element}>
+          <YamlWrapper ref={(element) => (this.wrapperRef = element)}>
             <YamlEditor
               value={yaml.dump(openedNode.RawYAML)}
               readOnly={true}
-              height={this.state.wrapperHeight + 'px'}
+              height={this.state.wrapperHeight + "px"}
             />
           </YamlWrapper>
         </Wrapped>
-      )
+      );
     } else if (currentNode) {
       return (
         <Div>
           {this.renderIcon(currentNode.kind)}
           {currentNode.kind}
-          <ResourceName>
-            {currentNode.name}
-          </ResourceName>
+          <ResourceName>{currentNode.name}</ResourceName>
         </Div>
       );
     } else if (currentEdge) {
@@ -96,7 +93,7 @@ export default class InfoPanel extends Component<PropsType, StateType> {
           {this.renderColorBlock(currentEdge.type)}
           {this.renderEdgeMessage(currentEdge)}
         </EdgeInfo>
-      )
+      );
     }
 
     return (
@@ -106,20 +103,20 @@ export default class InfoPanel extends Component<PropsType, StateType> {
         </IconWrapper>
         Hover over a node or edge to display info.
       </Div>
-    )
-  }
+    );
+  };
 
   renderEdgeMessage = (edge: EdgeType) => {
     // TODO: render more information about edges (labels, spec property field)
-    switch(edge.type) {
+    switch (edge.type) {
       case "ControlRel":
-        return "Controller Relation"
+        return "Controller Relation";
       case "LabelRel":
-        return "Label Relation"
+        return "Label Relation";
       case "SpecRel":
-        return "Spec Relation"
+        return "Spec Relation";
     }
-  }
+  };
 
   render() {
     let { openedNode, closeNode, setSuppressDisplay } = this.props;
@@ -133,7 +130,11 @@ export default class InfoPanel extends Component<PropsType, StateType> {
       >
         {this.renderContents()}
 
-        {openedNode ? <i onClick={closeNode} className="material-icons">close</i> : null}
+        {openedNode ? (
+          <i onClick={closeNode} className="material-icons">
+            close
+          </i>
+        ) : null}
       </StyledInfoPanel>
     );
   }
@@ -163,7 +164,8 @@ const ColorBlock = styled.div`
   border-radius: 3px;
   margin-left: -2px;
   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`
@@ -209,11 +211,13 @@ const StyledInfoPanel = styled.div`
   right: 15px;
   bottom: 15px;
   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;
   min-width: 400px;
-  background: #34373Cdf;
+  background: #34373cdf;
   border-radius: 3px;
   padding-left: 11px;
   display: inline-block;
@@ -238,4 +242,4 @@ const StyledInfoPanel = styled.div`
       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 = {
-  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> {
-  state = {
-  }
+  state = {};
 
   render() {
     let { x, y, w, h, name, kind } = this.props.node;
     let { originX, originY, nodeMouseDown, nodeMouseUp, isActive } = this.props;
 
-    let icon = 'tonality';
+    let icon = "tonality";
     if (Object.keys(kindToIcon).includes(kind)) {
-      icon = kindToIcon[kind]; 
+      icon = kindToIcon[kind];
     }
 
     return (
       <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)}
         h={Math.round(h)}
       >
-        <Kind>
-          {this.props.showKindLabels ? kind : null}
-        </Kind>
-        <NodeBlock 
+        <Kind>{this.props.showKindLabels ? kind : null}</Kind>
+        <NodeBlock
           onMouseDown={nodeMouseDown}
           onMouseUp={nodeMouseUp}
           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>
         </NodeBlock>
-        <NodeLabel>
-          {name}
-        </NodeLabel>
+        <NodeLabel>{name}</NodeLabel>
       </StyledNode>
     );
   }
@@ -70,7 +64,7 @@ const Kind = styled.div`
   min-width: 1px;
   height: 25px;
   font-size: 13px;
-  font-family: 'Work Sans', sans-serif;
+  font-family: "Work Sans", sans-serif;
   white-space: nowrap;
   overflow: hidden;
   text-overflow: ellipsis;
@@ -84,7 +78,7 @@ const NodeLabel = styled.div`
   color: #aaaabb;
   max-width: 140px;
   font-size: 13px;
-  font-family: 'Work Sans', sans-serif;
+  font-family: "Work Sans", sans-serif;
   text-align: center;
   white-space: nowrap;
   overflow: hidden;
@@ -100,8 +94,10 @@ const NodeBlock = styled.div`
   align-items: center;
   justify-content: center;
   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;
   cursor: pointer;
   :hover {
@@ -115,16 +111,16 @@ const NodeBlock = styled.div`
 
 const StyledNode: any = styled.div.attrs((props: NodeType) => ({
   style: {
-    top: props.y + 'px',
-    left: props.x + 'px',
-    },
+    top: props.y + "px",
+    left: props.x + "px",
+  },
 }))`
   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;
   border-radius: 100px;
   display: flex;
   flex-direction: column;
   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 = {
-  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> {
-  state = {
-  }
+  state = {};
 
   render() {
     let { cursorX, cursorY, anchorX, anchorY, originX, originY } = this.props;
-    
+
     var x, y, w, h;
     if (cursorY < 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;
   background: #ffffff22;
   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 = {
-    btnZoomIn: () => void,
-    btnZoomOut: () => void,
+  btnZoomIn: () => void;
+  btnZoomOut: () => void;
 };
 
 type StateType = {
-  wrapperHeight: number
+  wrapperHeight: number;
 };
 
 export default class ZoomPanel extends Component<PropsType, StateType> {
   state = {
-    wrapperHeight: 0
-  }
+    wrapperHeight: 0,
+  };
 
   wrapperRef: any = React.createRef();
 
@@ -32,15 +32,11 @@ export default class ZoomPanel extends Component<PropsType, StateType> {
           <i className="material-icons">remove</i>
         </IconWrapper>
       </Div>
-    )
-  }
+    );
+  };
 
   render() {
-    return (
-      <StyledZoomer>
-        {this.renderContents()}
-      </StyledZoomer>
-    );
+    return <StyledZoomer>{this.renderContents()}</StyledZoomer>;
   }
 }
 
@@ -75,7 +71,7 @@ const StyledZoomer = styled.div`
   color: #ffffff;
   height: 64px;
   width: 36px;
-  background: #34373Cdf;
+  background: #34373cdf;
   border-radius: 3px;
   padding-left: 11px;
   display: inline-block;
@@ -92,4 +88,4 @@ const ZoomBreaker = styled.div`
   background: #ffffff20;
   height: 1px;
   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 = {
-  controller: any,
-  selectedPod: any,
-  selectPod: Function,
-  isLast?: boolean,
-  isFirst?: boolean,
+  controller: any;
+  selectedPod: any;
+  selectPod: Function;
+  isLast?: boolean;
+  isFirst?: boolean;
 };
 
 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.
@@ -25,54 +25,60 @@ export default class ControllerTab extends Component<PropsType, StateType> {
     pods: [] as any[],
     raw: [] as any[],
     showTooltip: [] as boolean[],
-  }
+  };
 
   componentDidMount() {
     let { currentCluster, currentProject, setCurrentError } = this.context;
     let { controller, selectPod, isFirst } = this.props;
 
     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 selector = '';
+    let selector = "";
     for (var key in ml) {
-      selector += key + '=' + ml[key];
+      selector += key + "=" + ml[key];
       if (i != Object.keys(ml).length) {
-        selector += ',';
+        selector += ",";
       }
       i += 1;
     }
     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) => {
@@ -80,50 +86,55 @@ export default class ControllerTab extends Component<PropsType, StateType> {
       case "deployment":
       case "replicaset":
         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":
-       return [c.status?.readyReplicas || 0, c.status?.replicas || 0]
+        return [c.status?.readyReplicas || 0, c.status?.replicas || 0];
       case "daemonset":
-        return [c.status?.numberAvailable || 0, c.status?.desiredNumberScheduled || 0]
-      }
-  }
+        return [
+          c.status?.numberAvailable || 0,
+          c.status?.desiredNumberScheduled || 0,
+        ];
+    }
+  };
 
   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'
     }
 
-    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) => {
         if (s.state?.waiting) {
-          collatedStatus = 'waiting'
+          collatedStatus = "waiting";
         } else if (s.state?.terminated) {
-          collatedStatus = 'failed'
+          collatedStatus = "failed";
         }
-      })
+      });
       return collatedStatus;
     }
-  }
+  };
 
   renderTooltip = (x: string, ind: number): JSX.Element | undefined => {
     if (this.state.showTooltip[ind]) {
       return <Tooltip>{x}</Tooltip>;
     }
-  }
+  };
 
   render() {
     let { controller, selectedPod, isLast, selectPod, isFirst } = this.props;
     let [available, total] = this.getAvailability(controller.kind, controller);
-    let status = (available == total) ? 'running' : 'waiting'
+    let status = available == total ? "running" : "waiting";
     return (
       <ResourceTab
         label={controller.kind}
@@ -132,43 +143,43 @@ export default class ControllerTab extends Component<PropsType, StateType> {
         isLast={isLast}
         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>
     );
   }
@@ -178,7 +189,8 @@ ControllerTab.contextType = Context;
 
 const Rail = styled.div`
   width: 2px;
-  background: ${(props: { lastTab?: boolean }) => props.lastTab ? '' : '#52545D'};
+  background: ${(props: { lastTab?: boolean }) =>
+    props.lastTab ? "" : "#52545D"};
   height: 50%;
 `;
 
@@ -187,7 +199,7 @@ const Circle = styled.div`
   min-height: 2px;
   margin-bottom: -2px;
   margin-left: 8px;
-  background: #52545D;
+  background: #52545d;
 `;
 
 const Gutter = styled.div`
@@ -209,12 +221,16 @@ const Status = styled.div`
   text-transform: capitalize;
   justify-content: flex-end;
   align-items: center;
-  font-family: 'Work Sans', sans-serif;
+  font-family: "Work Sans", sans-serif;
   color: #aaaabb;
   animation: fadeIn 0.5s;
   @keyframes fadeIn {
-    from { opacity: 0 }
-    to { opacity: 1 }
+    from {
+      opacity: 0;
+    }
+    to {
+      opacity: 1;
+    }
   }
 `;
 
@@ -223,7 +239,12 @@ const StatusColor = styled.div`
   width: 7px;
   min-width: 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;
 `;
 
@@ -260,8 +281,12 @@ const Tooltip = styled.div`
   animation: faded-in 0.2s 0.15s;
   animation-fill-mode: forwards;
   @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;
   align-items: center;
   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;
   padding: 20px 19px 20px 42px;
   text-shadow: 0px 0px 8px none;
@@ -283,4 +310,4 @@ const Tab = styled.div`
     color: white;
     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 = {
-  selectedPod: any,
+  selectedPod: any;
 };
 
 type StateType = {
-  logs: string[],
-  ws: any,
-  scroll: boolean,
+  logs: string[];
+  ws: any;
+  scroll: boolean;
 };
 
 export default class Logs extends Component<PropsType, StateType> {
-  
   state = {
     logs: [] as string[],
-    ws : null as any,
+    ws: null as any,
     scroll: true,
-  }
+  };
 
   ws = null as any;
-  parentRef = React.createRef<HTMLDivElement>()
+  parentRef = React.createRef<HTMLDivElement>();
 
   scrollToBottom = (smooth: boolean) => {
     if (smooth) {
-      this.parentRef.current.lastElementChild.scrollIntoView({ behavior: "smooth" })
+      this.parentRef.current.lastElementChild.scrollIntoView({
+        behavior: "smooth",
+      });
     } else {
-      this.parentRef.current.lastElementChild.scrollIntoView({ behavior: "auto" })
+      this.parentRef.current.lastElementChild.scrollIntoView({
+        behavior: "auto",
+      });
     }
-  }
+  };
 
   renderLogs = () => {
     let { selectedPod } = this.props;
     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) {
-      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 <Log key={i}>{log}</Log>
-    })
-  }
+      return <Log key={i}>{log}</Log>;
+    });
+  };
 
-  setupWebsocket = () => {  
+  setupWebsocket = () => {
     let { currentCluster, currentProject } = this.context;
     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 = () => {
-      console.log('connected to websocket')
-    }
+      console.log("connected to websocket");
+    };
 
     this.ws.onmessage = (evt: MessageEvent) => {
       this.setState({ logs: [...this.state.logs, evt.data] }, () => {
         if (this.state.scroll) {
-          this.scrollToBottom(false)
+          this.scrollToBottom(false);
         }
-      })
-    }
+      });
+    };
 
     this.ws.onerror = (err: ErrorEvent) => {
-      console.log("websocket error:", err)
-    }
+      console.log("websocket error:", err);
+    };
 
     this.ws.onclose = () => {
-      console.log("closing pod logs")
-    }
-  }
+      console.log("closing pod logs");
+    };
+  };
 
   refreshLogs = () => {
     if (this.ws) {
       this.ws.close();
       this.ws = null;
-      this.setState({logs: []})
+      this.setState({ logs: [] });
       this.setupWebsocket();
     }
-  }
+  };
 
   componentDidMount() {
-    this.setupWebsocket()
+    this.setupWebsocket();
     this.scrollToBottom(false);
   }
 
   componentWillUnmount() {
-    console.log('log unmount')
+    console.log("log unmount");
     if (this.ws) {
-      this.ws.close()
+      this.ws.close();
     }
   }
 
   render() {
     return (
       <LogStream>
-        <Wrapper ref={this.parentRef}>
-          {this.renderLogs()}
-        </Wrapper>
+        <Wrapper ref={this.parentRef}>{this.renderLogs()}</Wrapper>
         <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>
-          <Refresh onClick={() => {this.refreshLogs()}}>
+          <Refresh
+            onClick={() => {
+              this.refreshLogs();
+            }}
+          >
             <i className="material-icons">autorenew</i>
             Refresh
           </Refresh>
@@ -139,7 +152,7 @@ const Scroll = styled.div`
     margin-right: 6px;
     pointer-events: none;
   }
-`
+`;
 
 const Refresh = styled.div`
   display: flex;
@@ -158,7 +171,7 @@ const Refresh = styled.div`
   :hover {
     background: #2468d6;
   }
-`
+`;
 
 const Options = styled.div`
   width: 100%;
@@ -168,7 +181,7 @@ const Options = styled.div`
   flex-direction: row;
   align-items: center;
   justify-content: space-between;
-`
+`;
 
 const Wrapper = styled.div`
   width: 100%;
@@ -187,7 +200,7 @@ const LogStream = styled.div`
   user-select: text;
   max-width: 65%;
   overflow-y: auto;
-  overflow-wrap: break-word; 
+  overflow-wrap: break-word;
 `;
 
 const Message = styled.div`
@@ -202,4 +215,4 @@ const Message = styled.div`
 
 const Log = styled.div`
   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 = {
-  selectors: string[],
-  currentChart: ChartType,
+  selectors: string[];
+  currentChart: ChartType;
 };
 
 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> {
@@ -29,90 +29,94 @@ export default class StatusSection extends Component<PropsType, StateType> {
     selectedPod: {} as any,
     controllers: [] as any[],
     loading: true,
-  }
+  };
 
   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) => {
     this.setState({
-      selectedPod: pod
-    })
-  }
+      selectedPod: pod,
+    });
+  };
 
   renderTabs = () => {
     return this.state.controllers.map((c, i) => {
       return (
-        <ControllerTab 
-          key={c.metadata.uid} 
-          selectedPod={this.state.selectedPod} 
+        <ControllerTab
+          key={c.metadata.uid}
+          selectedPod={this.state.selectedPod}
           selectPod={this.selectPod.bind(this)}
           controller={c}
           isLast={i === this.state.controllers.length - 1}
           isFirst={i === 0}
         />
-      )
-    })
-  }
+      );
+    });
+  };
 
   renderStatusSection = () => {
     if (this.state.loading) {
       return (
-        <NoControllers> 
+        <NoControllers>
           <Loading />
         </NoControllers>
-      )
+      );
     }
     if (this.state.controllers.length > 0) {
       return (
         <Wrapper>
-          <TabWrapper>
-            {this.renderTabs()}
-          </TabWrapper>
+          <TabWrapper>{this.renderTabs()}</TabWrapper>
           {this.renderLogs()}
         </Wrapper>
-      )
+      );
     } else {
       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>
-      )
+      );
     }
-  }
+  };
 
   componentDidMount() {
     const { selectors, currentChart } = this.props;
     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() {
     return (
-      <StyledStatusSection>
-        {this.renderStatusSection()}
-      </StyledStatusSection>
+      <StyledStatusSection>{this.renderStatusSection()}</StyledStatusSection>
     );
   }
 }
@@ -157,4 +161,4 @@ const NoControllers = styled.div`
     font-size: 18px;
     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 StateType = {
-  loading: boolean,
-  error: string,
-  clusters: ClusterType[],
+  loading: boolean;
+  error: string;
+  clusters: ClusterType[];
 };
 
 class Templates extends Component<PropsType, StateType> {
   state = {
     loading: true,
-    error: '',
+    error: "",
     clusters: [] as ClusterType[],
-  }
+  };
 
   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 = () => {
     return (
-      <DashboardIcon><i className="material-icons">device_hub</i></DashboardIcon>
+      <DashboardIcon>
+        <i className="material-icons">device_hub</i>
+      </DashboardIcon>
     );
-  }
+  };
 
   renderClusters = () => {
     return this.state.clusters.map((cluster: ClusterType, i: number) => {
       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}
         >
           {this.renderIcon()}
-          <TemplateTitle>
-            {cluster.name}
-          </TemplateTitle>
+          <TemplateTitle>{cluster.name}</TemplateTitle>
         </TemplateBlock>
       );
     });
-  }
-  
+  };
+
   render() {
     return (
       <>
         <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;
 
-export default withRouter(Templates)
+export default withRouter(Templates);
 
 const DashboardIcon = styled.div`
   position: relative;
@@ -84,7 +87,7 @@ const DashboardIcon = styled.div`
   display: flex;
   align-items: center;
   justify-content: center;
-  background: #676C7C;
+  background: #676c7c;
   border: 2px solid #8e94aa;
 
   > i {
@@ -126,8 +129,12 @@ const TemplateBlock = styled.div`
 
   animation: fadeIn 0.3s 0s;
   @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`
   font-size: 24px;
   font-weight: 600;
-  font-family: 'Work Sans', sans-serif;
+  font-family: "Work Sans", sans-serif;
   color: #ffffff;
   white-space: nowrap;
   overflow: hidden;
@@ -164,7 +171,7 @@ const TitleSection = styled.div`
       margin-bottom: -2px;
       font-size: 18px;
       margin-left: 18px;
-      color: #858FAAaa;
+      color: #858faaaa;
       cursor: pointer;
       :hover {
         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 = {
-  currentCluster: ClusterType,
+  currentCluster: ClusterType;
 };
 
 type StateType = {
-  loading: boolean,
+  loading: boolean;
 };
 
-export default class ClusterPlaceholder extends Component<PropsType, StateType> {
+export default class ClusterPlaceholder extends Component<
+  PropsType,
+  StateType
+> {
   state = {
     loading: true,
-  }
+  };
 
   componentDidMount() {
     setTimeout(() => {
@@ -48,18 +50,18 @@ export default class ClusterPlaceholder extends Component<PropsType, StateType>
             This project currently has no clusters connected.
           </Banner>
           <StyledStatusPlaceholder>
-            <Highlight onClick={() => {
-              this.context.setCurrentModal('ClusterInstructionsModal', {});
-            }}>
+            <Highlight
+              onClick={() => {
+                this.context.setCurrentModal("ClusterInstructionsModal", {});
+              }}
+            >
               + Connect a Cluster
             </Highlight>
           </StyledStatusPlaceholder>
         </>
       );
     } else {
-      return (
-        <ClusterList/>
-      );
+      return <ClusterList />;
     }
   }
 }
@@ -111,6 +113,6 @@ const StyledStatusPlaceholder = styled.div`
   display: flex;
   align-items: center;
   justify-content: center;
-  font-family: 'Work Sans', sans-serif;
+  font-family: "Work Sans", sans-serif;
   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 StateType = {
-};
+type StateType = {};
 
 // 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() {
-    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 & {
-  projectId: number | null,
+  projectId: number | null;
 };
 
 type StateType = {
-  infras: InfraType[],
+  infras: InfraType[];
 };
 
 class Dashboard extends Component<PropsType, StateType> {
   state = {
     infras: [] as InfraType[],
-  }
+  };
 
   refreshInfras = () => {
     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() {
     this.refreshInfras();
   }
@@ -50,7 +54,7 @@ class Dashboard extends Component<PropsType, StateType> {
 
   onShowProjectSettings = () => {
     this.props.history.push("project-settings");
-  }
+  };
 
   render() {
     let { currentProject, currentCluster } = this.context;
@@ -61,30 +65,27 @@ class Dashboard extends Component<PropsType, StateType> {
         {currentProject && (
           <DashboardWrapper>
             <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>
               {this.context.currentProject.roles.filter((obj: any) => {
                 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
                 </i>
-              }
+              )}
             </TitleSection>
 
             <InfoSection>
               <TopRow>
                 <InfoLabel>
                   <i className="material-icons">info</i> Info
-              </InfoLabel>
+                </InfoLabel>
               </TopRow>
               <Description>
                 Project overview for {currentProject && currentProject.name}.
@@ -93,21 +94,17 @@ class Dashboard extends Component<PropsType, StateType> {
 
             <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>
         )}
       </>
@@ -156,10 +153,10 @@ const InfoLabel = styled.div`
   height: 20px;
   display: flex;
   align-items: center;
-  color: #7A838F;
+  color: #7a838f;
   font-size: 13px;
   > i {
-    color: #8B949F;
+    color: #8b949f;
     font-size: 18px;
     margin-right: 5px;
   }
@@ -167,7 +164,7 @@ const InfoLabel = styled.div`
 
 const InfoSection = styled.div`
   margin-top: 20px;
-  font-family: 'Work Sans', sans-serif;
+  font-family: "Work Sans", sans-serif;
   margin-left: 0px;
   margin-bottom: 35px;
 `;
@@ -192,7 +189,7 @@ const Overlay = styled.div`
   justify-content: center;
   font-size: 24px;
   font-weight: 500;
-  font-family: 'Work Sans', sans-serif;
+  font-family: "Work Sans", sans-serif;
   color: white;
 `;
 
@@ -219,7 +216,7 @@ const DashboardIcon = styled.div`
 const Title = styled.div`
   font-size: 20px;
   font-weight: 500;
-  font-family: 'Work Sans', sans-serif;
+  font-family: "Work Sans", sans-serif;
   margin-left: 18px;
   color: #ffffff;
   white-space: nowrap;
@@ -248,4 +245,4 @@ const TitleSection = styled.div`
     }
     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> {
-  state = {
-  }
+  state = {};
 
   render() {
-    return (
-      <StyledPipelinesSection>
-        
-      </StyledPipelinesSection>
-    );
+    return <StyledPipelinesSection></StyledPipelinesSection>;
   }
 }
 
@@ -31,5 +24,5 @@ const StyledPipelinesSection = styled.div`
   text-align: center;
   font-size: 13px;
   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 = {
-  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> {
   renderContents = () => {
@@ -22,14 +19,16 @@ export default class IntegrationList extends Component<PropsType, StateType> {
     console.log(`integrations: ${integrations}`);
     if (titles && titles.length > 0) {
       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 disabled = integration === 'kubernetes' || integration === 'repo';
+        let disabled = integration === "kubernetes" || integration === "repo";
         return (
           <Integration
             key={i}
-            onClick={() => disabled ? null : setCurrent(integration)}
+            onClick={() => (disabled ? null : setCurrent(integration))}
             isCategory={isCategory}
             disabled={disabled}
           >
@@ -40,19 +39,23 @@ export default class IntegrationList extends Component<PropsType, StateType> {
                 <Subtitle>{subtitle}</Subtitle>
               </Description>
             </Flex>
-            <i className="material-icons">{isCategory ? 'launch' : 'more_vert'}</i>
+            <i className="material-icons">
+              {isCategory ? "launch" : "more_vert"}
+            </i>
           </Integration>
         );
       });
     } else if (integrations && integrations.length > 0) {
       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 (
           <Integration
             key={i}
-            onClick={() => disabled ? null : setCurrent(integration)}
+            onClick={() => (disabled ? null : setCurrent(integration))}
             isCategory={isCategory}
             disabled={disabled}
           >
@@ -60,23 +63,19 @@ export default class IntegrationList extends Component<PropsType, StateType> {
               <Icon src={icon && icon} />
               <Label>{label}</Label>
             </Flex>
-            <i className="material-icons">{isCategory ? 'launch' : 'more_vert'}</i>
+            <i className="material-icons">
+              {isCategory ? "launch" : "more_vert"}
+            </i>
           </Integration>
         );
       });
     }
-    return (
-      <Placeholder>
-        No integrations set up yet.
-      </Placeholder>
-    );
-  }
-  
+    return <Placeholder>No integrations set up yet.</Placeholder>;
+  };
+
   render() {
-    return ( 
-      <StyledIntegrationList>
-        {this.renderContents()}
-      </StyledIntegrationList>
+    return (
+      <StyledIntegrationList>{this.renderContents()}</StyledIntegrationList>
     );
   }
 }
@@ -96,15 +95,18 @@ const Integration = styled.div`
   justify-content: space-between;
   padding: 25px;
   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;
   border-radius: 5px;
   box-shadow: 0 5px 8px 0px #00000033;
   :hover {
-    background: ${(props: { isCategory: boolean, disabled: boolean }) => props.disabled ? '' : '#ffffff11'};
+    background: ${(props: { isCategory: boolean; disabled: boolean }) =>
+      props.disabled ? "" : "#ffffff11"};
 
     > 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;
     font-size: 18px;
     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;
   }
 `;
@@ -149,7 +152,7 @@ const Placeholder = styled.div`
   display: flex;
   align-items: center;
   font-size: 13px;
-  font-family: 'Work Sans', sans-serif;
+  font-family: "Work Sans", sans-serif;
   justify-content: center;
   margin-top: 30px;
   background: #ffffff11;
@@ -159,4 +162,4 @@ const Placeholder = styled.div`
 
 const StyledIntegrationList = styled.div`
   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 = {
-  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> {
@@ -27,103 +25,139 @@ export default class Integrations extends Component<PropsType, StateType> {
     currentOptions: [] as any[],
     currentTitles: [] as any[],
     currentIntegrationData: [] as any[],
-  }
+  };
 
   // TODO: implement once backend is restructured
   getIntegrations = (categoryType: string) => {
     let { currentProject } = this.context;
-    this.setState({ currentOptions: [], currentTitles: [], currentIntegrationData: [] });
+    this.setState({
+      currentOptions: [],
+      currentTitles: [],
+      currentIntegrationData: [],
+    });
     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;
-      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;
-      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;
       default:
-        console.log('Unknown integration category.');
+        console.log("Unknown integration category.");
     }
-  }
+  };
 
   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);
     }
   }
 
   renderIntegrationContents = () => {
     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) {
         return (
           <div>
             <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 />
           </div>
         );
       }
     }
-  }
+  };
 
   renderContents = () => {
     let { currentCategory, currentIntegration } = this.state;
 
     // TODO: Split integration page into separate component
     if (currentIntegration) {
-      let icon = integrationList[currentIntegration] && integrationList[currentIntegration].icon;
+      let icon =
+        integrationList[currentIntegration] &&
+        integrationList[currentIntegration].icon;
       return (
         <div>
           <TitleSectionAlt>
             <Flex>
-              <i className="material-icons" onClick={() => this.setState({ currentIntegration: null })}>
+              <i
+                className="material-icons"
+                onClick={() => this.setState({ currentIntegration: null })}
+              >
                 keyboard_backspace
               </i>
               <Icon src={icon && icon} />
@@ -131,7 +165,7 @@ export default class Integrations extends Component<PropsType, StateType> {
             </Flex>
           </TitleSectionAlt>
           {this.renderIntegrationContents()}
-          <IntegrationForm 
+          <IntegrationForm
             integrationName={currentIntegration}
             closeForm={() => {
               this.setState({ currentIntegration: null });
@@ -142,25 +176,37 @@ export default class Integrations extends Component<PropsType, StateType> {
         </div>
       );
     } 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 (
         <div>
           <TitleSectionAlt>
             <Flex>
-              <i className="material-icons" onClick={() => this.setState({ currentCategory: null })}>
+              <i
+                className="material-icons"
+                onClick={() => this.setState({ currentCategory: null })}
+              >
                 keyboard_backspace
               </i>
               <Icon src={icon && icon} />
               <Title>{label}</Title>
             </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>
               {buttonText}
@@ -184,20 +230,16 @@ export default class Integrations extends Component<PropsType, StateType> {
         </TitleSection>
 
         <IntegrationList
-          integrations={['kubernetes', 'registry', 'repo']}
+          integrations={["kubernetes", "registry", "repo"]}
           setCurrent={(x: any) => this.setState({ currentCategory: x })}
           isCategory={true}
         />
       </div>
     );
-  }
-  
+  };
+
   render() {
-    return ( 
-      <StyledIntegrations>
-        {this.renderContents()}
-      </StyledIntegrations>
-    );
+    return <StyledIntegrations>{this.renderContents()}</StyledIntegrations>;
   }
 }
 
@@ -221,7 +263,7 @@ const Credential = styled.div`
   border-radius: 5px;
   background: #ffffff11;
   margin-bottom: 5px;
-  
+
   > i {
     font-size: 22px;
     color: #ffffff44;
@@ -274,7 +316,8 @@ const Button = styled.div`
   flex-direction: row;
   align-items: center;
 
-  > img, i {
+  > img,
+  i {
     width: 20px;
     height: 20px;
     font-size: 16px;
@@ -288,7 +331,7 @@ const Button = styled.div`
 const Title = styled.div`
   font-size: 24px;
   font-weight: 600;
-  font-family: 'Work Sans', sans-serif;
+  font-family: "Work Sans", sans-serif;
   color: #ffffff;
   white-space: nowrap;
   overflow: hidden;
@@ -320,4 +363,4 @@ const LineBreak = styled.div`
   height: 2px;
   background: #ffffff20;
   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 = {
-  closeForm: () => void,
+  closeForm: () => void;
 };
 
 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> {
   state = {
-    registryURL: '',
-    dockerEmail: '',
-    dockerUsername: '',
-    dockerPassword: ''
-  }
+    registryURL: "",
+    dockerEmail: "",
+    dockerUsername: "",
+    dockerPassword: "",
+  };
 
   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 false;
-  }
+  };
 
   handleSubmit = () => {
     // TODO: implement once api is restructured
-  }
+  };
 
   render() {
-    return ( 
+    return (
       <StyledForm>
         <CredentialWrapper>
           <InputRow
-            type='text'
+            type="text"
             value={this.state.registryURL}
             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
-            type='text'
+            type="text"
             value={this.state.dockerEmail}
             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
-            type='text'
+            type="text"
             value={this.state.dockerUsername}
             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
-            type='password'
+            type="password"
             value={this.state.dockerPassword}
             setValue={(x: string) => this.setState({ dockerPassword: x })}
-            label='🔒 Docker Password'
-            placeholder='○ ○ ○ ○ ○ ○ ○ ○ ○'
-            width='100%'
+            label="🔒 Docker Password"
+            placeholder="○ ○ ○ ○ ○ ○ ○ ○ ○"
+            width="100%"
           />
         </CredentialWrapper>
         <SaveButton
-          text='Save Settings'
+          text="Save Settings"
           makeFlush={true}
           disabled={this.isDisabled()}
           onClick={this.isDisabled() ? null : this.handleSubmit}
@@ -95,4 +102,4 @@ const CredentialWrapper = styled.div`
 const StyledForm = styled.div`
   position: relative;
   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 = {
-  closeForm: () => void,
+  closeForm: () => void;
 };
 
 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> {
   state = {
-    credentialsName: '',
-    awsRegion: '',
-    awsAccessId: '',
-    awsSecretKey: '',
-  }
+    credentialsName: "",
+    awsRegion: "",
+    awsAccessId: "",
+    awsSecretKey: "",
+  };
 
   isDisabled = (): boolean => {
     let { awsRegion, awsAccessId, awsSecretKey, credentialsName } = this.state;
-    if (awsRegion === '' || awsAccessId === '' || awsSecretKey === '' || credentialsName === '') {
+    if (
+      awsRegion === "" ||
+      awsAccessId === "" ||
+      awsSecretKey === "" ||
+      credentialsName === ""
+    ) {
       return true;
     }
     return false;
-  }
+  };
 
   handleSubmit = () => {
     let { awsRegion, awsAccessId, awsSecretKey, credentialsName } = this.state;
     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() {
-    return ( 
+    return (
       <StyledForm>
         <CredentialWrapper>
           <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
-            type='text'
+            type="text"
             value={this.state.credentialsName}
             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>
           <Helper>AWS access credentials.</Helper>
           <InputRow
-            type='text'
+            type="text"
             value={this.state.awsRegion}
             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
-            type='text'
+            type="text"
             value={this.state.awsAccessId}
             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
-            type='password'
+            type="password"
             value={this.state.awsSecretKey}
             setValue={(x: string) => this.setState({ awsSecretKey: x })}
-            label='🔒 AWS Secret Key'
-            placeholder='○ ○ ○ ○ ○ ○ ○ ○ ○'
-            width='100%'
+            label="🔒 AWS Secret Key"
+            placeholder="○ ○ ○ ○ ○ ○ ○ ○ ○"
+            width="100%"
           />
         </CredentialWrapper>
         <SaveButton
-          text='Save Settings'
+          text="Save Settings"
           makeFlush={true}
           disabled={this.isDisabled()}
           onClick={this.isDisabled() ? null : this.handleSubmit}
@@ -126,4 +142,4 @@ const CredentialWrapper = styled.div`
 const StyledForm = styled.div`
   position: relative;
   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 = {
-  closeForm: () => void,
+  closeForm: () => void;
 };
 
 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> {
   state = {
-    clusterName: '',
-    clusterEndpoint: '',
-    clusterCA: '',
-    awsAccessId: '',
-    awsSecretKey: '',
-  }
+    clusterName: "",
+    clusterEndpoint: "",
+    clusterCA: "",
+    awsAccessId: "",
+    awsSecretKey: "",
+  };
 
   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 false;
-  }
+  };
 
   handleSubmit = () => {
     // TODO: implement once api is restructured
-  }
+  };
 
   render() {
-    return ( 
+    return (
       <StyledForm>
         <CredentialWrapper>
           <Heading>Cluster Settings</Heading>
           <Helper>Credentials for accessing your GKE cluster.</Helper>
           <InputRow
-            type='text'
+            type="text"
             value={this.state.clusterName}
             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
-            type='text'
+            type="text"
             value={this.state.clusterEndpoint}
             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
             value={this.state.clusterCA}
             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>
           <Helper>AWS access credentials.</Helper>
           <InputRow
-            type='text'
+            type="text"
             value={this.state.awsAccessId}
             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
-            type='password'
+            type="password"
             value={this.state.awsSecretKey}
             setValue={(x: string) => this.setState({ awsSecretKey: x })}
-            label='🔒 AWS Secret Key'
-            placeholder='○ ○ ○ ○ ○ ○ ○ ○ ○'
-            width='100%'
+            label="🔒 AWS Secret Key"
+            placeholder="○ ○ ○ ○ ○ ○ ○ ○ ○"
+            width="100%"
           />
         </CredentialWrapper>
         <SaveButton
-          text='Save Settings'
+          text="Save Settings"
           makeFlush={true}
           disabled={this.isDisabled()}
           onClick={this.isDisabled() ? null : this.handleSubmit}
@@ -113,4 +121,4 @@ const CredentialWrapper = styled.div`
 const StyledForm = styled.div`
   position: relative;
   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 = {
-  closeForm: () => void,
+  closeForm: () => void;
 };
 
 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> {
   state = {
-    credentialsName: '',
-    gcpRegion: '',
-    serviceAccountKey: '',
-    gcpProjectID: '',
-  }
+    credentialsName: "",
+    gcpRegion: "",
+    serviceAccountKey: "",
+    gcpProjectID: "",
+  };
 
   isDisabled = (): boolean => {
     let { credentialsName, serviceAccountKey } = this.state;
-    if (credentialsName === '' || serviceAccountKey === '') {
+    if (credentialsName === "" || serviceAccountKey === "") {
       return true;
     }
     return false;
-  }
-  
+  };
+
   handleSubmit = () => {
     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() {
-    return ( 
+    return (
       <StyledForm>
         <CredentialWrapper>
           <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
-            type='text'
+            type="text"
             value={this.state.credentialsName}
             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>
           <Helper>Service account credentials for GCP permissions.</Helper>
           <InputRow
-            type='text'
+            type="text"
             value={this.state.gcpRegion}
             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
             value={this.state.serviceAccountKey}
             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
-            type='text'
+            type="text"
             value={this.state.gcpProjectID}
             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>
         <SaveButton
-          text='Save Settings'
+          text="Save Settings"
           makeFlush={true}
           disabled={this.isDisabled()}
           onClick={this.isDisabled() ? null : this.handleSubmit}
@@ -117,4 +124,4 @@ const CredentialWrapper = styled.div`
 const StyledForm = styled.div`
   position: relative;
   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 = {
-  closeForm: () => void,
+  closeForm: () => void;
 };
 
 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> {
   state = {
-    clusterName: '',
-    clusterEndpoint: '',
-    clusterCA: '',
-    serviceAccountKey: ''
-  }
+    clusterName: "",
+    clusterEndpoint: "",
+    clusterCA: "",
+    serviceAccountKey: "",
+  };
 
   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 false;
-  }
+  };
 
   handleSubmit = () => {
     // TODO: implement once api is restructured
-  }
+  };
 
   render() {
-    return ( 
+    return (
       <StyledForm>
         <CredentialWrapper>
           <Heading>Cluster Settings</Heading>
           <Helper>Credentials for accessing your GKE cluster.</Helper>
           <InputRow
-            type='text'
+            type="text"
             value={this.state.clusterName}
             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
-            type='text'
+            type="text"
             value={this.state.clusterEndpoint}
             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
             value={this.state.clusterCA}
             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>
@@ -76,13 +83,13 @@ export default class GKEForm extends Component<PropsType, StateType> {
           <TextArea
             value={this.state.serviceAccountKey}
             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>
         <SaveButton
-          text='Save Settings'
+          text="Save Settings"
           makeFlush={true}
           disabled={this.isDisabled()}
           onClick={this.isDisabled() ? null : this.handleSubmit}
@@ -101,4 +108,4 @@ const CredentialWrapper = styled.div`
 const StyledForm = styled.div`
   position: relative;
   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 = {
-  integrationName: string,
-  closeForm: () => void,
+  integrationName: string;
+  closeForm: () => void;
 };
 
-type StateType = {
-};
+type StateType = {};
 
 export default class IntegrationForm extends Component<PropsType, StateType> {
-  state = {
-  }
+  state = {};
 
   render() {
     let { closeForm } = this.props;
     switch (this.props.integrationName) {
-      case 'docker-hub':
+      case "docker-hub":
         return <DockerHubForm closeForm={closeForm} />;
-      case 'gke':
+      case "gke":
         return <GKEForm closeForm={closeForm} />;
-      case 'eks':
+      case "eks":
         return <EKSForm closeForm={closeForm} />;
-      case 'ecr':
+      case "ecr":
         return <ECRForm closeForm={closeForm} />;
-      case 'gcr':
+      case "gcr":
         return <GCRForm closeForm={closeForm} />;
       default:
         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 = {
-  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 = {
-    currentTab: 'mac',
+    currentTab: "mac",
     currentPage: 0,
-  }
+  };
 
   renderPage = () => {
     switch (this.state.currentPage) {
@@ -30,27 +30,41 @@ export default class ClusterInstructionsModal extends Component<PropsType, State
           <Placeholder>
             1. To install the Porter CLI, first retrieve the latest binary:
             <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;
             </Code>
             2. Move the file into your bin:
             <Code>
-              chmod +x ./porter<br />
+              chmod +x ./porter
+              <br />
               sudo mv ./porter /usr/local/bin/porter
             </Code>
             3. Log in to the Porter CLI:
             <Code>
-              porter config set-host {location.protocol + '//' + location.host}<br/>
+              porter config set-host {location.protocol + "//" + location.host}
+              <br />
               porter auth login
             </Code>
             4. Configure the Porter CLI and link your current context:
             <Code>
-              porter config set-project {this.context.currentProject.id}<br/>
+              porter config set-project {this.context.currentProject.id}
+              <br />
               porter connect kubeconfig
             </Code>
           </Placeholder>
@@ -64,24 +78,30 @@ export default class ClusterInstructionsModal extends Component<PropsType, State
               porter connect kubeconfig --kubeconfig path/to/kubeconfig
             </Code>
             <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>
               porter connect kubeconfig --contexts minikube --contexts staging
             </Code>
           </Placeholder>
         );
       default:
-        return
+        return;
     }
-  }
- 
+  };
+
   render() {
     let { currentPage, currentTab } = this.state;
     return (
       <StyledClusterInstructionsModal>
-        <CloseButton onClick={() => {
-          this.context.setCurrentModal(null, null);
-        }}>
+        <CloseButton
+          onClick={() => {
+            this.context.setCurrentModal(null, null);
+          }}
+        >
           <CloseButtonImg src={close} />
         </CloseButton>
 
@@ -90,21 +110,31 @@ export default class ClusterInstructionsModal extends Component<PropsType, State
         <TabSelector
           options={tabOptions}
           currentTab={currentTab}
-          setCurrentTab={(value: string) => this.setState({ currentTab: value })}
+          setCurrentTab={(value: string) =>
+            this.setState({ currentTab: value })
+          }
         />
 
         {this.renderPage()}
         <PageSection>
           <PageCount>{currentPage + 1}/2</PageCount>
-          <i 
+          <i
             className="material-icons"
-            onClick={() => currentPage > 0 ? this.setState({ currentPage: currentPage - 1 }) : null}
+            onClick={() =>
+              currentPage > 0
+                ? this.setState({ currentPage: currentPage - 1 })
+                : null
+            }
           >
             arrow_back
           </i>
-          <i 
+          <i
             className="material-icons"
-            onClick={() => currentPage < 1 ? this.setState({ currentPage: currentPage + 1 }) : null}
+            onClick={() =>
+              currentPage < 1
+                ? this.setState({ currentPage: currentPage + 1 })
+                : null
+            }
           >
             arrow_forward
           </i>
@@ -132,7 +162,7 @@ const PageSection = styled.div`
   color: #ffffff;
   justify-content: flex-end;
   user-select: none;
-  
+
   > i {
     font-size: 18px;
     margin-left: 2px;
@@ -146,7 +176,7 @@ const PageSection = styled.div`
 `;
 
 const Code = styled.div`
-  background: #181B21;
+  background: #181b21;
   padding: 10px 15px;
   border: 1px solid #ffffff44;
   border-radius: 5px;
@@ -161,7 +191,8 @@ const Code = styled.div`
 const A = styled.a`
   color: #ffffff;
   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`
@@ -180,7 +211,7 @@ const Bold = styled.div`
 
 const Subtitle = styled.div`
   padding: 17px 0px 25px;
-  font-family: 'Work Sans', sans-serif;
+  font-family: "Work Sans", sans-serif;
   font-size: 13px;
   color: #aaaabb;
   margin-top: 3px;
@@ -193,7 +224,7 @@ const ModalTitle = styled.div`
   margin: 0px 0px 13px;
   display: flex;
   flex: 1;
-  font-family: 'Assistant';
+  font-family: "Assistant";
   font-size: 18px;
   color: #ffffff;
   user-select: none;
@@ -226,7 +257,7 @@ const CloseButtonImg = styled.img`
   margin: 0 auto;
 `;
 
-const StyledClusterInstructionsModal= styled.div`
+const StyledClusterInstructionsModal = styled.div`
   width: 100%;
   position: absolute;
   left: 0;
@@ -236,4 +267,4 @@ const StyledClusterInstructionsModal= styled.div`
   overflow: hidden;
   border-radius: 6px;
   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 = {
-  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 = {
-    currentTab: 'mac',
+    currentTab: "mac",
     currentPage: 0,
-  }
+  };
 
   renderPage = () => {
     switch (this.state.currentPage) {
@@ -30,31 +30,29 @@ export default class ClusterInstructionsModal extends Component<PropsType, State
           <Placeholder>
             <Bold>Elastic Container Registry (ECR):</Bold>
             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:
-            <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>
         );
       default:
-        return
+        return;
     }
-  }
- 
+  };
+
   render() {
     let { currentPage, currentTab } = this.state;
     return (
       <StyledClusterInstructionsModal>
-        <CloseButton onClick={() => {
-          this.context.setCurrentModal(null, null);
-        }}>
+        <CloseButton
+          onClick={() => {
+            this.context.setCurrentModal(null, null);
+          }}
+        >
           <CloseButtonImg src={close} />
         </CloseButton>
 
@@ -63,7 +61,9 @@ export default class ClusterInstructionsModal extends Component<PropsType, State
         <TabSelector
           options={tabOptions}
           currentTab={currentTab}
-          setCurrentTab={(value: string) => this.setState({ currentTab: value })}
+          setCurrentTab={(value: string) =>
+            this.setState({ currentTab: value })
+          }
         />
 
         {this.renderPage()}
@@ -90,7 +90,7 @@ const PageSection = styled.div`
   color: #ffffff;
   justify-content: flex-end;
   user-select: none;
-  
+
   > i {
     font-size: 18px;
     margin-left: 2px;
@@ -104,7 +104,7 @@ const PageSection = styled.div`
 `;
 
 const Code = styled.div`
-  background: #181B21;
+  background: #181b21;
   padding: 10px 15px;
   border: 1px solid #ffffff44;
   border-radius: 5px;
@@ -119,7 +119,8 @@ const Code = styled.div`
 const A = styled.a`
   color: #ffffff;
   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`
@@ -138,7 +139,7 @@ const Bold = styled.div`
 
 const Subtitle = styled.div`
   padding: 10px 0px 20px;
-  font-family: 'Work Sans', sans-serif;
+  font-family: "Work Sans", sans-serif;
   font-size: 13px;
   color: #aaaabb;
   margin-top: 3px;
@@ -151,7 +152,7 @@ const ModalTitle = styled.div`
   margin: 0px 0px 13px;
   display: flex;
   flex: 1;
-  font-family: 'Assistant';
+  font-family: "Assistant";
   font-size: 18px;
   color: #ffffff;
   user-select: none;
@@ -184,7 +185,7 @@ const CloseButtonImg = styled.img`
   margin: 0 auto;
 `;
 
-const StyledClusterInstructionsModal= styled.div`
+const StyledClusterInstructionsModal = styled.div`
   width: 100%;
   position: absolute;
   left: 0;
@@ -194,4 +195,4 @@ const StyledClusterInstructionsModal= styled.div`
   overflow: hidden;
   border-radius: 6px;
   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 = {
-  integrations: any[],
+  integrations: any[];
 };
 
 export default class IntegrationsModal extends Component<PropsType, StateType> {
   state = {
     integrations: [] as any[],
-  }
+  };
 
   componentDidMount() {
     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) {
           console.log(err);
         } else {
           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) {
           console.log(err);
         } else {
@@ -38,7 +37,7 @@ export default class IntegrationsModal extends Component<PropsType, StateType> {
         }
       });
     } else {
-      api.getRepoIntegrations('<token>', {}, {}, (err: any, res: any) => {
+      api.getRepoIntegrations("<token>", {}, {}, (err: any, res: any) => {
         if (err) {
           console.log(err);
         } else {
@@ -52,10 +51,15 @@ export default class IntegrationsModal extends Component<PropsType, StateType> {
     if (this.context.currentModalData) {
       let { setCurrentIntegration } = this.context.currentModalData;
       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 (
-          <IntegrationOption 
+          <IntegrationOption
             key={i}
             disabled={disabled}
             onClick={() => {
@@ -71,20 +75,22 @@ export default class IntegrationsModal extends Component<PropsType, StateType> {
         );
       });
     }
-  }
- 
+  };
+
   render() {
     return (
       <StyledIntegrationsModal>
-        <CloseButton onClick={() => {
-          this.context.setCurrentModal(null, null);
-        }}>
+        <CloseButton
+          onClick={() => {
+            this.context.setCurrentModal(null, null);
+          }}
+        >
           <CloseButtonImg src={close} />
         </CloseButton>
 
         <ModalTitle>Add a New Integration</ModalTitle>
         <Subtitle>Select the service you would like to connect to.</Subtitle>
-       
+
         <IntegrationsCatalog>
           {this.renderIntegrationsCatalog()}
         </IntegrationsCatalog>
@@ -114,9 +120,11 @@ const IntegrationOption = styled.div`
   display: flex;
   align-items: center;
   padding: 20px;
-  cursor: ${(props: { disabled: boolean }) => props.disabled ? 'not-allowed' : 'pointer'};
+  cursor: ${(props: { disabled: boolean }) =>
+    props.disabled ? "not-allowed" : "pointer"};
   :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`
   padding: 10px 0px;
-  font-family: 'Work Sans', sans-serif;
+  font-family: "Work Sans", sans-serif;
   font-size: 13px;
   color: #aaaabb;
   overflow: hidden;
@@ -144,7 +152,7 @@ const ModalTitle = styled.div`
   margin: 0px 0px 13px;
   display: flex;
   flex: 1;
-  font-family: 'Assistant';
+  font-family: "Assistant";
   font-size: 18px;
   color: #ffffff;
   user-select: none;
@@ -177,7 +185,7 @@ const CloseButtonImg = styled.img`
   margin: 0 auto;
 `;
 
-const StyledIntegrationsModal= styled.div`
+const StyledIntegrationsModal = styled.div`
   width: 100%;
   position: absolute;
   left: 0;
@@ -187,4 +195,4 @@ const StyledIntegrationsModal= styled.div`
   overflow: hidden;
   border-radius: 6px;
   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 = {
-  onRequestClose: () => void,
-  width?: string,
-  height?: string,
-}
+  onRequestClose: () => void;
+  width?: string;
+  height?: string;
+};
 
-type StateType = {
-}
+type StateType = {};
 
 export default class Modal extends Component<PropsType, StateType> {
   wrapperRef: any = React.createRef();
 
   componentDidMount() {
-    document.addEventListener('mousedown', this.handleClickOutside.bind(this));
+    document.addEventListener("mousedown", this.handleClickOutside.bind(this));
   }
 
   componentWillUnmount() {
-    document.removeEventListener('mousedown', this.handleClickOutside.bind(this));
+    document.removeEventListener(
+      "mousedown",
+      this.handleClickOutside.bind(this)
+    );
   }
 
   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();
     }
-  }
+  };
 
   render() {
     let { width, height } = this.props;
     return (
       <Overlay>
-        <StyledModal
-          ref={this.wrapperRef}
-          width={width}
-          height={height}
-        >
+        <StyledModal ref={this.wrapperRef} width={width} height={height}>
           {this.props.children}
         </StyledModal>
       </Overlay>
@@ -51,19 +53,33 @@ const Overlay = styled.div`
   left: 0;
   width: 100%;
   height: 100%;
-  background-color: rgba(0,0,0,0.6);
+  background-color: rgba(0, 0, 0, 0.6);
   z-index: 3;
 `;
 
 const StyledModal = styled.div`
   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;
   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;
-  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: 0;
   background-color: #202227;
@@ -72,10 +88,12 @@ const StyledModal = styled.div`
   animation: floatInModal 0.5s 0s;
   @keyframes floatInModal {
     from {
-      opacity: 0; transform: translateY(30px);
+      opacity: 0;
+      transform: translateY(30px);
     }
     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 & {
-  setRefreshClusters: (x: boolean) => void,
+  setRefreshClusters: (x: boolean) => void;
 };
 
 type StateType = {
-  clusterName: string,
-  status: string | null,
-  showDeleteOverlay: boolean
+  clusterName: string;
+  status: string | null;
+  showDeleteOverlay: boolean;
 };
 
 class UpdateClusterModal extends Component<PropsType, StateType> {
@@ -30,109 +30,132 @@ class UpdateClusterModal extends Component<PropsType, StateType> {
 
   handleDelete = () => {
     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.setState({ status: 'successful', showDeleteOverlay: false });
+        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;
-      }
-        
-      this.props.setRefreshClusters(true);
-      this.setState({ status: 'successful', showDeleteOverlay: false });
-      this.context.setCurrentModal(null, null);
-    });
-  }
+    );
+  };
 
   renderWarning = () => {
     let { currentCluster } = this.context;
     if (!currentCluster?.infra_id || !currentCluster.service) {
-      return(
+      return (
         <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>
-      )    
+      );
     }
 
-    return(
+    return (
       <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>
-    )    
-  }
+    );
+  };
 
   render() {
     return (
       <StyledUpdateProjectModal>
-        <CloseButton onClick={() => {
-          this.context.setCurrentModal(null, null);
-        }}>
+        <CloseButton
+          onClick={() => {
+            this.context.setCurrentModal(null, null);
+          }}
+        >
           <CloseButtonImg src={close} />
         </CloseButton>
 
         <ModalTitle>Cluster Settings</ModalTitle>
-        <Subtitle>
-          Cluster name
-        </Subtitle>
+        <Subtitle>Cluster name</Subtitle>
 
         <InputWrapper>
           <DashboardIcon>
@@ -140,26 +163,26 @@ class UpdateClusterModal extends Component<PropsType, StateType> {
           </DashboardIcon>
           <InputRow
             disabled={true}
-            type='string'
+            type="string"
             value={this.state.clusterName}
             setValue={(x: string) => this.setState({ clusterName: x })}
-            placeholder='ex: perspective-vortex'
-            width='470px'
+            placeholder="ex: perspective-vortex"
+            width="470px"
           />
         </InputWrapper>
 
         {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
         </Help>
 
         <SaveButton
-          text='Delete Cluster'
-          color='#b91133'
+          text="Delete Cluster"
+          color="#b91133"
           onClick={() => this.setState({ showDeleteOverlay: true })}
           status={this.state.status}
         />
@@ -171,7 +194,7 @@ class UpdateClusterModal extends Component<PropsType, StateType> {
           onNo={() => this.setState({ showDeleteOverlay: false })}
         />
       </StyledUpdateProjectModal>
-      );
+    );
   }
 }
 
@@ -212,7 +235,8 @@ const Warning = styled.div`
     margin-right: 10px;
     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`
@@ -228,7 +252,7 @@ const DashboardIcon = styled.div`
   display: flex;
   align-items: center;
   justify-content: center;
-  background: #676C7C;
+  background: #676c7c;
   border: 2px solid #8e94aa;
   color: white;
 
@@ -244,7 +268,7 @@ const InputWrapper = styled.div`
 
 const Subtitle = styled.div`
   margin-top: 23px;
-  font-family: 'Work Sans', sans-serif;
+  font-family: "Work Sans", sans-serif;
   font-size: 13px;
   color: #aaaabb;
   overflow: hidden;
@@ -257,7 +281,7 @@ const ModalTitle = styled.div`
   margin: 0px 0px 13px;
   display: flex;
   flex: 1;
-  font-family: 'Assistant';
+  font-family: "Assistant";
   font-size: 18px;
   color: #ffffff;
   user-select: none;
@@ -290,7 +314,7 @@ const CloseButtonImg = styled.img`
   margin: 0 auto;
 `;
 
-const StyledUpdateProjectModal= styled.div`
+const StyledUpdateProjectModal = styled.div`
   width: 100%;
   position: absolute;
   left: 0;
@@ -300,4 +324,4 @@ const StyledUpdateProjectModal= styled.div`
   overflow: hidden;
   border-radius: 6px;
   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 StateType = {
-  projectName: string,
-  selectedProvider: string | null,
+  projectName: string;
+  selectedProvider: string | null;
 };
 
 export default class NewProject extends Component<PropsType, StateType> {
   state = {
-    projectName: '',
+    projectName: "",
     selectedProvider: null as string | null,
-  }
+  };
 
   render() {
     let { projectName } = this.state;
@@ -31,7 +31,12 @@ export default class NewProject extends Component<PropsType, StateType> {
         </TitleSection>
         <Helper>
           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)
           </Warning>
           <Required>*</Required>
@@ -39,20 +44,21 @@ export default class NewProject extends Component<PropsType, StateType> {
         <InputWrapper>
           <ProjectIcon>
             <ProjectImage src={gradient} />
-            <Letter>{this.state.projectName ? this.state.projectName[0].toUpperCase() : '-'}</Letter>
+            <Letter>
+              {this.state.projectName
+                ? this.state.projectName[0].toUpperCase()
+                : "-"}
+            </Letter>
           </ProjectIcon>
           <InputRow
-            type='string'
+            type="string"
             value={this.state.projectName}
             setValue={(x: string) => this.setState({ projectName: x })}
-            placeholder='ex: perspective-vortex'
-            width='470px'
+            placeholder="ex: perspective-vortex"
+            width="470px"
           />
         </InputWrapper>
-        <ProvisionerSettings 
-          isInNewProject={true}
-          projectName={projectName}
-        />
+        <ProvisionerSettings isInNewProject={true} projectName={projectName} />
         <Br />
       </StyledNewProject>
     );
@@ -197,7 +203,7 @@ const Letter = styled.div`
   justify-content: center;
   font-size: 24px;
   font-weight: 500;
-  font-family: 'Work Sans', sans-serif;
+  font-family: "Work Sans", sans-serif;
 `;
 
 const ProjectImage = styled.img`
@@ -224,15 +230,17 @@ const InputWrapper = styled.div`
 `;
 
 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`
   height: 42px;
   margin-top: 30px;
   margin-bottom: 15px;
-  filter: ${(props: { bw?: boolean }) => props.bw ? 'grayscale(1)' : ''};
+  filter: ${(props: { bw?: boolean }) => (props.bw ? "grayscale(1)" : "")};
 `;
 
 const BlockDescription = styled.div`
@@ -247,7 +255,7 @@ const BlockDescription = styled.div`
   display: -webkit-box;
   overflow: hidden;
   -webkit-line-clamp: 2;
-  -webkit-box-orient: vertical;  
+  -webkit-box-orient: vertical;
 `;
 
 const BlockTitle = styled.div`
@@ -273,26 +281,40 @@ const Block = styled.div`
   align-item: center;
   justify-content: space-between;
   height: 170px;
-  cursor: ${(props: { disabled?: boolean }) => props.disabled ? '' : 'pointer'};
+  cursor: ${(props: { disabled?: boolean }) =>
+    props.disabled ? "" : "pointer"};
   color: #ffffff;
   position: relative;
   background: #26282f;
   box-shadow: 0 3px 5px 0px #00000022;
   :hover {
-    background: ${(props: { disabled?: boolean }) => props.disabled ? '' : '#ffffff11'};
+    background: ${(props: { disabled?: boolean }) =>
+      props.disabled ? "" : "#ffffff11"};
   }
 
   animation: fadeIn 0.3s 0s;
   @keyframes fadeIn {
-    from { opacity: 0 }
-    to { opacity: 1 }
+    from {
+      opacity: 0;
+    }
+    to {
+      opacity: 1;
+    }
   }
 `;
 
 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 {
-    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`
   font-size: 24px;
   font-weight: 600;
-  font-family: 'Work Sans', sans-serif;
+  font-family: "Work Sans", sans-serif;
   color: #ffffff;
   white-space: nowrap;
   overflow: hidden;
@@ -329,7 +351,7 @@ const TitleSection = styled.div`
       margin-bottom: -2px;
       font-size: 18px;
       margin-left: 18px;
-      color: #858FAAaa;
+      color: #858faaaa;
       cursor: pointer;
       :hover {
         color: #aaaabb;
@@ -344,4 +366,4 @@ const StyledNewProject = styled.div`
   position: relative;
   padding-top: 50px;
   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 = {
-  loading: boolean,
-  invites: InviteType[],
-  email: string,
-  invalidEmail: boolean,
-  isHTTPS: boolean,
-}
+  loading: boolean;
+  invites: InviteType[];
+  email: string;
+  invalidEmail: boolean;
+  isHTTPS: boolean;
+};
 
 const dummyInvites = [];
 
@@ -27,10 +26,10 @@ export default class InviteList extends Component<PropsType, StateType> {
   state = {
     loading: true,
     invites: [] as InviteType[],
-    email: '',
+    email: "",
     invalidEmail: false,
-    isHTTPS: (process.env.API_SERVER === 'dashboard.getporter.dev'),
-  }
+    isHTTPS: process.env.API_SERVER === "dashboard.getporter.dev",
+  };
 
   componentDidMount() {
     this.getInviteData();
@@ -38,18 +37,23 @@ export default class InviteList extends Component<PropsType, StateType> {
 
   getInviteData = () => {
     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 = () => {
     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 {
       this.setState({ invalidEmail: true });
     }
-  }
+  };
 
   createInvite = () => {
     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) => {
     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) => {
     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) => {
     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 = () => {
     let { currentProject } = this.context;
@@ -122,46 +156,35 @@ export default class InviteList extends Component<PropsType, StateType> {
     } else {
       var invContent: 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++) {
         if (this.state.invites[i].accepted) {
           collabList.push(
             <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}>
-                <CopyButton
-                  invis={true}
-                >
-                  Remove
-                </CopyButton>
+                <CopyButton invis={true}>Remove</CopyButton>
               </Td>
             </Tr>
           );
         } else if (this.state.invites[i].expired) {
           invContent.push(
             <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}>
                 <Rower>
                   Link Expired.
-                  <NewLinkButton
-                    onClick={() => this.replaceInvite(i)}
-                  >
+                  <NewLinkButton onClick={() => this.replaceInvite(i)}>
                     <u>Generate a new link</u>
                   </NewLinkButton>
                 </Rower>
               </LinkTd>
               <Td isTop={i === 0}>
-                <CopyButton
-                  onClick={() => this.deleteInvite(i)}
-                >
+                <CopyButton onClick={() => this.deleteInvite(i)}>
                   Delete Invite
                 </CopyButton>
               </Td>
@@ -170,33 +193,31 @@ export default class InviteList extends Component<PropsType, StateType> {
         } else {
           invContent.push(
             <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}>
                 <Rower>
                   <ShareLink
                     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
                   </CopyButton>
                 </Rower>
               </LinkTd>
               <Td isTop={i === 0}>
-                <CopyButton
-                  onClick={() => this.deleteInvite(i)}
-                >
+                <CopyButton onClick={() => this.deleteInvite(i)}>
                   Delete Invite
                 </CopyButton>
               </Td>
             </Tr>
-          )
+          );
         }
       }
 
@@ -204,14 +225,22 @@ export default class InviteList extends Component<PropsType, StateType> {
         <>
           <Heading>Invites & Collaborators</Heading>
           <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() {
     return (
@@ -221,27 +250,22 @@ export default class InviteList extends Component<PropsType, StateType> {
         <DarkMatter />
         <InputRow
           value={this.state.email}
-          type='text'
+          type="text"
           setValue={(x: string) => this.setState({ email: x })}
-          width='calc(100%)'
-          placeholder='ex: mrp@getporter.dev'
+          width="calc(100%)"
+          placeholder="ex: mrp@getporter.dev"
         />
         <ButtonWrapper>
-          <InviteButton
-            disabled={false}
-            onClick={() => this.validateEmail()}
-          >
+          <InviteButton disabled={false} onClick={() => this.validateEmail()}>
             Create Invite
           </InviteButton>
-          {this.state.invalidEmail &&
-            <Invalid>
-              Invalid email address. Please try again.
-            </Invalid>
-          }
+          {this.state.invalidEmail && (
+            <Invalid>Invalid email address. Please try again.</Invalid>
+          )}
         </ButtonWrapper>
         {this.renderInvitations()}
       </>
-    )
+    );
   }
 }
 
@@ -271,7 +295,8 @@ const DarkMatter = styled.div`
 `;
 
 const CopyButton = styled.div`
-  visibility: ${(props: { invis?: boolean }) => props.invis ? 'hidden' : 'visible'};
+  visibility: ${(props: { invis?: boolean }) =>
+    props.invis ? "hidden" : "visible"};
   color: #ffffff;
   font-weight: 400;
   font-size: 13px;
@@ -307,7 +332,7 @@ const InviteButton = styled.div<{ disabled: boolean }>`
   height: 35px;
   font-size: 13px;
   font-weight: 500;
-  font-family: 'Work Sans', sans-serif;
+  font-family: "Work Sans", sans-serif;
   color: white;
   display: flex;
   align-items: center;
@@ -320,13 +345,16 @@ const InviteButton = styled.div<{ disabled: boolean }>`
   justify-content: center;
   border: 0;
   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;
-  :focus { outline: 0 }
+  :focus {
+    outline: 0;
+  }
   :hover {
-    filter: ${props => !props.disabled ? 'brightness(120%)' : ''};
+    filter: ${(props) => (!props.disabled ? "brightness(120%)" : "")};
   }
   margin-bottom: 10px;
 `;
@@ -371,14 +399,14 @@ const Table = styled.table`
 const Td = styled.td`
   white-space: nowrap;
   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 {
     padding-right: 16px;
   }
 `;
 
-const Tr = styled.tr`
-`;
+const Tr = styled.tr``;
 
 const MailTd = styled(Td)`
   padding: 0 12px;
@@ -397,5 +425,5 @@ const Invalid = styled.div`
   color: #f5cb42;
   margin-left: 15px;
   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 StateType = {
-  projectName: string,
-  currentTab: string,
-}
+  projectName: string;
+  currentTab: string;
+};
 
 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> {
   state = {
-    projectName: '',
-    currentTab: 'manage-access',
-  }
+    projectName: "",
+    currentTab: "manage-access",
+  };
 
   componentDidMount() {
     let { currentProject } = this.context;
@@ -32,19 +32,21 @@ export default class ProjectSettings extends Component<PropsType, StateType> {
   }
 
   renderTabContents = () => {
-    if (this.state.currentTab === 'manage-access') {
+    if (this.state.currentTab === "manage-access") {
       return <InviteList />;
     } else {
       return (
         <>
           <Heading isAtTop={true}>Delete Project</Heading>
           <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>
 
           <Warning highlight={true}>This action cannot be undone.</Warning>
 
-
           <DeleteButton
             onClick={() => {
               this.context.setCurrentModal("UpdateProjectModal", {
@@ -57,9 +59,9 @@ export default class ProjectSettings extends Component<PropsType, StateType> {
         </>
       );
     }
-  }
+  };
 
-  render () {
+  render() {
     return (
       <StyledProjectSettings>
         <TitleSection>
@@ -81,14 +83,15 @@ ProjectSettings.contextType = Context;
 
 const Warning = styled.div`
   font-size: 13px;
-  color: ${(props: { highlight: boolean, makeFlush?: boolean }) => props.highlight ? '#f5cb42' : ''};
+  color: ${(props: { highlight: boolean; makeFlush?: boolean }) =>
+    props.highlight ? "#f5cb42" : ""};
   margin-bottom: 20px;
 `;
 
 const Title = styled.div`
   font-size: 24px;
   font-weight: 600;
-  font-family: 'Work Sans', sans-serif;
+  font-family: "Work Sans", sans-serif;
   color: #ffffff;
   white-space: nowrap;
   overflow: hidden;
@@ -114,7 +117,7 @@ const DeleteButton = styled.div`
   height: 35px;
   font-size: 13px;
   font-weight: 500;
-  font-family: 'Work Sans', sans-serif;
+  font-family: "Work Sans", sans-serif;
   color: white;
   display: flex;
   align-items: center;
@@ -129,7 +132,9 @@ const DeleteButton = styled.div`
   box-shadow: 0 2px 5px 0 #00000030;
   cursor: pointer;
   user-select: none;
-  :focus { outline: 0 }
+  :focus {
+    outline: 0;
+  }
   :hover {
     filter: brightness(120%);
   }
@@ -138,4 +143,4 @@ const DeleteButton = styled.div`
   :hover {
     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 & {
-  setSelectedProvisioner: (x: string | null) => void,
-  handleError: () => void,
-  projectName: string,
-  infras: InfraType[],
+  setSelectedProvisioner: (x: string | null) => void;
+  handleError: () => void;
+  projectName: string;
+  infras: InfraType[];
 };
 
 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 = [
-  { 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 = [
-  { 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
 class AWSFormSection extends Component<PropsType, StateType> {
   state = {
-    awsRegion: 'us-east-1',
-    awsAccessId: '',
-    awsSecretKey: '',
+    awsRegion: "us-east-1",
+    awsAccessId: "",
+    awsSecretKey: "",
     selectedInfras: [...provisionOptions],
-    buttonStatus: '',
-  }
+    buttonStatus: "",
+  };
 
   componentDidMount = () => {
     let { infras } = this.props;
     let { selectedInfras } = this.state;
 
     if (infras) {
-      
       // From the dashboard, only uncheck and disable if "creating" or "created"
       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 });
     }
-  }
+  };
 
   checkFormDisabled = () => {
-    let { 
-      awsRegion,
-      awsAccessId, 
-      awsSecretKey, 
-      selectedInfras,
-    } = this.state;
+    let { awsRegion, awsAccessId, awsSecretKey, selectedInfras } = this.state;
     let { projectName } = this.props;
-    if (projectName || projectName === '') {
+    if (projectName || projectName === "") {
       return (
-        !isAlphanumeric(projectName) 
-          || !(awsAccessId !== '' && awsSecretKey !== '' && awsRegion !== '')
-          || selectedInfras.length === 0
+        !isAlphanumeric(projectName) ||
+        !(awsAccessId !== "" && awsSecretKey !== "" && awsRegion !== "") ||
+        selectedInfras.length === 0
       );
     } else {
       return (
-        !(awsAccessId !== '' && awsSecretKey !== '' && awsRegion !== '')
-          || selectedInfras.length === 0
+        !(awsAccessId !== "" && awsSecretKey !== "" && awsRegion !== "") ||
+        selectedInfras.length === 0
       );
     }
-  }
+  };
 
   // Step 1: Create a project
   createProject = (callback?: any) => {
-    console.log('Creating project');
+    console.log("Creating project");
     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) => {
-    console.log('Provisioning ECR');
+    console.log("Provisioning ECR");
     let { awsAccessId, awsSecretKey, awsRegion } = this.state;
     let { currentProject } = this.context;
     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) {
           console.log(err);
           handleError();
           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 = () => {
-    console.log('Provisioning EKS');
+    console.log("Provisioning EKS");
     let { handleError } = this.props;
     let { awsAccessId, awsSecretKey, awsRegion } = this.state;
     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) {
           console.log(err);
           handleError();
           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)
   onCreateAWS = () => {
@@ -224,7 +239,7 @@ class AWSFormSection extends Component<PropsType, StateType> {
       if (selectedInfras.length === 2) {
         // Case: project exists, provision ECR + EKS
         this.provisionECR(this.provisionEKS);
-      } else if (selectedInfras[0].value === 'ecr') {
+      } else if (selectedInfras[0].value === "ecr") {
         // Case: project exists, only provision ECR
         this.provisionECR(() => this.props.history.push("provisioner"));
       } else {
@@ -233,28 +248,25 @@ class AWSFormSection extends Component<PropsType, StateType> {
       }
     } else {
       if (selectedInfras.length === 2) {
-        // Case: project DNE, provision ECR + EKS 
+        // Case: project DNE, provision ECR + EKS
         this.createProject(() => this.provisionECR(this.provisionEKS));
-      } else if (selectedInfras[0].value === 'ecr') {
+      } else if (selectedInfras[0].value === "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 {
         // Case: project DNE, only provision EKS
         this.createProject(this.provisionEKS);
       }
     }
-  }
+  };
 
   render() {
     let { setSelectedProvisioner } = this.props;
-    let {
-      awsRegion,
-      awsAccessId,
-      awsSecretKey,
-      selectedInfras,
-    } = this.state;
+    let { awsRegion, awsAccessId, awsSecretKey, selectedInfras } = this.state;
 
     return (
       <StyledAWSFormSection>
@@ -264,38 +276,38 @@ class AWSFormSection extends Component<PropsType, StateType> {
           </CloseButton>
           <Heading isAtTop={true}>
             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
             </GuideButton>
           </Heading>
           <SelectRow
             options={regionOptions}
-            width='100%'
+            width="100%"
             value={awsRegion}
-            dropdownMaxHeight='240px'
+            dropdownMaxHeight="240px"
             setActiveValue={(x: string) => this.setState({ awsRegion: x })}
-            label='📍 AWS Region'
+            label="📍 AWS Region"
           />
           <InputRow
-            type='text'
+            type="text"
             value={awsAccessId}
             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}
           />
           <InputRow
-            type='password'
+            type="password"
             value={awsSecretKey}
             setValue={(x: string) => this.setState({ awsSecretKey: x })}
-            label='🔒 AWS Secret Key'
-            placeholder='○ ○ ○ ○ ○ ○ ○ ○ ○'
-            width='100%'
+            label="🔒 AWS Secret Key"
+            placeholder="○ ○ ○ ○ ○ ○ ○ ○ ○"
+            width="100%"
             isRequired={true}
           />
           <Br />
@@ -304,18 +316,18 @@ class AWSFormSection extends Component<PropsType, StateType> {
           <CheckboxList
             options={provisionOptions}
             selected={selectedInfras}
-            setSelected={(x: { value: string, label: string }[]) => {
+            setSelected={(x: { value: string; label: string }[]) => {
               this.setState({ selectedInfras: x });
             }}
           />
         </FormSection>
         {this.props.children ? this.props.children : <Padding />}
         <SaveButton
-          text='Submit'
+          text="Submit"
           disabled={this.checkFormDisabled()}
           onClick={this.onCreateAWS}
           makeFlush={true}
-          helper='Note: Provisioning can take up to 15 minutes'
+          helper="Note: Provisioning can take up to 15 minutes"
         />
       </StyledAWSFormSection>
     );
@@ -402,4 +414,4 @@ const GuideButton = styled.a`
 const CloseButtonImg = styled.img`
   width: 14px;
   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 = {
-  setSelectedProvisioner: (x: string | null) => void,
-  handleError: () => void,
-  projectName: string,
-  infras: InfraType[],
+  setSelectedProvisioner: (x: string | null) => void;
+  handleError: () => void;
+  projectName: string;
+  infras: InfraType[];
 };
 
 type StateType = {
-  selectedInfras: { value: string, label: string }[],
-  subscriptionTier: string,
-  doRegion: string,
+  selectedInfras: { value: string; label: string }[];
+  subscriptionTier: string;
+  doRegion: string;
 };
 
 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 = [
-  { 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 = [
-  { 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
 export default class DOFormSection extends Component<PropsType, StateType> {
   state = {
     selectedInfras: [...provisionOptions],
-    subscriptionTier: 'starter',
-    doRegion: 'nyc1',
-  }
+    subscriptionTier: "starter",
+    doRegion: "nyc1",
+  };
 
   componentDidMount = () => {
     let { infras } = this.props;
     let { selectedInfras } = this.state;
 
     if (infras) {
-      
       // From the dashboard, only uncheck and disable if "creating" or "created"
       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 });
     }
-  }
+  };
 
   checkFormDisabled = () => {
-    let { 
-      selectedInfras,
-    } = this.state;
+    let { selectedInfras } = this.state;
     let { projectName } = this.props;
-    if (projectName || projectName === '') {
+    if (projectName || projectName === "") {
       return !isAlphanumeric(projectName) || selectedInfras.length === 0;
     } else {
       return selectedInfras.length === 0;
     }
-  }
+  };
 
   // Step 1: Create a project
   createProject = (callback?: any) => {
-    console.log('Creating project');
+    console.log("Creating project");
     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) => {
     let { subscriptionTier, doRegion, selectedInfras } = this.state;
     let redirectUrl = `/api/oauth/projects/${projectId}/digitalocean?project_id=${projectId}&provision=do`;
     redirectUrl += `&tier=${subscriptionTier}&region=${doRegion}`;
-    selectedInfras.forEach((option: { value: string, label: string }) => {
+    selectedInfras.forEach((option: { value: string; label: string }) => {
       redirectUrl += `&infras=${option.value}`;
     });
     window.location.href = redirectUrl;
-  }
+  };
 
   // TODO: handle generically (with > 2 steps)
   onCreateDO = () => {
@@ -150,7 +150,7 @@ export default class DOFormSection extends Component<PropsType, StateType> {
     } else {
       this.createProject((projectId: number) => this.doRedirect(projectId));
     }
-  }
+  };
 
   render() {
     let { setSelectedProvisioner } = this.props;
@@ -165,37 +165,41 @@ export default class DOFormSection extends Component<PropsType, StateType> {
           <Heading isAtTop={true}>DigitalOcean Settings</Heading>
           <SelectRow
             options={tierOptions}
-            width='100%'
+            width="100%"
             value={subscriptionTier}
-            setActiveValue={(x: string) => this.setState({ subscriptionTier: x })}
-            label='💰 Subscription Tier'
+            setActiveValue={(x: string) =>
+              this.setState({ subscriptionTier: x })
+            }
+            label="💰 Subscription Tier"
           />
           <SelectRow
             options={regionOptions}
-            width='100%'
-            dropdownMaxHeight='240px'
+            width="100%"
+            dropdownMaxHeight="240px"
             value={doRegion}
             setActiveValue={(x: string) => this.setState({ doRegion: x })}
-            label='📍 DigitalOcean Region'
+            label="📍 DigitalOcean Region"
           />
           <Br />
           <Heading>DigitalOcean Resources</Heading>
-          <Helper>Porter will provision the following DigitalOcean resources</Helper>
+          <Helper>
+            Porter will provision the following DigitalOcean resources
+          </Helper>
           <CheckboxList
             options={provisionOptions}
             selected={selectedInfras}
-            setSelected={(x: { value: string, label: string }[]) => {
+            setSelected={(x: { value: string; label: string }[]) => {
               this.setState({ selectedInfras: x });
             }}
           />
         </FormSection>
         {this.props.children ? this.props.children : <Padding />}
         <SaveButton
-          text='Submit'
+          text="Submit"
           disabled={this.checkFormDisabled()}
           onClick={this.onCreateDO}
           makeFlush={true}
-          helper='Note: Provisioning can take up to 15 minutes'
+          helper="Note: Provisioning can take up to 15 minutes"
         />
       </StyledAWSFormSection>
     );
@@ -280,4 +284,4 @@ const GuideButton = styled.a`
 const CloseButtonImg = styled.img`
   width: 14px;
   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 & {
-  projectName: string,
+  projectName: string;
 };
 
 type StateType = {
-  buttonStatus: string,
+  buttonStatus: string;
 };
 
 class ExistingClusterSection extends Component<PropsType, StateType> {
   state = {
-    buttonStatus: '',
-  }
+    buttonStatus: "",
+  };
 
   onCreateProject = () => {
     let { projectName } = this.props;
     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() {
     let { children, projectName } = this.props;
@@ -65,12 +73,12 @@ class ExistingClusterSection extends Component<PropsType, StateType> {
         </Placeholder>
         {children ? children : <Padding />}
         <SaveButton
-          text='Submit'
+          text="Submit"
           disabled={!isAlphanumeric(projectName)}
           onClick={this.onCreateProject}
           status={buttonStatus}
           makeFlush={true}
-          helper='Note: Provisioning can take up to 15 minutes'
+          helper="Note: Provisioning can take up to 15 minutes"
         />
       </StyledExistingClusterSection>
     );
@@ -101,4 +109,4 @@ const Placeholder = styled.div`
   justify-content: center;
   color: #ffffff44;
   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 & {
-  setSelectedProvisioner: (x: string | null) => void,
-  handleError: () => void,
-  projectName: string,
-  infras: InfraType[],
+  setSelectedProvisioner: (x: string | null) => void;
+  handleError: () => void;
+  projectName: string;
+  infras: InfraType[];
 };
 
 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 = [
-  { 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 = [
-  { 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> {
   state = {
-    gcpRegion: 'us-east1',
-    gcpProjectId: '',
-    gcpKeyData: '',
+    gcpRegion: "us-east1",
+    gcpProjectId: "",
+    gcpKeyData: "",
     selectedInfras: [...provisionOptions],
-    buttonStatus: '',
-  }
+    buttonStatus: "",
+  };
 
   componentDidMount = () => {
     let { infras } = this.props;
     let { selectedInfras } = this.state;
 
     if (infras) {
-      
       // From the dashboard, only uncheck and disable if "creating" or "created"
       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 });
     }
-  }
+  };
 
   checkFormDisabled = () => {
-    let { 
-      gcpRegion,
-      gcpProjectId, 
-      gcpKeyData, 
-      selectedInfras,
-    } = this.state;
+    let { gcpRegion, gcpProjectId, gcpKeyData, selectedInfras } = this.state;
     let { projectName } = this.props;
-    if (projectName || projectName === '') {
+    if (projectName || projectName === "") {
       return (
-        !isAlphanumeric(projectName) 
-          || !(gcpProjectId !== '' && gcpKeyData !== '' && gcpRegion !== '')
-          || selectedInfras.length === 0
+        !isAlphanumeric(projectName) ||
+        !(gcpProjectId !== "" && gcpKeyData !== "" && gcpRegion !== "") ||
+        selectedInfras.length === 0
       );
     } else {
       return (
-        !(gcpProjectId !== '' && gcpKeyData !== '' && gcpRegion !== '')
-          || selectedInfras.length === 0
+        !(gcpProjectId !== "" && gcpKeyData !== "" && gcpRegion !== "") ||
+        selectedInfras.length === 0
       );
     }
-  }
+  };
 
   // Step 1: Create a project
   createProject = (callback?: any) => {
-    console.log('Creating project');
+    console.log("Creating project");
     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) => {
-    console.log('Provisioning GCR')
+    console.log("Provisioning GCR");
     let { currentProject } = this.context;
     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) => {
-    console.log('Provisioning GKE');
+    console.log("Provisioning GKE");
     let { handleError } = this.props;
     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 = () => {
     let { selectedInfras, gcpKeyData, gcpProjectId, gcpRegion } = this.state;
     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)
   onCreateGCP = () => {
@@ -225,16 +237,11 @@ class GCPFormSection extends Component<PropsType, StateType> {
     } else {
       this.createProject(this.handleCreateFlow);
     }
-  }
+  };
 
   render() {
     let { setSelectedProvisioner } = this.props;
-    let {
-      gcpRegion,
-      gcpProjectId,
-      gcpKeyData,
-      selectedInfras,
-    } = this.state;
+    let { gcpRegion, gcpProjectId, gcpKeyData, selectedInfras } = this.state;
 
     return (
       <StyledGCPFormSection>
@@ -244,38 +251,38 @@ class GCPFormSection extends Component<PropsType, StateType> {
           </CloseButton>
           <Heading isAtTop={true}>
             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
             </GuideButton>
           </Heading>
           <SelectRow
             options={regionOptions}
-            width='100%'
+            width="100%"
             value={gcpRegion}
-            dropdownMaxHeight='240px'
+            dropdownMaxHeight="240px"
             setActiveValue={(x: string) => this.setState({ gcpRegion: x })}
-            label='📍 GCP Region'
+            label="📍 GCP Region"
           />
           <InputRow
-            type='text'
+            type="text"
             value={gcpProjectId}
             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}
           />
           <InputRow
-            type='password'
+            type="password"
             value={gcpKeyData}
             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}
           />
           <Br />
@@ -284,18 +291,18 @@ class GCPFormSection extends Component<PropsType, StateType> {
           <CheckboxList
             options={provisionOptions}
             selected={selectedInfras}
-            setSelected={(x: { value: string, label: string }[]) => {
+            setSelected={(x: { value: string; label: string }[]) => {
               this.setState({ selectedInfras: x });
             }}
           />
         </FormSection>
         {this.props.children ? this.props.children : <Padding />}
         <SaveButton
-          text='Submit'
+          text="Submit"
           disabled={this.checkFormDisabled()}
           onClick={this.onCreateGCP}
           makeFlush={true}
-          helper='Note: Provisioning can take up to 15 minutes'
+          helper="Note: Provisioning can take up to 15 minutes"
         />
       </StyledGCPFormSection>
     );
@@ -382,4 +389,4 @@ const GuideButton = styled.a`
 const CloseButtonImg = styled.img`
   width: 14px;
   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 = {
-  infras: InfraType[],
+  infras: InfraType[];
 };
 
-type StateType = {
-};
+type StateType = {};
 
 export default class InfraStatuses extends Component<PropsType, StateType> {
-  state = {
-  }
+  state = {};
 
   renderStatusIcon = (status: string) => {
-    if (status === 'created') {
+    if (status === "created") {
       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() {
     return (
@@ -35,7 +37,7 @@ export default class InfraStatuses extends Component<PropsType, StateType> {
               {this.renderStatusIcon(infra.status)}
               {infraNames[infra.kind]}
             </InfraRow>
-          )
+          );
         })}
       </StyledInfraStatuses>
     );
@@ -48,7 +50,7 @@ const StatusIcon = styled.div<{ color?: string }>`
   justify-content: center;
   width: 20px;
   font-size: 16px;
-  color: ${props => props.color ? props.color : '#68c49c'};
+  color: ${(props) => (props.color ? props.color : "#68c49c")};
   margin-right: 10px;
 `;
 
@@ -66,4 +68,4 @@ const InfraRow = styled.div`
 const StyledInfraStatuses = styled.div`
   margin-top: 20px;
   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 & {
-  isInNewProject?: boolean,
-  projectName?: string,
-  infras?: InfraType[],
+  isInNewProject?: boolean;
+  projectName?: string;
+  infras?: InfraType[];
 };
 
 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> {
   state = {
     selectedProvider: null as string | null,
     infras: [] as InfraType[],
-  }
+  };
 
   // Handle any submission (pre-status) error
   handleError = () => {
     let { setCurrentError } = this.context;
     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");
-  }
+  };
 
   renderSelectedProvider = () => {
     let { selectedProvider } = this.state;
@@ -47,39 +49,39 @@ class NewProject extends Component<PropsType, StateType> {
     let renderSkipHelper = () => {
       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>
-                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>
               </Helper>
-            ) : (
-              <PositionWrapper selectedProvider={selectedProvider}>
-                <Helper>
-                  Already have a Kubernetes cluster? 
-                  <Highlight 
-                    onClick={() => this.setState({ 
-                      selectedProvider: 'skipped' 
-                    })}
-                  >
-                    Skip
-                  </Highlight>
-                </Helper>
-              </PositionWrapper>
-            )
-          }
+            </PositionWrapper>
+          )}
         </>
       );
-    }
+    };
 
     switch (selectedProvider) {
-      case 'aws':
+      case "aws":
         return (
-          <AWSFormSection 
+          <AWSFormSection
             handleError={this.handleError}
             projectName={projectName}
             infras={infras}
@@ -90,9 +92,9 @@ class NewProject extends Component<PropsType, StateType> {
             {renderSkipHelper()}
           </AWSFormSection>
         );
-      case 'gcp':
+      case "gcp":
         return (
-          <GCPFormSection 
+          <GCPFormSection
             handleError={this.handleError}
             projectName={projectName}
             infras={infras}
@@ -103,9 +105,9 @@ class NewProject extends Component<PropsType, StateType> {
             {renderSkipHelper()}
           </GCPFormSection>
         );
-      case 'do':
+      case "do":
         return (
-          <DOFormSection 
+          <DOFormSection
             handleError={this.handleError}
             projectName={projectName}
             infras={infras}
@@ -113,28 +115,29 @@ class NewProject extends Component<PropsType, StateType> {
               this.setState({ selectedProvider: x });
             }}
           />
-        )
+        );
       default:
         return (
-          <ExistingClusterSection 
-            projectName={projectName}
-          >
+          <ExistingClusterSection projectName={projectName}>
             {renderSkipHelper()}
           </ExistingClusterSection>
         );
     }
-  }
-  
+  };
+
   render() {
     let { selectedProvider } = this.state;
     let { isInNewProject } = this.props;
     return (
       <StyledProvisionerSettings>
         <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>
         {!selectedProvider ? (
           <BlockList>
@@ -142,18 +145,14 @@ class NewProject extends Component<PropsType, StateType> {
               let providerInfo = integrationList[provider];
               return (
                 <Block
-                  key={i} 
+                  key={i}
                   onClick={() => {
                     this.setState({ selectedProvider: provider });
                   }}
                 >
                   <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>
               );
             })}
@@ -161,23 +160,23 @@ class NewProject extends Component<PropsType, StateType> {
         ) : (
           <>{this.renderSelectedProvider()}</>
         )}
-        {(isInNewProject && !selectedProvider) && (
+        {isInNewProject && !selectedProvider && (
           <>
             <Helper>
-              Already have a Kubernetes cluster? 
-              <Highlight 
-                onClick={() => this.setState({ selectedProvider: 'skipped' })}
+              Already have a Kubernetes cluster?
+              <Highlight
+                onClick={() => this.setState({ selectedProvider: "skipped" })}
               >
                 Skip
               </Highlight>
             </Helper>
             <Br />
             <SaveButton
-              text='Submit'
+              text="Submit"
               disabled={true}
               onClick={() => {}}
               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;
 `;
 
-const PositionWrapper = styled.div<{ selectedProvider: string | null}>`
-`;
+const PositionWrapper = styled.div<{ selectedProvider: string | null }>``;
 
 const Highlight = styled.div`
   margin-left: 5px;
@@ -227,7 +225,7 @@ const Icon = styled.img<{ bw?: boolean }>`
   height: 42px;
   margin-top: 30px;
   margin-bottom: 15px;
-  filter: ${props => props.bw ? 'grayscale(1)' : ''};
+  filter: ${(props) => (props.bw ? "grayscale(1)" : "")};
 `;
 
 const BlockDescription = styled.div`
@@ -242,7 +240,7 @@ const BlockDescription = styled.div`
   display: -webkit-box;
   overflow: hidden;
   -webkit-line-clamp: 2;
-  -webkit-box-orient: vertical;  
+  -webkit-box-orient: vertical;
 `;
 
 const BlockTitle = styled.div`
@@ -268,18 +266,22 @@ const Block = styled.div<{ disabled?: boolean }>`
   align-item: center;
   justify-content: space-between;
   height: 170px;
-  cursor: ${props => props.disabled ? '' : 'pointer'};
+  cursor: ${(props) => (props.disabled ? "" : "pointer")};
   color: #ffffff;
   position: relative;
   background: #26282f;
   box-shadow: 0 3px 5px 0px #00000022;
   :hover {
-    background: ${props => props.disabled ? '' : '#ffffff11'};
+    background: ${(props) => (props.disabled ? "" : "#ffffff11")};
   }
 
   animation: fadeIn 0.3s 0s;
   @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 { Link } from "react-router-dom";
 
 type PropsType = RouteComponentProps & {};
 
 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 = [
-  { 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> {
   state = {
     error: false,
     logs: [] as string[],
-    websockets : [] as any[],
+    websockets: [] as any[],
     maxStep: {} as Record<string, any>,
     currentStep: {} as Record<string, number>,
     triggerEnd: false,
     infras: [] as InfraType[],
-  }
+  };
 
-  parentRef = React.createRef<HTMLDivElement>()
+  parentRef = React.createRef<HTMLDivElement>();
 
   scrollToBottom = (smooth: boolean) => {
     if (smooth) {
-      this.parentRef.current.lastElementChild.scrollIntoView({ behavior: "smooth" })
+      this.parentRef.current.lastElementChild.scrollIntoView({
+        behavior: "smooth",
+      });
     } else {
-      this.parentRef.current.lastElementChild.scrollIntoView({ behavior: "auto" })
+      this.parentRef.current.lastElementChild.scrollIntoView({
+        behavior: "auto",
+      });
     }
-  }
+  };
 
   componentDidMount() {
-    console.log('mounting provisioner')
+    console.log("mounting provisioner");
     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
-    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() {
-    if (this.state.websockets.length == 0) { return; }
+    if (this.state.websockets.length == 0) {
+      return;
+    }
 
     this.state.websockets.forEach((ws: any) => {
-      ws.close()
-    })
+      ws.close();
+    });
   }
 
   isJSON = (str: string) => {
@@ -106,21 +125,25 @@ class ProvisionerStatus extends Component<PropsType, StateType> {
       return false;
     }
     return true;
-  }
+  };
 
   setupWebsocket = (ws: WebSocket, infra: any) => {
     ws.onopen = () => {
-      console.log('connected to websocket')
-    }
+      console.log("connected to websocket");
+    };
 
     ws.onmessage = (evt: MessageEvent) => {
       let event = JSON.parse(evt.data);
       let validEvents = [] as any[];
       let err = null;
-      
+
       for (var i = 0; i < event.length; 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"]);
 
           if (d["kind"] == "error") {
@@ -129,24 +152,32 @@ class ProvisionerStatus extends Component<PropsType, StateType> {
           }
 
           // 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);
           }
         }
       }
 
       if (err) {
-        posthog.capture('Provisioning Error', {error: err});
+        posthog.capture("Provisioning Error", { error: err });
 
         let e = ansiparse(err).map((el: any) => {
           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;
       }
 
@@ -154,107 +185,123 @@ class ProvisionerStatus extends Component<PropsType, StateType> {
         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({
           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) => {
-        logs.push(...ansiparse(e["log"]))
-      })
+        logs.push(...ansiparse(e["log"]));
+      });
 
       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) => {
-      console.log('websocket err', err)
-    }
+      console.log("websocket err", err);
+    };
 
     ws.onclose = () => {
-      console.log('closing provisioner websocket')
-    }
+      console.log("closing provisioner websocket");
+    };
 
-    return ws
-  }
+    return ws;
+  };
 
   renderLogs = () => {
     return this.state.logs.map((log, i) => {
       return <Log key={i}>{log}</Log>;
     });
-  }
+  };
 
   onEnd = () => {
     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);
-  }
+  };
 
   refreshLogs = () => {
-    if (this.state.websockets.length == 0) { return; }
+    if (this.state.websockets.length == 0) {
+      return;
+    }
     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) => {
-      ws.close()
-    })
+      ws.close();
+    });
 
-    this.setState({ 
+    this.setState({
       websockets: [],
-      logs: []
-    })
+      logs: [],
+    });
 
     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..."] });
-    
-  }
-  
+  };
+
   render() {
     let { error, triggerEnd, infras } = this.state;
-    
+
     let maxStep = 0;
     let currentStep = 0;
     let skip = false;
-    
+
     for (let i = 0; i < infras.length; i++) {
       if (!this.state.maxStep[infras[i].kind]) {
         skip = true;
@@ -263,14 +310,14 @@ class ProvisionerStatus extends Component<PropsType, StateType> {
 
     if (!skip) {
       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) {
-      posthog.capture('Provisioning complete!')
-      this.onEnd()
+      posthog.capture("Provisioning complete!");
+      this.onEnd();
       this.setState({ triggerEnd: true });
     }
 
@@ -339,7 +386,7 @@ const Options = styled.div`
   flex-direction: row;
   align-items: center;
   justify-content: space-between;
-`
+`;
 
 const Refresh = styled.div`
   display: flex;
@@ -358,7 +405,7 @@ const Refresh = styled.div`
   :hover {
     background: #2468d6;
   }
-`
+`;
 
 // const Link = styled.a`
 //   cursor: pointer;
@@ -367,8 +414,10 @@ const Refresh = styled.div`
 // `;
 
 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;
 `;
 
@@ -405,7 +454,7 @@ const Message = styled.div`
 `;
 
 const Loaded = styled.div<{ progress: string }>`
-  width: ${props => props.progress};
+  width: ${(props) => props.progress};
   height: 100%;
   background: linear-gradient(to right, #4f8aff, #8e7dff, #4f8aff);
   background-size: 400% 400%;
@@ -413,8 +462,12 @@ const Loaded = styled.div<{ progress: string }>`
   animation: linkLoad 2s infinite;
 
   @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`
   font-size: 24px;
   font-weight: 600;
-  font-family: 'Work Sans', sans-serif;
+  font-family: "Work Sans", sans-serif;
   color: #ffffff;
   white-space: nowrap;
   overflow: hidden;
@@ -456,7 +509,7 @@ const TitleSection = styled.div`
       margin-bottom: -2px;
       font-size: 18px;
       margin-left: 18px;
-      color: #858FAAaa;
+      color: #858faaaa;
       cursor: pointer;
       :hover {
         color: #aaaabb;
@@ -472,4 +525,4 @@ const StyledProvisioner = styled.div`
   position: relative;
   padding-top: 50px;
   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 & {
-  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 = {
-  showDrawer: boolean,
-  initializedDrawer: boolean,
-  clusters: ClusterType[],
+  showDrawer: boolean;
+  initializedDrawer: boolean;
+  clusters: ClusterType[];
 
   // Track last project id for refreshing clusters on project change
-  prevProjectId: number
+  prevProjectId: number;
 };
 
 class ClusterSection extends Component<PropsType, StateType> {
-
   // Need to track initialized for animation mounting
   state = {
     showDrawer: false,
     initializedDrawer: false,
     clusters: [] as ClusterType[],
-    prevProjectId: this.context.currentProject.id
+    prevProjectId: this.context.currentProject.id,
   };
 
   updateClusters = () => {
     let { currentProject, setCurrentCluster } = this.context;
 
     // 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() {
     this.updateClusters();
@@ -91,8 +96,7 @@ class ClusterSection extends Component<PropsType, StateType> {
   // Need to override showDrawer when the sidebar is closed
   componentDidUpdate(prevProps: PropsType) {
     if (prevProps !== this.props) {
-
-      // Refresh clusters on project change 
+      // Refresh clusters on project change
       if (this.state.prevProjectId !== this.context.currentProject.id) {
         this.updateClusters();
         this.setState({ prevProjectId: this.context.currentProject.id });
@@ -107,7 +111,7 @@ class ClusterSection extends Component<PropsType, StateType> {
       }
     }
   }
-  
+
   toggleDrawer = (): void => {
     if (!this.state.initializedDrawer) {
       this.setState({ initializedDrawer: true });
@@ -128,8 +132,10 @@ class ClusterSection extends Component<PropsType, StateType> {
   };
 
   showClusterConfigModal = () => {
-    this.context.setCurrentModal('ClusterConfigModal', { updateClusters: this.updateClusters });
-  }
+    this.context.setCurrentModal("ClusterConfigModal", {
+      updateClusters: this.updateClusters,
+    });
+  };
 
   renderContents = (): JSX.Element => {
     let { clusters, showDrawer } = this.state;
@@ -138,8 +144,12 @@ class ClusterSection extends Component<PropsType, StateType> {
     if (clusters.length > 0) {
       return (
         <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>
           </LinkWrapper>
           <DrawerButton onClick={this.toggleDrawer}>
@@ -154,11 +164,13 @@ class ClusterSection extends Component<PropsType, StateType> {
 
     return (
       <InitializeButton
-        onClick={() => this.context.setCurrentModal('ClusterInstructionsModal', {})}
+        onClick={() =>
+          this.context.setCurrentModal("ClusterInstructionsModal", {})
+        }
       >
         <Plus>+</Plus> Connect a Cluster
       </InitializeButton>
-    )
+    );
   };
 
   render() {
@@ -203,7 +215,7 @@ const InitializeButton = styled.div`
 
 const BgAccent = styled.img`
   height: 42px;
-  background: #819BFD;
+  background: #819bfd;
   width: 30px;
   border-top-left-radius: 100px;
   max-width: 30px;
@@ -240,14 +252,18 @@ const ClusterName = styled.div`
 
 const DropdownIcon = styled.span`
   position: absolute;
-  right: ${(props: { showDrawer: boolean }) => (props.showDrawer ? '-2px' : '2px')};
+  right: ${(props: { showDrawer: boolean }) =>
+    props.showDrawer ? "-2px" : "2px"};
   top: 10px;
   > i {
     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;
 
   @keyframes rotateLeft {
@@ -265,7 +281,6 @@ const DropdownIcon = styled.span`
       transform: rotate(-90deg);
     }
   }
-
 `;
 
 const ClusterIcon = styled.div`
@@ -298,10 +313,12 @@ const ClusterSelector = styled.div`
   font-weight: 500;
   color: white;
   cursor: pointer;
-  background: ${(props: { isSelected: boolean }) => props.isSelected ? '#ffffff11' : ''};
+  background: ${(props: { isSelected: boolean }) =>
+    props.isSelected ? "#ffffff11" : ""};
   z-index: 1;
 
   :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 & {
-  toggleDrawer: () => void,
-  showDrawer: boolean,
-  clusters: ClusterType[],
+  toggleDrawer: () => void;
+  showDrawer: boolean;
+  clusters: ClusterType[];
 };
 
-type StateType = {
-};
+type StateType = {};
 
 class Drawer extends Component<PropsType, StateType> {
-
   renderClusterList = (): JSX.Element[] | JSX.Element => {
     let { clusters } = this.props;
     let { currentCluster, setCurrentCluster } = this.context;
 
     if (clusters.length > 0 && currentCluster) {
       clusters.sort((a, b) => a.id - b.id);
-      
+
       return clusters.map((cluster: ClusterType, i: number) => {
         /*
         let active = this.context.activeProject &&
@@ -34,23 +32,26 @@ class Drawer extends Component<PropsType, StateType> {
           <ClusterOption
             key={i}
             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>
           </ClusterOption>
         );
       });
     }
 
-    return <Placeholder>No clusters selected</Placeholder>
+    return <Placeholder>No clusters selected</Placeholder>;
   };
 
   renderCloseOverlay = (): JSX.Element | undefined => {
     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()}
 
-          <InitializeButton onClick={() => {
-            this.context.setCurrentModal('ClusterInstructionsModal', {});
-          }}>
+          <InitializeButton
+            onClick={() => {
+              this.context.setCurrentModal("ClusterInstructionsModal", {});
+            }}
+          >
             <Plus>+</Plus> Add a Cluster
           </InitializeButton>
         </StyledDrawer>
@@ -128,7 +131,8 @@ const ClusterOption = styled.div`
   overflow: hidden;
   text-overflow: ellipsis;
   cursor: pointer;
-  background: ${(props: { active?: boolean }) => props.active ? '#ffffff18' : ''};
+  background: ${(props: { active?: boolean }) =>
+    props.active ? "#ffffff18" : ""};
   :hover {
     background: #ffffff22;
   }
@@ -194,18 +198,32 @@ const StyledDrawer = styled.div`
   overflow-y: auto;
   padding-bottom: 40px;
   top: 0;
-  left: ${(props: { showDrawer: boolean }) => (props.showDrawer ? '-30px' : '200px')};
+  left: ${(props: { showDrawer: boolean }) =>
+    props.showDrawer ? "-30px" : "200px"};
   z-index: -2;
   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;
   @keyframes slideDrawerRight {
-    from { left: -30px; opacity: 0; }
-    to { left: 200px; opacity: 1; }
+    from {
+      left: -30px;
+      opacity: 0;
+    }
+    to {
+      left: 200px;
+      opacity: 1;
+    }
   }
   @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 & {
-  currentProject: ProjectType,
-  projects: ProjectType[],
+  currentProject: ProjectType;
+  projects: ProjectType[];
 };
 
 type StateType = {
-  expanded: boolean
+  expanded: boolean;
 };
 
 class ProjectSection extends Component<PropsType, StateType> {
@@ -38,7 +38,7 @@ class ProjectSection extends Component<PropsType, StateType> {
         </Option>
       );
     });
-  }
+  };
 
   renderDropdown = () => {
     if (this.state.expanded) {
@@ -50,7 +50,7 @@ class ProjectSection extends Component<PropsType, StateType> {
             <Option
               selected={false}
               lastItem={true}
-              onClick={() => this.props.history.push('new-project')}
+              onClick={() => this.props.history.push("new-project")}
             >
               <ProjectIconAlt>+</ProjectIconAlt>
               <ProjectLabel>Create a Project</ProjectLabel>
@@ -59,11 +59,11 @@ class ProjectSection extends Component<PropsType, StateType> {
         </div>
       );
     }
-  }
+  };
 
   handleExpand = () => {
     this.setState({ expanded: !this.state.expanded });
-  }
+  };
 
   render() {
     let { currentProject } = this.props;
@@ -86,7 +86,7 @@ class ProjectSection extends Component<PropsType, StateType> {
       );
     }
     return (
-      <InitializeButton onClick={() => this.props.history.push('new-project')}>
+      <InitializeButton onClick={() => this.props.history.push("new-project")}>
         <Plus>+</Plus> Create a Project
       </InitializeButton>
     );
@@ -136,10 +136,12 @@ const InitializeButton = styled.div`
   }
 `;
 
-const Option = styled.div` 
+const Option = styled.div`
   width: 100%;
   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;
   display: flex;
   align-items: center;
@@ -148,9 +150,11 @@ const Option = styled.div`
   padding-left: 10px;
   cursor: pointer;
   padding-right: 10px;
-  background: ${(props: { selected: boolean, lastItem?: boolean }) => props.selected ? '#ffffff11' : ''};
+  background: ${(props: { selected: boolean; lastItem?: boolean }) =>
+    props.selected ? "#ffffff11" : ""};
   :hover {
-    background: ${(props: { selected: boolean, lastItem?: boolean }) => props.selected ? '' : '#ffffff22'};
+    background: ${(props: { selected: boolean; lastItem?: boolean }) =>
+      props.selected ? "" : "#ffffff22"};
   }
 
   > i {
@@ -234,7 +238,7 @@ const MainSelector = styled.div`
   align-items: center;
   margin: 10px 0 0;
   font-size: 14px;
-  font-family: 'Work Sans', sans-serif;
+  font-family: "Work Sans", sans-serif;
   font-weight: 600;
   cursor: pointer;
   padding: 10px 0;
@@ -253,6 +257,7 @@ const MainSelector = styled.div`
     align-items: center;
     justify-content: center;
     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 StateType = {
-};
+type StateType = {};
 
 // 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() {
     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 & {
-  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 = {
-  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> {
-
   // Need closeDrawer to hide drawer on sidebar close
   state = {
     showSidebar: true,
@@ -41,13 +40,13 @@ class Sidebar extends Component<PropsType, StateType> {
   };
 
   componentDidMount() {
-    document.addEventListener('keydown', this.handleKeyDown);
-    document.addEventListener('keyup', this.handleKeyUp);
+    document.addEventListener("keydown", this.handleKeyDown);
+    document.addEventListener("keyup", this.handleKeyUp);
   }
 
   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
@@ -55,10 +54,10 @@ class Sidebar extends Component<PropsType, StateType> {
     if (prevProps.forceSidebar !== this.props.forceSidebar) {
       this.setState({ showSidebar: this.props.forceSidebar });
     }
-  }  
+  }
 
   handleKeyDown = (e: KeyboardEvent): void => {
-    if (e.key === 'Meta' || e.key === 'Control') {
+    if (e.key === "Meta" || e.key === "Control") {
       this.setState({ pressingCtrl: true });
     } else if (e.code === "Backslash" && this.state.pressingCtrl) {
       this.toggleSidebar();
@@ -66,13 +65,16 @@ class Sidebar extends Component<PropsType, StateType> {
   };
 
   handleKeyUp = (e: KeyboardEvent): void => {
-    if (e.key === 'Meta' || e.key === 'Control') {
+    if (e.key === "Meta" || e.key === "Control") {
       this.setState({ pressingCtrl: false });
     }
   };
 
   toggleSidebar = (): void => {
-    this.setState({ showSidebar: !this.state.showSidebar, forceCloseDrawer: true });
+    this.setState({
+      showSidebar: !this.state.showSidebar,
+      forceCloseDrawer: true,
+    });
   };
 
   renderPullTab = (): JSX.Element | undefined => {
@@ -87,9 +89,7 @@ class Sidebar extends Component<PropsType, StateType> {
 
   renderTooltip = (): JSX.Element | undefined => {
     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>
           <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} />
             Dashboard
           </NavButton>
           <NavButton
             onClick={() => this.props.history.push("templates")}
-            selected={currentView === 'templates'}
+            selected={currentView === "templates"}
           >
             <Img src={filter} />
             Templates
           </NavButton>
           <NavButton
-            selected={currentView === 'integrations'}
+            selected={currentView === "integrations"}
             //onClick={() => {
             //  setCurrentView('integrations')
-           // }}
+            // }}
             onClick={() => {
-              setCurrentModal('IntegrationsInstructionsModal', {})
+              setCurrentModal("IntegrationsInstructionsModal", {});
             }}
           >
             <Img src={integrations} />
@@ -128,25 +133,25 @@ class Sidebar extends Component<PropsType, StateType> {
           </NavButton>
           {this.context.currentProject.roles.filter((obj: any) => {
             return obj.user_id === this.context.user.userId;
-          })[0].kind === 'admin' &&
+          })[0].kind === "admin" && (
             <NavButton
               onClick={() => this.props.history.push("project-settings")}
-              selected={this.props.currentView === 'project-settings'}
+              selected={this.props.currentView === "project-settings"}
             >
               <Img enlarge={true} src={settings} />
               Settings
             </NavButton>
-          }
+          )}
 
           <br />
 
           <SidebarLabel>Current Cluster</SidebarLabel>
-          <ClusterSection 
-            forceCloseDrawer={this.state.forceCloseDrawer} 
+          <ClusterSection
+            forceCloseDrawer={this.state.forceCloseDrawer}
             releaseDrawer={() => this.setState({ forceCloseDrawer: false })}
             setWelcome={this.props.setWelcome}
             currentView={currentView}
-            isSelected={currentView === 'cluster-dashboard'}
+            isSelected={currentView === "cluster-dashboard"}
             forceRefreshClusters={this.props.forceRefreshClusters}
             setRefreshClusters={this.props.setRefreshClusters}
           />
@@ -155,12 +160,8 @@ class Sidebar extends Component<PropsType, StateType> {
     }
 
     // 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
   render() {
@@ -169,10 +170,14 @@ class Sidebar extends Component<PropsType, StateType> {
         {this.renderPullTab()}
         <StyledSidebar showSidebar={this.state.showSidebar}>
           <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()}
             <i className="material-icons">double_arrow</i>
@@ -202,7 +207,7 @@ const ProjectPlaceholder = styled.div`
   justify-content: center;
   height: calc(100% - 100px);
   font-size: 13px;
-  font-family: 'Work Sans', sans-serif;
+  font-family: "Work Sans", sans-serif;
   color: #aaaabb;
   padding-bottom: 80px;
 
@@ -219,16 +224,19 @@ const NavButton = styled.div`
   height: 42px;
   padding: 12px 35px 1px 53px;
   font-size: 14px;
-  font-family: 'Work Sans', sans-serif;
+  font-family: "Work Sans", sans-serif;
   color: #ffffff;
   overflow: hidden;
   white-space: nowrap;
   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 {
-    background: ${(props: { disabled?: boolean, selected?: boolean }) => props.selected ? '' : '#ffffff08'};
+    background: ${(props: { disabled?: boolean; selected?: boolean }) =>
+      props.selected ? "" : "#ffffff08"};
   }
 
   > i {
@@ -246,11 +254,11 @@ const NavButton = styled.div`
 
 const Img = styled.img<{ enlarge?: boolean }>`
   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;
   position: absolute;
-  left: ${props => props.enlarge ? '19px' : '20px'};
+  left: ${(props) => (props.enlarge ? "19px" : "20px")};
   top: 9px;
 `;
 
@@ -261,7 +269,7 @@ const BottomSection = styled.div`
 `;
 
 const LogOutButton = styled(NavButton)`
-  width: calc(100% - 55px); 
+  width: calc(100% - 55px);
   border-top-right-radius: 3px;
   border-bottom-right-radius: 3px;
   margin-left: -1px;
@@ -312,7 +320,9 @@ const UserSection = styled.div`
 const RingWrapper = styled.div`
   width: 28px;
   border-radius: 30px;
-  :focus { outline: 0 }
+  :focus {
+    outline: 0;
+  }
   height: 28px;
   padding: 3px;
   border: 2px solid #ffffff44;
@@ -343,7 +353,7 @@ const PullTab = styled.div`
   position: fixed;
   width: 30px;
   height: 50px;
-  background: #7A838F77;
+  background: #7a838f77;
   top: calc(50vh - 60px);
   left: 0;
   z-index: 1;
@@ -352,7 +362,7 @@ const PullTab = styled.div`
   cursor: pointer;
 
   :hover {
-    background: #99a5aF77;
+    background: #99a5af77;
   }
 
   > i {
@@ -384,8 +394,12 @@ const Tooltip = styled.div`
   animation: faded-in 0.2s 0.15s;
   animation-fill-mode: forwards;
   @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`
-  font-family: 'Work Sans', sans-serif;
+  font-family: "Work Sans", sans-serif;
   width: 200px;
   position: relative;
   padding-top: 20px;
   height: 100vh;
   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;
   @keyframes showSidebar {
-    from { margin-left: -220px }
-    to { margin-left: 0px }
+    from {
+      margin-left: -220px;
+    }
+    to {
+      margin-left: 0px;
+    }
   }
   @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 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> {
   state = {
-    currentTemplate: null as (PorterTemplate | null),
-    currentTab: 'community',
+    currentTemplate: null as PorterTemplate | null,
+    currentTab: "community",
     porterTemplates: [] as PorterTemplate[],
     loading: true,
     error: false,
-  }
+  };
 
   componentDidMount() {
-    api.getTemplates('<token>', {}, {}, (err: any, res: any) => {
+    api.getTemplates("<token>", {}, {}, (err: any, res: any) => {
       if (err) {
         this.setState({ loading: false, error: true });
       } else {
         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 });
         });
       }
@@ -54,15 +54,21 @@ export default class Templates extends Component<PropsType, StateType> {
     }
 
     return (
-      <Polymer><i className="material-icons">layers</i></Polymer>
+      <Polymer>
+        <i className="material-icons">layers</i>
+      </Polymer>
     );
-  }
+  };
 
   renderTemplateList = () => {
     let { loading, error, porterTemplates } = this.state;
 
     if (loading) {
-      return <LoadingWrapper><Loading /></LoadingWrapper>
+      return (
+        <LoadingWrapper>
+          <Loading />
+        </LoadingWrapper>
+      );
     } else if (error) {
       return (
         <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 = () => {
     if (this.state.currentTemplate) {
       return (
         <ExpandedTemplate
           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>
         <TitleSection>
           <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>
           </a>
         </TitleSection>
         <TabSelector
           options={tabOptions}
           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>
     );
-  }
-  
+  };
+
   render() {
     return this.renderContents();
   }
@@ -173,7 +189,7 @@ const TemplateDescription = styled.div`
   display: -webkit-box;
   overflow: hidden;
   -webkit-line-clamp: 2;
-  -webkit-box-orient: vertical;  
+  -webkit-box-orient: vertical;
 `;
 
 const TemplateTitle = styled.div`
@@ -210,8 +226,12 @@ const TemplateBlock = styled.div`
 
   animation: fadeIn 0.3s 0s;
   @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`
   font-size: 24px;
   font-weight: 600;
-  font-family: 'Work Sans', sans-serif;
+  font-family: "Work Sans", sans-serif;
   color: #ffffff;
   white-space: nowrap;
   overflow: hidden;
@@ -248,7 +268,7 @@ const TitleSection = styled.div`
       margin-bottom: -2px;
       font-size: 18px;
       margin-left: 18px;
-      color: #858FAAaa;
+      color: #858faaaa;
       cursor: pointer;
       :hover {
         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 = {
-  currentTemplate: PorterTemplate,
-  setCurrentTemplate: (x: PorterTemplate) => void,
+  currentTemplate: PorterTemplate;
+  setCurrentTemplate: (x: PorterTemplate) => void;
 };
 
 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> {
@@ -32,28 +32,44 @@ export default class ExpandedTemplate extends Component<PropsType, StateType> {
     error: false,
     markdown: null as string | null,
     keywords: [] as string[],
-  }
+  };
 
   componentDidMount() {
     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 = () => {
-      if (this.state.loading) {
-        return <LoadingWrapper><Loading /></LoadingWrapper>;
-      }
+    if (this.state.loading) {
+      return (
+        <LoadingWrapper>
+          <Loading />
+        </LoadingWrapper>
+      );
+    }
     if (this.state.showLaunchTemplate) {
       return (
         <LaunchTemplate
@@ -76,13 +92,11 @@ export default class ExpandedTemplate extends Component<PropsType, StateType> {
         />
       </FadeWrapper>
     );
-  }
+  };
 
   render() {
     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`
   animation: fadeIn 0.2s;
   @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);
   min-width: 300px;
   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 = {
-  currentTemplate: any,
-  hideLaunch: () => void,
-  values: any,
-  form: any,
+  currentTemplate: any;
+  hideLaunch: () => void;
+  values: any;
+  form: any;
 };
 
 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> {
   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,
     selectedNamespace: "default",
-    selectedImageUrl: '' as string | null,
-    templateName: '',
-    selectedTag: '' as string | null,
+    selectedImageUrl: "" as string | null,
+    templateName: "",
+    selectedTag: "" as string | null,
     tabOptions: [] as ChoiceType[],
     currentTab: null as string | null,
     tabContents: [] as any,
-    namespaceOptions: [] as { label: string, value: string }[],
+    namespaceOptions: [] as { label: string; value: string }[],
   };
 
   onSubmitAddon = (wildcard?: any) => {
     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 = {};
     for (let key in wildcard) {
       _.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) => {
     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
     let values = {};
@@ -111,72 +123,81 @@ export default class LaunchTemplate extends Component<PropsType, StateType> {
     let imageUrl = this.state.selectedImageUrl;
     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];
       tag = splits[1];
     } 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 = () => {
     return (
       <ValuesWrapper
         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}
         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) => {
           return this.props.form?.tabs.map((tab: any, i: number) => {
-
             // If tab is current, render
             if (tab.name === this.state.currentTab) {
               return (
-                <ValuesForm 
+                <ValuesForm
                   metaState={metaState}
                   setMetaState={setMetaState}
                   key={tab.name}
-                  sections={tab.sections} 
+                  sections={tab.sections}
                 />
               );
             }
@@ -184,17 +205,17 @@ export default class LaunchTemplate extends Component<PropsType, StateType> {
         }}
       </ValuesWrapper>
     );
-  }
+  };
 
   componentDidMount() {
-    if (this.props.currentTemplate.name !== 'docker') {
-      this.setState({ saveValuesStatus: '' });
+    if (this.props.currentTemplate.name !== "docker") {
+      this.setState({ saveValuesStatus: "" });
     }
 
     // Retrieve tab options
     let tabOptions = [] as ChoiceType[];
     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 });
       }
     });
@@ -202,57 +223,75 @@ export default class LaunchTemplate extends Component<PropsType, StateType> {
 
     // TODO: query with selected filter once implemented
     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) => {
-    if (x === '') {
-      this.setState({ saveValuesStatus: 'No container image specified' });
+    if (x === "") {
+      this.setState({ saveValuesStatus: "No container image specified" });
     } else {
-      this.setState({ saveValuesStatus: '' });
+      this.setState({ saveValuesStatus: "" });
     }
     this.setState({ selectedImageUrl: x });
-  }
+  };
 
   renderIcon = (icon: string) => {
     if (icon) {
-      return <Icon src={icon} />
+      return <Icon src={icon} />;
     }
 
     return (
-      <Polymer><i className="material-icons">layers</i></Polymer>
+      <Polymer>
+        <i className="material-icons">layers</i>
+      </Polymer>
     );
-  }
+  };
 
   renderTabRegion = () => {
     if (this.state.tabOptions.length > 0) {
       return (
         <>
-          <Subtitle>Configure additional settings for this template. (Optional)</Subtitle>
+          <Subtitle>
+            Configure additional settings for this template. (Optional)
+          </Subtitle>
           <TabRegion
             options={this.state.tabOptions}
             currentTab={this.state.currentTab}
@@ -266,16 +305,17 @@ export default class LaunchTemplate extends Component<PropsType, StateType> {
       return (
         <Wrapper>
           <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
-            </Link>.
+            </Link>
+            .
           </Placeholder>
           <SaveButton
-            text='Deploy'
+            text="Deploy"
             onClick={() => this.onSubmitAddon()}
             status={this.state.saveValuesStatus}
             makeFlush={true}
@@ -283,7 +323,7 @@ export default class LaunchTemplate extends Component<PropsType, StateType> {
         </Wrapper>
       );
     }
-  }
+  };
 
   // Display if current template uses source (image or repo)
   renderSourceSelector = () => {
@@ -291,7 +331,8 @@ export default class LaunchTemplate extends Component<PropsType, StateType> {
       return (
         <>
           <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>
           </Subtitle>
           <DarkMatter />
@@ -306,7 +347,7 @@ export default class LaunchTemplate extends Component<PropsType, StateType> {
         </>
       );
     }
-  }
+  };
 
   render() {
     let { name, icon } = this.props.currentTemplate;
@@ -324,7 +365,9 @@ export default class LaunchTemplate extends Component<PropsType, StateType> {
         </TitleSection>
         <ClusterSection>
           <Template>
-            {icon ? this.renderIcon(icon) : this.renderIcon(currentTemplate.icon)}
+            {icon
+              ? this.renderIcon(icon)
+              : this.renderIcon(currentTemplate.icon)}
             {name}
           </Template>
           <i className="material-icons">arrow_right_alt</i>
@@ -333,36 +376,48 @@ export default class LaunchTemplate extends Component<PropsType, StateType> {
           </ClusterLabel>
           <Selector
             activeValue={this.state.selectedCluster}
-            setActiveValue={(cluster: string) => this.setState({ selectedCluster: cluster })}
+            setActiveValue={(cluster: string) =>
+              this.setState({ selectedCluster: cluster })
+            }
             options={this.state.clusterOptions}
-            width='250px'
-            dropdownWidth='335px'
+            width="250px"
+            dropdownWidth="335px"
             closeOverlay={true}
           />
           <NamespaceLabel>
             <i className="material-icons">view_list</i>Namespace
           </NamespaceLabel>
           <Selector
-            key={'namespace'}
+            key={"namespace"}
             activeValue={this.state.selectedNamespace}
-            setActiveValue={(namespace: string) => this.setState({ selectedNamespace: namespace })}
+            setActiveValue={(namespace: string) =>
+              this.setState({ selectedNamespace: namespace })
+            }
             options={this.state.namespaceOptions}
-            width='250px'
-            dropdownWidth='335px'
+            width="250px"
+            dropdownWidth="335px"
             closeOverlay={true}
           />
         </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)
-          </Warning>. (Optional)</Subtitle>
-        <DarkMatter antiHeight='-27px' />
+          </Warning>
+          . (Optional)
+        </Subtitle>
+        <DarkMatter antiHeight="-27px" />
         <InputRow
-          type='text'
+          type="text"
           value={this.state.templateName}
           setValue={(x: string) => this.setState({ templateName: x })}
-          placeholder='ex: doctor-scientist'
-          width='100%'
+          placeholder="ex: doctor-scientist"
+          width="100%"
         />
         {this.renderSourceSelector()}
         {this.renderTabRegion()}
@@ -373,9 +428,9 @@ export default class LaunchTemplate extends Component<PropsType, StateType> {
 
 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`
@@ -416,12 +471,12 @@ const Placeholder = styled.div`
 
 const DarkMatter = styled.div<{ antiHeight?: string }>`
   width: 100%;
-  margin-top: ${props => props.antiHeight || '-15px'};
+  margin-top: ${(props) => props.antiHeight || "-15px"};
 `;
 
 const Subtitle = styled.div`
   padding: 11px 0px 20px;
-  font-family: 'Work Sans', sans-serif;
+  font-family: "Work Sans", sans-serif;
   font-size: 13px;
   color: #aaaabb;
   line-height: 1.6em;
@@ -455,12 +510,11 @@ const Icon = styled.img`
   margin-right: 10px;
 `;
 
-
 const Polymer = styled.div`
   margin-bottom: -3px;
 
   > i {
-    color: ${props => props.theme.containerIcon};
+    color: ${(props) => props.theme.containerIcon};
     font-size: 18px;
     margin-right: 10px;
   }
@@ -476,7 +530,7 @@ const ClusterSection = styled.div`
   display: flex;
   align-items: center;
   color: #ffffff;
-  font-family: 'Work Sans', sans-serif;
+  font-family: "Work Sans", sans-serif;
   font-size: 14px;
   font-weight: 500;
   margin-top: 20px;
@@ -508,7 +562,7 @@ const Flex = styled.div`
 const Title = styled.div`
   font-size: 24px;
   font-weight: 600;
-  font-family: 'Work Sans', sans-serif;
+  font-family: "Work Sans", sans-serif;
   margin-left: 11px;
   border-radius: 2px;
   color: #ffffff;
@@ -527,4 +581,4 @@ const TitleSection = styled.div`
 const StyledLaunchTemplate = styled.div`
   width: 100%;
   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 = {
-  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> {
   renderIcon = (icon: string) => {
     if (icon) {
-      return <Icon src={icon} />
+      return <Icon src={icon} />;
     }
 
     return (
-      <Polymer><i className="material-icons">layers</i></Polymer>
+      <Polymer>
+        <i className="material-icons">layers</i>
+      </Polymer>
     );
-  }
+  };
 
   renderTagList = () => {
     if (this.props.keywords) {
       return this.props.keywords.map((tag: string, i: number) => {
-        return (
-          <Tag key={i}>{tag}</Tag>
-        )
+        return <Tag key={i}>{tag}</Tag>;
       });
     }
-  }
+  };
 
   renderMarkdown = () => {
     let { currentTemplate, markdown } = this.props;
     if (markdown) {
-      return (
-        <Markdown>{markdown}</Markdown>
-      );
+      return <Markdown>{markdown}</Markdown>;
     }
     return currentTemplate.description;
-  }
-
+  };
 
   renderTagSection = () => {
-
     // Rendering doesn't make sense until search + clicking on tags is supported
     if (false && this.props.keywords && this.props.keywords.length > 0) {
       return (
@@ -65,7 +59,7 @@ export default class TemplateInfo extends Component<PropsType, StateType> {
         </TagSection>
       );
     }
-  }
+  };
 
   renderBanner = () => {
     let { currentCluster } = this.context;
@@ -79,40 +73,44 @@ export default class TemplateInfo extends Component<PropsType, StateType> {
           </Banner>
         </>
       );
-    } else if (this.props.currentTemplate.name.toLowerCase() === 'docker') {
+    } else if (this.props.currentTemplate.name.toLowerCase() === "docker") {
       return (
         <>
           <Br />
           <Banner>
             <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"
               href="https://docs.getporter.dev/docs/cli-documentation#pushing-docker-images-to-your-porter-image-registry"
             >
               refer to our docs
-            </Link>.
+            </Link>
+            .
           </Banner>
         </>
       );
-    } else if (this.props.currentTemplate.name.toLowerCase() === 'https-issuer') {
+    } else if (
+      this.props.currentTemplate.name.toLowerCase() === "https-issuer"
+    ) {
       return (
         <>
           <Br />
           <Banner>
             <i className="material-icons-outlined">info</i>
             To use this template you must first follow
-            <Link 
+            <Link
               target="_blank"
               href="https://docs.getporter.dev/docs/https-and-custom-domains"
             >
               Porter's HTTPS setup guide
-            </Link> (5 minutes).
+            </Link>{" "}
+            (5 minutes).
           </Banner>
         </>
       );
     }
-  }
+  };
 
   render() {
     let { currentCluster } = this.context;
@@ -127,10 +125,15 @@ export default class TemplateInfo extends Component<PropsType, StateType> {
       <StyledExpandedTemplate>
         <TitleSection>
           <Flex>
-            <i className="material-icons" onClick={() => this.props.setCurrentTemplate(null)}>
+            <i
+              className="material-icons"
+              onClick={() => this.props.setCurrentTemplate(null)}
+            >
               keyboard_backspace
             </i>
-            {icon ? this.renderIcon(icon) : this.renderIcon(currentTemplate.icon)}
+            {icon
+              ? this.renderIcon(icon)
+              : this.renderIcon(currentTemplate.icon)}
             <Title>{name}</Title>
           </Flex>
           <Button
@@ -145,9 +148,7 @@ export default class TemplateInfo extends Component<PropsType, StateType> {
         {this.renderTagSection()}
         <LineBreak />
         {this.renderBanner()}
-        <ContentSection>
-          {this.renderMarkdown()}
-        </ContentSection>
+        <ContentSection>{this.renderMarkdown()}</ContentSection>
       </StyledExpandedTemplate>
     );
   }
@@ -211,7 +212,7 @@ const TagSection = styled.div`
   margin-top: 25px;
   display: flex;
   font-size: 13px;
-  font-family: 'Work Sans', sans-serif;
+  font-family: "Work Sans", sans-serif;
   align-items: center;
 
   > i {
@@ -239,16 +240,19 @@ const Flex = styled.div`
 
 const Button = styled.div`
   height: 35px;
-  background: ${(props: { isDisabled: boolean }) => (!props.isDisabled ? '#616feecc' : '#aaaabb')};
+  background: ${(props: { isDisabled: boolean }) =>
+    !props.isDisabled ? "#616feecc" : "#aaaabb"};
   :hover {
-    background: ${(props: { isDisabled: boolean }) => (!props.isDisabled ? '#505edddd' : '#aaaabb')};
+    background: ${(props: { isDisabled: boolean }) =>
+      !props.isDisabled ? "#505edddd" : "#aaaabb"};
   }
   color: white;
   font-weight: 500;
   font-size: 13px;
   padding: 10px 15px;
   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;
   display: flex;
   flex-direction: row;
@@ -271,12 +275,11 @@ const Icon = styled.img`
   margin-bottom: -1px;
 `;
 
-
 const Polymer = styled.div`
   margin-bottom: -3px;
 
   > i {
-    color: ${props => props.theme.containerIcon};
+    color: ${(props) => props.theme.containerIcon};
     font-size: 24px;
     margin-left: 12px;
     margin-right: 3px;
@@ -286,7 +289,7 @@ const Polymer = styled.div`
 const Title = styled.div`
   font-size: 24px;
   font-weight: 600;
-  font-family: 'Work Sans', sans-serif;
+  font-family: "Work Sans", sans-serif;
   margin-left: 10px;
   border-radius: 2px;
   color: #ffffff;
@@ -304,4 +307,4 @@ const TitleSection = styled.div`
 
 const StyledExpandedTemplate = styled.div`
   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;

Unele fișiere nu au fost afișate deoarece prea multe fișiere au fost modificate în acest diff