Skip to content

feat: handle update build for dynamic params #18226

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 16 commits into from
Jun 5, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
92 changes: 82 additions & 10 deletions site/src/api/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,10 @@ import type dayjs from "dayjs";
import userAgentParser from "ua-parser-js";
import { OneWayWebSocket } from "../utils/OneWayWebSocket";
import { delay } from "../utils/delay";
import type { PostWorkspaceUsageRequest } from "./typesGenerated";
import type {
DynamicParametersRequest,
PostWorkspaceUsageRequest,
} from "./typesGenerated";
import * as TypesGen from "./typesGenerated";

const getMissingParameters = (
Expand Down Expand Up @@ -73,8 +76,10 @@ const getMissingParameters = (
if (templateParameter.options.length === 0) {
continue;
}

// Check if there is a new value
// For multi-select, extra steps are necessary to JSON parse the value.
if (templateParameter.form_type === "multi-select") {
continue;
}
let buildParameter = newBuildParameters.find(
(p) => p.name === templateParameter.name,
);
Expand Down Expand Up @@ -231,7 +236,7 @@ export const watchWorkspaceAgentLogs = (
/**
* WebSocket compression in Safari (confirmed in 16.5) is broken when
* the server sends large messages. The following error is seen:
* WebSocket connection to 'wss://...' failed: The operation couldnt be completed.
* WebSocket connection to 'wss://...' failed: The operation couldn't be completed.
*/
if (userAgentParser(navigator.userAgent).browser.name === "Safari") {
searchParams.set("no_compression", "");
Expand Down Expand Up @@ -990,6 +995,17 @@ class ApiMethods {
return response.data;
};

getTemplateVersionDynamicParameters = async (
versionId: string,
data: TypesGen.DynamicParametersRequest,
): Promise<TypesGen.DynamicParametersResponse> => {
const response = await this.axios.post(
`/api/v2/templateversions/${versionId}/dynamic-parameters/evaluate`,
data,
);
return response.data;
};

getTemplateVersionRichParameters = async (
versionId: string,
): Promise<TypesGen.TemplateVersionParameter[]> => {
Expand Down Expand Up @@ -2132,6 +2148,38 @@ class ApiMethods {
await this.axios.delete(`/api/v2/licenses/${licenseId}`);
};

getDynamicParameters = async (
templateVersionId: string,
ownerId: string,
oldBuildParameters: TypesGen.WorkspaceBuildParameter[],
) => {
const request: DynamicParametersRequest = {
id: 1,
owner_id: ownerId,
inputs: Object.fromEntries(
new Map(oldBuildParameters.map((param) => [param.name, param.value])),
),
};

const dynamicParametersResponse =
await this.getTemplateVersionDynamicParameters(
templateVersionId,
request,
);

return dynamicParametersResponse.parameters.map((p) => ({
...p,
description_plaintext: p.description || "",
default_value: p.default_value?.valid ? p.default_value.value : "",
options: p.options
? p.options.map((opt) => ({
...opt,
value: opt.value?.valid ? opt.value.value : "",
}))
: [],
}));
};

/** Steps to change the workspace version
* - Get the latest template to access the latest active version
* - Get the current build parameters
Expand All @@ -2145,11 +2193,23 @@ class ApiMethods {
workspace: TypesGen.Workspace,
templateVersionId: string,
newBuildParameters: TypesGen.WorkspaceBuildParameter[] = [],
isDynamicParametersEnabled = false,
): Promise<TypesGen.WorkspaceBuild> => {
const [currentBuildParameters, templateParameters] = await Promise.all([
this.getWorkspaceBuildParameters(workspace.latest_build.id),
this.getTemplateVersionRichParameters(templateVersionId),
]);
const currentBuildParameters = await this.getWorkspaceBuildParameters(
workspace.latest_build.id,
);

let templateParameters: TypesGen.TemplateVersionParameter[] = [];
if (isDynamicParametersEnabled) {
templateParameters = await this.getDynamicParameters(
templateVersionId,
workspace.owner_id,
currentBuildParameters,
);
} else {
templateParameters =
await this.getTemplateVersionRichParameters(templateVersionId);
}

const missingParameters = getMissingParameters(
currentBuildParameters,
Expand Down Expand Up @@ -2180,15 +2240,27 @@ class ApiMethods {
updateWorkspace = async (
workspace: TypesGen.Workspace,
newBuildParameters: TypesGen.WorkspaceBuildParameter[] = [],
isDynamicParametersEnabled = false,
): Promise<TypesGen.WorkspaceBuild> => {
const [template, oldBuildParameters] = await Promise.all([
this.getTemplate(workspace.template_id),
this.getWorkspaceBuildParameters(workspace.latest_build.id),
]);

const activeVersionId = template.active_version_id;
const templateParameters =
await this.getTemplateVersionRichParameters(activeVersionId);

let templateParameters: TypesGen.TemplateVersionParameter[] = [];

if (isDynamicParametersEnabled) {
templateParameters = await this.getDynamicParameters(
activeVersionId,
workspace.owner_id,
oldBuildParameters,
);
} else {
templateParameters =
await this.getTemplateVersionRichParameters(activeVersionId);
}

const missingParameters = getMissingParameters(
oldBuildParameters,
Expand Down
22 changes: 19 additions & 3 deletions site/src/api/queries/workspaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,7 @@ export const updateDeadline = (
export const changeVersion = (
workspace: Workspace,
queryClient: QueryClient,
isDynamicParametersEnabled: boolean,
) => {
return {
mutationFn: ({
Expand All @@ -172,7 +173,12 @@ export const changeVersion = (
versionId: string;
buildParameters?: WorkspaceBuildParameter[];
}) => {
return API.changeWorkspaceVersion(workspace, versionId, buildParameters);
return API.changeWorkspaceVersion(
workspace,
versionId,
buildParameters,
isDynamicParametersEnabled,
);
},
onSuccess: async (build: WorkspaceBuild) => {
await updateWorkspaceBuild(build, queryClient);
Expand All @@ -185,8 +191,18 @@ export const updateWorkspace = (
queryClient: QueryClient,
) => {
return {
mutationFn: (buildParameters?: WorkspaceBuildParameter[]) => {
return API.updateWorkspace(workspace, buildParameters);
mutationFn: ({
buildParameters,
isDynamicParametersEnabled,
}: {
buildParameters?: WorkspaceBuildParameter[];
isDynamicParametersEnabled: boolean;
}) => {
return API.updateWorkspace(
workspace,
buildParameters,
isDynamicParametersEnabled,
);
},
onSuccess: async (build: WorkspaceBuild) => {
await updateWorkspaceBuild(build, queryClient);
Expand Down
6 changes: 3 additions & 3 deletions site/src/components/Dialog/Dialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ export const DialogContent = forwardRef<
<DialogPrimitive.Content
ref={ref}
className={cn(
`fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg gap-4
`fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg gap-6
border border-solid border-border bg-surface-primary p-8 shadow-lg duration-200 sm:rounded-lg
translate-x-[-50%] translate-y-[-50%]
data-[state=open]:animate-in data-[state=closed]:animate-out
Expand All @@ -68,7 +68,7 @@ export const DialogHeader: FC<HTMLAttributes<HTMLDivElement>> = ({
}) => (
<div
className={cn(
"flex flex-col space-y-1.5 text-center sm:text-left",
"flex flex-col space-y-5 text-center sm:text-left",
className,
)}
{...props}
Expand Down Expand Up @@ -108,7 +108,7 @@ export const DialogDescription = forwardRef<
>(({ className, ...props }, ref) => (
<DialogPrimitive.Description
ref={ref}
className={cn("text-sm text-content-secondary", className)}
className={cn("text-sm text-content-secondary font-medium", className)}
{...props}
/>
));
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { useQuery } from "react-query";

export const optOutKey = (id: string): string => `parameters.${id}.optOut`;

interface UseDynamicParametersOptOutOptions {
templateId: string | undefined;
templateUsesClassicParameters: boolean | undefined;
enabled: boolean;
}

export const useDynamicParametersOptOut = ({
templateId,
templateUsesClassicParameters,
enabled,
}: UseDynamicParametersOptOutOptions) => {
return useQuery({
enabled: !!templateId && enabled,
queryKey: ["dynamicParametersOptOut", templateId],
queryFn: () => {
if (!templateId) {
// This should not happen if enabled is working correctly,
// but as a type guard and sanity check.
throw new Error("templateId is required");
}
const localStorageKey = optOutKey(templateId);
const storedOptOutString = localStorage.getItem(localStorageKey);

let optedOut: boolean;

if (storedOptOutString !== null) {
optedOut = storedOptOutString === "true";
} else {
optedOut = Boolean(templateUsesClassicParameters);
}

return {
templateId,
optedOut,
};
},
});
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import type { TemplateVersionParameter } from "api/typesGenerated";
import { Button } from "components/Button/Button";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "components/Dialog/Dialog";
import type { FC } from "react";
import { useNavigate } from "react-router-dom";

type UpdateBuildParametersDialogExperimentalProps = {
open: boolean;
onClose: () => void;
missedParameters: TemplateVersionParameter[];
workspaceOwnerName: string;
workspaceName: string;
templateVersionId: string | undefined;
};

export const UpdateBuildParametersDialogExperimental: FC<
UpdateBuildParametersDialogExperimentalProps
> = ({
missedParameters,
open,
onClose,
workspaceOwnerName,
workspaceName,
templateVersionId,
}) => {
const navigate = useNavigate();

const handleGoToParameters = () => {
onClose();
navigate(
`/@${workspaceOwnerName}/${workspaceName}/settings/parameters?templateVersionId=${templateVersionId}`,
);
};

return (
<Dialog open={open} onOpenChange={(isOpen) => !isOpen && onClose()}>
<DialogContent>
<DialogHeader>
<DialogTitle>Update workspace parameters</DialogTitle>
<DialogDescription>
This template has{" "}
<strong className="text-content-primary">
{missedParameters.length} new parameter
{missedParameters.length === 1 ? "" : "s"}
</strong>{" "}
that must be configured to complete the update.
</DialogDescription>
<DialogDescription>
Would you like to go to the workspace parameters page to review and
update these parameters before continuing?
</DialogDescription>
</DialogHeader>
<DialogFooter>
<Button onClick={onClose} variant="outline">
Cancel
</Button>
<Button onClick={handleGoToParameters}>
Go to workspace parameters
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
};
Loading
Loading