Skip to content

Commit 0c7f4c1

Browse files
committed
chore: get parity with create workspace page
1 parent f19178c commit 0c7f4c1

File tree

4 files changed

+141
-87
lines changed

4 files changed

+141
-87
lines changed

site/src/pages/WorkspaceSettingsPage/WorkspaceParametersPage/WorkspaceParametersExperimentRouter.tsx

Lines changed: 28 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -23,12 +23,28 @@ const WorkspaceParametersExperimentRouter: FC = () => {
2323
workspace.template_id,
2424
"optOut",
2525
],
26-
queryFn: () => ({
27-
templateId: workspace.template_id,
28-
workspaceId: workspace.id,
29-
optedOut:
30-
localStorage.getItem(optOutKey(workspace.template_id)) === "true",
31-
}),
26+
queryFn: () => {
27+
const templateId = workspace.template_id;
28+
const workspaceId = workspace.id;
29+
const localStorageKey = optOutKey(templateId);
30+
const storedOptOutString = localStorage.getItem(localStorageKey);
31+
32+
let optOutResult: boolean;
33+
34+
if (storedOptOutString !== null) {
35+
optOutResult = storedOptOutString === "true";
36+
} else {
37+
optOutResult = Boolean(
38+
workspace.template_use_classic_parameter_flow,
39+
);
40+
}
41+
42+
return {
43+
templateId,
44+
workspaceId,
45+
optedOut: optOutResult,
46+
};
47+
},
3248
}
3349
: { enabled: false },
3450
);
@@ -43,7 +59,12 @@ const WorkspaceParametersExperimentRouter: FC = () => {
4359

4460
const toggleOptedOut = () => {
4561
const key = optOutKey(optOutQuery.data.templateId);
46-
const current = localStorage.getItem(key) === "true";
62+
const storedValue = localStorage.getItem(key);
63+
64+
const current = storedValue
65+
? storedValue === "true"
66+
: Boolean(workspace.template_use_classic_parameter_flow);
67+
4768
localStorage.setItem(key, (!current).toString());
4869
optOutQuery.refetch();
4970
};

site/src/pages/WorkspaceSettingsPage/WorkspaceParametersPage/WorkspaceParametersPage.tsx

Lines changed: 18 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ import { ErrorAlert } from "components/Alert/ErrorAlert";
77
import { Button as ShadcnButton } from "components/Button/Button";
88
import { EmptyState } from "components/EmptyState/EmptyState";
99
import { Loader } from "components/Loader/Loader";
10-
import { PageHeader, PageHeaderTitle } from "components/PageHeader/PageHeader";
1110
import { ExternalLinkIcon } from "lucide-react";
1211
import { type FC, useContext } from "react";
1312
import { Helmet } from "react-helmet-async";
@@ -116,27 +115,25 @@ export const WorkspaceParametersPageView: FC<
116115
}) => {
117116
const experimentalFormContext = useContext(ExperimentalFormContext);
118117
return (
119-
<>
120-
<PageHeader
121-
css={{ paddingTop: 0 }}
122-
actions={
123-
experimentalFormContext && (
124-
<ShadcnButton
125-
size="sm"
126-
variant="outline"
127-
onClick={experimentalFormContext.toggleOptedOut}
128-
>
129-
Try out the new workspace parameters ✨
130-
</ShadcnButton>
131-
)
132-
}
133-
>
134-
<PageHeaderTitle>Workspace parameters</PageHeaderTitle>
135-
</PageHeader>
118+
<div className="flex flex-col gap-10">
119+
<header className="flex flex-col items-start gap-2">
120+
<span className="flex flex-row justify-between items-center gap-2">
121+
<h1 className="text-3xl m-0">Workspace parameters</h1>
122+
</span>
123+
{experimentalFormContext && (
124+
<ShadcnButton
125+
size="sm"
126+
variant="outline"
127+
onClick={experimentalFormContext.toggleOptedOut}
128+
>
129+
Try out the new workspace parameters ✨
130+
</ShadcnButton>
131+
)}
132+
</header>
136133

137-
{submitError && !isApiValidationError(submitError) && (
134+
{submitError && !isApiValidationError(submitError) ? (
138135
<ErrorAlert error={submitError} css={{ marginBottom: 48 }} />
139-
)}
136+
) : null}
140137

141138
{data ? (
142139
data.templateVersionRichParameters.length > 0 ? (
@@ -177,7 +174,7 @@ export const WorkspaceParametersPageView: FC<
177174
) : (
178175
<Loader />
179176
)}
180-
</>
177+
</div>
181178
);
182179
};
183180

site/src/pages/WorkspaceSettingsPage/WorkspaceParametersPage/WorkspaceParametersPageExperimental.tsx

Lines changed: 30 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import { EmptyState } from "components/EmptyState/EmptyState";
1212
import { FeatureStageBadge } from "components/FeatureStageBadge/FeatureStageBadge";
1313
import { Link } from "components/Link/Link";
1414
import { Loader } from "components/Loader/Loader";
15+
import { useEffectEvent } from "hooks/hookPolyfills";
1516
import type { FC } from "react";
1617
import {
1718
useCallback,
@@ -39,21 +40,31 @@ const WorkspaceParametersPageExperimental: FC = () => {
3940
const navigate = useNavigate();
4041
const experimentalFormContext = useContext(ExperimentalFormContext);
4142

42-
const [currentResponse, setCurrentResponse] =
43+
const [latestResponse, setLatestResponse] =
4344
useState<DynamicParametersResponse | null>(null);
44-
const [wsResponseId, setWSResponseId] = useState<number>(-1);
45+
const wsResponseId = useRef<number>(-1);
4546
const ws = useRef<WebSocket | null>(null);
4647
const [wsError, setWsError] = useState<Error | null>(null);
4748

48-
const onMessage = useCallback((response: DynamicParametersResponse) => {
49-
setCurrentResponse((prev) => {
50-
if (prev?.id === response.id) {
51-
return prev;
52-
}
53-
return response;
54-
});
49+
const sendMessage = useCallback((formValues: Record<string, string>) => {
50+
const request: DynamicParametersRequest = {
51+
id: wsResponseId.current + 1,
52+
inputs: formValues,
53+
};
54+
if (ws.current && ws.current.readyState === WebSocket.OPEN) {
55+
ws.current.send(JSON.stringify(request));
56+
wsResponseId.current = wsResponseId.current + 1;
57+
}
5558
}, []);
5659

60+
const onMessage = useEffectEvent((response: DynamicParametersResponse) => {
61+
if (latestResponse && latestResponse?.id >= response.id) {
62+
return;
63+
}
64+
65+
setLatestResponse(response);
66+
});
67+
5768
useEffect(() => {
5869
if (!workspace.latest_build.template_version_id) return;
5970

@@ -63,7 +74,9 @@ const WorkspaceParametersPageExperimental: FC = () => {
6374
{
6475
onMessage,
6576
onError: (error) => {
66-
setWsError(error);
77+
if (ws.current === socket) {
78+
setWsError(error);
79+
}
6780
},
6881
onClose: () => {
6982
if (ws.current === socket) {
@@ -89,20 +102,6 @@ const WorkspaceParametersPageExperimental: FC = () => {
89102
onMessage,
90103
]);
91104

92-
const sendMessage = useCallback((formValues: Record<string, string>) => {
93-
setWSResponseId((prevId) => {
94-
const request: DynamicParametersRequest = {
95-
id: prevId + 1,
96-
inputs: formValues,
97-
};
98-
if (ws.current && ws.current.readyState === WebSocket.OPEN) {
99-
ws.current.send(JSON.stringify(request));
100-
return prevId + 1;
101-
}
102-
return prevId;
103-
});
104-
}, []);
105-
106105
const updateParameters = useMutation({
107106
mutationFn: (buildParameters: WorkspaceBuildParameter[]) =>
108107
API.postWorkspaceBuild(workspace.id, {
@@ -125,12 +124,12 @@ const WorkspaceParametersPageExperimental: FC = () => {
125124
const handleSubmit = (values: {
126125
rich_parameter_values: WorkspaceBuildParameter[];
127126
}) => {
128-
if (!currentResponse || !currentResponse.parameters) {
127+
if (!latestResponse || !latestResponse.parameters) {
129128
return;
130129
}
131130

132131
// Only submit mutable parameters
133-
const onlyMutableValues = currentResponse.parameters
132+
const onlyMutableValues = latestResponse.parameters
134133
.filter((p) => p.mutable)
135134
.map((p) => {
136135
const value = values.rich_parameter_values.find(
@@ -146,16 +145,16 @@ const WorkspaceParametersPageExperimental: FC = () => {
146145
};
147146

148147
const sortedParams = useMemo(() => {
149-
if (!currentResponse?.parameters) {
148+
if (!latestResponse?.parameters) {
150149
return [];
151150
}
152-
return [...currentResponse.parameters].sort((a, b) => a.order - b.order);
153-
}, [currentResponse?.parameters]);
151+
return [...latestResponse.parameters].sort((a, b) => a.order - b.order);
152+
}, [latestResponse?.parameters]);
154153

155154
const error = wsError || updateParameters.error;
156155

157156
if (
158-
!currentResponse ||
157+
!latestResponse ||
159158
(ws.current && ws.current.readyState === WebSocket.CONNECTING)
160159
) {
161160
return <Loader />;
@@ -190,7 +189,7 @@ const WorkspaceParametersPageExperimental: FC = () => {
190189
workspace={workspace}
191190
canChangeVersions={canChangeVersions}
192191
parameters={sortedParams}
193-
diagnostics={currentResponse.diagnostics}
192+
diagnostics={latestResponse.diagnostics}
194193
isSubmitting={updateParameters.isLoading}
195194
onSubmit={handleSubmit}
196195
onCancel={() =>

site/src/pages/WorkspaceSettingsPage/WorkspaceParametersPage/WorkspaceParametersPageViewExperimental.tsx

Lines changed: 65 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import type * as TypesGen from "api/typesGenerated";
12
import type {
23
PreviewParameter,
34
Workspace,
@@ -7,14 +8,13 @@ import { Alert } from "components/Alert/Alert";
78
import { Button } from "components/Button/Button";
89
import { Spinner } from "components/Spinner/Spinner";
910
import { useFormik } from "formik";
10-
import { useDebouncedFunction } from "hooks/debounce";
1111
import {
1212
DynamicParameter,
1313
getInitialParameterValues,
1414
useValidationSchemaForDynamicParameters,
1515
} from "modules/workspaces/DynamicParameter/DynamicParameter";
1616
import type { FC } from "react";
17-
17+
import { useEffect, useRef } from "react";
1818
export type WorkspaceParametersPageViewExperimentalProps = {
1919
workspace: Workspace;
2020
parameters: PreviewParameter[];
@@ -60,37 +60,17 @@ export const WorkspaceParametersPageViewExperimental: FC<
6060
workspace.template_require_active_version &&
6161
!canChangeVersions;
6262

63-
const { debounced: handleChangeDebounced } = useDebouncedFunction(
64-
async (
65-
parameter: PreviewParameter,
66-
parameterField: string,
67-
value: string,
68-
) => {
69-
await form.setFieldValue(parameterField, {
70-
name: parameter.name,
71-
value,
72-
});
73-
form.setFieldTouched(parameter.name, true);
74-
sendDynamicParamsRequest(parameter, value);
75-
},
76-
500,
77-
);
78-
7963
const handleChange = async (
8064
parameter: PreviewParameter,
8165
parameterField: string,
8266
value: string,
8367
) => {
84-
if (parameter.form_type === "input" || parameter.form_type === "textarea") {
85-
handleChangeDebounced(parameter, parameterField, value);
86-
} else {
87-
await form.setFieldValue(parameterField, {
88-
name: parameter.name,
89-
value,
90-
});
91-
form.setFieldTouched(parameter.name, true);
92-
sendDynamicParamsRequest(parameter, value);
93-
}
68+
await form.setFieldValue(parameterField, {
69+
name: parameter.name,
70+
value,
71+
});
72+
form.setFieldTouched(parameter.name, true);
73+
sendDynamicParamsRequest(parameter, value);
9474
};
9575

9676
// Send the changed parameter and all touched parameters to the websocket
@@ -114,6 +94,12 @@ export const WorkspaceParametersPageViewExperimental: FC<
11494
sendMessage(formInputs);
11595
};
11696

97+
useSyncFormParameters({
98+
parameters,
99+
formValues: form.values.rich_parameter_values ?? [],
100+
setFieldValue: form.setFieldValue,
101+
});
102+
117103
return (
118104
<>
119105
{disabled && (
@@ -171,6 +157,7 @@ export const WorkspaceParametersPageViewExperimental: FC<
171157
onChange={(value) =>
172158
handleChange(parameter, parameterField, value)
173159
}
160+
autofill={false}
174161
disabled={isDisabled}
175162
/>
176163
);
@@ -201,6 +188,7 @@ export const WorkspaceParametersPageViewExperimental: FC<
201188
onChange={(value) =>
202189
handleChange(parameter, parameterField, value)
203190
}
191+
autofill={false}
204192
disabled={isDisabled}
205193
/>
206194
);
@@ -225,3 +213,52 @@ export const WorkspaceParametersPageViewExperimental: FC<
225213
</>
226214
);
227215
};
216+
217+
type UseSyncFormParametersProps = {
218+
parameters: readonly PreviewParameter[];
219+
formValues: readonly TypesGen.WorkspaceBuildParameter[];
220+
setFieldValue: (
221+
field: string,
222+
value: TypesGen.WorkspaceBuildParameter[],
223+
) => void;
224+
};
225+
226+
function useSyncFormParameters({
227+
parameters,
228+
formValues,
229+
setFieldValue,
230+
}: UseSyncFormParametersProps) {
231+
// Form values only needs to be updated when parameters change
232+
// Keep track of form values in a ref to avoid unnecessary updates to rich_parameter_values
233+
const formValuesRef = useRef(formValues);
234+
235+
useEffect(() => {
236+
formValuesRef.current = formValues;
237+
}, [formValues]);
238+
239+
useEffect(() => {
240+
if (!parameters) return;
241+
const currentFormValues = formValuesRef.current;
242+
243+
const newParameterValues = parameters.map((param) => {
244+
return {
245+
name: param.name,
246+
value: param.value.valid ? param.value.value : "",
247+
};
248+
});
249+
250+
const isChanged =
251+
currentFormValues.length !== newParameterValues.length ||
252+
newParameterValues.some(
253+
(p) =>
254+
!currentFormValues.find(
255+
(formValue) =>
256+
formValue.name === p.name && formValue.value === p.value,
257+
),
258+
);
259+
260+
if (isChanged) {
261+
setFieldValue("rich_parameter_values", newParameterValues);
262+
}
263+
}, [parameters, setFieldValue]);
264+
}

0 commit comments

Comments
 (0)