Jelajahi Sumber

Merge branch 'beta.3.doks-redirect' into dev

mergin
Alexander Belanger 5 tahun lalu
induk
melakukan
034540173e
100 mengubah file dengan 2176 tambahan dan 662 penghapusan
  1. 8 37
      README.md
  2. 1 0
      cmd/app/main.go
  3. 0 6
      cmd/migrate/keyrotate/rotate.go
  4. 1 0
      cmd/migrate/main.go
  5. 1 1
      dashboard/src/components/InfoTooltip.tsx
  6. 1 1
      dashboard/src/components/RadioSelector.tsx
  7. 2 2
      dashboard/src/components/ResourceTab.tsx
  8. 4 5
      dashboard/src/components/SaveButton.tsx
  9. 2 2
      dashboard/src/components/Selector.tsx
  10. 1 1
      dashboard/src/components/StatusIndicator.tsx
  11. 1 1
      dashboard/src/components/TabRegion.tsx
  12. 1 1
      dashboard/src/components/TooltipParent.tsx
  13. 1 1
      dashboard/src/components/forms/VeleroForm.tsx
  14. 17 17
      dashboard/src/components/image-selector/ImageList.tsx
  15. 3 2
      dashboard/src/components/image-selector/ImageSelector.tsx
  16. 4 4
      dashboard/src/components/image-selector/TagList.tsx
  17. 2 2
      dashboard/src/components/repo-selector/ActionConfEditor.tsx
  18. 3 3
      dashboard/src/components/repo-selector/ActionDetails.tsx
  19. 4 4
      dashboard/src/components/repo-selector/BranchList.tsx
  20. 6 6
      dashboard/src/components/repo-selector/ContentsList.tsx
  21. 2 2
      dashboard/src/components/repo-selector/NewGHAction.tsx
  22. 140 63
      dashboard/src/components/repo-selector/RepoList.tsx
  23. 1 1
      dashboard/src/components/values-form/Base64InputRow.tsx
  24. 1 1
      dashboard/src/components/values-form/CheckboxList.tsx
  25. 1 1
      dashboard/src/components/values-form/Heading.tsx
  26. 1 1
      dashboard/src/components/values-form/InputRow.tsx
  27. 1 1
      dashboard/src/components/values-form/KeyValueArray.tsx
  28. 1 1
      dashboard/src/components/values-form/MultiSelect.tsx
  29. 3 3
      dashboard/src/components/values-form/ValuesForm.tsx
  30. 1 1
      dashboard/src/components/values-form/ValuesWrapper.tsx
  31. 2 2
      dashboard/src/main/CurrentError.tsx
  32. 72 12
      dashboard/src/main/Main.tsx
  33. 8 8
      dashboard/src/main/auth/Login.tsx
  34. 6 6
      dashboard/src/main/auth/Register.tsx
  35. 429 0
      dashboard/src/main/auth/ResetPasswordFinalize.tsx
  36. 373 0
      dashboard/src/main/auth/ResetPasswordInit.tsx
  37. 359 0
      dashboard/src/main/auth/VerifyEmail.tsx
  38. 15 23
      dashboard/src/main/home/Home.tsx
  39. 7 7
      dashboard/src/main/home/cluster-dashboard/ClusterDashboard.tsx
  40. 7 7
      dashboard/src/main/home/cluster-dashboard/NamespaceSelector.tsx
  41. 3 3
      dashboard/src/main/home/cluster-dashboard/SortSelector.tsx
  42. 2 2
      dashboard/src/main/home/cluster-dashboard/chart/Chart.tsx
  43. 15 15
      dashboard/src/main/home/cluster-dashboard/chart/ChartList.tsx
  44. 44 48
      dashboard/src/main/home/cluster-dashboard/expanded-chart/ExpandedChart.tsx
  45. 1 1
      dashboard/src/main/home/cluster-dashboard/expanded-chart/GraphSection.tsx
  46. 3 3
      dashboard/src/main/home/cluster-dashboard/expanded-chart/ListSection.tsx
  47. 9 9
      dashboard/src/main/home/cluster-dashboard/expanded-chart/RevisionSection.tsx
  48. 18 19
      dashboard/src/main/home/cluster-dashboard/expanded-chart/SettingsSection.tsx
  49. 5 5
      dashboard/src/main/home/cluster-dashboard/expanded-chart/ValuesYaml.tsx
  50. 1 1
      dashboard/src/main/home/cluster-dashboard/expanded-chart/deploy/DeploySection.tsx
  51. 3 3
      dashboard/src/main/home/cluster-dashboard/expanded-chart/graph/Edge.tsx
  52. 19 19
      dashboard/src/main/home/cluster-dashboard/expanded-chart/graph/GraphDisplay.tsx
  53. 2 2
      dashboard/src/main/home/cluster-dashboard/expanded-chart/graph/InfoPanel.tsx
  54. 2 2
      dashboard/src/main/home/cluster-dashboard/expanded-chart/graph/Node.tsx
  55. 2 2
      dashboard/src/main/home/cluster-dashboard/expanded-chart/graph/SelectRegion.tsx
  56. 1 1
      dashboard/src/main/home/cluster-dashboard/expanded-chart/graph/ZoomPanel.tsx
  57. 13 13
      dashboard/src/main/home/cluster-dashboard/expanded-chart/metrics/AreaChart.tsx
  58. 132 28
      dashboard/src/main/home/cluster-dashboard/expanded-chart/metrics/MetricsSection.tsx
  59. 11 11
      dashboard/src/main/home/cluster-dashboard/expanded-chart/status/ControllerTab.tsx
  60. 3 3
      dashboard/src/main/home/cluster-dashboard/expanded-chart/status/Logs.tsx
  61. 11 8
      dashboard/src/main/home/cluster-dashboard/expanded-chart/status/StatusSection.tsx
  62. 16 4
      dashboard/src/main/home/dashboard/ClusterList.tsx
  63. 2 2
      dashboard/src/main/home/dashboard/ClusterPlaceholder.tsx
  64. 4 4
      dashboard/src/main/home/dashboard/Dashboard.tsx
  65. 13 7
      dashboard/src/main/home/integrations/IntegrationCategories.tsx
  66. 77 1
      dashboard/src/main/home/integrations/IntegrationList.tsx
  67. 10 6
      dashboard/src/main/home/integrations/IntegrationRow.tsx
  68. 5 4
      dashboard/src/main/home/integrations/Integrations.tsx
  69. 2 2
      dashboard/src/main/home/integrations/create-integration/DockerHubForm.tsx
  70. 4 4
      dashboard/src/main/home/integrations/create-integration/ECRForm.tsx
  71. 2 2
      dashboard/src/main/home/integrations/create-integration/EKSForm.tsx
  72. 8 8
      dashboard/src/main/home/integrations/create-integration/GCRForm.tsx
  73. 2 2
      dashboard/src/main/home/integrations/create-integration/GKEForm.tsx
  74. 2 2
      dashboard/src/main/home/integrations/edit-integration/DockerHubForm.tsx
  75. 4 4
      dashboard/src/main/home/integrations/edit-integration/ECRForm.tsx
  76. 2 2
      dashboard/src/main/home/integrations/edit-integration/EKSForm.tsx
  77. 8 8
      dashboard/src/main/home/integrations/edit-integration/GCRForm.tsx
  78. 2 2
      dashboard/src/main/home/integrations/edit-integration/GKEForm.tsx
  79. 8 8
      dashboard/src/main/home/launch/Launch.tsx
  80. 5 5
      dashboard/src/main/home/launch/expanded-template/ExpandedTemplate.tsx
  81. 38 38
      dashboard/src/main/home/launch/expanded-template/LaunchTemplate.tsx
  82. 1 1
      dashboard/src/main/home/launch/expanded-template/TemplateInfo.tsx
  83. 1 1
      dashboard/src/main/home/launch/hardcodedNameDict.tsx
  84. 2 2
      dashboard/src/main/home/modals/ClusterInstructionsModal.tsx
  85. 1 1
      dashboard/src/main/home/modals/IntegrationsInstructionsModal.tsx
  86. 7 7
      dashboard/src/main/home/modals/IntegrationsModal.tsx
  87. 9 10
      dashboard/src/main/home/modals/UpdateClusterModal.tsx
  88. 4 4
      dashboard/src/main/home/navbar/Feedback.tsx
  89. 3 14
      dashboard/src/main/home/navbar/Navbar.tsx
  90. 1 1
      dashboard/src/main/home/new-project/NewProject.tsx
  91. 17 18
      dashboard/src/main/home/project-settings/InviteList.tsx
  92. 18 3
      dashboard/src/main/home/project-settings/ProjectSettings.tsx
  93. 13 13
      dashboard/src/main/home/provisioner/AWSFormSection.tsx
  94. 8 7
      dashboard/src/main/home/provisioner/DOFormSection.tsx
  95. 4 4
      dashboard/src/main/home/provisioner/ExistingClusterSection.tsx
  96. 12 12
      dashboard/src/main/home/provisioner/GCPFormSection.tsx
  97. 1 1
      dashboard/src/main/home/provisioner/InfraStatuses.tsx
  98. 53 5
      dashboard/src/main/home/provisioner/Provisioner.tsx
  99. 9 4
      dashboard/src/main/home/provisioner/ProvisionerLogs.tsx
  100. 5 5
      dashboard/src/main/home/provisioner/ProvisionerSettings.tsx

+ 8 - 37
README.md

@@ -1,6 +1,6 @@
 # Porter
 
