Kaynağa Gözat

support google cloud sql for gke clusters (#3289)

Feroze Mohideen 2 yıl önce
ebeveyn
işleme
d4b8c3043e

+ 0 - 141
dashboard/src/main/home/app-dashboard/new-app-flow/JobTabs.tsx

@@ -1,141 +0,0 @@
-import Input from "components/porter/Input";
-import React from "react"
-import Text from "components/porter/Text";
-import Spacer from "components/porter/Spacer";
-import TabSelector from "components/TabSelector";
-import Checkbox from "components/porter/Checkbox";
-import { JobService } from "./serviceTypes";
-import { Height } from "react-animate-height";
-import cronstrue from 'cronstrue';
-import Link from "components/porter/Link";
-
-interface Props {
-  service: JobService;
-  editService: (service: JobService) => void;
-  setHeight: (height: Height) => void;
-}
-
-const JobTabs: React.FC<Props> = ({
-  service,
-  editService,
-  setHeight,
-}) => {
-  const [currentTab, setCurrentTab] = React.useState<string>('main');
-
-  const getScheduleDescription = () => {
-    try {
-      return <Text color="helper">This job runs: {cronstrue.toString(service.cronSchedule.value)}</Text>;
-    } catch (err) {
-      return <Text color="helper">
-        Invalid cron schedule.{" "}
-        <Link
-          to={"https://crontab.cronhub.io/"}
-          hasunderline
-          target="_blank"
-        >
-          Need help?
-        </Link>
-      </Text>;
-    }
-  }
-
-  const renderMain = () => {
-    return (
-      <>
-        <Spacer y={1} />
-        <Input
-          label="Start command"
-          placeholder="ex: sh start.sh"
-          disabled={service.startCommand.readOnly}
-          value={service.startCommand.value}
-          width="300px"
-          setValue={(e) => { editService({ ...service, startCommand: { readOnly: false, value: e } }) }}
-          disabledTooltip={"You may only edit this field in your porter.yaml."}
-        />
-        <Spacer y={1} />
-        <Input
-          label="Cron schedule"
-          placeholder="ex: */5 * * * *"
-          value={service.cronSchedule.value}
-          disabled={service.cronSchedule.readOnly}
-          width="300px"
-          setValue={(e) => { editService({ ...service, cronSchedule: { readOnly: false, value: e } }) }}
-          disabledTooltip={"You may only edit this field in your porter.yaml."}
-        />
-        <Spacer y={0.5} />
-        {getScheduleDescription()}
-      </>
-    )
-  };
-
-  const renderResources = () => {
-    return (
-      <>
-        <Spacer y={1} />
-        <Input
-          label="CPUs (Millicores)"
-          placeholder="ex: 0.5"
-          value={service.cpu.value}
-          disabled={service.cpu.readOnly}
-          width="300px"
-          setValue={(e) => { editService({ ...service, cpu: { readOnly: false, value: e } }) }}
-          disabledTooltip={"You may only edit this field in your porter.yaml."}
-        />
-        <Spacer y={1} />
-        <Input
-          label="RAM (MB)"
-          placeholder="ex: 1"
-          value={service.ram.value}
-          disabled={service.ram.readOnly}
-          width="300px"
-          setValue={(e) => { editService({ ...service, ram: { readOnly: false, value: e } }) }}
-          disabledTooltip={"You may only edit this field in your porter.yaml."}
-        />
-      </>
-    )
-  };
-
-  const renderAdvanced = () => {
-    return (
-      <>
-        <Spacer y={1} />
-        <Checkbox
-          checked={service.jobsExecuteConcurrently.value}
-          toggleChecked={() => { editService({ ...service, jobsExecuteConcurrently: { readOnly: false, value: !service.jobsExecuteConcurrently.value } }) }}
-          disabled={service.jobsExecuteConcurrently.readOnly}
-          disabledTooltip={"You may only edit this field in your porter.yaml."}
-        >
-          <Text color="helper">Allow jobs to execute concurrently</Text>
-        </Checkbox>
-      </>
-    );
-  };
-
-  return (
-    <>
-      <TabSelector
-        options={[
-          { label: 'Main', value: 'main' },
-          { label: 'Resources', value: 'resources' },
-          { label: 'Advanced', value: 'advanced' },
-        ]}
-        currentTab={currentTab}
-        setCurrentTab={(value: string) => {
-          if (value === 'main') {
-            setHeight(276);
-          } else if (value === 'resources') {
-            setHeight(244);
-          } else if (value === 'advanced') {
-            setHeight(118);
-          }
-          setCurrentTab(value);
-        }}
-      />
-      {currentTab === 'main' && renderMain()}
-      {currentTab === 'resources' && renderResources()}
-      {currentTab === 'advanced' && renderAdvanced()}
-    </>
-  )
-}
-
-export default JobTabs;

+ 0 - 89
dashboard/src/main/home/app-dashboard/new-app-flow/ReleaseTabs.tsx

@@ -1,89 +0,0 @@
-import Input from "components/porter/Input";
-import React from "react"
-import Spacer from "components/porter/Spacer";
-import TabSelector from "components/TabSelector";
-import { ReleaseService } from "./serviceTypes";
-import { Height } from "react-animate-height";
-
-interface Props {
-    service: ReleaseService;
-    editService: (service: ReleaseService) => void;
-    setHeight: (height: Height) => void;
-}
-
-const ReleaseTabs: React.FC<Props> = ({
-    service,
-    editService,
-    setHeight,
-}) => {
-    const [currentTab, setCurrentTab] = React.useState<string>('main');
-
-    const renderMain = () => {
-        return (
-            <>
-                <Spacer y={1} />
-                <Input
-                    label="Start command"
-                    placeholder="ex: sh start.sh"
-                    disabled={service.startCommand.readOnly}
-                    value={service.startCommand.value}
-                    width="300px"
-                    setValue={(e) => { editService({ ...service, startCommand: { readOnly: false, value: e } }) }}
-                    disabledTooltip={"You may only edit this field in your porter.yaml."}
-                />
-            </>
-        )
-    };
-
-    const renderResources = () => {
-        return (
-            <>
-                <Spacer y={1} />
-                <Input
-                    label="CPU (Millicores)"
-                    placeholder="ex: 0.5"
-                    value={service.cpu.value}
-                    disabled={service.cpu.readOnly}
-                    width="300px"
-                    setValue={(e) => { editService({ ...service, cpu: { readOnly: false, value: e } }) }}
-                    disabledTooltip={"You may only edit this field in your porter.yaml."}
-                />
-                <Spacer y={1} />
-                <Input
-                    label="RAM (MB)"
-                    placeholder="ex: 1"
-                    value={service.ram.value}
-                    disabled={service.ram.readOnly}
-                    width="300px"
-                    setValue={(e) => { editService({ ...service, ram: { readOnly: false, value: e } }) }}
-                    disabledTooltip={"You may only edit this field in your porter.yaml."}
-                />
-            </>
-        )
-    };
-
-
-    return (
-        <>
-            <TabSelector
-                options={[
-                    { label: 'Main', value: 'main' },
-                    { label: 'Resources', value: 'resources' },
-                ]}
-                currentTab={currentTab}
-                setCurrentTab={(value: string) => {
-                    if (value === 'main') {
-                        setHeight(159);
-                    } else if (value === 'resources') {
-                        setHeight(244);
-                    }
-                    setCurrentTab(value);
-                }}
-            />
-            {currentTab === 'main' && renderMain()}
-            {currentTab === 'resources' && renderResources()}
-        </>
-    )
-}
-
-export default ReleaseTabs;

+ 4 - 4
dashboard/src/main/home/app-dashboard/new-app-flow/ServiceContainer.tsx

@@ -8,12 +8,12 @@ import worker from "assets/worker.png";
 import job from "assets/job.png";
 
 import Spacer from "components/porter/Spacer";
-import WebTabs from "./WebTabs";
-import WorkerTabs from "./WorkerTabs";
-import JobTabs from "./JobTabs";
+import WebTabs from "./tabs/WebTabs";
+import WorkerTabs from "./tabs/WorkerTabs";
+import JobTabs from "./tabs/JobTabs";
 import { Service } from "./serviceTypes";
 import StatusFooter from "../expanded-app/StatusFooter";
-import ReleaseTabs from "./ReleaseTabs";
+import ReleaseTabs from "./tabs/ReleaseTabs";
 
 interface ServiceProps {
   service: Service;

+ 0 - 149
dashboard/src/main/home/app-dashboard/new-app-flow/WorkerTabs.tsx

@@ -1,149 +0,0 @@
-import Input from "components/porter/Input";
-import React from "react"
-import Text from "components/porter/Text";
-import Spacer from "components/porter/Spacer";
-import TabSelector from "components/TabSelector";
-import Checkbox from "components/porter/Checkbox";
-import { WorkerService } from "./serviceTypes";
-import { Height } from "react-animate-height";
-
-interface Props {
-  service: WorkerService;
-  editService: (service: WorkerService) => void;
-  setHeight: (height: Height) => void;
-}
-
-const WorkerTabs: React.FC<Props> = ({
-  service,
-  editService,
-  setHeight,
-}) => {
-  const [currentTab, setCurrentTab] = React.useState<string>('main');
-
-  const renderMain = () => {
-    return (
-      <>
-        <Spacer y={1} />
-        <Input
-          label="Start command"
-          placeholder="ex: sh start.sh"
-          disabled={service.startCommand.readOnly}
-          value={service.startCommand.value}
-          width="300px"
-          setValue={(e) => { editService({ ...service, startCommand: { readOnly: false, value: e } }) }}
-          disabledTooltip={"You may only edit this field in your porter.yaml."}
-        />
-      </>
-    )
-  };
-
-  const renderResources = () => {
-    return (
-      <>
-        <Spacer y={1} />
-        <Input
-          label="CPUs (Millicores)"
-          placeholder="ex: 500"
-          value={service.cpu.value}
-          disabled={service.cpu.readOnly}
-          width="300px"
-          setValue={(e) => { editService({ ...service, cpu: { readOnly: false, value: e } }) }}
-          disabledTooltip={"You may only edit this field in your porter.yaml."}
-        />
-        <Spacer y={1} />
-        <Input
-          label="RAM (MB)"
-          placeholder="ex: 1"
-          value={service.ram.value}
-          disabled={service.ram.readOnly}
-          width="300px"
-          setValue={(e) => { editService({ ...service, ram: { readOnly: false, value: e } }) }}
-          disabledTooltip={"You may only edit this field in your porter.yaml."}
-        />
-        <Spacer y={1} />
-        <Input
-          label="Replicas"
-          placeholder="ex: 1"
-          value={service.replicas.value}
-          disabled={service.replicas.readOnly || service.autoscaling.enabled.value}
-          width="300px"
-          setValue={(e) => { editService({ ...service, replicas: { readOnly: false, value: e } }) }}
-          disabledTooltip={service.replicas.readOnly ? "You may only edit this field in your porter.yaml." : "Disable autoscaling to specify replicas."}
-        />
-        <Spacer y={1} />
-        <Checkbox
-          checked={service.autoscaling.enabled.value}
-          toggleChecked={() => { editService({ ...service, autoscaling: { ...service.autoscaling, enabled: { readOnly: false, value: !service.autoscaling.enabled.value } } }) }}
-          disabled={service.autoscaling.enabled.readOnly}
-          disabledTooltip={"You may only edit this field in your porter.yaml."}
-        >
-          <Text color="helper">Enable autoscaling (overrides replicas)</Text>
-        </Checkbox>
-        <Spacer y={1} />
-        <Input
-          label="Min replicas"
-          placeholder="ex: 1"
-          value={service.autoscaling.minReplicas.value}
-          disabled={service.autoscaling.minReplicas.readOnly || !service.autoscaling.enabled.value}
-          width="300px"
-          setValue={(e) => { editService({ ...service, autoscaling: { ...service.autoscaling, minReplicas: { readOnly: false, value: e } } }) }}
-          disabledTooltip={service.autoscaling.minReplicas.readOnly ? "You may only edit this field in your porter.yaml." : "Enable autoscaling to specify min replicas."}
-        />
-        <Spacer y={1} />
-        <Input
-          label="Max replicas"
-          placeholder="ex: 10"
-          value={service.autoscaling.maxReplicas.value}
-          disabled={service.autoscaling.maxReplicas.readOnly || !service.autoscaling.enabled.value}
-          width="300px"
-          setValue={(e) => { editService({ ...service, autoscaling: { ...service.autoscaling, maxReplicas: { readOnly: false, value: e } } }) }}
-          disabledTooltip={service.autoscaling.maxReplicas.readOnly ? "You may only edit this field in your porter.yaml." : "Enable autoscaling to specify max replicas."}
-        />
-        <Spacer y={1} />
-        <Input
-          label="Target CPU utilization (%)"
-          placeholder="ex: 50"
-          value={service.autoscaling.targetCPUUtilizationPercentage.value}
-          disabled={service.autoscaling.targetCPUUtilizationPercentage.readOnly || !service.autoscaling.enabled.value}
-          width="300px"
-          setValue={(e) => { editService({ ...service, autoscaling: { ...service.autoscaling, targetCPUUtilizationPercentage: { readOnly: false, value: e } } }) }}
-          disabledTooltip={service.autoscaling.targetCPUUtilizationPercentage.readOnly ? "You may only edit this field in your porter.yaml." : "Enable autoscaling to specify target CPU utilization."}
-        />
-        <Spacer y={1} />
-        <Input
-          label="Target RAM utilization (%)"
-          placeholder="ex: 50"
-          value={service.autoscaling.targetMemoryUtilizationPercentage.value}
-          disabled={service.autoscaling.targetMemoryUtilizationPercentage.readOnly || !service.autoscaling.enabled.value}
-          width="300px"
-          setValue={(e) => { editService({ ...service, autoscaling: { ...service.autoscaling, targetMemoryUtilizationPercentage: { readOnly: false, value: e } } }) }}
-          disabledTooltip={service.autoscaling.targetMemoryUtilizationPercentage.readOnly ? "You may only edit this field in your porter.yaml." : "Enable autoscaling to specify target RAM utilization."}
-        />
-      </>
-    )
-  };
-
-  return (
-    <>
-      <TabSelector
-        options={[
-          { label: 'Main', value: 'main' },
-          { label: 'Resources', value: 'resources' },
-        ]}
-        currentTab={currentTab}
-        setCurrentTab={(value: string) => {
-          if (value === 'main') {
-            setHeight(159);
-          } else if (value === 'resources') {
-            setHeight(713);
-          }
-          setCurrentTab(value);
-        }}
-      />
-      {currentTab === 'main' && renderMain()}
-      {currentTab === 'resources' && renderResources()}
-    </>
-  )
-}
-
-export default WorkerTabs;

+ 145 - 66
dashboard/src/main/home/app-dashboard/new-app-flow/serviceTypes.ts

@@ -50,6 +50,12 @@ type Health = {
     startupProbe: StartUpProbe,
     readinessProbe: ReadinessProbe,
 }
+type CloudSql = {
+    enabled: ServiceBoolean,
+    connectionName: ServiceString,
+    dbPort: ServiceString,
+    serviceAccountJson: ServiceString,
+}
 
 
 const ServiceField = {
@@ -74,69 +80,7 @@ type SharedServiceParams = {
     startCommand: ServiceString;
     type: ServiceType;
     canDelete: boolean;
-}
-
-export type WorkerService = SharedServiceParams & {
-    type: 'worker';
-    replicas: ServiceString;
-    autoscaling: Autoscaling;
-}
-const WorkerService = {
-    default: (name: string, porterJson?: PorterJson): WorkerService => ({
-        name,
-        cpu: ServiceField.string('100', porterJson?.apps?.[name]?.config?.resources?.requests?.cpu ? porterJson?.apps?.[name]?.config?.resources?.requests?.cpu.replace('m', '') : undefined),
-        ram: ServiceField.string('256', porterJson?.apps?.[name]?.config?.resources?.requests?.memory ? porterJson?.apps?.[name]?.config?.resources?.requests?.memory.replace('Mi', '') : undefined),
-        startCommand: ServiceField.string('', porterJson?.apps?.[name]?.run),
-        type: 'worker',
-        replicas: ServiceField.string('1', porterJson?.apps?.[name]?.config?.replicaCount),
-        autoscaling: {
-            enabled: ServiceField.boolean(false, porterJson?.apps?.[name]?.config?.autoscaling?.enabled),
-            minReplicas: ServiceField.string('1', porterJson?.apps?.[name]?.config?.autoscaling?.minReplicas),
-            maxReplicas: ServiceField.string('10', porterJson?.apps?.[name]?.config?.autoscaling?.maxReplicas),
-            targetCPUUtilizationPercentage: ServiceField.string('50', porterJson?.apps?.[name]?.config?.autoscaling?.targetCPUUtilizationPercentage),
-            targetMemoryUtilizationPercentage: ServiceField.string('50', porterJson?.apps?.[name]?.config?.autoscaling?.targetMemoryUtilizationPercentage),
-        },
-        canDelete: porterJson?.apps?.[name] == null,
-    }),
-    serialize: (service: WorkerService) => {
-        return {
-            replicaCount: service.replicas.value,
-            container: {
-                command: service.startCommand.value,
-            },
-            resources: {
-                requests: {
-                    cpu: service.cpu.value + 'm',
-                    memory: service.ram.value + 'Mi',
-                }
-            },
-            autoscaling: {
-                enabled: service.autoscaling.enabled.value,
-                minReplicas: service.autoscaling.minReplicas.value,
-                maxReplicas: service.autoscaling.maxReplicas.value,
-                targetCPUUtilizationPercentage: service.autoscaling.targetCPUUtilizationPercentage.value,
-                targetMemoryUtilizationPercentage: service.autoscaling.targetMemoryUtilizationPercentage.value,
-            },
-        }
-    },
-    deserialize: (name: string, values: any, porterJson?: PorterJson): WorkerService => {
-        return {
-            name,
-            cpu: ServiceField.string(values.resources?.requests?.cpu?.replace('m', ''), porterJson?.apps?.[name]?.config?.resources?.requests?.cpu ? porterJson?.apps?.[name]?.config?.resources?.requests?.cpu.replace('m', '') : undefined),
-            ram: ServiceField.string(values.resources?.requests?.memory?.replace('Mi', '') ?? '', porterJson?.apps?.[name]?.config?.resources?.requests?.memory ? porterJson?.apps?.[name]?.config?.resources?.requests?.memory.replace('Mi', '') : undefined),
-            startCommand: ServiceField.string(values.container?.command ?? '', porterJson?.apps?.[name]?.run),
-            type: 'worker',
-            replicas: ServiceField.string(values.replicaCount ?? '', porterJson?.apps?.[name]?.config?.replicaCount),
-            autoscaling: {
-                enabled: ServiceField.boolean(values.autoscaling?.enabled ?? false, porterJson?.apps?.[name]?.config?.autoscaling?.enabled),
-                minReplicas: ServiceField.string(values.autoscaling?.minReplicas ?? '', porterJson?.apps?.[name]?.config?.autoscaling?.minReplicas),
-                maxReplicas: ServiceField.string(values.autoscaling?.maxReplicas ?? '', porterJson?.apps?.[name]?.config?.autoscaling?.maxReplicas),
-                targetCPUUtilizationPercentage: ServiceField.string(values.autoscaling?.targetCPUUtilizationPercentage ?? '', porterJson?.apps?.[name]?.config?.autoscaling?.targetCPUUtilizationPercentage),
-                targetMemoryUtilizationPercentage: ServiceField.string(values.autoscaling?.targetMemoryUtilizationPercentage ?? '', porterJson?.apps?.[name]?.config?.autoscaling?.targetMemoryUtilizationPercentage),
-            },
-            canDelete: porterJson?.apps?.[name] == null,
-        }
-    },
+    cloudsql: CloudSql;
 }
 
 export type WebService = SharedServiceParams & Omit<WorkerService, 'type'> & {
@@ -187,7 +131,13 @@ const WebService = {
                 path: ServiceField.string('/livez', porterJson?.apps?.[name]?.config?.health?.livenessProbe?.path),
                 periodSeconds: ServiceField.string('5', porterJson?.apps?.[name]?.config?.health?.livenessProbe?.periodSeconds),
             },
-        }
+        },
+        cloudsql: {
+            enabled: ServiceField.boolean(false, porterJson?.apps?.[name]?.config?.cloudsql?.enabled),
+            connectionName: ServiceField.string('', porterJson?.apps?.[name]?.config?.cloudsql?.connectionName),
+            dbPort: ServiceField.string('5432', porterJson?.apps?.[name]?.config?.cloudsql?.dbPort),
+            serviceAccountJson: ServiceField.string('', porterJson?.apps?.[name]?.config?.cloudsql?.serviceAccountJson),
+        },
     }),
     serialize: (service: WebService) => {
         return {
@@ -234,7 +184,13 @@ const WebService = {
                     path: service.health.livenessProbe.path.value,
                     periodSeconds: service.health.livenessProbe.periodSeconds.value,
                 },
-            }
+            },
+            cloudsql: {
+                enabled: service.cloudsql.enabled.value,
+                connectionName: service.cloudsql.connectionName.value,
+                dbPort: service.cloudsql.dbPort.value,
+                serviceAccountJson: service.cloudsql.serviceAccountJson.value,
+            },
         }
     },
     deserialize: (name: string, values: any, porterJson?: PorterJson): WebService => {
@@ -279,7 +235,94 @@ const WebService = {
                     path: ServiceField.string(values.health?.livenessProbe?.path ?? '', porterJson?.apps?.[name]?.config?.health?.livenessProbe?.path),
                     periodSeconds: ServiceField.string(values.health?.livenessProbe?.periodSeconds ?? '', porterJson?.apps?.[name]?.config?.health?.livenessProbe?.periodSeconds),
                 },
-            }
+            },
+            cloudsql: {
+                enabled: ServiceField.boolean(values.cloudsql?.enabled ?? false, porterJson?.apps?.[name]?.config?.cloudsql?.enabled),
+                connectionName: ServiceField.string(values.cloudsql?.connectionName ?? '', porterJson?.apps?.[name]?.config?.cloudsql?.connectionName),
+                dbPort: ServiceField.string(values.cloudsql?.dbPort ?? '', porterJson?.apps?.[name]?.config?.cloudsql?.dbPort),
+                serviceAccountJson: ServiceField.string(values.cloudsql?.serviceAccountJson ?? '', porterJson?.apps?.[name]?.config?.cloudsql?.serviceAccountJson),
+            },
+        }
+    },
+}
+
+export type WorkerService = SharedServiceParams & {
+    type: 'worker';
+    replicas: ServiceString;
+    autoscaling: Autoscaling;
+}
+const WorkerService = {
+    default: (name: string, porterJson?: PorterJson): WorkerService => ({
+        name,
+        cpu: ServiceField.string('100', porterJson?.apps?.[name]?.config?.resources?.requests?.cpu ? porterJson?.apps?.[name]?.config?.resources?.requests?.cpu.replace('m', '') : undefined),
+        ram: ServiceField.string('256', porterJson?.apps?.[name]?.config?.resources?.requests?.memory ? porterJson?.apps?.[name]?.config?.resources?.requests?.memory.replace('Mi', '') : undefined),
+        startCommand: ServiceField.string('', porterJson?.apps?.[name]?.run),
+        type: 'worker',
+        replicas: ServiceField.string('1', porterJson?.apps?.[name]?.config?.replicaCount),
+        autoscaling: {
+            enabled: ServiceField.boolean(false, porterJson?.apps?.[name]?.config?.autoscaling?.enabled),
+            minReplicas: ServiceField.string('1', porterJson?.apps?.[name]?.config?.autoscaling?.minReplicas),
+            maxReplicas: ServiceField.string('10', porterJson?.apps?.[name]?.config?.autoscaling?.maxReplicas),
+            targetCPUUtilizationPercentage: ServiceField.string('50', porterJson?.apps?.[name]?.config?.autoscaling?.targetCPUUtilizationPercentage),
+            targetMemoryUtilizationPercentage: ServiceField.string('50', porterJson?.apps?.[name]?.config?.autoscaling?.targetMemoryUtilizationPercentage),
+        },
+        canDelete: porterJson?.apps?.[name] == null,
+        cloudsql: {
+            enabled: ServiceField.boolean(false, porterJson?.apps?.[name]?.config?.cloudsql?.enabled),
+            connectionName: ServiceField.string('', porterJson?.apps?.[name]?.config?.cloudsql?.connectionName),
+            dbPort: ServiceField.string('5432', porterJson?.apps?.[name]?.config?.cloudsql?.dbPort),
+            serviceAccountJson: ServiceField.string('', porterJson?.apps?.[name]?.config?.cloudsql?.serviceAccountJson),
+        },
+    }),
+    serialize: (service: WorkerService) => {
+        return {
+            replicaCount: service.replicas.value,
+            container: {
+                command: service.startCommand.value,
+            },
+            resources: {
+                requests: {
+                    cpu: service.cpu.value + 'm',
+                    memory: service.ram.value + 'Mi',
+                }
+            },
+            autoscaling: {
+                enabled: service.autoscaling.enabled.value,
+                minReplicas: service.autoscaling.minReplicas.value,
+                maxReplicas: service.autoscaling.maxReplicas.value,
+                targetCPUUtilizationPercentage: service.autoscaling.targetCPUUtilizationPercentage.value,
+                targetMemoryUtilizationPercentage: service.autoscaling.targetMemoryUtilizationPercentage.value,
+            },
+            cloudsql: {
+                enabled: service.cloudsql.enabled.value,
+                connectionName: service.cloudsql.connectionName.value,
+                dbPort: service.cloudsql.dbPort.value,
+                serviceAccountJson: service.cloudsql.serviceAccountJson.value,
+            },
+        }
+    },
+    deserialize: (name: string, values: any, porterJson?: PorterJson): WorkerService => {
+        return {
+            name,
+            cpu: ServiceField.string(values.resources?.requests?.cpu?.replace('m', ''), porterJson?.apps?.[name]?.config?.resources?.requests?.cpu ? porterJson?.apps?.[name]?.config?.resources?.requests?.cpu.replace('m', '') : undefined),
+            ram: ServiceField.string(values.resources?.requests?.memory?.replace('Mi', '') ?? '', porterJson?.apps?.[name]?.config?.resources?.requests?.memory ? porterJson?.apps?.[name]?.config?.resources?.requests?.memory.replace('Mi', '') : undefined),
+            startCommand: ServiceField.string(values.container?.command ?? '', porterJson?.apps?.[name]?.run),
+            type: 'worker',
+            replicas: ServiceField.string(values.replicaCount ?? '', porterJson?.apps?.[name]?.config?.replicaCount),
+            autoscaling: {
+                enabled: ServiceField.boolean(values.autoscaling?.enabled ?? false, porterJson?.apps?.[name]?.config?.autoscaling?.enabled),
+                minReplicas: ServiceField.string(values.autoscaling?.minReplicas ?? '', porterJson?.apps?.[name]?.config?.autoscaling?.minReplicas),
+                maxReplicas: ServiceField.string(values.autoscaling?.maxReplicas ?? '', porterJson?.apps?.[name]?.config?.autoscaling?.maxReplicas),
+                targetCPUUtilizationPercentage: ServiceField.string(values.autoscaling?.targetCPUUtilizationPercentage ?? '', porterJson?.apps?.[name]?.config?.autoscaling?.targetCPUUtilizationPercentage),
+                targetMemoryUtilizationPercentage: ServiceField.string(values.autoscaling?.targetMemoryUtilizationPercentage ?? '', porterJson?.apps?.[name]?.config?.autoscaling?.targetMemoryUtilizationPercentage),
+            },
+            canDelete: porterJson?.apps?.[name] == null,
+            cloudsql: {
+                enabled: ServiceField.boolean(values.cloudsql?.enabled ?? false, porterJson?.apps?.[name]?.config?.cloudsql?.enabled),
+                connectionName: ServiceField.string(values.cloudsql?.connectionName ?? '', porterJson?.apps?.[name]?.config?.cloudsql?.connectionName),
+                dbPort: ServiceField.string(values.cloudsql?.dbPort ?? '', porterJson?.apps?.[name]?.config?.cloudsql?.dbPort),
+                serviceAccountJson: ServiceField.string(values.cloudsql?.serviceAccountJson ?? '', porterJson?.apps?.[name]?.config?.cloudsql?.serviceAccountJson),
+            },
         }
     },
 }
