mirror of
https://github.com/coder/coder.git
synced 2025-07-13 21:36:50 +00:00
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:
@ -84,6 +84,7 @@ export const DynamicParameter: FC<DynamicParameterProps> = ({
|
|||||||
value={value}
|
value={value}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
|
isPreset={isPreset}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<ParameterField
|
<ParameterField
|
||||||
@ -231,6 +232,7 @@ interface DebouncedParameterFieldProps {
|
|||||||
onChange: (value: string) => void;
|
onChange: (value: string) => void;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
id: string;
|
id: string;
|
||||||
|
isPreset?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const DebouncedParameterField: FC<DebouncedParameterFieldProps> = ({
|
const DebouncedParameterField: FC<DebouncedParameterFieldProps> = ({
|
||||||
@ -239,6 +241,7 @@ const DebouncedParameterField: FC<DebouncedParameterFieldProps> = ({
|
|||||||
onChange,
|
onChange,
|
||||||
disabled,
|
disabled,
|
||||||
id,
|
id,
|
||||||
|
isPreset,
|
||||||
}) => {
|
}) => {
|
||||||
const [localValue, setLocalValue] = useState(
|
const [localValue, setLocalValue] = useState(
|
||||||
value !== undefined ? value : validValue(parameter.value),
|
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
|
// This is necessary in the case of fields being set by preset parameters
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (value !== undefined && value !== prevValueRef.current) {
|
if (isPreset && value !== undefined && value !== prevValueRef.current) {
|
||||||
setLocalValue(value);
|
setLocalValue(value);
|
||||||
prevValueRef.current = value;
|
prevValueRef.current = value;
|
||||||
}
|
}
|
||||||
}, [value]);
|
}, [value, isPreset]);
|
||||||
|
|
||||||
useEffect(() => {
|
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);
|
onChangeEvent(debouncedLocalValue);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Update the ref to the current debounced value for the next comparison
|
||||||
prevDebouncedValueRef.current = debouncedLocalValue;
|
prevDebouncedValueRef.current = debouncedLocalValue;
|
||||||
}, [debouncedLocalValue, onChangeEvent]);
|
}, [debouncedLocalValue, onChangeEvent]);
|
||||||
|
|
||||||
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
||||||
|
|
||||||
const resizeTextarea = useEffectEvent(() => {
|
const resizeTextarea = useEffectEvent(() => {
|
||||||
@ -513,7 +523,9 @@ const ParameterField: FC<ParameterFieldProps> = ({
|
|||||||
max={parameter.validations[0]?.validation_max ?? 100}
|
max={parameter.validations[0]?.validation_max ?? 100}
|
||||||
disabled={disabled}
|
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>
|
</div>
|
||||||
);
|
);
|
||||||
case "error":
|
case "error":
|
||||||
|
@ -26,6 +26,7 @@ import { useMutation, useQuery } from "react-query";
|
|||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
import { docs } from "utils/docs";
|
import { docs } from "utils/docs";
|
||||||
import { pageTitle } from "utils/page";
|
import { pageTitle } from "utils/page";
|
||||||
|
import type { AutofillBuildParameter } from "utils/richParameters";
|
||||||
import {
|
import {
|
||||||
type WorkspacePermissions,
|
type WorkspacePermissions,
|
||||||
workspaceChecks,
|
workspaceChecks,
|
||||||
@ -39,11 +40,27 @@ const WorkspaceParametersPageExperimental: FC = () => {
|
|||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const experimentalFormContext = useContext(ExperimentalFormContext);
|
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] =
|
const [latestResponse, setLatestResponse] =
|
||||||
useState<DynamicParametersResponse | null>(null);
|
useState<DynamicParametersResponse | null>(null);
|
||||||
const wsResponseId = useRef<number>(-1);
|
const wsResponseId = useRef<number>(-1);
|
||||||
const ws = useRef<WebSocket | null>(null);
|
const ws = useRef<WebSocket | null>(null);
|
||||||
const [wsError, setWsError] = useState<Error | 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 sendMessage = useEffectEvent((formValues: Record<string, string>) => {
|
||||||
const request: DynamicParametersRequest = {
|
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) => {
|
const onMessage = useEffectEvent((response: DynamicParametersResponse) => {
|
||||||
if (latestResponse && latestResponse?.id >= response.id) {
|
if (latestResponse && latestResponse?.id >= response.id) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!initialParamsSentRef.current && response.parameters?.length > 0) {
|
||||||
|
sendInitialParameters();
|
||||||
|
}
|
||||||
|
|
||||||
setLatestResponse(response);
|
setLatestResponse(response);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -149,6 +189,7 @@ const WorkspaceParametersPageExperimental: FC = () => {
|
|||||||
const error = wsError || updateParameters.error;
|
const error = wsError || updateParameters.error;
|
||||||
|
|
||||||
if (
|
if (
|
||||||
|
latestBuildParametersLoading ||
|
||||||
!latestResponse ||
|
!latestResponse ||
|
||||||
(ws.current && ws.current.readyState === WebSocket.CONNECTING)
|
(ws.current && ws.current.readyState === WebSocket.CONNECTING)
|
||||||
) {
|
) {
|
||||||
@ -202,6 +243,7 @@ const WorkspaceParametersPageExperimental: FC = () => {
|
|||||||
{sortedParams.length > 0 ? (
|
{sortedParams.length > 0 ? (
|
||||||
<WorkspaceParametersPageViewExperimental
|
<WorkspaceParametersPageViewExperimental
|
||||||
workspace={workspace}
|
workspace={workspace}
|
||||||
|
autofillParameters={autofillParameters}
|
||||||
canChangeVersions={canChangeVersions}
|
canChangeVersions={canChangeVersions}
|
||||||
parameters={sortedParams}
|
parameters={sortedParams}
|
||||||
diagnostics={latestResponse.diagnostics}
|
diagnostics={latestResponse.diagnostics}
|
||||||
|
@ -16,9 +16,11 @@ import {
|
|||||||
} from "modules/workspaces/DynamicParameter/DynamicParameter";
|
} from "modules/workspaces/DynamicParameter/DynamicParameter";
|
||||||
import type { FC } from "react";
|
import type { FC } from "react";
|
||||||
import { docs } from "utils/docs";
|
import { docs } from "utils/docs";
|
||||||
|
import type { AutofillBuildParameter } from "utils/richParameters";
|
||||||
|
|
||||||
type WorkspaceParametersPageViewExperimentalProps = {
|
type WorkspaceParametersPageViewExperimentalProps = {
|
||||||
workspace: Workspace;
|
workspace: Workspace;
|
||||||
|
autofillParameters: AutofillBuildParameter[];
|
||||||
parameters: PreviewParameter[];
|
parameters: PreviewParameter[];
|
||||||
diagnostics: PreviewParameter["diagnostics"];
|
diagnostics: PreviewParameter["diagnostics"];
|
||||||
canChangeVersions: boolean;
|
canChangeVersions: boolean;
|
||||||
@ -34,6 +36,7 @@ export const WorkspaceParametersPageViewExperimental: FC<
|
|||||||
WorkspaceParametersPageViewExperimentalProps
|
WorkspaceParametersPageViewExperimentalProps
|
||||||
> = ({
|
> = ({
|
||||||
workspace,
|
workspace,
|
||||||
|
autofillParameters,
|
||||||
parameters,
|
parameters,
|
||||||
diagnostics,
|
diagnostics,
|
||||||
canChangeVersions,
|
canChangeVersions,
|
||||||
@ -42,17 +45,32 @@ export const WorkspaceParametersPageViewExperimental: FC<
|
|||||||
sendMessage,
|
sendMessage,
|
||||||
onCancel,
|
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({
|
const form = useFormik({
|
||||||
onSubmit,
|
onSubmit,
|
||||||
initialValues: {
|
initialValues: {
|
||||||
rich_parameter_values: getInitialParameterValues(parameters),
|
rich_parameter_values: getInitialParameterValues(
|
||||||
|
parameters,
|
||||||
|
autofillParameters,
|
||||||
|
),
|
||||||
},
|
},
|
||||||
|
initialTouched,
|
||||||
validationSchema: useValidationSchemaForDynamicParameters(parameters),
|
validationSchema: useValidationSchemaForDynamicParameters(parameters),
|
||||||
enableReinitialize: false,
|
enableReinitialize: false,
|
||||||
validateOnChange: true,
|
validateOnChange: true,
|
||||||
validateOnBlur: true,
|
validateOnBlur: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Group parameters by ephemeral status
|
// Group parameters by ephemeral status
|
||||||
const ephemeralParameters = parameters.filter((p) => p.ephemeral);
|
const ephemeralParameters = parameters.filter((p) => p.ephemeral);
|
||||||
const standardParameters = parameters.filter((p) => !p.ephemeral);
|
const standardParameters = parameters.filter((p) => !p.ephemeral);
|
||||||
|
Reference in New Issue
Block a user