fix: autofill with workspace build parameters from the latest build (#18091)

Set the form parameters using autofill parameters based on the workspace
build parameters for the latest build

---------

Co-authored-by: Steven Masley <stevenmasley@gmail.com>
This commit is contained in:
Jaayden Halko
2025-05-29 10:24:55 -05:00
committed by GitHub
parent e4648b6fc1
commit 177bda3187
3 changed files with 78 additions and 6 deletions

View File

@ -84,6 +84,7 @@ export const DynamicParameter: FC<DynamicParameterProps> = ({
value={value}
onChange={onChange}
disabled={disabled}
isPreset={isPreset}
/>
) : (
<ParameterField
@ -231,6 +232,7 @@ interface DebouncedParameterFieldProps {
onChange: (value: string) => void;
disabled?: boolean;
id: string;
isPreset?: boolean;
}
const DebouncedParameterField: FC<DebouncedParameterFieldProps> = ({
@ -239,6 +241,7 @@ const DebouncedParameterField: FC<DebouncedParameterFieldProps> = ({
onChange,
disabled,
id,
isPreset,
}) => {
const [localValue, setLocalValue] = useState(
value !== undefined ? value : validValue(parameter.value),
@ -251,19 +254,26 @@ const DebouncedParameterField: FC<DebouncedParameterFieldProps> = ({
// 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<HTMLTextAreaElement>(null);
const resizeTextarea = useEffectEvent(() => {
@ -513,7 +523,9 @@ const ParameterField: FC<ParameterFieldProps> = ({
max={parameter.validations[0]?.validation_max ?? 100}
disabled={disabled}
/>
<span className="w-4 font-medium">{parameter.value.value}</span>
<span className="w-4 font-medium">
{Number.isFinite(Number(value)) ? value : "0"}
</span>
</div>
);
case "error":

View File

@ -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,11 +40,27 @@ const WorkspaceParametersPageExperimental: FC = () => {
const navigate = useNavigate();
const experimentalFormContext = useContext(ExperimentalFormContext);
// 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),
});
const [latestResponse, setLatestResponse] =
useState<DynamicParametersResponse | null>(null);
const wsResponseId = useRef<number>(-1);
const ws = useRef<WebSocket | null>(null);
const [wsError, setWsError] = useState<Error | null>(null);
const initialParamsSentRef = useRef(false);
const autofillParameters: AutofillBuildParameter[] =
latestBuildParameters?.map((p) => ({
...p,
source: "active_build",
})) ?? [];
const sendMessage = useEffectEvent((formValues: Record<string, string>) => {
const request: DynamicParametersRequest = {
@ -57,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<string, string> = {};
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);
});
@ -149,6 +189,7 @@ const WorkspaceParametersPageExperimental: FC = () => {
const error = wsError || updateParameters.error;
if (
latestBuildParametersLoading ||
!latestResponse ||
(ws.current && ws.current.readyState === WebSocket.CONNECTING)
) {
@ -202,6 +243,7 @@ const WorkspaceParametersPageExperimental: FC = () => {
{sortedParams.length > 0 ? (
<WorkspaceParametersPageViewExperimental
workspace={workspace}
autofillParameters={autofillParameters}
canChangeVersions={canChangeVersions}
parameters={sortedParams}
diagnostics={latestResponse.diagnostics}

View File

@ -16,9 +16,11 @@ import {
} from "modules/workspaces/DynamicParameter/DynamicParameter";
import type { FC } from "react";
import { docs } from "utils/docs";
import type { AutofillBuildParameter } from "utils/richParameters";
type WorkspaceParametersPageViewExperimentalProps = {
workspace: Workspace;
autofillParameters: AutofillBuildParameter[];
parameters: PreviewParameter[];
diagnostics: PreviewParameter["diagnostics"];
canChangeVersions: boolean;
@ -34,6 +36,7 @@ export const WorkspaceParametersPageViewExperimental: FC<
WorkspaceParametersPageViewExperimentalProps
> = ({
workspace,
autofillParameters,
parameters,
diagnostics,
canChangeVersions,
@ -42,17 +45,32 @@ export const WorkspaceParametersPageViewExperimental: FC<
sendMessage,
onCancel,
}) => {
const autofillByName = Object.fromEntries(
autofillParameters.map((param) => [param.name, param]),
);
const initialTouched = parameters.reduce(
(touched, parameter) => {
if (autofillByName[parameter.name] !== undefined) {
touched[parameter.name] = true;
}
return touched;
},
{} as Record<string, boolean>,
);
const form = useFormik({
onSubmit,
initialValues: {
rich_parameter_values: getInitialParameterValues(parameters),
rich_parameter_values: getInitialParameterValues(
parameters,
autofillParameters,
),
},
initialTouched,
validationSchema: useValidationSchemaForDynamicParameters(parameters),
enableReinitialize: false,
validateOnChange: true,
validateOnBlur: true,
});
// Group parameters by ephemeral status
const ephemeralParameters = parameters.filter((p) => p.ephemeral);
const standardParameters = parameters.filter((p) => !p.ephemeral);