diff --git a/site/src/pages/WorkspaceSettingsPage/WorkspaceParametersPage/WorkspaceParametersPageExperimental.tsx b/site/src/pages/WorkspaceSettingsPage/WorkspaceParametersPage/WorkspaceParametersPageExperimental.tsx index 8350a35d01..0178a4e6a2 100644 --- a/site/src/pages/WorkspaceSettingsPage/WorkspaceParametersPage/WorkspaceParametersPageExperimental.tsx +++ b/site/src/pages/WorkspaceSettingsPage/WorkspaceParametersPage/WorkspaceParametersPageExperimental.tsx @@ -96,11 +96,18 @@ const WorkspaceParametersPageExperimental: FC = () => { return; } - if (!initialParamsSentRef.current && response.parameters?.length > 0) { - sendInitialParameters(); + // Skip stale responses. If we've already sent a newer request, + // this response contains outdated parameter values that would + // overwrite the user's more recent input. + if (response.id < wsResponseId.current) { + return; } setLatestResponse(response); + + if (!initialParamsSentRef.current && response.parameters?.length > 0) { + sendInitialParameters(); + } }); useEffect(() => { @@ -197,7 +204,7 @@ const WorkspaceParametersPageExperimental: FC = () => { if ( latestBuildParametersLoading || - !latestResponse || + (!latestResponse && !wsError) || (ws.current && ws.current.readyState === WebSocket.CONNECTING) ) { return ; @@ -244,7 +251,7 @@ const WorkspaceParametersPageExperimental: FC = () => { autofillParameters={autofillParameters} canChangeVersions={canChangeVersions} parameters={sortedParams} - diagnostics={latestResponse.diagnostics} + diagnostics={latestResponse?.diagnostics ?? []} isSubmitting={updateParameters.isPending} onSubmit={handleSubmit} onCancel={() => diff --git a/site/src/pages/WorkspaceSettingsPage/WorkspaceParametersPage/WorkspaceParametersPageViewExperimental.tsx b/site/src/pages/WorkspaceSettingsPage/WorkspaceParametersPage/WorkspaceParametersPageViewExperimental.tsx index bdb6bfd382..9be64b2cc3 100644 --- a/site/src/pages/WorkspaceSettingsPage/WorkspaceParametersPage/WorkspaceParametersPageViewExperimental.tsx +++ b/site/src/pages/WorkspaceSettingsPage/WorkspaceParametersPage/WorkspaceParametersPageViewExperimental.tsx @@ -9,6 +9,7 @@ import { Label } from "components/Label/Label"; import { Link } from "components/Link/Link"; import { Spinner } from "components/Spinner/Spinner"; import { useFormik } from "formik"; +import { useDebouncedFunction } from "hooks/debounce"; import { useSyncFormParameters } from "modules/hooks/useSyncFormParameters"; import { DynamicParameter, @@ -16,6 +17,7 @@ import { useValidationSchemaForDynamicParameters, } from "modules/workspaces/DynamicParameter/DynamicParameter"; import type { FC } from "react"; +import { cn } from "utils/cn"; import { docs } from "utils/docs"; import type { AutofillBuildParameter } from "utils/richParameters"; @@ -67,6 +69,23 @@ export const WorkspaceParametersPageViewExperimental: FC< workspace.template_require_active_version && !canChangeVersions; + // Debounce websocket sends to avoid stale responses overwriting + // the form while the user is still typing. + const { debounced: sendDynamicParamsRequest } = useDebouncedFunction( + (parameter: PreviewParameter, value: string) => { + const formInputs: Record = {}; + const formParameters = form.values.rich_parameter_values ?? []; + for (const param of formParameters) { + if (param?.name && param?.value) { + formInputs[param.name] = param.value; + } + } + formInputs[parameter.name] = value; + sendMessage(formInputs); + }, + 500, + ); + const handleChange = async ( parameter: PreviewParameter, parameterField: string, @@ -79,29 +98,27 @@ export const WorkspaceParametersPageViewExperimental: FC< sendDynamicParamsRequest(parameter, value); }; - const sendDynamicParamsRequest = ( - parameter: PreviewParameter, - value: string, - ) => { - const formInputs: Record = {}; - const parameters = form.values.rich_parameter_values ?? []; - for (const param of parameters) { - if (param?.name && param?.value) { - formInputs[param.name] = param.value; - } - } - - formInputs[parameter.name] = value; - - sendMessage(formInputs); - }; - useSyncFormParameters({ parameters, formValues: form.values.rich_parameter_values ?? [], setFieldValue: form.setFieldValue, }); + // True when the form holds values the backend hasn't evaluated + // yet (debounce pending or WS round-trip in flight). + const hasUnsyncedParameters = (form.values.rich_parameter_values ?? []).some( + (formParam) => { + const responseParam = parameters.find((p) => p.name === formParam.name); + if (!responseParam) { + return true; + } + const responseValue = responseParam.value.valid + ? responseParam.value.value + : ""; + return formParam.value !== responseValue; + }, + ); + const hasIncompatibleParameters = parameters.some((parameter) => { if (!parameter.mutable && parameter.diagnostics.length > 0) { return true; @@ -155,12 +172,12 @@ export const WorkspaceParametersPageViewExperimental: FC< {diagnostics.map((diagnostic, index) => ( {diagnostic.summary} @@ -248,6 +265,7 @@ export const WorkspaceParametersPageViewExperimental: FC< disabled={ isSubmitting || disabled || + hasUnsyncedParameters || diagnostics.some( (diagnostic) => diagnostic.severity === "error", ) ||
{diagnostic.summary}