Skip to content

Commit 508fba8

Browse files
feat: handle update build for dynamic params (#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)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy