Skip to content

Commit cb7ce18

Browse files
authored
feat: add experimental workspace parameters page for dynamic params (#17841)
![Screenshot 2025-05-20 at 22 26 40](https://github.com/user-attachments/assets/639441d7-2349-4c92-a4ee-d8a5a724fe8e)
1 parent 3a6d5f5 commit cb7ce18

File tree

7 files changed

+607
-61
lines changed

7 files changed

+607
-61
lines changed
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import type * as TypesGen from "api/typesGenerated";
2+
import { useEffect, useRef } from "react";
3+
4+
import type { PreviewParameter } from "api/typesGenerated";
5+
6+
type UseSyncFormParametersProps = {
7+
parameters: readonly PreviewParameter[];
8+
formValues: readonly TypesGen.WorkspaceBuildParameter[];
9+
setFieldValue: (
10+
field: string,
11+
value: TypesGen.WorkspaceBuildParameter[],
12+
) => void;
13+
};
14+
15+
export function useSyncFormParameters({
16+
parameters,
17+
formValues,
18+
setFieldValue,
19+
}: UseSyncFormParametersProps) {
20+
// Form values only needs to be updated when parameters change
21+
// Keep track of form values in a ref to avoid unnecessary updates to rich_parameter_values
22+
const formValuesRef = useRef(formValues);
23+
24+
useEffect(() => {
25+
formValuesRef.current = formValues;
26+
}, [formValues]);
27+
28+
useEffect(() => {
29+
if (!parameters) return;
30+
const currentFormValues = formValuesRef.current;
31+
32+
const newParameterValues = parameters.map((param) => ({
33+
name: param.name,
34+
value: param.value.valid ? param.value.value : "",
35+
}));
36+
37+
const currentFormValuesMap = new Map(
38+
currentFormValues.map((value) => [value.name, value.value]),
39+
);
40+
41+
const isChanged =
42+
currentFormValues.length !== newParameterValues.length ||
43+
newParameterValues.some(
44+
(p) =>
45+
!currentFormValuesMap.has(p.name) ||
46+
currentFormValuesMap.get(p.name) !== p.value,
47+
);
48+
49+
if (isChanged) {
50+
setFieldValue("rich_parameter_values", newParameterValues);
51+
}
52+
}, [parameters, setFieldValue]);
53+
}

site/src/pages/CreateWorkspacePage/CreateWorkspacePageViewExperimental.tsx

Lines changed: 1 addition & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import { Switch } from "components/Switch/Switch";
2020
import { UserAutocomplete } from "components/UserAutocomplete/UserAutocomplete";
2121
import { type FormikContextType, useFormik } from "formik";
2222
import { ArrowLeft, CircleAlert, TriangleAlert } from "lucide-react";
23+
import { useSyncFormParameters } from "modules/hooks/useSyncFormParameters";
2324
import {
2425
DynamicParameter,
2526
getInitialParameterValues,
@@ -656,52 +657,3 @@ const Diagnostics: FC<DiagnosticsProps> = ({ diagnostics }) => {
656657
</div>
657658
);
658659
};
659-
660-
type UseSyncFormParametersProps = {
661-
parameters: readonly PreviewParameter[];
662-
formValues: readonly TypesGen.WorkspaceBuildParameter[];
663-
setFieldValue: (
664-
field: string,
665-
value: TypesGen.WorkspaceBuildParameter[],
666-
) => void;
667-
};
668-
669-
function useSyncFormParameters({
670-
parameters,
671-
formValues,
672-
setFieldValue,
673-
}: UseSyncFormParametersProps) {
674-
// Form values only needs to be updated when parameters change
675-
// Keep track of form values in a ref to avoid unnecessary updates to rich_parameter_values
676-
const formValuesRef = useRef(formValues);
677-
678-
useEffect(() => {
679-
formValuesRef.current = formValues;
680-
}, [formValues]);
681-
682-
useEffect(() => {
683-
if (!parameters) return;
684-
const currentFormValues = formValuesRef.current;
685-
686-
const newParameterValues = parameters.map((param) => {
687-
return {
688-
name: param.name,
689-
value: param.value.valid ? param.value.value : "",
690-
};
691-
});
692-
693-
const isChanged =
694-
currentFormValues.length !== newParameterValues.length ||
695-
newParameterValues.some(
696-
(p) =>
697-
!currentFormValues.find(
698-
(formValue) =>
699-
formValue.name === p.name && formValue.value === p.value,
700-
),
701-
);
702-
703-
if (isChanged) {
704-
setFieldValue("rich_parameter_values", newParameterValues);
705-
}
706-
}, [parameters, setFieldValue]);
707-
}
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
import { ErrorAlert } from "components/Alert/ErrorAlert";
2+
import { Loader } from "components/Loader/Loader";
3+
import { useDashboard } from "modules/dashboard/useDashboard";
4+
import type { FC } from "react";
5+
import { useQuery } from "react-query";
6+
import { ExperimentalFormContext } from "../../CreateWorkspacePage/ExperimentalFormContext";
7+
import { useWorkspaceSettings } from "../WorkspaceSettingsLayout";
8+
import WorkspaceParametersPage from "./WorkspaceParametersPage";
9+
import WorkspaceParametersPageExperimental from "./WorkspaceParametersPageExperimental";
10+
11+
const WorkspaceParametersExperimentRouter: FC = () => {
12+
const { experiments } = useDashboard();
13+
const workspace = useWorkspaceSettings();
14+
const dynamicParametersEnabled = experiments.includes("dynamic-parameters");
15+
16+
const optOutQuery = useQuery(
17+
dynamicParametersEnabled
18+
? {
19+
queryKey: [
20+
"workspace",
21+
workspace.id,
22+
"template_id",
23+
workspace.template_id,
24+
"optOut",
25+
],
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+
},
48+
}
49+
: { enabled: false },
50+
);
51+
52+
if (dynamicParametersEnabled) {
53+
if (optOutQuery.isLoading) {
54+
return <Loader />;
55+
}
56+
if (!optOutQuery.data) {
57+
return <ErrorAlert error={optOutQuery.error} />;
58+
}
59+
60+
const toggleOptedOut = () => {
61+
const key = optOutKey(optOutQuery.data.templateId);
62+
const storedValue = localStorage.getItem(key);
63+
64+
const current = storedValue
65+
? storedValue === "true"
66+
: Boolean(workspace.template_use_classic_parameter_flow);
67+
68+
localStorage.setItem(key, (!current).toString());
69+
optOutQuery.refetch();
70+
};
71+
72+
return (
73+
<ExperimentalFormContext.Provider value={{ toggleOptedOut }}>
74+
{optOutQuery.data.optedOut ? (
75+
<WorkspaceParametersPage />
76+
) : (
77+
<WorkspaceParametersPageExperimental />
78+
)}
79+
</ExperimentalFormContext.Provider>
80+
);
81+
}
82+
83+
return <WorkspaceParametersPage />;
84+
};
85+
86+
export default WorkspaceParametersExperimentRouter;
87+
88+
const optOutKey = (id: string) => `parameters.${id}.optOut`;

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

Lines changed: 22 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,11 @@ import { isApiValidationError } from "api/errors";
44
import { checkAuthorization } from "api/queries/authCheck";
55
import type { Workspace, WorkspaceBuildParameter } from "api/typesGenerated";
66
import { ErrorAlert } from "components/Alert/ErrorAlert";
7+
import { Button as ShadcnButton } from "components/Button/Button";
78
import { EmptyState } from "components/EmptyState/EmptyState";
89
import { Loader } from "components/Loader/Loader";
9-
import { PageHeader, PageHeaderTitle } from "components/PageHeader/PageHeader";
1010
import { ExternalLinkIcon } from "lucide-react";
11-
import type { FC } from "react";
11+
import { type FC, useContext } from "react";
1212
import { Helmet } from "react-helmet-async";
1313
import { useMutation, useQuery } from "react-query";
1414
import { useNavigate } from "react-router-dom";
@@ -18,6 +18,7 @@ import {
1818
type WorkspacePermissions,
1919
workspaceChecks,
2020
} from "../../../modules/workspaces/permissions";
21+
import { ExperimentalFormContext } from "../../CreateWorkspacePage/ExperimentalFormContext";
2122
import { useWorkspaceSettings } from "../WorkspaceSettingsLayout";
2223
import {
2324
WorkspaceParametersForm,
@@ -112,15 +113,27 @@ export const WorkspaceParametersPageView: FC<
112113
isSubmitting,
113114
onCancel,
114115
}) => {
116+
const experimentalFormContext = useContext(ExperimentalFormContext);
115117
return (
116-
<>
117-
<PageHeader css={{ paddingTop: 0 }}>
118-
<PageHeaderTitle>Workspace parameters</PageHeaderTitle>
119-
</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>
120133

121-
{submitError && !isApiValidationError(submitError) && (
134+
{submitError && !isApiValidationError(submitError) ? (
122135
<ErrorAlert error={submitError} css={{ marginBottom: 48 }} />
123-
)}
136+
) : null}
124137

125138
{data ? (
126139
data.templateVersionRichParameters.length > 0 ? (
@@ -161,7 +174,7 @@ export const WorkspaceParametersPageView: FC<
161174
) : (
162175
<Loader />
163176
)}
164-
</>
177+
</div>
165178
);
166179
};
167180

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy