From c530f5d3e4ebd1e708ad9cc5f6b1545af216e2f2 Mon Sep 17 00:00:00 2001 From: McKayla Washburn Date: Thu, 17 Jul 2025 23:58:50 +0000 Subject: [PATCH 1/5] something like this --- site/src/api/api.ts | 43 ++++++++++++++----- ...pdateBuildParametersDialogExperimental.tsx | 10 ++--- .../WorkspaceMoreActions.tsx | 12 +++--- .../workspaces/WorkspaceUpdateDialogs.tsx | 15 +++++-- 4 files changed, 54 insertions(+), 26 deletions(-) diff --git a/site/src/api/api.ts b/site/src/api/api.ts index 013c018d5c656..333583e19ffef 100644 --- a/site/src/api/api.ts +++ b/site/src/api/api.ts @@ -24,6 +24,7 @@ import type dayjs from "dayjs"; import userAgentParser from "ua-parser-js"; import { OneWayWebSocket } from "../utils/OneWayWebSocket"; import { delay } from "../utils/delay"; +import { type FieldError, isApiError } from "./errors"; import type { DynamicParametersRequest, PostWorkspaceUsageRequest, @@ -390,6 +391,14 @@ export class MissingBuildParameters extends Error { } } +export class ParameterValidationError extends Error { + constructor( + public readonly versionId: string, + public readonly validations: FieldError[], + ) { + super("Parameters are not valid for new template version"); + } +} export type GetProvisionerJobsParams = { status?: string; limit?: number; @@ -1239,7 +1248,6 @@ class ApiMethods { `/api/v2/workspaces/${workspaceId}/builds`, data, ); - return response.data; }; @@ -2268,19 +2276,32 @@ class ApiMethods { const activeVersionId = template.active_version_id; - let templateParameters: TypesGen.TemplateVersionParameter[] = []; - if (isDynamicParametersEnabled) { - templateParameters = await this.getDynamicParameters( - activeVersionId, - workspace.owner_id, - oldBuildParameters, - ); - } else { - templateParameters = - await this.getTemplateVersionRichParameters(activeVersionId); + try { + return this.postWorkspaceBuild(workspace.id, { + transition: "start", + template_version_id: activeVersionId, + rich_parameter_values: newBuildParameters, + }); + } catch (error) { + if ( + isApiError(error) && + error.response.status === 400 && + error.response.data.validations && + error.response.data.validations.length > 0 + ) { + throw new ParameterValidationError( + activeVersionId, + error.response.data.validations, + ); + } + throw error; + } } + const templateParameters = + await this.getTemplateVersionRichParameters(activeVersionId); + const missingParameters = getMissingParameters( oldBuildParameters, newBuildParameters, diff --git a/site/src/modules/workspaces/WorkspaceMoreActions/UpdateBuildParametersDialogExperimental.tsx b/site/src/modules/workspaces/WorkspaceMoreActions/UpdateBuildParametersDialogExperimental.tsx index 04bb92a5e79b2..850f31185af2c 100644 --- a/site/src/modules/workspaces/WorkspaceMoreActions/UpdateBuildParametersDialogExperimental.tsx +++ b/site/src/modules/workspaces/WorkspaceMoreActions/UpdateBuildParametersDialogExperimental.tsx @@ -1,4 +1,4 @@ -import type { TemplateVersionParameter } from "api/typesGenerated"; +import type { FieldError } from "api/errors"; import { Button } from "components/Button/Button"; import { Dialog, @@ -14,7 +14,7 @@ import { useNavigate } from "react-router-dom"; type UpdateBuildParametersDialogExperimentalProps = { open: boolean; onClose: () => void; - missedParameters: TemplateVersionParameter[]; + validations: FieldError[]; workspaceOwnerName: string; workspaceName: string; templateVersionId: string | undefined; @@ -23,7 +23,7 @@ type UpdateBuildParametersDialogExperimentalProps = { export const UpdateBuildParametersDialogExperimental: FC< UpdateBuildParametersDialogExperimentalProps > = ({ - missedParameters, + validations, open, onClose, workspaceOwnerName, @@ -47,8 +47,8 @@ export const UpdateBuildParametersDialogExperimental: FC< This template has{" "} - {missedParameters.length} new parameter - {missedParameters.length === 1 ? "" : "s"} + {validations.length} parameter + {validations.length === 1 ? "" : "s"} {" "} that must be configured to complete the update. diff --git a/site/src/modules/workspaces/WorkspaceMoreActions/WorkspaceMoreActions.tsx b/site/src/modules/workspaces/WorkspaceMoreActions/WorkspaceMoreActions.tsx index 3853af67d394f..19d12ab2a394e 100644 --- a/site/src/modules/workspaces/WorkspaceMoreActions/WorkspaceMoreActions.tsx +++ b/site/src/modules/workspaces/WorkspaceMoreActions/WorkspaceMoreActions.tsx @@ -1,4 +1,4 @@ -import { MissingBuildParameters } from "api/api"; +import { MissingBuildParameters, ParameterValidationError } from "api/api"; import { isApiError } from "api/errors"; import { type ApiError, getErrorMessage } from "api/errors"; import { @@ -192,19 +192,19 @@ export const WorkspaceMoreActions: FC = ({ /> ) : ( { changeVersionMutation.reset(); }} workspaceOwnerName={workspace.owner_name} workspaceName={workspace.name} templateVersionId={ - changeVersionMutation.error instanceof MissingBuildParameters + changeVersionMutation.error instanceof ParameterValidationError ? changeVersionMutation.error?.versionId : undefined } diff --git a/site/src/modules/workspaces/WorkspaceUpdateDialogs.tsx b/site/src/modules/workspaces/WorkspaceUpdateDialogs.tsx index bdad9e405bd48..435802e3c10ee 100644 --- a/site/src/modules/workspaces/WorkspaceUpdateDialogs.tsx +++ b/site/src/modules/workspaces/WorkspaceUpdateDialogs.tsx @@ -1,4 +1,4 @@ -import { MissingBuildParameters } from "api/api"; +import { MissingBuildParameters, ParameterValidationError } from "api/api"; import { updateWorkspace } from "api/queries/workspaces"; import type { TemplateVersion, @@ -78,7 +78,10 @@ export const useWorkspaceUpdate = ({ updateWorkspaceMutation.reset(); }, onUpdate: (buildParameters: WorkspaceBuildParameter[]) => { - if (updateWorkspaceMutation.error instanceof MissingBuildParameters) { + if ( + updateWorkspaceMutation.error instanceof MissingBuildParameters || + updateWorkspaceMutation.error instanceof ParameterValidationError + ) { confirmUpdate(buildParameters); } }, @@ -155,7 +158,9 @@ const MissingBuildParametersDialog: FC = ({ error instanceof MissingBuildParameters ? error.parameters : []; const versionId = error instanceof MissingBuildParameters ? error.versionId : undefined; - const isOpen = error instanceof MissingBuildParameters; + const isOpen = + error instanceof MissingBuildParameters || + error instanceof ParameterValidationError; return workspace.template_use_classic_parameter_flow ? ( = ({ /> ) : ( Date: Fri, 18 Jul 2025 00:03:07 +0000 Subject: [PATCH 2/5] `await` --- site/src/api/api.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/site/src/api/api.ts b/site/src/api/api.ts index 333583e19ffef..83f54e1933e87 100644 --- a/site/src/api/api.ts +++ b/site/src/api/api.ts @@ -2278,7 +2278,7 @@ class ApiMethods { if (isDynamicParametersEnabled) { try { - return this.postWorkspaceBuild(workspace.id, { + return await this.postWorkspaceBuild(workspace.id, { transition: "start", template_version_id: activeVersionId, rich_parameter_values: newBuildParameters, From 31ed5ea25071708f0a5b3f088859eae025a0fc2d Mon Sep 17 00:00:00 2001 From: McKayla Washburn Date: Fri, 18 Jul 2025 00:08:03 +0000 Subject: [PATCH 3/5] fix version check --- site/src/modules/workspaces/WorkspaceUpdateDialogs.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/site/src/modules/workspaces/WorkspaceUpdateDialogs.tsx b/site/src/modules/workspaces/WorkspaceUpdateDialogs.tsx index 435802e3c10ee..8985165ca1394 100644 --- a/site/src/modules/workspaces/WorkspaceUpdateDialogs.tsx +++ b/site/src/modules/workspaces/WorkspaceUpdateDialogs.tsx @@ -177,7 +177,9 @@ const MissingBuildParametersDialog: FC = ({ onClose={dialogProps.onClose} workspaceOwnerName={workspace.owner_name} workspaceName={workspace.name} - templateVersionId={versionId} + templateVersionId={ + error instanceof ParameterValidationError ? error.versionId : undefined + } /> ); }; From 68043536d9b4a9328b57641c014e05a7fc216873 Mon Sep 17 00:00:00 2001 From: McKayla Washburn Date: Fri, 18 Jul 2025 00:25:42 +0000 Subject: [PATCH 4/5] lil cleanup --- site/src/api/api.ts | 11 +++++++---- .../src/modules/workspaces/WorkspaceUpdateDialogs.tsx | 6 ++---- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/site/src/api/api.ts b/site/src/api/api.ts index 83f54e1933e87..d70965b61f2e3 100644 --- a/site/src/api/api.ts +++ b/site/src/api/api.ts @@ -399,6 +399,7 @@ export class ParameterValidationError extends Error { super("Parameters are not valid for new template version"); } } + export type GetProvisionerJobsParams = { status?: string; limit?: number; @@ -2199,9 +2200,9 @@ class ApiMethods { 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 : "", - })) + ...opt, + value: opt.value?.valid ? opt.value.value : "", + })) : [], })); }; @@ -2284,6 +2285,8 @@ class ApiMethods { rich_parameter_values: newBuildParameters, }); } catch (error) { + // If the build failed because of a parameter validation error, then we + // throw a special sentinel error that can be caught by the caller. if ( isApiError(error) && error.response.status === 400 && @@ -2625,7 +2628,7 @@ class ApiMethods { // All methods must be defined with arrow function syntax. See the docstring // above the ApiMethods class for a full explanation. class ExperimentalApiMethods { - constructor(protected readonly axios: AxiosInstance) {} + constructor(protected readonly axios: AxiosInstance) { } getAITasksPrompts = async ( buildIds: TypesGen.WorkspaceBuild["id"][], diff --git a/site/src/modules/workspaces/WorkspaceUpdateDialogs.tsx b/site/src/modules/workspaces/WorkspaceUpdateDialogs.tsx index 8985165ca1394..2fad94de2da73 100644 --- a/site/src/modules/workspaces/WorkspaceUpdateDialogs.tsx +++ b/site/src/modules/workspaces/WorkspaceUpdateDialogs.tsx @@ -157,7 +157,7 @@ const MissingBuildParametersDialog: FC = ({ const missedParameters = error instanceof MissingBuildParameters ? error.parameters : []; const versionId = - error instanceof MissingBuildParameters ? error.versionId : undefined; + error instanceof ParameterValidationError ? error.versionId : undefined; const isOpen = error instanceof MissingBuildParameters || error instanceof ParameterValidationError; @@ -177,9 +177,7 @@ const MissingBuildParametersDialog: FC = ({ onClose={dialogProps.onClose} workspaceOwnerName={workspace.owner_name} workspaceName={workspace.name} - templateVersionId={ - error instanceof ParameterValidationError ? error.versionId : undefined - } + templateVersionId={versionId} /> ); }; From 975a2e6e66a449f2a0d611570dddd24fa579087a Mon Sep 17 00:00:00 2001 From: McKayla Washburn Date: Fri, 18 Jul 2025 00:28:42 +0000 Subject: [PATCH 5/5] annoyed --- site/src/api/api.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/site/src/api/api.ts b/site/src/api/api.ts index d70965b61f2e3..6b38515a74f1a 100644 --- a/site/src/api/api.ts +++ b/site/src/api/api.ts @@ -2200,9 +2200,9 @@ class ApiMethods { 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 : "", - })) + ...opt, + value: opt.value?.valid ? opt.value.value : "", + })) : [], })); }; @@ -2628,7 +2628,7 @@ class ApiMethods { // All methods must be defined with arrow function syntax. See the docstring // above the ApiMethods class for a full explanation. class ExperimentalApiMethods { - constructor(protected readonly axios: AxiosInstance) { } + constructor(protected readonly axios: AxiosInstance) {} getAITasksPrompts = async ( buildIds: TypesGen.WorkspaceBuild["id"][],