@@ -299,6 +342,12 @@ const JobService = {
         jobsExecuteConcurrently: ServiceField.boolean(false, porterJson?.apps?.[name]?.config?.allowConcurrent),
         cronSchedule: ServiceField.string('*/10 * * * *', porterJson?.apps?.[name]?.config?.schedule?.value),
         canDelete: porterJson?.apps?.[name] == null,
+        cloudsql: {
+            enabled: ServiceField.boolean(false, porterJson?.apps?.[name]?.config?.cloudsql?.enabled),
+            connectionName: ServiceField.string('', porterJson?.apps?.[name]?.config?.cloudsql?.connectionName),
+            dbPort: ServiceField.string('5432', porterJson?.apps?.[name]?.config?.cloudsql?.dbPort),
+            serviceAccountJson: ServiceField.string('', porterJson?.apps?.[name]?.config?.cloudsql?.serviceAccountJson),
+        },
     }),
     serialize: (service: JobService) => {
         return {
@@ -317,6 +366,12 @@ const JobService = {
                 value: service.cronSchedule.value,
             },
             paused: true,
+            cloudsql: {
+                enabled: service.cloudsql.enabled.value,
+                connectionName: service.cloudsql.connectionName.value,
+                dbPort: service.cloudsql.dbPort.value,
+                serviceAccountJson: service.cloudsql.serviceAccountJson.value,
+            },
         }
     },
     deserialize: (name: string, values: any, porterJson?: PorterJson): JobService => {
@@ -329,6 +384,12 @@ const JobService = {
             jobsExecuteConcurrently: ServiceField.boolean(values.allowConcurrent ?? false, porterJson?.apps?.[name]?.config?.allowConcurrent),
             cronSchedule: ServiceField.string(values.schedule?.value ?? '', porterJson?.apps?.[name]?.config?.schedule?.value),
             canDelete: porterJson?.apps?.[name] == null,
+            cloudsql: {
+                enabled: ServiceField.boolean(values.cloudsql?.enabled ?? false, porterJson?.apps?.[name]?.config?.cloudsql?.enabled),
+                connectionName: ServiceField.string(values.cloudsql?.connectionName ?? '', porterJson?.apps?.[name]?.config?.cloudsql?.connectionName),
+                dbPort: ServiceField.string(values.cloudsql?.dbPort ?? '', porterJson?.apps?.[name]?.config?.cloudsql?.dbPort),
+                serviceAccountJson: ServiceField.string(values.cloudsql?.serviceAccountJson ?? '', porterJson?.apps?.[name]?.config?.cloudsql?.serviceAccountJson),
+            },
         }
     },
 }
