ianedwards 2 лет назад
Родитель
Сommit
b461224797
100 измененных файлов с 2279 добавлено и 1883 удалено
  1. 53 10
      dashboard/react-table.d.ts
  2. 5 4
      dashboard/src/App.tsx
  3. 11 7
      dashboard/src/components/AWSCostConsent.tsx
  4. 102 93
      dashboard/src/components/AzureCredentialForm.tsx
  5. 5 3
      dashboard/src/components/Breadcrumb.tsx
  6. 1 1
      dashboard/src/components/Button.tsx
  7. 1 1
      dashboard/src/components/CopyToClipboard.tsx
  8. 66 62
      dashboard/src/components/CredentialsForm.tsx
  9. 1 2
      dashboard/src/components/DocsHelper.tsx
  10. 6 1
      dashboard/src/components/DynamicLink.tsx
  11. 7 3
      dashboard/src/components/ExpandableResource.tsx
  12. 11 11
      dashboard/src/components/GCPCostConsent.tsx
  13. 134 130
      dashboard/src/components/GCPCredentialsForm.tsx
  14. 266 214
      dashboard/src/components/GCPProvisionerSettings.tsx
  15. 11 15
      dashboard/src/components/GPUCostConsent.tsx
  16. 112 86
      dashboard/src/components/GPUProvisionSettings.tsx
  17. 1 0
      dashboard/src/components/Loading.tsx
  18. 11 7
      dashboard/src/components/LogQueryModeSelectionToggle.tsx
  19. 5 4
      dashboard/src/components/LogSearchBar.tsx
  20. 16 4
      dashboard/src/components/MultiSaveButton.tsx
  21. 3 3
      dashboard/src/components/OldPlaceholder.tsx
  22. 19 10
      dashboard/src/components/OldTable.tsx
  23. 0 1
      dashboard/src/components/PageIllustration.tsx
  24. 6 6
      dashboard/src/components/Placeholder.tsx
  25. 94 90
      dashboard/src/components/PreflightChecks.tsx
  26. 1 3
      dashboard/src/components/ProvisionerFlow.tsx
  27. 8 7
      dashboard/src/components/ProvisionerForm.tsx
  28. 7 3
      dashboard/src/components/ProvisionerSettings.tsx
  29. 12 14
      dashboard/src/components/ProvisionerStatus.tsx
  30. 3 1
      dashboard/src/components/RadioSelector.tsx
  31. 4 2
      dashboard/src/components/ResourceTab.tsx
  32. 8 2
      dashboard/src/components/SaveButton.tsx
  33. 7 5
      dashboard/src/components/SearchBar.tsx
  34. 34 18
      dashboard/src/components/Selector.tsx
  35. 1 1
      dashboard/src/components/TitleSection.tsx
  36. 2 2
      dashboard/src/components/YamlEditor.tsx
  37. 2 2
      dashboard/src/components/date-time-picker/DateTimePicker.tsx
  38. 6 4
      dashboard/src/components/date-time-picker/react-datepicker.css
  39. 3 1
      dashboard/src/components/form-components/CheckboxList.tsx
  40. 1 1
      dashboard/src/components/form-components/Heading.tsx
  41. 1 1
      dashboard/src/components/form-components/Helper.tsx
  42. 5 3
      dashboard/src/components/form-components/InputRow.tsx
  43. 25 13
      dashboard/src/components/form-components/KeyValueArray.tsx
  44. 4 4
      dashboard/src/components/form-components/SelectRow.tsx
  45. 1 0
      dashboard/src/components/form-components/UploadArea.tsx
  46. 29 29
      dashboard/src/components/image-selector/ImageList.tsx
  47. 29 31
      dashboard/src/components/image-selector/ImageSelector.tsx
  48. 24 17
      dashboard/src/components/image-selector/TagList.tsx
  49. 28 23
      dashboard/src/components/porter-form/FormDebugger.tsx
  50. 46 35
      dashboard/src/components/porter-form/PorterForm.tsx
  51. 108 97
      dashboard/src/components/porter-form/PorterFormContextProvider.tsx
  52. 2 2
      dashboard/src/components/porter-form/PorterFormWrapper.tsx
  53. 11 14
      dashboard/src/components/porter-form/field-components/ArrayInput.tsx
  54. 4 3
      dashboard/src/components/porter-form/field-components/Checkbox.tsx
  55. 6 4
      dashboard/src/components/porter-form/field-components/CronInput.tsx
  56. 17 17
      dashboard/src/components/porter-form/field-components/Dictionary.tsx
  57. 40 41
      dashboard/src/components/porter-form/field-components/DictionaryArray.tsx
  58. 12 15
      dashboard/src/components/porter-form/field-components/Input.tsx
  59. 73 54
      dashboard/src/components/porter-form/field-components/KeyValueArray.tsx
  60. 42 43
      dashboard/src/components/porter-form/field-components/ResourceList.tsx
  61. 19 15
      dashboard/src/components/porter-form/field-components/Select.tsx
  62. 2 1
      dashboard/src/components/porter-form/field-components/ServiceIPList.tsx
  63. 18 14
      dashboard/src/components/porter-form/field-components/ServiceRow.tsx
  64. 2 1
      dashboard/src/components/porter-form/field-components/TextAreaInput.tsx
  65. 2 1
      dashboard/src/components/porter-form/field-components/UrlLink.tsx
  66. 3 1
      dashboard/src/components/porter-form/field-components/VeleroForm.tsx
  67. 3 2
      dashboard/src/components/porter-form/hooks/useFormField.tsx
  68. 54 53
      dashboard/src/components/porter-form/types.ts
  69. 2 0
      dashboard/src/components/porter-form/utils.ts
  70. 6 8
      dashboard/src/components/porter/Banner.tsx
  71. 10 9
      dashboard/src/components/porter/ClickToCopy.tsx
  72. 28 24
      dashboard/src/components/porter/DictionaryEditor.tsx
  73. 42 37
      dashboard/src/components/porter/ExpandableSection.tsx
  74. 35 22
      dashboard/src/components/porter/Filter.tsx
  75. 124 110
      dashboard/src/components/porter/InputSlider.tsx
  76. 1 6
      dashboard/src/components/porter/Link.tsx
  77. 4 9
      dashboard/src/components/porter/LoadingBar.tsx
  78. 16 13
      dashboard/src/components/porter/Pagination.tsx
  79. 18 22
      dashboard/src/components/porter/SearchBar.tsx
  80. 1 1
      dashboard/src/components/porter/Select.tsx
  81. 8 14
      dashboard/src/components/porter/Spacer.tsx
  82. 5 10
      dashboard/src/components/porter/Step.tsx
  83. 5 5
      dashboard/src/components/porter/Text.tsx
  84. 10 4
      dashboard/src/components/porter/Toggle.tsx
  85. 19 19
      dashboard/src/components/porter/ToggleRow.tsx
  86. 16 18
      dashboard/src/components/porter/VerticalSteps.tsx
  87. 2 2
      dashboard/src/components/porter/collapsible-container.css
  88. 20 8
      dashboard/src/components/repo-selector/ActionConfEditor.tsx
  89. 19 13
      dashboard/src/components/repo-selector/ActionDetails.tsx
  90. 18 15
      dashboard/src/components/repo-selector/BranchList.tsx
  91. 22 8
      dashboard/src/components/repo-selector/BuildpackSelection.tsx
  92. 53 29
      dashboard/src/components/repo-selector/ContentsList.tsx
  93. 38 33
      dashboard/src/components/repo-selector/RepoList.tsx
  94. 4 1
      dashboard/src/lib/github/workflows.ts
  95. 3 2
      dashboard/src/lib/hooks/useAppAnalytics.ts
  96. 1 6
      dashboard/src/lib/hooks/useCloudSqlSecret.ts
  97. 5 1
      dashboard/src/lib/hooks/useDeploymentTarget.ts
  98. 69 62
      dashboard/src/lib/hooks/useGithubContents.ts
  99. 18 4
      dashboard/src/main/CurrentError.tsx
  100. 1 0
      dashboard/src/main/auth/LoginWrapper.tsx

+ 53 - 10
dashboard/react-table.d.ts

@@ -51,21 +51,64 @@ import {
 declare module "react-table" {
   // take this file as-is, or comment out the sections that don't apply to your plugin configuration
 
-  export type TableOptions<
-    D extends object = {}
-  > = {} & UseExpandedOptions<D> & UseFiltersOptions<D> & UseGlobalFiltersOptions<D> & UseGroupByOptions<D> & UsePaginationOptions<D> & UseResizeColumnsOptions<D> & UseRowSelectOptions<D> & UseRowStateOptions<D> & UseSortByOptions<D> & Record<string, any>
+  export type TableOptions<D extends object = {}> = {} & UseExpandedOptions<D> &
+    UseFiltersOptions<D> &
+    UseGlobalFiltersOptions<D> &
+    UseGroupByOptions<D> &
+    UsePaginationOptions<D> &
+    UseResizeColumnsOptions<D> &
+    UseRowSelectOptions<D> &
+    UseRowStateOptions<D> &
+    UseSortByOptions<D> &
+    Record<string, any>;
 
-  export type Hooks<D extends object = {}> = {} & UseExpandedHooks<D> & UseGroupByHooks<D> & UseRowSelectHooks<D> & UseSortByHooks<D>
+  export type Hooks<D extends object = {}> = {} & UseExpandedHooks<D> &
+    UseGroupByHooks<D> &
+    UseRowSelectHooks<D> &
+    UseSortByHooks<D>;
 
-  export type TableInstance<D extends object = {}> = {} & UseColumnOrderInstanceProps<D> & UseExpandedInstanceProps<D> & UseFiltersInstanceProps<D> & UseGlobalFiltersInstanceProps<D> & UseGroupByInstanceProps<D> & UsePaginationInstanceProps<D> & UseRowSelectInstanceProps<D> & UseRowStateInstanceProps<D> & UseSortByInstanceProps<D>
+  export type TableInstance<D extends object = {}> =
+    {} & UseColumnOrderInstanceProps<D> &
+      UseExpandedInstanceProps<D> &
+      UseFiltersInstanceProps<D> &
+      UseGlobalFiltersInstanceProps<D> &
+      UseGroupByInstanceProps<D> &
+      UsePaginationInstanceProps<D> &
+      UseRowSelectInstanceProps<D> &
+      UseRowStateInstanceProps<D> &
+      UseSortByInstanceProps<D>;
 
-  export type TableState<D extends object = {}> = {} & UseColumnOrderState<D> & UseExpandedState<D> & UseFiltersState<D> & UseGlobalFiltersState<D> & UseGroupByState<D> & UsePaginationState<D> & UseResizeColumnsState<D> & UseRowSelectState<D> & UseRowStateState<D> & UseSortByState<D>
+  export type TableState<D extends object = {}> = {} & UseColumnOrderState<D> &
+    UseExpandedState<D> &
+    UseFiltersState<D> &
+    UseGlobalFiltersState<D> &
+    UseGroupByState<D> &
+    UsePaginationState<D> &
+    UseResizeColumnsState<D> &
+    UseRowSelectState<D> &
+    UseRowStateState<D> &
+    UseSortByState<D>;
 
-  export type ColumnInterface<D extends object = {}> = {} & UseFiltersColumnOptions<D> & UseGlobalFiltersColumnOptions<D> & UseGroupByColumnOptions<D> & UseResizeColumnsColumnOptions<D> & UseSortByColumnOptions<D>
+  export type ColumnInterface<D extends object = {}> =
+    {} & UseFiltersColumnOptions<D> &
+      UseGlobalFiltersColumnOptions<D> &
+      UseGroupByColumnOptions<D> &
+      UseResizeColumnsColumnOptions<D> &
+      UseSortByColumnOptions<D>;
 
-  export type ColumnInstance<D extends object = {}> = {} & UseFiltersColumnProps<D> & UseGroupByColumnProps<D> & UseResizeColumnsColumnProps<D> & UseSortByColumnProps<D>
+  export type ColumnInstance<D extends object = {}> =
+    {} & UseFiltersColumnProps<D> &
+      UseGroupByColumnProps<D> &
+      UseResizeColumnsColumnProps<D> &
+      UseSortByColumnProps<D>;
 
-  export type Cell<D extends object = {}, V = any> = {} & UseGroupByCellProps<D> & UseRowStateCellProps<D>
+  export type Cell<
+    D extends object = {},
+    V = any,
+  > = {} & UseGroupByCellProps<D> & UseRowStateCellProps<D>;
 
-  export type Row<D extends object = {}> = {} & UseExpandedRowProps<D> & UseGroupByRowProps<D> & UseRowSelectRowProps<D> & UseRowStateRowProps<D>
+  export type Row<D extends object = {}> = {} & UseExpandedRowProps<D> &
+    UseGroupByRowProps<D> &
+    UseRowSelectRowProps<D> &
+    UseRowStateRowProps<D>;
 }

+ 5 - 4
dashboard/src/App.tsx

@@ -1,13 +1,14 @@
 import React, { Component } from "react";
-import { BrowserRouter } from "react-router-dom";
-import PorterErrorBoundary from "shared/error_handling/PorterErrorBoundary";
-import styled, { ThemeProvider, createGlobalStyle } from "styled-components";
 import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
+import { BrowserRouter } from "react-router-dom";
+import styled, { createGlobalStyle, ThemeProvider } from "styled-components";
 
-import MainWrapper from "./main/MainWrapper";
+import PorterErrorBoundary from "shared/error_handling/PorterErrorBoundary";
 import midnight from "shared/themes/midnight";
 import standard from "shared/themes/standard";
 
+import MainWrapper from "./main/MainWrapper";
+
 const queryClient = new QueryClient();
 
 export default class App extends Component {

+ 11 - 7
dashboard/src/components/AWSCostConsent.tsx

@@ -1,17 +1,17 @@
-import React, { useState, useContext } from "react";
+import React, { useContext, useState } from "react";
 import styled from "styled-components";
 
-import { Context } from "shared/Context";
 import api from "shared/api";
+import { Context } from "shared/Context";
 
-import Modal from "./porter/Modal";
-import Text from "./porter/Text";
-import Spacer from "./porter/Spacer";
-import Fieldset from "./porter/Fieldset";
 import Button from "./porter/Button";
 import ExpandableSection from "./porter/ExpandableSection";
+import Fieldset from "./porter/Fieldset";
 import Input from "./porter/Input";
 import Link from "./porter/Link";
+import Modal from "./porter/Modal";
+import Spacer from "./porter/Spacer";
+import Text from "./porter/Text";
 
 type Props = {
   setCurrentStep: (step: string) => void;
@@ -46,7 +46,11 @@ const AWSCostConsent: React.FC<Props> = ({
           noWrapper
           expandText="[+] Show details"
           collapseText="[-] Hide details"
-          Header={<Text size={20} weight={600}>$224.58 / mo</Text>}
+          Header={
+            <Text size={20} weight={600}>
+              $224.58 / mo
+            </Text>
+          }
           ExpandedSection={
             <>
               <Spacer height="15px" />

+ 102 - 93
dashboard/src/components/AzureCredentialForm.tsx

@@ -1,19 +1,18 @@
-import React, { useEffect, useState, useContext, useMemo } from "react";
+import React, { useContext, useEffect, useMemo, useState } from "react";
 import styled from "styled-components";
 import { v4 as uuidv4 } from "uuid";
 
 import api from "shared/api";
-import azure from "assets/azure.png";
-
 import { Context } from "shared/Context";
+import azure from "assets/azure.png";
 
-import Text from "./porter/Text";
-import Spacer from "./porter/Spacer";
-import Input from "./porter/Input";
 import Button from "./porter/Button";
+import Container from "./porter/Container";
 import Error from "./porter/Error";
+import Input from "./porter/Input";
 import Link from "./porter/Link";
-import Container from "./porter/Container";
+import Spacer from "./porter/Spacer";
+import Text from "./porter/Text";
 import VerticalSteps from "./porter/VerticalSteps";
 
 type Props = {
@@ -55,20 +54,18 @@ const AzureCredentialForm: React.FC<Props> = ({ goBack, proceed }) => {
           },
           {
             id: currentProject.id,
-          });
-        const azureIntegrationId = azureIntegrationResponse.data.cloud_provider_credentials_id;
+          }
+        );
+        const azureIntegrationId =
+          azureIntegrationResponse.data.cloud_provider_credentials_id;
         try {
           if (currentProject?.id != null) {
-            api.inviteAdmin(
-              "<token>",
-              {},
-              { project_id: currentProject?.id }
-            );
+            api.inviteAdmin("<token>", {}, { project_id: currentProject?.id });
           }
         } catch (err) {
           console.log(err);
         }
-        proceed(azureIntegrationId)
+        proceed(azureIntegrationId);
       } catch (err) {
         if (err.response?.data?.error) {
           setErrorMessage(err.response?.data?.error.replace("unknown: ", ""));
@@ -85,9 +82,7 @@ const AzureCredentialForm: React.FC<Props> = ({ goBack, proceed }) => {
     if (isLoading) {
       return "loading";
     } else if (errorMessage !== "") {
-      return <Error
-        message={errorMessage}
-      />;
+      return <Error message={errorMessage} />;
     } else {
       return null;
     }
@@ -96,83 +91,97 @@ const AzureCredentialForm: React.FC<Props> = ({ goBack, proceed }) => {
   const renderContent = () => {
     return (
       <VerticalSteps
-          onlyShowCurrentStep={true}
-          currentStep={currentStep}
-          steps={[
-            <>
-              <Text size={16}>Set up your Azure subscription</Text>
-              <Spacer y={.5} />
-              <Text color="helper">
-                Follow our <Link to="https://docs.porter.run/provision/provisioning-on-azure" target="_blank">documentation</Link> to create your service principal and prepare your subscription for use with Porter.
-              </Text>
-              <Spacer y={1} />
-              <Button onClick={() => { setCurrentStep(1); }}>
+        onlyShowCurrentStep={true}
+        currentStep={currentStep}
+        steps={[
+          <>
+            <Text size={16}>Set up your Azure subscription</Text>
+            <Spacer y={0.5} />
+            <Text color="helper">
+              Follow our{" "}
+              <Link
+                to="https://docs.porter.run/provision/provisioning-on-azure"
+                target="_blank"
+              >
+                documentation
+              </Link>{" "}
+              to create your service principal and prepare your subscription for
+              use with Porter.
+            </Text>
+            <Spacer y={1} />
+            <Button
+              onClick={() => {
+                setCurrentStep(1);
+              }}
+            >
+              Continue
+            </Button>
+          </>,
+          <>
+            <Text size={16}>Input Azure service principal credentials</Text>
+            <Spacer height="15px" />
+            <Text color="helper">
+              Provide the credentials for an Azure Service Principal authorized
+              on your Azure subscription.
+            </Text>
+            <Spacer y={1} />
+            <Input
+              label={<Flex>Subscription ID</Flex>}
+              value={subscriptionId}
+              setValue={(e) => {
+                setSubscriptionId(e.trim());
+              }}
+              placeholder="ex: 12345678-abcd-1234-abcd-12345678abcd"
+              width="100%"
+            />
+            <Spacer y={1} />
+            <Input
+              label={<Flex>App ID</Flex>}
+              value={clientId}
+              setValue={(e) => {
+                setClientId(e.trim());
+              }}
+              placeholder="ex: 12345678-abcd-1234-abcd-12345678abcd"
+              width="100%"
+            />
+            <Spacer y={1} />
+            <Input
+              type="password"
+              label={<Flex>Password</Flex>}
+              value={servicePrincipalKey}
+              setValue={(e) => {
+                setServicePrincipalKey(e.trim());
+              }}
+              placeholder="○ ○ ○ ○ ○ ○ ○ ○ ○"
+              width="100%"
+            />
+            <Spacer y={1} />
+            <Input
+              label={<Flex>Tenant ID</Flex>}
+              value={tenantId}
+              setValue={(e) => {
+                setTenantId(e.trim());
+              }}
+              placeholder="ex: 12345678-abcd-1234-abcd-12345678abcd"
+              width="100%"
+            />
+            <Spacer y={1} />
+            <Container row>
+              <Button
+                onClick={() => {
+                  setCurrentStep(0);
+                }}
+                color="#222222"
+              >
+                Back
+              </Button>
+              <Spacer inline x={0.5} />
+              <Button onClick={saveCredentials} status={getButtonStatus()}>
                 Continue
               </Button>
-            </>,
-            <>
-                <Text size={16}>
-                  Input Azure service principal credentials
-                </Text>
-                <Spacer height="15px" />
-                <Text color="helper">
-                  Provide the credentials for an Azure Service Principal authorized on
-                  your Azure subscription.
-                </Text>
-                <Spacer y={1} />
-                <Input
-                    label={<Flex>Subscription ID</Flex>}
-                    value={subscriptionId}
-                    setValue={(e) => {
-                      setSubscriptionId(e.trim());
-                    }}
-                    placeholder="ex: 12345678-abcd-1234-abcd-12345678abcd"
-                    width="100%"
-                />
-                <Spacer y={1} />
-                <Input
-                    label={<Flex>App ID</Flex>}
-                    value={clientId}
-                    setValue={(e) => {
-                      setClientId(e.trim());
-                    }}
-                    placeholder="ex: 12345678-abcd-1234-abcd-12345678abcd"
-                    width="100%"
-                />
-                <Spacer y={1} />
-                <Input
-                    type="password"
-                    label={<Flex>Password</Flex>}
-                    value={servicePrincipalKey}
-                    setValue={(e) => {
-                      setServicePrincipalKey(e.trim());
-                    }}
-                    placeholder="○ ○ ○ ○ ○ ○ ○ ○ ○"
-                    width="100%"
-                />
-                <Spacer y={1} />
-                <Input
-                    label={<Flex>Tenant ID</Flex>}
-                    value={tenantId}
-                    setValue={(e) => {
-                      setTenantId(e.trim());
-                    }}
-                    placeholder="ex: 12345678-abcd-1234-abcd-12345678abcd"
-                    width="100%"
-                />
-              <Spacer y={1} />
-              <Container row>
-                  <Button onClick={() => { setCurrentStep(0); }} color="#222222">Back</Button>
-                  <Spacer inline x={0.5} />
-                  <Button
-                    onClick={saveCredentials}
-                    status={getButtonStatus()}
-                  >
-                  Continue
-                  </Button>
-              </Container>
-            </>,
-          ]}
+            </Container>
+          </>,
+        ]}
       />
     );
   };

+ 5 - 3
dashboard/src/components/Breadcrumb.tsx

@@ -1,8 +1,8 @@
-import { Steps } from "main/home/onboarding/types";
 import React, { Fragment, useState } from "react";
-
 import styled from "styled-components";
 
+import { Steps } from "main/home/onboarding/types";
+
 type Props = {
   currentStep: string;
   steps: Array<{ value: string; label: string }>;
@@ -17,7 +17,9 @@ const Breadcrumb: React.FC<Props> = ({ currentStep, steps, onClickStep }) => {
           <Fragment key={i}>
             <Crumb
               bold={currentStep === step.value}
-              onClick={() => { onClickStep && onClickStep(step.value); }}
+              onClick={() => {
+                onClickStep && onClickStep(step.value);
+              }}
             >
               {step.label}
             </Crumb>

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

@@ -6,7 +6,7 @@ type Props = {
   children: React.ReactNode;
   onClick: () => void;
   className?: string;
-}
+};
 
 const Button: React.FC<Props> = ({
   children,

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

@@ -1,7 +1,7 @@
 // import ClipboardJS from "clipboard";
-import ClipboardJS from "clipboard";
 import React, { Component, type RefObject } from "react";
 import Tooltip from "@material-ui/core/Tooltip";
+import ClipboardJS from "clipboard";
 import styled from "styled-components";
 
 type PropsType = {

+ 66 - 62
dashboard/src/components/CredentialsForm.tsx

@@ -1,23 +1,23 @@
-import React, { useEffect, useState, useContext, useMemo } from "react";
+import React, { useContext, useEffect, useMemo, useState } from "react";
 import styled from "styled-components";
 
+import Heading from "components/form-components/Heading";
+import Button from "components/porter/Button";
+
 import api from "shared/api";
+import { Context } from "shared/Context";
+import addCircle from "assets/add-circle.png";
 import aws from "assets/aws.png";
 import credsIcon from "assets/creds.png";
-import addCircle from "assets/add-circle.png";
 
-import { Context } from "shared/Context";
-
-import Heading from "components/form-components/Heading";
 import Helper from "./form-components/Helper";
 import InputRow from "./form-components/InputRow";
-import SaveButton from "./SaveButton";
-import Button from "components/porter/Button";
 import Loading from "./Loading";
 import Error from "./porter/Error";
 import Modal from "./porter/Modal";
-import Text from "./porter/Text";
 import Spacer from "./porter/Spacer";
+import Text from "./porter/Text";
+import SaveButton from "./SaveButton";
 
 type Props = {
   goBack: () => void;
@@ -32,11 +32,7 @@ type AWSCredential = {
   aws_arn: string;
 };
 
-
-const CredentialsForm: React.FC<Props> = ({
-  goBack,
-  proceed,
-}) => {
+const CredentialsForm: React.FC<Props> = ({ goBack, proceed }) => {
   const { currentProject } = useContext(Context);
   const [awsCredentials, setAWSCredentials] = useState<AWSCredential[]>(null);
   const [isLoading, setIsLoading] = useState(true);
@@ -100,30 +96,30 @@ const CredentialsForm: React.FC<Props> = ({
       return (
         <>
           <CredentialList>
-            {
-              awsCredentials.map((cred: AWSCredential, i: number) => {
-                return (
-                  <Credential
-                    key={cred.id}
-                    isSelected={cred.id === selectedCredentials?.id}
-                    onClick={() => {
-                      if (cred.id === selectedCredentials?.id) {
-                        setSelectedCredentials(null);
-                      } else {
-                        setSelectedCredentials(cred);
-                      }
-                    }}
-                  >
-                    <Icon src={credsIcon} />
-                    <Name>{cred.aws_arn || "n/a"}</Name>
-                  </Credential>
-                );
-              })
-            }
-            <CreateRow onClick={() => {
-              setShowCreateForm(true);
-              setSelectedCredentials(null);
-            }}>
+            {awsCredentials.map((cred: AWSCredential, i: number) => {
+              return (
+                <Credential
+                  key={cred.id}
+                  isSelected={cred.id === selectedCredentials?.id}
+                  onClick={() => {
+                    if (cred.id === selectedCredentials?.id) {
+                      setSelectedCredentials(null);
+                    } else {
+                      setSelectedCredentials(cred);
+                    }
+                  }}
+                >
+                  <Icon src={credsIcon} />
+                  <Name>{cred.aws_arn || "n/a"}</Name>
+                </Credential>
+              );
+            })}
+            <CreateRow
+              onClick={() => {
+                setShowCreateForm(true);
+                setSelectedCredentials(null);
+              }}
+            >
               <Icon src={addCircle} />
               Add new AWS credentials
             </CreateRow>
@@ -131,7 +127,9 @@ const CredentialsForm: React.FC<Props> = ({
           <Br height="34px" />
           <SaveButton
             disabled={!selectedCredentials && true}
-            onClick={() => { proceed(selectedCredentials.aws_arn); }}
+            onClick={() => {
+              proceed(selectedCredentials.aws_arn);
+            }}
             clearPosition
             text="Continue"
           />
@@ -141,17 +139,21 @@ const CredentialsForm: React.FC<Props> = ({
     return (
       <>
         <StyledForm>
-          {
-            awsCredentials.length > 0 && (
-              <CloseButton onClick={() => { setShowCreateForm(false); }}>
-                <i className="material-icons">close</i>
-              </CloseButton>
-            )
-          }
+          {awsCredentials.length > 0 && (
+            <CloseButton
+              onClick={() => {
+                setShowCreateForm(false);
+              }}
+            >
+              <i className="material-icons">close</i>
+            </CloseButton>
+          )}
           <InputRow
             type="string"
             value={awsAccessKeyID}
-            setValue={(e: string) => { setAWSAccessKeyID(e); }}
+            setValue={(e: string) => {
+              setAWSAccessKeyID(e);
+            }}
             label="👤 AWS access ID"
             placeholder="ex: AKIAIOSFODNN7EXAMPLE"
             isRequired
@@ -160,7 +162,7 @@ const CredentialsForm: React.FC<Props> = ({
             type="password"
             value={awsSecretAccessKey}
             setValue={(e: string) => {
-              setAWSSecretAccessKey(e)
+              setAWSSecretAccessKey(e);
             }}
             label="🔒 AWS secret key"
             placeholder="○ ○ ○ ○ ○ ○ ○ ○ ○"
@@ -176,7 +178,7 @@ const CredentialsForm: React.FC<Props> = ({
         </Button>
       </>
     );
-  }
+  };
 
   return (
     <>
@@ -188,22 +190,24 @@ const CredentialsForm: React.FC<Props> = ({
         <HSpacer />
         <Img src={aws} />
         Set AWS credentials
-        <HelperButton onClick={() => window.open("https://docs.porter.run/standard/getting-started/provisioning-on-aws", "_blank")}>
+        <HelperButton
+          onClick={() =>
+            window.open(
+              "https://docs.porter.run/standard/getting-started/provisioning-on-aws",
+              "_blank"
+            )
+          }
+        >
           <i className="material-icons">help_outline</i>
         </HelperButton>
       </Text>
       <Spacer y={1} />
       <Text color="helper">
-        Select your credentials from the list below, or add a new set of credentials:
+        Select your credentials from the list below, or add a new set of
+        credentials:
       </Text>
       <Spacer y={1} />
-      {
-        isLoading ? (
-          <Loading height="150px" />
-        ) : (
-          renderContent()
-        )
-      }
+      {isLoading ? <Loading height="150px" /> : renderContent()}
     </>
   );
 };
@@ -267,13 +271,13 @@ const CreateRow = styled.div`
   padding: 20px;
   background: #ffffff11;
   :hover {
-    background: #ffffff18; 
+    background: #ffffff18;
   }
 `;
 
 const Br = styled.div<{ height?: string }>`
   width: 100%;
-  height: ${props => props.height || "20px"};
+  height: ${(props) => props.height || "20px"};
 `;
 
 const Img = styled.img`
@@ -319,11 +323,11 @@ const Credential = styled.div<{ isLast?: boolean; isSelected?: boolean }>`
   cursor: pointer;
   align-items: center;
   padding: 20px;
-  border-bottom: ${props => props.isLast ? "" : "1px solid #7a7b80"};
-  background: ${props => props.isSelected ? "#ffffff33" : "#ffffff11"};
+  border-bottom: ${(props) => (props.isLast ? "" : "1px solid #7a7b80")};
+  background: ${(props) => (props.isSelected ? "#ffffff33" : "#ffffff11")};
 
   :hover {
-    background: ${props => props.isSelected ? "" : "#ffffff18"}; 
+    background: ${(props) => (props.isSelected ? "" : "#ffffff18")};
   }
 `;
 

+ 1 - 2
dashboard/src/components/DocsHelper.tsx

@@ -1,7 +1,6 @@
 import React from "react";
-import styled from "styled-components";
-
 import { ClickAwayListener } from "@material-ui/core";
+import styled from "styled-components";
 
 type Props = {
   tooltipText: string;

+ 6 - 1
dashboard/src/components/DynamicLink.tsx

@@ -1,7 +1,12 @@
 import React from "react";
 import { Link, type LinkProps } from "react-router-dom";
 
-const DynamicLink: React.FC<LinkProps> = ({ to, children, hasunderline, ...props }) => {
+const DynamicLink: React.FC<LinkProps> = ({
+  to,
+  children,
+  hasunderline,
+  ...props
+}) => {
   // It is a simple element with nothing to link to
   if (!to) return <span {...props}>{children}</span>;
 

+ 7 - 3
dashboard/src/components/ExpandableResource.tsx

@@ -1,10 +1,12 @@
 import React, { Component, useContext, useEffect } from "react";
 import styled from "styled-components";
+
+import { baseApi } from "shared/baseApi";
 import { Context } from "shared/Context";
+import { readableDate } from "shared/string_utils";
+
 import ResourceTab from "./ResourceTab";
 import SaveButton from "./SaveButton";
-import { baseApi } from "shared/baseApi";
-import { readableDate } from "shared/string_utils";
 
 type Props = {
   resource: any;
@@ -49,7 +51,9 @@ const ExpandableResource: React.FC<Props> = (props) => {
       {}
     )
       .then((res) => {})
-      .catch((err) => { console.log(err); });
+      .catch((err) => {
+        console.log(err);
+      });
   };
 
   return (

+ 11 - 11
dashboard/src/components/GCPCostConsent.tsx

@@ -1,17 +1,17 @@
-import React, { useState, useContext } from "react";
+import React, { useContext, useState } from "react";
 import styled from "styled-components";
 
-import { Context } from "shared/Context";
 import api from "shared/api";
+import { Context } from "shared/Context";
 
-import Modal from "./porter/Modal";
-import Text from "./porter/Text";
-import Spacer from "./porter/Spacer";
-import Fieldset from "./porter/Fieldset";
 import Button from "./porter/Button";
 import ExpandableSection from "./porter/ExpandableSection";
+import Fieldset from "./porter/Fieldset";
 import Input from "./porter/Input";
 import Link from "./porter/Link";
+import Modal from "./porter/Modal";
+import Spacer from "./porter/Spacer";
+import Text from "./porter/Text";
 
 type Props = {
   setCurrentStep: (step: string) => void;
@@ -38,9 +38,9 @@ const GCPCostConsent: React.FC<Props> = ({
         <Text size={16}>Base GCP cost consent</Text>
         <Spacer height="15px" />
         <Text color="helper">
-          Porter will create the underlying infrastructure in your own GCP project.
-          You will be separately charged by GCP for this infrastructure.
-          The cost for this base infrastructure is as follows:
+          Porter will create the underlying infrastructure in your own GCP
+          project. You will be separately charged by GCP for this
+          infrastructure. The cost for this base infrastructure is as follows:
         </Text>
         <Spacer y={1} />
         <ExpandableSection
@@ -93,8 +93,8 @@ const GCPCostConsent: React.FC<Props> = ({
         <Spacer y={0.5} />
         <Text color="helper">
           All GCP resources will be automatically deleted when you delete your
-          Porter project. Please enter the GCP base cost ("{costTotal}") below to
-          proceed:
+          Porter project. Please enter the GCP base cost ("{costTotal}") below
+          to proceed:
         </Text>
         <Spacer y={1} />
         <Input

+ 134 - 130
dashboard/src/components/GCPCredentialsForm.tsx

@@ -1,21 +1,22 @@
-import React, { useContext, useState, useEffect } from "react";
-import gcp from "assets/gcp.png";
-
-import { Context } from "shared/Context";
-import api from "shared/api";
+import React, { useContext, useEffect, useState } from "react";
 import styled from "styled-components";
-import Loading from "components/Loading";
-import Placeholder from "components/OldPlaceholder";
+
 import Helper from "components/form-components/Helper";
 import UploadArea from "components/form-components/UploadArea";
-import Text from "components/porter/Text";
+import Loading from "components/Loading";
+import Placeholder from "components/OldPlaceholder";
 import Button from "components/porter/Button";
-import Spacer from "./porter/Spacer";
-import Container from "./porter/Container";
-import VerticalSteps from "./porter/VerticalSteps";
-import Link from "./porter/Link";
+import Text from "components/porter/Text";
 import { Flex } from "main/home/cluster-dashboard/stacks/components/styles";
 
+import api from "shared/api";
+import { Context } from "shared/Context";
+import gcp from "assets/gcp.png";
+
+import Container from "./porter/Container";
+import Link from "./porter/Link";
+import Spacer from "./porter/Spacer";
+import VerticalSteps from "./porter/VerticalSteps";
 
 type Props = {
   goBack: () => void;
@@ -30,27 +31,26 @@ const GCPCredentialsForm: React.FC<Props> = ({ goBack, proceed }) => {
   const [isLoading, setIsLoading] = useState(false);
   const [errorMessage, setErrorMessage] = useState("");
   const [detected, setDetected] = useState<Detected | undefined>(undefined);
-  const [gcpCloudProviderCredentialID, setGCPCloudProviderCredentialId] = useState<string>("")
+  const [gcpCloudProviderCredentialID, setGCPCloudProviderCredentialId] =
+    useState<string>("");
   const [step, setStep] = useState(0);
   useEffect(() => {
     setDetected(undefined);
   }, []);
 
   useEffect(() => {
-
-    gcpIntegration()
-
-  }, [detected])
+    gcpIntegration();
+  }, [detected]);
   type FailureState = {
     condition: boolean;
     errorMessage: string;
-  }
+  };
   const failureStates: FailureState[] = [
     {
       condition: currentProject == null,
       errorMessage: "Project ID is required",
     },
-  ]
+  ];
 
   type Detected = {
     detected: boolean;
@@ -62,7 +62,7 @@ const GCPCredentialsForm: React.FC<Props> = ({ goBack, proceed }) => {
       if (failureState.condition) {
         setErrorMessage(failureState.errorMessage);
       }
-    })
+    });
     setIsLoading(true);
 
     try {
@@ -74,16 +74,20 @@ const GCPCredentialsForm: React.FC<Props> = ({ goBack, proceed }) => {
         },
         {
           project_id: currentProject.id,
-        });
+        }
+      );
       if (gcpIntegrationResponse.data.cloud_provider_credentials_id == "") {
-        setErrorMessage("Unable to store cluster credentials. Please try again later. If the problem persists, contact support@porter.run")
+        setErrorMessage(
+          "Unable to store cluster credentials. Please try again later. If the problem persists, contact support@porter.run"
+        );
         return;
       }
-      setGCPCloudProviderCredentialId(gcpIntegrationResponse.data.cloud_provider_credentials_id)
-      setIsLoading(false)
-    }
-    catch (err) {
-      setIsLoading(false)
+      setGCPCloudProviderCredentialId(
+        gcpIntegrationResponse.data.cloud_provider_credentials_id
+      );
+      setIsLoading(false);
+    } catch (err) {
+      setIsLoading(false);
 
       if (err.response?.data?.error) {
         setErrorMessage(err.response?.data?.error.replace("unknown: ", ""));
@@ -91,40 +95,33 @@ const GCPCredentialsForm: React.FC<Props> = ({ goBack, proceed }) => {
         setErrorMessage("Something went wrong, please try again later.");
       }
     }
-
-  }
-
+  };
 
   const saveCredentials = async () => {
     if (gcpCloudProviderCredentialID) {
       try {
         if (currentProject?.id != null) {
-          api.inviteAdmin(
-            "<token>",
-            {},
-            { project_id: currentProject?.id }
-          );
+          api.inviteAdmin("<token>", {}, { project_id: currentProject?.id });
         }
       } catch (err) {
         console.log(err);
       }
-      proceed(gcpCloudProviderCredentialID)
+      proceed(gcpCloudProviderCredentialID);
     }
-
-  }
+  };
 
   const handleLoadJSON = (serviceAccountJSONFile: string) => {
-    setServiceAccountKey(serviceAccountJSONFile)
+    setServiceAccountKey(serviceAccountJSONFile);
     const serviceAccountCredentials = JSON.parse(serviceAccountJSONFile);
 
     if (!serviceAccountCredentials.project_id) {
       setIsContinueEnabled(false);
-      setProjectId("")
+      setProjectId("");
       setDetected({
         detected: false,
         message: `Invalid GCP service account credentials. No project ID detected in uploaded file. Please try again.`,
       });
-      return
+      return;
     }
 
     setProjectId(serviceAccountCredentials.project_id);
@@ -133,11 +130,11 @@ const GCPCredentialsForm: React.FC<Props> = ({ goBack, proceed }) => {
       message: `Your cluster will be provisioned in Google Project: ${serviceAccountCredentials.project_id}`,
     });
     setIsContinueEnabled(true);
-  }
+  };
 
   const incrementStep = () => {
-    setStep(step + 1)
-  }
+    setStep(step + 1);
+  };
 
   return (
     <>
@@ -156,18 +153,27 @@ const GCPCredentialsForm: React.FC<Props> = ({ goBack, proceed }) => {
         steps={[
           <>
             <Text size={16}> Create the service account </Text>
-            <Spacer y={.5} />
-            <Link onClick={incrementStep} to="https://docs.porter.run/provision/provisioning-on-gcp" target="_blank">
-              Follow the steps in the Porter docs to generate your service account credentials
+            <Spacer y={0.5} />
+            <Link
+              onClick={incrementStep}
+              to="https://docs.porter.run/provision/provisioning-on-gcp"
+              target="_blank"
+            >
+              Follow the steps in the Porter docs to generate your service
+              account credentials
             </Link>
-            <Spacer y={.5} />
-            <Button onClick={incrementStep} height={"15px"} disabled={step > 1}>Continue</Button>
+            <Spacer y={0.5} />
+            <Button onClick={incrementStep} height={"15px"} disabled={step > 1}>
+              Continue
+            </Button>
           </>,
           <>
             <Text size={16}>Upload service account credentials</Text>
             <Spacer y={1} />
             <UploadArea
-              setValue={(x: string) => { handleLoadJSON(x); }}
+              setValue={(x: string) => {
+                handleLoadJSON(x);
+              }}
               label="🔒 GCP Key Data (JSON)"
               placeholder="Drag a GCP Service Account JSON here, or click to browse."
               width="100%"
@@ -175,115 +181,113 @@ const GCPCredentialsForm: React.FC<Props> = ({ goBack, proceed }) => {
               isRequired={true}
             />
 
-            {detected && serviceAccountKey && (<>
+            {detected && serviceAccountKey && (
               <>
-                <AppearingDiv color={projectId ? "#8590ff" : "#fcba03"}>
-                  {detected.detected ? (
-                    <>
-                      {incrementStep}
-                      <I className="material-icons">check</I>
-                    </>
-                  ) : (
-                    <I className="material-icons">error</I>
-                  )}
-
-                  <Text color={detected.detected ? "#8590ff" : "#fcba03"}>
-                    {detected.message}
-                  </Text>
-                </AppearingDiv>
-                <Spacer y={1} />
+                <>
+                  <AppearingDiv color={projectId ? "#8590ff" : "#fcba03"}>
+                    {detected.detected ? (
+                      <>
+                        {incrementStep}
+                        <I className="material-icons">check</I>
+                      </>
+                    ) : (
+                      <I className="material-icons">error</I>
+                    )}
+
+                    <Text color={detected.detected ? "#8590ff" : "#fcba03"}>
+                      {detected.message}
+                    </Text>
+                  </AppearingDiv>
+                  <Spacer y={1} />
+                </>
               </>
-            </>
             )}
             <Spacer y={0.5} />
-            <Button
-              disabled={!isContinueEnabled}
-              onClick={saveCredentials}
-            >Continue</Button>
-          </>
+            <Button disabled={!isContinueEnabled} onClick={saveCredentials}>
+              Continue
+            </Button>
+          </>,
         ].filter((x) => x)}
       />
     </>
   );
 };
 
-
 export default GCPCredentialsForm;
 
 const BackButton = styled.div`
-      display: flex;
-      align-items: center;
-      justify-content: space-between;
-      cursor: pointer;
-      font-size: 13px;
-      height: 35px;
-      padding: 5px 13px;
-      padding-right: 15px;
-      border: 1px solid #ffffff55;
-      border-radius: 100px;
-      width: ${(props: { width: string }) => props.width};
-      color: white;
-      background: #ffffff11;
-
-      :hover {
-        background: #ffffff22;
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  cursor: pointer;
+  font-size: 13px;
+  height: 35px;
+  padding: 5px 13px;
+  padding-right: 15px;
+  border: 1px solid #ffffff55;
+  border-radius: 100px;
+  width: ${(props: { width: string }) => props.width};
+  color: white;
+  background: #ffffff11;
+
+  :hover {
+    background: #ffffff22;
   }
 
   > i {
-        color: white;
-      font-size: 16px;
-      margin-right: 6px;
-      margin-left: -2px;
+    color: white;
+    font-size: 16px;
+    margin-right: 6px;
+    margin-left: -2px;
   }
-      `;
+`;
 
 const HelperButton = styled.div`
-      cursor: pointer;
-      display: flex;
-      align-items: center;
-      margin-left: 10px;
-      justify-content: center;
+  cursor: pointer;
+  display: flex;
+  align-items: center;
+  margin-left: 10px;
+  justify-content: center;
   > i {
-        color: #aaaabb;
-      width: 24px;
-      height: 24px;
-      font-size: 20px;
-      border-radius: 20px;
+    color: #aaaabb;
+    width: 24px;
+    height: 24px;
+    font-size: 20px;
+    border-radius: 20px;
   }
-      `;
+`;
 
 const Img = styled.img`
-      height: 18px;
-      margin-right: 15px;
-      `;
+  height: 18px;
+  margin-right: 15px;
+`;
 
 const AppearingDiv = styled.div<{ color?: string }>`
-        animation: floatIn 0.5s;
-        animation-fill-mode: forwards;
-        display: flex;
-        align-items: center;
-        color: ${(props) => props.color || "#ffffff44"};
-        margin-left: 10px;
-        @keyframes floatIn {
-          from {
-          opacity: 0;
-        transform: translateY(20px);
+  animation: floatIn 0.5s;
+  animation-fill-mode: forwards;
+  display: flex;
+  align-items: center;
+  color: ${(props) => props.color || "#ffffff44"};
+  margin-left: 10px;
+  @keyframes floatIn {
+    from {
+      opacity: 0;
+      transform: translateY(20px);
     }
-        to {
-          opacity: 1;
-        transform: translateY(0px);
+    to {
+      opacity: 1;
+      transform: translateY(0px);
     }
   }
-        `;
+`;
 
 const I = styled.i`
-        font-size: 18px;
-        margin-right: 5px;
-        `;
+  font-size: 18px;
+  margin-right: 5px;
+`;
 
 const StatusIcon = styled.img`
-        top: 20px;
-        right: 20px;
-        height: 18px;
-        `;
-
+  top: 20px;
+  right: 20px;
+  height: 18px;
+`;

+ 266 - 214
dashboard/src/components/GCPProvisionerSettings.tsx

@@ -1,48 +1,48 @@
-import React, { useEffect, useState, useContext } from "react";
-import styled from "styled-components";
-import { type RouteComponentProps, withRouter } from "react-router";
-
-import { OFState } from "main/home/onboarding/state";
-import api from "shared/api";
-import { Context } from "shared/Context";
-import { pushFiltered } from "shared/routing";
-
-import SelectRow from "components/form-components/SelectRow";
-import Heading from "components/form-components/Heading";
-import Helper from "components/form-components/Helper";
-import InputRow from "./form-components/InputRow";
+import { log } from "console";
+import React, { useContext, useEffect, useState } from "react";
 import {
+  Cluster,
   Contract,
-  EnumKubernetesKind,
   EnumCloudProvider,
-  Cluster,
+  EnumKubernetesKind,
   GKE,
   GKENetwork,
   GKENodePool,
   GKENodePoolType,
   GKEPreflightValues,
-  PreflightCheckRequest
+  PreflightCheckRequest,
 } from "@porter-dev/api-contracts";
+import { withRouter, type RouteComponentProps } from "react-router";
+import styled from "styled-components";
+
+import Heading from "components/form-components/Heading";
+import Helper from "components/form-components/Helper";
+import SelectRow from "components/form-components/SelectRow";
+import Loading from "components/Loading";
+import { OFState } from "main/home/onboarding/state";
+import { useIntercom } from "lib/hooks/useIntercom";
+
+import api from "shared/api";
+import { Context } from "shared/Context";
+import { pushFiltered } from "shared/routing";
 import { type ClusterType } from "shared/types";
+import failure from "assets/failure.svg";
+import healthy from "assets/status-healthy.png";
+
+import InputRow from "./form-components/InputRow";
+import Placeholder from "./Placeholder";
 import Button from "./porter/Button";
 import Error from "./porter/Error";
+import ExpandableSection from "./porter/ExpandableSection";
+import Fieldset from "./porter/Fieldset";
+import InputSlider from "./porter/InputSlider";
+import Link from "./porter/Link";
+import Select from "./porter/Select";
 import Spacer from "./porter/Spacer";
 import Step from "./porter/Step";
-import Link from "./porter/Link";
 import Text from "./porter/Text";
-import healthy from "assets/status-healthy.png";
-import failure from "assets/failure.svg";
-import Loading from "components/Loading";
-import Placeholder from "./Placeholder";
-import Fieldset from "./porter/Fieldset";
-import ExpandableSection from "./porter/ExpandableSection";
-import PreflightChecks from "./PreflightChecks";
 import VerticalSteps from "./porter/VerticalSteps";
-import { useIntercom } from "lib/hooks/useIntercom";
-import { log } from "console";
-import InputSlider from "./porter/InputSlider";
-import Select from "./porter/Select";
-
+import PreflightChecks from "./PreflightChecks";
 
 const locationOptions = [
   { value: "us-east1", label: "us-east1 (South Carolina, USA)" },
@@ -88,7 +88,7 @@ const instanceTypes = [
 ];
 
 const gpuMachineTypeOptions = [
-  { value: "n1-standard-1", label: "n1-standard-1" }, // start of GPU nodes. 
+  { value: "n1-standard-1", label: "n1-standard-1" }, // start of GPU nodes.
   { value: "n1-standard-2", label: "n1-standard-2" },
   { value: "n1-standard-4", label: "n1-standard-4" },
   { value: "n1-standard-8", label: "n1-standard-8" },
@@ -104,7 +104,6 @@ const gpuMachineTypeOptions = [
   { value: "n1-highcpu-32", label: "n1-highcpu-32" },
 ];
 
-
 const clusterVersionOptions = [{ value: "1.27", label: "v1.27" }];
 
 type Props = RouteComponentProps & {
@@ -115,8 +114,8 @@ type Props = RouteComponentProps & {
   gpuModal?: boolean;
 };
 
-const VALID_CIDR_RANGE_PATTERN = /^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\/(8|9|1\d|2[0-8])$/;
-
+const VALID_CIDR_RANGE_PATTERN =
+  /^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\/(8|9|1\d|2[0-8])$/;
 
 const GCPProvisionerSettings: React.FC<Props> = (props) => {
   const {
@@ -132,18 +131,22 @@ const GCPProvisionerSettings: React.FC<Props> = (props) => {
   const [region, setRegion] = useState(locationOptions[0].value);
   const [minInstances, setMinInstances] = useState(1);
   const [maxInstances, setMaxInstances] = useState(10);
-  const [clusterNetworking, setClusterNetworking] = useState(defaultClusterNetworking);
-  const [clusterVersion, setClusterVersion] = useState(clusterVersionOptions[0].value);
+  const [clusterNetworking, setClusterNetworking] = useState(
+    defaultClusterNetworking
+  );
+  const [clusterVersion, setClusterVersion] = useState(
+    clusterVersionOptions[0].value
+  );
   const [instanceType, setInstanceType] = useState(instanceTypes[0].value);
   const [isReadOnly, setIsReadOnly] = useState(false);
   const [errorMessage, setErrorMessage] = useState<string>("");
   const [errorDetails, setErrorDetails] = useState<string>("");
   const [isClicked, setIsClicked] = useState(false);
-  const [preflightData, setPreflightData] = useState(null)
-  const [preflightFailed, setPreflightFailed] = useState<boolean>(true)
+  const [preflightData, setPreflightData] = useState(null);
+  const [preflightFailed, setPreflightFailed] = useState<boolean>(true);
   const [isLoading, setIsLoading] = useState(false);
   const [isExpanded, setIsExpanded] = useState(false);
-  const [preflightError, setPreflightError] = useState<string>("")
+  const [preflightError, setPreflightError] = useState<string>("");
   const [gpuMinInstances, setGpuMinInstances] = useState(1);
   const [gpuMaxInstances, setGpuMaxInstances] = useState(5);
   const [gpuInstanceType, setGpuInstanceType] = useState("n1-standard-1");
@@ -152,9 +155,13 @@ const GCPProvisionerSettings: React.FC<Props> = (props) => {
 
   const markStepStarted = async (step: string, region?: string) => {
     try {
-      await api.updateOnboardingStep("<token>", { step, provider: "gcp", region }, {
-        project_id: currentProject.id,
-      });
+      await api.updateOnboardingStep(
+        "<token>",
+        { step, provider: "gcp", region },
+        {
+          project_id: currentProject.id,
+        }
+      );
     } catch (err) {
       console.log(err);
     }
@@ -162,14 +169,18 @@ const GCPProvisionerSettings: React.FC<Props> = (props) => {
 
   const getStatus = () => {
     if (isLoading) {
-      return <Loading />
+      return <Loading />;
     }
     if (isReadOnly && props.provisionerError == "") {
       return "Provisioning is still in progress...";
     } else if (errorMessage !== "") {
       return (
         <Error
-          message={errorDetails !== "" ? errorMessage + " (" + errorDetails + ")" : errorMessage}
+          message={
+            errorDetails !== ""
+              ? errorMessage + " (" + errorDetails + ")"
+              : errorMessage
+          }
           ctaText={
             errorMessage !== DEFAULT_ERROR_MESSAGE
               ? "Troubleshooting steps"
@@ -184,12 +195,12 @@ const GCPProvisionerSettings: React.FC<Props> = (props) => {
 
   const isDisabled = () => {
     return (
-      (!clusterName && true)
-      || (isReadOnly && props.provisionerError === "")
-      || currentCluster?.status === "UPDATING"
-      || isClicked
-      || (!currentProject?.enable_reprovision && props.clusterId)
-    )
+      (!clusterName && true) ||
+      (isReadOnly && props.provisionerError === "") ||
+      currentCluster?.status === "UPDATING" ||
+      isClicked ||
+      (!currentProject?.enable_reprovision && props.clusterId)
+    );
   };
 
   const validateInputs = (): string => {
@@ -199,85 +210,108 @@ const GCPProvisionerSettings: React.FC<Props> = (props) => {
     if (!region) {
       return "GCP region is required";
     }
-    if (!clusterNetworking.cidrRange || !clusterNetworking.controlPlaneCidr || !clusterNetworking.podCidr || !clusterNetworking.serviceCidr) {
+    if (
+      !clusterNetworking.cidrRange ||
+      !clusterNetworking.controlPlaneCidr ||
+      !clusterNetworking.podCidr ||
+      !clusterNetworking.serviceCidr
+    ) {
       return "CIDR ranges are required";
     }
-    if (!VALID_CIDR_RANGE_PATTERN.test(clusterNetworking.cidrRange) || !VALID_CIDR_RANGE_PATTERN.test(clusterNetworking.controlPlaneCidr) || !VALID_CIDR_RANGE_PATTERN.test(clusterNetworking.podCidr) || !VALID_CIDR_RANGE_PATTERN.test(clusterNetworking.serviceCidr)) {
+    if (
+      !VALID_CIDR_RANGE_PATTERN.test(clusterNetworking.cidrRange) ||
+      !VALID_CIDR_RANGE_PATTERN.test(clusterNetworking.controlPlaneCidr) ||
+      !VALID_CIDR_RANGE_PATTERN.test(clusterNetworking.podCidr) ||
+      !VALID_CIDR_RANGE_PATTERN.test(clusterNetworking.serviceCidr)
+    ) {
       return "CIDR ranges must be in the format of [0-255].[0-255].0.0/16";
     }
 
     return "";
-  }
+  };
   const renderAdvancedSettings = () => {
     return (
       <>
         {
-          < Heading >
+          <Heading>
             <ExpandHeader
-              onClick={() => { setIsExpanded(!isExpanded); }}
+              onClick={() => {
+                setIsExpanded(!isExpanded);
+              }}
               isExpanded={isExpanded}
             >
               <i className="material-icons">arrow_drop_down</i>
               Advanced settings
             </ExpandHeader>
-          </Heading >
+          </Heading>
         }
-        {
-          isExpanded && (
-            <>
-              <SelectRow
-                options={clusterVersionOptions}
-                width="350px"
-                disabled={isReadOnly}
-                value={clusterVersion}
-                scrollBuffer={true}
-                dropdownMaxHeight="240px"
-                setActiveValue={setClusterVersion}
-                label="Cluster version"
-              />
-              <Spacer y={0.25} />
-
-              <SelectRow
-                options={instanceTypes}
-                width="350px"
-                disabled={isReadOnly}
-                value={instanceType}
-                scrollBuffer={true}
-                dropdownMaxHeight="240px"
-                setActiveValue={setInstanceType}
-                label="Instance Type"
-              />
-              <Spacer y={0.25} />
-
-              <InputRow
-                width="350px"
-                type="string"
-                disabled={isReadOnly}
-                value={clusterNetworking.cidrRange}
-                setValue={(x: string) => { setClusterNetworking(new GKENetwork({ ...clusterNetworking, cidrRange: x })); }}
-                label="VPC CIDR range"
-                placeholder="ex: 10.78.0.0/16"
-              />
-              {
-                <Heading>
-                  <ExpandHeader
-                    onClick={() => {
-                      setAdvancedCidrs(!expandAdvancedCidrs);
-                    }}
-                    isExpanded={expandAdvancedCidrs}
-                  >
-                    <i className="material-icons">arrow_drop_down</i>
-                    Advanced CIDR settings
-                  </ExpandHeader>
-                </Heading>
-              }
-              {expandAdvancedCidrs && <>
+        {isExpanded && (
+          <>
+            <SelectRow
+              options={clusterVersionOptions}
+              width="350px"
+              disabled={isReadOnly}
+              value={clusterVersion}
+              scrollBuffer={true}
+              dropdownMaxHeight="240px"
+              setActiveValue={setClusterVersion}
+              label="Cluster version"
+            />
+            <Spacer y={0.25} />
+
+            <SelectRow
+              options={instanceTypes}
+              width="350px"
+              disabled={isReadOnly}
+              value={instanceType}
+              scrollBuffer={true}
+              dropdownMaxHeight="240px"
+              setActiveValue={setInstanceType}
+              label="Instance Type"
+            />
+            <Spacer y={0.25} />
+
+            <InputRow
+              width="350px"
+              type="string"
+              disabled={isReadOnly}
+              value={clusterNetworking.cidrRange}
+              setValue={(x: string) => {
+                setClusterNetworking(
+                  new GKENetwork({ ...clusterNetworking, cidrRange: x })
+                );
+              }}
+              label="VPC CIDR range"
+              placeholder="ex: 10.78.0.0/16"
+            />
+            {
+              <Heading>
+                <ExpandHeader
+                  onClick={() => {
+                    setAdvancedCidrs(!expandAdvancedCidrs);
+                  }}
+                  isExpanded={expandAdvancedCidrs}
+                >
+                  <i className="material-icons">arrow_drop_down</i>
+                  Advanced CIDR settings
+                </ExpandHeader>
+              </Heading>
+            }
+            {expandAdvancedCidrs && (
+              <>
                 <InputRow
                   width="350px"
                   type="string"
                   disabled={isReadOnly}
                   value={clusterNetworking.controlPlaneCidr}
-                  setValue={(x: string) => { setClusterNetworking(new GKENetwork({ ...clusterNetworking, controlPlaneCidr: x })); }}
+                  setValue={(x: string) => {
+                    setClusterNetworking(
+                      new GKENetwork({
+                        ...clusterNetworking,
+                        controlPlaneCidr: x,
+                      })
+                    );
+                  }}
                   label="Control Plane CIDR range"
                   placeholder="ex: 10.78.0.0/16"
                 />
@@ -286,7 +320,11 @@ const GCPProvisionerSettings: React.FC<Props> = (props) => {
                   type="string"
                   disabled={isReadOnly}
                   value={clusterNetworking.podCidr}
-                  setValue={(x: string) => { setClusterNetworking(new GKENetwork({ ...clusterNetworking, podCidr: x })); }}
+                  setValue={(x: string) => {
+                    setClusterNetworking(
+                      new GKENetwork({ ...clusterNetworking, podCidr: x })
+                    );
+                  }}
                   label="Pod CIDR range"
                   placeholder="ex: 10.78.0.0/16"
                 />
@@ -295,37 +333,44 @@ const GCPProvisionerSettings: React.FC<Props> = (props) => {
                   type="string"
                   disabled={isReadOnly}
                   value={clusterNetworking.serviceCidr}
-                  setValue={(x: string) => { setClusterNetworking(new GKENetwork({ ...clusterNetworking, serviceCidr: x })); }}
+                  setValue={(x: string) => {
+                    setClusterNetworking(
+                      new GKENetwork({ ...clusterNetworking, serviceCidr: x })
+                    );
+                  }}
                   label="Service CIDR range"
                   placeholder="ex: 10.78.0.0/16"
                 />
                 <Spacer y={0.25} />
-                <Text color="helper">The following ranges will be used: {clusterNetworking.cidrRange}, {clusterNetworking.controlPlaneCidr}, {clusterNetworking.serviceCidr}, {clusterNetworking.podCidr}</Text>
+                <Text color="helper">
+                  The following ranges will be used:{" "}
+                  {clusterNetworking.cidrRange},{" "}
+                  {clusterNetworking.controlPlaneCidr},{" "}
+                  {clusterNetworking.serviceCidr}, {clusterNetworking.podCidr}
+                </Text>
               </>
-              }
-            </>
-          )
-        }
+            )}
+          </>
+        )}
       </>
     );
   };
 
   const statusPreflight = (): string => {
-
-
     if (!clusterNetworking.cidrRange) {
       return "VPC CIDR range is required";
     }
-    if (!VALID_CIDR_RANGE_PATTERN.test(clusterNetworking.cidrRange)
-      || !VALID_CIDR_RANGE_PATTERN.test(clusterNetworking.controlPlaneCidr)
-      || !VALID_CIDR_RANGE_PATTERN.test(clusterNetworking.podCidr)
-      || !VALID_CIDR_RANGE_PATTERN.test(clusterNetworking.serviceCidr)
+    if (
+      !VALID_CIDR_RANGE_PATTERN.test(clusterNetworking.cidrRange) ||
+      !VALID_CIDR_RANGE_PATTERN.test(clusterNetworking.controlPlaneCidr) ||
+      !VALID_CIDR_RANGE_PATTERN.test(clusterNetworking.podCidr) ||
+      !VALID_CIDR_RANGE_PATTERN.test(clusterNetworking.serviceCidr)
     ) {
       return "VPC CIDR range must be in the format of [0-255].[0-255].0.0/16";
     }
 
     return "";
-  }
+  };
 
   const createClusterObj = (): Contract => {
     const nodePools = [
@@ -333,33 +378,34 @@ const GCPProvisionerSettings: React.FC<Props> = (props) => {
         instanceType: "custom-2-4096",
         minInstances: 1,
         maxInstances: 1,
-        nodePoolType: GKENodePoolType.GKE_NODE_POOL_TYPE_MONITORING
+        nodePoolType: GKENodePoolType.GKE_NODE_POOL_TYPE_MONITORING,
       }),
       new GKENodePool({
         instanceType: "custom-2-4096",
         minInstances: 1,
         maxInstances: 2,
-        nodePoolType: GKENodePoolType.GKE_NODE_POOL_TYPE_SYSTEM
+        nodePoolType: GKENodePoolType.GKE_NODE_POOL_TYPE_SYSTEM,
       }),
       new GKENodePool({
         instanceType,
         minInstances: 1, // TODO: make these customizable before merging
         maxInstances: 10, // TODO: make these customizable before merging
-        nodePoolType: GKENodePoolType.GKE_NODE_POOL_TYPE_APPLICATION
+        nodePoolType: GKENodePoolType.GKE_NODE_POOL_TYPE_APPLICATION,
       }),
     ];
 
     // Conditionally add the last EKSNodeGroup if gpuModal is enabled
     if (props.gpuModal) {
-      nodePools.push(new GKENodePool({
-        instanceType: gpuInstanceType,
-        minInstances: gpuMinInstances || 0,
-        maxInstances: gpuMaxInstances || 5,
-        nodePoolType: GKENodePoolType.GKE_NODE_POOL_TYPE_CUSTOM,
-      }));
+      nodePools.push(
+        new GKENodePool({
+          instanceType: gpuInstanceType,
+          minInstances: gpuMinInstances || 0,
+          maxInstances: gpuMaxInstances || 5,
+          nodePoolType: GKENodePoolType.GKE_NODE_POOL_TYPE_CUSTOM,
+        })
+      );
     }
 
-
     const data = new Contract({
       cluster: new Cluster({
         projectId: currentProject.id,
@@ -378,36 +424,33 @@ const GCPProvisionerSettings: React.FC<Props> = (props) => {
               podCidr: clusterNetworking.podCidr,
               serviceCidr: clusterNetworking.serviceCidr,
             }),
-            nodePools
+            nodePools,
           }),
         },
       }),
     });
 
-    return data
-  }
-
+    return data;
+  };
 
   const createCluster = async () => {
-
     const err = validateInputs();
     if (err !== "") {
-      setErrorMessage(err)
-      setErrorDetails("")
+      setErrorMessage(err);
+      setErrorDetails("");
       return;
     }
     setIsLoading(true);
 
     setIsClicked(true);
 
-
     try {
       window.dataLayer?.push({
-        event: 'provision-attempt',
+        event: "provision-attempt",
         data: {
-          cloud: 'gcp',
-          email: user?.email
-        }
+          cloud: "gcp",
+          email: user?.email,
+        },
       });
     } catch (err) {
       console.log(err);
@@ -422,7 +465,7 @@ const GCPProvisionerSettings: React.FC<Props> = (props) => {
     try {
       setIsReadOnly(true);
       setErrorMessage("");
-      setErrorDetails("")
+      setErrorDetails("");
 
       if (!props.clusterId) {
         markStepStarted("provisioning-started", region);
@@ -453,31 +496,30 @@ const GCPProvisionerSettings: React.FC<Props> = (props) => {
         })
         .catch((err) => {
           setErrorMessage("Error fetching clusters");
-          setErrorDetails(err)
+          setErrorDetails(err);
         });
-
     } catch (err) {
       const errMessage = err.response.data.error.replace("unknown: ", "");
       setIsClicked(false);
       setIsLoading(true);
-      showIntercomWithMessage({ message: "I am running into an issue provisioning a cluster." });
+      showIntercomWithMessage({
+        message: "I am running into an issue provisioning a cluster.",
+      });
       // TODO: handle different error conditions here from preflights
       setErrorMessage(DEFAULT_ERROR_MESSAGE);
-      setErrorDetails(errMessage)
+      setErrorDetails(errMessage);
     } finally {
       setIsReadOnly(false);
       setIsClicked(false);
       setIsLoading(true);
-
     }
-
   };
 
   useEffect(() => {
     setIsReadOnly(
       props.clusterId &&
-      (currentCluster?.status === "UPDATING" ||
-        currentCluster?.status === "UPDATING_UNAVAILABLE")
+        (currentCluster?.status === "UPDATING" ||
+          currentCluster?.status === "UPDATING_UNAVAILABLE")
     );
     setClusterName(
       `${currentProject.name.substring(0, 10)}-${Math.random()
@@ -487,7 +529,6 @@ const GCPProvisionerSettings: React.FC<Props> = (props) => {
   }, []);
 
   useEffect(() => {
-
     const contract = props.selectedClusterVersion as any;
     if (contract?.cluster) {
       if (contract.cluster?.gkeKind?.nodePools) {
@@ -508,26 +549,24 @@ const GCPProvisionerSettings: React.FC<Props> = (props) => {
         controlPlaneCidr: contract.cluster.gkeKind?.network?.controlPlaneCidr,
         podCidr: contract.cluster.gkeKind?.network?.podCidr,
         serviceCidr: contract.cluster.gkeKind?.network?.serviceCidr,
-      })
+      });
       setClusterNetworking(cn);
     }
   }, [props.selectedClusterVersion]);
 
   useEffect(() => {
     if (statusPreflight() == "" && !props.clusterId) {
-      setStep(1)
+      setStep(1);
 
-      preflightChecks()
+      preflightChecks();
     }
-
   }, [props.selectedClusterVersion, clusterNetworking, region]);
 
   const preflightChecks = async () => {
-
     try {
       setIsLoading(true);
       setPreflightData(null);
-      setPreflightFailed(true)
+      setPreflightFailed(true);
       setPreflightError("");
       const data = new PreflightCheckRequest({
         projectId: BigInt(currentProject.id),
@@ -541,27 +580,30 @@ const GCPProvisionerSettings: React.FC<Props> = (props) => {
               controlPlaneCidr: clusterNetworking.controlPlaneCidr,
               podCidr: clusterNetworking.podCidr,
               serviceCidr: clusterNetworking.serviceCidr,
-            })
-          })
-        }
+            }),
+          }),
+        },
       });
       const preflightDataResp = await api.legacyPreflightCheck(
-        "<token>", data,
+        "<token>",
+        data,
         {
           id: currentProject.id,
         }
-      )
+      );
       // Check if any of the preflight checks has a message
       let hasMessage = false;
       let errors = "Preflight Checks Failed : ";
       for (const check in preflightDataResp?.data?.Msg.preflight_checks) {
         if (preflightDataResp?.data?.Msg.preflight_checks[check]?.message) {
           hasMessage = true;
-          errors = errors + check + ", "
+          errors = errors + check + ", ";
         }
       }
       if (hasMessage) {
-        showIntercomWithMessage({ message: "I am running into an issue provisioning a cluster." });
+        showIntercomWithMessage({
+          message: "I am running into an issue provisioning a cluster.",
+        });
         markStepStarted("provisioning-failed", errors);
       }
       // If none of the checks have a message, set setPreflightFailed to false
@@ -570,13 +612,13 @@ const GCPProvisionerSettings: React.FC<Props> = (props) => {
         setStep(2);
       }
       setPreflightData(preflightDataResp?.data?.Msg);
-      setIsLoading(false)
+      setIsLoading(false);
     } catch (err) {
-      setPreflightError(err)
-      setIsLoading(false)
+      setPreflightError(err);
+      setIsLoading(false);
       setPreflightFailed(true);
     }
-  }
+  };
 
   const renderForm = () => {
     // Render simplified form if initial create
@@ -586,11 +628,13 @@ const GCPProvisionerSettings: React.FC<Props> = (props) => {
           currentStep={step}
           steps={[
             <>
-              <Text size={16}>Select a Google Cloud Region for your cluster</Text>
+              <Text size={16}>
+                Select a Google Cloud Region for your cluster
+              </Text>
               <Spacer y={1} />
               <Text color="helper">
-                Porter will provision your infrastructure in the
-                specified location.
+                Porter will provision your infrastructure in the specified
+                location.
               </Text>
               <Spacer height="10px" />
               <SelectRow
@@ -601,19 +645,26 @@ const GCPProvisionerSettings: React.FC<Props> = (props) => {
                 scrollBuffer={true}
                 dropdownMaxHeight="240px"
                 setActiveValue={setRegion}
-                label="📍 GCP location" />
+                label="📍 GCP location"
+              />
               {renderAdvancedSettings()}
-
             </>,
             <>
-              <PreflightChecks provider='GCP' preflightData={preflightData} error={preflightError} />
-              <Spacer y={.5} />
-              {(preflightFailed && preflightData || preflightError) &&
+              <PreflightChecks
+                provider="GCP"
+                preflightData={preflightData}
+                error={preflightError}
+              />
+              <Spacer y={0.5} />
+              {((preflightFailed && preflightData) || preflightError) && (
                 <>
-                  {!preflightError && <Text color="helper">
-                    Preflight checks for the account didn't pass. Please fix the issues and retry.
-                  </Text>}
-                  < Button
+                  {!preflightError && (
+                    <Text color="helper">
+                      Preflight checks for the account didn't pass. Please fix
+                      the issues and retry.
+                    </Text>
+                  )}
+                  <Button
                     // disabled={isDisabled()}
                     disabled={isLoading}
                     onClick={preflightChecks}
@@ -621,18 +672,25 @@ const GCPProvisionerSettings: React.FC<Props> = (props) => {
                     Retry Checks
                   </Button>
                 </>
-              }
+              )}
             </>,
             <>
               <Text size={16}>Provision your cluster</Text>
               <Spacer y={1} />
               <Button
-                disabled={isDisabled() || isLoading || preflightFailed || statusPreflight() != ""}
+                disabled={
+                  isDisabled() ||
+                  isLoading ||
+                  preflightFailed ||
+                  statusPreflight() != ""
+                }
                 onClick={createCluster}
                 status={getStatus()}
               >
                 Provision
-              </Button><Spacer y={1} /></>
+              </Button>
+              <Spacer y={1} />
+            </>,
           ].filter((x) => x)}
         />
       );
@@ -647,9 +705,8 @@ const GCPProvisionerSettings: React.FC<Props> = (props) => {
             disabled={isReadOnly}
             value={gpuInstanceType}
             setValue={(x: string) => {
-              setGpuInstanceType(x)
-            }
-            }
+              setGpuInstanceType(x);
+            }}
             label="GPU Instance type"
           />
           <Spacer y={1} />
@@ -663,7 +720,7 @@ const GCPProvisionerSettings: React.FC<Props> = (props) => {
             disabled={isReadOnly || isLoading}
             value={gpuMaxInstances.toString()}
             setValue={(x: number) => {
-              setGpuMaxInstances(x)
+              setGpuMaxInstances(x);
             }}
           />
           <Button
@@ -674,9 +731,9 @@ const GCPProvisionerSettings: React.FC<Props> = (props) => {
             Provision
           </Button>
 
-          <Spacer y={.5} />
+          <Spacer y={0.5} />
         </>
-      )
+      );
     }
     // If settings, update full form
     return (
@@ -725,13 +782,16 @@ const GCPProvisionerSettings: React.FC<Props> = (props) => {
           Provision
         </Button>
 
-        {
-          (!currentProject?.enable_reprovision && props.clusterId) &&
+        {!currentProject?.enable_reprovision && props.clusterId && (
           <>
             <Spacer y={1} />
-            <Text>Updates to the cluster are disabled on this project. Enable re-provisioning by contacting <a href="mailto:support@porter.run">Porter Support</a>.</Text>
+            <Text>
+              Updates to the cluster are disabled on this project. Enable
+              re-provisioning by contacting{" "}
+              <a href="mailto:support@porter.run">Porter Support</a>.
+            </Text>
           </>
-        }
+        )}
       </>
     );
   };
@@ -740,38 +800,30 @@ const GCPProvisionerSettings: React.FC<Props> = (props) => {
     <>
       {renderForm()}
 
-
-      {user.isPorterUser &&
+      {user.isPorterUser && (
         <>
-
           <Spacer y={1} />
           <Text color="yellow">Visible to Admin Only</Text>
-          <Button
-            color="red"
-            onClick={createCluster}
-            status={getStatus()}
-          >
+          <Button color="red" onClick={createCluster} status={getStatus()}>
             Override Provision
           </Button>
         </>
-      }
-
+      )}
     </>
   );
 };
 
 export default withRouter(GCPProvisionerSettings);
 
-
 const StyledForm = styled.div`
-              position: relative;
-              padding: 30px 30px 25px;
-              border-radius: 5px;
-              background: ${({ theme }) => theme.fg};
-              border: 1px solid #494b4f;
-              font-size: 13px;
-              margin-bottom: 30px;
-              `;
+  position: relative;
+  padding: 30px 30px 25px;
+  border-radius: 5px;
+  background: ${({ theme }) => theme.fg};
+  border: 1px solid #494b4f;
+  font-size: 13px;
+  margin-bottom: 30px;
+`;
 
 const DEFAULT_ERROR_MESSAGE =
   "An error occurred while provisioning your infrastructure. Please try again.";
@@ -791,7 +843,7 @@ const ExpandHeader = styled.div<{ isExpanded: boolean }>`
                 margin - right: 7px;
               margin-left: -7px;
               transform: ${(props) =>
-    props.isExpanded ? "rotate(0deg)" : "rotate(-90deg)"};
+                props.isExpanded ? "rotate(0deg)" : "rotate(-90deg)"};
               transition: transform 0.1s ease;
   }
               `;

+ 11 - 15
dashboard/src/components/GPUCostConsent.tsx

@@ -1,17 +1,17 @@
-import React, { useState, useContext } from "react";
+import React, { useContext, useState } from "react";
 import styled from "styled-components";
 
-import { Context } from "shared/Context";
 import api from "shared/api";
+import { Context } from "shared/Context";
 
-import Modal from "./porter/Modal";
-import Text from "./porter/Text";
-import Spacer from "./porter/Spacer";
-import Fieldset from "./porter/Fieldset";
 import Button from "./porter/Button";
 import ExpandableSection from "./porter/ExpandableSection";
+import Fieldset from "./porter/Fieldset";
 import Input from "./porter/Input";
 import Link from "./porter/Link";
+import Modal from "./porter/Modal";
+import Spacer from "./porter/Spacer";
+import Text from "./porter/Text";
 
 type Props = {
   setCurrentStep: (step: string) => void;
@@ -26,13 +26,12 @@ const GPUCostConsent: React.FC<Props> = ({
 
   return (
     <>
-
       <Text size={16}>Base AWS cost consent</Text>
       <Spacer height="15px" />
       <Text color="helper">
         Porter will create the underlying infrastructure in your own AWS
-        account. You will be separately charged by AWS for this
-        infrastructure. The cost for this base infrastructure is as follows:
+        account. You will be separately charged by AWS for this infrastructure.
+        The cost for this base infrastructure is as follows:
       </Text>
       <Spacer y={1} />
       <ExpandableSection
@@ -52,17 +51,15 @@ const GPUCostConsent: React.FC<Props> = ({
               <Spacer height="15px" />
               <Tab />+ Monitoring workloads: t3.large instance (1) = $60.74/mo
               <Spacer height="15px" />
-              <Tab />+ Application workloads: t3.medium instance (1) =
-              $30.1/mo
+              <Tab />+ Application workloads: t3.medium instance (1) = $30.1/mo
             </Fieldset>
           </>
         }
       />
       <Spacer y={1} />
       <Text color="helper">
-        The base AWS infrastructure covers up to 2 vCPU and 4GB of RAM.
-        Separate from the AWS cost, Porter charges based on your resource
-        usage.
+        The base AWS infrastructure covers up to 2 vCPU and 4GB of RAM. Separate
+        from the AWS cost, Porter charges based on your resource usage.
       </Text>
       <Spacer inline width="5px" />
       <Spacer y={0.5} />
@@ -108,7 +105,6 @@ const GPUCostConsent: React.FC<Props> = ({
       >
         Continue
       </Button>
-
     </>
   );
 };

+ 112 - 86
dashboard/src/components/GPUProvisionSettings.tsx

@@ -1,37 +1,33 @@
 import React, { useContext, useState } from "react";
-import {
-  type EKSPreflightValues,
-} from "@porter-dev/api-contracts";
+import { type EKSPreflightValues } from "@porter-dev/api-contracts";
 import { withRouter, type RouteComponentProps } from "react-router";
 import styled from "styled-components";
 
 import Heading from "components/form-components/Heading";
 
+import { Context } from "shared/Context";
 import { type ClusterState } from "shared/types";
-
 import healthy from "assets/status-healthy.png";
 
 import Button from "./porter/Button";
-
+import InputSlider from "./porter/InputSlider";
 import Select from "./porter/Select";
 import Spacer from "./porter/Spacer";
 import Text from "./porter/Text";
 import VerticalSteps from "./porter/VerticalSteps";
 import PreflightChecks from "./PreflightChecks";
-import InputSlider from "./porter/InputSlider";
-import { Context } from "shared/Context";
-
 
 const gpuMachineTypeOptions = [
-
   { value: "g4dn.xlarge", label: "g4dn.xlarge" },
   { value: "g4dn.2xlarge", label: "g4dn.2xlarge" },
   { value: "p4d.24xlarge", label: "p4d.24xlarge" },
 ];
 
-
 type Props = RouteComponentProps & {
-  handleClusterStateChange: <K extends keyof ClusterState>(key: K, value: ClusterState[K]) => void;
+  handleClusterStateChange: <K extends keyof ClusterState>(
+    key: K,
+    value: ClusterState[K]
+  ) => void;
   clusterState: ClusterState;
   isReadOnly: boolean;
   isLoading: boolean;
@@ -64,12 +60,9 @@ const GPUProvisionerSettings: React.FC<Props> = ({
   dismissPreflight,
   getStatus,
   requestQuotasAndProvision,
-
 }) => {
   const [gpuStep, setGPUStep] = useState(0);
-  const {
-    currentProject,
-  } = useContext(Context);
+  const { currentProject } = useContext(Context);
 
   const renderGPUSettings = (): JSX.Element => {
     return (
@@ -79,17 +72,16 @@ const GPUProvisionerSettings: React.FC<Props> = ({
         steps={[
           <>
             <Heading isAtTop> Select GPU Instance Type </Heading>
-            <Spacer y={.5} />
+            <Spacer y={0.5} />
             <Select
               options={gpuMachineTypeOptions}
               width="350px"
               disabled={isReadOnly}
               value={clusterState.gpuInstanceType}
               setValue={(x: string) => {
-                handleClusterStateChange("gpuInstanceType", x)
+                handleClusterStateChange("gpuInstanceType", x);
                 // handleClusterStateChange("machineType", x)
-              }
-              }
+              }}
               label="Machine type"
             />
             <Spacer y={1} />
@@ -103,109 +95,143 @@ const GPUProvisionerSettings: React.FC<Props> = ({
               disabled={isReadOnly || isLoading}
               value={clusterState.gpuMaxInstances.toString()}
               setValue={(x: number) => {
-                handleClusterStateChange("gpuMaxInstances", x)
-
+                handleClusterStateChange("gpuMaxInstances", x);
               }}
             />
-            <Button onClick={() => {
-              setGPUStep(1)
-              preflightChecks();
-            }}>
+            <Button
+              onClick={() => {
+                setGPUStep(1);
+                preflightChecks();
+              }}
+            >
               Continue
             </Button>
 
-            <Spacer y={.5} />
+            <Spacer y={0.5} />
           </>,
           <>
-            {showEmailMessage ?
+            {showEmailMessage ? (
               <>
                 <CheckItemContainer>
                   <CheckItemTop>
                     <StatusIcon src={healthy} />
                     <Spacer inline x={1} />
-                    <Text style={{ marginLeft: '10px', flex: 1 }}>{"Porter will request to increase quotas when you provision"}</Text>
+                    <Text style={{ marginLeft: "10px", flex: 1 }}>
+                      {
+                        "Porter will request to increase quotas when you provision"
+                      }
+                    </Text>
                   </CheckItemTop>
                 </CheckItemContainer>
-
-              </> :
+              </>
+            ) : (
               <>
-                <PreflightChecks provider='AWS' preflightData={preflightData} error={preflightError} />
-                <Spacer y={.5} />
-                {(preflightFailed && preflightData) &&
+                <PreflightChecks
+                  provider="AWS"
+                  preflightData={preflightData}
+                  error={preflightError}
+                />
+                <Spacer y={0.5} />
+                {preflightFailed && preflightData && (
                   <>
-                    {(showHelpMessage && currentProject?.quota_increase) ? <>
-                      <Text color="helper">
-                        Your account currently is blocked from provisioning in {clusterState.awsRegion} due to a quota limit imposed by AWS. Either change the region or request to increase quotas.
-                      </Text>
-                      <Spacer y={.5} />
-                      <Text color="helper">
-                        Porter can automatically request quota increases on your behalf and email you once the cluster is provisioned.
-                      </Text>
-                      <Spacer y={.5} />
-                      <div style={{ display: 'flex', justifyContent: 'flex-start', alignItems: 'center', gap: '15px' }}>
-                        <Button
-                          disabled={isLoading}
-                          onClick={proceedToProvision}
-
+                    {showHelpMessage && currentProject?.quota_increase ? (
+                      <>
+                        <Text color="helper">
+                          Your account currently is blocked from provisioning in{" "}
+                          {clusterState.awsRegion} due to a quota limit imposed
+                          by AWS. Either change the region or request to
+                          increase quotas.
+                        </Text>
+                        <Spacer y={0.5} />
+                        <Text color="helper">
+                          Porter can automatically request quota increases on
+                          your behalf and email you once the cluster is
+                          provisioned.
+                        </Text>
+                        <Spacer y={0.5} />
+                        <div
+                          style={{
+                            display: "flex",
+                            justifyContent: "flex-start",
+                            alignItems: "center",
+                            gap: "15px",
+                          }}
                         >
-                          Auto request increase
-                        </Button>
-                        <Button
-                          disabled={isLoading}
-                          onClick={dismissPreflight}
-                          color="#313539"
-                        >
-                          I'll do it myself
-                        </Button>
-                      </div>
-
-                    </> : (
-                      <><Text color="helper">
-                        Your account currently is blocked from provisioning in {clusterState.awsRegion} due to a quota limit imposed by AWS. Either change the region or request to increase quotas.
-                      </Text><Spacer y={.5} /><Button
-                        disabled={isLoading}
-                        onClick={preflightChecks}
-
-                      >
+                          <Button
+                            disabled={isLoading}
+                            onClick={proceedToProvision}
+                          >
+                            Auto request increase
+                          </Button>
+                          <Button
+                            disabled={isLoading}
+                            onClick={dismissPreflight}
+                            color="#313539"
+                          >
+                            I'll do it myself
+                          </Button>
+                        </div>
+                      </>
+                    ) : (
+                      <>
+                        <Text color="helper">
+                          Your account currently is blocked from provisioning in{" "}
+                          {clusterState.awsRegion} due to a quota limit imposed
+                          by AWS. Either change the region or request to
+                          increase quotas.
+                        </Text>
+                        <Spacer y={0.5} />
+                        <Button disabled={isLoading} onClick={preflightChecks}>
                           Retry checks
-                        </Button></>)}
-                  </>}
-              </>}
+                        </Button>
+                      </>
+                    )}
+                  </>
+                )}
+              </>
+            )}
 
             <Spacer y={1} />
-            {showEmailMessage && <>
-              <Text color="helper">
-                After your quota requests have been approved by AWS, Porter will email you when your cluster has been provisioned.
-              </Text>
-              <Spacer y={1} />
-            </>}
+            {showEmailMessage && (
+              <>
+                <Text color="helper">
+                  After your quota requests have been approved by AWS, Porter
+                  will email you when your cluster has been provisioned.
+                </Text>
+                <Spacer y={1} />
+              </>
+            )}
             <StepChangeButtonsContainer>
               <Button
                 disabled={(preflightFailed && !showEmailMessage) || isLoading}
-                onClick={showEmailMessage ? requestQuotasAndProvision : createCluster}
+                onClick={
+                  showEmailMessage ? requestQuotasAndProvision : createCluster
+                }
                 status={getStatus()}
               >
                 Provision
               </Button>
               <Spacer inline x={0.5} />
-              <Button onClick={() => { setGPUStep(0); }} color="#222222">Back</Button>
+              <Button
+                onClick={() => {
+                  setGPUStep(0);
+                }}
+                color="#222222"
+              >
+                Back
+              </Button>
             </StepChangeButtonsContainer>
-            <Spacer y={1} /></>,
-
+            <Spacer y={1} />
+          </>,
         ].filter((x) => x)}
       />
     );
   };
-  return (
-    <>
-      {renderGPUSettings()}
-    </>
-  );
+  return <>{renderGPUSettings()}</>;
 };
 
 export default withRouter(GPUProvisionerSettings);
 
-
 const CheckItemContainer = styled.div`
   display: flex;
   flex-direction: column;
@@ -232,4 +258,4 @@ const StatusIcon = styled.img`
 
 const StepChangeButtonsContainer = styled.div`
   display: flex;
-`;
+`;

+ 1 - 0
dashboard/src/components/Loading.tsx

@@ -1,5 +1,6 @@
 import React, { Component } from "react";
 import styled from "styled-components";
+
 import loading from "assets/loading.gif";
 
 type PropsType = {

+ 11 - 7
dashboard/src/components/LogQueryModeSelectionToggle.tsx

@@ -1,13 +1,15 @@
-import DateTimePicker from "components/date-time-picker/DateTimePicker";
-import dayjs from "dayjs";
-import time from "assets/time.svg";
 import React from "react";
+import dayjs from "dayjs";
 import styled from "styled-components";
 
+import DateTimePicker from "components/date-time-picker/DateTimePicker";
+
+import time from "assets/time.svg";
+
 type LogQueryModeSelectionToggleProps = {
   selectedDate?: Date;
   setSelectedDate: (date?: Date) => void;
-}
+};
 
 const LogQueryModeSelectionToggle = (
   props: LogQueryModeSelectionToggleProps
@@ -32,7 +34,9 @@ const LogQueryModeSelectionToggle = (
         </ToggleOption>
         <ToggleOption
           nudgeLeft
-          onClick={() => { props.setSelectedDate(dayjs().toDate()); }}
+          onClick={() => {
+            props.setSelectedDate(dayjs().toDate());
+          }}
           selected={!!props.selectedDate}
         >
           <TimeIcon src={time} selected={!!props.selectedDate} />
@@ -63,11 +67,11 @@ const ToggleOption = styled.div<{ selected: boolean; nudgeLeft?: boolean }>`
   :hover {
     border: 1px solid #7a7b80;
     z-index: 2;
-  };
+  }
 `;
 
 const ToggleButton = styled.div`
-  background: #181B20;
+  background: #181b20;
   border-radius: 5px;
   font-size: 13px;
   height: 30px;

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

@@ -1,7 +1,8 @@
 import React, { useState } from "react";
-import Button from "./Button";
-import styled from "styled-components";
 import dayjs from "dayjs";
+import styled from "styled-components";
+
+import Button from "./Button";
 import SearchBar from "./porter/SearchBar";
 
 type Props = {
@@ -9,7 +10,7 @@ type Props = {
   setSearchText: (x: string) => void;
   setEnteredSearchText: (x: string) => void;
   setSelectedDate: () => void;
-}
+};
 
 const escapeExp = (str: string) => {
   // regex special character need to be escaped twice
@@ -41,4 +42,4 @@ const LogSearchBar: React.FC<Props> = ({
   );
 };
 
-export default LogSearchBar;
+export default LogSearchBar;

+ 16 - 4
dashboard/src/components/MultiSaveButton.tsx

@@ -1,6 +1,8 @@
 import React, { useState } from "react";
 import styled from "styled-components";
+
 import loading from "assets/loading.gif";
+
 import Description from "./Description";
 
 type MultiSelectOption = {
@@ -82,12 +84,18 @@ const MultiSaveButton: React.FC<Props> = (props) => {
     if (isDropdownExpanded) {
       return (
         <>
-          <DropdownOverlay onClick={() => { setIsDropdownExpanded(false); }} />
+          <DropdownOverlay
+            onClick={() => {
+              setIsDropdownExpanded(false);
+            }}
+          />
           <OptionWrapper
             expandTo={props.expandTo || "right"}
             dropdownWidth="400px"
             dropdownMaxHeight="300px"
-            onClick={() => { setIsDropdownExpanded(false); }}
+            onClick={() => {
+              setIsDropdownExpanded(false);
+            }}
           >
             {renderOptionList()}
           </OptionWrapper>
@@ -102,7 +110,9 @@ const MultiSaveButton: React.FC<Props> = (props) => {
         <Option
           key={i}
           selected={option.text === originalArray[currOptionIndex]?.text}
-          onClick={() => { setCurrOptionIndex(i); }}
+          onClick={() => {
+            setCurrOptionIndex(i);
+          }}
           lastItem={i === originalArray.length - 1}
         >
           {option.text}
@@ -133,7 +143,9 @@ const MultiSaveButton: React.FC<Props> = (props) => {
         <DropdownButton
           disabled={props.disabled}
           color={props.color || "#5561C0"}
-          onClick={() => { setIsDropdownExpanded(!isDropdownExpanded); }}
+          onClick={() => {
+            setIsDropdownExpanded(!isDropdownExpanded);
+          }}
         >
           <i className="material-icons expand-icon">
             {isDropdownExpanded ? "expand_less" : "expand_more"}

+ 3 - 3
dashboard/src/components/OldPlaceholder.tsx

@@ -5,7 +5,7 @@ type Props = {
   height?: string;
   minHeight?: string;
   children: React.ReactNode;
-}
+};
 
 const OldPlaceholder: React.FC<Props> = ({ height, minHeight, children }) => {
   return (
@@ -30,6 +30,6 @@ const StyledPlaceholder = styled.div<{
   font-size: 13px;
   color: #ffffff44;
   border-radius: 5px;
-  background: ${props => props.theme.fg};
-  border: 1px solid ${props => props.theme.border};
+  background: ${(props) => props.theme.fg};
+  border: 1px solid ${(props) => props.theme.border};
 `;

+ 19 - 10
dashboard/src/components/OldTable.tsx

@@ -1,16 +1,19 @@
 import React, { useEffect, useState } from "react";
-import styled from "styled-components";
 import {
-  type Column,
-  type Row,
   useGlobalFilter,
   usePagination,
   useTable,
+  type Column,
+  type Row,
 } from "react-table";
+import styled from "styled-components";
+
 import Loading from "components/Loading";
-import Selector from "./Selector";
+
 import loading from "assets/loading.gif";
+
 import Button from "./porter/Button";
+import Selector from "./Selector";
 
 const GlobalFilter: React.FunctionComponent<any> = ({
   setGlobalFilter,
@@ -158,7 +161,9 @@ const Table: React.FC<TableProps> = ({
               disableHover={disableHover}
               {...row.getRowProps()}
               enablePointer={!!onRowClick}
-              onClick={() => { onRowClick && onRowClick(row); }}
+              onClick={() => {
+                onRowClick && onRowClick(row);
+              }}
               selected={false}
             >
               {/* TODO: This is actually broken, not sure why but we need the width to be properly setted, this is a temporary solution */}
@@ -249,10 +254,14 @@ const Table: React.FC<TableProps> = ({
             <PageCounter>
               {currentPageIndex + 1} of {pageCount || pageCount + 1}
             </PageCounter>
-            <PaginationAction disabled={!canNextPage} onClick={() => {
-              nextPage();
-              setCurrentPageIndex(currentPageIndex + 1);
-            }} type={"button"}>
+            <PaginationAction
+              disabled={!canNextPage}
+              onClick={() => {
+                nextPage();
+                setCurrentPageIndex(currentPageIndex + 1);
+              }}
+              type={"button"}
+            >
               {">"}
             </PaginationAction>
           </PaginationActionsWrapper>
@@ -320,7 +329,7 @@ export const StyledTr = styled.tr`
   background: ${(props: StyledTrProps) => (props.selected ? "#ffffff11" : "")};
   :hover {
     background: ${(props: StyledTrProps) =>
-    props.disableHover ? "" : "#ffffff22"};
+      props.disableHover ? "" : "#ffffff22"};
   }
   cursor: ${(props: StyledTrProps) =>
     props.enablePointer ? "pointer" : "unset"};

+ 0 - 1
dashboard/src/components/PageIllustration.tsx

@@ -1,5 +1,4 @@
 import React from "react";
-
 import styled from "styled-components";
 
 function PageIllustration() {

+ 6 - 6
dashboard/src/components/Placeholder.tsx

@@ -6,11 +6,11 @@ type Props = {
   minHeight?: string;
   children: React.ReactNode;
   title?: string;
-}
+};
 
-const Placeholder: React.FC<Props> = ({ 
-  height, 
-  minHeight, 
+const Placeholder: React.FC<Props> = ({
+  height,
+  minHeight,
   children,
   title,
 }) => {
@@ -55,12 +55,12 @@ const StyledPlaceholder = styled.div<{
   min-height: ${(props) => props.minHeight || ""};
   display: flex;
   align-items: center;
-  color: #8D949E;
+  color: #8d949e;
   padding: 50px;
   justify-content: center;
   font-size: 13px;
   border-radius: 5px;
-  background: ${props => props.theme.fg};
+  background: ${(props) => props.theme.fg};
   border: 1px solid #494b4f;
   padding-bottom: 60px;
 

+ 94 - 90
dashboard/src/components/PreflightChecks.tsx

@@ -1,36 +1,43 @@
-import React, { useEffect, useState, useContext } from "react";
+import React, { useContext, useEffect, useState } from "react";
+import { withRouter, type RouteComponentProps } from "react-router";
 import styled from "styled-components";
-import { type RouteComponentProps, withRouter } from "react-router";
+
+import {
+  PREFLIGHT_MESSAGE_CONST,
+  PREFLIGHT_MESSAGE_CONST_AWS,
+  PREFLIGHT_MESSAGE_CONST_GCP,
+  PROVISIONING_STATUS,
+} from "shared/util";
+import failure from "assets/failure.svg";
+import healthy from "assets/status-healthy.png";
+
+import Loading from "./Loading";
+import Error from "./porter/Error";
+import Link from "./porter/Link";
 import Spacer from "./porter/Spacer";
 import Step from "./porter/Step";
-import Link from "./porter/Link";
 import Text from "./porter/Text";
-import Error from "./porter/Error";
-import healthy from "assets/status-healthy.png";
-import failure from "assets/failure.svg";
-import { PREFLIGHT_MESSAGE_CONST, PREFLIGHT_MESSAGE_CONST_AWS, PREFLIGHT_MESSAGE_CONST_GCP, PROVISIONING_STATUS } from "shared/util";
-import Loading from "./Loading";
+
 type Props = RouteComponentProps & {
-  preflightData: any
-  provider: 'AWS' | 'GCP' | 'DEFAULT' | 'PROVISIONING_STATUS';
+  preflightData: any;
+  provider: "AWS" | "GCP" | "DEFAULT" | "PROVISIONING_STATUS";
   error?: string;
-
 };
 type ItemProps = RouteComponentProps & {
-  checkKey: string
-  checkLabel?: string
+  checkKey: string;
+  checkLabel?: string;
 };
 
-
 const PreflightChecks: React.FC<Props> = (props) => {
-
-  const getMessageConstByProvider = (provider: 'AWS' | 'GCP' | 'DEFAULT' | 'PROVISIONING_STATUS') => {
+  const getMessageConstByProvider = (
+    provider: "AWS" | "GCP" | "DEFAULT" | "PROVISIONING_STATUS"
+  ) => {
     switch (provider) {
-      case 'PROVISIONING_STATUS':
+      case "PROVISIONING_STATUS":
         return PROVISIONING_STATUS;
-      case 'AWS':
+      case "AWS":
         return PREFLIGHT_MESSAGE_CONST_AWS;
-      case 'GCP':
+      case "GCP":
         return PREFLIGHT_MESSAGE_CONST_GCP;
       default:
         return PREFLIGHT_MESSAGE_CONST;
@@ -42,11 +49,13 @@ const PreflightChecks: React.FC<Props> = (props) => {
 
   const combinedKeys = new Set([
     ...Object.keys(currentMessageConst),
-    ...Object.keys(preflightChecks)
+    ...Object.keys(preflightChecks),
   ]);
 
-
-  const PreflightCheckItem: React.FC<ItemProps> = ({ checkKey, checkLabel }) => {
+  const PreflightCheckItem: React.FC<ItemProps> = ({
+    checkKey,
+    checkLabel,
+  }) => {
     // Using optional chaining to prevent potential null/undefined errors
     const checkLabelConst = currentMessageConst[checkKey];
     const checkData = props.preflightData?.preflight_checks?.[checkKey];
@@ -59,26 +68,25 @@ const PreflightChecks: React.FC<Props> = (props) => {
       }
     };
 
-
-
     return (
       <CheckItemContainer hasMessage={hasMessage}>
         <CheckItemTop onClick={handleToggle}>
           {!props.preflightData ? (
-            <Loading
-              offset="0px"
-              width="20px"
-              height="20px" />
+            <Loading offset="0px" width="20px" height="20px" />
           ) : hasMessage ? (
             <StatusIcon src={failure} />
           ) : (
             <StatusIcon src={healthy} />
           )}
           <Spacer inline x={1} />
-          <Text style={{ marginLeft: '10px', flex: 1 }}>{checkLabel ?? checkLabelConst}</Text>
-          {hasMessage && <ExpandIcon className="material-icons" isExpanded={isExpanded}>
-            arrow_drop_down
-          </ExpandIcon>}
+          <Text style={{ marginLeft: "10px", flex: 1 }}>
+            {checkLabel ?? checkLabelConst}
+          </Text>
+          {hasMessage && (
+            <ExpandIcon className="material-icons" isExpanded={isExpanded}>
+              arrow_drop_down
+            </ExpandIcon>
+          )}
         </CheckItemTop>
         {isExpanded && hasMessage && (
           <div>
@@ -91,7 +99,7 @@ const PreflightChecks: React.FC<Props> = (props) => {
               }
               errorModalContents={errorMessageToModal(checkData?.message)}
             />
-            <Spacer y={.5} />
+            <Spacer y={0.5} />
             {checkData?.metadata &&
               Object.entries(checkData.metadata).map(([key, value]) => (
                 <>
@@ -106,62 +114,59 @@ const PreflightChecks: React.FC<Props> = (props) => {
       </CheckItemContainer>
     );
   };
-  return (
-
-    props.provider === 'DEFAULT' ?
-      <AppearingDiv>
-        {Object.keys(currentMessageConst).map((checkKey) => (
-          <PreflightCheckItem key={checkKey} checkKey={checkKey} />
-        ))}
-      </AppearingDiv >
-      :
-
-      (
-        <AppearingDiv>
-          <Text size={16}>Cluster provision check</Text>
-          <Spacer y={.5} />
-          <Text color="helper">
-            Porter checks that the account has the right permissions and resources to provision a cluster.
-          </Text>
-          <Spacer y={1} />
-          {
-            props.error ?
-              props.provider === 'AWS' ?
-                <Error message="Selected region is not available for your account. Please select another region" /> :
-                <>
-                  <Error message="There is an error with your account. Please ensure billing is enabled or contact Porter Support: support@porter.run" />
-                  <Spacer y={.5} />
-                  <Link to="https://support.google.com/googleapi/answer/6158867?hl=en" target="_blank">
-                    Check to see if billing is enabled on your account
-                  </Link>
-                  <Spacer y={.5} />
-                </>
-              :
-              Array.from(combinedKeys).map((checkKey) => (
-                <PreflightCheckItem
-                  key={checkKey}
-                  checkKey={checkKey}
-                  checkLabel={currentMessageConst[checkKey] || checkKey}
-                />
-              ))
-          }
-        </AppearingDiv >
-      )
-  )
+  return props.provider === "DEFAULT" ? (
+    <AppearingDiv>
+      {Object.keys(currentMessageConst).map((checkKey) => (
+        <PreflightCheckItem key={checkKey} checkKey={checkKey} />
+      ))}
+    </AppearingDiv>
+  ) : (
+    <AppearingDiv>
+      <Text size={16}>Cluster provision check</Text>
+      <Spacer y={0.5} />
+      <Text color="helper">
+        Porter checks that the account has the right permissions and resources
+        to provision a cluster.
+      </Text>
+      <Spacer y={1} />
+      {props.error ? (
+        props.provider === "AWS" ? (
+          <Error message="Selected region is not available for your account. Please select another region" />
+        ) : (
+          <>
+            <Error message="There is an error with your account. Please ensure billing is enabled or contact Porter Support: support@porter.run" />
+            <Spacer y={0.5} />
+            <Link
+              to="https://support.google.com/googleapi/answer/6158867?hl=en"
+              target="_blank"
+            >
+              Check to see if billing is enabled on your account
+            </Link>
+            <Spacer y={0.5} />
+          </>
+        )
+      ) : (
+        Array.from(combinedKeys).map((checkKey) => (
+          <PreflightCheckItem
+            key={checkKey}
+            checkKey={checkKey}
+            checkLabel={currentMessageConst[checkKey] || checkKey}
+          />
+        ))
+      )}
+    </AppearingDiv>
+  );
 };
 
-
-
 export default withRouter(PreflightChecks);
 
-
 const AppearingDiv = styled.div<{ color?: string }>`
   animation: floatIn 0.5s;
   animation-fill-mode: forwards;
   display: flex;
-  flex-direction: column; 
+  flex-direction: column;
   color: ${(props) => props.color || "#ffffff44"};
- 
+
   @keyframes floatIn {
     from {
       opacity: 0;
@@ -174,28 +179,27 @@ const AppearingDiv = styled.div<{ color?: string }>`
   }
 `;
 const StatusIcon = styled.img`
-height: 14px;
+  height: 14px;
 `;
 
 const CheckItemContainer = styled.div`
   display: flex;
   flex-direction: column;
-  border: 1px solid ${props => props.theme.border};
+  border: 1px solid ${(props) => props.theme.border};
   border-radius: 5px;
   font-size: 13px;
   width: 100%;
   margin-bottom: 10px;
   padding-left: 10px;
-  cursor: ${props => (props.hasMessage ? 'pointer' : 'default')};
-  background: ${props => props.theme.clickable.bg};
-
+  cursor: ${(props) => (props.hasMessage ? "pointer" : "default")};
+  background: ${(props) => props.theme.clickable.bg};
 `;
 
 const CheckItemTop = styled.div`
   display: flex;
   align-items: center;
   padding: 10px;
-  background: ${props => props.theme.clickable.bg};
+  background: ${(props) => props.theme.clickable.bg};
 `;
 
 const ExpandIcon = styled.i<{ isExpanded: boolean }>`
@@ -204,19 +208,19 @@ const ExpandIcon = styled.i<{ isExpanded: boolean }>`
   font-size: 20px;
   cursor: pointer;
   border-radius: 20px;
-  transform: ${props => props.isExpanded ? "" : "rotate(-90deg)"};
+  transform: ${(props) => (props.isExpanded ? "" : "rotate(-90deg)")};
 `;
 const ErrorMessageLabel = styled.span`
   font-weight: bold;
   margin-left: 10px;
 `;
 const ErrorMessageContent = styled.div`
-  font-family: 'Courier New', Courier, monospace;
+  font-family: "Courier New", Courier, monospace;
   padding: 5px 10px;
   border-radius: 4px;
   margin-left: 10px;
   user-select: text;
-  cursor: text
+  cursor: text;
 `;
 
 const AWS_LOGIN_ERROR_MESSAGE =
@@ -505,4 +509,4 @@ const errorMessageToModal = (errorMessage: string) => {
     default:
       return null;
   }
-};
+};

+ 1 - 3
dashboard/src/components/ProvisionerFlow.tsx

@@ -92,9 +92,7 @@ const ProvisionerFlow: React.FC<Props> = ({}) => {
               Want to test Porter without linking your own cloud account?
             </Text>
             <Spacer y={0.5} />
-            <Text color={"helper"}>
-              Get started on the Porter Cloud.
-            </Text>
+            <Text color={"helper"}>Get started on the Porter Cloud.</Text>
             <Spacer y={1} />
             <Link to="https://cloud.porter.run">
               <Button alt height="35px">

+ 8 - 7
dashboard/src/components/ProvisionerForm.tsx

@@ -1,18 +1,19 @@
-import React, { useEffect, useState, useContext } from "react";
+import React, { useContext, useEffect, useState } from "react";
 import styled from "styled-components";
 
+import Heading from "components/form-components/Heading";
+
 import aws from "assets/aws.png";
 import azure from "assets/azure.png";
 import gcp from "assets/gcp.png";
 
-import Heading from "components/form-components/Heading";
-import Helper from "./form-components/Helper";
-import ProvisionerSettings from "./ProvisionerSettings";
-import Text from "./porter/Text";
-import Spacer from "./porter/Spacer";
-import Container from "./porter/Container";
 import AzureProvisionerSettings from "./AzureProvisionerSettings";
+import Helper from "./form-components/Helper";
 import GCPProvisionerSettings from "./GCPProvisionerSettings";
+import Container from "./porter/Container";
+import Spacer from "./porter/Spacer";
+import Text from "./porter/Text";
+import ProvisionerSettings from "./ProvisionerSettings";
 
 type Props = {
   goBack: () => void;

+ 7 - 3
dashboard/src/components/ProvisionerSettings.tsx

@@ -698,9 +698,13 @@ const ProvisionerSettings: React.FC<Props> = (props) => {
       const data = new PreflightCheckRequest({
         contract,
       });
-      const preflightDataResp = await api.legacyPreflightCheck("<token>", data, {
-        id: currentProject.id,
-      });
+      const preflightDataResp = await api.legacyPreflightCheck(
+        "<token>",
+        data,
+        {
+          id: currentProject.id,
+        }
+      );
       // Check if any of the preflight checks has a message
       let hasMessage = false;
       let errors = "Preflight Checks Failed : ";

+ 12 - 14
dashboard/src/components/ProvisionerStatus.tsx

@@ -1,22 +1,24 @@
 import React, { useContext, useEffect, useRef, useState } from "react";
-import { integrationList } from "shared/common";
 import styled, { keyframes } from "styled-components";
+
+import api from "shared/api";
+import { integrationList } from "shared/common";
+import { Context } from "shared/Context";
+import { useWebsockets } from "shared/hooks/useWebsockets";
 import { readableDate } from "shared/string_utils";
 import {
-  type Infrastructure,
   KindMap,
+  type Infrastructure,
   type Operation,
   type OperationStatus,
   type OperationType,
   type TFResourceState,
   type TFState,
 } from "shared/types";
-import api from "shared/api";
-import Placeholder from "./OldPlaceholder";
-import Loading from "./Loading";
-import { Context } from "shared/Context";
-import { useWebsockets } from "shared/hooks/useWebsockets";
+
 import Description from "./Description";
+import Loading from "./Loading";
+import Placeholder from "./OldPlaceholder";
 
 type Props = {
   infras: Infrastructure[];
@@ -398,13 +400,9 @@ type OperationDetailsProps = {
   padding?: string;
 };
 
-export const OperationDetails: React.FunctionComponent<OperationDetailsProps> = ({
-  infra,
-  can_delete,
-  refreshInfra,
-  useOperation,
-  padding,
-}) => {
+export const OperationDetails: React.FunctionComponent<
+  OperationDetailsProps
+> = ({ infra, can_delete, refreshInfra, useOperation, padding }) => {
   const [isLoading, setIsLoading] = useState(!useOperation);
   const [hasError, setHasError] = useState(false);
   const [operation, setOperation] = useState<Operation>(useOperation);

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

@@ -19,7 +19,9 @@ export default class RadioSelector extends Component<PropsType, StateType> {
             return (
               <RadioRow
                 key={option.value}
-                onClick={() => { this.props.setSelected(option.value); }}
+                onClick={() => {
+                  this.props.setSelected(option.value);
+                }}
               >
                 <Indicator selected={selected}>
                   {selected && <Circle />}

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

@@ -100,7 +100,9 @@ export default class ResourceTab extends Component<PropsType, StateType> {
     return (
       <StyledResourceTab
         isLast={isLast}
-        onClick={() => { handleClick && handleClick(); }}
+        onClick={() => {
+          handleClick && handleClick();
+        }}
         roundAllCorners={roundAllCorners}
       >
         <ResourceHeader
@@ -143,7 +145,7 @@ const StyledResourceTab = styled.div`
   width: 100%;
   margin-bottom: 2px;
   font-size: 13px;
-  background: ${props => props.theme.fg};
+  background: ${(props) => props.theme.fg};
   border-bottom-left-radius: ${(props: {
     isLast: boolean;
     roundAllCorners: boolean;

+ 8 - 2
dashboard/src/components/SaveButton.tsx

@@ -1,5 +1,6 @@
 import React, { Component } from "react";
 import styled from "styled-components";
+
 import loading from "assets/loading.gif";
 
 type Props = {
@@ -150,7 +151,11 @@ const StatusWrapper = styled.div<{
 `;
 
 const ButtonWrapper = styled.div`
-  ${(props: { makeFlush: boolean; clearPosition?: boolean; absoluteSave: boolean }) => {
+  ${(props: {
+    makeFlush: boolean;
+    clearPosition?: boolean;
+    absoluteSave: boolean;
+  }) => {
     const baseStyles = `
       display: flex;
       position: ${props.absoluteSave ? "absolute" : ""};
@@ -199,7 +204,8 @@ const Button = styled.button<{
   text-align: left;
   border: 0;
   border-radius: ${(props) => (props.rounded ? "100px" : "5px")};
-  background: ${(props) => (!props.disabled ? (props.color || props.theme.button) : "#aaaabb")};
+  background: ${(props) =>
+    !props.disabled ? props.color || props.theme.button : "#aaaabb"};
   cursor: ${(props) => (props.disabled ? "not-allowed" : "pointer")};
   user-select: none;
   :focus {

+ 7 - 5
dashboard/src/components/SearchBar.tsx

@@ -1,13 +1,14 @@
 import React, { useEffect, useRef, useState } from "react";
-import Button from "./Button";
 import styled from "styled-components";
 
+import Button from "./Button";
+
 type Props = {
   setSearchFilter: (x: string) => void;
   disabled: boolean;
   prompt?: string;
   fullWidth?: boolean;
-}
+};
 
 const SearchBar: React.FC<Props> = ({
   setSearchFilter,
@@ -25,7 +26,6 @@ const SearchBar: React.FC<Props> = ({
     }, 0);
   }, []);
 
-
   return (
     <SearchRowWrapper fullWidth={fullWidth}>
       <SearchBarWrapper>
@@ -47,7 +47,9 @@ const SearchBar: React.FC<Props> = ({
       </SearchBarWrapper>
       <ButtonWrapper disabled={disabled}>
         <Button
-          onClick={() => { setSearchFilter(searchInput); }}
+          onClick={() => {
+            setSearchFilter(searchInput);
+          }}
           disabled={disabled}
         >
           Search
@@ -84,7 +86,7 @@ const ButtonWrapper = styled.div`
     props.disabled ? "#aaaabbee" : "#616FEEcc"};
   :hover {
     background: ${(props: { disabled?: boolean }) =>
-    props.disabled ? "" : "#505edddd"};
+      props.disabled ? "" : "#505edddd"};
   }
   height: 40px;
   display: flex;

+ 34 - 18
dashboard/src/components/Selector.tsx

@@ -1,6 +1,8 @@
 import React, { Component } from "react";
 import styled from "styled-components";
+
 import { Context } from "shared/Context";
+
 import Loading from "./Loading";
 
 export type SelectorPropsType<T> = {
@@ -24,7 +26,10 @@ export type SelectorPropsType<T> = {
 
 type StateType = {};
 
-export default class Selector<T> extends Component<SelectorPropsType<T>, StateType> {
+export default class Selector<T> extends Component<
+  SelectorPropsType<T>,
+  StateType
+> {
   state = {
     expanded: false,
     showTooltip: false,
@@ -69,7 +74,9 @@ export default class Selector<T> extends Component<SelectorPropsType<T>, StateTy
             key={i}
             height={this.props.height}
             selected={option.value === activeValue}
-            onClick={() => { this.handleOptionClick(option); }}
+            onClick={() => {
+              this.handleOptionClick(option);
+            }}
             lastItem={i === options.length - 1}
           >
             {option.icon && (
@@ -117,7 +124,9 @@ export default class Selector<T> extends Component<SelectorPropsType<T>, StateTy
                 : this.props.width
             }
             dropdownMaxHeight={this.props.dropdownMaxHeight}
-            onClick={() => { this.setState({ expanded: false }); }}
+            onClick={() => {
+              this.setState({ expanded: false });
+            }}
           >
             {this.renderDropdownLabel()}
             {this.renderOptionList()}
@@ -175,12 +184,16 @@ export default class Selector<T> extends Component<SelectorPropsType<T>, StateTy
           expanded={this.state.expanded}
           width={this.props.width}
           height={this.props.height}
-          onMouseEnter={() => { this.setState({ showTooltip: true }); }}
-          onMouseLeave={() => { this.setState({ showTooltip: false }); }}
+          onMouseEnter={() => {
+            this.setState({ showTooltip: true });
+          }}
+          onMouseLeave={() => {
+            this.setState({ showTooltip: false });
+          }}
         >
-          {isLoading ?
+          {isLoading ? (
             <Loading />
-            :
+          ) : (
             <>
               <Flex>
                 {this.renderIcon()}
@@ -194,7 +207,7 @@ export default class Selector<T> extends Component<SelectorPropsType<T>, StateTy
               </Flex>
               <i className="material-icons">arrow_drop_down</i>
             </>
-          }
+          )}
         </MainSelector>
         {!this.props.disableTooltip && this.state.showTooltip && (
           <Tooltip>
@@ -340,28 +353,31 @@ const MainSelector = styled.div<{
   width: string;
   height?: string;
 }>`
-  width: ${props => props.width};
-  height: ${props => props.height ? props.height : "35px"};
+  width: ${(props) => props.width};
+  height: ${(props) => (props.height ? props.height : "35px")};
   border: 1px solid #ffffff55;
   font-size: 13px;
   padding: 5px 10px;
   padding-left: 15px;
   border-radius: 3px;
   display: flex;
-  color: ${props => props.disabled ? "#ffffff44" : "#ffffff"};
+  color: ${(props) => (props.disabled ? "#ffffff44" : "#ffffff")};
   justify-content: space-between;
   align-items: center;
-  cursor: ${props => props.disabled ? "not-allowed" : "pointer"};
-  background: ${props => props.expanded ? "#ffffff33" : props.theme.fg};
+  cursor: ${(props) => (props.disabled ? "not-allowed" : "pointer")};
+  background: ${(props) => (props.expanded ? "#ffffff33" : props.theme.fg)};
   :hover {
-    background: ${props => props.expanded ? "#ffffff33" : (
-    props.disabled ? "#ffffff11" : "#ffffff22"
-  )};
+    background: ${(props) =>
+      props.expanded
+        ? "#ffffff33"
+        : props.disabled
+        ? "#ffffff11"
+        : "#ffffff22"};
   }
 
   > i {
     font-size: 20px;
-    transform: ${props => props.expanded ? "rotate(180deg)" : ""};
+    transform: ${(props) => (props.expanded ? "rotate(180deg)" : "")};
   }
 `;
 
@@ -395,4 +411,4 @@ const Tooltip = styled.div`
       opacity: 1;
     }
   }
-`;
+`;

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

@@ -10,7 +10,7 @@ type Props = {
   materialIconClass?: string;
   handleNavBack?: () => void;
   onClick?: any;
-}
+};
 
 const TitleSection: React.FC<Props> = ({
   children,

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

@@ -1,9 +1,9 @@
 import React, { Component } from "react";
-import styled from "styled-components";
 import AceEditor from "react-ace";
+import styled from "styled-components";
 
 import "shared/ace-porter-theme";
-import 'ace-builds/src-noconflict/ext-searchbox';
+import "ace-builds/src-noconflict/ext-searchbox";
 import "ace-builds/src-noconflict/mode-yaml";
 
 type PropsType = {

+ 2 - 2
dashboard/src/components/date-time-picker/DateTimePicker.tsx

@@ -1,9 +1,9 @@
 import React, { useState } from "react";
-
 import DatePicker from "react-datepicker";
+import styled from "styled-components";
+
 import time from "assets/time.svg";
 
-import styled from "styled-components";
 import "./react-datepicker.css";
 
 type Props = {

+ 6 - 4
dashboard/src/components/date-time-picker/react-datepicker.css

@@ -223,7 +223,9 @@
   right: 10px;
   border-left-color: #ccc;
 }
-.react-datepicker__navigation--next--with-time:not(.react-datepicker__navigation--next--with-today-button) {
+.react-datepicker__navigation--next--with-time:not(
+    .react-datepicker__navigation--next--with-today-button
+  ) {
   right: 94px;
 }
 .react-datepicker__navigation--next:hover {
@@ -371,7 +373,7 @@
   li.react-datepicker__time-list-item:hover {
   cursor: pointer;
   background-color: #525882;
-  border-radius: 0.3rem
+  border-radius: 0.3rem;
 }
 .react-datepicker__time-container
   .react-datepicker__time
@@ -381,7 +383,7 @@
   background-color: #949eff;
   color: white;
   font-weight: bold;
-  border-radius: 0.3rem
+  border-radius: 0.3rem;
 }
 .react-datepicker__time-container
   .react-datepicker__time
@@ -421,7 +423,7 @@
   background-color: #26292e;
 }
 .react-datepicker__month {
-  cursor: default
+  cursor: default;
 }
 .react-datepicker__day-names,
 .react-datepicker__week {

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

@@ -26,7 +26,9 @@ const CheckboxList = ({ label, options, selected, setSelected }: PropsType) => {
         return (
           <CheckboxOption
             isLast={i === options.length - 1}
-            onClick={() => { onSelectOption(option); }}
+            onClick={() => {
+              onSelectOption(option);
+            }}
             key={i}
           >
             <Checkbox checked={selected.includes(option)}>

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

@@ -20,7 +20,7 @@ export default function Heading(props: {
 
 const StyledHeading = styled.div<{ isAtTop: boolean }>`
   color: white;
-  margin-top: ${props => props.isAtTop ? "" : "40px"};
+  margin-top: ${(props) => (props.isAtTop ? "" : "40px")};
   font-size: 16px;
   margin-bottom: 5px;
   display: flex;

+ 1 - 1
dashboard/src/components/form-components/Helper.tsx

@@ -2,7 +2,7 @@ import React from "react";
 import styled from "styled-components";
 
 export const Helper = styled.div<{ color?: string }>`
-  color: ${({ color }) => (color || "#aaaabb")};
+  color: ${({ color }) => color || "#aaaabb"};
   line-height: 1.6em;
   font-size: 13px;
   margin-bottom: 20px;

+ 5 - 3
dashboard/src/components/form-components/InputRow.tsx

@@ -1,4 +1,4 @@
-import React, { type ChangeEvent, Component } from "react";
+import React, { Component, type ChangeEvent } from "react";
 import Tooltip from "@material-ui/core/Tooltip";
 import styled from "styled-components";
 
@@ -69,7 +69,9 @@ export default class InputRow extends Component<PropsType, StateType> {
         <InputWrapper hasError={this.props.hasError} width={width}>
           <Input
             readOnly={this.state.readOnly}
-            onFocus={() => { this.setState({ readOnly: false }); }}
+            onFocus={() => {
+              this.setState({ readOnly: false });
+            }}
             disabled={this.props.disabled}
             placeholder={placeholder}
             width={width}
@@ -120,7 +122,7 @@ const Input = styled.input<{ disabled: boolean; width: string }>`
   outline: none;
   border: none;
   font-size: 13px;
-  background: ${props => props.theme.fg};
+  background: ${(props) => props.theme.fg};
   cursor: ${(props) => (props.disabled ? "not-allowed" : "")};
   width: ${(props) => (props.width ? props.width : "100%")};
   color: ${(props) => (props.disabled ? "#ffffff44" : "white")};

+ 25 - 13
dashboard/src/components/form-components/KeyValueArray.tsx

@@ -1,13 +1,15 @@
 import React, { Component } from "react";
 import styled from "styled-components";
-import Modal from "../../main/home/modals/Modal";
-import LoadEnvGroupModal from "../../main/home/modals/LoadEnvGroupModal";
-import EnvEditorModal from "../../main/home/modals/EnvEditorModal";
-import { dotenv_parse } from "shared/string_utils";
 
+import { MultiLineInput } from "components/porter-form/field-components/KeyValueArray";
+
+import { dotenv_parse } from "shared/string_utils";
 import sliders from "assets/sliders.svg";
 import upload from "assets/upload.svg";
-import { MultiLineInput } from "components/porter-form/field-components/KeyValueArray";
+
+import EnvEditorModal from "../../main/home/modals/EnvEditorModal";
+import LoadEnvGroupModal from "../../main/home/modals/LoadEnvGroupModal";
+import Modal from "../../main/home/modals/Modal";
 
 export type KeyValue = {
   key: string;
@@ -179,7 +181,9 @@ export default class KeyValueArray extends Component<PropsType, StateType> {
     if (this.state.showEnvModal) {
       return (
         <Modal
-          onRequestClose={() => { this.setState({ showEnvModal: false }); }}
+          onRequestClose={() => {
+            this.setState({ showEnvModal: false });
+          }}
           width="765px"
           height="542px"
         >
@@ -187,7 +191,9 @@ export default class KeyValueArray extends Component<PropsType, StateType> {
             existingValues={this.props.values}
             namespace={this.props.externalValues?.namespace}
             clusterId={this.props.externalValues?.clusterId}
-            closeModal={() => { this.setState({ showEnvModal: false }); }}
+            closeModal={() => {
+              this.setState({ showEnvModal: false });
+            }}
             setValues={(values) => {
               const newValues = { ...this.props.values, ...values };
               this.props.setValues(newValues);
@@ -204,13 +210,19 @@ export default class KeyValueArray extends Component<PropsType, StateType> {
     if (this.state.showEditorModal) {
       return (
         <Modal
-          onRequestClose={() => { this.setState({ showEditorModal: false }); }}
+          onRequestClose={() => {
+            this.setState({ showEditorModal: false });
+          }}
           width="60%"
           height="80%"
         >
           <EnvEditorModal
-            closeModal={() => { this.setState({ showEditorModal: false }); }}
-            setEnvVariables={(envFile: string) => { this.readFile(envFile); }}
+            closeModal={() => {
+              this.setState({ showEditorModal: false });
+            }}
+            setEnvVariables={(envFile: string) => {
+              this.readFile(envFile);
+            }}
           />
         </Modal>
       );
@@ -262,9 +274,9 @@ export default class KeyValueArray extends Component<PropsType, StateType> {
               <Spacer />
               {this.props.externalValues?.namespace && this.props.envLoader && (
                 <LoadButton
-                  onClick={() =>
-                    { this.setState({ showEnvModal: !this.state.showEnvModal }); }
-                  }
+                  onClick={() => {
+                    this.setState({ showEnvModal: !this.state.showEnvModal });
+                  }}
                 >
                   <img src={sliders} /> Load from Env Group
                 </LoadButton>

+ 4 - 4
dashboard/src/components/form-components/SelectRow.tsx

@@ -72,10 +72,10 @@ const Label = styled.div<{ displayFlex?: boolean }>`
   color: #ffffff;
   font-size: 13px;
   margin-bottom: 10px;
-  margin-top: ${props => props.displayFlex ? "10px" : 0};
-  margin-right: ${props => props.displayFlex ? "10px" : 0};
+  margin-top: ${(props) => (props.displayFlex ? "10px" : 0)};
+  margin-right: ${(props) => (props.displayFlex ? "10px" : 0)};
 `;
 
 const StyledSelectRow = styled.div<{ displayFlex?: boolean }>`
-  display: ${props => props.displayFlex ? "flex" : "block"};
-`;
+  display: ${(props) => (props.displayFlex ? "flex" : "block")};
+`;

+ 1 - 0
dashboard/src/components/form-components/UploadArea.tsx

@@ -1,5 +1,6 @@
 import React, { Component } from "react";
 import styled from "styled-components";
+
 import upload from "assets/upload.svg";
 
 type PropsType = {

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

@@ -11,29 +11,29 @@ import TagList from "./TagList";
 
 type PropsType =
   | {
-    selectedImageUrl: string | null;
-    selectedTag: string | null;
-    clickedImage: ImageType | null;
-    registry?: any;
-    noTagSelection?: boolean;
-    setSelectedImageUrl: (x: string) => void;
-    setSelectedTag: (x: string) => void;
-    setClickedImage: (x: ImageType) => void;
-    disableImageSelect?: boolean;
-    readOnly?: boolean;
-  }
+      selectedImageUrl: string | null;
+      selectedTag: string | null;
+      clickedImage: ImageType | null;
+      registry?: any;
+      noTagSelection?: boolean;
+      setSelectedImageUrl: (x: string) => void;
+      setSelectedTag: (x: string) => void;
+      setClickedImage: (x: ImageType) => void;
+      disableImageSelect?: boolean;
+      readOnly?: boolean;
+    }
   | {
-    selectedImageUrl: string | null;
-    selectedTag: string | null;
-    clickedImage: ImageType | null;
-    registry?: any;
-    noTagSelection?: boolean;
-    setSelectedImageUrl?: (x: string) => void;
-    setSelectedTag?: (x: string) => void;
-    setClickedImage?: (x: ImageType) => void;
-    disableImageSelect?: boolean;
-    readOnly: true;
-  };
+      selectedImageUrl: string | null;
+      selectedTag: string | null;
+      clickedImage: ImageType | null;
+      registry?: any;
+      noTagSelection?: boolean;
+      setSelectedImageUrl?: (x: string) => void;
+      setSelectedTag?: (x: string) => void;
+      setClickedImage?: (x: ImageType) => void;
+      disableImageSelect?: boolean;
+      readOnly: true;
+    };
 
 type StateType = {
   loading: boolean;
@@ -165,12 +165,12 @@ export default class ImageList extends Component<PropsType, StateType> {
             error: false,
           });
         })
-        .catch((err) =>
-          { this.setState({
+        .catch((err) => {
+          this.setState({
             loading: false,
             error: true,
-          }); }
-        );
+          });
+        });
     }
   }
 
@@ -190,8 +190,7 @@ export default class ImageList extends Component<PropsType, StateType> {
     }
 
     return images.map((image: ImageType, i: number) => {
-      let icon =
-        integrationList[image.kind]?.icon;
+      let icon = integrationList[image.kind]?.icon;
       if (!icon) {
         icon = integrationList.dockerhub.icon;
       }
@@ -213,7 +212,8 @@ export default class ImageList extends Component<PropsType, StateType> {
   };
 
   renderBackButton = () => {
-    const { setSelectedImageUrl, clickedImage, disableImageSelect } = this.props;
+    const { setSelectedImageUrl, clickedImage, disableImageSelect } =
+      this.props;
     if (clickedImage && !disableImageSelect) {
       return (
         <BackButton

+ 29 - 31
dashboard/src/components/image-selector/ImageSelector.tsx

@@ -1,36 +1,36 @@
 import React, { Component } from "react";
 import styled from "styled-components";
-import info from "assets/info.svg";
-import edit from "assets/edit.svg";
 
 import { integrationList } from "shared/common";
 import { Context } from "shared/Context";
 import { type ImageType } from "shared/types";
+import edit from "assets/edit.svg";
+import info from "assets/info.svg";
 
 import Loading from "../Loading";
 import ImageList from "./ImageList";
 
 type PropsType =
   | {
-    forceExpanded?: boolean;
-    selectedImageUrl: string | null;
-    selectedTag: string | null;
-    setSelectedImageUrl: (x: string) => void;
-    setSelectedTag: (x: string) => void;
-    noTagSelection?: boolean;
-    disableImageSelect?: boolean;
-    readOnly?: boolean;
-  }
+      forceExpanded?: boolean;
+      selectedImageUrl: string | null;
+      selectedTag: string | null;
+      setSelectedImageUrl: (x: string) => void;
+      setSelectedTag: (x: string) => void;
+      noTagSelection?: boolean;
+      disableImageSelect?: boolean;
+      readOnly?: boolean;
+    }
   | {
-    forceExpanded?: boolean;
-    selectedImageUrl: string | null;
-    selectedTag: string | null;
-    setSelectedImageUrl?: (x: string) => void;
-    setSelectedTag?: (x: string) => void;
-    noTagSelection?: boolean;
-    disableImageSelect?: boolean;
-    readOnly: true;
-  };
+      forceExpanded?: boolean;
+      selectedImageUrl: string | null;
+      selectedTag: string | null;
+      setSelectedImageUrl?: (x: string) => void;
+      setSelectedTag?: (x: string) => void;
+      noTagSelection?: boolean;
+      disableImageSelect?: boolean;
+      readOnly: true;
+    };
 
 type StateType = {
   isExpanded: boolean;
@@ -65,8 +65,7 @@ export default class ImageSelector extends Component<PropsType, StateType> {
     }
 
     return images.map((image: ImageType, i: number) => {
-      let icon =
-        integrationList[image.kind]?.icon;
+      let icon = integrationList[image.kind]?.icon;
       if (!icon) {
         icon = integrationList.dockerhub.icon;
       }
@@ -93,8 +92,7 @@ export default class ImageSelector extends Component<PropsType, StateType> {
     let icon = info;
     if (clickedImage) {
       icon = clickedImage.kind;
-      icon =
-        integrationList[clickedImage.kind]?.icon;
+      icon = integrationList[clickedImage.kind]?.icon;
       if (!icon) {
         icon = integrationList.dockerhub.icon;
       }
@@ -149,9 +147,9 @@ export default class ImageSelector extends Component<PropsType, StateType> {
             noTagSelection={this.props.noTagSelection}
             setSelectedImageUrl={this.props.setSelectedImageUrl}
             setSelectedTag={this.props.setSelectedTag}
-            setClickedImage={(x: ImageType) =>
-              { this.setState({ clickedImage: x }); }
-            }
+            setClickedImage={(x: ImageType) => {
+              this.setState({ clickedImage: x });
+            }}
             readOnly
           />
         </>
@@ -182,9 +180,9 @@ export default class ImageSelector extends Component<PropsType, StateType> {
             noTagSelection={this.props.noTagSelection}
             setSelectedImageUrl={this.props.setSelectedImageUrl}
             setSelectedTag={this.props.setSelectedTag}
-            setClickedImage={(x: ImageType) =>
-              { this.setState({ clickedImage: x }); }
-            }
+            setClickedImage={(x: ImageType) => {
+              this.setState({ clickedImage: x });
+            }}
           />
         ) : null}
       </div>
@@ -244,7 +242,7 @@ const ImageItem = styled.div`
   user-select: text;
   cursor: text;
     ${(props: { lastItem: boolean; isSelected: boolean }) =>
-    props.lastItem ? "#00000000" : "#606166"};
+      props.lastItem ? "#00000000" : "#606166"};
   color: #ffffff;
   align-items: center;
   padding: 10px 0px;

+ 24 - 17
dashboard/src/components/image-selector/TagList.tsx

@@ -1,30 +1,31 @@
 import React, { Component } from "react";
 import styled from "styled-components";
-import tag_icon from "assets/tag.png";
-import info from "assets/info.svg";
 
 import api from "shared/api";
 import { Context } from "shared/Context";
+import info from "assets/info.svg";
+import tag_icon from "assets/tag.png";
 
 import Loading from "../Loading";
 
-const ecrRepoRegex = /(^[a-zA-Z0-9][a-zA-Z0-9-_]*)\.dkr\.ecr(\-fips)?\.([a-zA-Z0-9][a-zA-Z0-9-_]*)\.amazonaws\.com(\.cn)?/gim;
+const ecrRepoRegex =
+  /(^[a-zA-Z0-9][a-zA-Z0-9-_]*)\.dkr\.ecr(\-fips)?\.([a-zA-Z0-9][a-zA-Z0-9-_]*)\.amazonaws\.com(\.cn)?/gim;
 
 type PropsType =
   | {
-    setSelectedTag: (x: string) => void;
-    selectedTag: string;
-    selectedImageUrl: string;
-    registryId: number;
-    readOnly?: boolean;
-  }
+      setSelectedTag: (x: string) => void;
+      selectedTag: string;
+      selectedImageUrl: string;
+      registryId: number;
+      readOnly?: boolean;
+    }
   | {
-    setSelectedTag?: (x: string) => void;
-    selectedTag: string;
-    selectedImageUrl: string;
-    registryId: number;
-    readOnly: true;
-  };
+      setSelectedTag?: (x: string) => void;
+      selectedTag: string;
+      selectedImageUrl: string;
+      registryId: number;
+      readOnly: true;
+    };
 
 type StateType = {
   loading: boolean;
@@ -86,7 +87,11 @@ export default class TagList extends Component<PropsType, StateType> {
           const [latestImage] = tags.splice(latestImageIndex, 1);
           tags.unshift(latestImage);
         }
-        this.setState({ tags: tags.map((tag) => tag.tag), loading: false, error: false });
+        this.setState({
+          tags: tags.map((tag) => tag.tag),
+          loading: false,
+          error: false,
+        });
       })
       .catch((err) => {
         console.log(err);
@@ -124,7 +129,9 @@ export default class TagList extends Component<PropsType, StateType> {
           key={i}
           isSelected={tag === this.state.currentTag}
           lastItem={i === tags.length - 1}
-          onClick={() => { this.setTag(tag); }}
+          onClick={() => {
+            this.setTag(tag);
+          }}
         >
           <img src={tag_icon} />
           {tag}

+ 28 - 23
dashboard/src/components/porter-form/FormDebugger.tsx

@@ -1,17 +1,20 @@
 import React, { Component } from "react";
-import styled from "styled-components";
+import yaml from "js-yaml";
 import AceEditor from "react-ace";
-import PorterFormWrapper from "./PorterFormWrapper";
+import styled from "styled-components";
+
 import CheckboxRow from "components/form-components/CheckboxRow";
 import InputRow from "components/form-components/InputRow";
-import yaml from "js-yaml";
+
+import PorterFormWrapper from "./PorterFormWrapper";
 
 import "shared/ace-porter-theme";
 import "ace-builds/src-noconflict/mode-text";
 
+import { type ChartType } from "shared/types";
+
 import Heading from "../form-components/Heading";
 import Helper from "../form-components/Helper";
-import { type ChartType } from "shared/types";
 
 type PropsType = {
   goBack: () => void;
@@ -78,7 +81,9 @@ export default class FormDebugger extends Component<PropsType, StateType> {
             mode="yaml"
             value={this.state.rawYaml}
             theme="porter"
-            onChange={(e: string) => { this.setState({ rawYaml: e }); }}
+            onChange={(e: string) => {
+              this.setState({ rawYaml: e });
+            }}
             name="codeEditor"
             editorProps={{ $blockScrolling: true }}
             height="450px"
@@ -98,31 +103,31 @@ export default class FormDebugger extends Component<PropsType, StateType> {
         <CheckboxRow
           label="Show form state debugger"
           checked={this.state.showStateDebugger}
-          toggle={() =>
-            { this.setState({ showStateDebugger: !this.state.showStateDebugger }); }
-          }
+          toggle={() => {
+            this.setState({ showStateDebugger: !this.state.showStateDebugger });
+          }}
         />
         <CheckboxRow
           label="Read-only"
           checked={this.state.isReadOnly}
-          toggle={() =>
-            { this.setState({
+          toggle={() => {
+            this.setState({
               isReadOnly: !this.state.isReadOnly,
-            }); }
-          }
+            });
+          }}
         />
         <CheckboxRow
           label="Include non-form dummy tabs"
           checked={this.state.showBonusTabs}
-          toggle={() =>
-            { this.setState({ showBonusTabs: !this.state.showBonusTabs }); }
-          }
+          toggle={() => {
+            this.setState({ showBonusTabs: !this.state.showBonusTabs });
+          }}
         />
         <CheckboxRow
           label="checkbox_a"
           checked={this.state.checkbox_a}
-          toggle={() =>
-            { this.setState({
+          toggle={() => {
+            this.setState({
               checkbox_a: !this.state.checkbox_a,
 
               // Override the form value for checkbox_a
@@ -132,14 +137,14 @@ export default class FormDebugger extends Component<PropsType, StateType> {
                   value: !this.state.checkbox_a,
                 },
               },
-            }); }
-          }
+            });
+          }}
         />
         <InputRow
           type="string"
           value={this.state.input_a}
-          setValue={(x: string) =>
-            { this.setState({
+          setValue={(x: string) => {
+            this.setState({
               input_a: x,
 
               // Override the form value for input_a
@@ -149,8 +154,8 @@ export default class FormDebugger extends Component<PropsType, StateType> {
                   value: x,
                 },
               },
-            }); }
-          }
+            });
+          }}
           label={"input_a"}
           placeholder="ex: override text"
         />

+ 46 - 35
dashboard/src/components/porter-form/PorterForm.tsx

@@ -1,8 +1,32 @@
 import React, { useContext } from "react";
+import styled from "styled-components";
+
+import Button from "components/porter/Button";
+
+import Heading from "../form-components/Heading";
+import Helper from "../form-components/Helper";
+import SaveButton from "../SaveButton";
+import TabRegion, { type TabOption } from "../TabRegion";
+import ArrayInput from "./field-components/ArrayInput";
+import Checkbox from "./field-components/Checkbox";
+import CronInput from "./field-components/CronInput";
+import Dictionary from "./field-components/Dictionary";
+import DictionaryArray from "./field-components/DictionaryArray";
+import Input from "./field-components/Input";
+import KeyValueArray from "./field-components/KeyValueArray";
+import ResourceList from "./field-components/ResourceList";
+import Select from "./field-components/Select";
+import ServiceIPList from "./field-components/ServiceIPList";
+import TextAreaInput from "./field-components/TextAreaInput";
+import UrlLink from "./field-components/UrlLink";
+import VeleroForm from "./field-components/VeleroForm";
+import { PorterFormContext } from "./PorterFormContextProvider";
 import {
   type ArrayInputField,
   type CheckboxField,
   type CronField,
+  type DictionaryArrayField,
+  type DictionaryField,
   type FormField,
   type InjectedProps,
   type InputField,
@@ -13,29 +37,7 @@ import {
   type ServiceIPListField,
   type TextAreaField,
   type UrlLinkField,
-  type DictionaryField,
-  type DictionaryArrayField,
 } from "./types";
-import TabRegion, { type TabOption } from "../TabRegion";
-import Heading from "../form-components/Heading";
-import Helper from "../form-components/Helper";
-import Input from "./field-components/Input";
-import { PorterFormContext } from "./PorterFormContextProvider";
-import Checkbox from "./field-components/Checkbox";
-import KeyValueArray from "./field-components/KeyValueArray";
-import styled from "styled-components";
-import SaveButton from "../SaveButton";
-import ArrayInput from "./field-components/ArrayInput";
-import Select from "./field-components/Select";
-import ServiceIPList from "./field-components/ServiceIPList";
-import ResourceList from "./field-components/ResourceList";
-import VeleroForm from "./field-components/VeleroForm";
-import CronInput from "./field-components/CronInput";
-import TextAreaInput from "./field-components/TextAreaInput";
-import UrlLink from "./field-components/UrlLink";
-import Button from "components/porter/Button";
-import DictionaryArray from "./field-components/DictionaryArray";
-import Dictionary from "./field-components/Dictionary";
 
 type Props = {
   leftTabOptions?: TabOption[];
@@ -61,20 +63,19 @@ type Props = {
   injectedProps?: InjectedProps;
 
   absoluteSave: boolean;
-}
+};
 
 const PorterForm: React.FC<Props> = (props) => {
-  const {
-    formData,
-    isReadOnly,
-    validationInfo,
-    onSubmit,
-    formState,
-  } = useContext(PorterFormContext);
+  const { formData, isReadOnly, validationInfo, onSubmit, formState } =
+    useContext(PorterFormContext);
 
   const { currentTab, setCurrentTab } = props;
 
-  const renderSectionField = (field: FormField, num?: number, i?: number): JSX.Element => {
+  const renderSectionField = (
+    field: FormField,
+    num?: number,
+    i?: number
+  ): JSX.Element => {
     const injected = props.injectedProps?.[field.type];
 
     const bundledProps = {
@@ -87,7 +88,17 @@ const PorterForm: React.FC<Props> = (props) => {
       case "heading":
         // Remove top margin from heading if it's the first form element in the tab
         // TODO: Handle Job form and form variables more gracefully
-        return <Heading isAtTop={num + i < 1 || (formData.name === "Job" && num + i === 1) || (formData.name === "Worker" && num + i === 1)}>{field.label}</Heading>;
+        return (
+          <Heading
+            isAtTop={
+              num + i < 1 ||
+              (formData.name === "Job" && num + i === 1) ||
+              (formData.name === "Worker" && num + i === 1)
+            }
+          >
+            {field.label}
+          </Heading>
+        );
       case "subtitle":
         return <Helper>{field.label}</Helper>;
       case "input":
@@ -233,7 +244,7 @@ const PorterForm: React.FC<Props> = (props) => {
         {renderTab()}
       </TabRegion>
       <br />
-      {(showSaveButton() && props.buttonStatus === undefined) && (
+      {showSaveButton() && props.buttonStatus === undefined && (
         <SaveButton
           text={props.saveButtonText || "Deploy application"}
           onClick={submit}
@@ -248,7 +259,7 @@ const PorterForm: React.FC<Props> = (props) => {
         />
       )}
       {/* TODO: change button when deploying */}
-      {(props.buttonStatus !== undefined) && (
+      {props.buttonStatus !== undefined && (
         <Button
           onClick={submit}
           status={props.buttonStatus}
@@ -288,6 +299,6 @@ const StyledPorterForm = styled.div<{ showSave?: boolean }>`
   margin-bottom: 5px;
   font-size: 13px;
   border-radius: 5px;
-  background: ${props => props.theme.fg};
+  background: ${(props) => props.theme.fg};
   border: 1px solid #494b4f;
 `;

+ 108 - 97
dashboard/src/components/porter-form/PorterFormContextProvider.tsx

@@ -1,13 +1,6 @@
 import React, { createContext, useContext, useReducer } from "react";
-import {
-  type GetFinalVariablesFunction,
-  type GetMetadataFunction,
-  type PorterFormAction,
-  type PorterFormData,
-  type PorterFormState,
-  type PorterFormValidationInfo,
-  type PorterFormVariableList,
-} from "./types";
+
+import { Context } from "../../shared/Context";
 import {
   type ShowIf,
   type ShowIfAnd,
@@ -15,15 +8,23 @@ import {
   type ShowIfNot,
   type ShowIfOr,
 } from "../../shared/types";
+import { getFinalVariablesForArrayInput } from "./field-components/ArrayInput";
+import { getFinalVariablesForCheckbox } from "./field-components/Checkbox";
 import { getFinalVariablesForStringInput } from "./field-components/Input";
 import {
   getFinalVariablesForKeyValueArray,
   getMetadata as getMetadataForKeyValueArray,
 } from "./field-components/KeyValueArray";
-import { Context } from "../../shared/Context";
-import { getFinalVariablesForArrayInput } from "./field-components/ArrayInput";
-import { getFinalVariablesForCheckbox } from "./field-components/Checkbox";
 import { getFinalVariablesForSelect } from "./field-components/Select";
+import {
+  type GetFinalVariablesFunction,
+  type GetMetadataFunction,
+  type PorterFormAction,
+  type PorterFormData,
+  type PorterFormState,
+  type PorterFormValidationInfo,
+  type PorterFormVariableList,
+} from "./types";
 
 export type BaseProps = {
   rawFormData: PorterFormData;
@@ -32,7 +33,7 @@ export type BaseProps = {
   includeHiddenFields?: boolean;
   isReadOnly?: boolean;
   doDebug?: boolean;
-}
+};
 
 export type PropsWithMetadata = {
   onSubmit: (
@@ -40,12 +41,12 @@ export type PropsWithMetadata = {
     cb?: () => void
   ) => void;
   includeMetadata: true;
-} & BaseProps
+} & BaseProps;
 
 export type PropsWithoutMetadata = {
   onSubmit: (vars: PorterFormVariableList, cb?: () => void) => void;
   includeMetadata: false;
-} & BaseProps
+} & BaseProps;
 
 export type Props = PropsWithMetadata | PropsWithoutMetadata;
 
@@ -57,7 +58,7 @@ type ContextProps = {
   validationInfo: PorterFormValidationInfo;
   getSubmitValues: () => PorterFormVariableList;
   isReadOnly?: boolean;
-}
+};
 
 export const PorterFormContext = createContext<ContextProps | undefined>(
   undefined
@@ -148,14 +149,16 @@ export const PorterFormContextProvider: React.FC<Props> = (props) => {
   // get variables initiated by variable field
   const getInitialVariables = (data: PorterFormData) => {
     const ret: Record<string, any> = {};
-    data?.tabs?.map((tab) =>
-      tab.sections?.map((section) =>
-        section.contents?.map((field) => {
-          if (field?.type == "variable") {
-            ret[field.variable] = field.settings?.default;
-          }
-        })
-      )
+    data?.tabs?.map(
+      (tab) =>
+        tab.sections?.map(
+          (section) =>
+            section.contents?.map((field) => {
+              if (field?.type == "variable") {
+                ret[field.variable] = field.settings?.default;
+              }
+            })
+        )
     );
 
     let scopedVars = {};
@@ -179,27 +182,29 @@ export const PorterFormContextProvider: React.FC<Props> = (props) => {
 
   const getInitialValidation = (data: PorterFormData) => {
     const ret: Record<string, any> = {};
-    data?.tabs?.map((tab, i) =>
-      tab.sections?.map((section, j) =>
-        section.contents?.map((field, k) => {
-          if (
-            field?.type == "heading" ||
-            field?.type == "subtitle" ||
-            field?.type == "resource-list" ||
-            field?.type == "service-ip-list" ||
-            field?.type == "velero-create-backup"
-          )
-            return;
-          if (
-            field.required &&
-            (field.settings?.default || (field.value?.[0]))
-          ) {
-            ret[`${i}-${j}-${k}`] = {
-              validated: true,
-            };
-          }
-        })
-      )
+    data?.tabs?.map(
+      (tab, i) =>
+        tab.sections?.map(
+          (section, j) =>
+            section.contents?.map((field, k) => {
+              if (
+                field?.type == "heading" ||
+                field?.type == "subtitle" ||
+                field?.type == "resource-list" ||
+                field?.type == "service-ip-list" ||
+                field?.type == "velero-create-backup"
+              )
+                return;
+              if (
+                field.required &&
+                (field.settings?.default || field.value?.[0])
+              ) {
+                ret[`${i}-${j}-${k}`] = {
+                  validated: true,
+                };
+              }
+            })
+        )
     );
     return ret;
   };
@@ -388,28 +393,30 @@ export const PorterFormContextProvider: React.FC<Props> = (props) => {
   ): [string[], Record<string, string[]>] => {
     const requiredIds: string[] = [];
     const mapping: Record<string, string[]> = {};
-    data?.tabs?.map((tab) =>
-      tab.sections?.map((section) =>
-        section.contents?.map((field) => {
-          if (
-            field?.type == "heading" ||
-            field?.type == "subtitle" ||
-            field?.type == "resource-list" ||
-            field?.type == "service-ip-list" ||
-            field?.type == "velero-create-backup"
-          )
-            return;
-          // fields that have defaults can't be required since we can always
-          // compute their value
-          if (field.required) {
-            requiredIds.push(field.id);
-          }
-          if (!mapping[field.variable]) {
-            mapping[field.variable] = [];
-          }
-          mapping[field.variable].push(field.id);
-        })
-      )
+    data?.tabs?.map(
+      (tab) =>
+        tab.sections?.map(
+          (section) =>
+            section.contents?.map((field) => {
+              if (
+                field?.type == "heading" ||
+                field?.type == "subtitle" ||
+                field?.type == "resource-list" ||
+                field?.type == "service-ip-list" ||
+                field?.type == "velero-create-backup"
+              )
+                return;
+              // fields that have defaults can't be required since we can always
+              // compute their value
+              if (field.required) {
+                requiredIds.push(field.id);
+              }
+              if (!mapping[field.variable]) {
+                mapping[field.variable] = [];
+              }
+              mapping[field.variable].push(field.id);
+            })
+        )
     );
     return [requiredIds, mapping];
   };
@@ -454,21 +461,23 @@ export const PorterFormContextProvider: React.FC<Props> = (props) => {
       ? restructureToNewFields(props.rawFormData)
       : formData;
 
-    data?.tabs?.map((tab) =>
-      tab.sections?.map((section) =>
-        section.contents?.map((field) => {
-          if (finalFunctions[field?.type]) {
-            varList.push(
-              finalFunctions[field?.type](
-                state.variables,
-                field,
-                state.components[field.id]?.state,
-                context
-              )
-            );
-          }
-        })
-      )
+    data?.tabs?.map(
+      (tab) =>
+        tab.sections?.map(
+          (section) =>
+            section.contents?.map((field) => {
+              if (finalFunctions[field?.type]) {
+                varList.push(
+                  finalFunctions[field?.type](
+                    state.variables,
+                    field,
+                    state.components[field.id]?.state,
+                    context
+                  )
+                );
+              }
+            })
+        )
     );
 
     if (props.includeMetadata) {
@@ -476,21 +485,23 @@ export const PorterFormContextProvider: React.FC<Props> = (props) => {
         "key-value-array": getMetadataForKeyValueArray,
       };
       const metadataList: PorterFormVariableList[] = [];
-      data?.tabs?.map((tab) =>
-        tab.sections?.map((section) =>
-          section.contents?.map((field) => {
-            if (metadataFunctions[field?.type]) {
-              metadataList.push(
-                metadataFunctions[field?.type](
-                  state.variables,
-                  field,
-                  state.components[field.id]?.state,
-                  context
-                )
-              );
-            }
-          })
-        )
+      data?.tabs?.map(
+        (tab) =>
+          tab.sections?.map(
+            (section) =>
+              section.contents?.map((field) => {
+                if (metadataFunctions[field?.type]) {
+                  metadataList.push(
+                    metadataFunctions[field?.type](
+                      state.variables,
+                      field,
+                      state.components[field.id]?.state,
+                      context
+                    )
+                  );
+                }
+              })
+          )
       );
 
       if (props.doDebug)

+ 2 - 2
dashboard/src/components/porter-form/PorterFormWrapper.tsx

@@ -1,9 +1,9 @@
 import React, { useEffect, useState } from "react";
+import _ from "lodash";
 
 import PorterForm from "./PorterForm";
-import { type InjectedProps, type PorterFormData } from "./types";
 import { PorterFormContextProvider } from "./PorterFormContextProvider";
-import _ from "lodash";
+import { type InjectedProps, type PorterFormData } from "./types";
 
 type PropsType = {
   formData: any;

+ 11 - 14
dashboard/src/components/porter-form/field-components/ArrayInput.tsx

@@ -1,11 +1,12 @@
 import React from "react";
 import styled from "styled-components";
+
+import useFormField from "../hooks/useFormField";
 import {
   type ArrayInputField,
   type ArrayInputFieldState,
   type GetFinalVariablesFunction,
 } from "../types";
-import useFormField from "../hooks/useFormField";
 import { hasSetValue } from "../utils";
 
 // this is used to set validation for the below form component in case
@@ -15,19 +16,15 @@ const validateArray = (arr: any[]) => {
 };
 
 const ArrayInput: React.FC<ArrayInputField> = (props) => {
-  const {
-    state,
-    variables,
-    setVars,
-    setValidation,
-  } = useFormField<ArrayInputFieldState>(props.id, {
-    initVars: {
-      [props.variable]: hasSetValue(props) ? props.value[0] : [],
-    },
-    initValidation: {
-      validated: validateArray(hasSetValue(props) ? props.value[0] : []),
-    },
-  });
+  const { state, variables, setVars, setValidation } =
+    useFormField<ArrayInputFieldState>(props.id, {
+      initVars: {
+        [props.variable]: hasSetValue(props) ? props.value[0] : [],
+      },
+      initValidation: {
+        validated: validateArray(hasSetValue(props) ? props.value[0] : []),
+      },
+    });
 
   if (state == undefined) return <></>;
 

+ 4 - 3
dashboard/src/components/porter-form/field-components/Checkbox.tsx

@@ -1,15 +1,16 @@
 import React from "react";
+
+import CheckboxRow from "../../form-components/CheckboxRow";
+import useFormField from "../hooks/useFormField";
 import {
   type CheckboxField,
   type CheckboxFieldState,
   type GetFinalVariablesFunction,
 } from "../types";
-import CheckboxRow from "../../form-components/CheckboxRow";
-import useFormField from "../hooks/useFormField";
 
 type Props = {
   id: string;
-} & CheckboxField
+} & CheckboxField;
 
 const Checkbox: React.FC<Props> = ({
   id,

+ 6 - 4
dashboard/src/components/porter-form/field-components/CronInput.tsx

@@ -1,13 +1,15 @@
-import InputRow from "components/form-components/InputRow";
 import React from "react";
-import useFormField from "../hooks/useFormField";
-import { type CronField } from "../types";
-import { hasSetValue } from "../utils";
 import { isValidCron } from "cron-validator";
 import CronParser from "cronstrue";
 import styled from "styled-components";
+
 import DocsHelper from "components/DocsHelper";
 import DynamicLink from "components/DynamicLink";
+import InputRow from "components/form-components/InputRow";
+
+import useFormField from "../hooks/useFormField";
+import { type CronField } from "../types";
+import { hasSetValue } from "../utils";
 
 const CronInput: React.FC<CronField> = (props) => {
   const { id, variable, label, placeholder, value, isReadOnly } = props;

+ 17 - 17
dashboard/src/components/porter-form/field-components/Dictionary.tsx

@@ -1,28 +1,26 @@
 import React, { useEffect } from "react";
+
+import DictionaryEditor from "components/porter/DictionaryEditor";
+
 import InputRow from "../../form-components/InputRow";
 import useFormField from "../hooks/useFormField";
 import {
-  type GetFinalVariablesFunction,
   type DictionaryField,
   type DictionaryFieldState,
+  type GetFinalVariablesFunction,
 } from "../types";
-import DictionaryEditor from "components/porter/DictionaryEditor";
 import { hasSetValue } from "../utils";
 
 const Dictionary: React.FC<DictionaryField> = (props) => {
-  const {
-    state,
-    variables,
-    setVars,
-    setValidation,
-  } = useFormField<DictionaryFieldState>(props.id, {
-    initValidation: {
-      validated: hasSetValue(props),
-    },
-    initVars: {
-      [props.variable]: hasSetValue(props) ? props.value[0] : undefined,
-    },
-  });
+  const { state, variables, setVars, setValidation } =
+    useFormField<DictionaryFieldState>(props.id, {
+      initValidation: {
+        validated: hasSetValue(props),
+      },
+      initVars: {
+        [props.variable]: hasSetValue(props) ? props.value[0] : undefined,
+      },
+    });
 
   if (state == undefined) return <></>;
 
@@ -53,8 +51,10 @@ export const getFinalVariablesForStringInput: GetFinalVariablesFunction = (
 ) => {
   const val =
     vars[props.variable] != undefined && vars[props.variable] != null
-      ? vars[props.variable] : hasSetValue(props)
-      ? props.value[0] : undefined;
+      ? vars[props.variable]
+      : hasSetValue(props)
+      ? props.value[0]
+      : undefined;
 
   return {
     [props.variable]:

+ 40 - 41
dashboard/src/components/porter-form/field-components/DictionaryArray.tsx

@@ -1,13 +1,15 @@
 import React from "react";
 import styled from "styled-components";
+
+import DictionaryEditor from "components/porter/DictionaryEditor";
+
+import useFormField from "../hooks/useFormField";
 import {
   type DictionaryArrayField,
   type DictionaryArrayFieldState,
   type GetFinalVariablesFunction,
 } from "../types";
-import useFormField from "../hooks/useFormField";
 import { hasSetValue } from "../utils";
-import DictionaryEditor from "components/porter/DictionaryEditor";
 
 // this is used to set validation for the below form component in case
 // input validation needs to get more complicated in the future
@@ -16,19 +18,15 @@ const validateArray = (arr: any[]) => {
 };
 
 const DictionaryArray: React.FC<DictionaryArrayField> = (props) => {
-  const {
-    state,
-    variables,
-    setVars,
-    setValidation,
-  } = useFormField<DictionaryArrayFieldState>(props.id, {
-    initVars: {
-      [props.variable]: hasSetValue(props) ? props.value[0] : [],
-    },
-    initValidation: {
-      validated: validateArray(hasSetValue(props) ? props.value[0] : []),
-    },
-  });
+  const { state, variables, setVars, setValidation } =
+    useFormField<DictionaryArrayFieldState>(props.id, {
+      initVars: {
+        [props.variable]: hasSetValue(props) ? props.value[0] : [],
+      },
+      initValidation: {
+        validated: validateArray(hasSetValue(props) ? props.value[0] : []),
+      },
+    });
 
   if (state == undefined) return <></>;
 
@@ -62,35 +60,36 @@ const DictionaryArray: React.FC<DictionaryArrayField> = (props) => {
   const renderInputList = (values: string[]) => {
     return (
       <>
-        {values.length > 0 && values.map((value: string, i: number) => {
-          return (
-            <InputWrapper>
-              <DictionaryEditor
-                key={i}
-                value={value}
-                onChange={(e: any) => {
-                  setVars((prev) => {
-                    const val = prev[props.variable]?.map(
-                      (t: string, j: number) => {
-                        return i == j ? e : t;
-                      }
-                    );
-                    setValidation((prev) => {
+        {values.length > 0 &&
+          values.map((value: string, i: number) => {
+            return (
+              <InputWrapper>
+                <DictionaryEditor
+                  key={i}
+                  value={value}
+                  onChange={(e: any) => {
+                    setVars((prev) => {
+                      const val = prev[props.variable]?.map(
+                        (t: string, j: number) => {
+                          return i == j ? e : t;
+                        }
+                      );
+                      setValidation((prev) => {
+                        return {
+                          ...prev,
+                          validated: validateArray(val),
+                        };
+                      });
                       return {
-                        ...prev,
-                        validated: validateArray(val),
+                        [props.variable]: val,
                       };
                     });
-                    return {
-                      [props.variable]: val,
-                    };
-                  });
-                }}
-              />
-              {renderDeleteButton(values, i)}
-            </InputWrapper>
-          );
-        })}
+                  }}
+                />
+                {renderDeleteButton(values, i)}
+              </InputWrapper>
+            );
+          })}
       </>
     );
   };

+ 12 - 15
dashboard/src/components/porter-form/field-components/Input.tsx

@@ -1,4 +1,5 @@
 import React from "react";
+
 import InputRow from "../../form-components/InputRow";
 import useFormField from "../hooks/useFormField";
 import {
@@ -30,21 +31,17 @@ const Input: React.FC<InputField> = (props) => {
     value,
   } = props;
 
-  const {
-    state,
-    variables,
-    setVars,
-    setValidation,
-  } = useFormField<StringInputFieldState>(id, {
-    initValidation: {
-      validated: hasSetValue(props),
-    },
-    initVars: {
-      [variable]: hasSetValue(props)
-        ? clipOffUnit(settings?.unit, value[0])
-        : undefined,
-    },
-  });
+  const { state, variables, setVars, setValidation } =
+    useFormField<StringInputFieldState>(id, {
+      initValidation: {
+        validated: hasSetValue(props),
+      },
+      initVars: {
+        [variable]: hasSetValue(props)
+          ? clipOffUnit(settings?.unit, value[0])
+          : undefined,
+      },
+    });
 
   if (state == undefined) {
     return <></>;

+ 73 - 54
dashboard/src/components/porter-form/field-components/KeyValueArray.tsx

@@ -1,4 +1,21 @@
 import React, { useContext, useEffect, useState } from "react";
+import _, { differenceBy, isObject, omit } from "lodash";
+import styled, { keyframes } from "styled-components";
+
+import Heading from "components/form-components/Heading";
+import Helper from "components/form-components/Helper";
+import Loading from "components/Loading";
+
+import api from "shared/api";
+import { Context } from "shared/Context";
+import { dotenv_parse } from "shared/string_utils";
+
+import sliders from "../../../assets/sliders.svg";
+import upload from "../../../assets/upload.svg";
+import EnvEditorModal from "../../../main/home/modals/EnvEditorModal";
+import LoadEnvGroupModal from "../../../main/home/modals/LoadEnvGroupModal";
+import Modal from "../../../main/home/modals/Modal";
+import useFormField from "../hooks/useFormField";
 import {
   type GetFinalVariablesFunction,
   type GetMetadataFunction,
@@ -7,42 +24,28 @@ import {
   type PartialEnvGroup,
   type PopulatedEnvGroup,
 } from "../types";
-import sliders from "../../../assets/sliders.svg";
-import upload from "../../../assets/upload.svg";
-import styled, { keyframes } from "styled-components";
-import useFormField from "../hooks/useFormField";
-import Modal from "../../../main/home/modals/Modal";
-import LoadEnvGroupModal from "../../../main/home/modals/LoadEnvGroupModal";
-import EnvEditorModal from "../../../main/home/modals/EnvEditorModal";
 import { hasSetValue } from "../utils";
-import _, { isObject, differenceBy, omit } from "lodash";
-import Helper from "components/form-components/Helper";
-import Heading from "components/form-components/Heading";
-import Loading from "components/Loading";
-import api from "shared/api";
-import { Context } from "shared/Context";
-import { dotenv_parse } from "shared/string_utils";
 
 type Props = {
   id: string;
-} & KeyValueArrayField
+} & KeyValueArrayField;
 
 const KeyValueArray: React.FC<Props> = (props) => {
   const { state, setState, variables } = useFormField<KeyValueArrayFieldState>(
     props.id,
     {
       initState: () => {
-        let values = {}
+        let values = {};
         if (props?.value?.length > 0) {
-          values = props.value[0]
+          values = props.value[0];
         }
         const normalValues = Object.entries(values?.normal || {});
         values = omit(values, ["normal", "synced", "build"]);
         return {
           values: hasSetValue(props)
             ? ([...Object.entries(values), ...normalValues]?.map(([k, v]) => {
-              return { key: k, value: v };
-            }) as any[])
+                return { key: k, value: v };
+              }) as any[])
             : [],
           showEnvModal: false,
           showEditorModal: false,
@@ -80,13 +83,13 @@ const KeyValueArray: React.FC<Props> = (props) => {
           .filter(Boolean);
 
         setState(() => ({
-          synced_env_groups: currentProject?.stacks_enabled ?
-            (Array.isArray(values?.synced)
+          synced_env_groups: currentProject?.stacks_enabled
+            ? Array.isArray(values?.synced)
               ? values?.synced
-              : []) :
-            (Array.isArray(populatedEnvGroups)
-              ? populatedEnvGroups
-              : [])
+              : []
+            : Array.isArray(populatedEnvGroups)
+            ? populatedEnvGroups
+            : [],
         }));
         return;
       }
@@ -160,21 +163,23 @@ const KeyValueArray: React.FC<Props> = (props) => {
     if (state.showEditorModal) {
       return (
         <Modal
-          onRequestClose={() =>
-            { setState(() => {
+          onRequestClose={() => {
+            setState(() => {
               return { showEditorModal: false };
-            }); }
-          }
+            });
+          }}
           width="60%"
           height="80%"
         >
           <EnvEditorModal
-            closeModal={() =>
-              { setState(() => {
+            closeModal={() => {
+              setState(() => {
                 return { showEditorModal: false };
-              }); }
-            }
-            setEnvVariables={(envFile: string) => { readFile(envFile); }}
+              });
+            }}
+            setEnvVariables={(envFile: string) => {
+              readFile(envFile);
+            }}
           />
         </Modal>
       );
@@ -195,11 +200,11 @@ const KeyValueArray: React.FC<Props> = (props) => {
     if (state.showEnvModal) {
       return (
         <Modal
-          onRequestClose={() =>
-            { setState(() => {
+          onRequestClose={() => {
+            setState(() => {
               return { showEnvModal: false };
-            }); }
-          }
+            });
+          }}
           width="800px"
           height="542px"
         >
@@ -210,13 +215,13 @@ const KeyValueArray: React.FC<Props> = (props) => {
             availableEnvGroups={props.injectedProps?.availableSyncEnvGroups}
             namespace={variables.namespace}
             clusterId={variables.clusterId}
-            closeModal={() =>
-              { setState(() => {
+            closeModal={() => {
+              setState(() => {
                 return {
                   showEnvModal: false,
                 };
-              }); }
-            }
+              });
+            }}
             setSyncedEnvGroups={(value) => {
               setState((prev) => {
                 return {
@@ -227,10 +232,13 @@ const KeyValueArray: React.FC<Props> = (props) => {
             setValues={(values) => {
               setState((prev) => {
                 // Transform array to object similar on what we receive from setValues
-                const prevValues = prev.values.reduce<Record<string, string>>((acc, currentValue) => {
-                  acc[currentValue.key] = currentValue.value;
-                  return acc;
-                }, {});
+                const prevValues = prev.values.reduce<Record<string, string>>(
+                  (acc, currentValue) => {
+                    acc[currentValue.key] = currentValue.value;
+                    return acc;
+                  },
+                  {}
+                );
 
                 // Deconstruct the two records/objects inside one to merge their values (this will override the old duped vars too)
                 // and convert the new object back to an array usable for the component
@@ -413,13 +421,13 @@ const KeyValueArray: React.FC<Props> = (props) => {
             <Spacer />
             {variables.namespace && props.envLoader && (
               <LoadButton
-                onClick={() =>
-                  { setState((prev) => {
+                onClick={() => {
+                  setState((prev) => {
                     return {
                       showEnvModal: !prev.showEnvModal,
                     };
-                  }); }
-                }
+                  });
+                }}
               >
                 <img src={sliders} /> Load from Env Group
               </LoadButton>
@@ -552,10 +560,13 @@ export const getFinalVariablesForKeyValueArray: GetFinalVariablesFunction = (
   }
 };
 
-type KeyValueArrayMetadata = Record<string, {
+type KeyValueArrayMetadata = Record<
+  string,
+  {
     added: Array<{ name: string }>;
     deleted: Array<{ name: string }>;
-  }>;
+  }
+>;
 
 export const getMetadata: GetMetadataFunction<KeyValueArrayMetadata> = (
   vars,
@@ -612,10 +623,18 @@ const ExpandableEnvGroup: React.FC<{
             </EventInformation>
           </ContentContainer>
           <ActionContainer>
-            <ActionButton onClick={() => { onDelete(); }}>
+            <ActionButton
+              onClick={() => {
+                onDelete();
+              }}
+            >
               <span className="material-icons">delete</span>
             </ActionButton>
-            <ActionButton onClick={() => { setIsExpanded((prev) => !prev); }}>
+            <ActionButton
+              onClick={() => {
+                setIsExpanded((prev) => !prev);
+              }}
+            >
               <i className="material-icons">
                 {isExpanded ? "arrow_drop_up" : "arrow_drop_down"}
               </i>

+ 42 - 43
dashboard/src/components/porter-form/field-components/ResourceList.tsx

@@ -1,22 +1,21 @@
-import React, { useEffect, useContext, useState } from "react";
-import { type ResourceListField } from "../types";
+import React, { useContext, useEffect, useState } from "react";
+import styled from "styled-components";
+
+import { PorterFormContext } from "components/porter-form/PorterFormContextProvider";
+
 import { Context } from "shared/Context";
 import { useWebsockets } from "shared/hooks/useWebsockets";
+
 import ExpandableResource from "../../ExpandableResource";
-import { PorterFormContext } from "components/porter-form/PorterFormContextProvider";
-import styled from "styled-components";
+import { type ResourceListField } from "../types";
 
 const ResourceList: React.FC<ResourceListField> = (props) => {
   const { currentCluster, currentProject } = useContext(Context);
   const { formState } = useContext(PorterFormContext);
   const [resourceList, updateResourceList] = useState<any[]>(props.value);
 
-  const {
-    newWebsocket,
-    openWebsocket,
-    closeAllWebsockets,
-    closeWebsocket,
-  } = useWebsockets();
+  const { newWebsocket, openWebsocket, closeAllWebsockets, closeWebsocket } =
+    useWebsockets();
 
   const sortAndUpdateResources = (list: any[]) => {
     list.sort((a, b) => {
@@ -31,7 +30,7 @@ const ResourceList: React.FC<ResourceListField> = (props) => {
       !formState?.variables?.currentChart?.name ||
       !formState?.variables?.namespace
     ) {
-      return () => { };
+      return () => {};
     }
 
     const { group, version, resource } = props.context.config;
@@ -50,24 +49,25 @@ const ResourceList: React.FC<ResourceListField> = (props) => {
           // attempt to find a corresponding name and label in the current array
           let foundMatch = false;
 
-          Array.isArray(resourceList) && resourceList?.forEach((resource, index) => {
-            if (resource.name == name && resource.label == label) {
-              foundMatch = true;
-
-              switch (kind) {
-                case "update":
-                case "create":
-                  // replace this resource in the list
-                  resourceList[index] = data[key][0];
-                  break;
-                case "delete":
-                  // remove this resource from the list
-                  resourceList.splice(index, 1);
-                  break;
-                default:
+          Array.isArray(resourceList) &&
+            resourceList?.forEach((resource, index) => {
+              if (resource.name == name && resource.label == label) {
+                foundMatch = true;
+
+                switch (kind) {
+                  case "update":
+                  case "create":
+                    // replace this resource in the list
+                    resourceList[index] = data[key][0];
+                    break;
+                  case "delete":
+                    // remove this resource from the list
+                    resourceList.splice(index, 1);
+                    break;
+                  default:
+                }
               }
-            }
-          });
+            });
 
           if (!foundMatch && kind != "delete") {
             // add this resource to the list
@@ -92,21 +92,20 @@ const ResourceList: React.FC<ResourceListField> = (props) => {
 
   return (
     <ResourceListWrapper>
-      {Array.isArray(resourceList) && resourceList?.map((resource: any, i: number) => {
-        if (resource.data) {
-          return (
-            <ExpandableResource
-              key={i}
-              button={
-                props?.settings?.options?.["resource-button"]
-              }
-              resource={resource}
-              isLast={i === resourceList.length - 1}
-              roundAllCorners={true}
-            />
-          );
-        }
-      })}
+      {Array.isArray(resourceList) &&
+        resourceList?.map((resource: any, i: number) => {
+          if (resource.data) {
+            return (
+              <ExpandableResource
+                key={i}
+                button={props?.settings?.options?.["resource-button"]}
+                resource={resource}
+                isLast={i === resourceList.length - 1}
+                roundAllCorners={true}
+              />
+            );
+          }
+        })}
     </ResourceListWrapper>
   );
 };

+ 19 - 15
dashboard/src/components/porter-form/field-components/Select.tsx

@@ -1,13 +1,14 @@
 import React, { useContext } from "react";
+import styled from "styled-components";
+
+import { Context } from "../../../shared/Context";
+import Selector from "../../Selector";
+import useFormField from "../hooks/useFormField";
 import {
   type GetFinalVariablesFunction,
   type SelectField,
   type SelectFieldState,
 } from "../types";
-import Selector from "../../Selector";
-import styled from "styled-components";
-import useFormField from "../hooks/useFormField";
-import { Context } from "../../../shared/Context";
 import { hasSetValue } from "../utils";
 
 const Select: React.FC<SelectField> = (props) => {
@@ -17,11 +18,13 @@ const Select: React.FC<SelectField> = (props) => {
       [props.variable]: hasSetValue(props)
         ? props.value[0]
         : props.settings.type == "provider"
-        ? ({
-            gke: "gcp",
-            eks: "aws",
-            doks: "do",
-          } as Record<string, string>)[currentCluster.service] || "aws"
+        ? (
+            {
+              gke: "gcp",
+              eks: "aws",
+              doks: "do",
+            } as Record<string, string>
+          )[currentCluster.service] || "aws"
         : props.settings.options[0].value,
     },
   });
@@ -74,12 +77,13 @@ export const getFinalVariablesForSelect: GetFinalVariablesFunction = (
         [props.variable]: hasSetValue(props)
           ? props.value[0]
           : props.settings.type == "provider"
-          ? ({
-              gke: "gcp",
-              eks: "aws",
-              doks: "do",
-            } as Record<string, string>)[context.currentCluster.service] ||
-            "aws"
+          ? (
+              {
+                gke: "gcp",
+                eks: "aws",
+                doks: "do",
+              } as Record<string, string>
+            )[context.currentCluster.service] || "aws"
           : props.settings.options[0].value,
       };
 };

+ 2 - 1
dashboard/src/components/porter-form/field-components/ServiceIPList.tsx

@@ -1,7 +1,8 @@
 import React from "react";
+import styled from "styled-components";
+
 import { type ServiceIPListField } from "../types";
 import ServiceRow from "./ServiceRow";
-import styled from "styled-components";
 
 const ServiceIPList: React.FC<ServiceIPListField> = (props) => {
   return (

+ 18 - 14
dashboard/src/components/porter-form/field-components/ServiceRow.tsx

@@ -1,5 +1,6 @@
 import React, { Component } from "react";
 import styled from "styled-components";
+
 import { Context } from "shared/Context";
 import { hardcodedIcons } from "shared/hardcodedNameDict";
 
@@ -21,7 +22,7 @@ const getIcon = (type: string) => {
     return hardcodedIcons[type];
   }
   return hardcodedIcons.web;
-}
+};
 
 export default class ServiceRow extends Component<PropsType, StateType> {
   render() {
@@ -30,19 +31,22 @@ export default class ServiceRow extends Component<PropsType, StateType> {
     type = type || app;
     return (
       <>
-        {name &&
-          type &&
-          namespace !== "kube-system" && (
-            <StyledServiceRow>
-              <Flex>
-                <Icon src={getIcon(type)} />
-                <Name>{name}</Name> <Dash>-</Dash> <IP>{clusterIP}</IP>
-              </Flex>
-              <TagWrapper>
-                Namespace: <NamespaceTag>{namespace.startsWith("porter-stack-") ? namespace.replace("porter-stack-", "") : namespace}</NamespaceTag>
-              </TagWrapper>
-            </StyledServiceRow>
-          )}
+        {name && type && namespace !== "kube-system" && (
+          <StyledServiceRow>
+            <Flex>
+              <Icon src={getIcon(type)} />
+              <Name>{name}</Name> <Dash>-</Dash> <IP>{clusterIP}</IP>
+            </Flex>
+            <TagWrapper>
+              Namespace:{" "}
+              <NamespaceTag>
+                {namespace.startsWith("porter-stack-")
+                  ? namespace.replace("porter-stack-", "")
+                  : namespace}
+              </NamespaceTag>
+            </TagWrapper>
+          </StyledServiceRow>
+        )}
       </>
     );
   }

+ 2 - 1
dashboard/src/components/porter-form/field-components/TextAreaInput.tsx

@@ -1,6 +1,7 @@
-import { Tooltip } from "@material-ui/core";
 import React from "react";
+import { Tooltip } from "@material-ui/core";
 import styled from "styled-components";
+
 import useFormField from "../hooks/useFormField";
 import { type StringInputFieldState, type TextAreaField } from "../types";
 import { hasSetValue } from "../utils";

+ 2 - 1
dashboard/src/components/porter-form/field-components/UrlLink.tsx

@@ -1,6 +1,7 @@
-import { get } from "lodash";
 import React from "react";
+import { get } from "lodash";
 import styled from "styled-components";
+
 import { type UrlLinkField } from "../types";
 import { hasSetValue } from "../utils";
 

+ 3 - 1
dashboard/src/components/porter-form/field-components/VeleroForm.tsx

@@ -36,7 +36,9 @@ export default class VeleroForm extends Component<PropsType, StateType> {
           type="text"
           width="300px"
           value={this.state.name}
-          setValue={(x: string) => { this.setState({ name: x }); }}
+          setValue={(x: string) => {
+            this.setState({ name: x });
+          }}
           label="Name"
         />
         <MultiSelect />

+ 3 - 2
dashboard/src/components/porter-form/hooks/useFormField.tsx

@@ -1,4 +1,5 @@
 import { useContext, useEffect } from "react";
+
 import { PorterFormContext } from "../PorterFormContextProvider";
 import {
   type PorterFormFieldFieldState,
@@ -19,13 +20,13 @@ type FormFieldData<T> = {
       state: PorterFormFieldValidationState
     ) => PorterFormFieldValidationState
   ) => void;
-}
+};
 
 type Options<T> = {
   initState?: T | (() => T);
   initValidation?: Partial<PorterFormFieldValidationState>;
   initVars?: PorterFormVariableList;
-}
+};
 
 const useFormField = <T extends PorterFormFieldFieldState>(
   fieldId: string,

+ 54 - 53
dashboard/src/components/porter-form/types.ts

@@ -10,7 +10,7 @@ import { type ChartType, type ContextProps } from "../../shared/types";
 export type GenericField = {
   id: string;
   injectedProps: unknown;
-}
+};
 
 export type GenericInputField = {
   isReadOnly?: boolean;
@@ -20,22 +20,22 @@ export type GenericInputField = {
 
   // Read in value from Helm for existing revisions
   value?: [any] | [];
-} & GenericField
+} & GenericField;
 
 export type HeadingField = {
   type: "heading";
   label: string;
-} & GenericField
+} & GenericField;
 
 export type SubtitleField = {
   type: "subtitle";
   label: string;
-} & GenericField
+} & GenericField;
 
 export type ServiceIPListField = {
   type: "service-ip-list";
   value: any[];
-} & GenericField
+} & GenericField;
 
 export type ResourceListField = {
   type: "resource-list";
@@ -52,11 +52,11 @@ export type ResourceListField = {
       "resource-button": any;
     };
   };
-} & GenericField
+} & GenericField;
 
 export type VeleroBackupField = {
   type: "velero-create-backup";
-} & GenericField
+} & GenericField;
 
 export type InputField = {
   type: "input";
@@ -69,13 +69,13 @@ export type InputField = {
     omitUnitFromValue?: boolean;
     default: string | number;
   };
-} & GenericInputField
+} & GenericInputField;
 
 export type CheckboxField = {
   type: "checkbox";
   label?: string;
   settings?: {};
-} & GenericInputField
+} & GenericInputField;
 
 export type KeyValueArrayField = {
   type: "key-value-array";
@@ -92,46 +92,46 @@ export type KeyValueArrayField = {
   injectedProps: {
     availableSyncEnvGroups: PopulatedEnvGroup[];
   };
-} & GenericInputField
+} & GenericInputField;
 
 export type ArrayInputField = {
   type: "array-input";
   label?: string;
-} & GenericInputField
+} & GenericInputField;
 
 export type DictionaryField = {
   type: "dictionary";
   label?: string;
-} & GenericInputField
+} & GenericInputField;
 
 export type DictionaryArrayField = {
   type: "dictionary-array";
   label?: string;
-} & GenericInputField
+} & GenericInputField;
 
 export type SelectField = {
   type: "select";
   settings:
-  | {
-    type: "normal";
-    options: Array<{ value: string; label: string }>;
-  }
-  | {
-    type: "provider";
-  };
+    | {
+        type: "normal";
+        options: Array<{ value: string; label: string }>;
+      }
+    | {
+        type: "provider";
+      };
   width: string;
   label?: string;
   dropdownLabel?: string;
   dropdownWidth?: number;
   dropdownMaxHeight?: string;
-} & GenericInputField
+} & GenericInputField;
 
 export type VariableField = {
   type: "variable";
   settings?: {
     default: any;
   };
-} & GenericInputField
+} & GenericInputField;
 
 export type CronField = {
   type: "cron";
@@ -140,7 +140,7 @@ export type CronField = {
   settings: {
     default: string;
   };
-} & GenericInputField
+} & GenericInputField;
 
 export type TextAreaField = {
   type: "text-area";
@@ -154,7 +154,7 @@ export type TextAreaField = {
       minCount?: number;
     };
   };
-} & GenericInputField
+} & GenericInputField;
 
 export type UrlLinkField = {
   type: "url-link";
@@ -162,7 +162,7 @@ export type UrlLinkField = {
   injectedProps: {
     chart: ChartType;
   };
-} & GenericInputField
+} & GenericInputField;
 
 export type FormField =
   | HeadingField
@@ -184,15 +184,15 @@ export type FormField =
 
 export type ShowIfAnd = {
   and: ShowIf[];
-}
+};
 
 export type ShowIfOr = {
   or: ShowIf[];
-}
+};
 
 export type ShowIfNot = {
   not: ShowIf;
-}
+};
 
 export type ShowIf = string | ShowIfAnd | ShowIfOr | ShowIfNot;
 
@@ -200,7 +200,7 @@ export type Section = {
   name: string;
   show_if?: ShowIf;
   contents: FormField[];
-}
+};
 
 export type Tab = {
   name: string;
@@ -209,7 +209,7 @@ export type Tab = {
   settings?: {
     omitFromLaunch?: boolean;
   };
-}
+};
 
 export type PorterFormData = {
   name: string;
@@ -217,16 +217,16 @@ export type PorterFormData = {
   includeHiddenFields: boolean;
   isClusterScoped?: boolean;
   tabs: Tab[];
-}
+};
 
 export type PorterFormValidationInfo = {
   validated: boolean;
   error?: string;
-}
+};
 
 // internal field state interfaces
-export type StringInputFieldState = { }
-export type CheckboxFieldState = { }
+export type StringInputFieldState = {};
+export type CheckboxFieldState = {};
 
 export type PartialEnvGroup = {
   name: string;
@@ -261,11 +261,11 @@ export type KeyValueArrayFieldState = {
   showEnvModal: boolean;
   showEditorModal: boolean;
   synced_env_groups: PopulatedEnvGroup[];
-}
-export type ArrayInputFieldState = { }
-export type DictionaryFieldState = {}
-export type DictionaryArrayFieldState = { }
-export type SelectFieldState = { }
+};
+export type ArrayInputFieldState = {};
+export type DictionaryFieldState = {};
+export type DictionaryArrayFieldState = {};
+export type SelectFieldState = {};
 
 export type PorterFormFieldFieldState =
   | StringInputFieldState
@@ -280,17 +280,20 @@ export type PorterFormFieldFieldState =
 
 export type PorterFormFieldValidationState = {
   validated: boolean;
-}
+};
 
-export type PorterFormVariableList = Record<string, any>
+export type PorterFormVariableList = Record<string, any>;
 
 export type PorterFormState = {
-  components: Record<string, {
+  components: Record<
+    string,
+    {
       state: PorterFormFieldFieldState;
-    }>;
+    }
+  >;
   validation: Record<string, PorterFormFieldValidationState>;
   variables: PorterFormVariableList;
-}
+};
 
 export type PorterFormInitFieldAction = {
   type: "init-field";
@@ -298,7 +301,7 @@ export type PorterFormInitFieldAction = {
   initValue: PorterFormFieldFieldState;
   initValidation?: Partial<PorterFormFieldValidationState>;
   initVars?: PorterFormVariableList;
-}
+};
 
 export type PorterFormUpdateFieldAction = {
   type: "update-field";
@@ -306,7 +309,7 @@ export type PorterFormUpdateFieldAction = {
   updateFunc: (
     prev: PorterFormFieldFieldState
   ) => Partial<PorterFormFieldFieldState>;
-}
+};
 
 export type PorterFormUpdateValidationAction = {
   type: "update-validation";
@@ -314,12 +317,12 @@ export type PorterFormUpdateValidationAction = {
   updateFunc: (
     prev: PorterFormFieldValidationState
   ) => PorterFormFieldValidationState;
-}
+};
 
 export type PorterFormMutateVariablesAction = {
   type: "mutate-vars";
   mutateFunc: (prev: PorterFormVariableList) => PorterFormVariableList;
-}
+};
 
 export type PorterFormAction =
   | PorterFormInitFieldAction
@@ -341,8 +344,6 @@ export type GetMetadataFunction<T = unknown> = (
   context: Partial<ContextProps>
 ) => T;
 
-export type InjectedProps = Partial<
-  {
-    [K in FormField["type"]]: Extract<FormField, { type: K }>["injectedProps"];
-  }
->;
+export type InjectedProps = Partial<{
+  [K in FormField["type"]]: Extract<FormField, { type: K }>["injectedProps"];
+}>;

+ 2 - 0
dashboard/src/components/porter-form/utils.ts

@@ -1,5 +1,7 @@
 import { merge, unionBy } from "lodash";
+
 import { KeyValueType } from "main/home/cluster-dashboard/env-groups/EnvGroupArray";
+
 import { type GenericInputField } from "./types";
 
 export const hasSetValue = (field: GenericInputField) => {

+ 6 - 8
dashboard/src/components/porter/Banner.tsx

@@ -10,9 +10,9 @@ type Props = {
   children: React.ReactNode;
   noMargin?: boolean;
   suffix?: React.ReactNode;
-}
+};
 
-const Banner: React.FC<Props> = ({ 
+const Banner: React.FC<Props> = ({
   type,
   icon,
   children,
@@ -23,7 +23,7 @@ const Banner: React.FC<Props> = ({
     if (icon === "none") {
       return null;
     }
-    
+
     if (icon) {
       return icon;
     }
@@ -40,12 +40,10 @@ const Banner: React.FC<Props> = ({
       noMargin={noMargin}
     >
       <>
-      {renderIcon()}
-      {children}
+        {renderIcon()}
+        {children}
       </>
-      {suffix && (
-        <Suffix>{suffix}</Suffix>
-      )}
+      {suffix && <Suffix>{suffix}</Suffix>}
     </StyledBanner>
   );
 };

+ 10 - 9
dashboard/src/components/porter/ClickToCopy.tsx

@@ -1,8 +1,9 @@
 import React, { useEffect, useState } from "react";
 import styled from "styled-components";
 
-import copy from "assets/copy.svg";
 import check from "assets/check.svg";
+import copy from "assets/copy.svg";
+
 import Text from "./Text";
 
 type Props = {
@@ -47,7 +48,7 @@ const ClickToCopy: React.FC<Props> = ({
         setShowCopyPrompt(false);
         setCopied(false);
       }}
-      onClick= {() => {
+      onClick={() => {
         navigator.clipboard.writeText(children);
         setCopied(true);
       }}
@@ -75,13 +76,13 @@ const ClickToCopy: React.FC<Props> = ({
 export default ClickToCopy;
 
 const Img = styled.img<{ small?: boolean }>`
-  height: ${props => props.small ? "10px" : "12px"};
+  height: ${(props) => (props.small ? "10px" : "12px")};
   margin-right: 5px;
 `;
 
 const CopyPrompt = styled.div<{ width: string }>`
   position: absolute;
-  width: ${props => props.width};
+  width: ${(props) => props.width};
   display: flex;
   align-items: center;
   justify-content: center;
@@ -90,7 +91,7 @@ const CopyPrompt = styled.div<{ width: string }>`
   height: 28px;
   background: #121212;
   z-index: 999;
-  border: 1px solid #494B4F;
+  border: 1px solid #494b4f;
   opacity: 0;
   border-radius: 3px;
   animation: fadeIn 0.5s 0.2s;
@@ -113,13 +114,13 @@ const StyledClickToCopy = styled.div<{
   truncate?: boolean;
 }>`
   line-height: 1.5;
-  font-weight: ${props => props.weight || 400};
-  color: ${props => props.color || props.theme.text.primary};
-  font-size: ${props => props.size || 13}px;
+  font-weight: ${(props) => props.weight || 400};
+  color: ${(props) => props.color || props.theme.text.primary};
+  font-size: ${(props) => props.size || 13}px;
   display: inline;
   align-items: center;
   user-select: text;
-  ${props => props.additionalStyles ? props.additionalStyles : ""}
+  ${(props) => (props.additionalStyles ? props.additionalStyles : "")}
   cursor: pointer;
   position: relative;
 `;

+ 28 - 24
dashboard/src/components/porter/DictionaryEditor.tsx

@@ -1,22 +1,24 @@
 import { ok } from "assert";
 import React, { useEffect, useState } from "react";
 import styled from "styled-components";
+
 import Container from "./Container";
-import Text from "./Text";
 import Spacer from "./Spacer";
+import Text from "./Text";
 
 type Props = {
   value: any;
   onChange: any;
 };
 
-const DictionaryEditor: React.FC<Props> = ({
-  value,
-  onChange,
-}) => {
+const DictionaryEditor: React.FC<Props> = ({ value, onChange }) => {
   const [rawEditor, setRawEditor] = useState<boolean>(true);
-  const [savedValue, setSavedValue] = useState<any>(JSON.stringify(value, null, 2));
-  const [rawValue, setRawValue] = useState<string>(JSON.stringify(value, null, 2));
+  const [savedValue, setSavedValue] = useState<any>(
+    JSON.stringify(value, null, 2)
+  );
+  const [rawValue, setRawValue] = useState<string>(
+    JSON.stringify(value, null, 2)
+  );
   const [changesNotSaved, setChangesNotSaved] = useState<boolean>(false);
   const [isValidJSON, setIsValidJSON] = useState<boolean>(true);
 
@@ -39,9 +41,7 @@ const DictionaryEditor: React.FC<Props> = ({
         <Div>
           <TextArea
             color={
-              !isValidJSON ? "#ff385d" : (
-                changesNotSaved ? "#f5cb42" : "#494b4f"
-              )
+              !isValidJSON ? "#ff385d" : changesNotSaved ? "#f5cb42" : "#494b4f"
             }
             value={rawValue}
             onChange={(e) => {
@@ -50,21 +50,25 @@ const DictionaryEditor: React.FC<Props> = ({
           />
           {changesNotSaved && (
             <Flex>
-              <SaveButton onClick={() => {
-                try {
-                  const parsedValue = JSON.parse(rawValue);
-                  setIsValidJSON(true);
-                  onChange(parsedValue);
-                  setChangesNotSaved(false);
-                } catch (e) {
-                  setIsValidJSON(false);
-                }
-              }}>
+              <SaveButton
+                onClick={() => {
+                  try {
+                    const parsedValue = JSON.parse(rawValue);
+                    setIsValidJSON(true);
+                    onChange(parsedValue);
+                    setChangesNotSaved(false);
+                  } catch (e) {
+                    setIsValidJSON(false);
+                  }
+                }}
+              >
                 Update
               </SaveButton>
               <Spacer width="10px" inline />
               {isValidJSON ? (
-                <Text color="#f5cb42">Existing changes have not been saved.</Text>
+                <Text color="#f5cb42">
+                  Existing changes have not been saved.
+                </Text>
               ) : (
                 <Text color="#ff385d">Object is not valid.</Text>
               )}
@@ -74,7 +78,7 @@ const DictionaryEditor: React.FC<Props> = ({
       ) : (
         <>
           {Object.keys(value).map((key: string, i: number) => {
-            return <Block>{key}</Block>
+            return <Block>{key}</Block>;
           })}
         </>
       )}
@@ -98,7 +102,7 @@ const Div = styled.div`
   max-width: calc(100% - 30px);
   width: 400px;
   background: #ffffff11;
-  border: 1px solid ${props => props.color || "#494b4f"};
+  border: 1px solid ${(props) => props.color || "#494b4f"};
   border-radius: 5px;
 `;
 
@@ -106,7 +110,7 @@ const SaveButton = styled.div`
   width: 60px;
   border-radius: 3px;
   height: 25px;
-  background: #616FEEcc;
+  background: #616feecc;
   cursor: pointer;
   display: flex;
   align-items: center;

+ 42 - 37
dashboard/src/components/porter/ExpandableSection.tsx

@@ -1,8 +1,10 @@
 import React, { useEffect, useState } from "react";
 import styled from "styled-components";
-import Container from "./Container";
+
 import CopyToClipboard from "components/CopyToClipboard";
 
+import Container from "./Container";
+
 type Props = {
   isInitiallyExpanded?: boolean;
   Header: any;
@@ -41,45 +43,48 @@ const ExpandableSection: React.FC<Props> = ({
       {noWrapper ? (
         <Container row spaced={spaced}>
           {Header}
-          {copy ?
-            (
-              <CopyWrapper>
-                <ExpandButton onClick={() => { setIsExpanded(!isExpanded); }}>
-                  {isExpanded ? collapseText : expandText}
-                </ExpandButton>
-                <CopyToClipboard
-                  as="i"
-                  text={copy}
-                  wrapperProps={{
-                    className: "material-icons",
-                  }}
-                >
-                  content_copy
-                </CopyToClipboard>
-              </CopyWrapper>
-            ) :
-            (
-              <ExpandButton onClick={() => { setIsExpanded(!isExpanded); }}>
+          {copy ? (
+            <CopyWrapper>
+              <ExpandButton
+                onClick={() => {
+                  setIsExpanded(!isExpanded);
+                }}
+              >
                 {isExpanded ? collapseText : expandText}
               </ExpandButton>
-            )
-          }
+              <CopyToClipboard
+                as="i"
+                text={copy}
+                wrapperProps={{
+                  className: "material-icons",
+                }}
+              >
+                content_copy
+              </CopyToClipboard>
+            </CopyWrapper>
+          ) : (
+            <ExpandButton
+              onClick={() => {
+                setIsExpanded(!isExpanded);
+              }}
+            >
+              {isExpanded ? collapseText : expandText}
+            </ExpandButton>
+          )}
         </Container>
       ) : (
         <HeaderRow
           isExpanded={isExpanded}
-          onClick={() => { setIsExpanded(!isExpanded); }}
+          onClick={() => {
+            setIsExpanded(!isExpanded);
+          }}
           color={color}
         >
           {!noWrapper && <i className="material-icons">arrow_drop_down</i>}
           {Header}
         </HeaderRow>
       )}
-      {
-        isExpanded && (
-          ExpandedSection
-        )
-      }
+      {isExpanded && ExpandedSection}
     </StyledExpandableSection>
   );
 };
@@ -109,16 +114,16 @@ const HeaderRow = styled.div<{
   padding-left: 10px;
   cursor: pointer;
   :hover {
-    background: ${props => props.isExpanded && "#ffffff18"};
+    background: ${(props) => props.isExpanded && "#ffffff18"};
   }
 
   > i {
     margin-right: 8px;
-    color: ${props => props.color || "#ffffff66"};
+    color: ${(props) => props.color || "#ffffff66"};
     font-size: 20px;
     cursor: pointer;
     border-radius: 20px;
-    transform: ${props => props.isExpanded ? "" : "rotate(-90deg)"};
+    transform: ${(props) => (props.isExpanded ? "" : "rotate(-90deg)")};
   }
 `;
 
@@ -128,16 +133,16 @@ const StyledExpandableSection = styled.div<{
   noWrapper?: boolean;
 }>`
   width: 100%;
-  height: ${props => (props.isExpanded || props.noWrapper) ? "" : "40px"};
+  height: ${(props) => (props.isExpanded || props.noWrapper ? "" : "40px")};
   max-height: 350px;
   overflow: hidden;
   border-radius: 5px;
-  background: ${props => !props.noWrapper && (props.background || "#181B20")};
-  border: ${props => !props.noWrapper && "1px solid #494b4f"};
+  background: ${(props) => !props.noWrapper && (props.background || "#181B20")};
+  border: ${(props) => !props.noWrapper && "1px solid #494b4f"};
   :hover {
-    border: ${props => !props.noWrapper && "1px solid #7a7b80"};
+    border: ${(props) => !props.noWrapper && "1px solid #7a7b80"};
   }
-  animation: ${props => props.isExpanded ? "expandRevisions 0.3s" : ""};
+  animation: ${(props) => (props.isExpanded ? "expandRevisions 0.3s" : "")};
   animation-timing-function: ease-out;
   @keyframes expandRevisions {
     from {
@@ -153,4 +158,4 @@ const CopyWrapper = styled.div`
   display: flex;
   align-items: center;
   gap: 10px;
-`;
+`;

+ 35 - 22
dashboard/src/components/porter/Filter.tsx

@@ -1,20 +1,22 @@
-import React, {  useMemo, useState } from "react";
+import React, { useMemo, useState } from "react";
 import styled from "styled-components";
-import Select from "./Select";
-import Spacer from "./Spacer";
+
+import {
+  type FilterName,
+  type GenericFilter,
+} from "main/home/app-dashboard/expanded-app/logs/types";
 
 import filter from "assets/filter.svg";
-import { type GenericFilter, type FilterName } from "main/home/app-dashboard/expanded-app/logs/types";
+
+import Select from "./Select";
+import Spacer from "./Spacer";
 
 type Props = {
   filters: GenericFilter[];
   selectedFilterValues: Partial<Record<FilterName, string>>;
 };
 
-const Filter: React.FC<Props> = ({
-  filters,
-  selectedFilterValues,
-}) => {
+const Filter: React.FC<Props> = ({ filters, selectedFilterValues }) => {
   const [isExpanded, setIsExpanded] = useState(false);
 
   const filterLabelString = useMemo(() => {
@@ -26,7 +28,7 @@ const Filter: React.FC<Props> = ({
     if (serviceName && serviceName !== "all") {
       filterString += serviceName;
     } else if (podName && podName !== "all") {
-      filterString += podName.replace(/-[^-]*$/, '');
+      filterString += podName.replace(/-[^-]*$/, "");
     }
     if (revision && revision !== "all") {
       if (filterString !== "") {
@@ -35,11 +37,15 @@ const Filter: React.FC<Props> = ({
       filterString += "v" + revision;
     }
     return filterString;
-},[JSON.stringify(selectedFilterValues)]);
+  }, [JSON.stringify(selectedFilterValues)]);
 
   return (
     <Relative>
-      <StyledFilter onClick={() => { setIsExpanded(!isExpanded); }}>
+      <StyledFilter
+        onClick={() => {
+          setIsExpanded(!isExpanded);
+        }}
+      >
         <img src={filter} />
         Filter
         {filterLabelString !== "" && (
@@ -50,7 +56,12 @@ const Filter: React.FC<Props> = ({
           </>
         )}
       </StyledFilter>
-      <CloseOverlay onClick={() => { setIsExpanded(false); }} isExpanded={isExpanded} />
+      <CloseOverlay
+        onClick={() => {
+          setIsExpanded(false);
+        }}
+        isExpanded={isExpanded}
+      />
       <Dropdown isExpanded={isExpanded}>
         {filters.map((filter: GenericFilter, i: number) => {
           return (
@@ -58,7 +69,11 @@ const Filter: React.FC<Props> = ({
               <FilterLabel>{filter.displayName}</FilterLabel>
               <Spacer y={0.5} />
               <Select
-                options={filter.default ? [filter.default, ...filter.options] : filter.options}
+                options={
+                  filter.default
+                    ? [filter.default, ...filter.options]
+                    : filter.options
+                }
                 setValue={filter.setValue}
                 value={selectedFilterValues[filter.name]}
               />
@@ -87,11 +102,10 @@ const CloseOverlay = styled.div<{ isExpanded: boolean }>`
   z-index: 998;
   width: 100vw;
   height: 100vh;
-  display: ${props => props.isExpanded ? "block" : "none"};
+  display: ${(props) => (props.isExpanded ? "block" : "none")};
 `;
 
-const FilterLabel = styled.div`
-`;
+const FilterLabel = styled.div``;
 
 const Dropdown = styled.div<{ isExpanded: boolean }>`
   position: absolute;
@@ -100,26 +114,25 @@ const Dropdown = styled.div<{ isExpanded: boolean }>`
   border-radius: 5px;
   font-size: 13px;
   background: #121212;
-  border: 1px solid #494B4F;
+  border: 1px solid #494b4f;
   padding: 10px;
   padding-bottom: 15px;
   z-index: 999;
-  display: ${props => props.isExpanded ? "block" : "none"};
+  display: ${(props) => (props.isExpanded ? "block" : "none")};
 `;
 
 const Relative = styled.div`
   position: relative;
 `;
 
-const StyledFilter = styled.div<{
-}>`
+const StyledFilter = styled.div<{}>`
   display: flex;
   align-items: center;
   color: #ffffff;
   font-size: 13px;
   height: 30px;
   border-radius: 5px;
-  background: ${props => props.theme.fg};
+  background: ${(props) => props.theme.fg};
   padding: 0px 10px;
   cursor: pointer;
 
@@ -133,4 +146,4 @@ const StyledFilter = styled.div<{
     width: 13px;
     margin-right: 8px;
   }
-`;
+`;

+ 124 - 110
dashboard/src/components/porter/InputSlider.tsx

@@ -1,11 +1,12 @@
-import React, { useEffect, useState } from 'react';
-import Slider, { type Mark } from '@material-ui/core/Slider';
-import Tooltip from '@material-ui/core/Tooltip';
-import Typography from '@material-ui/core/Typography';
-import styled from 'styled-components';
-import { withStyles } from '@material-ui/core/styles';
-import Text from './Text';
-import Spacer from './Spacer';
+import React, { useEffect, useState } from "react";
+import Slider, { type Mark } from "@material-ui/core/Slider";
+import { withStyles } from "@material-ui/core/styles";
+import Tooltip from "@material-ui/core/Tooltip";
+import Typography from "@material-ui/core/Typography";
+import styled from "styled-components";
+
+import Spacer from "./Spacer";
+import Text from "./Text";
 
 type InputSliderProps = {
   label?: string;
@@ -28,15 +29,11 @@ const ValueLabelComponent: React.FC<any> = (props) => {
   const { children, value } = props;
 
   return (
-    <StyledTooltip
-      placement="bottom"
-      title={value}
-      arrow
-    >
+    <StyledTooltip placement="bottom" title={value} arrow>
       {children}
     </StyledTooltip>
   );
-}
+};
 
 const InputSlider: React.FC<InputSliderProps> = ({
   label,
@@ -52,7 +49,7 @@ const InputSlider: React.FC<InputSliderProps> = ({
   width,
   smartLimit,
   override,
-  nodeCount
+  nodeCount,
 }) => {
   const [showNeedHelpModal, setShowNeedHelpModal] = useState(false);
 
@@ -83,11 +80,11 @@ const InputSlider: React.FC<InputSliderProps> = ({
   // }
 
   if (smartLimit) {
-
-    marks.push({
-      value: smartLimit,
-      label: smartLimit.toString(),
-    },
+    marks.push(
+      {
+        value: smartLimit,
+        label: smartLimit.toString(),
+      },
       {
         value: mid,
         label: "",
@@ -95,56 +92,74 @@ const InputSlider: React.FC<InputSliderProps> = ({
       {
         value: quarter,
         label: "",
-      },);
+      }
+    );
     displayOptimalText = Number(value) == mid || Number(value) == quarter;
     isExceedingLimit = Number(value) > smartLimit;
   }
   const isCloseToMark = (value, marks, threshold = 0.1) => {
-    return marks.some(mark => Math.abs(mark.value - value) < threshold);
+    return marks.some((mark) => Math.abs(mark.value - value) < threshold);
   };
 
   const getClosestMark = (value, marks) => {
-    return marks.reduce((prev, curr) => (
+    return marks.reduce((prev, curr) =>
       Math.abs(curr.value - value) < Math.abs(prev.value - value) ? curr : prev
-    )).value;
+    ).value;
   };
 
-
   return (
     <SliderContainer width={width}>
       <LabelContainer>
         <>
           {label && <Label>{label}</Label>}
           <Value>{`${Math.floor(value * 100) / 100} ${unit}`}</Value>
-          {displayOptimalText &&
-            <><Spacer inline x={1} /><Label>Recommended based on the available compute </Label>  <StyledIcon
-              className="material-icons"
-              onClick={() => {
-                setShowNeedHelpModal(true)
-              }}
-            >
-              help_outline
-            </StyledIcon></>}
-          {isExceedingLimit &&
-            <><Spacer inline x={1} /><Label color="#FFBF00"> Value is not optimal for cost</Label></>}
+          {displayOptimalText && (
+            <>
+              <Spacer inline x={1} />
+              <Label>Recommended based on the available compute </Label>{" "}
+              <StyledIcon
+                className="material-icons"
+                onClick={() => {
+                  setShowNeedHelpModal(true);
+                }}
+              >
+                help_outline
+              </StyledIcon>
+            </>
+          )}
+          {isExceedingLimit && (
+            <>
+              <Spacer inline x={1} />
+              <Label color="#FFBF00"> Value is not optimal for cost</Label>
+            </>
+          )}
         </>
       </LabelContainer>
 
-      <DisabledTooltip title={disabled ? disabledTooltip || '' : ''} arrow>
-        <div style={{ position: 'relative' }}>
+      <DisabledTooltip title={disabled ? disabledTooltip || "" : ""} arrow>
+        <div style={{ position: "relative" }}>
           {/* <div style={{ position: 'absolute', bottom: '100%', left: `calc(${((threeQuarter - min) / (max - min)) * 100}% - 50px)` }}>
             Recommended
           </div> */}
-          <MaxedOutToolTip title={smartLimit?.toString() == value && !override ? "Using resources beyond this limit is not cost optimal - to override toggle off Smart Optimization" || '' : ''} arrow>
-            <div style={{ position: 'relative' }}>
-
+          <MaxedOutToolTip
+            title={
+              smartLimit?.toString() == value && !override
+                ? "Using resources beyond this limit is not cost optimal - to override toggle off Smart Optimization" ||
+                  ""
+                : ""
+            }
+            arrow
+          >
+            <div style={{ position: "relative" }}>
               <StyledSlider
                 ValueLabelComponent={ValueLabelComponent}
                 aria-label="input slider"
                 isExceedingLimit={isExceedingLimit}
                 min={min}
                 max={max}
-                value={(!override && isExceedingLimit) ? smartLimit : Number(value)}
+                value={
+                  !override && isExceedingLimit ? smartLimit : Number(value)
+                }
                 onChange={(event, newValue) => {
                   if (!override && smartLimit && newValue > smartLimit) {
                     setValue(smartLimit);
@@ -156,13 +171,15 @@ const InputSlider: React.FC<InputSliderProps> = ({
                   }
                 }}
                 classes={{
-                  track: isExceedingLimit ? 'exceeds-limit' : '',
-                  rail: isExceedingLimit ? 'exceeds-limit' : ''
+                  track: isExceedingLimit ? "exceeds-limit" : "",
+                  rail: isExceedingLimit ? "exceeds-limit" : "",
                 }}
-                valueLabelDisplay={smartLimit && Number(value) > smartLimit ? "off" : "auto"}
+                valueLabelDisplay={
+                  smartLimit && Number(value) > smartLimit ? "off" : "auto"
+                }
                 disabled={disabled}
                 marks={marks}
-                step={(step || 1)}
+                step={step || 1}
                 style={{
                   color: disabled ? "gray" : color,
                 }}
@@ -172,28 +189,26 @@ const InputSlider: React.FC<InputSliderProps> = ({
           {disabled && (
             <div
               style={{
-                position: 'absolute',
+                position: "absolute",
                 top: 0,
                 left: 0,
                 right: 0,
                 bottom: 0,
-                cursor: 'not-allowed',
-                zIndex: 1
+                cursor: "not-allowed",
+                zIndex: 1,
               }}
             />
           )}
         </div>
       </DisabledTooltip>
-
-    </SliderContainer >
+    </SliderContainer>
   );
 };
 
-
 export default InputSlider;
 
 const SliderContainer = styled.div<{ width?: string }>`
-  width: ${({ width }) => width || '90%'};
+  width: ${({ width }) => width || "90%"};
   margin: 1px 0;
 `;
 
@@ -201,7 +216,7 @@ const Label = styled.div<{ color?: string }>`
   font-size: 13px;
   margin-right: 5px;
   margin-bottom: 10px;
-  color: ${props => props.color ? props.color : '#aaaabb'};
+  color: ${(props) => (props.color ? props.color : "#aaaabb")};
 `;
 
 const Value = styled.div<{ color?: string }>`
@@ -210,82 +225,84 @@ const Value = styled.div<{ color?: string }>`
   color: #ffff;
 `;
 
-const DisabledTooltip = withStyles(theme => ({
+const DisabledTooltip = withStyles((theme) => ({
   tooltip: {
-    backgroundColor: '#333',
-    color: '#fff',
-    padding: '8px',
-    borderRadius: '4px',
-    fontSize: '14px',
-    textAlign: 'center',
-    whiteSpace: 'pre-wrap',
-    wordWrap: 'break-word',
-    maxWidth: '200px',
-    width: '200px',
-    [theme.breakpoints.up('sm')]: {
-      margin: '0 14px',
+    backgroundColor: "#333",
+    color: "#fff",
+    padding: "8px",
+    borderRadius: "4px",
+    fontSize: "14px",
+    textAlign: "center",
+    whiteSpace: "pre-wrap",
+    wordWrap: "break-word",
+    maxWidth: "200px",
+    width: "200px",
+    [theme.breakpoints.up("sm")]: {
+      margin: "0 14px",
     },
   },
   arrow: {
-    color: '#333',
+    color: "#333",
   },
 }))(Tooltip);
 
-const MaxedOutToolTip = withStyles(theme => ({
+const MaxedOutToolTip = withStyles((theme) => ({
   tooltip: {
-    backgroundColor: '#333',
-    color: '#fff',
-    padding: '5px',
-    borderRadius: '2px',
-    fontSize: '12px',
-    textAlign: 'center',
-    whiteSpace: 'pre-wrap',
-    wordWrap: 'break-word',
-    maxWidth: '200px',
-    width: '200px',
-    [theme.breakpoints.up('sm')]: {
-      margin: '0 2px',
+    backgroundColor: "#333",
+    color: "#fff",
+    padding: "5px",
+    borderRadius: "2px",
+    fontSize: "12px",
+    textAlign: "center",
+    whiteSpace: "pre-wrap",
+    wordWrap: "break-word",
+    maxWidth: "200px",
+    width: "200px",
+    [theme.breakpoints.up("sm")]: {
+      margin: "0 2px",
     },
   },
 }))(Tooltip);
 
 const StyledSlider = withStyles({
   root: {
-    height: '8px', // height of the track
+    height: "8px", // height of the track
   },
   mark: {
-    backgroundColor: '#fff',  // mark color
+    backgroundColor: "#fff", // mark color
     height: 4, // size of the mark
     width: 1, // size of the mark
-    borderRadius: '50%',
+    borderRadius: "50%",
     marginTop: 6,
     marginLeft: -1,
   },
   markActive: {
-    backgroundColor: '#fff',
+    backgroundColor: "#fff",
   },
   markLabel: {
-    color: '#6e717d',
-    fontSize: '12px',
+    color: "#6e717d",
+    fontSize: "12px",
     marginRight: 5,
-    '&[data-mark-value="Recommended"]': { // targeting the Recommended label
-      transform: 'translateY(-100%)', // move it upwards
-      marginBottom: '15px', // adjust the margin to position it
+    '&[data-mark-value="Recommended"]': {
+      // targeting the Recommended label
+      transform: "translateY(-100%)", // move it upwards
+      marginBottom: "15px", // adjust the margin to position it
     },
   },
   markLabelActive: {
-    color: '#6e717d',
+    color: "#6e717d",
     marginRight: 5,
   },
   thumb: {
     height: 16, // Size of the thumb
     width: 16, // Size of the thumb
-    backgroundColor: '#fff',
-    border: '2px solid currentColor',
-    '&:focus, &:hover, &$active': {
-      boxShadow: 'inherit',
+    backgroundColor: "#fff",
+    border: "2px solid currentColor",
+    "&:focus, &:hover, &$active": {
+      boxShadow: "inherit",
     },
-    '&$disabled': { // Targeting the thumb when the slider is disabled
+    "&$disabled": {
+      // Targeting the thumb when the slider is disabled
       height: 16,
       width: 16,
     },
@@ -293,31 +310,28 @@ const StyledSlider = withStyles({
   track: (props) => ({
     height: 8,
     borderRadius: 4,
-    backgroundColor: props.isExceedingLimit ? '#FFBF00' : '',  // setting color conditionally
+    backgroundColor: props.isExceedingLimit ? "#FFBF00" : "", // setting color conditionally
   }),
   rail: (props) => ({
     height: 8,
     borderRadius: 4,
-    backgroundColor: props.isExceedingLimit ? '#FFBF00' : '',  // setting color conditionally
+    backgroundColor: props.isExceedingLimit ? "#FFBF00" : "", // setting color conditionally
   }),
   valueLabel: {
     top: -22,
-    '& *': {
-      background: 'transparent',
-      border: 'none', // remove the default border
+    "& *": {
+      background: "transparent",
+      border: "none", // remove the default border
     },
-  }
-  ,
+  },
   disabled: {},
 })(Slider);
 
-
 const StyledTooltip = withStyles({
   tooltip: {
     fontSize: 12,
     padding: "5px 10px",
-
-  }
+  },
 })(Tooltip);
 
 const LabelContainer = styled.div`
@@ -327,9 +341,9 @@ const LabelContainer = styled.div`
 
 const StyledIcon = styled.i`
   cursor: pointer;
-  font-size: 16px; 
-  margin-bottom : 10px;
+  font-size: 16px;
+  margin-bottom: 10px;
   &:hover {
-    color: #666;  
+    color: #666;
   }
-`;
+`;

+ 1 - 6
dashboard/src/components/porter/Link.tsx

@@ -29,12 +29,7 @@ const Link: React.FC<Props> = ({
   return (
     <LinkWrapper hoverColor={hoverColor} color={color}>
       {to ? (
-        <StyledLink
-          to={to}
-          target={target}
-          color={color}
-          inline={inline}
-        >
+        <StyledLink to={to} target={target} color={color} inline={inline}>
           {children}
           {target === "_blank" && showTargetBlankIcon && (
             <Svg

+ 4 - 9
dashboard/src/components/porter/LoadingBar.tsx

@@ -8,12 +8,7 @@ type Props = {
   color?: string;
 };
 
-const LoadingBar: React.FC<Props> = ({
-  percent,
-  completed,
-  total,
-  color,
-}) => {
+const LoadingBar: React.FC<Props> = ({ percent, completed, total, color }) => {
   const getColor = () => {
     switch (color) {
       case "failed":
@@ -27,7 +22,7 @@ const LoadingBar: React.FC<Props> = ({
     <StyledLoadingBar>
       <LoadingFill
         color={getColor()}
-        percent={(percent || ((100.0 * completed) / total)) + "%"}
+        percent={(percent || (100.0 * completed) / total) + "%"}
       />
     </StyledLoadingBar>
   );
@@ -56,8 +51,8 @@ const LoadingFill = styled.div<{
   percent: string;
   color?: string;
 }>`
-  width: ${props => props.percent};
-  background: ${props =>
+  width: ${(props) => props.percent};
+  background: ${(props) =>
     props.color || "linear-gradient(to right, #8ce1ff, #616FEE)"};
   height: 100%;
   background-size: 250% 100%;

+ 16 - 13
dashboard/src/components/porter/Pagination.tsx

@@ -1,9 +1,10 @@
 import React from "react";
 import styled from "styled-components";
-import Container from "./Container";
-import Text from "./Text";
+
 import Button from "./Button";
+import Container from "./Container";
 import Spacer from "./Spacer";
+import Text from "./Text";
 
 type Props = {
   page: number;
@@ -11,17 +12,17 @@ type Props = {
   totalPages: number;
 };
 
-const Pagination: React.FC<Props> = ({
-  page,
-  setPage,
-  totalPages,
-}) => {
+const Pagination: React.FC<Props> = ({ page, setPage, totalPages }) => {
   return (
     <Container row spaced>
-      <Text color="helper">Viewing page {page} out of {totalPages}</Text>
+      <Text color="helper">
+        Viewing page {page} out of {totalPages}
+      </Text>
       <Container row>
-        <Button 
-          onClick={() => { setPage(page - 1); }} 
+        <Button
+          onClick={() => {
+            setPage(page - 1);
+          }}
           disabled={page === 1}
           height="20px"
           color="fg"
@@ -30,8 +31,10 @@ const Pagination: React.FC<Props> = ({
           Previous
         </Button>
         <Spacer inline x={0.5} />
-        <Button 
-          onClick={() => { setPage(page + 1); }}
+        <Button
+          onClick={() => {
+            setPage(page + 1);
+          }}
           disabled={page === totalPages}
           height="20px"
           color="fg"
@@ -44,4 +47,4 @@ const Pagination: React.FC<Props> = ({
   );
 };
 
-export default Pagination;
+export default Pagination;

+ 18 - 22
dashboard/src/components/porter/SearchBar.tsx

@@ -34,16 +34,12 @@ const SearchBar: React.FC<Props> = ({
 }) => {
   return (
     <Block width={width} style={style}>
-      {
-        label && (
-          <Label>{label}</Label>
-        )
-      }
+      {label && <Label>{label}</Label>}
       <StyledSearchBar
         style={style}
         width={width}
         height={height}
-        hasError={(error && true) || (error === "")}
+        hasError={(error && true) || error === ""}
         onKeyDown={(event) => {
           if (event.key === "Enter") {
             event.preventDefault();
@@ -56,19 +52,19 @@ const SearchBar: React.FC<Props> = ({
         <Icon src={search} />
         <Input
           value={value}
-          onChange={e => { setValue(e.target.value); }}
+          onChange={(e) => {
+            setValue(e.target.value);
+          }}
           placeholder={placeholder}
           type={type || "text"}
           autoFocus={autoFocus}
         />
-        {
-          error && (
-            <Error>
-              <i className="material-icons">error</i>
-              {error}
-            </Error>
-          )
-        }
+        {error && (
+          <Error>
+            <i className="material-icons">error</i>
+            {error}
+          </Error>
+        )}
       </StyledSearchBar>
       {children}
     </Block>
@@ -91,7 +87,7 @@ const Block = styled.div<{
 }>`
   display: block;
   position: relative;
-  width: ${props => props.width || "200px"};
+  width: ${(props) => props.width || "200px"};
 `;
 
 const Label = styled.div`
@@ -118,18 +114,18 @@ const StyledSearchBar = styled.div<{
   height: string;
   hasError: boolean;
 }>`
-  height: ${props => props.height || "30px"};
+  height: ${(props) => props.height || "30px"};
   padding: 5px 10px;
-  width: ${props => props.width || "200px"};
+  width: ${(props) => props.width || "200px"};
   color: #ffffff;
   font-size: 13px;
   border-radius: 5px;
-  background: ${props => props.theme.fg};
+  background: ${(props) => props.theme.fg};
   transition: all 0.2s;
 
-  border: 1px solid ${props => props.hasError ? "#ff3b62" : "#494b4f"};
+  border: 1px solid ${(props) => (props.hasError ? "#ff3b62" : "#494b4f")};
   :hover {
-    border: 1px solid ${props => props.hasError ? "#ff3b62" : "#7a7b80"};
+    border: 1px solid ${(props) => (props.hasError ? "#ff3b62" : "#7a7b80")};
   }
 `;
 
@@ -140,4 +136,4 @@ const Input = styled.input`
   width: 100%;
   height: 100%;
   padding-left: 23px;
-`;
+`;

+ 1 - 1
dashboard/src/components/porter/Select.tsx

@@ -88,7 +88,7 @@ const Select: React.FC<Props> = ({
 
 export default Select;
 
-const Div = styled.div<{ width?: string, noShrink?: boolean }>`
+const Div = styled.div<{ width?: string; noShrink?: boolean }>`
   width: ${({ width }) => width || ""};
   ${({ noShrink }) => (noShrink ? "flex-shrink: 0;" : "")}
 `;

+ 8 - 14
dashboard/src/components/porter/Spacer.tsx

@@ -9,18 +9,12 @@ type Props = {
   inline?: boolean;
 };
 
-const Spacer: React.FC<Props> = ({
-  height,
-  width,
-  y,
-  x,
-  inline,
-}) => {
+const Spacer: React.FC<Props> = ({ height, width, y, x, inline }) => {
   const getCalcHeight = () => {
     if (y) {
       return 25 * y + "px";
     }
-    return null
+    return null;
   };
 
   const getCalcWidth = () => {
@@ -29,7 +23,7 @@ const Spacer: React.FC<Props> = ({
     }
     return "15px";
   };
-  
+
   return (
     <StyledSpacer
       height={height || getCalcHeight()}
@@ -41,12 +35,12 @@ const Spacer: React.FC<Props> = ({
 
 export default Spacer;
 
-const StyledSpacer = styled.div<{ 
+const StyledSpacer = styled.div<{
   height: string;
   width: string;
   inline: boolean;
 }>`
-  display: ${props => props.inline ? "inline-block" : "block"};
-  height: ${props => props.height || "100%"};
-  width: ${props => props.height ? "100%" : props.width};
-`;
+  display: ${(props) => (props.inline ? "inline-block" : "block")};
+  height: ${(props) => props.height || "100%"};
+  width: ${(props) => (props.height ? "100%" : props.width)};
+`;

+ 5 - 10
dashboard/src/components/porter/Step.tsx

@@ -1,5 +1,6 @@
 import React, { useEffect, useState } from "react";
 import styled from "styled-components";
+
 import Container from "./Container";
 
 type Props = {
@@ -7,16 +8,11 @@ type Props = {
   children: any;
 };
 
-const Step: React.FC<Props> = ({
-  number,
-  children
-}) => {  
+const Step: React.FC<Props> = ({ number, children }) => {
   return (
     <StyledStep>
       <StepNumber>{number}</StepNumber>
-      <Container>
-        {children}
-      </Container>
+      <Container>{children}</Container>
     </StyledStep>
   );
 };
@@ -37,12 +33,11 @@ const StepNumber = styled.div`
   top: 0;
 `;
 
-const StyledStep = styled.div<{ 
-}>`
+const StyledStep = styled.div<{}>`
   font-size: 13px;
   position: relative;
   padding-left: 35px;
   display: flex;
   align-items: center;
   line-height: 1.5;
-`;
+`;

+ 5 - 5
dashboard/src/components/porter/Text.tsx

@@ -58,15 +58,15 @@ const StyledText = styled.div<{
   truncate?: boolean;
 }>`
   line-height: 1.5;
-  font-weight: ${props => props.weight || 400};
-  color: ${props => props.color || props.theme.text.primary};
-  font-size: ${props => props.size || 13}px;
+  font-weight: ${(props) => props.weight || 400};
+  color: ${(props) => props.color || props.theme.text.primary};
+  font-size: ${(props) => props.size || 13}px;
   display: inline;
   align-items: center;
   user-select: text;
-  ${props => props.additionalStyles ? props.additionalStyles : ""}
+  ${(props) => (props.additionalStyles ? props.additionalStyles : "")}
 
-  ${props =>
+  ${(props) =>
     props.truncate
       ? `
         white-space: nowrap;

+ 10 - 4
dashboard/src/components/porter/Toggle.tsx

@@ -46,14 +46,20 @@ const StyledToggle = styled.div`
   align-items: center;
 `;
 
-const Item = styled.div<{ active: boolean; activeColor?: string; inactiveColor?: string }>`
+const Item = styled.div<{
+  active: boolean;
+  activeColor?: string;
+  inactiveColor?: string;
+}>`
   display: flex;
   align-items: center;
   height: 100%;
   cursor: pointer;
   justify-content: center;
   padding: 10px;
-  opacity: ${(props) => props.active ? "1" : "0.4"};
+  opacity: ${(props) => (props.active ? "1" : "0.4")};
   background: ${(props) =>
-    props.active ? props.activeColor ?? "#ffffff11" : props.inactiveColor ?? "transparent"};
-`;
+    props.active
+      ? props.activeColor ?? "#ffffff11"
+      : props.inactiveColor ?? "transparent"};
+`;

+ 19 - 19
dashboard/src/components/porter/ToggleRow.tsx

@@ -1,5 +1,6 @@
 import React from "react";
 import styled from "styled-components";
+
 import Tooltip from "./Tooltip";
 
 type Props = {
@@ -17,35 +18,34 @@ const ToggleRow: React.FC<Props> = ({
   disabled = false,
   disabledTooltip,
 }) => {
-  return (
-    disabled && disabledTooltip ?
-      <Tooltip content={disabledTooltip} position="right">
-        <StyledToggle>
-          <ToggleContainer
-            isToggled={isToggled}
-            onClick={disabled ? () => { } : onToggle}
-            disabled={disabled}
-          >
-            <ToggleDot />
-          </ToggleContainer>
-          {children}
-        </StyledToggle>
-      </Tooltip>
-      :
+  return disabled && disabledTooltip ? (
+    <Tooltip content={disabledTooltip} position="right">
       <StyledToggle>
         <ToggleContainer
           isToggled={isToggled}
-          onClick={disabled ? () => { } : onToggle}
+          onClick={disabled ? () => {} : onToggle}
           disabled={disabled}
         >
           <ToggleDot />
         </ToggleContainer>
         {children}
       </StyledToggle>
+    </Tooltip>
+  ) : (
+    <StyledToggle>
+      <ToggleContainer
+        isToggled={isToggled}
+        onClick={disabled ? () => {} : onToggle}
+        disabled={disabled}
+      >
+        <ToggleDot />
+      </ToggleContainer>
+      {children}
+    </StyledToggle>
   );
 };
 
-export default ToggleRow; 
+export default ToggleRow;
 
 const StyledToggle = styled.div`
   display: flex;
@@ -72,9 +72,9 @@ const ToggleContainer = styled.div<{
   background: ${(props) => (props.isToggled ? "#ffffff33" : "#ffffff11")};
   display: flex;
   align-items: center;
-  justify-content ${props => props.isToggled ? "flex-end" : "flex-start"};
+  justify-content ${(props) => (props.isToggled ? "flex-end" : "flex-start")};
   cursor: ${(props) => (props.disabled ? "not-allowed" : "pointer")};
   :hover {
     border: 1px solid #aaaabb;
   }
-`;
+`;

+ 16 - 18
dashboard/src/components/porter/VerticalSteps.tsx

@@ -1,12 +1,13 @@
 import React, { useEffect, useState } from "react";
-import styled from "styled-components";
 import AnimateHeight from "react-animate-height";
-import Button from "./Button";
-import Spacer from "./Spacer";
-import Container from "./Container";
+import styled from "styled-components";
 
 import check from "assets/check.png";
 
+import Button from "./Button";
+import Container from "./Container";
+import Spacer from "./Spacer";
+
 type Props = {
   steps: React.ReactNode[];
   currentStep: number;
@@ -26,26 +27,24 @@ const VerticalSteps: React.FC<Props> = ({
       {steps.map((step, i) => {
         return (
           <Relative key={i}>
-            {i === steps.length - 1 && (
-              <LineCover />
-            )}
+            {i === steps.length - 1 && <LineCover />}
             {onlyShowCurrentStep && i < currentStep ? (
               <Check src={check} />
             ) : (
               <Dot isActive={i <= currentStep}>
-                <Number>{i+1}</Number>
+                <Number>{i + 1}</Number>
               </Dot>
             )}
             <StepWrapper
-              height={onlyShowCurrentStep ? (i === currentStep ? "auto" : 30) : "auto"}
+              height={
+                onlyShowCurrentStep ? (i === currentStep ? "auto" : 30) : "auto"
+              }
               isLast={i === steps.length - 1}
               key={i}
             >
               <OpacityWrapper isActive={i <= currentStep}>
                 {step}
-                {i > currentStep && (
-                  <ReadOnlyOverlay />
-                )}
+                {i > currentStep && <ReadOnlyOverlay />}
               </OpacityWrapper>
             </StepWrapper>
           </Relative>
@@ -114,7 +113,7 @@ const Dot = styled.div<{
 }>`
   width: 31px;
   height: 31px;
-  background: ${props => props.isActive ? "#3D48C3" : "#121212"};
+  background: ${(props) => (props.isActive ? "#3D48C3" : "#121212")};
   border-radius: 50%;
   position: absolute;
   left: -11px;
@@ -129,7 +128,7 @@ const Dot = styled.div<{
 const OpacityWrapper = styled.div<{
   isActive: boolean;
 }>`
-  opacity: ${props => props.isActive ? 1 : 0.5};
+  opacity: ${(props) => (props.isActive ? 1 : 0.5)};
 `;
 
 const StepWrapper = styled(AnimateHeight)<{
@@ -137,11 +136,10 @@ const StepWrapper = styled(AnimateHeight)<{
 }>`
   padding-left: 30px;
   position: relative;
-  margin-bottom: ${props => props.isLast ? "" : "35px"};
+  margin-bottom: ${(props) => (props.isLast ? "" : "35px")};
 `;
 
-const StyledVerticalSteps = styled.div<{
-}>`
+const StyledVerticalSteps = styled.div<{}>`
   position: relative;
   margin-left: 8px;
-`;
+`;

+ 2 - 2
dashboard/src/components/porter/collapsible-container.css

@@ -1,3 +1,3 @@
 .ReactCollapse--collapse {
-    transition: height 500ms;
-}
+  transition: height 500ms;
+}

+ 20 - 8
dashboard/src/components/repo-selector/ActionConfEditor.tsx

@@ -3,10 +3,10 @@ import styled from "styled-components";
 
 import { type ActionConfigType } from "shared/types";
 
-import RepoList from "./RepoList";
+import ActionDetails from "./ActionDetails";
 import BranchList from "./BranchList";
 import ContentsList from "./ContentsList";
-import ActionDetails from "./ActionDetails";
+import RepoList from "./RepoList";
 
 type Props = {
   actionConfig: ActionConfigType | null;
@@ -43,7 +43,9 @@ const ActionConfEditor: React.FC<Props> = (props) => {
       <ExpandedWrapperAlt>
         <RepoList
           actionConfig={actionConfig}
-          setActionConfig={(x: ActionConfigType) => { setActionConfig(x); }}
+          setActionConfig={(x: ActionConfigType) => {
+            setActionConfig(x);
+          }}
           readOnly={false}
         />
       </ExpandedWrapperAlt>
@@ -54,7 +56,9 @@ const ActionConfEditor: React.FC<Props> = (props) => {
         <ExpandedWrapperAlt>
           <BranchList
             actionConfig={actionConfig}
-            setBranch={(branch: string) => { setBranch(branch); }}
+            setBranch={(branch: string) => {
+              setBranch(branch);
+            }}
           />
         </ExpandedWrapperAlt>
         <Br />
@@ -89,10 +93,18 @@ const ActionConfEditor: React.FC<Props> = (props) => {
           procfilePath={props.procfilePath}
           folderPath={props.folderPath}
           setActionConfig={setActionConfig}
-          setDockerfilePath={(x: string) => { props.setDockerfilePath(x); }}
-          setProcfilePath={(x: string) => { props.setProcfilePath(x); }}
-          setProcfileProcess={(x: string) => { props.setProcfileProcess(x); }}
-          setFolderPath={(x: string) => { props.setFolderPath(x); }}
+          setDockerfilePath={(x: string) => {
+            props.setDockerfilePath(x);
+          }}
+          setProcfilePath={(x: string) => {
+            props.setProcfilePath(x);
+          }}
+          setProcfileProcess={(x: string) => {
+            props.setProcfileProcess(x);
+          }}
+          setFolderPath={(x: string) => {
+            props.setFolderPath(x);
+          }}
         />
         <Br />
         <BackButton

+ 19 - 13
dashboard/src/components/repo-selector/ActionDetails.tsx

@@ -1,13 +1,15 @@
 import React, { useContext, useEffect, useState } from "react";
 import styled, { keyframes } from "styled-components";
 
+import Heading from "components/form-components/Heading";
+import Loading from "components/Loading";
+
+import api from "shared/api";
 import { integrationList } from "shared/common";
 import { Context } from "shared/Context";
-import api from "shared/api";
-import Loading from "components/Loading";
+
 import { type ActionConfigType } from "../../shared/types";
 import InputRow from "../form-components/InputRow";
-import Heading from "components/form-components/Heading";
 import { BuildpackSelection } from "./BuildpackSelection";
 
 type PropsType = {
@@ -59,7 +61,9 @@ const ActionDetails: React.FC<PropsType> = (props) => {
           setSelectedRegistry(res.data[0]);
         }
       })
-      .catch((err: any) => { console.log(err); });
+      .catch((err: any) => {
+        console.log(err);
+      });
   }, [currentProject]);
 
   const renderIntegrationList = () => {
@@ -72,8 +76,7 @@ const ActionDetails: React.FC<PropsType> = (props) => {
     }
 
     return registries.map((registry: any, i: number) => {
-      let icon =
-        integrationList[registry?.service]?.icon;
+      let icon = integrationList[registry?.service]?.icon;
 
       if (!icon) {
         icon = integrationList.dockerhub?.icon;
@@ -84,7 +87,9 @@ const ActionDetails: React.FC<PropsType> = (props) => {
           key={i}
           isSelected={selectedRegistry && registry.id === selectedRegistry?.id}
           lastItem={i === registries?.length - 1}
-          onClick={() => { setSelectedRegistry(registry); }}
+          onClick={() => {
+            setSelectedRegistry(registry);
+          }}
         >
           <img src={icon && icon} />
           {registry.url}
@@ -95,7 +100,6 @@ const ActionDetails: React.FC<PropsType> = (props) => {
 
   const renderRegistrySection = () => {
     if (!registries || registries.length === 0 || registries.length === 1) {
-      
     } else {
       return (
         <>
@@ -141,11 +145,11 @@ const ActionDetails: React.FC<PropsType> = (props) => {
         label={dockerfilePath ? "Docker build context" : "Application folder"}
         type="text"
         width="100%"
-        setValue={(value) =>
-          { typeof value === "string" && dockerfilePath
+        setValue={(value) => {
+          typeof value === "string" && dockerfilePath
             ? setFolderPath(value)
-            : typeof value === "string" && setProcfilePath(value); }
-        }
+            : typeof value === "string" && setProcfilePath(value);
+        }}
         value={
           dockerfilePath
             ? folderPath
@@ -162,7 +166,9 @@ const ActionDetails: React.FC<PropsType> = (props) => {
         <>
           <Heading>
             <ExpandHeader
-              onClick={() => { setShowBuildpacksConfig((prev) => !prev); }}
+              onClick={() => {
+                setShowBuildpacksConfig((prev) => !prev);
+              }}
               isExpanded={showBuildpacksConfig}
             >
               Buildpacks settings

+ 18 - 15
dashboard/src/components/repo-selector/BranchList.tsx

@@ -1,11 +1,11 @@
 import React, { useContext, useEffect, useState } from "react";
 import styled from "styled-components";
+
 import branch_icon from "assets/branch.png";
 
 import api from "../../shared/api";
 import { Context } from "../../shared/Context";
 import { type ActionConfigType } from "../../shared/types";
-
 import Loading from "../Loading";
 import SearchBar from "../SearchBar";
 
@@ -125,7 +125,9 @@ const BranchList: React.FC<Props> = ({
         <BranchName
           key={i}
           lastItem={i === branches.length - 1}
-          onClick={() => { setBranch(branch); }}
+          onClick={() => {
+            setBranch(branch);
+          }}
         >
           <img src={branch_icon} alt={"branch icon"} />
           {branch}
@@ -148,19 +150,20 @@ const BranchList: React.FC<Props> = ({
         prompt={"Search branches..."}
       />
       <BranchListWrapper>
-        {actionConfig.git_branch && actionConfig.git_branch !== currentBranch && (
-          <WarningRow lastItem={false} disabled>
-            <i className="material-icons-round">warning</i>
-            <span>
-              You have unsaved changes. Please click save to commit your
-              changes.
-              <p>
-                Current Branch: <b>{actionConfig.git_branch}</b>. New branch:{" "}
-                <b>{currentBranch}</b>
-              </p>
-            </span>
-          </WarningRow>
-        )}
+        {actionConfig.git_branch &&
+          actionConfig.git_branch !== currentBranch && (
+            <WarningRow lastItem={false} disabled>
+              <i className="material-icons-round">warning</i>
+              <span>
+                You have unsaved changes. Please click save to commit your
+                changes.
+                <p>
+                  Current Branch: <b>{actionConfig.git_branch}</b>. New branch:{" "}
+                  <b>{currentBranch}</b>
+                </p>
+              </span>
+            </WarningRow>
+          )}
         <ExpandedWrapper>{renderBranchList()}</ExpandedWrapper>
       </BranchListWrapper>
     </>

+ 22 - 8
dashboard/src/components/repo-selector/BuildpackSelection.tsx

@@ -1,13 +1,15 @@
-import { DeviconsNameList } from "assets/devicons-name-list";
+import React, { useContext, useEffect, useMemo, useState } from "react";
+import styled, { keyframes } from "styled-components";
+
 import Helper from "components/form-components/Helper";
 import InputRow from "components/form-components/InputRow";
 import SelectRow from "components/form-components/SelectRow";
 import Loading from "components/Loading";
-import React, { useContext, useEffect, useMemo, useState } from "react";
+
 import api from "shared/api";
 import { Context } from "shared/Context";
 import { type ActionConfigType } from "shared/types";
-import styled, { keyframes } from "styled-components";
+import { DeviconsNameList } from "assets/devicons-name-list";
 
 const DEFAULT_BUILDER_NAME = "heroku";
 const DEFAULT_PAKETO_STACK = "paketobuildpacks/builder-jammy-full:latest";
@@ -215,14 +217,18 @@ export const BuildpackSelection: React.FC<{
           <ActionContainer>
             {action === "add" && (
               <ActionButton
-                onClick={() => { handleAddBuildpack(buildpack.buildpack); }}
+                onClick={() => {
+                  handleAddBuildpack(buildpack.buildpack);
+                }}
               >
                 <span className="material-icons-outlined">add</span>
               </ActionButton>
             )}
             {action === "remove" && (
               <ActionButton
-                onClick={() => { handleRemoveBuildpack(buildpack.buildpack); }}
+                onClick={() => {
+                  handleRemoveBuildpack(buildpack.buildpack);
+                }}
               >
                 <span className="material-icons">delete</span>
               </ActionButton>
@@ -293,7 +299,9 @@ export const BuildpackSelection: React.FC<{
           value={selectedBuilder}
           width="100%"
           options={builderOptions}
-          setActiveValue={(option) => { handleSelectBuilder(option); }}
+          setActiveValue={(option) => {
+            handleSelectBuilder(option);
+          }}
           label="Select a builder"
         />
 
@@ -301,7 +309,9 @@ export const BuildpackSelection: React.FC<{
           value={selectedStack}
           width="100%"
           options={stackOptions}
-          setActiveValue={(option) => { setSelectedStack(option); }}
+          setActiveValue={(option) => {
+            setSelectedStack(option);
+          }}
           label="Select your stack"
         />
         <Helper>
@@ -363,7 +373,11 @@ export const AddCustomBuildpackForm: React.FC<{
         </EventInformation>
       </ContentContainer>
       <ActionContainer>
-        <ActionButton onClick={() => { handleAddCustomBuildpack(); }}>
+        <ActionButton
+          onClick={() => {
+            handleAddCustomBuildpack();
+          }}
+        >
           <span className="material-icons-outlined">add</span>
         </ActionButton>
       </ActionContainer>

+ 53 - 29
dashboard/src/components/repo-selector/ContentsList.tsx

@@ -1,20 +1,20 @@
 import React, { Component } from "react";
 import styled from "styled-components";
+
+import close from "assets/close.png";
 import file from "assets/file.svg";
 import folder from "assets/folder.svg";
 import info from "assets/info.svg";
-import close from "assets/close.png";
 
 import api from "../../shared/api";
 import { Context } from "../../shared/Context";
 import { type ActionConfigType, type FileType } from "../../shared/types";
-
 import Loading from "../Loading";
 
 type AutoBuildpack = {
   name?: string;
   valid: boolean;
-}
+};
 
 type PropsType = {
   actionConfig: ActionConfigType | null;
@@ -60,7 +60,9 @@ export default class ContentsList extends Component<PropsType, StateType> {
   }
 
   setSubdirectory = (x: string) => {
-    this.setState({ currentDir: x }, () => { this.updateContents(); });
+    this.setState({ currentDir: x }, () => {
+      this.updateContents();
+    });
   };
 
   fetchContents = async () => {
@@ -248,7 +250,9 @@ export default class ContentsList extends Component<PropsType, StateType> {
             key={i}
             isSelected={item.path === this.state.currentDir}
             lastItem={i === contents.length - 1}
-            onClick={() => { this.setSubdirectory(item.path); }}
+            onClick={() => {
+              this.setSubdirectory(item.path);
+            }}
           >
             <img src={folder} />
             {fileName}
@@ -262,7 +266,9 @@ export default class ContentsList extends Component<PropsType, StateType> {
             key={i}
             lastItem={i === contents.length - 1}
             isADocker
-            onClick={() => { this.props.setDockerfilePath(item.path); }}
+            onClick={() => {
+              this.props.setDockerfilePath(item.path);
+            }}
           >
             <img src={file} />
             {fileName}
@@ -291,7 +297,12 @@ export default class ContentsList extends Component<PropsType, StateType> {
       }
 
       return (
-        <Item lastItem={false} onClick={() => { this.setSubdirectory(subdir); }}>
+        <Item
+          lastItem={false}
+          onClick={() => {
+            this.setSubdirectory(subdir);
+          }}
+        >
           <BackLabel>..</BackLabel>
         </Item>
       );
@@ -360,19 +371,19 @@ export default class ContentsList extends Component<PropsType, StateType> {
       return (
         <Overlay>
           <BgOverlay
-            onClick={() =>
-              { this.setState({ dockerfiles: [] }, () => {
+            onClick={() => {
+              this.setState({ dockerfiles: [] }, () => {
                 this.props.setFolderPath("");
                 this.props.setProcfilePath("");
-              }); }
-            }
+              });
+            }}
           />
           <CloseButton
-            onClick={() =>
-              { this.setState({ dockerfiles: [] }, () => {
+            onClick={() => {
+              this.setState({ dockerfiles: [] }, () => {
                 this.props.setProcfilePath("");
-              }); }
-            }
+              });
+            }}
           >
             <CloseButtonImg src={close} />
           </CloseButton>
@@ -408,8 +419,16 @@ export default class ContentsList extends Component<PropsType, StateType> {
     if (this.state.dockerfiles.length > 0 && !this.props.dockerfilePath) {
       return (
         <Overlay>
-          <BgOverlay onClick={() => { this.setState({ dockerfiles: [] }); }} />
-          <CloseButton onClick={() => { this.setState({ dockerfiles: [] }); }}>
+          <BgOverlay
+            onClick={() => {
+              this.setState({ dockerfiles: [] });
+            }}
+          />
+          <CloseButton
+            onClick={() => {
+              this.setState({ dockerfiles: [] });
+            }}
+          >
             <CloseButtonImg src={close} />
           </CloseButton>
           <Label>
@@ -421,11 +440,11 @@ export default class ContentsList extends Component<PropsType, StateType> {
               return (
                 <Row
                   key={i}
-                  onClick={() =>
-                    { this.props.setDockerfilePath(
+                  onClick={() => {
+                    this.props.setDockerfilePath(
                       `${this.state.currentDir || "."}/${dockerfile}`
-                    ); }
-                  }
+                    );
+                  }}
                   isLast={this.state.dockerfiles.length - 1 === i}
                 >
                   <Indicator selected={false}></Indicator>
@@ -457,11 +476,15 @@ export default class ContentsList extends Component<PropsType, StateType> {
     ) {
       return (
         <Overlay>
-          <BgOverlay onClick={() => { this.props.setDockerfilePath(""); }} />
+          <BgOverlay
+            onClick={() => {
+              this.props.setDockerfilePath("");
+            }}
+          />
           <CloseButton
-            onClick={() =>
-              { this.props.setFolderPath(this.state.currentDir || "./"); }
-            }
+            onClick={() => {
+              this.props.setFolderPath(this.state.currentDir || "./");
+            }}
           >
             <CloseButtonImg src={close} />
           </CloseButton>
@@ -479,9 +502,9 @@ export default class ContentsList extends Component<PropsType, StateType> {
               Yes
             </ConfirmButton>
             <ConfirmButton
-              onClick={() =>
-                { this.props.setFolderPath(this.state.currentDir || "./"); }
-              }
+              onClick={() => {
+                this.props.setFolderPath(this.state.currentDir || "./");
+              }}
             >
               No
             </ConfirmButton>
@@ -501,7 +524,8 @@ export default class ContentsList extends Component<PropsType, StateType> {
               <b>{this.state.autoBuildpack.name}</b> buildpack was{" "}
               <a
                 href="https://docs.porter.run/deploying-applications/deploying-from-github/selecting-application-and-build-method#customizing-buildpacks"
-                target="_blank" rel="noreferrer"
+                target="_blank"
+                rel="noreferrer"
               >
                 detected automatically
               </a>

+ 38 - 33
dashboard/src/components/repo-selector/RepoList.tsx

@@ -1,17 +1,18 @@
 import React, { useContext, useEffect, useRef, useState } from "react";
 import styled from "styled-components";
-import github from "assets/github-white.png";
+
+import DynamicLink from "components/DynamicLink";
+import Text from "components/porter/Text";
 
 import api from "shared/api";
-import { type ActionConfigType, type RepoType } from "shared/types";
 import { Context } from "shared/Context";
+import { useOutsideAlerter } from "shared/hooks/useOutsideAlerter";
+import { type ActionConfigType, type RepoType } from "shared/types";
+import github from "assets/github-white.png";
 
+import { search } from "../../shared/search";
 import Loading from "../Loading";
 import SearchBar from "../SearchBar";
-import DynamicLink from "components/DynamicLink";
-import { useOutsideAlerter } from "shared/hooks/useOutsideAlerter";
-import Text from "components/porter/Text";
-import { search } from "../../shared/search";
 
 type Props = {
   actionConfig: ActionConfigType | null;
@@ -23,15 +24,15 @@ type Props = {
 
 type Provider =
   | {
-    provider: "github";
-    name: string;
-    installation_id: number;
-  }
+      provider: "github";
+      name: string;
+      installation_id: number;
+    }
   | {
-    provider: "gitlab";
-    instance_url: string;
-    integration_id: number;
-  };
+      provider: "gitlab";
+      instance_url: string;
+      integration_id: number;
+    };
 
 // Sort provider by name if it's github or instance url if it's gitlab
 const sortProviders = (providers: Provider[]) => {
@@ -111,7 +112,7 @@ const RepoList: React.FC<Props> = ({
 
       const repos = res.data.map((repo) => ({ ...repo, GHRepoID: repoId }));
       return repos;
-    } catch (error) { }
+    } catch (error) {}
   };
 
   const loadGitlabRepos = async (integrationId: number) => {
@@ -129,7 +130,7 @@ const RepoList: React.FC<Props> = ({
         GitIntegrationId: integrationId,
       }));
       return repos;
-    } catch (error) { }
+    } catch (error) {}
   };
 
   const loadRepos = async (provider: any) => {
@@ -243,20 +244,20 @@ const RepoList: React.FC<Props> = ({
     const results =
       searchFilter != null
         ? repos
-          .filter((repo: RepoType) => {
-            return repo.FullName.toLowerCase().includes(
-              searchFilter.toLowerCase()
-            );
-          })
-          .sort((a: RepoType, b: RepoType) => {
-            const aIndex = a.FullName.toLowerCase().indexOf(
-              searchFilter.toLowerCase()
-            );
-            const bIndex = b.FullName.toLowerCase().indexOf(
-              searchFilter.toLowerCase()
-            );
-            return aIndex - bIndex;
-          })
+            .filter((repo: RepoType) => {
+              return repo.FullName.toLowerCase().includes(
+                searchFilter.toLowerCase()
+              );
+            })
+            .sort((a: RepoType, b: RepoType) => {
+              const aIndex = a.FullName.toLowerCase().indexOf(
+                searchFilter.toLowerCase()
+              );
+              const bIndex = b.FullName.toLowerCase().indexOf(
+                searchFilter.toLowerCase()
+              );
+              return aIndex - bIndex;
+            })
         : repos.slice(0, 10);
 
     if (results.length == 0) {
@@ -271,7 +272,9 @@ const RepoList: React.FC<Props> = ({
             key={i}
             isSelected={repo.FullName === selectedRepo}
             lastItem={i === repos.length - 1}
-            onClick={() => { setRepo(repo); }}
+            onClick={() => {
+              setRepo(repo);
+            }}
             readOnly={readOnly}
             disabled={shouldDisable}
           >
@@ -366,7 +369,7 @@ const ConnectToGithubButton = styled.a`
     props.disabled ? "#aaaabbee" : "#2E3338"};
   :hover {
     background: ${(props: { disabled?: boolean }) =>
-    props.disabled ? "" : "#353a3e"};
+      props.disabled ? "" : "#353a3e"};
   }
 
   > i {
@@ -417,7 +420,9 @@ const ProviderSelector = (props: {
         <ProviderSelectorStyles.Icon className={icon} />
 
         <ProviderSelectorStyles.Button
-          onClick={() => { setIsOpen((prev) => !prev); }}
+          onClick={() => {
+            setIsOpen((prev) => !prev);
+          }}
         >
           {currentValue?.name || currentValue?.instance_url}
         </ProviderSelectorStyles.Button>

+ 4 - 1
dashboard/src/lib/github/workflows.ts

@@ -3,7 +3,10 @@ import axios from "axios";
 import api from "shared/api";
 
 // GithubResultErrorCode is an enum of possible errors that may occur when hitting the Github API.
-export type GithubResultErrorCode = "NO_PERMISSION" | "FILE_NOT_FOUND" | "UNKNOWN";
+export type GithubResultErrorCode =
+  | "NO_PERMISSION"
+  | "FILE_NOT_FOUND"
+  | "UNKNOWN";
 
 // GithubResult is a generic type that should be returned to handle common errors resulting from hitting the Github API.
 export type GithubResult<T extends object> =

+ 3 - 2
dashboard/src/lib/hooks/useAppAnalytics.ts

@@ -1,6 +1,7 @@
 import { useContext } from "react";
-import { Context } from "shared/Context";
+
 import api from "shared/api";
+import { Context } from "shared/Context";
 
 type AppStep =
   | "stack-launch-start"
@@ -44,7 +45,7 @@ export const useAppAnalytics = () => {
           project_id: currentProject.id,
         }
       );
-    } catch (err) { }
+    } catch (err) {}
   };
 
   return {

+ 1 - 6
dashboard/src/lib/hooks/useCloudSqlSecret.ts

@@ -13,12 +13,7 @@ export function useCloudSqlSecret({
   projectId: number;
 }): boolean {
   const { data } = useQuery(
-    [
-      "getCloudSqlSecret",
-      projectId,
-      appName,
-      deploymentTargetId,
-    ],
+    ["getCloudSqlSecret", projectId, appName, deploymentTargetId],
     async () => {
       const res = await api.getCloudSqlSecret(
         "<token>",

+ 5 - 1
dashboard/src/lib/hooks/useDeploymentTarget.ts

@@ -85,7 +85,11 @@ export function useDeploymentTargetList(input: { preview: boolean }): {
 } {
   const { currentProject, currentCluster } = useContext(Context);
 
-  const { data = [], isLoading, refetch } = useQuery(
+  const {
+    data = [],
+    isLoading,
+    refetch,
+  } = useQuery(
     [
       "listDeploymentTargets",
       currentProject?.id,

+ 69 - 62
dashboard/src/lib/hooks/useGithubContents.ts

@@ -1,81 +1,88 @@
+import { useEffect, useState } from "react";
 import { useQuery } from "@tanstack/react-query";
 import _ from "lodash";
-import { useEffect, useState } from "react";
-import api from "shared/api";
 import { z } from "zod";
 
+import api from "shared/api";
+
 type UseGithubContentsOptions = {
-    repoId: number;
-    repoOwner: string;
-    repoName: string;
-    branch: string;
-    path: string;
-    projectId: number;
+  repoId: number;
+  repoOwner: string;
+  repoName: string;
+  branch: string;
+  path: string;
+  projectId: number;
 };
 
 const githubContentsValidator = z.discriminatedUnion("type", [
-    z.object({
-        path: z.string(),
-        type: z.literal("file"),
-    }),
-    z.object({
-        path: z.string(),
-        type: z.literal("dir"),
-    }),
-    z.object({
-        path: z.string(),
-        type: z.literal("symlink"),
-    }),
+  z.object({
+    path: z.string(),
+    type: z.literal("file"),
+  }),
+  z.object({
+    path: z.string(),
+    type: z.literal("dir"),
+  }),
+  z.object({
+    path: z.string(),
+    type: z.literal("symlink"),
+  }),
 ]);
 type GithubContents = z.infer<typeof githubContentsValidator>;
 
 export const useGithubContents = ({
-    repoId,
-    repoOwner,
-    repoName,
-    branch,
-    path,
-    projectId,
+  repoId,
+  repoOwner,
+  repoName,
+  branch,
+  path,
+  projectId,
 }: UseGithubContentsOptions) => {
-    const [contents, setContents] = useState<GithubContents[]>([]);
-    
-    const result = useQuery(
-        ["getGithubContentsAtPath", repoOwner, repoName, branch, path],
-        async () => {
-            const res = await api.getBranchContents(
-                "<token>", 
-                {
-                    dir: path,
-                },
-                {
-                    kind: "github",
-                    project_id: projectId,
-                    git_repo_id: repoId,
-                    owner: repoOwner,
-                    name: repoName,
-                    branch,
-                }
-            );
+  const [contents, setContents] = useState<GithubContents[]>([]);
 
-            const parsed = await z.array(githubContentsValidator).parseAsync(res.data);
-            return parsed;
+  const result = useQuery(
+    ["getGithubContentsAtPath", repoOwner, repoName, branch, path],
+    async () => {
+      const res = await api.getBranchContents(
+        "<token>",
+        {
+          dir: path,
+        },
+        {
+          kind: "github",
+          project_id: projectId,
+          git_repo_id: repoId,
+          owner: repoOwner,
+          name: repoName,
+          branch,
         }
-    );
+      );
 
-    useEffect(() => {
-        if (result.isSuccess) {
-            const folders = result.data.filter((c) => c.type === "dir").sort((a, b) => a.path.localeCompare(b.path));
-            const files = result.data.filter((c) => c.type === "file").sort((a, b) => a.path.localeCompare(b.path));
-            const updatedContents = [...folders, ...files];
-    
-            if (!_.isEqual(updatedContents, contents)) {
-                setContents(updatedContents);
-            }
-        }
-    }, [result, contents]);
+      const parsed = await z
+        .array(githubContentsValidator)
+        .parseAsync(res.data);
+      return parsed;
+    }
+  );
 
-    return {
-        contents,
-        isLoading: result.isLoading,
+  useEffect(() => {
+    if (result.isSuccess) {
+      const folders = result.data
+        .filter((c) => c.type === "dir")
+        .sort((a, b) => a.path.localeCompare(b.path));
+      const files = result.data
+        .filter((c) => c.type === "file")
+        .sort((a, b) => a.path.localeCompare(b.path));
+      const updatedContents = [...folders, ...files];
+
+      if (!_.isEqual(updatedContents, contents)) {
+        setContents(updatedContents);
+      }
     }
+  }, [result, contents]);
+
+  return {
+    contents,
+    isLoading: result.isLoading,
+  };
 };

+ 18 - 4
dashboard/src/main/CurrentError.tsx

@@ -1,8 +1,8 @@
 import React, { Component } from "react";
 import styled from "styled-components";
-import close from "assets/close.png";
 
 import { Context } from "shared/Context";
+import close from "assets/close.png";
 
 type PropsType = {
   currentError: any;
@@ -37,12 +37,22 @@ export default class CurrentError extends Component<PropsType, StateType> {
       currentError = String(currentError);
     }
 
-    if (currentError && currentError !== "" && currentError !== "{}" && currentError !== undefined && currentError.length > 3) {
+    if (
+      currentError &&
+      currentError !== "" &&
+      currentError !== "{}" &&
+      currentError !== undefined &&
+      currentError.length > 3
+    ) {
       if (!this.state.expanded) {
         return (
           <StyledCurrentError>
             <ErrorText>Error: {currentError}</ErrorText>
-            <ExpandButton onClick={() => { this.setState({ expanded: true }); }}>
+            <ExpandButton
+              onClick={() => {
+                this.setState({ expanded: true });
+              }}
+            >
               <i className="material-icons">launch</i>
             </ExpandButton>
             <CloseButton
@@ -65,7 +75,11 @@ export default class CurrentError extends Component<PropsType, StateType> {
           <ExpandedError>
             Porter encountered an error. Full error log:
             <CodeBlock>{currentError}</CodeBlock>
-            <ExpandButtonAlt onClick={() => { this.setState({ expanded: false }); }}>
+            <ExpandButtonAlt
+              onClick={() => {
+                this.setState({ expanded: false });
+              }}
+            >
               <i className="material-icons">remove</i>
             </ExpandButtonAlt>
             <CloseButtonAlt

+ 1 - 0
dashboard/src/main/auth/LoginWrapper.tsx

@@ -4,6 +4,7 @@ import styled from "styled-components";
 import DynamicLink from "components/DynamicLink";
 import Container from "components/porter/Container";
 import Spacer from "components/porter/Spacer";
+
 import blog from "assets/blog.png";
 import docs from "assets/docs.png";
 import logo from "assets/logo.png";

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