-[![MIT License](https://img.shields.io/apm/l/atomic-design-ui.svg?)](https://github.com/tterb/atomic-design-ui/blob/master/LICENSEs) [![Go Report Card](https://goreportcard.com/badge/gojp/goreportcard)](https://goreportcard.com/report/github.com/porter-dev/porter) [![Discord](https://img.shields.io/discord/542888846271184896?color=7389D8&label=community&logo=discord&logoColor=ffffff)](https://discord.gg/MhYNuWwqum)
+[![MIT License](https://img.shields.io/apm/l/atomic-design-ui.svg?)](https://github.com/tterb/atomic-design-ui/blob/master/LICENSEs) [![Go Report Card](https://goreportcard.com/badge/gojp/goreportcard)](https://goreportcard.com/report/github.com/porter-dev/porter) [![Discord](https://img.shields.io/discord/542888846271184896?color=7389D8&label=community&logo=discord&logoColor=ffffff)](https://discord.gg/34n7NN7FJ7)
 [![Twitter](https://img.shields.io/twitter/url/https/twitter.com/cloudposse.svg?style=social&label=Follow)](https://twitter.com/getporterdev)
 
 **Porter is a Kubernetes-powered PaaS that runs in your own cloud provider.** Porter brings the Heroku experience to Kubernetes without compromising its flexibility. Get started on Porter without the overhead of DevOps and fully customize your infra later when you need to.
@@ -9,7 +9,7 @@
 
 ## Community and Updates
 
-For help, questions, or if you just want a place to hang out, [join our Discord community.](https://discord.gg/MhYNuWwqum)
+For help, questions, or if you just want a place to hang out, [join our Discord community.](https://discord.gg/34n7NN7FJ7)
 
 To keep updated on our progress, please watch the repo for new releases (**Watch > Custom > Releases**) and [follow us on Twitter](https://twitter.com/getporterdev)!
 
@@ -32,11 +32,11 @@ Porter brings the simplicity of a traditional PaaS to your own cloud provider wh
   - ✅ GCP
   - ✅ Digital Ocean
 - Simple deploy of any public or private Docker image
+- Auto CI/CD with [buildpacks](https://buildpacks.io) for non-Dockerized apps
 - Heroku-like GUI to monitor application status, logs, and history
-- Marketplace for one click add-ons (e.g. MongoDB, Redis, PostgreSQL)
 - Application rollback to previously deployed versions
-- Deploy webhooks that can be triggered from CI/CD pipelines
-- Native CI/CD with buildpacks for non-Dockerized apps (🚧 Coming Soon)
+- Zero-downtime deploy and health checks
+- Marketplace for one click add-ons (e.g. MongoDB, Redis, PostgreSQL)
 
 ### DevOps Mode
 
@@ -54,45 +54,16 @@ For those who are familiar with Kubernetes and Helm:
 
 Below are instructions for a quickstart. For full documentation, please visit our [official Docs.](https://docs.getporter.dev)
 
-## CLI Installation
-
-### Mac
-
-Run the following command to grab the latest binary:
-
-```sh
-{
-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 \")
-name=$(basename $name)
-curl -L https://github.com/porter-dev/porter/releases/latest/download/$name --output $name
-unzip -a $name
-rm $name
-}
-```
-
-Then move the file into your bin:
-
-```sh
-chmod +x ./porter
-sudo mv ./porter /usr/local/bin/porter
-```
-
-For Linux and Windows installation, see our [Docs](https://docs.getporter.dev/docs/cli-documentation#linux).
-
 ## Getting Started
 
 1. Sign up and log into [Porter Dashboard](https://dashboard.getporter.dev).
 
-2. Create a Project and select a cloud provider you want to provision a Kubernetes cluster in (AWS, GCP, DO). It is also possible to [link up your own Kubernetes cluster.](https://docs.getporter.dev/docs/cli-documentation#connecting-to-an-existing-cluster)
-
-3. [Put in your credentials](https://docs.getporter.dev/docs/getting-started-with-porter-on-aws), then Porter will automatically provision a cluster and an image registry in your own cloud account.
-
-4. [Build and push your Docker image](https://docs.getporter.dev/docs/cli-documentation#porter-docker-configure), or connect your git repository. We are currently working on supporting the latter option for non-Dockerized applications.
+2. Create a Project and [put in your cloud provider credentials](https://docs.getporter.dev/docs/getting-started-with-porter-on-aws). Porter will automatically provision a Kubernetes cluster in your own cloud. It is also possible to [link up an existing Kubernetes cluster.](https://docs.getporter.dev/docs/cli-documentation#connecting-to-an-existing-cluster)
 
-5. From the Templates tab on the Dashboard, select the Docker template. Click on the image you have just pushed, configure the port, then hit deploy.
+3. Deploy your applications from a [git repository](https://docs.getporter.dev/docs/applications) or [Docker image registry](https://docs.getporter.dev/docs/cli-documentation#porter-docker-configure).
 
 ## Want to Help?
 
-We welcome all contributions. Submit an issue or a pull request to help us improve Porter! If you're interested in contributing, please [join our Discord community.](https://discord.gg/MhYNuWwqum)
+We welcome all contributions. Submit an issue or a pull request to help us improve Porter! If you're interested in contributing, please [join our Discord community](https://discord.gg/34n7NN7FJ7) for more info.
 
 ![porter](https://user-images.githubusercontent.com/65516095/103712859-def9ee00-4f88-11eb-804c-4b775d697ec4.jpeg)

+ 1 - 0
cmd/app/main.go

@@ -61,6 +61,7 @@ func main() {
 		&models.Invite{},
 		&models.AuthCode{},
 		&models.DNSRecord{},
+		&models.PWResetToken{},
 		&ints.KubeIntegration{},
 		&ints.BasicIntegration{},
 		&ints.OIDCIntegration{},

+ 0 - 6
cmd/migrate/keyrotate/rotate.go

@@ -22,12 +22,6 @@ func Rotate(db *_gorm.DB, oldKey, newKey *[32]byte) error {
 	copy(oldKeyBytes[:], oldKey[:])
 	copy(newKeyBytes[:], newKey[:])
 
-	fmt.Printf("beginning key rotation from %s to %s\n", string(oldKeyBytes), string(newKeyBytes))
-
-	for i, b := range oldKeyBytes {
-		fmt.Println(i, ":", string(b), string(newKeyBytes[i]))
-	}
-
 	err := rotateClusterModel(db, oldKey, newKey)
 
 	if err != nil {

+ 1 - 0
cmd/migrate/main.go

@@ -46,6 +46,7 @@ func main() {
 		&models.Invite{},
 		&models.AuthCode{},
 		&models.DNSRecord{},
+		&models.PWResetToken{},
 		&ints.KubeIntegration{},
 		&ints.BasicIntegration{},
 		&ints.OIDCIntegration{},

+ 1 - 1
dashboard/src/components/InfoTooltip.tsx

@@ -11,7 +11,7 @@ type StateType = {
 
 export default class InfoTooltip extends Component<PropsType, StateType> {
   state = {
-    showTooltip: false,
+    showTooltip: false
   };
 
   render() {

+ 1 - 1
dashboard/src/components/RadioSelector.tsx

@@ -53,7 +53,7 @@ const Indicator = styled.div<{ selected: boolean }>`
   height: 16px;
   border: 1px solid #ffffff55;
   margin: 1px 10px 0px 1px;
-  background: ${(props) => (props.selected ? "#ffffff22" : "#ffffff11")};
+  background: ${props => (props.selected ? "#ffffff22" : "#ffffff11")};
 `;
 
 const Circle = styled.div`

+ 2 - 2
dashboard/src/components/ResourceTab.tsx

@@ -26,7 +26,7 @@ type StateType = {
 export default class ResourceTab extends Component<PropsType, StateType> {
   state = {
     expanded: this.props.expanded || false,
-    showTooltip: false,
+    showTooltip: false
   };
 
   renderDropdownIcon = () => {
@@ -95,7 +95,7 @@ export default class ResourceTab extends Component<PropsType, StateType> {
       handleClick,
       selected,
       status,
-      roundAllCorners,
+      roundAllCorners
     } = this.props;
     return (
       <StyledResourceTab

+ 4 - 5
dashboard/src/components/SaveButton.tsx

@@ -132,15 +132,14 @@ const Button = styled.button`
   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;
   }
   :hover {
-    filter: ${(props) => (!props.disabled ? "brightness(120%)" : "")};
+    filter: ${props => (!props.disabled ? "brightness(120%)" : "")};
   }
 `;

+ 2 - 2
dashboard/src/components/Selector.tsx

@@ -17,7 +17,7 @@ type StateType = {};
 
 export default class Selector extends Component<PropsType, StateType> {
   state = {
-    expanded: false,
+    expanded: false
   };
 
   wrapperRef: any = React.createRef();
@@ -192,7 +192,7 @@ const Dropdown = styled.div`
 
 const StyledSelector = styled.div<{ width: string }>`
   position: relative;
-  width: ${(props) => props.width};
+  width: ${props => props.width};
 `;
 
 const MainSelector = styled.div`

+ 1 - 1
dashboard/src/components/StatusIndicator.tsx

@@ -68,7 +68,7 @@ export default class StatusIndicator extends Component<PropsType, StateType> {
       case "daemonset":
         return c.status.numberAvailable == c.status.desiredNumberScheduled;
       case "cronjob":
-        return 1
+        return 1;
     }
   };
 

+ 1 - 1
dashboard/src/components/TabRegion.tsx

@@ -27,7 +27,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.props.setCurrentTab(this.defaultTab());
       }
     }

+ 1 - 1
dashboard/src/components/TooltipParent.tsx

@@ -11,7 +11,7 @@ type StateType = {
 
 export default class TooltipParent extends Component<PropsType, StateType> {
   state = {
-    showTooltip: false,
+    showTooltip: false
   };
 
   renderTooltip = (): JSX.Element | undefined => {

+ 1 - 1
dashboard/src/components/forms/VeleroForm.tsx

@@ -24,7 +24,7 @@ export default class VeleroForm extends Component<PropsType, StateType> {
     includeNamespaces: [] as string[],
     includeResources: [] as string[],
     storageLocation: "",
-    volumeSnapshotLocations: [] as string[],
+    volumeSnapshotLocations: [] as string[]
   };
 
   render() {

+ 17 - 17
dashboard/src/components/image-selector/ImageList.tsx

@@ -30,7 +30,7 @@ export default class ImageList extends Component<PropsType, StateType> {
   state = {
     loading: true,
     error: false,
-    images: [] as ImageType[],
+    images: [] as ImageType[]
   };
 
   // TODO: Try to unhook before unmount
@@ -42,7 +42,7 @@ export default class ImageList extends Component<PropsType, StateType> {
     if (!this.props.registry) {
       api
         .getProjectRegistries("<token>", {}, { id: currentProject.id })
-        .then((res) => {
+        .then(res => {
           let registries = res.data;
           if (registries.length === 0) {
             this.setState({ loading: false });
@@ -58,10 +58,10 @@ export default class ImageList extends Component<PropsType, StateType> {
                     {},
                     {
                       project_id: currentProject.id,
-                      registry_id: registry.id,
+                      registry_id: registry.id
                     }
                   )
-                  .then((res) => {
+                  .then(res => {
                     res.data.sort((a: any, b: any) =>
                       a.name > b.name ? 1 : -1
                     );
@@ -72,20 +72,20 @@ export default class ImageList extends Component<PropsType, StateType> {
                           kind: registry.service,
                           source: img.uri,
                           name: img.name,
-                          registryId: registry.id,
+                          registryId: registry.id
                         });
                       }
                       return {
                         kind: registry.service,
                         source: img.uri,
                         name: img.name,
-                        registryId: registry.id,
+                        registryId: registry.id
                       };
                     });
                     images.push(...newImg);
                     errors.push(0);
                   })
-                  .catch((err) => errors.push(1))
+                  .catch(err => errors.push(1))
                   .finally(() => {
                     if (i == registries.length - 1) {
                       let error =
@@ -98,11 +98,11 @@ export default class ImageList extends Component<PropsType, StateType> {
                       this.setState({
                         images,
                         loading: false,
-                        error,
+                        error
                       });
                     } else {
                       this.setState({
-                        images,
+                        images
                       });
                     }
 
@@ -112,7 +112,7 @@ export default class ImageList extends Component<PropsType, StateType> {
             );
           });
         })
-        .catch((err) => {
+        .catch(err => {
           console.log(err);
           this.setState({ loading: false, error: true });
         });
@@ -123,10 +123,10 @@ export default class ImageList extends Component<PropsType, StateType> {
           {},
           {
             project_id: currentProject.id,
-            registry_id: this.props.registry.id,
+            registry_id: this.props.registry.id
           }
         )
-        .then((res) => {
+        .then(res => {
           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) => {
@@ -135,14 +135,14 @@ export default class ImageList extends Component<PropsType, StateType> {
                 kind: this.props.registry.service,
                 source: img.uri,
                 name: img.name,
-                registryId: this.props.registry.id,
+                registryId: this.props.registry.id
               });
             }
             return {
               kind: this.props.registry.service,
               source: img.uri,
               name: img.name,
-              registryId: this.props.registry.id,
+              registryId: this.props.registry.id
             };
           });
           images.push(...newImg);
@@ -150,13 +150,13 @@ export default class ImageList extends Component<PropsType, StateType> {
           this.setState({
             images,
             loading: false,
-            error: false,
+            error: false
           });
         })
-        .catch((err) =>
+        .catch(err =>
           this.setState({
             loading: false,
-            error: true,
+            error: true
           })
         );
     }

+ 3 - 2
dashboard/src/components/image-selector/ImageSelector.tsx

@@ -33,7 +33,7 @@ export default class ImageSelector extends Component<PropsType, StateType> {
     loading: true,
     error: false,
     images: [] as ImageType[],
-    clickedImage: null as ImageType | null,
+    clickedImage: null as ImageType | null
   };
 
   // componentDidMount() {
@@ -192,6 +192,7 @@ export default class ImageSelector extends Component<PropsType, StateType> {
       <Label>
         <img src={icon} />
         <Input
+          autoFocus={true}
           onClick={(e: any) => e.stopPropagation()}
           value={selectedImageUrl}
           onChange={(e: any) => {
@@ -202,7 +203,7 @@ export default class ImageSelector extends Component<PropsType, StateType> {
               this.setState({ isExpanded: true });
             }
           }}
-          placeholder="Enter or select your container image URL"
+          placeholder="Type your container image URL here (or select below)"
         />
       </Label>
     );

+ 4 - 4
dashboard/src/components/image-selector/TagList.tsx

@@ -27,7 +27,7 @@ export default class TagList extends Component<PropsType, StateType> {
     loading: true,
     error: false,
     tags: [] as string[],
-    currentTag: this.props.selectedTag,
+    currentTag: this.props.selectedTag
   };
 
   componentDidMount() {
@@ -41,16 +41,16 @@ export default class TagList extends Component<PropsType, StateType> {
         {
           project_id: currentProject.id,
           registry_id: this.props.registryId,
-          repo_name: repoName,
+          repo_name: repoName
         }
       )
-      .then((res) => {
+      .then(res => {
         let tags = res.data.map((tag: any, i: number) => {
           return tag.tag;
         });
         this.setState({ tags, loading: false });
       })
-      .catch((err) => {
+      .catch(err => {
         console.log(err);
         this.setState({ loading: false, error: true });
       });

+ 2 - 2
dashboard/src/components/repo-selector/ActionConfEditor.tsx

@@ -31,13 +31,13 @@ type StateType = {
 const defaultActionConfig: ActionConfigType = {
   git_repo: "",
   image_repo_uri: "",
-  git_repo_id: 0,
+  git_repo_id: 0
 };
 
 export default class ActionConfEditor extends Component<PropsType, StateType> {
   state = {
     loading: true,
-    error: false,
+    error: false
   };
 
   renderExpanded = () => {

+ 3 - 3
dashboard/src/components/repo-selector/ActionDetails.tsx

@@ -32,7 +32,7 @@ type StateType = {
 const dummyRegistries = [
   { id: 1, service: "ecr", url: "https://idfkasdfasdf" },
   { id: 12, service: "ecr", url: "https://dfasdfidfkasdfasdf" },
-  { id: 11, service: "gcr", url: "https://idfkasdfasdf" },
+  { id: 11, service: "gcr", url: "https://idfkasdfasdf" }
 ] as any[];
 
 export default class ActionDetails extends Component<PropsType, StateType> {
@@ -40,7 +40,7 @@ export default class ActionDetails extends Component<PropsType, StateType> {
     dockerRepo: "",
     error: false,
     registries: null as any[] | null,
-    loading: true,
+    loading: true
   };
 
   componentDidMount() {
@@ -281,7 +281,7 @@ const StatusWrapper = styled.div<{ successful?: boolean }>`
   > i {
     font-size: 18px;
     margin-right: 10px;
-    color: ${(props) => (props.successful ? "#4797ff" : "#fcba03")};
+    color: ${props => (props.successful ? "#4797ff" : "#fcba03")};
   }
 
   animation: statusFloatIn 0.5s;

+ 4 - 4
dashboard/src/components/repo-selector/BranchList.tsx

@@ -24,7 +24,7 @@ export default class BranchList extends Component<PropsType, StateType> {
   state = {
     loading: true,
     error: false,
-    branches: [] as string[],
+    branches: [] as string[]
   };
 
   componentDidMount() {
@@ -41,13 +41,13 @@ export default class BranchList extends Component<PropsType, StateType> {
           git_repo_id: actionConfig.git_repo_id,
           kind: "github",
           owner: actionConfig.git_repo.split("/")[0],
-          name: actionConfig.git_repo.split("/")[1],
+          name: actionConfig.git_repo.split("/")[1]
         }
       )
-      .then((res) =>
+      .then(res =>
         this.setState({ branches: res.data, loading: false, error: false })
       )
-      .catch((err) => {
+      .catch(err => {
         console.log(err);
         this.setState({ loading: false, error: true });
       });

+ 6 - 6
dashboard/src/components/repo-selector/ContentsList.tsx

@@ -35,7 +35,7 @@ export default class ContentsList extends Component<PropsType, StateType> {
     error: false,
     contents: [] as FileType[],
     currentDir: "",
-    dockerfiles: [] as string[],
+    dockerfiles: [] as string[]
   };
 
   componentDidMount() {
@@ -61,10 +61,10 @@ export default class ContentsList extends Component<PropsType, StateType> {
           kind: "github",
           owner: actionConfig.git_repo.split("/")[0],
           name: actionConfig.git_repo.split("/")[1],
-          branch: branch,
+          branch: branch
         }
       )
-      .then((res) => {
+      .then(res => {
         let files = [] as FileType[];
         let folders = [] as FileType[];
         res.data.map((x: FileType, i: number) => {
@@ -81,7 +81,7 @@ export default class ContentsList extends Component<PropsType, StateType> {
 
         this.setState({ contents, loading: false, error: false });
       })
-      .catch((err) => {
+      .catch(err => {
         console.log(err);
 
         this.setState({ loading: false, error: true });
@@ -317,7 +317,7 @@ const Indicator = styled.div<{ selected: boolean }>`
   border: 1px solid #ffffff55;
   margin: 1px 10px 0px 1px;
   margin-right: 13px;
-  background: ${(props) => (props.selected ? "#ffffff22" : "#ffffff11")};
+  background: ${props => (props.selected ? "#ffffff22" : "#ffffff11")};
 `;
 
 const Label = styled.div`
@@ -343,7 +343,7 @@ const Row = styled.div<{ isLast: boolean }>`
   padding-left: 10px;
   display: flex;
   align-items: center;
-  border-bottom: ${(props) => !props.isLast && "1px solid #aaaabb"};
+  border-bottom: ${props => !props.isLast && "1px solid #aaaabb"};
   cursor: pointer;
   :hover {
     background: #ffffff22;

+ 2 - 2
dashboard/src/components/repo-selector/NewGHAction.tsx

@@ -27,7 +27,7 @@ export default class NewGHAction extends Component<PropsType, StateType> {
     dockerRepo: "",
     trueDockerPath: this.props.dockerPath,
     loading: false,
-    error: false,
+    error: false
   };
 
   componentDidMount() {
@@ -36,7 +36,7 @@ export default class NewGHAction extends Component<PropsType, StateType> {
         trueDockerPath: this.props.dockerPath.substring(
           1,
           this.props.dockerPath.length
-        ),
+        )
       });
     }
   }

+ 140 - 63
dashboard/src/components/repo-selector/RepoList.tsx

@@ -20,13 +20,15 @@ type StateType = {
   repos: RepoType[];
   loading: boolean;
   error: boolean;
+  searchFilter: string;
 };
 
-export default class ActionConfEditor extends Component<PropsType, StateType> {
+export default class RepoList extends Component<PropsType, StateType> {
   state = {
     repos: [] as RepoType[],
     loading: true,
     error: false,
+    searchFilter: ""
   };
 
   // TODO: Try to unhook before unmount
@@ -37,64 +39,103 @@ export default class ActionConfEditor extends Component<PropsType, StateType> {
     if (!this.props.userId && this.props.userId !== 0) {
       api
         .getGitRepos("<token>", {}, { project_id: currentProject.id })
-        .then((res) => {
+        .then(async res => {
+          if (res.data.length == 0) {
+            this.setState({ loading: false, error: false });
+            return;
+          }
+
           var allRepos: any = [];
-          // TODO: make into promise.all
-          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 }
-              )
-              .then((res) => {
-                res.data.forEach((repo: any, id: number) => {
-                  repo.GHRepoID = grid;
-                });
-                allRepos = allRepos.concat(res.data);
-                allRepos.sort((a: any, b: any) => {
-                  if (a.FullName < b.FullName) {
-                    return -1;
-                  } else if (a.FullName > b.FullName) {
-                    return 1;
-                  } else {
-                    return 0;
-                  }
-                });
-                this.setState({
-                  repos: allRepos,
-                  loading: false,
-                  error: false,
+          var errors: any = [];
+
+          var promises = res.data.map((gitrepo: any, id: number) => {
+            return new Promise((resolve, reject) => {
+              api
+                .getGitRepoList(
+                  "<token>",
+                  {},
+                  { project_id: currentProject.id, git_repo_id: gitrepo.id }
+                )
+                .then(res => {
+                  res.data.forEach((repo: any, id: number) => {
+                    repo.GHRepoID = gitrepo.id;
+                  });
+
+                  resolve(res.data);
+                })
+                .catch(err => {
+                  errors.push(err);
+                  resolve([]);
                 });
-              })
-              .catch((err) => {
-                console.log(err);
-                this.setState({ loading: false, error: true });
+            });
+          });
+
+          var sepRepos = await Promise.all(promises);
+
+          allRepos = [].concat.apply([], sepRepos);
+
+          // remove duplicates based on name
+          allRepos = allRepos.filter((repo: any, index: number, self: any) => {
+            var keep =
+              index ===
+              self.findIndex((_repo: any) => {
+                return repo.FullName === _repo.FullName;
               });
-          }
-          if (res.data.length < 1) {
-            this.setState({ loading: false, error: false });
+
+            return keep;
+          });
+
+          // sort repos based on name
+          allRepos.sort((a: any, b: any) => {
+            if (a.FullName < b.FullName) {
+              return -1;
+            } else if (a.FullName > b.FullName) {
+              return 1;
+            } else {
+              return 0;
+            }
+          });
+
+          if (allRepos.length == 0 && errors.length > 0) {
+            this.setState({ loading: false, error: true });
+          } else {
+            this.setState({
+              repos: allRepos,
+              loading: false,
+              error: false
+            });
           }
         })
-        .catch((_) => this.setState({ loading: false, error: true }));
+        .catch(_ => this.setState({ loading: false, error: true }));
     } else {
       let grid = this.props.userId;
+
       api
         .getGitRepoList(
           "<token>",
           {},
           { project_id: currentProject.id, git_repo_id: grid }
         )
-        .then((res) => {
-          res.data.forEach((repo: any, id: number) => {
+        .then(res => {
+          var repos: any = res.data;
+
+          repos.forEach((repo: any, id: number) => {
             repo.GHRepoID = grid;
           });
-          // TODO: sort repos alphabetically
-          this.setState({ repos: res.data, loading: false, error: false });
+
+          repos.sort((a: any, b: any) => {
+            if (a.FullName < b.FullName) {
+              return -1;
+            } else if (a.FullName > b.FullName) {
+              return 1;
+            } else {
+              return 0;
+            }
+          });
+
+          this.setState({ repos: repos, loading: false, error: false });
         })
-        .catch((err) => {
-          console.log(err);
+        .catch(err => {
           this.setState({ loading: false, error: true });
         });
     }
@@ -132,20 +173,24 @@ export default class ActionConfEditor extends Component<PropsType, StateType> {
       );
     }
 
-    return repos.map((repo: RepoType, i: number) => {
-      return (
-        <RepoName
-          key={i}
-          isSelected={repo.FullName === this.props.actionConfig.git_repo}
-          lastItem={i === repos.length - 1}
-          onClick={() => this.setRepo(repo)}
-          readOnly={this.props.readOnly}
-        >
-          <img src={github} />
-          {repo.FullName}
-        </RepoName>
-      );
-    });
+    return repos
+      .filter((repo: RepoType, i: number) => {
+        return repo.FullName.includes(this.state.searchFilter || "");
+      })
+      .map((repo: RepoType, i: number) => {
+        return (
+          <RepoName
+            key={i}
+            isSelected={repo.FullName === this.props.actionConfig.git_repo}
+            lastItem={i === repos.length - 1}
+            onClick={() => this.setRepo(repo)}
+            readOnly={this.props.readOnly}
+          >
+            <img src={github} />
+            {repo.FullName}
+          </RepoName>
+        );
+      });
   };
 
   renderExpanded = () => {
@@ -159,10 +204,16 @@ export default class ActionConfEditor extends Component<PropsType, StateType> {
             lastItem={false}
             readOnly={this.props.readOnly}
           >
-            <img src={info} />
-            Select Repo
+            <i className="material-icons">search</i>
+            <SearchInput
+              value={this.state.searchFilter}
+              onChange={(e: any) => {
+                this.setState({ searchFilter: e.target.value });
+              }}
+              placeholder="Search repos..."
+            />
           </InfoRow>
-          {this.renderRepoList()}
+          <ExpandedWrapper>{this.renderRepoList()}</ExpandedWrapper>
         </ExpandedWrapper>
       );
     }
@@ -173,7 +224,7 @@ export default class ActionConfEditor extends Component<PropsType, StateType> {
   }
 }
 
-ActionConfEditor.contextType = Context;
+RepoList.contextType = Context;
 
 const RepoName = styled.div`
   display: flex;
@@ -209,11 +260,13 @@ const RepoName = styled.div`
     }
   }
 
-  > img {
+  > img,
+  i {
     width: 18px;
     height: 18px;
     margin-left: 12px;
     margin-right: 12px;
+    font-size: 20px;
   }
 `;
 
@@ -222,6 +275,10 @@ const InfoRow = styled(RepoName)`
   color: #ffffff55;
   :hover {
     background: #ffffff11;
+
+    > i {
+      background: none;
+    }
   }
 `;
 
@@ -239,7 +296,16 @@ const ExpandedWrapper = styled.div`
   width: 100%;
   border-radius: 3px;
   border: 0px solid #ffffff44;
-  max-height: 275px;
+  max-height: 235px;
+  top: 40px;
+
+  > i {
+    font-size: 18px;
+    display: block;
+    position: absolute;
+    left: 10px;
+    top: 10px;
+  }
 `;
 
 const ExpandedWrapperAlt = styled(ExpandedWrapper)`
@@ -254,3 +320,14 @@ const A = styled.a`
   margin-left: 5px;
   cursor: pointer;
 `;
+
+const SearchInput = styled.input`
+  outline: none;
+  border: none;
+  font-size: 13px;
+  background: none;
+  width: 100%;
+  color: white;
+  padding: 0;
+  height: 20px;
+`;

+ 1 - 1
dashboard/src/components/values-form/Base64InputRow.tsx

@@ -19,7 +19,7 @@ type StateType = {
 
 export default class InputRow extends Component<PropsType, StateType> {
   state = {
-    readOnly: true,
+    readOnly: true
   };
 
   handleChange = (e: ChangeEvent<HTMLInputElement>) => {

+ 1 - 1
dashboard/src/components/values-form/CheckboxList.tsx

@@ -67,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 {

+ 1 - 1
dashboard/src/components/values-form/Heading.tsx

@@ -11,7 +11,7 @@ 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;

+ 1 - 1
dashboard/src/components/values-form/InputRow.tsx

@@ -19,7 +19,7 @@ type StateType = {
 
 export default class InputRow extends Component<PropsType, StateType> {
   state = {
-    readOnly: true,
+    readOnly: true
   };
 
   handleChange = (e: ChangeEvent<HTMLInputElement>) => {

+ 1 - 1
dashboard/src/components/values-form/KeyValueArray.tsx

@@ -14,7 +14,7 @@ type StateType = {
 
 export default class KeyValueArray extends Component<PropsType, StateType> {
   state = {
-    values: [] as any[],
+    values: [] as any[]
   };
 
   componentDidMount() {

+ 1 - 1
dashboard/src/components/values-form/MultiSelect.tsx

@@ -9,7 +9,7 @@ type StateType = {
 
 export default class MultiSelect extends Component<PropsType, StateType> {
   state = {
-    options: [] as { label: string; value: string }[],
+    options: [] as { label: string; value: string }[]
   };
 
   renderOptions = () => {};

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

@@ -168,7 +168,7 @@ export default class ValuesForm extends Component<PropsType, StateType> {
             <SelectRow
               key={i}
               value={this.props.metaState[key]}
-              setActiveValue={(val) => this.props.setMetaState({ [key]: val })}
+              setActiveValue={val => this.props.setMetaState({ [key]: val })}
               options={item.settings.options}
               dropdownLabel=""
               label={item.label}
@@ -179,11 +179,11 @@ export default class ValuesForm extends Component<PropsType, StateType> {
             <SelectRow
               key={i}
               value={this.props.metaState[key]}
-              setActiveValue={(val) => this.props.setMetaState({ [key]: val })}
+              setActiveValue={val => this.props.setMetaState({ [key]: val })}
               options={[
                 { value: "aws", label: "Amazon Web Services (AWS)" },
                 { value: "gcp", label: "Google Cloud Platform (GCP)" },
-                { value: "do", label: "DigitalOcean" },
+                { value: "do", label: "DigitalOcean" }
               ]}
               dropdownLabel=""
               label={item.label}

+ 1 - 1
dashboard/src/components/values-form/ValuesWrapper.tsx

@@ -20,7 +20,7 @@ type StateType = any;
 const providerMap: any = {
   gke: "gcp",
   eks: "aws",
-  doks: "do",
+  doks: "do"
 };
 
 // Manages the consolidated state of all form tabs ("metastate")

+ 2 - 2
dashboard/src/main/CurrentError.tsx

@@ -12,7 +12,7 @@ type StateType = {};
 
 export default class CurrentError extends Component<PropsType, StateType> {
   state = {
-    expanded: false,
+    expanded: false
   };
 
   componentDidUpdate(prevProps: PropsType) {
@@ -32,7 +32,7 @@ export default class CurrentError extends Component<PropsType, StateType> {
           <StyledCurrentError onClick={() => this.setState({ expanded: true })}>
             <ErrorText>Error: {this.props.currentError}</ErrorText>
             <CloseButton
-              onClick={(e) => {
+              onClick={e => {
                 this.context.setCurrentError(null);
                 e.stopPropagation();
               }}

+ 72 - 12
dashboard/src/main/Main.tsx

@@ -5,8 +5,11 @@ import { BrowserRouter, Route, Redirect, Switch } from "react-router-dom";
 import api from "shared/api";
 import { Context } from "shared/Context";
 
-import Login from "./Login";
-import Register from "./Register";
+import ResetPasswordInit from "./auth/ResetPasswordInit";
+import ResetPasswordFinalize from "./auth/ResetPasswordFinalize";
+import Login from "./auth/Login";
+import Register from "./auth/Register";
+import VerifyEmail from "./auth/VerifyEmail";
 import CurrentError from "./CurrentError";
 import Home from "./home/Home";
 import Loading from "components/Loading";
@@ -17,6 +20,7 @@ type PropsType = {};
 type StateType = {
   loading: boolean;
   isLoggedIn: boolean;
+  isEmailVerified: boolean;
   initialized: boolean;
 };
 
@@ -24,7 +28,8 @@ export default class Main extends Component<PropsType, StateType> {
   state = {
     loading: true,
     isLoggedIn: false,
-    initialized: localStorage.getItem("init") === "true",
+    isEmailVerified: false,
+    initialized: localStorage.getItem("init") === "true"
   };
 
   componentDidMount() {
@@ -34,19 +39,20 @@ export default class Main extends Component<PropsType, StateType> {
     error && setCurrentError(error);
     api
       .checkAuth("", {}, {})
-      .then((res) => {
+      .then(res => {
         if (res && res.data) {
           setUser(res?.data?.id, res?.data?.email);
           this.setState({
             isLoggedIn: true,
+            isEmailVerified: res?.data?.email_verified,
             initialized: true,
-            loading: false,
+            loading: false
           });
         } else {
           this.setState({ isLoggedIn: false, loading: false });
         }
       })
-      .catch((err) => this.setState({ isLoggedIn: false, loading: false }));
+      .catch(err => this.setState({ isLoggedIn: false, loading: false }));
   }
 
   initialize = () => {
@@ -55,15 +61,35 @@ export default class Main extends Component<PropsType, StateType> {
   };
 
   authenticate = () => {
-    this.setState({ isLoggedIn: true, initialized: true });
+    api
+      .checkAuth("", {}, {})
+      .then(res => {
+        if (res && res.data) {
+          this.context.setUser(res?.data?.id, res?.data?.email);
+          this.setState({
+            isLoggedIn: true,
+            isEmailVerified: res?.data?.email_verified,
+            initialized: true,
+            loading: false
+          });
+        } else {
+          this.setState({ isLoggedIn: false, loading: false });
+        }
+      })
+      .catch(err => this.setState({ isLoggedIn: false, loading: false }));
   };
 
   handleLogOut = () => {
     // Clears local storage for proper rendering of clusters
-    localStorage.clear();
-
-    this.context.clearContext();
-    this.setState({ isLoggedIn: false, initialized: true });
+    // Attempt user logout
+    api
+      .logOutUser("<token>", {}, {})
+      .then(() => {
+        this.context.clearContext();
+        this.setState({ isLoggedIn: false, initialized: true });
+        localStorage.clear();
+      })
+      .catch(err => this.context.setCurrentError(err.response.data.errors[0]));
   };
 
   renderMain = () => {
@@ -71,6 +97,20 @@ export default class Main extends Component<PropsType, StateType> {
       return <Loading />;
     }
 
+    // if logged in but not verified, block until email verification
+    if (this.state.isLoggedIn && !this.state.isEmailVerified) {
+      return (
+        <Switch>
+          <Route
+            path="/"
+            render={() => {
+              return <VerifyEmail handleLogout={this.handleLogOut} />;
+            }}
+          />
+        </Switch>
+      );
+    }
+
     return (
       <Switch>
         <Route
@@ -93,6 +133,26 @@ export default class Main extends Component<PropsType, StateType> {
             }
           }}
         />
+        <Route
+          path="/password/reset/finalize"
+          render={() => {
+            if (!this.state.isLoggedIn) {
+              return <ResetPasswordFinalize />;
+            } else {
+              return <Redirect to="/" />;
+            }
+          }}
+        />
+        <Route
+          path="/password/reset"
+          render={() => {
+            if (!this.state.isLoggedIn) {
+              return <ResetPasswordInit />;
+            } else {
+              return <Redirect to="/" />;
+            }
+          }}
+        />
         <Route
           exact
           path="/"
@@ -106,7 +166,7 @@ export default class Main extends Component<PropsType, StateType> {
         />
         <Route
           path={`/:baseRoute`}
-          render={(routeProps) => {
+          render={routeProps => {
             const baseRoute = routeProps.match.params.baseRoute;
             if (
               this.state.isLoggedIn &&

+ 8 - 8
dashboard/src/main/Login.tsx → dashboard/src/main/auth/Login.tsx

@@ -23,7 +23,7 @@ export default class Login extends Component<PropsType, StateType> {
     email: "",
     password: "",
     emailError: false,
-    credentialError: false,
+    credentialError: false
   };
 
   handleKeyDown = (e: any) => {
@@ -57,11 +57,11 @@ export default class Login extends Component<PropsType, StateType> {
           "",
           {
             email: email,
-            password: password,
+            password: password
           },
           {}
         )
-        .then((res) => {
+        .then(res => {
           // TODO: case and set credential error
           if (res?.data?.redirect) {
             window.location.href = res.data.redirect;
@@ -70,7 +70,7 @@ export default class Login extends Component<PropsType, StateType> {
             authenticate();
           }
         })
-        .catch((err) =>
+        .catch(err =>
           this.context.setCurrentError(err.response.data.errors[0])
         );
     }
@@ -137,7 +137,7 @@ export default class Login extends Component<PropsType, StateType> {
                   this.setState({
                     email: e.target.value,
                     emailError: false,
-                    credentialError: false,
+                    credentialError: false
                   })
                 }
                 valid={!credentialError && !emailError}
@@ -152,7 +152,7 @@ export default class Login extends Component<PropsType, StateType> {
                 onChange={(e: ChangeEvent<HTMLInputElement>) =>
                   this.setState({
                     password: e.target.value,
-                    credentialError: false,
+                    credentialError: false
                   })
                 }
                 valid={!credentialError}
@@ -162,8 +162,8 @@ export default class Login extends Component<PropsType, StateType> {
             <Button onClick={this.handleLogin}>Continue</Button>
 
             <Helper>
-              Don't have an account?
-              <Link href="/register">Sign up</Link>
+              <Link href="/register">Sign up</Link> |
+              <Link href="/password/reset">Forgot password?</Link>
             </Helper>
           </FormWrapper>
         </LoginPanel>

+ 6 - 6
dashboard/src/main/Register.tsx → dashboard/src/main/auth/Register.tsx

@@ -25,7 +25,7 @@ export default class Register extends Component<PropsType, StateType> {
     password: "",
     confirmPassword: "",
     emailError: false,
-    confirmPasswordError: false,
+    confirmPasswordError: false
   };
 
   handleKeyDown = (e: any) => {
@@ -66,7 +66,7 @@ export default class Register extends Component<PropsType, StateType> {
           "",
           {
             email: email,
-            password: password,
+            password: password
           },
           {}
         )
@@ -78,7 +78,7 @@ export default class Register extends Component<PropsType, StateType> {
             authenticate();
           }
         })
-        .catch((err) => setCurrentError(err.response.data.errors[0]));
+        .catch(err => setCurrentError(err.response.data.errors[0]));
     }
   };
 
@@ -112,7 +112,7 @@ export default class Register extends Component<PropsType, StateType> {
       password,
       confirmPassword,
       emailError,
-      confirmPasswordError,
+      confirmPasswordError
     } = this.state;
 
     return (
@@ -154,7 +154,7 @@ export default class Register extends Component<PropsType, StateType> {
               onChange={(e: ChangeEvent<HTMLInputElement>) =>
                 this.setState({
                   password: e.target.value,
-                  confirmPasswordError: false,
+                  confirmPasswordError: false
                 })
               }
               valid={true}
@@ -167,7 +167,7 @@ export default class Register extends Component<PropsType, StateType> {
                 onChange={(e: ChangeEvent<HTMLInputElement>) =>
                   this.setState({
                     confirmPassword: e.target.value,
-                    confirmPasswordError: false,
+                    confirmPasswordError: false
                   })
                 }
                 valid={!confirmPasswordError}

+ 429 - 0
dashboard/src/main/auth/ResetPasswordFinalize.tsx

@@ -0,0 +1,429 @@
+import React, { ChangeEvent, Component } from "react";
+import styled from "styled-components";
+import logo from "assets/logo.png";
+
+import api from "shared/api";
+import { Context } from "shared/Context";
+import Loading from "components/Loading";
+
+type PropsType = {};
+
+type StateType = {
+  email: string;
+  token: string;
+  token_id: number;
+  password: string;
+  passwordError: boolean;
+  tokenError: boolean;
+  loading: boolean;
+  submitted: boolean;
+};
+
+export default class ResetPasswordInit extends Component<PropsType, StateType> {
+  state = {
+    email: "",
+    token: "",
+    password: "",
+    token_id: 0,
+    passwordError: false,
+    tokenError: false,
+    loading: true,
+    submitted: false
+  };
+
+  handleKeyDown = (e: any) => {
+    e.key === "Enter" ? this.handleResetPasswordFinalize() : null;
+  };
+
+  componentDidMount() {
+    let urlParams = new URLSearchParams(window.location.search);
+
+    let emailFromParam = urlParams.get("email");
+    let tokenFromParams = urlParams.get("token");
+    let tokenIDFromParams = urlParams.get("token_id");
+
+    api
+      .createPasswordResetVerify(
+        "",
+        {
+          email: emailFromParam,
+          token: tokenFromParams,
+          token_id: parseInt(tokenIDFromParams)
+        },
+        {}
+      )
+      .then(() => {
+        this.setState({ loading: false });
+      })
+      .catch(err => this.setState({ loading: false, tokenError: true }));
+
+    document.addEventListener("keydown", this.handleKeyDown);
+
+    this.setState({
+      email: emailFromParam,
+      token: tokenFromParams,
+      token_id: parseInt(tokenIDFromParams)
+    });
+  }
+
+  componentWillUnmount() {
+    document.removeEventListener("keydown", this.handleKeyDown);
+  }
+
+  renderPasswordError = () => {
+    let { passwordError } = this.state;
+    if (passwordError) {
+      return (
+        <ErrorHelper>
+          <div />
+          Invalid Password
+        </ErrorHelper>
+      );
+    }
+  };
+
+  handleResetPasswordFinalize = (): void => {
+    let { email, token, token_id, password } = this.state;
+
+    // Call reset password
+    api
+      .createPasswordResetFinalize(
+        "",
+        {
+          email: email,
+          token: token,
+          token_id: token_id,
+          new_password: password
+        },
+        {}
+      )
+      .then(res => {
+        // redirect to dashboard with message after timeout
+        this.setState({ submitted: true });
+
+        setTimeout(() => {
+          window.location.href = "/login";
+        }, 2000);
+      })
+      .catch(err => this.setState({ tokenError: true }));
+  };
+
+  render() {
+    let {
+      password,
+      passwordError,
+      submitted,
+      loading,
+      tokenError
+    } = this.state;
+
+    let inputSection = (
+      <div>
+        <InputWrapper>
+          <Input
+            type="password"
+            placeholder="Password"
+            value={password}
+            onChange={(e: ChangeEvent<HTMLInputElement>) =>
+              this.setState({
+                password: e.target.value,
+                passwordError: false
+              })
+            }
+            valid={!passwordError}
+          />
+          {this.renderPasswordError()}
+        </InputWrapper>
+        <Button onClick={this.handleResetPasswordFinalize}>Continue</Button>
+      </div>
+    );
+
+    if (loading) {
+      inputSection = (
+        <StatusText>
+          <Loading />
+        </StatusText>
+      );
+    } else if (tokenError) {
+      inputSection = (
+        <StatusText>
+          Link has already been used or has expired. Please
+          <Link href="/password/reset">try again.</Link>
+        </StatusText>
+      );
+    } else if (submitted) {
+      inputSection = (
+        <StatusText>
+          Password changed successfully! Redirecting to login...
+        </StatusText>
+      );
+    }
+
+    return (
+      <StyledLogin>
+        <LoginPanel>
+          <OverflowWrapper>
+            <GradientBg />
+          </OverflowWrapper>
+          <FormWrapper>
+            <Logo src={logo} />
+            <Prompt>Reset Password</Prompt>
+            <DarkMatter />
+            {inputSection}
+            <Helper>
+              Don't have an account?
+              <Link href="/register">Sign up</Link>
+            </Helper>
+          </FormWrapper>
+        </LoginPanel>
+
+        <Footer>
+          © 2021 Porter Technologies Inc. •
+          <Link
+            href="https://docs.getporter.dev/docs/terms-of-service"
+            target="_blank"
+          >
+            Terms & Privacy
+          </Link>
+        </Footer>
+      </StyledLogin>
+    );
+  }
+}
+
+ResetPasswordInit.contextType = Context;
+
+const Footer = styled.div`
+  position: absolute;
+  bottom: 0;
+  left: 0;
+  margin-bottom: 30px;
+  width: 100vw;
+  text-align: center;
+  color: #aaaabb;
+  font-size: 13px;
+  padding-right: 8px;
+  font: Work Sans, sans-serif;
+`;
+
+const DarkMatter = styled.div`
+  margin-top: -10px;
+`;
+
+const Or = styled.div`
+  position: absolute;
+  width: 30px;
+  text-align: center;
+  background: #111114;
+  z-index: 999;
+  left: calc(50% - 15px);
+  margin-top: -1px;
+`;
+
+const OrWrapper = styled.div`
+  display: flex;
+  align-items: center;
+  color: #ffffff44;
+  font-size: 14px;
+  position: relative;
+`;
+
+const IconWrapper = styled.div`
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  padding: 0 10px;
+  height: 100%;
+`;
+
+const Icon = styled.img`
+  height: 18px;
+  margin-right: 20px;
+`;
+
+const OAuthButton = styled.div`
+  width: 200px;
+  height: 30px;
+  display: flex;
+  background: #ffffff;
+  align-items: center;
+  border-radius: 3px;
+  color: #000000;
+  cursor: pointer;
+  user-select: none;
+  font-weight: 500;
+  font-size: 13px;
+  :hover {
+    background: #ffffffdd;
+  }
+`;
+
+const Link = styled.a`
+  margin-left: 5px;
+  color: #819bfd;
+`;
+
+const Helper = styled.div`
+  position: absolute;
+  bottom: 30px;
+  width: 100%;
+  text-align: center;
+  font-size: 13px;
+  font-family: "Work Sans", sans-serif;
+  color: #ffffff44;
+`;
+
+const OverflowWrapper = styled.div`
+  position: absolute;
+  top: 0;
+  left: 0;
+  width: 100%;
+  height: 100%;
+  overflow: hidden;
+  border-radius: 10px;
+`;
+
+const ErrorHelper = styled.div`
+  position: absolute;
+  right: -185px;
+  top: 8px;
+  height: 30px;
+  width: 170px;
+  user-select: none;
+  background: #272731;
+  font-family: "Work Sans", sans-serif;
+  font-size: 12px;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  color: #ff3b62;
+  border-radius: 3px;
+
+  > div {
+    background: #272731;
+    height: 15px;
+    width: 15px;
+    position: absolute;
+    left: -3px;
+    top: 7px;
+    transform: rotate(45deg);
+    z-index: -1;
+  }
+`;
+
+const Line = styled.div`
+  min-height: 3px;
+  width: 100px;
+  z-index: 999;
+  background: #ffffff22;
+  margin: 30px 0px 30px;
+`;
+
+const Button = styled.button`
+  width: 200px;
+  min-height: 30px;
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  font-family: "Work Sans", sans-serif;
+  cursor: pointer;
+  margin-top: 9px;
+  border-radius: 2px;
+  border: 0;
+  background: #819bfd;
+  color: white;
+  font-weight: 500;
+  font-size: 14px;
+`;
+
+const InputWrapper = styled.div`
+  position: relative;
+`;
+
+const Input = styled.input`
+  width: 200px;
+  font-family: "Work Sans", sans-serif;
+  margin: 8px 0px;
+  height: 30px;
+  padding: 8px;
+  background: #ffffff12;
+  color: #ffffff;
+  border: ${(props: { valid?: boolean }) =>
+    props.valid ? "0" : "1px solid #ff3b62"};
+  border-radius: 2px;
+  font-size: 14px;
+`;
+
+const Prompt = styled.div`
+  font-family: "Work Sans", sans-serif;
+  font-weight: 500;
+  font-size: 15px;
+  margin-bottom: 18px;
+`;
+
+const StatusText = styled.div`
+  padding: 18px 30px;
+  font-family: "Work Sans", sans-serif;
+  font-size: 14px;
+  line-height: 160%;
+`;
+
+const Logo = styled.img`
+  width: 140px;
+  margin-top: 50px;
+  margin-bottom: 75px;
+  user-select: none;
+`;
+
+const FormWrapper = styled.div`
+  width: calc(100% - 8px);
+  height: calc(100% - 8px);
+  background: #111114;
+  z-index: 1;
+  border-radius: 10px;
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+`;
+
+const GradientBg = styled.div`
+  background: linear-gradient(#8ce1ff, #a59eff, #fba8ff);
+  width: 180%;
+  height: 180%;
+  position: absolute;
+  top: -40%;
+  left: -40%;
+  animation: flip 6s infinite linear;
+  @keyframes flip {
+    from {
+      transform: rotate(0deg);
+    }
+    to {
+      transform: rotate(360deg);
+    }
+  }
+`;
+
+const LoginPanel = styled.div`
+  width: 330px;
+  height: 470px;
+  background: white;
+  margin-top: -20px;
+  border-radius: 10px;
+  display: flex;
+  justify-content: center;
+  position: relative;
+  align-items: center;
+`;
+
+const StyledLogin = styled.div`
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  width: 100vw;
+  height: 100vh;
+  position: fixed;
+  top: 0;
+  left: 0;
+  background: #111114;
+`;

+ 373 - 0
dashboard/src/main/auth/ResetPasswordInit.tsx

@@ -0,0 +1,373 @@
+import React, { ChangeEvent, Component } from "react";
+import styled from "styled-components";
+import logo from "assets/logo.png";
+
+import api from "shared/api";
+import { emailRegex } from "shared/regex";
+import { Context } from "shared/Context";
+
+type PropsType = {};
+
+type StateType = {
+  email: string;
+  emailError: boolean;
+  submitted: boolean;
+};
+
+export default class ResetPasswordInit extends Component<PropsType, StateType> {
+  state = {
+    email: "",
+    emailError: false,
+    submitted: false
+  };
+
+  handleKeyDown = (e: any) => {
+    e.key === "Enter" ? this.handleResetPasswordInit() : null;
+  };
+
+  componentDidMount() {
+    document.addEventListener("keydown", this.handleKeyDown);
+  }
+
+  componentWillUnmount() {
+    document.removeEventListener("keydown", this.handleKeyDown);
+  }
+
+  renderEmailError = () => {
+    let { emailError } = this.state;
+    if (emailError) {
+      return (
+        <ErrorHelper>
+          <div />
+          Please enter a valid email
+        </ErrorHelper>
+      );
+    }
+  };
+
+  handleResetPasswordInit = (): void => {
+    let { email } = this.state;
+
+    // Check for valid input
+    if (!emailRegex.test(email)) {
+      this.setState({ emailError: true });
+    } else {
+      // Call reset password
+      api
+        .createPasswordReset(
+          "",
+          {
+            email: email
+          },
+          {}
+        )
+        .then(res => {
+          this.setState({ submitted: true });
+        })
+        .catch(err =>
+          this.context.setCurrentError(err.response.data.errors[0])
+        );
+    }
+  };
+
+  render() {
+    let { email, emailError, submitted } = this.state;
+
+    let formSection = (
+      <div>
+        <InputWrapper>
+          <Input
+            type="email"
+            placeholder="Email"
+            value={email}
+            onChange={(e: ChangeEvent<HTMLInputElement>) =>
+              this.setState({
+                email: e.target.value,
+                emailError: false
+              })
+            }
+            valid={!emailError}
+          />
+          {this.renderEmailError()}
+        </InputWrapper>
+        <Button onClick={this.handleResetPasswordInit}>Continue</Button>
+      </div>
+    );
+
+    if (submitted) {
+      formSection = (
+        <StatusText>
+          If we found an account matching {email}, we've sent you password reset
+          instructions. Remember to check your spam folder.
+        </StatusText>
+      );
+    }
+
+    return (
+      <StyledLogin>
+        <LoginPanel>
+          <OverflowWrapper>
+            <GradientBg />
+          </OverflowWrapper>
+          <FormWrapper>
+            <Logo src={logo} />
+            <Prompt>Reset Password</Prompt>
+            <DarkMatter />
+            {formSection}
+            <Helper>
+              Don't have an account?
+              <Link href="/register">Sign up</Link>
+            </Helper>
+          </FormWrapper>
+        </LoginPanel>
+
+        <Footer>
+          © 2021 Porter Technologies Inc. •
+          <Link
+            href="https://docs.getporter.dev/docs/terms-of-service"
+            target="_blank"
+          >
+            Terms & Privacy
+          </Link>
+        </Footer>
+      </StyledLogin>
+    );
+  }
+}
+
+ResetPasswordInit.contextType = Context;
+
+const Footer = styled.div`
+  position: absolute;
+  bottom: 0;
+  left: 0;
+  margin-bottom: 30px;
+  width: 100vw;
+  text-align: center;
+  color: #aaaabb;
+  font-size: 13px;
+  padding-right: 8px;
+  font: Work Sans, sans-serif;
+`;
+
+const DarkMatter = styled.div`
+  margin-top: -10px;
+`;
+
+const Or = styled.div`
+  position: absolute;
+  width: 30px;
+  text-align: center;
+  background: #111114;
+  z-index: 999;
+  left: calc(50% - 15px);
+  margin-top: -1px;
+`;
+
+const OrWrapper = styled.div`
+  display: flex;
+  align-items: center;
+  color: #ffffff44;
+  font-size: 14px;
+  position: relative;
+`;
+
+const IconWrapper = styled.div`
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  padding: 0 10px;
+  height: 100%;
+`;
+
+const Icon = styled.img`
+  height: 18px;
+  margin-right: 20px;
+`;
+
+const OAuthButton = styled.div`
+  width: 200px;
+  height: 30px;
+  display: flex;
+  background: #ffffff;
+  align-items: center;
+  border-radius: 3px;
+  color: #000000;
+  cursor: pointer;
+  user-select: none;
+  font-weight: 500;
+  font-size: 13px;
+  :hover {
+    background: #ffffffdd;
+  }
+`;
+
+const Link = styled.a`
+  margin-left: 5px;
+  color: #819bfd;
+`;
+
+const Helper = styled.div`
+  position: absolute;
+  bottom: 30px;
+  width: 100%;
+  text-align: center;
+  font-size: 13px;
+  font-family: "Work Sans", sans-serif;
+  color: #ffffff44;
+`;
+
+const OverflowWrapper = styled.div`
+  position: absolute;
+  top: 0;
+  left: 0;
+  width: 100%;
+  height: 100%;
+  overflow: hidden;
+  border-radius: 10px;
+`;
+
+const ErrorHelper = styled.div`
+  position: absolute;
+  right: -185px;
+  top: 8px;
+  height: 30px;
+  width: 170px;
+  user-select: none;
+  background: #272731;
+  font-family: "Work Sans", sans-serif;
+  font-size: 12px;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  color: #ff3b62;
+  border-radius: 3px;
+
+  > div {
+    background: #272731;
+    height: 15px;
+    width: 15px;
+    position: absolute;
+    left: -3px;
+    top: 7px;
+    transform: rotate(45deg);
+    z-index: -1;
+  }
+`;
+
+const Line = styled.div`
+  min-height: 3px;
+  width: 100px;
+  z-index: 999;
+  background: #ffffff22;
+  margin: 30px 0px 30px;
+`;
+
+const Button = styled.button`
+  width: 200px;
+  min-height: 30px;
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  font-family: "Work Sans", sans-serif;
+  cursor: pointer;
+  margin-top: 9px;
+  border-radius: 2px;
+  border: 0;
+  background: #819bfd;
+  color: white;
+  font-weight: 500;
+  font-size: 14px;
+`;
+
+const InputWrapper = styled.div`
+  position: relative;
+`;
+
+const Input = styled.input`
+  width: 200px;
+  font-family: "Work Sans", sans-serif;
+  margin: 8px 0px;
+  height: 30px;
+  padding: 8px;
+  background: #ffffff12;
+  color: #ffffff;
+  border: ${(props: { valid?: boolean }) =>
+    props.valid ? "0" : "1px solid #ff3b62"};
+  border-radius: 2px;
+  font-size: 14px;
+`;
+
+const Prompt = styled.div`
+  font-family: "Work Sans", sans-serif;
+  font-weight: 500;
+  font-size: 15px;
+  margin-bottom: 18px;
+`;
+
+const Logo = styled.img`
+  width: 140px;
+  margin-top: 50px;
+  margin-bottom: 75px;
+  user-select: none;
+`;
+
+const StatusText = styled.div`
+  padding: 18px 30px;
+  font-family: "Work Sans", sans-serif;
+  font-size: 14px;
+  line-height: 160%;
+`;
+
+const FormWrapper = styled.div`
+  width: calc(100% - 8px);
+  height: calc(100% - 8px);
+  background: #111114;
+  z-index: 1;
+  border-radius: 10px;
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+`;
+
+const GradientBg = styled.div`
+  background: linear-gradient(#8ce1ff, #a59eff, #fba8ff);
+  width: 180%;
+  height: 180%;
+  position: absolute;
+  top: -40%;
+  left: -40%;
+  animation: flip 6s infinite linear;
+  @keyframes flip {
+    from {
+      transform: rotate(0deg);
+    }
+    to {
+      transform: rotate(360deg);
+    }
+  }
+`;
+
+const LoginPanel = styled.div`
+  width: 330px;
+  height: 470px;
+  background: white;
+  margin-top: -20px;
+  border-radius: 10px;
+  display: flex;
+  justify-content: center;
+  position: relative;
+  align-items: center;
+`;
+
+const StyledLogin = styled.div`
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  width: 100vw;
+  height: 100vh;
+  position: fixed;
+  top: 0;
+  left: 0;
+  background: #111114;
+`;

+ 359 - 0
dashboard/src/main/auth/VerifyEmail.tsx

@@ -0,0 +1,359 @@
+import React, { ChangeEvent, Component } from "react";
+import styled from "styled-components";
+import logo from "assets/logo.png";
+
+import api from "shared/api";
+import { emailRegex } from "shared/regex";
+import { Context } from "shared/Context";
+
+type PropsType = {
+  handleLogout: () => void;
+};
+
+type StateType = {
+  submitted: boolean;
+};
+
+export default class VerifyEmail extends Component<PropsType, StateType> {
+  state = {
+    submitted: false
+  };
+
+  handleSendEmail = (): void => {
+    api
+      .createEmailVerification("", {}, {})
+      .then(res => {
+        this.setState({ submitted: true });
+      })
+      .catch(err => this.context.setCurrentError(err.response.data.errors[0]));
+  };
+
+  render() {
+    let { submitted } = this.state;
+
+    let formSection = (
+      <div>
+        <InputWrapper>
+          <StatusText>A verification email will be sent to</StatusText>
+          <Email>{this.context.user?.email}</Email>
+        </InputWrapper>
+        <StatusText>
+          Proceed below to verify your email and finish setting up your profile
+        </StatusText>
+        <Button onClick={this.handleSendEmail}>Send Verification Email</Button>
+      </div>
+    );
+
+    if (submitted) {
+      formSection = (
+        <>
+          <Buffer />
+          <StatusText lessPadding={true}>
+            A verification email was sent to{" "}
+            <White>{this.context.user?.email}</White>
+          </StatusText>
+          <StatusText lessPadding={true}>
+            Check your inbox for a verification email. Don't forget to check
+            your spam folder
+          </StatusText>
+          <StatusText lessPadding={true}>
+            Need help?
+            <Link href="mailto:contact@getporter.dev">Contact us</Link>
+          </StatusText>
+        </>
+      );
+    }
+
+    return (
+      <StyledLogin>
+        <LoginPanel>
+          <OverflowWrapper>
+            <GradientBg />
+          </OverflowWrapper>
+          <FormWrapper>
+            <Logo src={logo} />
+            <Prompt>Verify Your Email</Prompt>
+            <DarkMatter />
+            {formSection}
+            <Helper>
+              Want to use a different email?
+              <Link onClick={this.props.handleLogout}>Log out</Link>
+            </Helper>
+          </FormWrapper>
+        </LoginPanel>
+
+        <Footer>
+          © 2021 Porter Technologies Inc. •
+          <Link
+            href="https://docs.getporter.dev/docs/terms-of-service"
+            target="_blank"
+          >
+            Terms & Privacy
+          </Link>
+        </Footer>
+      </StyledLogin>
+    );
+  }
+}
+
+VerifyEmail.contextType = Context;
+
+const Buffer = styled.div`
+  width: 100%;
+  height: 20px;
+`;
+
+const White = styled.div`
+  color: white;
+`;
+
+const Email = styled.div`
+  background: #ffffff11;
+  border: 1px solid #ffffff44;
+  border-radius: 3px;
+  font-size: 14px;
+  color: #aaaabb;
+  height: 30px;
+  margin: 0 60px;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+`;
+
+const Footer = styled.div`
+  position: absolute;
+  bottom: 0;
+  left: 0;
+  margin-bottom: 30px;
+  width: 100vw;
+  text-align: center;
+  color: #aaaabb;
+  font-size: 13px;
+  padding-right: 8px;
+  font: Work Sans, sans-serif;
+`;
+
+const DarkMatter = styled.div`
+  margin-top: -20px;
+`;
+
+const Or = styled.div`
+  position: absolute;
+  width: 30px;
+  text-align: center;
+  background: #111114;
+  z-index: 999;
+  left: calc(50% - 15px);
+  margin-top: -1px;
+`;
+
+const OrWrapper = styled.div`
+  display: flex;
+  align-items: center;
+  color: #ffffff44;
+  font-size: 14px;
+  position: relative;
+`;
+
+const IconWrapper = styled.div`
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  padding: 0 10px;
+  height: 100%;
+`;
+
+const Icon = styled.img`
+  height: 18px;
+  margin-right: 20px;
+`;
+
+const OAuthButton = styled.div`
+  width: 200px;
+  height: 30px;
+  display: flex;
+  background: #ffffff;
+  align-items: center;
+  border-radius: 3px;
+  color: #000000;
+  cursor: pointer;
+  user-select: none;
+  font-weight: 500;
+  font-size: 13px;
+  :hover {
+    background: #ffffffdd;
+  }
+`;
+
+const Link = styled.a`
+  margin-left: 5px;
+  color: #819bfd;
+  cursor: pointer;
+`;
+
+const Helper = styled.div`
+  position: absolute;
+  bottom: 30px;
+  width: 100%;
+  text-align: center;
+  font-size: 13px;
+  font-family: "Work Sans", sans-serif;
+  color: #ffffff44;
+`;
+
+const OverflowWrapper = styled.div`
+  position: absolute;
+  top: 0;
+  left: 0;
+  width: 100%;
+  height: 100%;
+  overflow: hidden;
+  border-radius: 10px;
+`;
+
+const ErrorHelper = styled.div`
+  position: absolute;
+  right: -185px;
+  top: 8px;
+  height: 30px;
+  width: 170px;
+  user-select: none;
+  background: #272731;
+  font-family: "Work Sans", sans-serif;
+  font-size: 12px;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  color: #ff3b62;
+  border-radius: 3px;
+
+  > div {
+    background: #272731;
+    height: 15px;
+    width: 15px;
+    position: absolute;
+    left: -3px;
+    top: 7px;
+    transform: rotate(45deg);
+    z-index: -1;
+  }
+`;
+
+const Line = styled.div`
+  min-height: 3px;
+  width: 100px;
+  z-index: 999;
+  background: #ffffff22;
+  margin: 30px 0px 30px;
+`;
+
+const Button = styled.button`
+  width: 200px;
+  min-height: 30px;
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  font-family: "Work Sans", sans-serif;
+  cursor: pointer;
+  margin: 9px auto;
+  border-radius: 2px;
+  border: 0;
+  background: #819bfd;
+  color: white;
+  font-weight: 500;
+  font-size: 14px;
+`;
+
+const InputWrapper = styled.div`
+  position: relative;
+`;
+
+const Input = styled.input`
+  width: 200px;
+  font-family: "Work Sans", sans-serif;
+  margin: 8px 0px;
+  height: 30px;
+  padding: 8px;
+  background: #ffffff12;
+  color: #ffffff;
+  border: ${(props: { valid?: boolean }) =>
+    props.valid ? "0" : "1px solid #ff3b62"};
+  border-radius: 2px;
+  font-size: 14px;
+`;
+
+const Prompt = styled.div`
+  font-family: "Work Sans", sans-serif;
+  font-weight: 500;
+  font-size: 15px;
+  margin-bottom: 18px;
+`;
+
+const Logo = styled.img`
+  width: 140px;
+  margin-top: 50px;
+  margin-bottom: 60px;
+  user-select: none;
+`;
+
+const StatusText = styled.div<{ lessPadding?: boolean }>`
+  padding: ${props => (props.lessPadding ? "10px" : "18px")} 40px;
+  font-family: "Work Sans", sans-serif;
+  font-size: 14px;
+  line-height: 160%;
+  color: #aaaabb;
+  text-align: center;
+`;
+
+const FormWrapper = styled.div`
+  width: calc(100% - 8px);
+  height: calc(100% - 8px);
+  background: #111114;
+  z-index: 1;
+  border-radius: 10px;
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+`;
+
+const GradientBg = styled.div`
+  background: linear-gradient(#8ce1ff, #a59eff, #fba8ff);
+  width: 180%;
+  height: 180%;
+  position: absolute;
+  top: -40%;
+  left: -40%;
+  animation: flip 6s infinite linear;
+  @keyframes flip {
+    from {
+      transform: rotate(0deg);
+    }
+    to {
+      transform: rotate(360deg);
+    }
+  }
+`;
+
+const LoginPanel = styled.div`
+  width: 330px;
+  height: 470px;
+  background: white;
+  margin-top: -20px;
+  border-radius: 10px;
+  display: flex;
+  justify-content: center;
+  position: relative;
+  align-items: center;
+`;
+
+const StyledLogin = styled.div`
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  width: 100vw;
+  height: 100vh;
+  position: fixed;
+  top: 0;
+  left: 0;
+  background: #111114;
+`;

+ 15 - 23
dashboard/src/main/home/Home.tsx

@@ -50,7 +50,7 @@ class Home extends Component<PropsType, StateType> {
     forceRefreshClusters: false,
     sidebarReady: false,
     handleDO: false,
-    ghRedirect: false,
+    ghRedirect: false
   };
 
   // TODO: Refactor and prevent flash + multiple reload
@@ -64,10 +64,10 @@ class Home extends Component<PropsType, StateType> {
         "<token>",
         {},
         {
-          project_id: currentProject.id,
+          project_id: currentProject.id
         }
       )
-      .then((res) => {
+      .then(res => {
         let creating = false;
 
         for (var i = 0; i < res.data.length; i++) {
@@ -87,7 +87,7 @@ class Home extends Component<PropsType, StateType> {
     let { currentProject } = this.props;
     api
       .getProjects("<token>", {}, { id: user.userId })
-      .then((res) => {
+      .then(res => {
         if (res.data) {
           if (res.data.length === 0) {
             this.props.history.push("new-project");
@@ -134,10 +134,10 @@ class Home extends Component<PropsType, StateType> {
       {
         do_integration_id: integrationId,
         docr_name: this.props.currentProject.name,
-        docr_subscription_tier: tier,
+        docr_subscription_tier: tier
       },
       {
-        project_id: this.props.currentProject.id,
+        project_id: this.props.currentProject.id
       }
     );
     return callback();
@@ -150,10 +150,10 @@ class Home extends Component<PropsType, StateType> {
       {
         do_integration_id: integrationId,
         doks_name: this.props.currentProject.name,
-        do_region: region,
+        do_region: region
       },
       {
-        project_id: this.props.currentProject.id,
+        project_id: this.props.currentProject.id
       }
     );
     return this.props.history.push("dashboard?tab=provisioner");
@@ -167,10 +167,10 @@ class Home extends Component<PropsType, StateType> {
           "<token>",
           {},
           {
-            project_id: currentProject.id,
+            project_id: currentProject.id
           }
         )
-        .then((res) => {
+        .then(res => {
           let tgtIntegration = res.data.find((integration: any) => {
             return integration.client === "do";
           });
@@ -197,18 +197,10 @@ class Home extends Component<PropsType, StateType> {
   };
 
   componentDidMount() {
-    let { user, projects } = this.context;
-
     // Handle redirect from DO
     let queryString = window.location.search;
     let urlParams = new URLSearchParams(queryString);
 
-    window.analytics.identify(user.userId, {
-      email: user.email,
-      createdAt: Date.now(),
-      projects,
-    });
-
     let err = urlParams.get("error");
     if (err) {
       this.context.setCurrentError(err);
@@ -338,7 +330,7 @@ class Home extends Component<PropsType, StateType> {
     let { user, setProjects } = this.context;
     api
       .getProjects("<token>", {}, { id: user.userId })
-      .then((res) => {
+      .then(res => {
         if (res.data) {
           setProjects(res.data);
           if (res.data.length > 0) {
@@ -364,7 +356,7 @@ class Home extends Component<PropsType, StateType> {
     // Loop through and delete infra of all clusters we've provisioned
     api
       .getClusters("<token>", {}, { id: currentProject.id })
-      .then((res) => {
+      .then(res => {
         // TODO: promise.map
         for (var i = 0; i < res.data.length; i++) {
           let cluster = res.data[i];
@@ -379,7 +371,7 @@ class Home extends Component<PropsType, StateType> {
                   { eks_name: cluster.name },
                   {
                     project_id: currentProject.id,
-                    infra_id: cluster.infra_id,
+                    infra_id: cluster.infra_id
                   }
                 )
                 .then(() =>
@@ -395,7 +387,7 @@ class Home extends Component<PropsType, StateType> {
                   { gke_name: cluster.name },
                   {
                     project_id: currentProject.id,
-                    infra_id: cluster.infra_id,
+                    infra_id: cluster.infra_id
                   }
                 )
                 .then(() =>
@@ -411,7 +403,7 @@ class Home extends Component<PropsType, StateType> {
                   { doks_name: cluster.name },
                   {
                     project_id: currentProject.id,
-                    infra_id: cluster.infra_id,
+                    infra_id: cluster.infra_id
                   }
                 )
                 .then(() =>

+ 7 - 7
dashboard/src/main/home/cluster-dashboard/ClusterDashboard.tsx

@@ -32,7 +32,7 @@ class ClusterDashboard extends Component<PropsType, StateType> {
       ? localStorage.getItem("SortType")
       : "Newest",
     currentChart: null as ChartType | null,
-    isMetricsInstalled: false,
+    isMetricsInstalled: false
   };
 
   componentDidMount() {
@@ -40,13 +40,13 @@ class ClusterDashboard extends Component<PropsType, StateType> {
       .getPrometheusIsInstalled(
         "<token>",
         {
-          cluster_id: this.context.currentCluster.id,
+          cluster_id: this.context.currentCluster.id
         },
         {
-          id: this.context.currentProject.id,
+          id: this.context.currentProject.id
         }
       )
-      .then((res) => {
+      .then(res => {
         this.setState({ isMetricsInstalled: true });
       })
       .catch(() => {
@@ -63,7 +63,7 @@ class ClusterDashboard extends Component<PropsType, StateType> {
         sortType: localStorage.getItem("SortType")
           ? localStorage.getItem("SortType")
           : "Newest",
-        currentChart: null,
+        currentChart: null
       });
     }
   }
@@ -138,11 +138,11 @@ class ClusterDashboard extends Component<PropsType, StateType> {
           </Button>
           <SortFilterWrapper>
             <SortSelector
-              setSortType={(sortType) => this.setState({ sortType })}
+              setSortType={sortType => this.setState({ sortType })}
               sortType={this.state.sortType}
             />
             <NamespaceSelector
-              setNamespace={(namespace) => this.setState({ namespace })}
+              setNamespace={namespace => this.setState({ namespace })}
               namespace={this.state.namespace}
             />
           </SortFilterWrapper>

+ 7 - 7
dashboard/src/main/home/cluster-dashboard/NamespaceSelector.tsx

@@ -20,7 +20,7 @@ export default class NamespaceSelector extends Component<PropsType, StateType> {
   _isMounted = false;
 
   state = {
-    namespaceOptions: [] as { label: string; value: string }[],
+    namespaceOptions: [] as { label: string; value: string }[]
   };
 
   updateOptions = () => {
@@ -30,27 +30,27 @@ export default class NamespaceSelector extends Component<PropsType, StateType> {
       .getNamespaces(
         "<token>",
         {
-          cluster_id: currentCluster.id,
+          cluster_id: currentCluster.id
         },
         { id: currentProject.id }
       )
-      .then((res) => {
+      .then(res => {
         if (this._isMounted) {
           let namespaceOptions: { label: string; value: string }[] = [
-            { label: "All", value: "" },
+            { label: "All", value: "" }
           ];
           res.data.items.forEach(
             (x: { metadata: { name: string } }, i: number) => {
               namespaceOptions.push({
                 label: x.metadata.name,
-                value: x.metadata.name,
+                value: x.metadata.name
               });
             }
           );
           this.setState({ namespaceOptions });
         }
       })
-      .catch((err) => {
+      .catch(err => {
         if (this._isMounted) {
           this.setState({ namespaceOptions: [{ label: "All", value: "" }] });
         }
@@ -80,7 +80,7 @@ export default class NamespaceSelector extends Component<PropsType, StateType> {
         </Label>
         <Selector
           activeValue={this.props.namespace}
-          setActiveValue={(namespace) => this.props.setNamespace(namespace)}
+          setActiveValue={namespace => this.props.setNamespace(namespace)}
           options={this.state.namespaceOptions}
           dropdownLabel="Namespace"
           width="150px"

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

@@ -20,8 +20,8 @@ export default class SortSelector extends Component<PropsType, StateType> {
     sortOptions: [
       { label: "Newest", value: "Newest" },
       { label: "Oldest", value: "Oldest" },
-      { label: "Alphabetical", value: "Alphabetical" },
-    ] as { label: string; value: string }[],
+      { label: "Alphabetical", value: "Alphabetical" }
+    ] as { label: string; value: string }[]
   };
 
   render() {
@@ -32,7 +32,7 @@ export default class SortSelector extends Component<PropsType, StateType> {
         </Label>
         <Selector
           activeValue={this.props.sortType}
-          setActiveValue={(sortType) => this.props.setSortType(sortType)}
+          setActiveValue={sortType => this.props.setSortType(sortType)}
           options={this.state.sortOptions}
           dropdownLabel="Sort By"
           width="150px"

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

@@ -19,7 +19,7 @@ type StateType = {
 export default class Chart extends Component<PropsType, StateType> {
   state = {
     expand: false,
-    update: [] as any[],
+    update: [] as any[]
   };
 
   renderIcon = () => {
@@ -37,7 +37,7 @@ export default class Chart extends Component<PropsType, StateType> {
     let date = ts.toLocaleDateString();
     let time = ts.toLocaleTimeString([], {
       hour: "numeric",
-      minute: "2-digit",
+      minute: "2-digit"
     });
     return `${time} on ${date}`;
   };

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

@@ -31,7 +31,7 @@ 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>
   };
 
   // TODO: promisify
@@ -56,12 +56,12 @@ export default class ChartList extends Component<PropsType, StateType> {
             "pending_upgrade",
             "pending_rollback",
             "superseded",
-            "failed",
-          ],
+            "failed"
+          ]
         },
         { id: currentProject.id }
       )
-      .then((res) => {
+      .then(res => {
         let charts = res.data || [];
         if (this.props.sortType == "Newest") {
           charts.sort((a: any, b: any) =>
@@ -83,7 +83,7 @@ export default class ChartList extends Component<PropsType, StateType> {
         });
         callback(charts);
       })
-      .catch((err) => {
+      .catch(err => {
         console.log(err);
         setCurrentError(JSON.stringify(err));
         this.setState({ loading: false, error: true });
@@ -117,8 +117,8 @@ export default class ChartList extends Component<PropsType, StateType> {
       this.setState({
         controllers: {
           ...this.state.controllers,
-          [chartKey]: chartControllers,
-        },
+          [chartKey]: chartControllers
+        }
       });
     };
 
@@ -155,15 +155,15 @@ export default class ChartList extends Component<PropsType, StateType> {
             {
               namespace: chart.namespace,
               cluster_id: currentCluster.id,
-              storage: StorageType.Secret,
+              storage: StorageType.Secret
             },
             {
               id: currentProject.id,
               name: chart.name,
-              revision: chart.version,
+              revision: chart.version
             }
           )
-          .then((res) => {
+          .then(res => {
             // transform controller array into hash table for easy lookup during updates.
             let chartControllers = {} as Record<string, Record<string, any>>;
             res.data.forEach((c: any) => {
@@ -177,12 +177,12 @@ export default class ChartList extends Component<PropsType, StateType> {
                   {
                     chartLookupTable: {
                       ...this.state.chartLookupTable,
-                      [c.metadata.uid]: `${chart.namespace}-${chart.name}`,
+                      [c.metadata.uid]: `${chart.namespace}-${chart.name}`
                     },
                     controllers: {
                       ...this.state.controllers,
-                      [`${chart.namespace}-${chart.name}`]: chartControllers,
-                    },
+                      [`${chart.namespace}-${chart.name}`]: chartControllers
+                    }
                   },
                   () => {
                     nextController();
@@ -192,7 +192,7 @@ export default class ChartList extends Component<PropsType, StateType> {
             });
             next();
           })
-          .catch((err) => {
+          .catch(err => {
             setCurrentError(JSON.stringify(err));
             return;
           });
@@ -206,7 +206,7 @@ export default class ChartList extends Component<PropsType, StateType> {
       "deployment",
       "statefulset",
       "daemonset",
-      "replicaset",
+      "replicaset"
     ]);
   }
 

+ 44 - 48
dashboard/src/main/home/cluster-dashboard/expanded-chart/ExpandedChart.tsx

@@ -8,7 +8,7 @@ import {
   ResourceType,
   ChartType,
   StorageType,
-  ClusterType,
+  ClusterType
 } from "shared/types";
 import { Context } from "shared/Context";
 import api from "shared/api";
@@ -72,7 +72,7 @@ export default class ExpandedChart extends Component<PropsType, StateType> {
     websockets: {} as Record<string, any>,
     url: null as string | null,
     showDeleteOverlay: false,
-    deleting: false,
+    deleting: false
   };
 
   // Retrieve full chart data (includes form and values)
@@ -87,15 +87,15 @@ export default class ExpandedChart extends Component<PropsType, StateType> {
         {
           namespace: currentChart.namespace,
           cluster_id: currentCluster.id,
-          storage: StorageType.Secret,
+          storage: StorageType.Secret
         },
         {
           name: chart.name,
           revision: chart.version,
-          id: currentProject.id,
+          id: currentProject.id
         }
       )
-      .then((res) => {
+      .then(res => {
         setCurrentChart(res.data);
         this.setState({ loading: false });
       })
@@ -116,15 +116,15 @@ export default class ExpandedChart extends Component<PropsType, StateType> {
           {
             namespace: chart.namespace,
             cluster_id: currentCluster.id,
-            storage: StorageType.Secret,
+            storage: StorageType.Secret
           },
           {
             id: currentProject.id,
             name: chart.name,
-            revision: chart.version,
+            revision: chart.version
           }
         )
-        .then((res) => {
+        .then(res => {
           res.data.forEach(async (c: any) => {
             await new Promise((nextController: (res?: any) => void) => {
               c.metadata.kind = c.kind;
@@ -132,8 +132,8 @@ export default class ExpandedChart extends Component<PropsType, StateType> {
                 {
                   controllers: {
                     ...this.state.controllers,
-                    [c.metadata.uid]: c,
-                  },
+                    [c.metadata.uid]: c
+                  }
                 },
                 () => {
                   nextController();
@@ -143,7 +143,7 @@ export default class ExpandedChart extends Component<PropsType, StateType> {
           });
           next();
         })
-        .catch((err) => setCurrentError(JSON.stringify(err)));
+        .catch(err => setCurrentError(JSON.stringify(err)));
     });
   };
 
@@ -167,8 +167,8 @@ export default class ExpandedChart extends Component<PropsType, StateType> {
       this.setState({
         controllers: {
           ...this.state.controllers,
-          [object.metadata.uid]: object,
-        },
+          [object.metadata.uid]: object
+        }
       });
     };
 
@@ -201,18 +201,18 @@ export default class ExpandedChart extends Component<PropsType, StateType> {
         {
           namespace: currentChart.namespace,
           cluster_id: currentCluster.id,
-          storage: StorageType.Secret,
+          storage: StorageType.Secret
         },
         {
           id: currentProject.id,
           name: currentChart.name,
-          revision: currentChart.version,
+          revision: currentChart.version
         }
       )
-      .then((res) => {
+      .then(res => {
         this.setState({
           components: res.data.Objects,
-          podSelectors: res.data.PodSelectors,
+          podSelectors: res.data.PodSelectors
         });
       })
       .catch(console.log);
@@ -233,7 +233,7 @@ export default class ExpandedChart extends Component<PropsType, StateType> {
     // Weave in preexisting values and convert to yaml
     let valuesYaml = yaml.dump({
       ...(this.props.currentChart.config as Object),
-      ...values,
+      ...values
     });
 
     this.setState({ saveValuesStatus: "loading" });
@@ -244,31 +244,31 @@ export default class ExpandedChart extends Component<PropsType, StateType> {
         {
           namespace: this.props.currentChart.namespace,
           storage: StorageType.Secret,
-          values: valuesYaml,
+          values: valuesYaml
         },
         {
           id: currentProject.id,
           name: this.props.currentChart.name,
-          cluster_id: currentCluster.id,
+          cluster_id: currentCluster.id
         }
       )
-      .then((res) => {
+      .then(res => {
         this.setState({
           saveValuesStatus: "successful",
-          forceRefreshRevisions: true,
+          forceRefreshRevisions: true
         });
 
         window.analytics.track("Chart Upgraded", {
           chart: this.props.currentChart.name,
-          values: valuesYaml,
+          values: valuesYaml
         });
       })
-      .catch((err) => {
+      .catch(err => {
         this.setState({ saveValuesStatus: "error" });
         window.analytics.track("Failed to Upgrade Chart", {
           chart: this.props.currentChart.name,
           values: valuesYaml,
-          error: err,
+          error: err
         });
       });
   };
@@ -280,7 +280,7 @@ export default class ExpandedChart extends Component<PropsType, StateType> {
       components,
       showRevisions,
       saveValuesStatus,
-      tabOptions,
+      tabOptions
     } = this.state;
     let { currentChart, setSidebar } = this.props;
     let chart = currentChart;
@@ -289,18 +289,14 @@ export default class ExpandedChart extends Component<PropsType, StateType> {
       case "metrics":
         return <MetricsSection currentChart={chart} />;
       case "status":
-        let activeJobs = Object.values(this.state.controllers)[0]?.status.active;
+        let activeJobs = Object.values(this.state.controllers)[0]?.status
+          .active;
         let selectors = activeJobs?.map((job: any) => {
-          return `job-name=${job.name},controller-uid=${job.uid}`
-        })
+          return `job-name=${job.name},controller-uid=${job.uid}`;
+        });
 
         if (chart.chart.metadata.name == "job") {
-          return (
-            <StatusSection
-              currentChart={chart}
-              selectors={selectors}
-            />
-          );
+          return <StatusSection currentChart={chart} selectors={selectors} />;
         }
         return <StatusSection currentChart={chart} />;
       case "settings":
@@ -378,7 +374,7 @@ export default class ExpandedChart extends Component<PropsType, StateType> {
           value: "@" + tab.name,
           label: tab.label,
           sections: tab.sections,
-          context: tab.context,
+          context: tab.context
         });
       });
     }
@@ -451,7 +447,7 @@ export default class ExpandedChart extends Component<PropsType, StateType> {
     let date = ts.toLocaleDateString();
     let time = ts.toLocaleTimeString([], {
       hour: "numeric",
-      minute: "2-digit",
+      minute: "2-digit"
     });
     return `${time} on ${date}`;
   };
@@ -503,7 +499,7 @@ export default class ExpandedChart extends Component<PropsType, StateType> {
     let { currentChart } = this.props;
 
     window.analytics.track("Opened Chart", {
-      chart: currentChart.name,
+      chart: currentChart.name
     });
 
     this.getChartData(currentChart);
@@ -519,15 +515,15 @@ export default class ExpandedChart extends Component<PropsType, StateType> {
         {
           namespace: currentChart.namespace,
           cluster_id: currentCluster.id,
-          storage: StorageType.Secret,
+          storage: StorageType.Secret
         },
         {
           id: currentProject.id,
           name: currentChart.name,
-          revision: currentChart.version,
+          revision: currentChart.version
         }
       )
-      .then((res) =>
+      .then(res =>
         this.setState({ components: res.data.Objects }, () => {
           let ingressName = null;
           for (var i = 0; i < this.state.components.length; i++) {
@@ -540,25 +536,25 @@ export default class ExpandedChart extends Component<PropsType, StateType> {
             .getIngress(
               "<token>",
               {
-                cluster_id: currentCluster.id,
+                cluster_id: currentCluster.id
               },
               {
                 id: currentProject.id,
                 name: ingressName,
-                namespace: `${this.props.currentChart.namespace}`,
+                namespace: `${this.props.currentChart.namespace}`
               }
             )
-            .then((res) => {
+            .then(res => {
               if (res.data?.spec?.rules && res.data?.spec?.rules[0]?.host) {
                 this.setState({
-                  url: `https://${res.data?.spec?.rules[0]?.host}`,
+                  url: `https://${res.data?.spec?.rules[0]?.host}`
                 });
                 return;
               }
 
               if (res.data?.status?.loadBalancer?.ingress) {
                 this.setState({
-                  url: `http://${res.data?.status?.loadBalancer?.ingress[0]?.hostname}`,
+                  url: `http://${res.data?.status?.loadBalancer?.ingress[0]?.hostname}`
                 });
                 return;
               }
@@ -627,10 +623,10 @@ export default class ExpandedChart extends Component<PropsType, StateType> {
           storage: StorageType.Secret,
           name: currentChart.name,
           id: currentProject.id,
-          cluster_id: currentCluster.id,
+          cluster_id: currentCluster.id
         }
       )
-      .then((res) => {
+      .then(res => {
         this.setState({ showDeleteOverlay: false });
         this.props.setCurrentChart(null);
       })

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

@@ -20,7 +20,7 @@ type StateType = {
 
 export default class GraphSection extends Component<PropsType, StateType> {
   state = {
-    isExpanded: false,
+    isExpanded: false
   };
 
   renderContents = () => {

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

@@ -27,7 +27,7 @@ export default class ListSection extends Component<PropsType, StateType> {
     showKindLabels: true,
     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();
@@ -75,7 +75,7 @@ export default class ListSection extends Component<PropsType, StateType> {
           handleClick={() =>
             this.setState({
               yaml: rawYaml,
-              selectedResource: { kind: resource.Kind, name: resource.Name },
+              selectedResource: { kind: resource.Kind, name: resource.Name }
             })
           }
           selected={this.state.yaml === rawYaml}
@@ -99,7 +99,7 @@ export default class ListSection extends Component<PropsType, StateType> {
     return (
       <StyledListSection>
         {this.renderTabs()}
-        <FlexWrapper ref={(element) => (this.wrapperRef = element)}>
+        <FlexWrapper ref={element => (this.wrapperRef = element)}>
           <YamlWrapper>
             <YamlEditor
               value={this.state.yaml}

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

@@ -32,7 +32,7 @@ export default class RevisionSection extends Component<PropsType, StateType> {
     revisions: [] as ChartType[],
     rollbackRevision: null as number | null,
     loading: false,
-    maxVersion: 0, // Track most recent version even when previewing old revisions
+    maxVersion: 0 // Track most recent version even when previewing old revisions
   };
 
   refreshHistory = () => {
@@ -44,17 +44,17 @@ export default class RevisionSection extends Component<PropsType, StateType> {
         {
           namespace: chart.namespace,
           cluster_id: currentCluster.id,
-          storage: StorageType.Secret,
+          storage: StorageType.Secret
         },
         { id: currentProject.id, name: chart.name }
       )
-      .then((res) => {
+      .then(res => {
         res.data.sort((a: ChartType, b: ChartType) => {
           return -(a.version - b.version);
         });
         this.setState({
           revisions: res.data,
-          maxVersion: res.data[0].version,
+          maxVersion: res.data[0].version
         });
       })
       .catch(console.log);
@@ -83,7 +83,7 @@ export default class RevisionSection extends Component<PropsType, StateType> {
     let date = ts.toLocaleDateString();
     let time = ts.toLocaleTimeString([], {
       hour: "numeric",
-      minute: "2-digit",
+      minute: "2-digit"
     });
     return `${time} on ${date}`;
   };
@@ -100,21 +100,21 @@ export default class RevisionSection extends Component<PropsType, StateType> {
         {
           namespace: this.props.chart.namespace,
           storage: StorageType.Secret,
-          revision: revisionNumber,
+          revision: revisionNumber
         },
         {
           id: currentProject.id,
           name: this.props.chart.name,
-          cluster_id: currentCluster.id,
+          cluster_id: currentCluster.id
         }
       )
-      .then((res) => {
+      .then(res => {
         this.setState({ loading: false });
         this.refreshHistory().then(() => {
           this.props.setRevision(this.state.revisions[0], true);
         });
       })
-      .catch((err) => {
+      .catch(err => {
         console.log(err);
         setCurrentError(err.response.data);
         this.setState({ loading: false });

+ 18 - 19
dashboard/src/main/home/cluster-dashboard/expanded-chart/SettingsSection.tsx

@@ -7,7 +7,7 @@ import {
   ChartType,
   RepoType,
   StorageType,
-  ActionConfigType,
+  ActionConfigType
 } from "shared/types";
 import { Context } from "shared/Context";
 
@@ -47,8 +47,8 @@ export default class SettingsSection extends Component<PropsType, StateType> {
     action: {
       git_repo: "",
       image_repo_uri: "",
-      git_repo_id: 0,
-    } as ActionConfigType,
+      git_repo_id: 0
+    } as ActionConfigType
   };
 
   // TODO: read in set image from form context instead of config
@@ -58,7 +58,7 @@ export default class SettingsSection extends Component<PropsType, StateType> {
     let image = this.props.currentChart.config?.image;
     this.setState({
       selectedImageUrl: image?.repository,
-      selectedTag: image?.tag,
+      selectedTag: image?.tag
     });
 
     api
@@ -67,15 +67,15 @@ export default class SettingsSection extends Component<PropsType, StateType> {
         {
           namespace: this.props.currentChart.namespace,
           cluster_id: currentCluster.id,
-          storage: StorageType.Secret,
+          storage: StorageType.Secret
         },
         { id: currentProject.id, name: this.props.currentChart.name }
       )
-      .then((res) => {
+      .then(res => {
         console.log(res.data);
         this.setState({
           action: res.data.git_action_config,
-          webhookToken: res.data.webhook_token,
+          webhookToken: res.data.webhook_token
         });
       })
       .catch(console.log);
@@ -96,8 +96,8 @@ export default class SettingsSection extends Component<PropsType, StateType> {
     let image = {
       image: {
         repository: img,
-        tag: parsedTag || tag,
-      },
+        tag: parsedTag || tag
+      }
     };
 
     let values = {};
@@ -109,7 +109,7 @@ export default class SettingsSection extends Component<PropsType, StateType> {
     // Weave in preexisting values and convert to yaml
     let valuesYaml = yaml.dump({
       ...values,
-      ...image,
+      ...image
     });
 
     api
@@ -118,19 +118,19 @@ export default class SettingsSection extends Component<PropsType, StateType> {
         {
           namespace: this.props.currentChart.namespace,
           storage: StorageType.Secret,
-          values: valuesYaml,
+          values: valuesYaml
         },
         {
           id: currentProject.id,
           name: this.props.currentChart.name,
-          cluster_id: currentCluster.id,
+          cluster_id: currentCluster.id
         }
       )
-      .then((res) => {
+      .then(res => {
         this.setState({ saveValuesStatus: "successful" });
         this.props.refreshChart();
       })
-      .catch((err) => {
+      .catch(err => {
         console.log(err);
         this.setState({ saveValuesStatus: "error" });
       });
@@ -213,16 +213,15 @@ const Button = styled.button`
   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;
   }
   :hover {
-    filter: ${(props) => (!props.disabled ? "brightness(120%)" : "")};
+    filter: ${props => (!props.disabled ? "brightness(120%)" : "")};
   }
 `;
 

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

@@ -23,7 +23,7 @@ type StateType = {
 export default class ValuesYaml extends Component<PropsType, StateType> {
   state = {
     values: "",
-    saveValuesStatus: null as string | null,
+    saveValuesStatus: null as string | null
   };
 
   updateValues() {
@@ -54,19 +54,19 @@ export default class ValuesYaml extends Component<PropsType, StateType> {
         {
           namespace: this.props.currentChart.namespace,
           storage: StorageType.Secret,
-          values: this.state.values,
+          values: this.state.values
         },
         {
           id: currentProject.id,
           name: this.props.currentChart.name,
-          cluster_id: currentCluster.id,
+          cluster_id: currentCluster.id
         }
       )
-      .then((res) => {
+      .then(res => {
         this.setState({ saveValuesStatus: "successful" });
         this.props.refreshChart();
       })
-      .catch((err) => {
+      .catch(err => {
         console.log(err);
         this.setState({ saveValuesStatus: "error" });
       });

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

@@ -19,7 +19,7 @@ type StateType = {
 export default class StatusSection extends Component<PropsType, StateType> {
   state = {
     events: [] as any[],
-    loading: true,
+    loading: true
   };
 
   renderTabs = () => {

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

@@ -23,7 +23,7 @@ type StateType = {
 
 export default class Edge extends Component<PropsType, StateType> {
   state = {
-    showArrowHead: true,
+    showArrowHead: true
   };
 
   render() {
@@ -81,8 +81,8 @@ const StyledEdge: any = styled.div.attrs((props: any) => ({
     top: props.cy + "px",
     left: props.cx + "px",
     transform: "rotate(" + props.angle + "deg)",
-    width: props.length + "px",
-  },
+    width: props.length + "px"
+  }
 }))`
   position: absolute;
   height: ${thickness}px;

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

@@ -82,7 +82,7 @@ export default class GraphDisplay extends Component<PropsType, StateType> {
     currentEdge: null as EdgeType | null,
     openedNode: null as NodeType | null,
     suppressCloseNode: false,
-    suppressDisplay: false,
+    suppressDisplay: false
   };
 
   spaceRef: any = React.createRef();
@@ -122,7 +122,7 @@ export default class GraphDisplay extends Component<PropsType, StateType> {
     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
@@ -164,7 +164,7 @@ export default class GraphDisplay extends Component<PropsType, StateType> {
             x: this.getRandomIntBetweenRange(-500, 0),
             y: this.getRandomIntBetweenRange(0, 250),
             w: 40,
-            h: 40,
+            h: 40
           };
         case "Deployment":
         case "StatefulSet":
@@ -178,7 +178,7 @@ export default class GraphDisplay extends Component<PropsType, StateType> {
             x: this.getRandomIntBetweenRange(0, 500),
             y: this.getRandomIntBetweenRange(0, 250),
             w: 40,
-            h: 40,
+            h: 40
           };
         case "Service":
         case "Ingress":
@@ -191,7 +191,7 @@ export default class GraphDisplay extends Component<PropsType, StateType> {
             x: this.getRandomIntBetweenRange(0, 500),
             y: this.getRandomIntBetweenRange(-250, 0),
             w: 40,
-            h: 40,
+            h: 40
           };
         default:
           return {
@@ -202,7 +202,7 @@ export default class GraphDisplay extends Component<PropsType, StateType> {
             x: this.getRandomIntBetweenRange(-400, 0),
             y: this.getRandomIntBetweenRange(-250, 0),
             w: 40,
-            h: 40,
+            h: 40
           };
       }
     });
@@ -216,7 +216,7 @@ export default class GraphDisplay extends Component<PropsType, StateType> {
           edges.push({
             type: "ControlRel",
             source: rel.Source,
-            target: rel.Target,
+            target: rel.Target
           });
         }
       });
@@ -225,7 +225,7 @@ export default class GraphDisplay extends Component<PropsType, StateType> {
           edges.push({
             type: "LabelRel",
             source: rel.Source,
-            target: rel.Target,
+            target: rel.Target
           });
         }
       });
@@ -234,7 +234,7 @@ export default class GraphDisplay extends Component<PropsType, StateType> {
           edges.push({
             type: "SpecRel",
             source: rel.Source,
-            target: rel.Target,
+            target: rel.Target
           });
         }
       });
@@ -287,7 +287,7 @@ export default class GraphDisplay extends Component<PropsType, StateType> {
         panX: null,
         panY: null,
         deltaX: null,
-        deltaY: null,
+        deltaY: null
       });
     }
   };
@@ -302,7 +302,7 @@ export default class GraphDisplay extends Component<PropsType, StateType> {
         panX: null,
         panY: null,
         deltaX: null,
-        deltaY: null,
+        deltaY: null
       });
     }
   };
@@ -314,7 +314,7 @@ export default class GraphDisplay extends Component<PropsType, StateType> {
     this.setState({
       nodeClickX: cursorX,
       nodeClickY: cursorY,
-      suppressCloseNode: true,
+      suppressCloseNode: true
     });
 
     // Push to activeIds if not already present
@@ -334,7 +334,7 @@ export default class GraphDisplay extends Component<PropsType, StateType> {
     this.setState({
       activeIds: holding,
       preventBgDrag: true,
-      relocateAllowed: true,
+      relocateAllowed: true
     });
   };
 
@@ -362,7 +362,7 @@ export default class GraphDisplay extends Component<PropsType, StateType> {
       deltaY: null,
       panX: null,
       panY: null,
-      scale: 1,
+      scale: 1
     });
   };
 
@@ -372,7 +372,7 @@ export default class GraphDisplay extends Component<PropsType, StateType> {
       nodeClickX,
       cursorY,
       nodeClickY,
-      suppressCloseNode,
+      suppressCloseNode
     } = this.state;
     this.setState({ dragBg: false, activeIds: [] });
 
@@ -401,7 +401,7 @@ export default class GraphDisplay extends Component<PropsType, StateType> {
       anchorY,
       nodes,
       activeIds,
-      relocateAllowed,
+      relocateAllowed
     } = this.state;
 
     // Suppress navigation gestures
@@ -478,7 +478,7 @@ 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)
       });
     });
   };
@@ -496,7 +496,7 @@ export default class GraphDisplay extends Component<PropsType, StateType> {
       panY,
       anchorX,
       anchorY,
-      relocateAllowed,
+      relocateAllowed
     } = this.state;
 
     let minX = 0;
@@ -606,7 +606,7 @@ export default class GraphDisplay extends Component<PropsType, StateType> {
     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}

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

@@ -23,7 +23,7 @@ type StateType = {
 
 export default class InfoPanel extends Component<PropsType, StateType> {
   state = {
-    wrapperHeight: 0,
+    wrapperHeight: 0
   };
 
   renderIcon = (kind: string) => {
@@ -70,7 +70,7 @@ export default class InfoPanel extends Component<PropsType, StateType> {
             {openedNode.kind}
             <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}

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

@@ -116,8 +116,8 @@ const NodeBlock = styled.div`
 const StyledNode: any = styled.div.attrs((props: NodeType) => ({
   style: {
     top: props.y + "px",
-    left: props.x + "px",
-  },
+    left: props.x + "px"
+  }
 }))`
   position: absolute;
   width: ${(props: NodeType) => props.w + "px"};

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

@@ -50,8 +50,8 @@ const StyledSelectRegion: any = styled.div.attrs(
       top: props.y + "px",
       left: props.x + "px",
       width: props.w + "px",
-      height: props.h + "px",
-    },
+      height: props.h + "px"
+    }
   })
 )`
   position: absolute;

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

@@ -12,7 +12,7 @@ type StateType = {
 
 export default class ZoomPanel extends Component<PropsType, StateType> {
   state = {
-    wrapperHeight: 0,
+    wrapperHeight: 0
   };
 
   wrapperRef: any = React.createRef();

+ 13 - 13
dashboard/src/main/home/cluster-dashboard/expanded-chart/metrics/AreaChart.tsx

@@ -8,7 +8,7 @@ import {
   withTooltip,
   Tooltip,
   TooltipWithBounds,
-  defaultStyles,
+  defaultStyles
 } from "@visx/tooltip";
 
 import { GridRows, GridColumns } from "@visx/grid";
@@ -41,7 +41,7 @@ const tooltipStyles = {
   ...defaultStyles,
   background,
   border: "1px solid white",
-  color: "white",
+  color: "white"
 };
 
 // util
@@ -55,14 +55,14 @@ const formats: { [range: string]: (date: Date) => string } = {
   "1H": hourFormat,
   "6H": hourFormat,
   "1D": hourFormat,
-  "1M": dayFormat,
+  "1M": dayFormat
 };
 
 // accessors
 const getDate = (d: MetricsData) => new Date(d.date * 1000);
 const getValue = (d: MetricsData) => d.value;
 
-const bisectDate = bisector<MetricsData, Date>((d) => new Date(d.date * 1000))
+const bisectDate = bisector<MetricsData, Date>(d => new Date(d.date * 1000))
   .left;
 
 export type AreaProps = {
@@ -84,7 +84,7 @@ export default withTooltip<AreaProps, TooltipData>(
     hideTooltip,
     tooltipData,
     tooltipTop = 0,
-    tooltipLeft = 0,
+    tooltipLeft = 0
   }: AreaProps & WithTooltipProvidedProps<TooltipData>) => {
     globalData = data;
 
@@ -101,7 +101,7 @@ export default withTooltip<AreaProps, TooltipData>(
       () =>
         scaleTime({
           range: [margin.left, innerWidth + margin.left],
-          domain: extent(globalData, getDate) as [Date, Date],
+          domain: extent(globalData, getDate) as [Date, Date]
         }),
       [innerWidth, margin.left, width, height, data]
     );
@@ -110,7 +110,7 @@ export default withTooltip<AreaProps, TooltipData>(
         scaleLinear({
           range: [innerHeight + margin.top, margin.top],
           domain: [0, 1.25 * max(globalData, getValue)],
-          nice: true,
+          nice: true
         }),
       [margin.top, innerHeight, width, height, data]
     );
@@ -140,7 +140,7 @@ export default withTooltip<AreaProps, TooltipData>(
         showTooltip({
           tooltipData: d,
           tooltipLeft: x || 0,
-          tooltipTop: valueScale(getValue(d)) || 0,
+          tooltipTop: valueScale(getValue(d)) || 0
         });
       },
       [showTooltip, valueScale, dateScale, width, height, data]
@@ -188,8 +188,8 @@ export default withTooltip<AreaProps, TooltipData>(
           />
           <AreaClosed<MetricsData>
             data={data}
-            x={(d) => dateScale(getDate(d)) ?? 0}
-            y={(d) => valueScale(getValue(d)) ?? 0}
+            x={d => dateScale(getDate(d)) ?? 0}
+            y={d => valueScale(getValue(d)) ?? 0}
             height={innerHeight}
             yScale={valueScale}
             strokeWidth={1}
@@ -207,7 +207,7 @@ export default withTooltip<AreaProps, TooltipData>(
               fontSize: 11,
               textAnchor: "start",
               fillOpacity: 0.4,
-              dy: 0,
+              dy: 0
             })}
           />
           <AxisBottom
@@ -220,7 +220,7 @@ export default withTooltip<AreaProps, TooltipData>(
               fill: "white",
               fontSize: 11,
               textAnchor: "middle",
-              fillOpacity: 0.4,
+              fillOpacity: 0.4
             })}
           />
           <Bar
@@ -288,7 +288,7 @@ export default withTooltip<AreaProps, TooltipData>(
                 width: 100,
                 paddingTop: 35,
                 textAlign: "center",
-                transform: "translateX(-60px)",
+                transform: "translateX(-60px)"
               }}
             >
               {formatDate(getDate(tooltipData))}

+ 132 - 28
dashboard/src/main/home/cluster-dashboard/expanded-chart/metrics/MetricsSection.tsx

@@ -17,7 +17,9 @@ type PropsType = {
 
 type StateType = {
   controllerOptions: any[];
+  ingressOptions: any[];
   selectedController: any;
+  selectedIngress: any;
   pods: any[];
   selectedPod: string;
   selectedRange: string;
@@ -28,6 +30,7 @@ type StateType = {
   dropdownExpanded: boolean;
   data: MetricsData[];
   showMetricsSettings: boolean;
+  metricsOptions: MetricsOption[];
 };
 
 type MetricsCPUDataResponse = {
@@ -54,18 +57,31 @@ type MetricsNetworkDataResponse = {
   }[];
 }[];
 
+type MetricsNGINXErrorsDataResponse = {
+  pod?: string;
+  results: {
+    date: number;
+    error_pct: string;
+  }[];
+}[];
+
+type MetricsOption = {
+  value: string;
+  label: string;
+};
+
 const resolutions: { [range: string]: string } = {
   "1H": "15s",
   "6H": "15s",
   "1D": "15s",
-  "1M": "5h",
+  "1M": "5h"
 };
 
 const secondsBeforeNow: { [range: string]: number } = {
   "1H": 60 * 60,
   "6H": 60 * 60 * 6,
   "1D": 60 * 60 * 24,
-  "1M": 60 * 60 * 24 * 30,
+  "1M": 60 * 60 * 24 * 30
 };
 
 export default class MetricsSection extends Component<PropsType, StateType> {
@@ -74,6 +90,8 @@ export default class MetricsSection extends Component<PropsType, StateType> {
     selectedPod: "",
     controllerOptions: [] as any[],
     selectedController: null as any,
+    ingressOptions: [] as any[],
+    selectedIngress: null as any,
     selectedRange: "1H",
     selectedMetric: "cpu",
     selectedMetricLabel: "CPU Utilization (vCPUs)",
@@ -82,6 +100,11 @@ export default class MetricsSection extends Component<PropsType, StateType> {
     controllerDropdownExpanded: false,
     data: [] as MetricsData[],
     showMetricsSettings: false,
+    metricsOptions: [
+      { value: "cpu", label: "CPU Utilization (vCPUs)" },
+      { value: "memory", label: "RAM Utilization (Mi)" },
+      { value: "network", label: "Network Received Bytes (Ki)" }
+    ]
   };
 
   componentDidMount() {
@@ -89,21 +112,57 @@ export default class MetricsSection extends Component<PropsType, StateType> {
     let { currentChart } = this.props;
     let { currentCluster, currentProject, setCurrentError } = this.context;
 
+    if (currentChart.chart?.metadata?.name == "ingress-nginx") {
+      api
+        .getNGINXIngresses(
+          "<token>",
+          {
+            cluster_id: currentCluster.id
+          },
+          {
+            id: currentProject.id
+          }
+        )
+        .then(res => {
+          let metricsOptions = this.state.metricsOptions;
+          metricsOptions.push({
+            value: "nginx:errors",
+            label: "5XX Error Percentage"
+          });
+
+          let ingressOptions = [] as any[];
+          res.data.map((ingress: string) => {
+            ingressOptions.push({ value: ingress, label: ingress });
+          });
+
+          // iterate through the controllers to get the list of pods
+          this.setState({
+            metricsOptions,
+            ingressOptions,
+            selectedIngress: ingressOptions[0].value
+          });
+        })
+        .catch(err => {
+          setCurrentError(JSON.stringify(err));
+          this.setState({ controllerOptions: [] as any[] });
+        });
+    }
+
     api
       .getChartControllers(
         "<token>",
         {
           namespace: currentChart.namespace,
           cluster_id: currentCluster.id,
-          storage: StorageType.Secret,
+          storage: StorageType.Secret
         },
         {
           id: currentProject.id,
           name: currentChart.name,
-          revision: currentChart.version,
+          revision: currentChart.version
         }
       )
-      .then((res) => {
+      .then(res => {
         // TODO -- check at least one controller returned
         let controllerOptions = [] as any[];
         res.data.map((controller: any) => {
@@ -114,12 +173,12 @@ export default class MetricsSection extends Component<PropsType, StateType> {
         // iterate through the controllers to get the list of pods
         this.setState({
           controllerOptions,
-          selectedController: controllerOptions[0].value,
+          selectedController: controllerOptions[0].value
         });
 
         this.getPods();
       })
-      .catch((err) => {
+      .catch(err => {
         setCurrentError(JSON.stringify(err));
         this.setState({ controllerOptions: [] as any[] });
       });
@@ -145,6 +204,10 @@ export default class MetricsSection extends Component<PropsType, StateType> {
     ) {
       this.getMetrics();
     }
+
+    if (this.state.selectedIngress != prevState.selectedIngress) {
+      this.getMetrics();
+    }
   }
 
   getMetrics = () => {
@@ -170,6 +233,11 @@ export default class MetricsSection extends Component<PropsType, StateType> {
       pods = [this.state.selectedPod];
     }
 
+    if (this.state.selectedMetric == "nginx:errors") {
+      pods = [this.state.selectedIngress];
+      shouldsum = false;
+    }
+
     api
       .getMetrics(
         "<token>",
@@ -181,13 +249,13 @@ export default class MetricsSection extends Component<PropsType, StateType> {
           namespace: currentChart.namespace,
           startrange: start,
           endrange: end,
-          resolution: resolutions[this.state.selectedRange],
+          resolution: resolutions[this.state.selectedRange]
         },
         {
-          id: currentProject.id,
+          id: currentProject.id
         }
       )
-      .then((res) => {
+      .then(res => {
         // transform the metrics to expected form
         if (kind == "cpu") {
           let data = res.data as MetricsCPUDataResponse;
@@ -203,7 +271,7 @@ export default class MetricsSection extends Component<PropsType, StateType> {
             ) => {
               return {
                 date: d.date,
-                value: parseFloat(d.cpu),
+                value: parseFloat(d.cpu)
               };
             }
           );
@@ -222,7 +290,7 @@ export default class MetricsSection extends Component<PropsType, StateType> {
             ) => {
               return {
                 date: d.date,
-                value: parseFloat(d.memory) / (1024 * 1024), // put units in Mi
+                value: parseFloat(d.memory) / (1024 * 1024) // put units in Mi
               };
             }
           );
@@ -241,7 +309,26 @@ export default class MetricsSection extends Component<PropsType, StateType> {
             ) => {
               return {
                 date: d.date,
-                value: parseFloat(d.bytes) / 1024, // put units in Ki
+                value: parseFloat(d.bytes) / 1024 // put units in Ki
+              };
+            }
+          );
+
+          this.setState({ data: tData });
+        } else if (kind == "nginx:errors") {
+          let data = res.data as MetricsNGINXErrorsDataResponse;
+
+          let tData = data[0].results.map(
+            (
+              d: {
+                date: number;
+                error_pct: string;
+              },
+              i: number
+            ) => {
+              return {
+                date: d.date,
+                value: parseFloat(d.error_pct) // put units in Ki
               };
             }
           );
@@ -249,7 +336,7 @@ export default class MetricsSection extends Component<PropsType, StateType> {
           this.setState({ data: tData });
         }
       })
-      .catch((err) => {
+      .catch(err => {
         setCurrentError(JSON.stringify(err));
         // this.setState({ controllers: [], loading: false });
       });
@@ -279,13 +366,13 @@ export default class MetricsSection extends Component<PropsType, StateType> {
         "<token>",
         {
           cluster_id: currentCluster.id,
-          selectors,
+          selectors
         },
         {
-          id: currentProject.id,
+          id: currentProject.id
         }
       )
-      .then((res) => {
+      .then(res => {
         let pods = [{ value: "All", label: "All (Summed)" }] as any[];
         res?.data?.forEach((pod: any) => {
           let name = pod?.metadata?.name;
@@ -296,7 +383,7 @@ export default class MetricsSection extends Component<PropsType, StateType> {
 
         this.getMetrics();
       })
-      .catch((err) => {
+      .catch(err => {
         console.log(err);
         setCurrentError(JSON.stringify(err));
         return;
@@ -323,12 +410,7 @@ export default class MetricsSection extends Component<PropsType, StateType> {
   };
 
   renderOptionList = () => {
-    let metricOptions = [
-      { value: "cpu", label: "CPU Utilization (vCPUs)" },
-      { value: "memory", label: "RAM Utilization (Mi)" },
-      { value: "network", label: "Network Received Bytes (Ki)" },
-    ];
-    return metricOptions.map(
+    return this.state.metricsOptions.map(
       (option: { value: string; label: string }, i: number) => {
         return (
           <Option
@@ -337,10 +419,10 @@ export default class MetricsSection extends Component<PropsType, StateType> {
             onClick={() =>
               this.setState({
                 selectedMetric: option.value,
-                selectedMetricLabel: option.label,
+                selectedMetricLabel: option.label
               })
             }
-            lastItem={i === metricOptions.length - 1}
+            lastItem={i === this.state.metricsOptions.length - 1}
           >
             {option.label}
           </Option>
@@ -351,6 +433,28 @@ export default class MetricsSection extends Component<PropsType, StateType> {
 
   renderMetricsSettings = () => {
     if (this.state.showMetricsSettings && true) {
+      if (this.state.selectedMetric == "nginx:errors") {
+        return (
+          <>
+            <DropdownOverlay
+              onClick={() => this.setState({ showMetricsSettings: false })}
+            />
+            <DropdownAlt dropdownWidth="330px" dropdownMaxHeight="300px">
+              <Label>Additional Settings</Label>
+              <SelectRow
+                label="Target Ingress"
+                value={this.state.selectedIngress}
+                setActiveValue={(x: any) =>
+                  this.setState({ selectedIngress: x })
+                }
+                options={this.state.ingressOptions}
+                width="100%"
+              />
+            </DropdownAlt>
+          </>
+        );
+      }
+
       return (
         <>
           <DropdownOverlay
@@ -388,7 +492,7 @@ export default class MetricsSection extends Component<PropsType, StateType> {
             <MetricSelector
               onClick={() =>
                 this.setState({
-                  dropdownExpanded: !this.state.dropdownExpanded,
+                  dropdownExpanded: !this.state.dropdownExpanded
                 })
               }
             >
@@ -412,7 +516,7 @@ export default class MetricsSection extends Component<PropsType, StateType> {
                 { value: "1H", label: "1H" },
                 { value: "6H", label: "6H" },
                 { value: "1D", label: "1D" },
-                { value: "1M", label: "1M" },
+                { value: "1M", label: "1M" }
               ]}
               currentTab={this.state.selectedRange}
               setCurrentTab={(x: string) => this.setState({ selectedRange: x })}

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

@@ -26,7 +26,7 @@ export default class ControllerTab extends Component<PropsType, StateType> {
   state = {
     pods: [] as any[],
     raw: [] as any[],
-    showTooltip: [] as boolean[],
+    showTooltip: [] as boolean[]
   };
 
   componentDidMount() {
@@ -56,18 +56,18 @@ export default class ControllerTab extends Component<PropsType, StateType> {
         "<token>",
         {
           cluster_id: currentCluster.id,
-          selectors,
+          selectors
         },
         {
-          id: currentProject.id,
+          id: currentProject.id
         }
       )
-      .then((res) => {
+      .then(res => {
         let pods = res?.data?.map((pod: any) => {
           return {
             namespace: pod?.metadata?.namespace,
             name: pod?.metadata?.name,
-            phase: pod?.status?.phase,
+            phase: pod?.status?.phase
           };
         });
         let showTooltip = new Array(pods.length);
@@ -86,7 +86,7 @@ export default class ControllerTab extends Component<PropsType, StateType> {
           selectPod(res.data[0]);
         }
       })
-      .catch((err) => {
+      .catch(err => {
         console.log(err);
         setCurrentError(JSON.stringify(err));
         return;
@@ -101,18 +101,18 @@ export default class ControllerTab extends Component<PropsType, StateType> {
           c.status?.availableReplicas ||
             c.status?.replicas - c.status?.unavailableReplicas ||
             0,
-          c.status?.replicas || 0,
+          c.status?.replicas || 0
         ];
       case "statefulset":
         return [c.status?.readyReplicas || 0, c.status?.replicas || 0];
       case "daemonset":
         return [
           c.status?.numberAvailable || 0,
-          c.status?.desiredNumberScheduled || 0,
+          c.status?.desiredNumberScheduled || 0
         ];
       case "job":
-        console.log(c)
-        return [1, 1]
+        console.log(c);
+        return [1, 1];
     }
   };
 
@@ -156,7 +156,7 @@ export default class ControllerTab extends Component<PropsType, StateType> {
     let status = available == total ? "running" : "waiting";
 
     if (controller.kind.toLowerCase() === "job" && this.state.raw.length == 0) {
-      status = "completed" 
+      status = "completed";
     }
 
     return (

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

@@ -17,7 +17,7 @@ export default class Logs extends Component<PropsType, StateType> {
   state = {
     logs: [] as string[],
     ws: null as any,
-    scroll: true,
+    scroll: true
   };
 
   ws = null as any;
@@ -26,11 +26,11 @@ export default class Logs extends Component<PropsType, StateType> {
   scrollToBottom = (smooth: boolean) => {
     if (smooth) {
       this.parentRef.current.lastElementChild.scrollIntoView({
-        behavior: "smooth",
+        behavior: "smooth"
       });
     } else {
       this.parentRef.current.lastElementChild.scrollIntoView({
-        behavior: "auto",
+        behavior: "auto"
       });
     }
   };

+ 11 - 8
dashboard/src/main/home/cluster-dashboard/expanded-chart/status/StatusSection.tsx

@@ -30,7 +30,7 @@ export default class StatusSection extends Component<PropsType, StateType> {
     selectedPod: {} as any,
     controllers: [] as any[],
     loading: true,
-    podError: "",
+    podError: ""
   };
 
   renderLogs = () => {
@@ -45,7 +45,7 @@ export default class StatusSection extends Component<PropsType, StateType> {
 
   selectPod = (pod: any) => {
     this.setState({
-      selectedPod: pod,
+      selectedPod: pod
     });
   };
 
@@ -112,19 +112,22 @@ export default class StatusSection extends Component<PropsType, StateType> {
         {
           namespace: currentChart.namespace,
           cluster_id: currentCluster.id,
-          storage: StorageType.Secret,
+          storage: StorageType.Secret
         },
         {
           id: currentProject.id,
           name: currentChart.name,
-          revision: currentChart.version,
+          revision: currentChart.version
         }
       )
-      .then((res : any) => {
-        let controllers = currentChart.chart.metadata.name == "job" ? res.data[0]?.status.active : res.data
-        this.setState({controllers, loading: false})
+      .then((res: any) => {
+        let controllers =
+          currentChart.chart.metadata.name == "job"
+            ? res.data[0]?.status.active
+            : res.data;
+        this.setState({ controllers, loading: false });
       })
-      .catch((err) => {
+      .catch(err => {
         setCurrentError(JSON.stringify(err));
         this.setState({ controllers: [], loading: false });
       });

+ 16 - 4
dashboard/src/main/home/dashboard/ClusterList.tsx

@@ -9,7 +9,9 @@ import Helper from "components/values-form/Helper";
 import Loading from "components/Loading";
 import { RouteComponentProps, withRouter } from "react-router";
 
-type PropsType = RouteComponentProps;
+type PropsType = RouteComponentProps & {
+  currentCluster: ClusterType;
+};
 
 type StateType = {
   loading: boolean;
@@ -21,20 +23,30 @@ class Templates extends Component<PropsType, StateType> {
   state = {
     loading: true,
     error: "",
-    clusters: [] as ClusterType[],
+    clusters: [] as ClusterType[]
   };
 
   componentDidMount() {
+    this.updateClusterList()
+  }
+
+  componentDidUpdate(prevProps : PropsType) {
+    if (prevProps.currentCluster?.name != this.props.currentCluster?.name) {
+      this.updateClusterList()
+    }
+  }
+
+  updateClusterList = () => {
     api
       .getClusters("<token>", {}, { id: this.context.currentProject.id })
-      .then((res) => {
+      .then(res => {
         if (res.data) {
           this.setState({ clusters: res.data, loading: false, error: "" });
         } else {
           this.setState({ loading: false, error: "Response data missing" });
         }
       })
-      .catch((err) => this.setState(err));
+      .catch(err => this.setState(err));
   }
 
   renderIcon = () => {

+ 2 - 2
dashboard/src/main/home/dashboard/ClusterPlaceholder.tsx

@@ -20,7 +20,7 @@ export default class ClusterPlaceholder extends Component<
   StateType
 > {
   state = {
-    loading: true,
+    loading: true
   };
 
   componentDidMount() {
@@ -61,7 +61,7 @@ export default class ClusterPlaceholder extends Component<
         </>
       );
     } else {
-      return <ClusterList />;
+      return <ClusterList currentCluster={this.props.currentCluster}/>;
     }
   }
 }

+ 4 - 4
dashboard/src/main/home/dashboard/Dashboard.tsx

@@ -20,7 +20,7 @@ type PropsType = RouteComponentProps & {
 
 const tabOptions = [
   { label: "Project Overview", value: "overview" },
-  { label: "Provisioner Status", value: "provisioner" },
+  { label: "Provisioner Status", value: "provisioner" }
 ];
 // TODO: rethink this list, should be coupled with tabOptions
 const tabOptionStrings = ["overview", "provisioner"];
@@ -31,7 +31,7 @@ type StateType = {
 
 class Dashboard extends Component<PropsType, StateType> {
   state = {
-    infras: [] as InfraType[],
+    infras: [] as InfraType[]
   };
 
   refreshInfras = () => {
@@ -41,10 +41,10 @@ class Dashboard extends Component<PropsType, StateType> {
           "<token>",
           {},
           {
-            project_id: this.props.projectId,
+            project_id: this.props.projectId
           }
         )
-        .then((res) => this.setState({ infras: res.data }))
+        .then(res => this.setState({ infras: res.data }))
         .catch(console.log);
     }
   };

+ 13 - 7
dashboard/src/main/home/integrations/IntegrationCategories.tsx

@@ -25,7 +25,7 @@ class IntegrationCategories extends Component<PropsType, StateType> {
     currentOptions: [] as any[],
     currentTitles: [] as any[],
     currentIds: [] as any[],
-    currentIntegrationData: [] as any[],
+    currentIntegrationData: [] as any[]
   };
 
   componentDidMount() {
@@ -43,7 +43,7 @@ class IntegrationCategories extends Component<PropsType, StateType> {
     this.setState({
       currentOptions: [],
       currentTitles: [],
-      currentIntegrationData: [],
+      currentIntegrationData: []
     });
     switch (categoryType) {
       case "kubernetes":
@@ -55,7 +55,7 @@ class IntegrationCategories extends Component<PropsType, StateType> {
       case "registry":
         api
           .getProjectRegistries("<token>", {}, { id: currentProject.id })
-          .then((res) => {
+          .then(res => {
             // Sort res.data into service type and sort each service's registry alphabetically
             let grouped: any = {};
             let final: any = [];
@@ -81,7 +81,7 @@ class IntegrationCategories extends Component<PropsType, StateType> {
             this.setState({
               currentOptions,
               currentTitles,
-              currentIntegrationData: final,
+              currentIntegrationData: final
             });
           })
           .catch(console.log);
@@ -89,7 +89,7 @@ class IntegrationCategories extends Component<PropsType, StateType> {
       case "repo":
         api
           .getGitRepos("<token>", {}, { project_id: currentProject.id })
-          .then((res) => {
+          .then(res => {
             let currentOptions = [] as string[];
             let currentTitles = [] as string[];
             let currentIds = [] as any[];
@@ -102,7 +102,7 @@ class IntegrationCategories extends Component<PropsType, StateType> {
               currentOptions,
               currentTitles,
               currentIds,
-              currentIntegrationData: res.data,
+              currentIntegrationData: res.data
             });
           })
           .catch(console.log);
@@ -143,7 +143,7 @@ class IntegrationCategories extends Component<PropsType, StateType> {
                   setCurrentIntegration: (x: string) =>
                     this.props.history.push(
                       `/integrations/${this.props.category}/create/${x}`
-                    ),
+                    )
                 })
               }
             >
@@ -159,6 +159,9 @@ class IntegrationCategories extends Component<PropsType, StateType> {
             integrations={this.state.currentOptions}
             titles={this.state.currentTitles}
             itemIdentifier={this.state.currentIntegrationData}
+            updateIntegrationList={() =>
+              this.getIntegrationsForCategory(this.props.category)
+            }
           />
         </div>
       );
@@ -195,6 +198,9 @@ class IntegrationCategories extends Component<PropsType, StateType> {
             integrations={this.state.currentOptions}
             titles={this.state.currentTitles}
             itemIdentifier={this.state.currentIds}
+            updateIntegrationList={() =>
+              this.getIntegrationsForCategory(this.props.category)
+            }
           />
         </div>
       );

+ 77 - 1
dashboard/src/main/home/integrations/IntegrationList.tsx

@@ -4,6 +4,8 @@ import styled from "styled-components";
 import { Context } from "shared/Context";
 import { integrationList } from "shared/common";
 import IntegrationRow from "./IntegrationRow";
+import ConfirmOverlay from "components/ConfirmOverlay";
+import api from "shared/api";
 
 type PropsType = {
   setCurrent?: (x: string) => void;
@@ -12,15 +14,22 @@ type PropsType = {
   itemIdentifier?: any[];
   titles?: string[];
   isCategory?: boolean;
+  updateIntegrationList: () => void;
 };
 
 type StateType = {
   displayExpanded: boolean[];
+  isDelete: boolean;
+  deleteName: string;
+  deleteID: number;
 };
 
 export default class IntegrationList extends Component<PropsType, StateType> {
   state = {
     displayExpanded: this.props.integrations.map(() => false),
+    isDelete: false,
+    deleteName: "",
+    deleteID: 0
   };
 
   allCollapsed = () =>
@@ -34,7 +43,7 @@ export default class IntegrationList extends Component<PropsType, StateType> {
 
   collapseAll = () => {
     this.setState({
-      displayExpanded: this.props.integrations.map(() => false),
+      displayExpanded: this.props.integrations.map(() => false)
     });
   };
 
@@ -51,14 +60,70 @@ export default class IntegrationList extends Component<PropsType, StateType> {
     this.setState({ displayExpanded: x });
   };
 
+  triggerDelete = (event: MouseEvent, i: number, id: number) => {
+    if (event) {
+      event.stopPropagation();
+    }
+
+    this.setState({
+      isDelete: true,
+      deleteName: this.props.titles[i],
+      deleteID: id
+    });
+  };
+
+  handleDeleteIntegration = () => {
+    let { currentProject } = this.context;
+
+    if (this.props.currentCategory === "registry") {
+      api
+        .deleteRegistryIntegration(
+          "<token>",
+          {},
+          {
+            project_id: currentProject.id,
+            registry_id: this.state.deleteID
+          }
+        )
+        .then(() => {
+          this.setState({ isDelete: false });
+          this.props.updateIntegrationList();
+        })
+        .catch(err => {
+          this.context.setCurrentError(err);
+        });
+    } else if (this.props.currentCategory === "repo") {
+      api
+        .deleteGitRepoIntegration(
+          "<token>",
+          {},
+          {
+            project_id: currentProject.id,
+            git_repo_id: this.state.deleteID
+          }
+        )
+        .then(() => {
+          this.setState({ isDelete: false });
+          this.props.updateIntegrationList();
+        })
+        .catch(err => {
+          this.context.setCurrentError(err);
+        });
+    }
+  };
+
   handleParent = (event: any, integration: string) =>
     this.props.setCurrent && this.props.setCurrent(integration);
 
   renderContents = () => {
     let { integrations, titles, setCurrent, isCategory } = this.props;
+
     if (titles && titles.length > 0) {
       return integrations.map((integration: string, i: number) => {
         let label = titles[i];
+        let item_id =
+          this.props.itemIdentifier[i].id || this.props.itemIdentifier[i];
+
         return (
           <IntegrationRow
             category={this.props.currentCategory}
@@ -68,6 +133,7 @@ export default class IntegrationList extends Component<PropsType, StateType> {
             itemId={this.props.itemIdentifier[i]}
             label={label}
             toggleCollapse={(e: MouseEvent) => this.toggleDisplay(e, i)}
+            triggerDelete={(e: MouseEvent) => this.triggerDelete(e, i, item_id)}
           ></IntegrationRow>
         );
       });
@@ -123,6 +189,16 @@ export default class IntegrationList extends Component<PropsType, StateType> {
   render() {
     return (
       <StyledIntegrationList>
+        <ConfirmOverlay
+          show={this.state.isDelete}
+          message={`Are you sure you want to delete the ${
+            this.props.currentCategory === "registry"
+              ? "Docker registry integration"
+              : "Github integration"
+          } with name ${this.state.deleteName}?`}
+          onYes={this.handleDeleteIntegration}
+          onNo={() => this.setState({ isDelete: false })}
+        />
         {this.props.titles && this.props.titles.length > 0 && (
           <ControlRow>{this.collapseAllButton()}</ControlRow>
         )}

+ 10 - 6
dashboard/src/main/home/integrations/IntegrationRow.tsx

@@ -10,6 +10,7 @@ import CreateIntegrationForm from "./create-integration/CreateIntegrationForm";
 
 type PropsType = {
   toggleCollapse: MouseEventHandler;
+  triggerDelete: MouseEventHandler;
   label: string;
   integration: string;
   expanded: boolean;
@@ -23,19 +24,19 @@ type StateType = {
 
 export default class IntegrationRow extends Component<PropsType, StateType> {
   state = {
-    editMode: false,
+    editMode: false
   };
 
   editButtonOnClick = (e: MouseEvent) => {
     e.stopPropagation();
     if (!this.props.expanded) {
       this.setState({
-        editMode: true,
+        editMode: true
       });
       this.props.toggleCollapse(null);
     } else {
       this.setState({
-        editMode: !this.state.editMode,
+        editMode: !this.state.editMode
       });
       if (this.state.editMode) {
         this.props.toggleCollapse(null);
@@ -63,6 +64,9 @@ export default class IntegrationRow extends Component<PropsType, StateType> {
           <MaterialIconTray disabled={false}>
             {/* <i className="material-icons"
             onClick={this.editButtonOnClick}>mode_edit</i> */}
+            <i className="material-icons" onClick={this.props.triggerDelete}>
+              delete
+            </i>
             <I
               className="material-icons"
               showList={this.props.expanded}
@@ -91,7 +95,7 @@ export default class IntegrationRow extends Component<PropsType, StateType> {
                     git_repo: "",
                     image_repo_uri: "",
                     git_repo_id: 0,
-                    dockerfile_path: "",
+                    dockerfile_path: ""
                   } as ActionConfigType
                 }
                 setActionConfig={() => {}}
@@ -168,8 +172,7 @@ const MainRow = styled.div`
 `;
 
 const MaterialIconTray = styled.div`
-  width: 32px;
-  margin-right: -7px;
+  max-width: 60px;
   display: flex;
   align-items: center;
   justify-content: space-between;
@@ -178,6 +181,7 @@ const MaterialIconTray = styled.div`
     border-radius: 20px;
     font-size: 18px;
     padding: 5px;
+    margin: 0 5px;
     color: #ffffff44;
     :hover {
       background: ${(props: { disabled: boolean }) =>

+ 5 - 4
dashboard/src/main/home/integrations/Integrations.tsx

@@ -18,7 +18,7 @@ const IntegrationCategoryStrings = ["registry", "repo"]; /*"kubernetes",*/
 
 class Integrations extends Component<PropsType, StateType> {
   state = {
-    currentIntegrationData: [] as any[],
+    currentIntegrationData: [] as any[]
   };
 
   render = () => (
@@ -26,7 +26,7 @@ class Integrations extends Component<PropsType, StateType> {
       <Switch>
         <Route
           path="/integrations/:category/create/:integration"
-          render={(rp) => {
+          render={rp => {
             const { integration, category } = rp.match.params;
             if (!IntegrationCategoryStrings.includes(category)) {
               this.props.history.push("/integrations");
@@ -62,7 +62,7 @@ class Integrations extends Component<PropsType, StateType> {
         />
         <Route
           path="/integrations/:category"
-          render={(rp) => {
+          render={rp => {
             const currentCategory = rp.match.params.category;
             if (!IntegrationCategoryStrings.includes(currentCategory)) {
               this.props.history.push("/integrations");
@@ -83,8 +83,9 @@ class Integrations extends Component<PropsType, StateType> {
             <IntegrationList
               currentCategory={""}
               integrations={["kubernetes", "registry", "repo"]}
-              setCurrent={(x) => this.props.history.push(`/integrations/${x}`)}
+              setCurrent={x => this.props.history.push(`/integrations/${x}`)}
               isCategory={true}
+              updateIntegrationList={() => {}}
             />
           </div>
         </Route>

+ 2 - 2
dashboard/src/main/home/integrations/create-integration/DockerHubForm.tsx

@@ -20,7 +20,7 @@ export default class DockerHubForm extends Component<PropsType, StateType> {
     registryURL: "",
     dockerEmail: "",
     dockerUsername: "",
-    dockerPassword: "",
+    dockerPassword: ""
   };
 
   isDisabled = (): boolean => {
@@ -28,7 +28,7 @@ export default class DockerHubForm extends Component<PropsType, StateType> {
       registryURL,
       dockerEmail,
       dockerUsername,
-      dockerPassword,
+      dockerPassword
     } = this.state;
     if (
       registryURL === "" ||

+ 4 - 4
dashboard/src/main/home/integrations/create-integration/ECRForm.tsx

@@ -25,7 +25,7 @@ export default class ECRForm extends Component<PropsType, StateType> {
     credentialsName: "",
     awsRegion: "",
     awsAccessId: "",
-    awsSecretKey: "",
+    awsSecretKey: ""
   };
 
   isDisabled = (): boolean => {
@@ -53,16 +53,16 @@ export default class ECRForm extends Component<PropsType, StateType> {
         {
           aws_region: awsRegion,
           aws_access_key_id: awsAccessId,
-          aws_secret_access_key: awsSecretKey,
+          aws_secret_access_key: awsSecretKey
         },
         { id: currentProject.id }
       )
-      .then((res) =>
+      .then(res =>
         api.connectECRRegistry(
           "<token>",
           {
             name: credentialsName,
-            aws_integration_id: res.data.id,
+            aws_integration_id: res.data.id
           },
           { id: currentProject.id }
         )

+ 2 - 2
dashboard/src/main/home/integrations/create-integration/EKSForm.tsx

@@ -25,7 +25,7 @@ export default class EKSForm extends Component<PropsType, StateType> {
     clusterEndpoint: "",
     clusterCA: "",
     awsAccessId: "",
-    awsSecretKey: "",
+    awsSecretKey: ""
   };
 
   isDisabled = (): boolean => {
@@ -34,7 +34,7 @@ export default class EKSForm extends Component<PropsType, StateType> {
       clusterEndpoint,
       clusterCA,
       awsAccessId,
-      awsSecretKey,
+      awsSecretKey
     } = this.state;
     if (
       clusterName === "" ||

+ 8 - 8
dashboard/src/main/home/integrations/create-integration/GCRForm.tsx

@@ -28,7 +28,7 @@ export default class GCRForm extends Component<PropsType, StateType> {
     gcpRegion: "",
     serviceAccountKey: "",
     gcpProjectID: "",
-    url: "",
+    url: ""
   };
 
   isDisabled = (): boolean => {
@@ -36,7 +36,7 @@ export default class GCRForm extends Component<PropsType, StateType> {
       credentialsName,
       gcpRegion,
       gcpProjectID,
-      serviceAccountKey,
+      serviceAccountKey
     } = this.state;
     if (
       credentialsName === "" ||
@@ -60,26 +60,26 @@ export default class GCRForm extends Component<PropsType, StateType> {
         {
           gcp_region: this.state.gcpRegion,
           gcp_key_data: this.state.serviceAccountKey,
-          gcp_project_id: this.state.gcpProjectID,
+          gcp_project_id: this.state.gcpProjectID
         },
         {
-          project_id: currentProject.id,
+          project_id: currentProject.id
         }
       )
-      .then((res) =>
+      .then(res =>
         api.connectGCRRegistry(
           "<token>",
           {
             name: this.state.credentialsName,
             gcp_integration_id: res.data.id,
-            url: this.state.url,
+            url: this.state.url
           },
           {
-            id: currentProject.id,
+            id: currentProject.id
           }
         )
       )
-      .then((res) => {
+      .then(res => {
         console.log(res.data);
         this.props.closeForm();
       })

+ 2 - 2
dashboard/src/main/home/integrations/create-integration/GKEForm.tsx

@@ -23,7 +23,7 @@ export default class GKEForm extends Component<PropsType, StateType> {
     clusterName: "",
     clusterEndpoint: "",
     clusterCA: "",
-    serviceAccountKey: "",
+    serviceAccountKey: ""
   };
 
   isDisabled = (): boolean => {
@@ -31,7 +31,7 @@ export default class GKEForm extends Component<PropsType, StateType> {
       clusterName,
       clusterEndpoint,
       clusterCA,
-      serviceAccountKey,
+      serviceAccountKey
     } = this.state;
     if (
       clusterName === "" ||

+ 2 - 2
dashboard/src/main/home/integrations/edit-integration/DockerHubForm.tsx

@@ -20,7 +20,7 @@ export default class DockerHubForm extends Component<PropsType, StateType> {
     registryURL: "",
     dockerEmail: "",
     dockerUsername: "",
-    dockerPassword: "",
+    dockerPassword: ""
   };
 
   isDisabled = (): boolean => {
@@ -28,7 +28,7 @@ export default class DockerHubForm extends Component<PropsType, StateType> {
       registryURL,
       dockerEmail,
       dockerUsername,
-      dockerPassword,
+      dockerPassword
     } = this.state;
     if (
       registryURL === "" ||

+ 4 - 4
dashboard/src/main/home/integrations/edit-integration/ECRForm.tsx

@@ -25,7 +25,7 @@ export default class ECRForm extends Component<PropsType, StateType> {
     credentialsName: "",
     awsRegion: "",
     awsAccessId: "",
-    awsSecretKey: "",
+    awsSecretKey: ""
   };
 
   isDisabled = (): boolean => {
@@ -53,16 +53,16 @@ export default class ECRForm extends Component<PropsType, StateType> {
         {
           aws_region: awsRegion,
           aws_access_key_id: awsAccessId,
-          aws_secret_access_key: awsSecretKey,
+          aws_secret_access_key: awsSecretKey
         },
         { id: currentProject.id }
       )
-      .then((res) =>
+      .then(res =>
         api.connectECRRegistry(
           "<token>",
           {
             name: credentialsName,
-            aws_integration_id: res.data.id,
+            aws_integration_id: res.data.id
           },
           { id: currentProject.id }
         )

+ 2 - 2
dashboard/src/main/home/integrations/edit-integration/EKSForm.tsx

@@ -25,7 +25,7 @@ export default class EKSForm extends Component<PropsType, StateType> {
     clusterEndpoint: "",
     clusterCA: "",
     awsAccessId: "",
-    awsSecretKey: "",
+    awsSecretKey: ""
   };
 
   isDisabled = (): boolean => {
@@ -34,7 +34,7 @@ export default class EKSForm extends Component<PropsType, StateType> {
       clusterEndpoint,
       clusterCA,
       awsAccessId,
-      awsSecretKey,
+      awsSecretKey
     } = this.state;
     if (
       clusterName === "" ||

+ 8 - 8
dashboard/src/main/home/integrations/edit-integration/GCRForm.tsx

@@ -28,7 +28,7 @@ export default class GCRForm extends Component<PropsType, StateType> {
     gcpRegion: "",
     serviceAccountKey: "",
     gcpProjectID: "",
-    url: "",
+    url: ""
   };
 
   isDisabled = (): boolean => {
@@ -36,7 +36,7 @@ export default class GCRForm extends Component<PropsType, StateType> {
       credentialsName,
       gcpRegion,
       gcpProjectID,
-      serviceAccountKey,
+      serviceAccountKey
     } = this.state;
     if (
       credentialsName === "" ||
@@ -60,26 +60,26 @@ export default class GCRForm extends Component<PropsType, StateType> {
         {
           gcp_region: this.state.gcpRegion,
           gcp_key_data: this.state.serviceAccountKey,
-          gcp_project_id: this.state.gcpProjectID,
+          gcp_project_id: this.state.gcpProjectID
         },
         {
-          project_id: currentProject.id,
+          project_id: currentProject.id
         }
       )
-      .then((res) =>
+      .then(res =>
         api.connectGCRRegistry(
           "<token>",
           {
             name: this.state.credentialsName,
             gcp_integration_id: res.data.id,
-            url: this.state.url,
+            url: this.state.url
           },
           {
-            id: currentProject.id,
+            id: currentProject.id
           }
         )
       )
-      .then((res) => {
+      .then(res => {
         console.log(res.data);
         this.props.closeForm();
       })

+ 2 - 2
dashboard/src/main/home/integrations/edit-integration/GKEForm.tsx

@@ -23,7 +23,7 @@ export default class GKEForm extends Component<PropsType, StateType> {
     clusterName: "",
     clusterEndpoint: "",
     clusterCA: "",
-    serviceAccountKey: "",
+    serviceAccountKey: ""
   };
 
   isDisabled = (): boolean => {
@@ -31,7 +31,7 @@ export default class GKEForm extends Component<PropsType, StateType> {
       clusterName,
       clusterEndpoint,
       clusterCA,
-      serviceAccountKey,
+      serviceAccountKey
     } = this.state;
     if (
       clusterName === "" ||

+ 8 - 8
dashboard/src/main/home/launch/Launch.tsx

@@ -14,7 +14,7 @@ import { Link } from "react-router-dom";
 
 const tabOptions = [
   { label: "New Application", value: "docker" },
-  { label: "Community Add-ons", value: "community" },
+  { label: "Community Add-ons", value: "community" }
 ];
 
 type PropsType = {};
@@ -35,17 +35,17 @@ export default class Templates extends Component<PropsType, StateType> {
     addonTemplates: [] as PorterTemplate[],
     applicationTemplates: [] as PorterTemplate[],
     loading: true,
-    error: false,
+    error: false
   };
 
   componentDidMount() {
     api
       .getAddonTemplates("<token>", {}, {})
-      .then((res) => {
+      .then(res => {
         this.setState({ addonTemplates: res.data, error: false }, () => {
           this.state.addonTemplates.sort((a, b) => (a.name > b.name ? 1 : -1));
           this.setState({
-            loading: false,
+            loading: false
           });
         });
       })
@@ -55,17 +55,17 @@ export default class Templates extends Component<PropsType, StateType> {
       .getApplicationTemplates(
         "<token>",
         {
-          repo_url: process.env.APPLICATION_CHART_REPO_URL,
+          repo_url: process.env.APPLICATION_CHART_REPO_URL
         },
         {}
       )
-      .then((res) => {
+      .then(res => {
         this.setState({ applicationTemplates: res.data, error: false }, () => {
           this.state.applicationTemplates.sort((a, b) =>
             a.version > b.version ? 1 : -1
           );
           this.setState({
-            loading: false,
+            loading: false
           });
         });
       })
@@ -238,7 +238,7 @@ export default class Templates extends Component<PropsType, StateType> {
           setCurrentTab={(value: string) =>
             this.setState({
               currentTab: value,
-              currentTemplate: null,
+              currentTemplate: null
             })
           }
         />

+ 5 - 5
dashboard/src/main/home/launch/expanded-template/ExpandedTemplate.tsx

@@ -33,7 +33,7 @@ export default class ExpandedTemplate extends Component<PropsType, StateType> {
     loading: true,
     error: false,
     markdown: null as string | null,
-    keywords: [] as string[],
+    keywords: [] as string[]
   };
 
   componentDidMount() {
@@ -50,9 +50,9 @@ export default class ExpandedTemplate extends Component<PropsType, StateType> {
     api
       .getTemplateInfo("<token>", params, {
         name: this.props.currentTemplate.name.toLowerCase().trim(),
-        version: "latest",
+        version: "latest"
       })
-      .then((res) => {
+      .then(res => {
         let { form, values, markdown, metadata } = res.data;
         let keywords = metadata.keywords;
         this.setState({
@@ -61,10 +61,10 @@ export default class ExpandedTemplate extends Component<PropsType, StateType> {
           markdown,
           keywords,
           loading: false,
-          error: false,
+          error: false
         });
       })
-      .catch((err) => this.setState({ loading: false, error: true }));
+      .catch(err => this.setState({ loading: false, error: true }));
   };
 
   componentDidUpdate = (prevProps: PropsType) => {

+ 38 - 38
dashboard/src/main/home/launch/expanded-template/LaunchTemplate.tsx

@@ -11,7 +11,7 @@ import {
   ActionConfigType,
   ChoiceType,
   ClusterType,
-  StorageType,
+  StorageType
 } from "shared/types";
 import Selector from "components/Selector";
 import ImageSelector from "components/image-selector/ImageSelector";
@@ -60,7 +60,7 @@ type StateType = {
 const defaultActionConfig: ActionConfigType = {
   git_repo: "",
   image_repo_uri: "",
-  git_repo_id: 0,
+  git_repo_id: 0
 };
 
 class LaunchTemplate extends Component<PropsType, StateType> {
@@ -85,7 +85,7 @@ class LaunchTemplate extends Component<PropsType, StateType> {
     dockerfilePath: null as string | null,
     folderPath: null as string | null,
     selectedRegistry: null as any | null,
-    env: {},
+    env: {}
   };
 
   createGHAction = (chartName: string, chartNamespace: string) => {
@@ -108,16 +108,16 @@ class LaunchTemplate extends Component<PropsType, StateType> {
           folder_path: this.state.folderPath,
           image_repo_uri: imageRepoUri,
           git_repo_id: actionConfig.git_repo_id,
-          env: this.state.env,
+          env: this.state.env
         },
         {
           project_id: currentProject.id,
           CLUSTER_ID: currentCluster.id,
           RELEASE_NAME: chartName,
-          RELEASE_NAMESPACE: chartNamespace,
+          RELEASE_NAMESPACE: chartNamespace
         }
       )
-      .then((res) => console.log(res.data))
+      .then(res => console.log(res.data))
       .catch(console.log);
   };
 
@@ -140,16 +140,16 @@ class LaunchTemplate extends Component<PropsType, StateType> {
           storage: StorageType.Secret,
           formValues: values,
           namespace: this.state.selectedNamespace,
-          name,
+          name
         },
         {
           id: currentProject.id,
           cluster_id: currentCluster.id,
           name: this.props.currentTemplate.name.toLowerCase().trim(),
-          version: "latest",
+          version: "latest"
         }
       )
-      .then((_) => {
+      .then(_ => {
         // this.props.setCurrentView('cluster-dashboard');
         this.setState({ saveValuesStatus: "successful" }, () => {
           // redirect to dashboard
@@ -159,18 +159,18 @@ class LaunchTemplate extends Component<PropsType, StateType> {
           window.analytics.track("Deployed Add-on", {
             name: this.props.currentTemplate.name,
             namespace: this.state.selectedNamespace,
-            values: values,
+            values: values
           });
         });
       })
-      .catch((err) => {
+      .catch(err => {
         this.setState({ saveValuesStatus: "error" });
         setCurrentError(err.response.data.errors[0]);
         window.analytics.track("Failed to Deploy Add-on", {
           name: this.props.currentTemplate.name,
           namespace: this.state.selectedNamespace,
           values: values,
-          error: err,
+          error: err
         });
       });
   };
@@ -235,17 +235,17 @@ class LaunchTemplate extends Component<PropsType, StateType> {
             .createSubdomain(
               "<token>",
               {
-                release_name: name,
+                release_name: name
               },
               {
                 id: currentProject.id,
-                cluster_id: currentCluster.id,
+                cluster_id: currentCluster.id
               }
             )
-            .then((res) => {
+            .then(res => {
               resolve(res.data?.external_url);
             })
-            .catch((err) => {
+            .catch(err => {
               this.setState({ saveValuesStatus: "error" });
             });
         });
@@ -266,17 +266,17 @@ class LaunchTemplate extends Component<PropsType, StateType> {
           storage: StorageType.Secret,
           formValues: values,
           namespace: this.state.selectedNamespace,
-          name,
+          name
         },
         {
           id: currentProject.id,
           cluster_id: currentCluster.id,
           name: this.props.currentTemplate.name.toLowerCase().trim(),
           version: "latest",
-          repo_url: process.env.APPLICATION_CHART_REPO_URL,
+          repo_url: process.env.APPLICATION_CHART_REPO_URL
         }
       )
-      .then((_) => {
+      .then(_ => {
         console.log("Deployed template.");
         if (this.state.sourceType === "repo") {
           console.log("Creating GHA");
@@ -302,7 +302,7 @@ class LaunchTemplate extends Component<PropsType, StateType> {
         }
         */
       })
-      .catch((err) => {
+      .catch(err => {
         this.setState({ saveValuesStatus: "error" });
         /*
         try {
@@ -326,7 +326,7 @@ class LaunchTemplate extends Component<PropsType, StateType> {
       sourceType,
       selectedImageUrl,
       dockerfilePath,
-      folderPath,
+      folderPath
     } = this.state;
 
     // Allow if name is invalid
@@ -356,7 +356,7 @@ class LaunchTemplate extends Component<PropsType, StateType> {
       selectedRegistry,
       sourceType,
       dockerfilePath,
-      folderPath,
+      folderPath
     } = this.state;
 
     if (this.submitIsDisabled()) {
@@ -423,12 +423,12 @@ class LaunchTemplate extends Component<PropsType, StateType> {
 
     this.setState({
       tabOptions,
-      currentTab: tabOptions[0] && tabOptions[0]["value"],
+      currentTab: tabOptions[0] && tabOptions[0]["value"]
     });
 
     // TODO: query with selected filter once implemented
     let { currentProject, currentCluster } = this.context;
-    api.getClusters("<token>", {}, { id: currentProject.id }).then((res) => {
+    api.getClusters("<token>", {}, { id: currentProject.id }).then(res => {
       if (res.data) {
         let clusterOptions: { label: string; value: string }[] = [];
         let clusterMap: { [clusterId: string]: ClusterType } = {};
@@ -451,11 +451,11 @@ class LaunchTemplate extends Component<PropsType, StateType> {
       .getNamespaces(
         "<token>",
         {
-          cluster_id: id,
+          cluster_id: id
         },
         { id: currentProject.id }
       )
-      .then((res) => {
+      .then(res => {
         if (res.data) {
           let namespaceOptions = res.data.items.map(
             (x: { metadata: { name: string } }) => {
@@ -597,12 +597,12 @@ class LaunchTemplate extends Component<PropsType, StateType> {
             options={[
               {
                 value: "dockerfile",
-                label: "Yes, I am using an existing Dockerfile",
+                label: "Yes, I am using an existing Dockerfile"
               },
               {
                 value: "buildpack",
-                label: "No, I am not using an existing Dockerfile",
-              },
+                label: "No, I am not using an existing Dockerfile"
+              }
             ]}
             selected={this.state.repoType}
             setSelected={(x: string) => this.setState({ repoType: x })}
@@ -647,7 +647,7 @@ class LaunchTemplate extends Component<PropsType, StateType> {
                 actionConfig: { ...defaultActionConfig },
                 branch: "",
                 dockerfilePath: null,
-                folderPath: null,
+                folderPath: null
               });
             }}
             setSelectedRegistry={(x: any) => {
@@ -791,7 +791,7 @@ const BlockIcon = styled.img<{ bw?: boolean }>`
   padding: 2px;
   margin-top: 30px;
   margin-bottom: 15px;
-  filter: ${(props) => (props.bw ? "grayscale(1)" : "")};
+  filter: ${props => (props.bw ? "grayscale(1)" : "")};
 `;
 
 const BlockDescription = styled.div`
@@ -832,13 +832,13 @@ 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;
@@ -893,14 +893,14 @@ const Heading = styled.div<{ isAtTop?: boolean }>`
   font-weight: 500;
   font-size: 16px;
   margin-bottom: 5px;
-  margin-top: ${(props) => (props.isAtTop ? "10px" : "30px")};
+  margin-top: ${props => (props.isAtTop ? "10px" : "30px")};
   display: flex;
   align-items: center;
 `;
 
 const Warning = styled.span<{ highlight: boolean; makeFlush?: boolean }>`
-  color: ${(props) => (props.highlight ? "#f5cb42" : "")};
-  margin-left: ${(props) => (props.makeFlush ? "" : "5px")};
+  color: ${props => (props.highlight ? "#f5cb42" : "")};
+  margin-left: ${props => (props.makeFlush ? "" : "5px")};
 `;
 
 const Required = styled.div`
@@ -935,7 +935,7 @@ 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`
@@ -979,7 +979,7 @@ const Polymer = styled.div`
   margin-bottom: -3px;
 
   > i {
-    color: ${(props) => props.theme.containerIcon};
+    color: ${props => props.theme.containerIcon};
     font-size: 18px;
     margin-right: 10px;
   }

+ 1 - 1
dashboard/src/main/home/launch/expanded-template/TemplateInfo.tsx

@@ -280,7 +280,7 @@ 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;

+ 1 - 1
dashboard/src/main/home/launch/hardcodedNameDict.tsx

@@ -9,7 +9,7 @@ const hardcodedNames: { [key: string]: string } = {
   ubuntu: "Ubuntu",
   web: "Web Service",
   worker: "Worker",
-  job: "Job",
+  job: "Job"
 };
 
 export default hardcodedNames;

+ 2 - 2
dashboard/src/main/home/modals/ClusterInstructionsModal.tsx

@@ -20,7 +20,7 @@ export default class ClusterInstructionsModal extends Component<
 > {
   state = {
     currentTab: "mac",
-    currentPage: 0,
+    currentPage: 0
   };
 
   renderPage = () => {
@@ -84,7 +84,7 @@ export default class ClusterInstructionsModal extends Component<
             there are two contexts named minikube and staging, you could connect
             both of them via:
             <Code>
-              porter connect kubeconfig --contexts minikube --contexts staging
+              porter connect kubeconfig --context minikube --context staging
             </Code>
           </Placeholder>
         );

+ 1 - 1
dashboard/src/main/home/modals/IntegrationsInstructionsModal.tsx

@@ -20,7 +20,7 @@ export default class ClusterInstructionsModal extends Component<
 > {
   state = {
     currentTab: "mac",
-    currentPage: 0,
+    currentPage: 0
   };
 
   renderPage = () => {

+ 7 - 7
dashboard/src/main/home/modals/IntegrationsModal.tsx

@@ -14,7 +14,7 @@ type StateType = {
 
 export default class IntegrationsModal extends Component<PropsType, StateType> {
   state = {
-    integrations: [] as any[],
+    integrations: [] as any[]
   };
 
   componentDidMount() {
@@ -22,18 +22,18 @@ export default class IntegrationsModal extends Component<PropsType, StateType> {
     if (category === "kubernetes") {
       api
         .getClusterIntegrations("<token>", {}, {})
-        .then((res) => this.setState({ integrations: res.data }))
-        .catch((err) => console.log(err));
+        .then(res => this.setState({ integrations: res.data }))
+        .catch(err => console.log(err));
     } else if (category === "registry") {
       api
         .getRegistryIntegrations("<token>", {}, {})
-        .then((res) => this.setState({ integrations: res.data }))
-        .catch((err) => console.log(err));
+        .then(res => this.setState({ integrations: res.data }))
+        .catch(err => console.log(err));
     } else {
       api
         .getRepoIntegrations("<token>", {}, {})
-        .then((res) => this.setState({ integrations: res.data }))
-        .catch((err) => console.log(err));
+        .then(res => this.setState({ integrations: res.data }))
+        .catch(err => console.log(err));
     }
   }
 

+ 9 - 10
dashboard/src/main/home/modals/UpdateClusterModal.tsx

@@ -25,7 +25,7 @@ class UpdateClusterModal extends Component<PropsType, StateType> {
   state = {
     clusterName: this.context.currentCluster.name,
     status: null as string | null,
-    showDeleteOverlay: false,
+    showDeleteOverlay: false
   };
 
   catchErr = (err: any) => {
@@ -43,10 +43,10 @@ class UpdateClusterModal extends Component<PropsType, StateType> {
         {},
         {
           project_id: currentProject.id,
-          cluster_id: currentCluster.id,
+          cluster_id: currentCluster.id
         }
       )
-      .then((_) => {
+      .then(_ => {
         if (!currentCluster?.infra_id) {
           // TODO: make this more declarative from the Home component
           this.props.setRefreshClusters(true);
@@ -65,7 +65,7 @@ class UpdateClusterModal extends Component<PropsType, StateType> {
                 { eks_name: currentCluster.name },
                 {
                   project_id: currentProject.id,
-                  infra_id: currentCluster.infra_id,
+                  infra_id: currentCluster.infra_id
                 }
               )
               .then(() => console.log("destroyed provisioned infra."))
@@ -78,7 +78,7 @@ class UpdateClusterModal extends Component<PropsType, StateType> {
                 { gke_name: currentCluster.name },
                 {
                   project_id: currentProject.id,
-                  infra_id: currentCluster.infra_id,
+                  infra_id: currentCluster.infra_id
                 }
               )
               .then(() => console.log("destroyed provisioned infra."))
@@ -92,7 +92,7 @@ class UpdateClusterModal extends Component<PropsType, StateType> {
                 { doks_name: currentCluster.name },
                 {
                   project_id: currentProject.id,
-                  infra_id: currentCluster.infra_id,
+                  infra_id: currentCluster.infra_id
                 }
               )
               .then(() => console.log("destroyed provisioned infra."))
@@ -121,9 +121,8 @@ class UpdateClusterModal extends Component<PropsType, StateType> {
 
     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. For a guide on how to
+        delete dangling resources, click on the Help Button below.
       </Warning>
     );
   };
@@ -159,7 +158,7 @@ class UpdateClusterModal extends Component<PropsType, StateType> {
         {this.renderWarning()}
 
         <Help
-          href="https://docs.getporter.dev/docs/getting-started-with-porter-on-aws#deleting-provisioned-resources"
+          href="https://docs.getporter.dev/docs/deleting-dangling-resources"
           target="_blank"
         >
           <i className="material-icons">help_outline</i> Help

+ 4 - 4
dashboard/src/main/home/navbar/Feedback.tsx

@@ -18,7 +18,7 @@ export default class Feedback extends Component<PropsType, StateType> {
   state = {
     feedbackSent: false,
     showFeedbackDropdown: false,
-    feedbackText: "",
+    feedbackText: ""
   };
 
   renderReceipt = () => {
@@ -57,7 +57,7 @@ export default class Feedback extends Component<PropsType, StateType> {
             onClick={() =>
               this.setState({
                 showFeedbackDropdown: false,
-                feedbackSent: false,
+                feedbackSent: false
               })
             }
           />
@@ -69,7 +69,7 @@ export default class Feedback extends Component<PropsType, StateType> {
             <FeedbackInput
               autoFocus={true}
               value={this.state.feedbackText}
-              onChange={(e) => this.setState({ feedbackText: e.target.value })}
+              onChange={e => this.setState({ feedbackText: e.target.value })}
               placeholder="Help us improve this page."
             />
             <SendButton
@@ -91,7 +91,7 @@ export default class Feedback extends Component<PropsType, StateType> {
         <Flex
           onClick={() =>
             this.setState({
-              showFeedbackDropdown: !this.state.showFeedbackDropdown,
+              showFeedbackDropdown: !this.state.showFeedbackDropdown
             })
           }
         >

+ 3 - 14
dashboard/src/main/home/navbar/Navbar.tsx

@@ -17,18 +17,7 @@ type StateType = {
 
 export default class Navbar extends Component<PropsType, StateType> {
   state = {
-    showDropdown: false,
-  };
-
-  handleLogout = (): void => {
-    let { logOut } = this.props;
-    let { setCurrentError } = this.context;
-
-    // Attempt user logout
-    api
-      .logOutUser("<token>", {}, {})
-      .then(logOut)
-      .catch((err) => setCurrentError(err.response.data.errors[0]));
+    showDropdown: false
   };
 
   renderSettingsDropdown = () => {
@@ -42,7 +31,7 @@ export default class Navbar extends Component<PropsType, StateType> {
             <DropdownLabel>
               {this.context.user && this.context.user.email}
             </DropdownLabel>
-            <LogOutButton onClick={this.handleLogout}>
+            <LogOutButton onClick={this.props.logOut}>
               <i className="material-icons">keyboard_return</i> Log Out
             </LogOutButton>
           </Dropdown>
@@ -94,7 +83,7 @@ const LogOutButton = styled.button`
   border: 0;
   text-align: left;
   background: none;
-  cursor: ${(props) => (!props.disabled ? "pointer" : "default")};
+  cursor: ${props => (!props.disabled ? "pointer" : "default")};
   user-select: none;
   :focus {
     outline: 0;

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

@@ -19,7 +19,7 @@ type StateType = {
 export default class NewProject extends Component<PropsType, StateType> {
   state = {
     projectName: "",
-    selectedProvider: null as string | null,
+    selectedProvider: null as string | null
   };
 
   render() {

+ 17 - 18
dashboard/src/main/home/project-settings/InviteList.tsx

@@ -28,7 +28,7 @@ export default class InviteList extends Component<PropsType, StateType> {
     invites: [] as InviteType[],
     email: "",
     invalidEmail: false,
-    isHTTPS: process.env.API_SERVER === "dashboard.getporter.dev",
+    isHTTPS: process.env.API_SERVER === "dashboard.getporter.dev"
   };
 
   componentDidMount() {
@@ -44,11 +44,11 @@ export default class InviteList extends Component<PropsType, StateType> {
         "<token>",
         {},
         {
-          id: currentProject.id,
+          id: currentProject.id
         }
       )
-      .then((res) => this.setState({ invites: res.data, loading: false }))
-      .catch((err) => console.log(err));
+      .then(res => this.setState({ invites: res.data, loading: false }))
+      .catch(err => console.log(err));
   };
 
   validateEmail = () => {
@@ -69,11 +69,11 @@ export default class InviteList extends Component<PropsType, StateType> {
         { email: this.state.email },
         { id: currentProject.id }
       )
-      .then((_) => {
+      .then(_ => {
         this.getInviteData();
         this.setState({ email: "" });
       })
-      .catch((err) => console.log(err));
+      .catch(err => console.log(err));
   };
 
   deleteInvite = (index: number) => {
@@ -84,11 +84,11 @@ export default class InviteList extends Component<PropsType, StateType> {
         {},
         {
           id: currentProject.id,
-          invId: this.state.invites[index].id,
+          invId: this.state.invites[index].id
         }
       )
       .then(this.getInviteData)
-      .catch((err) => console.log(err));
+      .catch(err => console.log(err));
   };
 
   replaceInvite = (index: number) => {
@@ -99,18 +99,18 @@ export default class InviteList extends Component<PropsType, StateType> {
         { email: this.state.invites[index].email },
         { id: currentProject.id }
       )
-      .then((_) =>
+      .then(_ =>
         api.deleteInvite(
           "<token>",
           {},
           {
             id: currentProject.id,
-            invId: this.state.invites[index].id,
+            invId: this.state.invites[index].id
           }
         )
       )
       .then(this.getInviteData)
-      .catch((err) => console.log(err));
+      .catch(err => console.log(err));
   };
 
   copyToClip = (index: number) => {
@@ -124,8 +124,8 @@ export default class InviteList extends Component<PropsType, StateType> {
         }`
       )
       .then(
-        function () {},
-        function () {
+        function() {},
+        function() {
           console.log("couldn't copy link to clipboard");
         }
       );
@@ -327,16 +327,15 @@ 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;
   }
   :hover {
-    filter: ${(props) => (!props.disabled ? "brightness(120%)" : "")};
+    filter: ${props => (!props.disabled ? "brightness(120%)" : "")};
   }
   margin-bottom: 10px;
 `;

+ 18 - 3
dashboard/src/main/home/project-settings/ProjectSettings.tsx

@@ -17,13 +17,13 @@ type StateType = {
 
 const tabOptions = [
   { value: "manage-access", label: "Manage Access" },
-  { value: "additional-settings", label: "Additional Settings" },
+  { value: "additional-settings", label: "Additional Settings" }
 ];
 
 export default class ProjectSettings extends Component<PropsType, StateType> {
   state = {
     projectName: "",
-    currentTab: "manage-access",
+    currentTab: "manage-access"
   };
 
   componentDidMount() {
@@ -45,12 +45,27 @@ export default class ProjectSettings extends Component<PropsType, StateType> {
             delete the registries, please do so manually in your cloud console.
           </Helper>
 
+          <Helper>
+            Destruction of resources sometimes results in dangling resources. To
+            ensure that everything has been properly destroyed, please visit
+            your cloud provider's console. Instructions to properly delete all
+            resources can be found
+            <a
+              target="none"
+              href="https://docs.getporter.dev/docs/deleting-dangling-resources"
+            >
+              {" "}
+              here
+            </a>
+            .
+          </Helper>
+
           <Warning highlight={true}>This action cannot be undone.</Warning>
 
           <DeleteButton
             onClick={() => {
               this.context.setCurrentModal("UpdateProjectModal", {
-                currentProject: this.context.currentProject,
+                currentProject: this.context.currentProject
               });
             }}
           >

+ 13 - 13
dashboard/src/main/home/provisioner/AWSFormSection.tsx

@@ -34,7 +34,7 @@ type StateType = {
 
 const provisionOptions = [
   { value: "ecr", label: "Elastic Container Registry (ECR)" },
-  { value: "eks", label: "Elastic Kubernetes Service (EKS)" },
+  { value: "eks", label: "Elastic Kubernetes Service (EKS)" }
 ];
 
 const regionOptions = [
@@ -57,7 +57,7 @@ const regionOptions = [
   { 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: "sa-east-1", label: "South America (São Paulo) sa-east-1" }
 ];
 
 // TODO: Consolidate across forms w/ HOC
@@ -68,7 +68,7 @@ class AWSFormSection extends Component<PropsType, StateType> {
     awsSecretKey: "",
     selectedInfras: [...provisionOptions],
     buttonStatus: "",
-    provisionConfirmed: false,
+    provisionConfirmed: false
   };
 
   componentDidMount = () => {
@@ -125,7 +125,7 @@ class AWSFormSection extends Component<PropsType, StateType> {
 
     api
       .createProject("<token>", { name: projectName }, {})
-      .then((res) => {
+      .then(res => {
         let proj = res.data;
         // Need to set project list for dropdown
         // TODO: consolidate into ProjectSection (case on exists in list on set)
@@ -134,10 +134,10 @@ class AWSFormSection extends Component<PropsType, StateType> {
             "<token>",
             {},
             {
-              id: user.userId,
+              id: user.userId
             }
           )
-          .then((res) => {
+          .then(res => {
             setProjects(res.data);
             setCurrentProject(proj, () => {
               callback && callback();
@@ -159,16 +159,16 @@ class AWSFormSection extends Component<PropsType, StateType> {
         {
           aws_region: awsRegion,
           aws_access_key_id: awsAccessId,
-          aws_secret_access_key: awsSecretKey,
+          aws_secret_access_key: awsSecretKey
         },
         { id: currentProject.id }
       )
-      .then((res) =>
+      .then(res =>
         api.provisionECR(
           "<token>",
           {
             aws_integration_id: res.data.id,
-            ecr_name: `${currentProject.name}-registry`,
+            ecr_name: `${currentProject.name}-registry`
           },
           { id: currentProject.id }
         )
@@ -189,16 +189,16 @@ class AWSFormSection extends Component<PropsType, StateType> {
           aws_region: awsRegion,
           aws_access_key_id: awsAccessId,
           aws_secret_access_key: awsSecretKey,
-          aws_cluster_id: clusterName,
+          aws_cluster_id: clusterName
         },
         { id: currentProject.id }
       )
-      .then((res) =>
+      .then(res =>
         api.provisionEKS(
           "<token>",
           {
             aws_integration_id: res.data.id,
-            eks_name: clusterName,
+            eks_name: clusterName
           },
           { id: currentProject.id }
         )
@@ -336,7 +336,7 @@ class AWSFormSection extends Component<PropsType, StateType> {
             checked={this.state.provisionConfirmed}
             toggle={() =>
               this.setState({
-                provisionConfirmed: !this.state.provisionConfirmed,
+                provisionConfirmed: !this.state.provisionConfirmed
               })
             }
             label="I understand and wish to proceed"

+ 8 - 7
dashboard/src/main/home/provisioner/DOFormSection.tsx

@@ -30,13 +30,13 @@ type StateType = {
 
 const provisionOptions = [
   { value: "docr", label: "Digital Ocean Container Registry" },
-  { value: "doks", label: "Digital Ocean Kubernetes Service" },
+  { value: "doks", label: "Digital Ocean Kubernetes Service" }
 ];
 
 const tierOptions = [
   { value: "basic", label: "Basic" },
   { value: "starter", label: "Starter" },
-  { value: "professional", label: "Professional" },
+  { value: "professional", label: "Professional" }
 ];
 
 const regionOptions = [
@@ -49,7 +49,7 @@ const regionOptions = [
   { value: "sfo2", label: "San Francisco 2" },
   { value: "sfo3", label: "San Francisco 3" },
   { value: "sgp1", label: "Singapore 1" },
-  { value: "tor1", label: "Toronto 1" },
+  { value: "tor1", label: "Toronto 1" }
 ];
 
 // TODO: Consolidate across forms w/ HOC
@@ -58,7 +58,7 @@ export default class DOFormSection extends Component<PropsType, StateType> {
     selectedInfras: [...provisionOptions],
     subscriptionTier: "starter",
     doRegion: "nyc1",
-    provisionConfirmed: false,
+    provisionConfirmed: false
   };
 
   componentDidMount = () => {
@@ -108,7 +108,7 @@ export default class DOFormSection extends Component<PropsType, StateType> {
 
     api
       .createProject("<token>", { name: projectName }, {})
-      .then(async (res) => {
+      .then(async res => {
         let proj = res.data;
 
         // Need to set project list for dropdown
@@ -117,7 +117,7 @@ export default class DOFormSection extends Component<PropsType, StateType> {
           "<token>",
           {},
           {
-            id: user.userId,
+            id: user.userId
           }
         );
         setProjects(res_1.data);
@@ -134,6 +134,7 @@ export default class DOFormSection extends Component<PropsType, StateType> {
     selectedInfras.forEach((option: { value: string; label: string }) => {
       redirectUrl += `&infras=${option.value}`;
     });
+    redirectUrl += "&tab=provisioner"
     window.location.href = redirectUrl;
   };
 
@@ -219,7 +220,7 @@ export default class DOFormSection extends Component<PropsType, StateType> {
             checked={this.state.provisionConfirmed}
             toggle={() =>
               this.setState({
-                provisionConfirmed: !this.state.provisionConfirmed,
+                provisionConfirmed: !this.state.provisionConfirmed
               })
             }
             label="I understand and wish to proceed"

+ 4 - 4
dashboard/src/main/home/provisioner/ExistingClusterSection.tsx

@@ -19,7 +19,7 @@ type StateType = {
 
 class ExistingClusterSection extends Component<PropsType, StateType> {
   state = {
-    buttonStatus: "",
+    buttonStatus: ""
   };
 
   onCreateProject = () => {
@@ -29,16 +29,16 @@ class ExistingClusterSection extends Component<PropsType, StateType> {
     this.setState({ buttonStatus: "loading" });
     api
       .createProject("<token>", { name: projectName }, {})
-      .then((res) =>
+      .then(res =>
         api.getProjects(
           "<token>",
           {},
           {
-            id: user.userId,
+            id: user.userId
           }
         )
       )
-      .then((res) => {
+      .then(res => {
         if (res.data) {
           setProjects(res.data);
           if (res.data.length > 0) {

+ 12 - 12
dashboard/src/main/home/provisioner/GCPFormSection.tsx

@@ -34,7 +34,7 @@ type StateType = {
 
 const provisionOptions = [
   { value: "gcr", label: "Google Container Registry (GCR)" },
-  { value: "gke", label: "Google Kubernetes Engine (GKE)" },
+  { value: "gke", label: "Google Kubernetes Engine (GKE)" }
 ];
 
 const regionOptions = [
@@ -61,7 +61,7 @@ const regionOptions = [
   { 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: "us-west4", label: "us-west4" }
 ];
 
 class GCPFormSection extends Component<PropsType, StateType> {
@@ -71,7 +71,7 @@ class GCPFormSection extends Component<PropsType, StateType> {
     gcpKeyData: "",
     selectedInfras: [...provisionOptions],
     buttonStatus: "",
-    provisionConfirmed: false,
+    provisionConfirmed: false
   };
 
   componentDidMount = () => {
@@ -127,7 +127,7 @@ class GCPFormSection extends Component<PropsType, StateType> {
 
     api
       .createProject("<token>", { name: projectName }, {})
-      .then((res) => {
+      .then(res => {
         let proj = res.data;
 
         // Need to set project list for dropdown
@@ -137,10 +137,10 @@ class GCPFormSection extends Component<PropsType, StateType> {
             "<token>",
             {},
             {
-              id: user.userId,
+              id: user.userId
             }
           )
-          .then((res) => {
+          .then(res => {
             setProjects(res.data);
             setCurrentProject(proj);
             callback && callback();
@@ -158,7 +158,7 @@ class GCPFormSection extends Component<PropsType, StateType> {
       .createGCR(
         "<token>",
         {
-          gcp_integration_id: id,
+          gcp_integration_id: id
         },
         { project_id: currentProject.id }
       )
@@ -176,11 +176,11 @@ class GCPFormSection extends Component<PropsType, StateType> {
         "<token>",
         {
           gke_name: clusterName,
-          gcp_integration_id: id,
+          gcp_integration_id: id
         },
         { project_id: currentProject.id }
       )
-      .then((res) => {
+      .then(res => {
         this.props.history.push("dashboard?tab=provisioner");
       })
       .catch(this.catchError);
@@ -195,11 +195,11 @@ class GCPFormSection extends Component<PropsType, StateType> {
         {
           gcp_region: gcpRegion,
           gcp_key_data: gcpKeyData,
-          gcp_project_id: gcpProjectId,
+          gcp_project_id: gcpProjectId
         },
         { project_id: currentProject.id }
       )
-      .then((res) => {
+      .then(res => {
         if (res?.data) {
           console.log("gcp provisioned with response: ", res.data);
           let { id } = res.data;
@@ -325,7 +325,7 @@ class GCPFormSection extends Component<PropsType, StateType> {
             checked={this.state.provisionConfirmed}
             toggle={() =>
               this.setState({
-                provisionConfirmed: !this.state.provisionConfirmed,
+                provisionConfirmed: !this.state.provisionConfirmed
               })
             }
             label="I understand and wish to proceed"

+ 1 - 1
dashboard/src/main/home/provisioner/InfraStatuses.tsx

@@ -56,7 +56,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-left: 10px;
 `;
 

+ 53 - 5
dashboard/src/main/home/provisioner/Provisioner.tsx

@@ -9,6 +9,8 @@ import Loading from "components/Loading";
 import InfraStatuses from "./InfraStatuses";
 import ProvisionerLogs from "./ProvisionerLogs";
 import { RouteComponentProps, withRouter } from "react-router";
+import { stringify } from "qs";
+import { forEach } from "lodash";
 
 type PropsType = RouteComponentProps & {};
 
@@ -36,7 +38,7 @@ class Provisioner extends Component<PropsType, StateType> {
     infras: [] as InfraType[],
     selectedInfra: null as InfraType,
     loading: true,
-    currentProject: this.context.currentProject,
+    currentProject: this.context.currentProject
   };
 
   selectInfra = (infra: InfraType) => {
@@ -44,6 +46,42 @@ class Provisioner extends Component<PropsType, StateType> {
   };
 
   componentDidMount() {
+    this.updateInfras()
+  }
+
+  componentDidUpdate(prevProps : PropsType, prevState : StateType) {
+    // Check that an infra that was previously in a non-created state, and 
+    // which was a cluster, is now in a created state. If so, propagate update
+    // so that cluster can be refreshed. 
+    let prevInfraStates : Record<number, string> = {}
+
+    prevState.infras.forEach((infra, i) => {
+      prevInfraStates[infra.id] = infra.status
+    })
+
+    this.state.infras.forEach((infra, i) => {
+      if (prevInfraStates[infra.id] && infra.status == "created" && prevInfraStates[infra.id] != "created") {
+        api
+        .getClusters("<token>", {}, { id: this.context.currentProject.id })
+        .then(res => {
+          this.context.setCurrentCluster(res.data[0])
+        })
+        .catch(err => {
+          this.context.setCurrentError(err)
+        });
+      }
+    })
+  }
+
+  refresh = () => {
+    this.updateInfras()
+  }
+
+  updateInfras = () => {
+    this.setState({
+      loading: true,
+    });
+
     let { currentProject } = this.state;
 
     api
@@ -51,10 +89,10 @@ class Provisioner extends Component<PropsType, StateType> {
         "<token>",
         {},
         {
-          project_id: currentProject.id,
+          project_id: currentProject.id
         }
       )
-      .then((res) => {
+      .then(res => {
         let infras = res.data.sort((a: InfraType, b: InfraType) => {
           return b.id - a.id;
         });
@@ -63,7 +101,7 @@ class Provisioner extends Component<PropsType, StateType> {
           error: false,
           infras,
           loading: false,
-          selectedInfra: infras[0],
+          selectedInfra: infras[0]
         });
       })
       .catch();
@@ -92,6 +130,7 @@ class Provisioner extends Component<PropsType, StateType> {
           <ProvisionerLogs
             key={this.state.selectedInfra?.id}
             selectedInfra={this.state.selectedInfra}
+            updateInfras={this.updateInfras}
           />
         </StyledProvisioner>
       );
@@ -99,7 +138,9 @@ class Provisioner extends Component<PropsType, StateType> {
 
     return (
       <StyledProvisioner>
-        You have not provisioned any resources for this project through Porter.
+        You have not provisioned any resources for this project through Porter. <RefreshText
+          onClick={this.refresh}
+        >Refresh</RefreshText>
       </StyledProvisioner>
     );
   }
@@ -128,3 +169,10 @@ const TabWrapper = styled.div`
   height: 100%;
   overflow-y: auto;
 `;
+
+const RefreshText = styled.div`
+  display: inline;
+  margin-left: 4px;
+  color: #8590ff;
+  cursor: pointer;
+`

+ 9 - 4
dashboard/src/main/home/provisioner/ProvisionerLogs.tsx

@@ -10,6 +10,7 @@ import warning from "assets/warning.png";
 
 type PropsType = RouteComponentProps & {
   selectedInfra: InfraType;
+  updateInfras: () => void;
 };
 
 type StateType = {
@@ -26,7 +27,7 @@ class ProvisionerLogs extends Component<PropsType, StateType> {
     ws: null as any,
     scroll: true,
     maxStep: 0,
-    error: false,
+    error: false
   };
 
   ws = null as any;
@@ -34,7 +35,7 @@ class ProvisionerLogs extends Component<PropsType, StateType> {
 
   scrollToBottom = () => {
     this.parentRef.current.lastElementChild.scrollIntoView({
-      behavior: "auto",
+      behavior: "auto"
     });
   };
 
@@ -138,7 +139,7 @@ class ProvisionerLogs extends Component<PropsType, StateType> {
 
       if (err) {
         window.analytics.track("Provisioning Error", {
-          error: err,
+          error: err
         });
         let e = ansiparse(err).map((el: any) => {
           return el.text;
@@ -164,12 +165,16 @@ class ProvisionerLogs extends Component<PropsType, StateType> {
       this.setState(
         {
           logs: [...this.state.logs, ...logs],
-          maxStep: validEvents[validEvents.length - 1]["total_resources"],
+          maxStep: validEvents[validEvents.length - 1]["total_resources"]
         },
         () => {
           this.scrollToBottom();
         }
       );
+
+      if (validEvents.length == parseInt(validEvents[validEvents.length - 1]["total_resources"])) {
+        this.props.updateInfras()
+      }
     };
 
     this.ws.onerror = (err: ErrorEvent) => {

+ 5 - 5
dashboard/src/main/home/provisioner/ProvisionerSettings.tsx

@@ -29,7 +29,7 @@ const providers = ["aws", "gcp", "do"];
 class NewProject extends Component<PropsType, StateType> {
   state = {
     selectedProvider: null as string | null,
-    infras: [] as InfraType[],
+    infras: [] as InfraType[]
   };
 
   // Handle any submission (pre-status) error
@@ -65,7 +65,7 @@ class NewProject extends Component<PropsType, StateType> {
                 <Highlight
                   onClick={() =>
                     this.setState({
-                      selectedProvider: "skipped",
+                      selectedProvider: "skipped"
                     })
                   }
                 >
@@ -227,7 +227,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`
@@ -268,13 +268,13 @@ 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;

Beberapa file tidak ditampilkan karena terlalu banyak file yang berubah dalam diff ini