@@ -344,6 +405,12 @@ const ReleaseService = {
         startCommand: ServiceField.string('', porterJson?.release?.run),
         type: 'release',
         canDelete: porterJson?.release == null,
+        cloudsql: {
+            enabled: ServiceField.boolean(false, porterJson?.apps?.[name]?.config?.cloudsql?.enabled),
+            connectionName: ServiceField.string('', porterJson?.apps?.[name]?.config?.cloudsql?.connectionName),
+            dbPort: ServiceField.string('5432', porterJson?.apps?.[name]?.config?.cloudsql?.dbPort),
+            serviceAccountJson: ServiceField.string('', porterJson?.apps?.[name]?.config?.cloudsql?.serviceAccountJson),
+        },
     }),
 
     serialize: (service: ReleaseService) => {
@@ -358,6 +425,12 @@ const ReleaseService = {
                 }
             },
             paused: true, // this makes sure the release isn't run immediately. it is flipped when the porter apply runs the release in the GHA
+            cloudsql: {
+                enabled: service.cloudsql.enabled.value,
+                connectionName: service.cloudsql.connectionName.value,
+                dbPort: service.cloudsql.dbPort.value,
+                serviceAccountJson: service.cloudsql.serviceAccountJson.value,
+            },
         }
     },
 
@@ -369,6 +442,12 @@ const ReleaseService = {
             startCommand: ServiceField.string(values?.container?.command ?? '', porterJson?.release?.run),
             type: 'release',
             canDelete: porterJson?.release == null,
+            cloudsql: {
+                enabled: ServiceField.boolean(values.cloudsql?.enabled ?? false, porterJson?.apps?.[name]?.config?.cloudsql?.enabled),
+                connectionName: ServiceField.string(values.cloudsql?.connectionName ?? '', porterJson?.apps?.[name]?.config?.cloudsql?.connectionName),
+                dbPort: ServiceField.string(values.cloudsql?.dbPort ?? '', porterJson?.apps?.[name]?.config?.cloudsql?.dbPort),
+                serviceAccountJson: ServiceField.string(values.cloudsql?.serviceAccountJson ?? '', porterJson?.apps?.[name]?.config?.cloudsql?.serviceAccountJson),
+            },
         }
     },
 }

