Skip to content

Commit b4dff88

Browse files
committed
feat(frontend): support deploying to multiple environments at once
- Change environment selection table from radio (single-select) to checkbox (multi-select) - Deploy to all selected environments in one action - Support partial success: show success for completed envs, warning for failures
1 parent 6dc40e5 commit b4dff88

3 files changed

Lines changed: 63 additions & 41 deletions

File tree

web/oss/src/components/Playground/Components/Modals/DeployVariantModal/assets/DeployVariantModalContent/index.tsx

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -51,15 +51,16 @@ const DeployVariantModalContent = ({variantName, revision, isLoading}: any) => {
5151
return (
5252
<section className="flex flex-col gap-4" data-tour="deploy-variant-modal">
5353
<Typography.Text>
54-
Select an environment to deploy <span className="font-medium">{variantName}</span>{" "}
54+
Select environments to deploy{" "}
55+
<span className="font-medium">{variantName}</span>{" "}
5556
{typeof revision !== "undefined" && (
5657
<VersionBadge version={revision} variant="chip" />
5758
)}
5859
</Typography.Text>
5960

6061
<Table
6162
rowSelection={{
62-
type: "radio",
63+
type: "checkbox",
6364
columnWidth: 48,
6465
selectedRowKeys: selectedEnvName,
6566
onChange: (selectedRowKeys: React.Key[]) => {
@@ -76,7 +77,11 @@ const DeployVariantModalContent = ({variantName, revision, isLoading}: any) => {
7677
onRow={(env) => ({
7778
className: "cursor-pointer",
7879
onClick: () => {
79-
setSelectedEnvName([env.name])
80+
setSelectedEnvName((prev) =>
81+
prev.includes(env.name)
82+
? prev.filter((n) => n !== env.name)
83+
: [...prev, env.name],
84+
)
8085
},
8186
})}
8287
/>

web/oss/src/components/Playground/Components/Modals/DeployVariantModal/index.tsx

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -84,11 +84,15 @@ const DeployVariantModal = ({
8484
if (result?.error) message.error(result.error)
8585
return
8686
}
87-
const env = result.env as string
87+
const envs = result.envs as string[]
8888
onClose()
89-
message.success(`Published ${variantName} to ${env}`)
90-
posthog?.capture?.("app_deployed", {app_id: appId, environment: env})
89+
message.success(`Published ${variantName} to ${envs.join(", ")}`)
90+
posthog?.capture?.("app_deployed", {app_id: appId, environment: envs.join(",")})
9191
recordWidgetEvent("variant_deployed")
92+
if (result.error) {
93+
// Partial success: some environments failed
94+
message.warning(result.error)
95+
}
9296
}, [submitDeploy, onClose, variantName, appId, posthog, recordWidgetEvent])
9397

9498
return (

web/oss/src/components/Playground/Components/Modals/DeployVariantModal/store/deployVariantModalStore.ts

Lines changed: 48 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,8 @@ export const deployResetAtom = atom(null, (get, set) => {
6363
})
6464

6565
// Async submit action. Optional overrides allow providing ids directly.
66-
// Returns { ok: boolean, env?: string, error?: string }
66+
// Deploys to all selected environments.
67+
// Returns { ok: boolean, envs?: string[], error?: string }
6768
export const deploySubmitAtom = atom(
6869
null,
6970
async (get, set, overrides?: {parentVariantId?: string | null; revisionId?: string | null}) => {
@@ -73,7 +74,7 @@ export const deploySubmitAtom = atom(
7374
parentVariantId: overrides?.parentVariantId ?? baseState.parentVariantId ?? null,
7475
revisionId: overrides?.revisionId ?? baseState.revisionId ?? null,
7576
}
76-
const selectedEnvName = get(deploySelectedEnvAtom)
77+
const selectedEnvNames = get(deploySelectedEnvAtom)
7778
const note = get(deployNoteAtom)
7879
const appId = get(routerAppIdAtom)
7980
const {mutateAsync: publish} = get(publishMutationAtom)
@@ -82,12 +83,11 @@ export const deploySubmitAtom = atom(
8283
console.debug("[DeployModal] submit:start", {
8384
state,
8485
overrides,
85-
selectedEnvName,
86+
selectedEnvNames,
8687
note,
8788
})
8889

89-
const env = selectedEnvName[0]
90-
if (!env) {
90+
if (!selectedEnvNames.length) {
9191
console.debug("[DeployModal] submit:fail", {reason: "no_env_selected"})
9292
return {ok: false, error: "No environment selected"}
9393
}
@@ -116,36 +116,49 @@ export const deploySubmitAtom = atom(
116116
const workflowEntity = workflows.find((w) => w.id === workflowId)
117117
const applicationSlug = workflowEntity?.slug || undefined
118118

119-
try {
120-
console.debug("[DeployModal] submit:publish", {
121-
revisionId,
122-
env,
123-
workflowData: workflowData
124-
? {
125-
workflow_id: workflowData.workflow_id,
126-
workflow_variant_id: workflowData.workflow_variant_id,
127-
slug: workflowData.slug,
128-
resolvedVariantSlug,
129-
applicationSlug,
130-
}
131-
: null,
132-
})
133-
await publish({
134-
revisionId,
135-
environmentSlug: env,
136-
applicationId: workflowId || appId || "",
137-
workflowVariantId: workflowData?.workflow_variant_id ?? undefined,
138-
variantSlug: resolvedVariantSlug ?? undefined,
139-
applicationSlug,
140-
revisionVersion: workflowData?.version ?? undefined,
141-
note,
142-
})
143-
console.debug("[DeployModal] submit:success", {env})
144-
return {ok: true, env}
145-
} catch (e: unknown) {
146-
const errorMessage = e instanceof Error ? e.message : "Failed to deploy"
147-
console.debug("[DeployModal] submit:error", {error: e})
148-
return {ok: false, error: errorMessage}
119+
const succeeded: string[] = []
120+
const errors: string[] = []
121+
122+
for (const env of selectedEnvNames) {
123+
try {
124+
console.debug("[DeployModal] submit:publish", {
125+
revisionId,
126+
env,
127+
workflowData: workflowData
128+
? {
129+
workflow_id: workflowData.workflow_id,
130+
workflow_variant_id: workflowData.workflow_variant_id,
131+
slug: workflowData.slug,
132+
resolvedVariantSlug,
133+
applicationSlug,
134+
}
135+
: null,
136+
})
137+
await publish({
138+
revisionId,
139+
environmentSlug: env,
140+
applicationId: workflowId || appId || "",
141+
workflowVariantId: workflowData?.workflow_variant_id ?? undefined,
142+
variantSlug: resolvedVariantSlug ?? undefined,
143+
applicationSlug,
144+
revisionVersion: workflowData?.version ?? undefined,
145+
note,
146+
})
147+
console.debug("[DeployModal] submit:success", {env})
148+
succeeded.push(env)
149+
} catch (e: unknown) {
150+
const errorMessage = e instanceof Error ? e.message : "Failed to deploy"
151+
console.debug("[DeployModal] submit:error", {env, error: e})
152+
errors.push(`${env}: ${errorMessage}`)
153+
}
154+
}
155+
156+
if (succeeded.length && !errors.length) {
157+
return {ok: true, envs: succeeded}
158+
} else if (succeeded.length && errors.length) {
159+
return {ok: true, envs: succeeded, error: `Failed for: ${errors.join("; ")}`}
160+
} else {
161+
return {ok: false, error: errors.join("; ")}
149162
}
150163
},
151164
)

0 commit comments

Comments
 (0)