Skip to content

Commit 508fba8

Browse files
feat: handle update build for dynamic params (coder#18226)
resolves coder/preview#110 --------- Co-authored-by: blink-so[bot] <211532188+blink-so[bot]@users.noreply.github.com>
1 parent c339066 commit 508fba8

File tree

15 files changed

+431
-132
lines changed

15 files changed

+431
-132
lines changed

site/src/api/api.ts

Lines changed: 82 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,10 @@ import type dayjs from "dayjs";
2424
import userAgentParser from "ua-parser-js";
2525
import { OneWayWebSocket } from "../utils/OneWayWebSocket";
2626
import { delay } from "../utils/delay";
27-
import type { PostWorkspaceUsageRequest } from "./typesGenerated";
27+
import type {
28+
DynamicParametersRequest,
29+
PostWorkspaceUsageRequest,
30+
} from "./typesGenerated";
2831
import * as TypesGen from "./typesGenerated";
2932

3033
const getMissingParameters = (
@@ -73,8 +76,10 @@ const getMissingParameters = (
7376
if (templateParameter.options.length === 0) {
7477
continue;
7578
}
76-
77-
// Check if there is a new value
79+
// For multi-select, extra steps are necessary to JSON parse the value.
80+
if (templateParameter.form_type === "multi-select") {
81+
continue;
82+
}
7883
let buildParameter = newBuildParameters.find(
7984
(p) => p.name === templateParameter.name,
8085
);
@@ -231,7 +236,7 @@ export const watchWorkspaceAgentLogs = (
231236
/**
232237
* WebSocket compression in Safari (confirmed in 16.5) is broken when
233238
* the server sends large messages. The following error is seen:
234-
* WebSocket connection to 'wss://...' failed: The operation couldnt be completed.
239+
* WebSocket connection to 'wss://...' failed: The operation couldn't be completed.
235240
*/
236241
if (userAgentParser(navigator.userAgent).browser.name === "Safari") {
237242
searchParams.set("no_compression", "");
@@ -990,6 +995,17 @@ class ApiMethods {
990995
return response.data;
991996
};
992997

998+
getTemplateVersionDynamicParameters = async (
999+
versionId: string,
1000+
data: TypesGen.DynamicParametersRequest,
1001+
): Promise<TypesGen.DynamicParametersResponse> => {
1002+
const response = await this.axios.post(
1003+
`/api/v2/templateversions/${versionId}/dynamic-parameters/evaluate`,
1004+
data,
1005+
);
1006+
return response.data;
1007+
};
1008+
9931009
getTemplateVersionRichParameters = async (
9941010
versionId: string,
9951011
): Promise<TypesGen.TemplateVersionParameter[]> => {
@@ -2132,6 +2148,38 @@ class ApiMethods {
21322148
await this.axios.delete(`/api/v2/licenses/${licenseId}`);
21332149
};
21342150

2151+
getDynamicParameters = async (
2152+
templateVersionId: string,
2153+
ownerId: string,
2154+
oldBuildParameters: TypesGen.WorkspaceBuildParameter[],
2155+
) => {
2156+
const request: DynamicParametersRequest = {
2157+
id: 1,
2158+
owner_id: ownerId,
2159+
inputs: Object.fromEntries(
2160+
new Map(oldBuildParameters.map((param) => [param.name, param.value])),
2161+
),
2162+
};
2163+
2164+
const dynamicParametersResponse =
2165+
await this.getTemplateVersionDynamicParameters(
2166+
templateVersionId,
2167+
request,
2168+
);
2169+
2170+
return dynamicParametersResponse.parameters.map((p) => ({
2171+
...p,
2172+
description_plaintext: p.description || "",
2173+
default_value: p.default_value?.valid ? p.default_value.value : "",
2174+
options: p.options
2175+
? p.options.map((opt) => ({
2176+
...opt,
2177+
value: opt.value?.valid ? opt.value.value : "",
2178+
}))
2179+
: [],
2180+
}));
2181+
};
2182+
21352183
/** Steps to change the workspace version
21362184
* - Get the latest template to access the latest active version
21372185
* - Get the current build parameters
@@ -2145,11 +2193,23 @@ class ApiMethods {
21452193
workspace: TypesGen.Workspace,
21462194
templateVersionId: string,
21472195
newBuildParameters: TypesGen.WorkspaceBuildParameter[] = [],
2196+
isDynamicParametersEnabled = false,
21482197
): Promise<TypesGen.WorkspaceBuild> => {
2149-
const [currentBuildParameters, templateParameters] = await Promise.all([
2150-
this.getWorkspaceBuildParameters(workspace.latest_build.id),
2151-
this.getTemplateVersionRichParameters(templateVersionId),
2152-
]);
2198+
const currentBuildParameters = await this.getWorkspaceBuildParameters(
2199+
workspace.latest_build.id,
2200+
);
2201+
2202+
let templateParameters: TypesGen.TemplateVersionParameter[] = [];
2203+
if (isDynamicParametersEnabled) {
2204+
templateParameters = await this.getDynamicParameters(
2205+
templateVersionId,
2206+
workspace.owner_id,
2207+
currentBuildParameters,
2208+
);
2209+
} else {
2210+
templateParameters =
2211+
await this.getTemplateVersionRichParameters(templateVersionId);
2212+
}
21532213

21542214
const missingParameters = getMissingParameters(
21552215
currentBuildParameters,
@@ -2180,15 +2240,27 @@ class ApiMethods {
21802240
updateWorkspace = async (
21812241
workspace: TypesGen.Workspace,
21822242
newBuildParameters: TypesGen.WorkspaceBuildParameter[] = [],
2243+
isDynamicParametersEnabled = false,
21832244
): Promise<TypesGen.WorkspaceBuild> => {
21842245
const [template, oldBuildParameters] = await Promise.all([
21852246
this.getTemplate(workspace.template_id),
21862247
this.getWorkspaceBuildParameters(workspace.latest_build.id),
21872248
]);
21882249

21892250
const activeVersionId = template.active_version_id;
2190-
const templateParameters =
2191-
await this.getTemplateVersionRichParameters(activeVersionId);
2251+
2252+
let templateParameters: TypesGen.TemplateVersionParameter[] = [];
2253+
2254+
if (isDynamicParametersEnabled) {
2255+
templateParameters = await this.getDynamicParameters(
2256+
activeVersionId,
2257+
workspace.owner_id,
2258+
oldBuildParameters,
2259+
);
2260+
} else {
2261+
templateParameters =
2262+
await this.getTemplateVersionRichParameters(activeVersionId);
2263+
}
21922264

21932265
const missingParameters = getMissingParameters(
21942266
oldBuildParameters,

site/src/api/queries/workspaces.ts

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,7 @@ export const updateDeadline = (
163163
export const changeVersion = (
164164
workspace: Workspace,
165165
queryClient: QueryClient,
166+
isDynamicParametersEnabled: boolean,
166167
) => {
167168
return {
168169
mutationFn: ({
@@ -172,7 +173,12 @@ export const changeVersion = (
172173
versionId: string;
173174
buildParameters?: WorkspaceBuildParameter[];
174175
}) => {
175-
return API.changeWorkspaceVersion(workspace, versionId, buildParameters);
176+
return API.changeWorkspaceVersion(
177+
workspace,
178+
versionId,
179+
buildParameters,
180+
isDynamicParametersEnabled,
181+
);
176182
},
177183
onSuccess: async (build: WorkspaceBuild) => {
178184
await updateWorkspaceBuild(build, queryClient);
@@ -185,8 +191,18 @@ export const updateWorkspace = (
185191
queryClient: QueryClient,
186192
) => {
187193
return {
188-
mutationFn: (buildParameters?: WorkspaceBuildParameter[]) => {
189-
return API.updateWorkspace(workspace, buildParameters);
194+
mutationFn: ({
195+
buildParameters,
196+
isDynamicParametersEnabled,
197+
}: {
198+
buildParameters?: WorkspaceBuildParameter[];
199+
isDynamicParametersEnabled: boolean;
200+
}) => {
201+
return API.updateWorkspace(
202+
workspace,
203+
buildParameters,
204+
isDynamicParametersEnabled,
205+
);
190206
},
191207
onSuccess: async (build: WorkspaceBuild) => {
192208
await updateWorkspaceBuild(build, queryClient);

site/src/components/Dialog/Dialog.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ export const DialogContent = forwardRef<
4545
<DialogPrimitive.Content
4646
ref={ref}
4747
className={cn(
48-
`fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg gap-4
48+
`fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg gap-6
4949
border border-solid border-border bg-surface-primary p-8 shadow-lg duration-200 sm:rounded-lg
5050
translate-x-[-50%] translate-y-[-50%]
5151
data-[state=open]:animate-in data-[state=closed]:animate-out
@@ -68,7 +68,7 @@ export const DialogHeader: FC<HTMLAttributes<HTMLDivElement>> = ({
6868
}) => (
6969
<div
7070
className={cn(
71-
"flex flex-col space-y-1.5 text-center sm:text-left",
71+
"flex flex-col space-y-5 text-center sm:text-left",
7272
className,
7373
)}
7474
{...props}
@@ -108,7 +108,7 @@ export const DialogDescription = forwardRef<
108108
>(({ className, ...props }, ref) => (
109109
<DialogPrimitive.Description
110110
ref={ref}
111-
className={cn("text-sm text-content-secondary", className)}
111+
className={cn("text-sm text-content-secondary font-medium", className)}
112112
{...props}
113113
/>
114114
));
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import { useQuery } from "react-query";
2+
3+
export const optOutKey = (id: string): string => `parameters.${id}.optOut`;
4+
5+
interface UseDynamicParametersOptOutOptions {
6+
templateId: string | undefined;
7+
templateUsesClassicParameters: boolean | undefined;
8+
enabled: boolean;
9+
}
10+
11+
export const useDynamicParametersOptOut = ({
12+
templateId,
13+
templateUsesClassicParameters,
14+
enabled,
15+
}: UseDynamicParametersOptOutOptions) => {
16+
return useQuery({
17+
enabled: !!templateId && enabled,
18+
queryKey: ["dynamicParametersOptOut", templateId],
19+
queryFn: () => {
20+
if (!templateId) {
21+
// This should not happen if enabled is working correctly,
22+
// but as a type guard and sanity check.
23+
throw new Error("templateId is required");
24+
}
25+
const localStorageKey = optOutKey(templateId);
26+
const storedOptOutString = localStorage.getItem(localStorageKey);
27+
28+
let optedOut: boolean;
29+
30+
if (storedOptOutString !== null) {
31+
optedOut = storedOptOutString === "true";
32+
} else {
33+
optedOut = Boolean(templateUsesClassicParameters);
34+
}
35+
36+
return {
37+
templateId,
38+
optedOut,
39+
};
40+
},
41+
});
42+
};
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
import type { TemplateVersionParameter } from "api/typesGenerated";
2+
import { Button } from "components/Button/Button";
3+
import {
4+
Dialog,
5+
DialogContent,
6+
DialogDescription,
7+
DialogFooter,
8+
DialogHeader,
9+
DialogTitle,
10+
} from "components/Dialog/Dialog";
11+
import type { FC } from "react";
12+
import { useNavigate } from "react-router-dom";
13+
14+
type UpdateBuildParametersDialogExperimentalProps = {
15+
open: boolean;
16+
onClose: () => void;
17+
missedParameters: TemplateVersionParameter[];
18+
workspaceOwnerName: string;
19+
workspaceName: string;
20+
templateVersionId: string | undefined;
21+
};
22+
23+
export const UpdateBuildParametersDialogExperimental: FC<
24+
UpdateBuildParametersDialogExperimentalProps
25+
> = ({
26+
missedParameters,
27+
open,
28+
onClose,
29+
workspaceOwnerName,
30+
workspaceName,
31+
templateVersionId,
32+
}) => {
33+
const navigate = useNavigate();
34+
35+
const handleGoToParameters = () => {
36+
onClose();
37+
navigate(
38+
`/@${workspaceOwnerName}/${workspaceName}/settings/parameters?templateVersionId=${templateVersionId}`,
39+
);
40+
};
41+
42+
return (
43+
<Dialog open={open} onOpenChange={(isOpen) => !isOpen && onClose()}>
44+
<DialogContent>
45+
<DialogHeader>
46+
<DialogTitle>Update workspace parameters</DialogTitle>
47+
<DialogDescription>
48+
This template has{" "}
49+
<strong className="text-content-primary">
50+
{missedParameters.length} new parameter
51+
{missedParameters.length === 1 ? "" : "s"}
52+
</strong>{" "}
53+
that must be configured to complete the update.
54+
</DialogDescription>
55+
<DialogDescription>
56+
Would you like to go to the workspace parameters page to review and
57+
update these parameters before continuing?
58+
</DialogDescription>
59+
</DialogHeader>
60+
<DialogFooter>
61+
<Button onClick={onClose} variant="outline">
62+
Cancel
63+
</Button>
64+
<Button onClick={handleGoToParameters}>
65+
Go to workspace parameters
66+
</Button>
67+
</DialogFooter>
68+
</DialogContent>
69+
</Dialog>
70+
);
71+
};

0 commit comments

Comments
 (0)