+ 237 - 0
dashboard/src/main/home/app-dashboard/new-app-flow/tabs/JobTabs.tsx

@@ -0,0 +1,237 @@
+import Input from "components/porter/Input";
+import React, { useContext } from "react"
+import Text from "components/porter/Text";
+import Spacer from "components/porter/Spacer";
+import TabSelector from "components/TabSelector";
+import Checkbox from "components/porter/Checkbox";
+import { JobService } from "../serviceTypes";
+import AnimateHeight, { Height } from "react-animate-height";
+import cronstrue from 'cronstrue';
+import Link from "components/porter/Link";
+import { Context } from "shared/Context";
+import { DATABASE_HEIGHT_DISABLED, DATABASE_HEIGHT_ENABLED } from "./utils";
+
+interface Props {
+  service: JobService;
+  editService: (service: JobService) => void;
+  setHeight: (height: Height) => void;
+}
+
+const JobTabs: React.FC<Props> = ({
+  service,
+  editService,
+  setHeight,
+}) => {
+  const [currentTab, setCurrentTab] = React.useState<string>('main');
+  const { currentCluster } = useContext(Context);
+
+  const getScheduleDescription = () => {
+    try {
+      return <Text color="helper">This job runs: {cronstrue.toString(service.cronSchedule.value)}</Text>;
+    } catch (err) {
+      return <Text color="helper">
+        Invalid cron schedule.{" "}
+        <Link
+          to={"https://crontab.cronhub.io/"}
+          hasunderline
+          target="_blank"
+        >
+          Need help?
+        </Link>
+      </Text>;
+    }
+  }
+
+  const renderMain = () => {
+    setHeight(276);
+    return (
+      <>
+        <Spacer y={1} />
+        <Input
+          label="Start command"
+          placeholder="ex: sh start.sh"
+          disabled={service.startCommand.readOnly}
+          value={service.startCommand.value}
+          width="300px"
+          setValue={(e) => { editService({ ...service, startCommand: { readOnly: false, value: e } }) }}
+          disabledTooltip={"You may only edit this field in your porter.yaml."}
+        />
+        <Spacer y={1} />
+        <Input
+          label="Cron schedule"
+          placeholder="ex: */5 * * * *"
+          value={service.cronSchedule.value}
+          disabled={service.cronSchedule.readOnly}
+          width="300px"
+          setValue={(e) => { editService({ ...service, cronSchedule: { readOnly: false, value: e } }) }}
+          disabledTooltip={"You may only edit this field in your porter.yaml."}
+        />
+        <Spacer y={0.5} />
+        {getScheduleDescription()}
+      </>
+    )
+  };
+
+  const renderResources = () => {
+    setHeight(244);
+    return (
+      <>
+        <Spacer y={1} />
+        <Input
+          label="CPUs (Millicores)"
+          placeholder="ex: 0.5"
+          value={service.cpu.value}
+          disabled={service.cpu.readOnly}
+          width="300px"
+          setValue={(e) => { editService({ ...service, cpu: { readOnly: false, value: e } }) }}
+          disabledTooltip={"You may only edit this field in your porter.yaml."}
+        />
+        <Spacer y={1} />
+        <Input
+          label="RAM (MB)"
+          placeholder="ex: 1"
+          value={service.ram.value}
+          disabled={service.ram.readOnly}
+          width="300px"
+          setValue={(e) => { editService({ ...service, ram: { readOnly: false, value: e } }) }}
+          disabledTooltip={"You may only edit this field in your porter.yaml."}
+        />
+      </>
+    )
+  };
+
+  const renderDatabase = () => {
+    setHeight(service.cloudsql.enabled.value ? DATABASE_HEIGHT_ENABLED : DATABASE_HEIGHT_DISABLED)
+    return (
+      <>
+        <Spacer y={1} />
+        <Checkbox
+          checked={service.cloudsql.enabled.value}
+          disabled={service.cloudsql.enabled.readOnly}
+          toggleChecked={() => {
+            editService({
+              ...service,
+              cloudsql: {
+                ...service.cloudsql,
+                enabled: {
+                  readOnly: false,
+                  value: !service.cloudsql.enabled.value,
+                },
+              },
+            });
+          }}
+          disabledTooltip={"You may only edit this field in your porter.yaml."}
+        >
+          <Text color="helper">Securely connect to Google Cloud SQL</Text>
+        </Checkbox>
+        <AnimateHeight height={service.cloudsql.enabled.value ? 'auto' : 0}>
+          <Spacer y={1} />
+          <Input
+            label={"Instance Connection Name"}
+            placeholder="ex: project-123:us-east1:pachyderm"
+            value={service.cloudsql.connectionName.value}
+            disabled={service.cloudsql.connectionName.readOnly}
+            width="300px"
+            setValue={(e) => {
+              editService({
+                ...service,
+                cloudsql: {
+                  ...service.cloudsql,
+                  connectionName: { readOnly: false, value: e },
+                },
+              });
+            }}
+            disabledTooltip={
+              "You may only edit this field in your porter.yaml."
+            }
+          />
+          <Spacer y={1} />
+          <Input
+            label={"DB Port"}
+            placeholder="5432"
+            value={service.cloudsql.dbPort.value}
+            disabled={service.cloudsql.dbPort.readOnly}
+            width="300px"
+            setValue={(e) => {
+              editService({
+                ...service,
+                cloudsql: {
+                  ...service.cloudsql,
+                  dbPort: { readOnly: false, value: e },
+                },
+              });
+            }}
+            disabledTooltip={
+              "You may only edit this field in your porter.yaml."
+            }
+          />
+          <Spacer y={1} />
+          <Input
+            label={"Service Account JSON"}
+            placeholder="ex: { <SERVICE_ACCOUNT_JSON> }"
+            value={service.cloudsql.serviceAccountJson.value}
+            disabled={service.cloudsql.serviceAccountJson.readOnly}
+            width="300px"
+            setValue={(e) => {
+              editService({
+                ...service,
+                cloudsql: {
+                  ...service.cloudsql,
+                  serviceAccountJson: { readOnly: false, value: e },
+                },
+              });
+            }}
+            disabledTooltip={
+              "You may only edit this field in your porter.yaml."
+            }
+          />
+        </AnimateHeight>
+      </>
+    );
+  }
+
+  const renderAdvanced = () => {
+    setHeight(118);
+    return (
+      <>
+        <Spacer y={1} />
+        <Checkbox
+          checked={service.jobsExecuteConcurrently.value}
+          toggleChecked={() => { editService({ ...service, jobsExecuteConcurrently: { readOnly: false, value: !service.jobsExecuteConcurrently.value } }) }}
+          disabled={service.jobsExecuteConcurrently.readOnly}
+          disabledTooltip={"You may only edit this field in your porter.yaml."}
+        >
+          <Text color="helper">Allow jobs to execute concurrently</Text>
+        </Checkbox>
+      </>
+    );
+  };
+
+  return (
+    <>
+      <TabSelector
+        options={currentCluster?.cloud_provider === "GKE" ?
+          [
+            { label: 'Main', value: 'main' },
+            { label: 'Resources', value: 'resources' },
+            { label: "Database", value: "database" },
+            { label: 'Advanced', value: 'advanced' },
+          ] :
+          [
+            { label: 'Main', value: 'main' },
+            { label: 'Resources', value: 'resources' },
+            { label: 'Advanced', value: 'advanced' },
+          ]
+        }
+        currentTab={currentTab}
+        setCurrentTab={setCurrentTab}
+      />
+      {currentTab === 'main' && renderMain()}
+      {currentTab === 'resources' && renderResources()}
+      {currentTab === 'advanced' && renderAdvanced()}
+      {currentTab === "database" && renderDatabase()}
+    </>
+  )
+}
+
+export default JobTabs;

