From 53a57543dd116ccc81e93f1c786332ef9228f6a6 Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Wed, 28 May 2025 12:22:24 -0500 Subject: [PATCH 1/3] chore: autofill previous values from build on workspace settings --- .../WorkspaceParametersPageExperimental.tsx | 7 +++++++ .../WorkspaceParametersPageViewExperimental.tsx | 7 ++++++- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/site/src/pages/WorkspaceSettingsPage/WorkspaceParametersPage/WorkspaceParametersPageExperimental.tsx b/site/src/pages/WorkspaceSettingsPage/WorkspaceParametersPage/WorkspaceParametersPageExperimental.tsx index 781f8b12e8c67..51061862f33ed 100644 --- a/site/src/pages/WorkspaceSettingsPage/WorkspaceParametersPage/WorkspaceParametersPageExperimental.tsx +++ b/site/src/pages/WorkspaceSettingsPage/WorkspaceParametersPage/WorkspaceParametersPageExperimental.tsx @@ -39,6 +39,11 @@ const WorkspaceParametersPageExperimental: FC = () => { const navigate = useNavigate(); const experimentalFormContext = useContext(ExperimentalFormContext); + const { data: originalParameters, isLoading: originalParametersLoading } = useQuery({ + queryKey: ["workspace", "build", workspace.id, "parameters"], + queryFn: () => API.getWorkspaceBuildParameters(workspace.latest_build.id), + }); + const [latestResponse, setLatestResponse] = useState(null); const wsResponseId = useRef(-1); @@ -149,6 +154,7 @@ const WorkspaceParametersPageExperimental: FC = () => { const error = wsError || updateParameters.error; if ( + originalParametersLoading || !latestResponse || (ws.current && ws.current.readyState === WebSocket.CONNECTING) ) { @@ -203,6 +209,7 @@ const WorkspaceParametersPageExperimental: FC = () => { = ({ workspace, + originalParameters, parameters, diagnostics, canChangeVersions, @@ -45,7 +47,10 @@ export const WorkspaceParametersPageViewExperimental: FC< const form = useFormik({ onSubmit, initialValues: { - rich_parameter_values: getInitialParameterValues(parameters), + rich_parameter_values: getInitialParameterValues(parameters, originalParameters!.map((p) => ({ + ...p, + source: "active_build", + }))), }, validationSchema: useValidationSchemaForDynamicParameters(parameters), enableReinitialize: false, From 296219601c16fb1e4d9622e909e8c86b9ab61283 Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Wed, 28 May 2025 12:29:36 -0500 Subject: [PATCH 2/3] add initialTouched --- ...orkspaceParametersPageViewExperimental.tsx | 27 ++++++++++++++++--- 1 file changed, 23 insertions(+), 4 deletions(-) diff --git a/site/src/pages/WorkspaceSettingsPage/WorkspaceParametersPage/WorkspaceParametersPageViewExperimental.tsx b/site/src/pages/WorkspaceSettingsPage/WorkspaceParametersPage/WorkspaceParametersPageViewExperimental.tsx index d2c8f7207fd86..a3b4480ac4c67 100644 --- a/site/src/pages/WorkspaceSettingsPage/WorkspaceParametersPage/WorkspaceParametersPageViewExperimental.tsx +++ b/site/src/pages/WorkspaceSettingsPage/WorkspaceParametersPage/WorkspaceParametersPageViewExperimental.tsx @@ -16,6 +16,7 @@ import { } from "modules/workspaces/DynamicParameter/DynamicParameter"; import type { FC } from "react"; import { docs } from "utils/docs"; +import { AutofillBuildParameter } from "utils/richParameters"; type WorkspaceParametersPageViewExperimentalProps = { workspace: Workspace; @@ -44,14 +45,32 @@ export const WorkspaceParametersPageViewExperimental: FC< sendMessage, onCancel, }) => { + + const autoFillValues: AutofillBuildParameter[] = originalParameters!.map((p) => ({ + ...p, + source: "active_build", + })) + const autofillByName = Object.fromEntries( + autoFillValues.map((param) => [param.name, param]), + ); + + + const initialTouched = parameters.reduce( + (touched, parameter) => { + if (autofillByName[parameter.name] !== undefined) { + touched[parameter.name] = true; + } + return touched; + }, + {} as Record, + ); + const form = useFormik({ onSubmit, initialValues: { - rich_parameter_values: getInitialParameterValues(parameters, originalParameters!.map((p) => ({ - ...p, - source: "active_build", - }))), + rich_parameter_values: getInitialParameterValues(parameters, autoFillValues), }, + initialTouched, validationSchema: useValidationSchemaForDynamicParameters(parameters), enableReinitialize: false, validateOnChange: true, From 9e678449ad6705e7cd8d436749cc106131450900 Mon Sep 17 00:00:00 2001 From: Jaayden Halko Date: Wed, 28 May 2025 21:58:56 +0000 Subject: [PATCH 3/3] fix: set initial autofill parameters from latest workspace build parameters --- .../DynamicParameter/DynamicParameter.tsx | 20 +++++++-- .../WorkspaceParametersPageExperimental.tsx | 43 +++++++++++++++++-- ...orkspaceParametersPageViewExperimental.tsx | 22 ++++------ 3 files changed, 63 insertions(+), 22 deletions(-) diff --git a/site/src/modules/workspaces/DynamicParameter/DynamicParameter.tsx b/site/src/modules/workspaces/DynamicParameter/DynamicParameter.tsx index 72113ce8f504b..35c5763c23d25 100644 --- a/site/src/modules/workspaces/DynamicParameter/DynamicParameter.tsx +++ b/site/src/modules/workspaces/DynamicParameter/DynamicParameter.tsx @@ -84,6 +84,7 @@ export const DynamicParameter: FC = ({ value={value} onChange={onChange} disabled={disabled} + isPreset={isPreset} /> ) : ( void; disabled?: boolean; id: string; + isPreset?: boolean; } const DebouncedParameterField: FC = ({ @@ -239,6 +241,7 @@ const DebouncedParameterField: FC = ({ onChange, disabled, id, + isPreset, }) => { const [localValue, setLocalValue] = useState( value !== undefined ? value : validValue(parameter.value), @@ -251,19 +254,26 @@ const DebouncedParameterField: FC = ({ // This is necessary in the case of fields being set by preset parameters useEffect(() => { - if (value !== undefined && value !== prevValueRef.current) { + if (isPreset && value !== undefined && value !== prevValueRef.current) { setLocalValue(value); prevValueRef.current = value; } - }, [value]); + }, [value, isPreset]); useEffect(() => { - if (prevDebouncedValueRef.current !== undefined) { + // Only call onChangeEvent if debouncedLocalValue is different from the previously committed value + // and it's not the initial undefined state. + if ( + prevDebouncedValueRef.current !== undefined && + prevDebouncedValueRef.current !== debouncedLocalValue + ) { onChangeEvent(debouncedLocalValue); } + // Update the ref to the current debounced value for the next comparison prevDebouncedValueRef.current = debouncedLocalValue; }, [debouncedLocalValue, onChangeEvent]); + const textareaRef = useRef(null); const resizeTextarea = useEffectEvent(() => { @@ -513,7 +523,9 @@ const ParameterField: FC = ({ max={parameter.validations[0]?.validation_max ?? 100} disabled={disabled} /> - {parameter.value.value} + + {Number.isFinite(Number(value)) ? value : "0"} + ); case "error": diff --git a/site/src/pages/WorkspaceSettingsPage/WorkspaceParametersPage/WorkspaceParametersPageExperimental.tsx b/site/src/pages/WorkspaceSettingsPage/WorkspaceParametersPage/WorkspaceParametersPageExperimental.tsx index 51061862f33ed..e80542f3144da 100644 --- a/site/src/pages/WorkspaceSettingsPage/WorkspaceParametersPage/WorkspaceParametersPageExperimental.tsx +++ b/site/src/pages/WorkspaceSettingsPage/WorkspaceParametersPage/WorkspaceParametersPageExperimental.tsx @@ -26,6 +26,7 @@ import { useMutation, useQuery } from "react-query"; import { useNavigate } from "react-router-dom"; import { docs } from "utils/docs"; import { pageTitle } from "utils/page"; +import type { AutofillBuildParameter } from "utils/richParameters"; import { type WorkspacePermissions, workspaceChecks, @@ -39,8 +40,12 @@ const WorkspaceParametersPageExperimental: FC = () => { const navigate = useNavigate(); const experimentalFormContext = useContext(ExperimentalFormContext); - const { data: originalParameters, isLoading: originalParametersLoading } = useQuery({ - queryKey: ["workspace", "build", workspace.id, "parameters"], + // autofill the form with the workspace build parameters from the latest build + const { + data: latestBuildParameters, + isLoading: latestBuildParametersLoading, + } = useQuery({ + queryKey: ["workspaceBuilds", workspace.latest_build.id, "parameters"], queryFn: () => API.getWorkspaceBuildParameters(workspace.latest_build.id), }); @@ -49,6 +54,13 @@ const WorkspaceParametersPageExperimental: FC = () => { const wsResponseId = useRef(-1); const ws = useRef(null); const [wsError, setWsError] = useState(null); + const initialParamsSentRef = useRef(false); + + const autofillParameters: AutofillBuildParameter[] = + latestBuildParameters?.map((p) => ({ + ...p, + source: "active_build", + })) ?? []; const sendMessage = useEffectEvent((formValues: Record) => { const request: DynamicParametersRequest = { @@ -62,11 +74,34 @@ const WorkspaceParametersPageExperimental: FC = () => { } }); + // On page load, sends initial workspace build parameters to the websocket. + // This ensures the backend has the form's complete initial state, + // vital for rendering dynamic UI elements dependent on initial parameter values. + const sendInitialParameters = useEffectEvent(() => { + if (initialParamsSentRef.current) return; + if (autofillParameters.length === 0) return; + + const initialParamsToSend: Record = {}; + for (const param of autofillParameters) { + if (param.name && param.value) { + initialParamsToSend[param.name] = param.value; + } + } + if (Object.keys(initialParamsToSend).length === 0) return; + + sendMessage(initialParamsToSend); + initialParamsSentRef.current = true; + }); + const onMessage = useEffectEvent((response: DynamicParametersResponse) => { if (latestResponse && latestResponse?.id >= response.id) { return; } + if (!initialParamsSentRef.current && response.parameters?.length > 0) { + sendInitialParameters(); + } + setLatestResponse(response); }); @@ -154,7 +189,7 @@ const WorkspaceParametersPageExperimental: FC = () => { const error = wsError || updateParameters.error; if ( - originalParametersLoading || + latestBuildParametersLoading || !latestResponse || (ws.current && ws.current.readyState === WebSocket.CONNECTING) ) { @@ -208,8 +243,8 @@ const WorkspaceParametersPageExperimental: FC = () => { {sortedParams.length > 0 ? ( = ({ workspace, - originalParameters, + autofillParameters, parameters, diagnostics, canChangeVersions, @@ -45,16 +45,9 @@ export const WorkspaceParametersPageViewExperimental: FC< sendMessage, onCancel, }) => { - - const autoFillValues: AutofillBuildParameter[] = originalParameters!.map((p) => ({ - ...p, - source: "active_build", - })) const autofillByName = Object.fromEntries( - autoFillValues.map((param) => [param.name, param]), + autofillParameters.map((param) => [param.name, param]), ); - - const initialTouched = parameters.reduce( (touched, parameter) => { if (autofillByName[parameter.name] !== undefined) { @@ -64,11 +57,13 @@ export const WorkspaceParametersPageViewExperimental: FC< }, {} as Record, ); - const form = useFormik({ onSubmit, initialValues: { - rich_parameter_values: getInitialParameterValues(parameters, autoFillValues), + rich_parameter_values: getInitialParameterValues( + parameters, + autofillParameters, + ), }, initialTouched, validationSchema: useValidationSchemaForDynamicParameters(parameters), @@ -76,7 +71,6 @@ export const WorkspaceParametersPageViewExperimental: FC< validateOnChange: true, validateOnBlur: true, }); - // Group parameters by ephemeral status const ephemeralParameters = parameters.filter((p) => p.ephemeral); const standardParameters = parameters.filter((p) => !p.ephemeral);