diff --git a/dashboard/src/lib/hooks/usePorterYaml.ts b/dashboard/src/lib/hooks/usePorterYaml.ts index 12612fbb18..a9d69cde1f 100644 --- a/dashboard/src/lib/hooks/usePorterYaml.ts +++ b/dashboard/src/lib/hooks/usePorterYaml.ts @@ -139,17 +139,18 @@ export const usePorterYaml = ({ ignoreUnknownFields: true, }); - const { services, predeploy, build } = serviceOverrides({ + const { services, predeploy, initialDeploy, build } = serviceOverrides({ overrides: proto, useDefaults, defaultCPU: newServiceDefaultCpuCores, defaultRAM: newServiceDefaultRamMegabytes, }); - if (services.length || predeploy || build) { + if (services.length || predeploy || initialDeploy || build) { setDetectedServices({ build, services, + initialDeploy, predeploy, }); } @@ -164,19 +165,26 @@ export const usePorterYaml = ({ const { services: previewServices, predeploy: previewPredeploy, + initialDeploy: previewInitialDeploy, build: previewBuild, } = serviceOverrides({ overrides: previewProto, useDefaults, }); - if (previewServices.length || previewPredeploy || previewBuild) { + if ( + previewServices.length || + previewPredeploy || + previewInitialDeploy || + previewBuild + ) { setDetectedServices((prev) => ({ ...prev, services: prev?.services ? prev.services : [], previews: { services: previewServices, predeploy: previewPredeploy, + initialDeploy: previewInitialDeploy, build: previewBuild, variables: data.preview_app?.env_variables ?? {}, }, diff --git a/dashboard/src/lib/porter-apps/index.ts b/dashboard/src/lib/porter-apps/index.ts index dd83fdc450..05a1b8b842 100644 --- a/dashboard/src/lib/porter-apps/index.ts +++ b/dashboard/src/lib/porter-apps/index.ts @@ -7,7 +7,7 @@ import { PorterApp, Service, } from "@porter-dev/api-contracts"; -import { match } from "ts-pattern"; +import { match, P } from "ts-pattern"; import { z } from "zod"; import { BUILDPACK_TO_NAME } from "main/home/app-dashboard/types/buildpack"; @@ -64,6 +64,11 @@ export const deletionValidator = z.object({ name: z.string(), }) .array(), + initialDeploy: z + .object({ + name: z.string(), + }) + .array(), envGroupNames: z .object({ name: z.string(), @@ -106,6 +111,7 @@ export const clientAppValidator = z.object({ .default([]), services: serviceValidator.array(), predeploy: serviceValidator.array().optional(), + initialDeploy: serviceValidator.array().optional(), env: z .object({ key: z.string(), @@ -236,41 +242,101 @@ export function serviceOverrides({ }; } - if (useDefaults) { - return { - build: validatedBuild, - services, - predeploy: deserializeService({ - service: defaultSerialized({ - name: "pre-deploy", - type: "predeploy", - defaultCPU, - defaultRAM, - }), - override: serializedServiceFromProto({ + const predeploy = match({ + predeployOverride: overrides.predeploy, + useDefaults, + }) + .with( + { + predeployOverride: P.nullish, + }, + () => undefined + ) + .with( + { + useDefaults: true, + }, + ({ predeployOverride }) => + deserializeService({ + service: defaultSerialized({ + name: "pre-deploy", + type: "predeploy", + defaultCPU, + defaultRAM, + }), + override: serializedServiceFromProto({ + service: new Service({ + ...predeployOverride, + name: "pre-deploy", + }), + isPredeploy: true, + }), + expanded: true, + }) + ) + .otherwise(({ predeployOverride }) => + deserializeService({ + service: serializedServiceFromProto({ service: new Service({ - ...overrides.predeploy, + ...predeployOverride, name: "pre-deploy", }), isPredeploy: true, }), - expanded: true, - }), - }; - } + }) + ); + + const initialDeploy = match({ + initialDeployOverride: overrides.initialDeploy, + useDefaults, + }) + .with( + { + initialDeployOverride: P.nullish, + }, + () => undefined + ) + .with( + { + useDefaults: true, + initialDeployOverride: P.not(P.nullish), + }, + ({ initialDeployOverride }) => + deserializeService({ + service: defaultSerialized({ + name: "initdeploy", + type: "initdeploy", + defaultCPU, + defaultRAM, + }), + override: serializedServiceFromProto({ + service: new Service({ + ...initialDeployOverride, + name: "initdeploy", + }), + isPredeploy: false, + isInitdeploy: true, + }), + expanded: true, + }) + ) + .otherwise(({ initialDeployOverride }) => + deserializeService({ + service: serializedServiceFromProto({ + service: new Service({ + ...(initialDeployOverride ?? {}), + name: "initdeploy", + }), + isInitdeploy: true, + }), + }) + ); return { build: validatedBuild, services, - predeploy: deserializeService({ - service: serializedServiceFromProto({ - service: new Service({ - ...overrides.predeploy, - name: "pre-deploy", - }), - isPredeploy: true, - }), - }), + predeploy, + initialDeploy, }; } @@ -312,6 +378,9 @@ export function clientAppToProto(data: PorterAppFormData): PorterApp { const predeploy = app.predeploy?.[0]?.run.value ? app.predeploy[0] : undefined; + const initialDeploy = app.initialDeploy?.[0]?.run.value + ? app.initialDeploy[0] + : undefined; const proto = match(source) .with( @@ -329,6 +398,9 @@ export function clientAppToProto(data: PorterAppFormData): PorterApp { ...(predeploy && { predeploy: serviceProto(serializeService(predeploy)), }), + ...(initialDeploy && { + initialDeploy: serviceProto(serializeService(initialDeploy)), + }), helmOverrides: app.helmOverrides != null ? new HelmOverrides({ b64Values: btoa(app.helmOverrides) }) @@ -477,7 +549,43 @@ export function clientAppFromProto({ }); }); - const predeployList = []; + const predeployList = (proto.predeploy ? [proto.predeploy] : []) + .map((service) => + serializedServiceFromProto({ service, isPredeploy: true }) + ) + .map((svc) => { + const override = overrides?.predeploy; + if (override) { + return deserializeService({ + service: svc, + override: serializeService(override), + }); + } + + return deserializeService({ + service: svc, + lockDeletions: lockServiceDeletions, + }); + }); + const initialDeployList = (proto.initialDeploy ? [proto.initialDeploy] : []) + .map((service) => + serializedServiceFromProto({ service, isInitdeploy: true }) + ) + .map((svc) => { + const override = overrides?.initialDeploy; + if (override) { + return deserializeService({ + service: svc, + override: serializeService(override), + }); + } + + return deserializeService({ + service: svc, + lockDeletions: lockServiceDeletions, + }); + }); + const parsedEnv: KeyValueType[] = [ ...Object.entries(variables).map(([key, value]) => ({ key, @@ -498,83 +606,14 @@ export function clientAppFromProto({ const helmOverrides = proto.helmOverrides == null ? "" : atob(proto.helmOverrides.b64Values); - if (proto.predeploy) { - predeployList.push( - deserializeService({ - service: serializedServiceFromProto({ - service: new Service({ - ...proto.predeploy, - name: "pre-deploy", - }), - isPredeploy: true, - }), - lockDeletions: lockServiceDeletions, - }) - ); - } - if (!overrides?.predeploy) { - return { - name: { - readOnly: true, - value: proto.name, - }, - services, - predeploy: predeployList, - env: parsedEnv, - envGroups: proto.envGroups.map((eg) => ({ - name: eg.name, - version: eg.version, - })), - build: clientBuildFromProto(proto.build) ?? { - method: "pack", - context: "./", - buildpacks: [], - builder: "", - }, - helmOverrides, - efsStorage: new EFS({ - enabled: proto.efsStorage?.enabled ?? false, - }), - cloudSql: { - enabled: proto.cloudSql?.enabled ?? false, - connectionName: proto.cloudSql?.connectionName ?? "", - serviceAccountJsonSecret: - proto.cloudSql?.serviceAccountJsonSecret ?? "", - dbPort: proto.cloudSql?.dbPort ?? 5432, - }, - requiredApps: proto.requiredApps.map((app) => ({ - name: app.name, - })), - autoRollback: { - enabled: proto.autoRollback?.enabled ?? true, // enabled by default if not found in proto - readOnly: false, // TODO: detect autorollback from porter.yaml - }, - }; - } - - const predeployOverrides = serializeService(overrides.predeploy); - const predeploy = proto.predeploy - ? [ - deserializeService({ - service: serializedServiceFromProto({ - service: new Service({ - ...proto.predeploy, - name: "pre-deploy", - }), - isPredeploy: true, - }), - override: predeployOverrides, - }), - ] - : undefined; - return { name: { readOnly: true, value: proto.name, }, services, - predeploy, + predeploy: predeployList.length ? predeployList : undefined, + initialDeploy: initialDeployList.length ? initialDeployList : undefined, env: parsedEnv, envGroups: proto.envGroups.map((eg) => ({ name: eg.name, @@ -667,6 +706,18 @@ export function applyPreviewOverrides({ } } + if (app.initialDeploy) { + const initialDeployOverride = overrides?.initialDeploy; + if (initialDeployOverride) { + app.initialDeploy = [ + deserializeService({ + service: serializeService(app.initialDeploy[0]), + override: serializeService(initialDeployOverride), + }), + ]; + } + } + const envOverrides = overrides?.variables; const env = app.env.map((e) => { diff --git a/dashboard/src/lib/porter-apps/services.ts b/dashboard/src/lib/porter-apps/services.ts index f0f1cb4aea..418987b9e3 100644 --- a/dashboard/src/lib/porter-apps/services.ts +++ b/dashboard/src/lib/porter-apps/services.ts @@ -32,10 +32,12 @@ const LAUNCHER_PREFIX = "/cnb/lifecycle/launcher "; export type DetectedServices = { services: ClientService[]; predeploy?: ClientService; + initialDeploy?: ClientService; build?: BuildOptions; previews?: { services: ClientService[]; predeploy?: ClientService; + initialDeploy?: ClientService; variables?: Record; }; }; @@ -196,6 +198,12 @@ export function isPredeployService( return service.config.type === "predeploy"; } +export function isInitdeployService( + service: SerializedService | ClientService +): boolean { + return service.config.type === "initdeploy"; +} + export function prefixSubdomain(subdomain: string): string { if (subdomain.startsWith("https://") || subdomain.startsWith("http://")) { return subdomain; @@ -713,9 +721,11 @@ export function serviceProto(service: SerializedService): Service { export function serializedServiceFromProto({ service, isPredeploy, + isInitdeploy, }: { service: Service; isPredeploy?: boolean; + isInitdeploy?: boolean; }): SerializedService { const config = service.config; if (!config.case) { @@ -758,6 +768,15 @@ export function serializedServiceFromProto({ type: "predeploy" as const, }, } + : isInitdeploy + ? { + ...service, + run: service.runOptional ?? service.run, + instances: service.instancesOptional ?? service.instances, + config: { + type: "initdeploy" as const, + }, + } : { ...service, run: service.runOptional ?? service.run, diff --git a/dashboard/src/main/home/app-dashboard/app-view/AppDataContainer.tsx b/dashboard/src/main/home/app-dashboard/app-view/AppDataContainer.tsx index 8d0ccbb08c..b9a077dfce 100644 --- a/dashboard/src/main/home/app-dashboard/app-view/AppDataContainer.tsx +++ b/dashboard/src/main/home/app-dashboard/app-view/AppDataContainer.tsx @@ -137,6 +137,7 @@ const AppDataContainer: React.FC = ({ tabParam }) => { serviceNames: [], envGroupNames: [], predeploy: [], + initialDeploy: [], }, }, }); @@ -363,6 +364,7 @@ const AppDataContainer: React.FC = ({ tabParam }) => { predeploy: [], envGroupNames: [], serviceNames: [], + initialDeploy: [], }, redeployOnSave: false, }); @@ -534,6 +536,7 @@ const AppDataContainer: React.FC = ({ tabParam }) => { envGroupNames: [], serviceNames: [], predeploy: [], + initialDeploy: [], }, redeployOnSave: false, }); diff --git a/dashboard/src/main/home/app-dashboard/app-view/tabs/Environment.tsx b/dashboard/src/main/home/app-dashboard/app-view/tabs/Environment.tsx index cc94b63d60..79b63cc0ca 100644 --- a/dashboard/src/main/home/app-dashboard/app-view/tabs/Environment.tsx +++ b/dashboard/src/main/home/app-dashboard/app-view/tabs/Environment.tsx @@ -5,22 +5,21 @@ import { z } from "zod"; import Spacer from "components/porter/Spacer"; import Text from "components/porter/Text"; -import { type PorterAppFormData, type SourceOptions } from "lib/porter-apps"; +import PreviewSaveButton from "main/home/cluster-dashboard/preview-environments/v2/setup-app/PreviewSaveButton"; +import { type PorterAppFormData } from "lib/porter-apps"; import api from "shared/api"; import EnvSettings from "../../validate-apply/app-settings/EnvSettings"; import { populatedEnvGroup } from "../../validate-apply/app-settings/types"; import { type ButtonStatus } from "../AppDataContainer"; -import AppSaveButton from "../AppSaveButton"; import { useLatestRevision } from "../LatestRevisionContext"; type Props = { - latestSource: SourceOptions; buttonStatus: ButtonStatus; }; -const Environment: React.FC = ({ latestSource, buttonStatus }) => { +const Environment: React.FC = ({ buttonStatus }) => { const { latestRevision, latestProto, @@ -67,11 +66,10 @@ const Environment: React.FC = ({ latestSource, buttonStatus }) => { appName={latestProto.name} revision={previewRevision || latestRevision} // get versions of env groups attached to preview revision if set baseEnvGroups={baseEnvGroups} - latestSource={latestSource} attachedEnvGroups={attachedEnvGroups} /> - = ({ buttonStatus }) => { diff --git a/dashboard/src/main/home/app-dashboard/create-app/CreateApp.tsx b/dashboard/src/main/home/app-dashboard/create-app/CreateApp.tsx index 8c2885eb18..fa7be5a6f0 100644 --- a/dashboard/src/main/home/app-dashboard/create-app/CreateApp.tsx +++ b/dashboard/src/main/home/app-dashboard/create-app/CreateApp.tsx @@ -165,6 +165,7 @@ const CreateApp: React.FC = ({ history }) => { serviceNames: [], envGroupNames: [], predeploy: [], + initialDeploy: [], }, }, }); @@ -715,7 +716,7 @@ const CreateApp: React.FC = ({ history }) => { , <> diff --git a/dashboard/src/main/home/app-dashboard/validate-apply/services-settings/ServiceContainer.tsx b/dashboard/src/main/home/app-dashboard/validate-apply/services-settings/ServiceContainer.tsx index 6add174305..4ed6afcfc4 100644 --- a/dashboard/src/main/home/app-dashboard/validate-apply/services-settings/ServiceContainer.tsx +++ b/dashboard/src/main/home/app-dashboard/validate-apply/services-settings/ServiceContainer.tsx @@ -16,6 +16,7 @@ import { import chip from "assets/computer-chip.svg"; import job from "assets/job.png"; import moon from "assets/moon.svg"; +import seed from "assets/seed.svg"; import web from "assets/web.png"; import worker from "assets/worker.png"; @@ -30,7 +31,7 @@ type ServiceProps = { service: ClientService; update: UseFieldArrayUpdate< PorterAppFormData, - "app.services" | "app.predeploy" + "app.services" | "app.predeploy" | "app.initialDeploy" >; remove: (index: number) => void; status?: ClientServiceStatus[]; @@ -66,7 +67,10 @@ const ServiceContainer: React.FC = ({ )) .with({ config: { type: "predeploy" } }, (svc) => ( - + + )) + .with({ config: { type: "initdeploy" } }, (svc) => ( + )) .exhaustive(); }; @@ -81,6 +85,8 @@ const ServiceContainer: React.FC = ({ return ; case "predeploy": return ; + case "initdeploy": + return ; } }; diff --git a/dashboard/src/main/home/app-dashboard/validate-apply/services-settings/ServiceList.tsx b/dashboard/src/main/home/app-dashboard/validate-apply/services-settings/ServiceList.tsx index fa82786197..6f77fc9aa0 100644 --- a/dashboard/src/main/home/app-dashboard/validate-apply/services-settings/ServiceList.tsx +++ b/dashboard/src/main/home/app-dashboard/validate-apply/services-settings/ServiceList.tsx @@ -22,6 +22,7 @@ import { defaultSerialized, deserializeService, getServiceResourceAllowances, + isInitdeployService, isPredeployService, } from "lib/porter-apps/services"; @@ -47,9 +48,9 @@ type AddServiceFormValues = z.infer; type ServiceListProps = { addNewText: string; - isPredeploy?: boolean; + lifecycleJobType?: "predeploy" | "initdeploy"; existingServiceNames?: string[]; - fieldArrayName: "app.services" | "app.predeploy"; + fieldArrayName: "app.services" | "app.predeploy" | "app.initialDeploy"; serviceVersionStatus?: Record; internalNetworkingDetails?: { namespace: string; @@ -62,7 +63,7 @@ type ServiceListProps = { const ServiceList: React.FC = ({ addNewText, fieldArrayName, - isPredeploy = false, + lifecycleJobType, existingServiceNames = [], serviceVersionStatus, internalNetworkingDetails = { @@ -115,7 +116,9 @@ const ServiceList: React.FC = ({ name: fieldArrayName === "app.services" ? "deletions.serviceNames" - : "deletions.predeploy", + : lifecycleJobType === "predeploy" + ? "deletions.predeploy" + : "deletions.initialDeploy", }); const serviceName = watch("name"); @@ -126,12 +129,35 @@ const ServiceList: React.FC = ({ const services = useMemo(() => { // if predeploy, only show predeploy services // if not predeploy, only show non-predeploy services + if (lifecycleJobType === "predeploy") { + return fields.map((svc, idx) => { + const predeploy = isPredeployService(svc); + return { + svc, + idx, + included: predeploy, + }; + }); + } + + if (lifecycleJobType === "initdeploy") { + return fields.map((svc, idx) => { + const initdeploy = isInitdeployService(svc); + return { + svc, + idx, + included: initdeploy, + }; + }); + } + return fields.map((svc, idx) => { const predeploy = isPredeployService(svc); + const initdeploy = isInitdeployService(svc); return { svc, idx, - included: isPredeploy ? predeploy : !predeploy, + included: !predeploy && !initdeploy, }; }); }, [fields]); @@ -141,14 +167,24 @@ const ServiceList: React.FC = ({ setError("name", { message: "A service with this name already exists", }); - } else if (!isPredeploy && serviceName === "predeploy") { + } else if ( + lifecycleJobType !== "predeploy" && + serviceName === "predeploy" + ) { setError("name", { message: "predeploy is a reserved service name", }); + } else if ( + lifecycleJobType !== "initdeploy" && + serviceName === "initdeploy" + ) { + setError("name", { + message: "initdeploy is a reserved service name", + }); } else { clearErrors("name"); } - }, [serviceName, isPredeploy]); + }, [serviceName, lifecycleJobType]); const isServiceNameDuplicate = (name: string): boolean => { return services.some(({ svc: s }) => s.name.value === name); @@ -156,7 +192,10 @@ const ServiceList: React.FC = ({ const maybeRenderAddServicesButton = (): JSX.Element | null => { if ( - (isPredeploy && services.find((s) => isPredeployService(s.svc))) || + (lifecycleJobType === "predeploy" && + services.find((s) => isPredeployService(s.svc))) || + (lifecycleJobType === "initdeploy" && + services.find((s) => isInitdeployService(s.svc))) || !allowAddServices ) { return null; @@ -165,22 +204,37 @@ const ServiceList: React.FC = ({ <> { - if (!isPredeploy) { - setShowAddServiceModal(true); + if (lifecycleJobType === "initdeploy") { + append( + deserializeService({ + service: defaultSerialized({ + name: "initdeploy", + type: "initdeploy", + defaultCPU: newServiceDefaultCpuCores, + defaultRAM: newServiceDefaultRamMegabytes, + }), + expanded: true, + }) + ); + return; + } + + if (lifecycleJobType === "predeploy") { + append( + deserializeService({ + service: defaultSerialized({ + name: "pre-deploy", + type: "predeploy", + defaultCPU: newServiceDefaultCpuCores, + defaultRAM: newServiceDefaultRamMegabytes, + }), + expanded: true, + }) + ); return; } - append( - deserializeService({ - service: defaultSerialized({ - name: "pre-deploy", - type: "predeploy", - defaultCPU: newServiceDefaultCpuCores, - defaultRAM: newServiceDefaultRamMegabytes, - }), - expanded: true, - }) - ); + setShowAddServiceModal(true); }} > add_icon diff --git a/dashboard/src/main/home/app-dashboard/validate-apply/services-settings/tabs/JobTabs.tsx b/dashboard/src/main/home/app-dashboard/validate-apply/services-settings/tabs/JobTabs.tsx index 8c47446058..97c81a9621 100644 --- a/dashboard/src/main/home/app-dashboard/validate-apply/services-settings/tabs/JobTabs.tsx +++ b/dashboard/src/main/home/app-dashboard/validate-apply/services-settings/tabs/JobTabs.tsx @@ -18,19 +18,19 @@ type Props = { index: number; service: ClientService & { config: { - type: "job" | "predeploy"; + type: "job" | "predeploy" | "initdeploy"; }; }; - isPredeploy?: boolean; + lifecycleJobType?: "predeploy" | "initdeploy"; }; -const JobTabs: React.FC = ({ index, service, isPredeploy }) => { +const JobTabs: React.FC = ({ index, service, lifecycleJobType }) => { const { control, register } = useFormContext(); const [currentTab, setCurrentTab] = React.useState< "main" | "resources" | "advanced" >("main"); - const tabs = isPredeploy + const tabs = lifecycleJobType ? [ { label: "Main", value: "main" as const }, { label: "Resources", value: "resources" as const }, @@ -50,13 +50,17 @@ const JobTabs: React.FC = ({ index, service, isPredeploy }) => { /> {match(currentTab) .with("main", () => ( - + )) .with("resources", () => ( )) .with( diff --git a/dashboard/src/main/home/app-dashboard/validate-apply/services-settings/tabs/Main.tsx b/dashboard/src/main/home/app-dashboard/validate-apply/services-settings/tabs/Main.tsx index aee7e89555..809b2ba237 100644 --- a/dashboard/src/main/home/app-dashboard/validate-apply/services-settings/tabs/Main.tsx +++ b/dashboard/src/main/home/app-dashboard/validate-apply/services-settings/tabs/Main.tsx @@ -16,18 +16,19 @@ import { type ClientService } from "lib/porter-apps/services"; type MainTabProps = { index: number; service: ClientService; - isPredeploy?: boolean; + lifecycleJobType?: "predeploy" | "initdeploy"; }; const MainTab: React.FC = ({ index, service, - isPredeploy = false, + lifecycleJobType, }) => { const { register, control, watch } = useFormContext(); const cron = watch(`app.services.${index}.config.cron.value`); const run = watch(`app.services.${index}.run.value`); const predeployRun = watch(`app.predeploy.${index}.run.value`); + const initdeployRun = watch(`app.initialDeploy.${index}.run.value`); const build = watch("app.build"); const source = watch("source"); @@ -55,9 +56,14 @@ const MainTab: React.FC = ({ }, []); const isStartCommandValid = useMemo(() => { - const runCommand = isPredeploy ? predeployRun : run; + const runCommand = + lifecycleJobType === "predeploy" + ? predeployRun + : lifecycleJobType === "initdeploy" + ? initdeployRun + : run; return runCommand.includes("&&") || runCommand.includes(";"); - }, [isPredeploy, predeployRun, run]); + }, [lifecycleJobType, predeployRun, run]); // if your Docker image has a CMD or ENTRYPOINT return ( @@ -87,8 +93,10 @@ const MainTab: React.FC = ({ disabled={service.run.readOnly} disabledTooltip={"You may only edit this field in your porter.yaml."} {...register( - isPredeploy + lifecycleJobType === "predeploy" ? `app.predeploy.${index}.run.value` + : lifecycleJobType === "initdeploy" + ? `app.initialDeploy.${index}.run.value` : `app.services.${index}.run.value` )} /> diff --git a/dashboard/src/main/home/app-dashboard/validate-apply/services-settings/tabs/Resources.tsx b/dashboard/src/main/home/app-dashboard/validate-apply/services-settings/tabs/Resources.tsx index f8d29b4f6b..e8f4ef5009 100644 --- a/dashboard/src/main/home/app-dashboard/validate-apply/services-settings/tabs/Resources.tsx +++ b/dashboard/src/main/home/app-dashboard/validate-apply/services-settings/tabs/Resources.tsx @@ -23,13 +23,13 @@ import IntelligentSlider from "./IntelligentSlider"; type ResourcesProps = { index: number; service: ClientService; - isPredeploy?: boolean; + lifecycleJobType?: "predeploy" | "initdeploy"; }; const Resources: React.FC = ({ index, service, - isPredeploy = false, + lifecycleJobType, }) => { const { control, register, watch } = useFormContext(); const { currentProject } = useContext(Context); @@ -69,8 +69,10 @@ const Resources: React.FC = ({ )} = ({ = ({ {match(service.config) .with({ type: "job" }, () => null) .with({ type: "predeploy" }, () => null) + .with({ type: "initdeploy" }, () => null) .otherwise((config) => ( <> diff --git a/dashboard/src/main/home/cluster-dashboard/preview-environments/v2/setup-app/Addons.tsx b/dashboard/src/main/home/cluster-dashboard/preview-environments/v2/setup-app/Addons.tsx index f67e599e16..1d2f5c6de5 100644 --- a/dashboard/src/main/home/cluster-dashboard/preview-environments/v2/setup-app/Addons.tsx +++ b/dashboard/src/main/home/cluster-dashboard/preview-environments/v2/setup-app/Addons.tsx @@ -5,10 +5,11 @@ import { useFormContext } from "react-hook-form"; import Spacer from "components/porter/Spacer"; import Text from "components/porter/Text"; import { type ButtonStatus } from "main/home/app-dashboard/app-view/AppDataContainer"; -import AppSaveButton from "main/home/app-dashboard/app-view/AppSaveButton"; import { AddonsList } from "main/home/managed-addons/AddonsList"; import { type PorterAppFormData } from "lib/porter-apps"; +import PreviewSaveButton from "./PreviewSaveButton"; + type Props = { buttonStatus: ButtonStatus; }; @@ -30,7 +31,7 @@ export const Addons: React.FC = ({ buttonStatus }) => { - = ({ serviceNames: [], envGroupNames: [], predeploy: [], + initialDeploy: [], }, addons: [], }, @@ -281,6 +282,7 @@ export const PreviewAppDataContainer: React.FC = ({ serviceNames: [], envGroupNames: [], predeploy: [], + initialDeploy: [], }, addons: existingAddonsWithEnv, }); @@ -315,12 +317,7 @@ export const PreviewAppDataContainer: React.FC = ({ .with("services", () => ( )) - .with("variables", () => ( - - )) + .with("variables", () => ) .with("required-apps", () => ( )) diff --git a/dashboard/src/main/home/cluster-dashboard/preview-environments/v2/setup-app/PreviewSaveButton.tsx b/dashboard/src/main/home/cluster-dashboard/preview-environments/v2/setup-app/PreviewSaveButton.tsx new file mode 100644 index 0000000000..3ef5d9737a --- /dev/null +++ b/dashboard/src/main/home/cluster-dashboard/preview-environments/v2/setup-app/PreviewSaveButton.tsx @@ -0,0 +1,35 @@ +import React from "react"; + +import Button from "components/porter/Button"; +import { type ButtonStatus } from "main/home/app-dashboard/app-view/AppDataContainer"; + +type Props = { + status: ButtonStatus; + isDisabled: boolean; + disabledTooltipMessage: string; + height?: string; + disabledTooltipPosition?: "top" | "bottom" | "left" | "right"; +}; +const PreviewSaveButton: React.FC = ({ + status, + isDisabled, + disabledTooltipMessage, + height, + disabledTooltipPosition, +}) => { + return ( + + ); +}; + +export default PreviewSaveButton; diff --git a/dashboard/src/main/home/cluster-dashboard/preview-environments/v2/setup-app/RequiredApps.tsx b/dashboard/src/main/home/cluster-dashboard/preview-environments/v2/setup-app/RequiredApps.tsx index 2da472fb0f..b4171a04c9 100644 --- a/dashboard/src/main/home/cluster-dashboard/preview-environments/v2/setup-app/RequiredApps.tsx +++ b/dashboard/src/main/home/cluster-dashboard/preview-environments/v2/setup-app/RequiredApps.tsx @@ -4,7 +4,6 @@ import { useFieldArray, useFormContext } from "react-hook-form"; import Spacer from "components/porter/Spacer"; import Text from "components/porter/Text"; import { type ButtonStatus } from "main/home/app-dashboard/app-view/AppDataContainer"; -import AppSaveButton from "main/home/app-dashboard/app-view/AppSaveButton"; import { useLatestRevision } from "main/home/app-dashboard/app-view/LatestRevisionContext"; import SelectableAppList from "main/home/app-dashboard/apps/SelectableAppList"; import { useLatestAppRevisions } from "lib/hooks/useLatestAppRevisions"; @@ -12,6 +11,8 @@ import { type PorterAppFormData } from "lib/porter-apps"; import { Context } from "shared/Context"; +import PreviewSaveButton from "./PreviewSaveButton"; + type Props = { buttonStatus: ButtonStatus; }; @@ -72,7 +73,7 @@ export const RequiredApps: React.FC = ({ buttonStatus }) => { })} /> - = ({ buttonStatus }) => { return ( <> + Initial deploy job + + + Pre-deploy job @@ -44,7 +54,7 @@ export const ServiceSettings: React.FC = ({ buttonStatus }) => { allowAddServices={false} /> -