+ 187 - 0
dashboard/src/main/home/app-dashboard/new-app-flow/tabs/ReleaseTabs.tsx

@@ -0,0 +1,187 @@
+import Input from "components/porter/Input";
+import React, { useContext } from "react"
+import Spacer from "components/porter/Spacer";
+import TabSelector from "components/TabSelector";
+import { ReleaseService } from "../serviceTypes";
+import AnimateHeight, { Height } from "react-animate-height";
+import { Context } from "shared/Context";
+import Checkbox from "components/porter/Checkbox";
+import Text from "components/porter/Text";
+import { DATABASE_HEIGHT_DISABLED, DATABASE_HEIGHT_ENABLED } from "./utils";
+
+interface Props {
+    service: ReleaseService;
+    editService: (service: ReleaseService) => void;
+    setHeight: (height: Height) => void;
+}
+
+const ReleaseTabs: React.FC<Props> = ({
+    service,
+    editService,
+    setHeight,
+}) => {
+    const [currentTab, setCurrentTab] = React.useState<string>('main');
+    const { currentCluster } = useContext(Context);
+
+    const renderMain = () => {
+        setHeight(159);
+        return (
+            <>
+                <Spacer y={1} />
+                <Input
+                    label="Start command"
+                    placeholder="ex: sh start.sh"
+                    disabled={service.startCommand.readOnly}
+                    value={service.startCommand.value}
+                    width="300px"
+                    setValue={(e) => { editService({ ...service, startCommand: { readOnly: false, value: e } }) }}
+                    disabledTooltip={"You may only edit this field in your porter.yaml."}
+                />
+            </>
+        )
+    };
+
+    const renderResources = () => {
+        setHeight(244);
+        return (
+            <>
+                <Spacer y={1} />
+                <Input
+                    label="CPU (Millicores)"
+                    placeholder="ex: 0.5"
+                    value={service.cpu.value}
+                    disabled={service.cpu.readOnly}
+                    width="300px"
+                    setValue={(e) => { editService({ ...service, cpu: { readOnly: false, value: e } }) }}
+                    disabledTooltip={"You may only edit this field in your porter.yaml."}
+                />
+                <Spacer y={1} />
+                <Input
+                    label="RAM (MB)"
+                    placeholder="ex: 1"
+                    value={service.ram.value}
+                    disabled={service.ram.readOnly}
+                    width="300px"
+                    setValue={(e) => { editService({ ...service, ram: { readOnly: false, value: e } }) }}
+                    disabledTooltip={"You may only edit this field in your porter.yaml."}
+                />
+            </>
+        )
+    };
+
+    const renderDatabase = () => {
+        setHeight(service.cloudsql.enabled.value ? DATABASE_HEIGHT_ENABLED : DATABASE_HEIGHT_DISABLED)
+        return (
+            <>
+                <Spacer y={1} />
+                <Checkbox
+                    checked={service.cloudsql.enabled.value}
+                    disabled={service.cloudsql.enabled.readOnly}
+                    toggleChecked={() => {
+                        editService({
+                            ...service,
+                            cloudsql: {
+                                ...service.cloudsql,
+                                enabled: {
+                                    readOnly: false,
+                                    value: !service.cloudsql.enabled.value,
+                                },
+                            },
+                        });
+                    }}
+                    disabledTooltip={"You may only edit this field in your porter.yaml."}
+                >
+                    <Text color="helper">Securely connect to Google Cloud SQL</Text>
+                </Checkbox>
+                <AnimateHeight height={service.cloudsql.enabled.value ? 'auto' : 0}>
+                    <Spacer y={1} />
+                    <Input
+                        label={"Instance Connection Name"}
+                        placeholder="ex: project-123:us-east1:pachyderm"
+                        value={service.cloudsql.connectionName.value}
+                        disabled={service.cloudsql.connectionName.readOnly}
+                        width="300px"
+                        setValue={(e) => {
+                            editService({
+                                ...service,
+                                cloudsql: {
+                                    ...service.cloudsql,
+                                    connectionName: { readOnly: false, value: e },
+                                },
+                            });
+                        }}
+                        disabledTooltip={
+                            "You may only edit this field in your porter.yaml."
+                        }
+                    />
+                    <Spacer y={1} />
+                    <Input
+                        label={"DB Port"}
+                        placeholder="5432"
+                        value={service.cloudsql.dbPort.value}
+                        disabled={service.cloudsql.dbPort.readOnly}
+                        width="300px"
+                        setValue={(e) => {
+                            editService({
+                                ...service,
+                                cloudsql: {
+                                    ...service.cloudsql,
+                                    dbPort: { readOnly: false, value: e },
+                                },
+                            });
+                        }}
+                        disabledTooltip={
+                            "You may only edit this field in your porter.yaml."
+                        }
+                    />
+                    <Spacer y={1} />
+                    <Input
+                        label={"Service Account JSON"}
+                        placeholder="ex: { <SERVICE_ACCOUNT_JSON> }"
+                        value={service.cloudsql.serviceAccountJson.value}
+                        disabled={service.cloudsql.serviceAccountJson.readOnly}
+                        width="300px"
+                        setValue={(e) => {
+                            editService({
+                                ...service,
+                                cloudsql: {
+                                    ...service.cloudsql,
+                                    serviceAccountJson: { readOnly: false, value: e },
+                                },
+                            });
+                        }}
+                        disabledTooltip={
+                            "You may only edit this field in your porter.yaml."
+                        }
+                    />
+                </AnimateHeight>
+            </>
+        );
+    }
+
+
+    return (
+        <>
+            <TabSelector
+                options={currentCluster?.cloud_provider === "GKE" ?
+                    [
+                        { label: 'Main', value: 'main' },
+                        { label: 'Resources', value: 'resources' },
+                        { label: "Database", value: "database" },
+                    ] :
+                    [
+                        { label: 'Main', value: 'main' },
+                        { label: 'Resources', value: 'resources' },
+                    ]
+                }
+                currentTab={currentTab}
+                setCurrentTab={setCurrentTab}
+            />
+            {currentTab === 'main' && renderMain()}
+            {currentTab === 'resources' && renderResources()}
+            {currentTab === "database" && renderDatabase()}
+        </>
+    )
+}
+
+export default ReleaseTabs;

