Skip to content

Commit 9f667a8

Browse files
fix-build-progress-status (#5215)
* env-vars-updates * fix-reconciliation-state * fix-build-progress-status * multi-env-sliders (#5216) * multi-env-sliders * chore: fmt * feat: extract shared logic * fix: pr comments * fix: apply changes to select env and upsert
1 parent 5084861 commit 9f667a8

34 files changed

+1278
-688
lines changed

web/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/(overview)/deployments/[deploymentId]/(deployment-progress)/deployment-info.tsx

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,17 +4,25 @@ import { formatCpuParts, formatMemoryParts } from "@/lib/utils/deployment-format
44
import { Bolt, Cloud, Grid, LayoutRight } from "@unkey/icons";
55
import { Button, InfoTooltip } from "@unkey/ui";
66
import { ActiveDeploymentCard } from "../../../../components/active-deployment-card";
7-
import { DeploymentStatusBadge } from "../../../../components/deployment-status-badge";
7+
import {
8+
type DeploymentStatus,
9+
DeploymentStatusBadge,
10+
} from "../../../../components/deployment-status-badge";
811
import { InfoChip } from "../../../../components/info-chip";
912
import { RegionFlags } from "../../../../components/region-flags";
1013
import { Section, SectionHeader } from "../../../../components/section";
1114
import { useOptionalProjectLayout } from "../../../layout-provider";
1215
import { useDeployment } from "../layout-provider";
1316

14-
export function DeploymentInfo({ title = "Deployment" }: { title?: string }) {
17+
type DeploymentInfoProps = {
18+
title?: string;
19+
statusOverride?: DeploymentStatus;
20+
};
21+
22+
export function DeploymentInfo({ title = "Deployment", statusOverride }: DeploymentInfoProps) {
1523
const { deployment } = useDeployment();
1624
const projectLayout = useOptionalProjectLayout();
17-
const deploymentStatus = deployment.status;
25+
const deploymentStatus = statusOverride ?? deployment.status;
1826

1927
return (
2028
<Section>

web/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/(overview)/deployments/[deploymentId]/(deployment-progress)/deployment-progress.tsx

Lines changed: 7 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
"use client";
22

33
import { trpc } from "@/lib/trpc/client";
4+
import type { Router } from "@/lib/trpc/routers";
5+
import type { inferRouterOutputs } from "@trpc/server";
46
import { CloudUp, Earth, Hammer2, LayerFront, Pulse, Sparkle3 } from "@unkey/icons";
57
import { Button, SettingCardGroup } from "@unkey/ui";
68
import { useParams, useRouter } from "next/navigation";
@@ -12,22 +14,16 @@ import { useDeployment } from "../layout-provider";
1214
import { DeploymentBuildStepsTable } from "./build-steps-table/deployment-build-steps-table";
1315
import { DeploymentStep } from "./deployment-step";
1416

15-
export function DeploymentProgress() {
17+
type RouterOutputs = inferRouterOutputs<Router>;
18+
export type StepsData = RouterOutputs["deploy"]["deployment"]["steps"];
19+
20+
export function DeploymentProgress({ stepsData }: { stepsData?: StepsData }) {
1621
const { deployment } = useDeployment();
1722
const router = useRouter();
1823
const params = useParams();
1924
const workspaceSlug = params.workspaceSlug as string;
2025
const isFailed = deployment.status === "failed";
2126

22-
const steps = trpc.deploy.deployment.steps.useQuery(
23-
{
24-
deploymentId: deployment.id,
25-
},
26-
{
27-
refetchInterval: 1_000,
28-
},
29-
);
30-
3127
const buildSteps = trpc.deploy.deployment.buildSteps.useQuery(
3228
{
3329
deploymentId: deployment.id,
@@ -51,7 +47,7 @@ export function DeploymentProgress() {
5147
};
5248
}, [isFailed]);
5349

54-
const { building, deploying, network, queued, starting, finalizing } = steps.data ?? {};
50+
const { building, deploying, network, queued, starting, finalizing } = stepsData ?? {};
5551

5652
const [redeployOpen, setRedeployOpen] = useState(false);
5753
const domainsForDeployment = getDomainsForDeployment(deployment.id);
Lines changed: 60 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
"use client";
2-
import { useEffect } from "react";
2+
import { trpc } from "@/lib/trpc/client";
3+
import { useEffect, useMemo } from "react";
34
import { DeploymentDomainsCard } from "../../../components/deployment-domains-card";
5+
import type { DeploymentStatus } from "../../../components/deployment-status-badge";
46
import { ProjectContentWrapper } from "../../../components/project-content-wrapper";
57
import { useProjectData } from "../../data-provider";
68
import { DeploymentInfo } from "./(deployment-progress)/deployment-info";
7-
import { DeploymentProgress } from "./(deployment-progress)/deployment-progress";
9+
import { DeploymentProgress, type StepsData } from "./(deployment-progress)/deployment-progress";
810
import { DeploymentNetworkSection } from "./(overview)/components/sections/deployment-network-section";
911
import { useDeployment } from "./layout-provider";
1012

@@ -14,25 +16,78 @@ export default function DeploymentOverview() {
1416

1517
const ready = deployment.status === "ready";
1618

19+
const stepsQuery = trpc.deploy.deployment.steps.useQuery(
20+
{ deploymentId: deployment.id },
21+
{ refetchInterval: ready ? false : 1_000, refetchOnWindowFocus: false },
22+
);
23+
24+
const derivedStatus = useMemo(
25+
() => deriveStatusFromSteps(stepsQuery.data, deployment.status),
26+
[stepsQuery.data, deployment.status],
27+
);
28+
1729
useEffect(() => {
1830
if (ready) {
31+
stepsQuery.refetch();
1932
refetchDomains();
2033
}
21-
}, [ready, refetchDomains]);
34+
}, [ready, refetchDomains, stepsQuery.refetch]);
2235

2336
return (
2437
<ProjectContentWrapper centered>
25-
<DeploymentInfo />
38+
<DeploymentInfo statusOverride={derivedStatus} />
2639
{ready ? (
2740
<div key="ready" className="flex flex-col gap-5 animate-fade-slide-in">
2841
<DeploymentDomainsCard />
2942
<DeploymentNetworkSection />
3043
</div>
3144
) : (
3245
<div key="progress" className="animate-fade-slide-in">
33-
<DeploymentProgress />
46+
<DeploymentProgress stepsData={stepsQuery.data} />
3447
</div>
3548
)}
3649
</ProjectContentWrapper>
3750
);
3851
}
52+
53+
const DEPLOYMENT_STATUSES: ReadonlySet<string> = new Set<DeploymentStatus>([
54+
"pending",
55+
"building",
56+
"deploying",
57+
"network",
58+
"ready",
59+
"failed",
60+
]);
61+
62+
function isDeploymentStatus(value: string): value is DeploymentStatus {
63+
return DEPLOYMENT_STATUSES.has(value);
64+
}
65+
66+
function deriveStatusFromSteps(steps: StepsData | undefined, fallback: string): DeploymentStatus {
67+
if (!steps) {
68+
return isDeploymentStatus(fallback) ? fallback : "pending";
69+
}
70+
71+
const { queued, building, deploying, network } = steps;
72+
73+
if ([queued, building, deploying, network].some((s) => s?.error)) {
74+
return "failed";
75+
}
76+
if (network?.completed) {
77+
return "ready";
78+
}
79+
if (network && !network.endedAt) {
80+
return "network";
81+
}
82+
if (deploying && !deploying.endedAt) {
83+
return "deploying";
84+
}
85+
if (building && !building.endedAt) {
86+
return "building";
87+
}
88+
if (queued && !queued.endedAt) {
89+
return "pending";
90+
}
91+
92+
return isDeploymentStatus(fallback) ? fallback : "pending";
93+
}

web/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/(overview)/settings/components/build-settings/dockerfile-settings.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ const dockerfileSchema = z.object({
1212
});
1313

1414
export const Dockerfile = () => {
15-
const { settings, autoSave } = useEnvironmentSettings();
15+
const { settings, variant } = useEnvironmentSettings();
1616
const { environmentId, dockerfile: defaultValue } = settings;
1717

1818
const {
@@ -48,7 +48,7 @@ export const Dockerfile = () => {
4848
displayValue={defaultValue}
4949
onSubmit={handleSubmit(onSubmit)}
5050
saveState={saveState}
51-
autoSave={autoSave}
51+
autoSave={variant === "onboarding"}
5252
>
5353
<FormInput
5454
required

web/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/(overview)/settings/components/build-settings/root-directory-settings.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ const rootDirectorySchema = z.object({
1212
});
1313

1414
export const RootDirectory = () => {
15-
const { settings, autoSave } = useEnvironmentSettings();
15+
const { settings, variant } = useEnvironmentSettings();
1616
const { environmentId, dockerContext: defaultValue } = settings;
1717

1818
const {
@@ -48,7 +48,7 @@ export const RootDirectory = () => {
4848
displayValue={defaultValue || "."}
4949
onSubmit={handleSubmit(onSubmit)}
5050
saveState={saveState}
51-
autoSave={autoSave}
51+
autoSave={variant === "onboarding"}
5252
>
5353
<FormInput
5454
label="Root directory"

web/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/(overview)/settings/components/runtime-settings/command.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ const commandSchema = z.object({
1717
type CommandFormValues = z.infer<typeof commandSchema>;
1818

1919
export const Command = () => {
20-
const { settings, autoSave } = useEnvironmentSettings();
20+
const { settings, variant } = useEnvironmentSettings();
2121
const { command, environmentId } = settings;
2222
const defaultCommand = command.join(" ");
2323

@@ -70,7 +70,7 @@ export const Command = () => {
7070
}
7171
onSubmit={handleSubmit(onSubmit)}
7272
saveState={saveState}
73-
autoSave={autoSave}
73+
autoSave={variant === "onboarding"}
7474
>
7575
<FormTextarea
7676
label="Command"
Lines changed: 18 additions & 103 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,11 @@
11
"use client";
22

3-
import { collection } from "@/lib/collections";
43
import { formatCpuParts } from "@/lib/utils/deployment-formatters";
5-
import { zodResolver } from "@hookform/resolvers/zod";
64
import { Bolt } from "@unkey/icons";
7-
import { Slider } from "@unkey/ui";
8-
import { useEffect } from "react";
9-
import { useForm, useWatch } from "react-hook-form";
10-
import { z } from "zod";
11-
import { useEnvironmentSettings } from "../../environment-provider";
12-
import { FormSettingCard, resolveSaveState } from "../shared/form-setting-card";
13-
import { SettingDescription } from "../shared/setting-description";
14-
import { indexToValue, valueToIndex } from "../shared/slider-utils";
5+
import {
6+
type ResourceSliderConfig,
7+
ResourceSliderSetting,
8+
} from "../shared/resource-slider-setting";
159

1610
const CPU_OPTIONS = [
1711
{ label: "1/4 vCPU", value: 256 },
@@ -24,97 +18,18 @@ const CPU_OPTIONS = [
2418
{ label: "32 vCPU", value: 32768 },
2519
] as const;
2620

27-
const cpuSchema = z.object({
28-
cpu: z.number(),
29-
});
30-
31-
type CpuFormValues = z.infer<typeof cpuSchema>;
32-
33-
export const Cpu = () => {
34-
const { settings, autoSave } = useEnvironmentSettings();
35-
const { environmentId, cpuMillicores: defaultCpu } = settings;
36-
37-
const {
38-
handleSubmit,
39-
setValue,
40-
formState: { isValid, isSubmitting },
41-
control,
42-
reset,
43-
} = useForm<CpuFormValues>({
44-
resolver: zodResolver(cpuSchema),
45-
mode: "onChange",
46-
defaultValues: { cpu: defaultCpu },
47-
});
48-
49-
useEffect(() => {
50-
reset({ cpu: defaultCpu });
51-
}, [defaultCpu, reset]);
52-
53-
const currentCpu = useWatch({ control, name: "cpu" });
54-
55-
const onSubmit = async (values: CpuFormValues) => {
56-
collection.environmentSettings.update(environmentId, (draft) => {
57-
draft.cpuMillicores = values.cpu;
58-
});
59-
};
60-
61-
const hasChanges = currentCpu !== defaultCpu;
62-
const currentIndex = valueToIndex(CPU_OPTIONS, currentCpu);
63-
64-
const saveState = resolveSaveState([
65-
[isSubmitting, { status: "saving" }],
66-
[!isValid, { status: "disabled" }],
67-
[!hasChanges, { status: "disabled", reason: "No changes to save" }],
68-
]);
69-
70-
return (
71-
<FormSettingCard
72-
icon={<Bolt className="text-gray-12" iconSize="xl-medium" />}
73-
title="CPU"
74-
description="CPU allocation for each instance"
75-
displayValue={(() => {
76-
const parts = formatCpuParts(defaultCpu);
77-
return (
78-
<div className="space-x-1">
79-
<span className="font-medium text-gray-12">{parts.value}</span>
80-
<span className="text-gray-11 font-normal">{parts.unit}</span>
81-
</div>
82-
);
83-
})()}
84-
onSubmit={handleSubmit(onSubmit)}
85-
saveState={saveState}
86-
autoSave={autoSave}
87-
>
88-
<div className="flex flex-col">
89-
<span className="text-gray-11 text-[13px]">CPU per instance</span>
90-
<div className="flex items-center gap-3">
91-
<Slider
92-
min={0}
93-
max={CPU_OPTIONS.length - 1}
94-
step={1}
95-
value={[currentIndex]}
96-
onValueChange={([value]) => {
97-
if (value !== undefined) {
98-
setValue("cpu", indexToValue(CPU_OPTIONS, value, 256), { shouldValidate: true });
99-
}
100-
}}
101-
onValueCommit={autoSave ? () => handleSubmit(onSubmit)() : undefined}
102-
className="flex-1 max-w-[480px]"
103-
rangeStyle={{
104-
background: "linear-gradient(to right, hsla(var(--infoA-4)), hsla(var(--infoA-12)))",
105-
backgroundSize: `${currentIndex > 0 ? 100 / (currentIndex / (CPU_OPTIONS.length - 1)) : 100}% 100%`,
106-
backgroundRepeat: "no-repeat",
107-
}}
108-
/>
109-
<span className="text-[13px]">
110-
<span className="font-medium text-gray-12">{formatCpuParts(currentCpu).value}</span>{" "}
111-
<span className="text-gray-11">{formatCpuParts(currentCpu).unit}</span>
112-
</span>
113-
</div>
114-
<SettingDescription>
115-
Higher CPU improves compute-heavy workloads. Changes apply on next deploy.
116-
</SettingDescription>
117-
</div>
118-
</FormSettingCard>
119-
);
21+
const cpuConfig: ResourceSliderConfig = {
22+
icon: <Bolt className="text-gray-12" iconSize="xl-medium" />,
23+
title: "CPU",
24+
description: "CPU allocation for each instance",
25+
settingDescription: "Higher CPU improves compute-heavy workloads. Changes apply on next deploy.",
26+
colorVar: "infoA",
27+
slider: { kind: "index-mapped", options: CPU_OPTIONS, fallback: 256 },
28+
formatValue: formatCpuParts,
29+
readValue: (s) => s.cpuMillicores,
30+
writeValue: (draft, value) => {
31+
draft.cpuMillicores = value;
32+
},
12033
};
34+
35+
export const Cpu = () => <ResourceSliderSetting config={cpuConfig} />;

web/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/(overview)/settings/components/runtime-settings/healthcheck/index.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ import { HTTP_METHODS, type HealthcheckFormValues, healthcheckSchema } from "./s
2121
import { intervalToSeconds, secondsToInterval } from "./utils";
2222

2323
export const Healthcheck = () => {
24-
const { settings, autoSave } = useEnvironmentSettings();
24+
const { settings, variant } = useEnvironmentSettings();
2525
const { healthcheck, environmentId } = settings;
2626

2727
const defaultValues: HealthcheckFormValues = {
@@ -92,7 +92,7 @@ export const Healthcheck = () => {
9292
}
9393
onSubmit={handleSubmit(onSubmit)}
9494
saveState={saveState}
95-
autoSave={autoSave}
95+
autoSave={variant === "onboarding"}
9696
>
9797
<div className="flex flex-col gap-2 w-120">
9898
{/* TODO: multi-check when API supports

0 commit comments

Comments
 (0)