+ 118 - 25
dashboard/src/main/home/app-dashboard/new-app-flow/WebTabs.tsx → dashboard/src/main/home/app-dashboard/new-app-flow/tabs/WebTabs.tsx

@@ -1,11 +1,13 @@
 import Input from "components/porter/Input";
-import React from "react";
+import React, { useContext } from "react";
 import Text from "components/porter/Text";
 import Spacer from "components/porter/Spacer";
 import TabSelector from "components/TabSelector";
 import Checkbox from "components/porter/Checkbox";
-import { Service, WebService } from "./serviceTypes";
+import { Service, WebService } from "../serviceTypes";
 import AnimateHeight, { Height } from "react-animate-height";
+import { Context } from "shared/Context";
+import { DATABASE_HEIGHT_DISABLED, DATABASE_HEIGHT_ENABLED, RESOURCE_HEIGHT_WITHOUT_AUTOSCALING, RESOURCE_HEIGHT_WITH_AUTOSCALING } from "./utils";
 
 interface Props {
   service: WebService;
@@ -13,8 +15,7 @@ interface Props {
   setHeight: (height: Height) => void;
 }
 
-const RESOURCE_HEIGHT_WITHOUT_AUTOSCALING = 373;
-const RESOURCE_HEIGHT_WITH_AUTOSCALING = 713;
+
 const NETWORKING_HEIGHT_WITHOUT_INGRESS = 204;
 const NETWORKING_HEIGHT_WITH_INGRESS = 333;
 const ADVANCED_BASE_HEIGHT = 215;
@@ -26,6 +27,7 @@ const WebTabs: React.FC<Props> = ({
   setHeight,
 }) => {
   const [currentTab, setCurrentTab] = React.useState<string>("main");
+  const { currentCluster } = useContext(Context);
 
   const renderMain = () => {
     setHeight(159);
@@ -137,6 +139,96 @@ const WebTabs: React.FC<Props> = ({
     );
   }
 
+  const renderDatabase = () => {
+    setHeight(service.cloudsql.enabled.value ? DATABASE_HEIGHT_ENABLED : DATABASE_HEIGHT_DISABLED)
+    return (
+      <>
+        <Spacer y={1} />
+        <Checkbox
+          checked={service.cloudsql.enabled.value}
+          disabled={service.cloudsql.enabled.readOnly}
+          toggleChecked={() => {
+            editService({
+              ...service,
+              cloudsql: {
+                ...service.cloudsql,
+                enabled: {
+                  readOnly: false,
+                  value: !service.cloudsql.enabled.value,
+                },
+              },
+            });
+          }}
+          disabledTooltip={"You may only edit this field in your porter.yaml."}
+        >
+          <Text color="helper">Securely connect to Google Cloud SQL</Text>
+        </Checkbox>
+        <AnimateHeight height={service.cloudsql.enabled.value ? 'auto' : 0}>
+          <Spacer y={1} />
+          <Input
+            label={"Instance Connection Name"}
+            placeholder="ex: project-123:us-east1:pachyderm"
+            value={service.cloudsql.connectionName.value}
+            disabled={service.cloudsql.connectionName.readOnly}
+            width="300px"
+            setValue={(e) => {
+              editService({
+                ...service,
+                cloudsql: {
+                  ...service.cloudsql,
+                  connectionName: { readOnly: false, value: e },
+                },
+              });
+            }}
+            disabledTooltip={
+              "You may only edit this field in your porter.yaml."
+            }
+          />
+          <Spacer y={1} />
+          <Input
+            label={"DB Port"}
+            placeholder="5432"
+            value={service.cloudsql.dbPort.value}
+            disabled={service.cloudsql.dbPort.readOnly}
+            width="300px"
+            setValue={(e) => {
+              editService({
+                ...service,
+                cloudsql: {
+                  ...service.cloudsql,
+                  dbPort: { readOnly: false, value: e },
+                },
+              });
+            }}
+            disabledTooltip={
+              "You may only edit this field in your porter.yaml."
+            }
+          />
+          <Spacer y={1} />
+          <Input
+            label={"Service Account JSON"}
+            placeholder="ex: { <SERVICE_ACCOUNT_JSON> }"
+            value={service.cloudsql.serviceAccountJson.value}
+            disabled={service.cloudsql.serviceAccountJson.readOnly}
+            width="300px"
+            setValue={(e) => {
+              editService({
+                ...service,
+                cloudsql: {
+                  ...service.cloudsql,
+                  serviceAccountJson: { readOnly: false, value: e },
+                },
+              });
+            }}
+            disabledTooltip={
+              "You may only edit this field in your porter.yaml."
+            }
+          />
+        </AnimateHeight>
+      </>
+    );
+  }
+
   const renderResources = () => {
     setHeight(service.autoscaling.enabled.value ? RESOURCE_HEIGHT_WITH_AUTOSCALING : RESOURCE_HEIGHT_WITHOUT_AUTOSCALING)
     return (
@@ -330,7 +422,7 @@ const WebTabs: React.FC<Props> = ({
     return height;
   };
 
-  const renderHealth = () => {
+  const renderAdvanced = () => {
     setHeight(calculateHealthHeight());
     return (
       <>
@@ -671,14 +763,6 @@ const WebTabs: React.FC<Props> = ({
     );
   };
 
-  const renderAdvanced = () => {
-    return (
-      <>
-        {renderHealth()}
-      </>
-    );
-  };
-
   const getApplicationURLText = () => {
     if (service.ingress.hosts.value !== "") {
       return (
@@ -713,24 +797,33 @@ const WebTabs: React.FC<Props> = ({
       )
     }
   }
+
   return (
     <>
-      <>
-        <TabSelector
-          options={[
+      <TabSelector
+        options={currentCluster?.cloud_provider === "GKE" ?
+          [
             { label: "Main", value: "main" },
             { label: "Resources", value: "resources" },
             { label: "Networking", value: "networking" },
+            { label: "Database", value: "database" },
             { label: "Advanced", value: "advanced" },
-          ]}
-          currentTab={currentTab}
-          setCurrentTab={setCurrentTab}
-        />
-        {currentTab === "main" && renderMain()}
-        {currentTab === "resources" && renderResources()}
-        {currentTab === "networking" && renderNetworking()}
-        {currentTab === "advanced" && renderAdvanced()}
-      </>
+          ] :
+          [
+            { label: "Main", value: "main" },
+            { label: "Resources", value: "resources" },
+            { label: "Networking", value: "networking" },
+            { label: "Advanced", value: "advanced" },
+          ]
+        }
+        currentTab={currentTab}
+        setCurrentTab={setCurrentTab}
+      />
+      {currentTab === "main" && renderMain()}
+      {currentTab === "resources" && renderResources()}
+      {currentTab === "networking" && renderNetworking()}
+      {currentTab === "database" && renderDatabase()}
+      {currentTab === "advanced" && renderAdvanced()}
     </>
   );
 };

+ 310 - 0
dashboard/src/main/home/app-dashboard/new-app-flow/tabs/WorkerTabs.tsx

@@ -0,0 +1,310 @@
+import Input from "components/porter/Input";
+import React, { useContext } from "react"
+import Text from "components/porter/Text";
+import Spacer from "components/porter/Spacer";
+import TabSelector from "components/TabSelector";
+import Checkbox from "components/porter/Checkbox";
+import { WorkerService } from "../serviceTypes";
+import AnimateHeight, { Height } from "react-animate-height";
+import { DATABASE_HEIGHT_DISABLED, DATABASE_HEIGHT_ENABLED, RESOURCE_HEIGHT_WITHOUT_AUTOSCALING, RESOURCE_HEIGHT_WITH_AUTOSCALING } from "./utils";
+import { Context } from "shared/Context";
+
+interface Props {
+  service: WorkerService;
+  editService: (service: WorkerService) => void;
+  setHeight: (height: Height) => void;
+}
+
+const WorkerTabs: React.FC<Props> = ({
+  service,
+  editService,
+  setHeight,
+}) => {
+  const [currentTab, setCurrentTab] = React.useState<string>('main');
+  const { currentCluster } = useContext(Context);
+
+  const renderMain = () => {
+    setHeight(159);
+    return (
+      <>
+        <Spacer y={1} />
+        <Input
+          label="Start command"
+          placeholder="ex: sh start.sh"
+          disabled={service.startCommand.readOnly}
+          value={service.startCommand.value}
+          width="300px"
+          setValue={(e) => { editService({ ...service, startCommand: { readOnly: false, value: e } }) }}
+          disabledTooltip={"You may only edit this field in your porter.yaml."}
+        />
+      </>
+    )
+  };
+
+  const renderResources = () => {
+    setHeight(service.autoscaling.enabled.value ? RESOURCE_HEIGHT_WITH_AUTOSCALING : RESOURCE_HEIGHT_WITHOUT_AUTOSCALING)
+    return (
+      <>
+        <Spacer y={1} />
+        <Input
+          label="CPUs (Millicores)"
+          placeholder="ex: 500"
+          value={service.cpu.value}
+          disabled={service.cpu.readOnly}
+          width="300px"
+          setValue={(e) => { editService({ ...service, cpu: { readOnly: false, value: e } }) }}
+          disabledTooltip={"You may only edit this field in your porter.yaml."}
+        />
+        <Spacer y={1} />
+        <Input
+          label="RAM (MB)"
+          placeholder="ex: 1"
+          value={service.ram.value}
+          disabled={service.ram.readOnly}
+          width="300px"
+          setValue={(e) => { editService({ ...service, ram: { readOnly: false, value: e } }) }}
+          disabledTooltip={"You may only edit this field in your porter.yaml."}
+        />
+        <Spacer y={1} />
+        <Input
+          label="Replicas"
+          placeholder="ex: 1"
+          value={service.replicas.value}
+          disabled={service.replicas.readOnly || service.autoscaling.enabled.value}
+          width="300px"
+          setValue={(e) => { editService({ ...service, replicas: { readOnly: false, value: e } }) }}
+          disabledTooltip={service.replicas.readOnly ? "You may only edit this field in your porter.yaml." : "Disable autoscaling to specify replicas."}
+        />
+        <Spacer y={1} />
+        <Checkbox
+          checked={service.autoscaling.enabled.value}
+          toggleChecked={() => { editService({ ...service, autoscaling: { ...service.autoscaling, enabled: { readOnly: false, value: !service.autoscaling.enabled.value } } }) }}
+          disabled={service.autoscaling.enabled.readOnly}
+          disabledTooltip={"You may only edit this field in your porter.yaml."}
+        >
+          <Text color="helper">Enable autoscaling (overrides replicas)</Text>
+        </Checkbox>
+        <AnimateHeight height={service.autoscaling.enabled.value ? 'auto' : 0}>
+          <Spacer y={1} />
+          <Input
+            label="Min replicas"
+            placeholder="ex: 1"
+            value={service.autoscaling.minReplicas.value}
+            disabled={
+              service.autoscaling.minReplicas.readOnly ||
+              !service.autoscaling.enabled.value
+            }
+            width="300px"
+            setValue={(e) => {
+              editService({
+                ...service,
+                autoscaling: {
+                  ...service.autoscaling,
+                  minReplicas: { readOnly: false, value: e },
+                },
+              });
+            }}
+            disabledTooltip={
+              service.autoscaling.minReplicas.readOnly
+                ? "You may only edit this field in your porter.yaml."
+                : "Enable autoscaling to specify min replicas."
+            }
+          />
+          <Spacer y={1} />
+          <Input
+            label="Max replicas"
+            placeholder="ex: 10"
+            value={service.autoscaling.maxReplicas.value}
+            disabled={
+              service.autoscaling.maxReplicas.readOnly ||
+              !service.autoscaling.enabled.value
+            }
+            width="300px"
+            setValue={(e) => {
+              editService({
+                ...service,
+                autoscaling: {
+                  ...service.autoscaling,
+                  maxReplicas: { readOnly: false, value: e },
+                },
+              });
+            }}
+            disabledTooltip={
+              service.autoscaling.maxReplicas.readOnly
+                ? "You may only edit this field in your porter.yaml."
+                : "Enable autoscaling to specify max replicas."
+            }
+          />
+          <Spacer y={1} />
+          <Input
+            label="Target CPU utilization (%)"
+            placeholder="ex: 50"
+            value={service.autoscaling.targetCPUUtilizationPercentage.value}
+            disabled={
+              service.autoscaling.targetCPUUtilizationPercentage.readOnly ||
+              !service.autoscaling.enabled.value
+            }
+            width="300px"
+            setValue={(e) => {
+              editService({
+                ...service,
+                autoscaling: {
+                  ...service.autoscaling,
+                  targetCPUUtilizationPercentage: { readOnly: false, value: e },
+                },
+              });
+            }}
+            disabledTooltip={
+              service.autoscaling.targetCPUUtilizationPercentage.readOnly
+                ? "You may only edit this field in your porter.yaml."
+                : "Enable autoscaling to specify target CPU utilization."
+            }
+          />
+          <Spacer y={1} />
+          <Input
+            label="Target RAM utilization (%)"
+            placeholder="ex: 50"
+            value={service.autoscaling.targetMemoryUtilizationPercentage.value}
+            disabled={
+              service.autoscaling.targetMemoryUtilizationPercentage.readOnly ||
+              !service.autoscaling.enabled.value
+            }
+            width="300px"
+            setValue={(e) => {
+              editService({
+                ...service,
+                autoscaling: {
+                  ...service.autoscaling,
+                  targetMemoryUtilizationPercentage: {
+                    readOnly: false,
+                    value: e,
+                  },
+                },
+              });
+            }}
+            disabledTooltip={
+              service.autoscaling.targetMemoryUtilizationPercentage.readOnly
+                ? "You may only edit this field in your porter.yaml."
+                : "Enable autoscaling to specify target RAM utilization."
+            }
+          />
+        </AnimateHeight>
+      </>
+    )
+  };
+
+  const renderDatabase = () => {
+    setHeight(service.cloudsql.enabled.value ? DATABASE_HEIGHT_ENABLED : DATABASE_HEIGHT_DISABLED)
+    return (
+      <>
+        <Spacer y={1} />
+        <Checkbox
+          checked={service.cloudsql.enabled.value}
+          disabled={service.cloudsql.enabled.readOnly}
+          toggleChecked={() => {
+            editService({
+              ...service,
+              cloudsql: {
+                ...service.cloudsql,
+                enabled: {
+                  readOnly: false,
+                  value: !service.cloudsql.enabled.value,
+                },
+              },
+            });
+          }}
+          disabledTooltip={"You may only edit this field in your porter.yaml."}
+        >
+          <Text color="helper">Securely connect to Google Cloud SQL</Text>
+        </Checkbox>
+        <AnimateHeight height={service.cloudsql.enabled.value ? 'auto' : 0}>
+          <Spacer y={1} />
+          <Input
+            label={"Instance Connection Name"}
+            placeholder="ex: project-123:us-east1:pachyderm"
+            value={service.cloudsql.connectionName.value}
+            disabled={service.cloudsql.connectionName.readOnly}
+            width="300px"
+            setValue={(e) => {
+              editService({
+                ...service,
+                cloudsql: {
+                  ...service.cloudsql,
+                  connectionName: { readOnly: false, value: e },
+                },
+              });
+            }}
+            disabledTooltip={
+              "You may only edit this field in your porter.yaml."
+            }
+          />
+          <Spacer y={1} />
+          <Input
+            label={"DB Port"}
+            placeholder="5432"
+            value={service.cloudsql.dbPort.value}
+            disabled={service.cloudsql.dbPort.readOnly}
+            width="300px"
+            setValue={(e) => {
+              editService({
+                ...service,
+                cloudsql: {
+                  ...service.cloudsql,
+                  dbPort: { readOnly: false, value: e },
+                },
+              });
+            }}
+            disabledTooltip={
+              "You may only edit this field in your porter.yaml."
+            }
+          />
+          <Spacer y={1} />
+          <Input
+            label={"Service Account JSON"}
+            placeholder="ex: { <SERVICE_ACCOUNT_JSON> }"
+            value={service.cloudsql.serviceAccountJson.value}
+            disabled={service.cloudsql.serviceAccountJson.readOnly}
+            width="300px"
+            setValue={(e) => {
+              editService({
+                ...service,
+                cloudsql: {
+                  ...service.cloudsql,
+                  serviceAccountJson: { readOnly: false, value: e },
+                },
+              });
+            }}
+            disabledTooltip={
+              "You may only edit this field in your porter.yaml."
+            }
+          />
+        </AnimateHeight>
+      </>
+    );
+  }
+
+  return (
+    <>
+      <TabSelector
+        options={currentCluster?.cloud_provider === "GKE" ?
+          [
+            { label: 'Main', value: 'main' },
+            { label: 'Resources', value: 'resources' },
+            { label: "Database", value: "database" },
+          ] :
+          [
+            { label: 'Main', value: 'main' },
+            { label: 'Resources', value: 'resources' },
+          ]
+        }
+        currentTab={currentTab}
+        setCurrentTab={setCurrentTab}
+      />
+      {currentTab === 'main' && renderMain()}
+      {currentTab === 'resources' && renderResources()}
+      {currentTab === 'database' && renderDatabase()}
+    </>
+  )
+}
+
+export default WorkerTabs;

+ 4 - 0
dashboard/src/main/home/app-dashboard/new-app-flow/tabs/utils.ts

@@ -0,0 +1,4 @@
+export const DATABASE_HEIGHT_ENABLED = 374;
+export const DATABASE_HEIGHT_DISABLED = 119;
+export const RESOURCE_HEIGHT_WITHOUT_AUTOSCALING = 373;
+export const RESOURCE_HEIGHT_WITH_AUTOSCALING = 713;