Skip to content

Commit 2ea438c

Browse files
refactor(site): Show immutable parameters in the settings (#7383)
1 parent 434c4be commit 2ea438c

13 files changed

+449
-236
lines changed

site/src/AppRouter.tsx

+10
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,12 @@ const WorkspaceSchedulePage = lazy(
5555
"./pages/WorkspaceSettingsPage/WorkspaceSchedulePage/WorkspaceSchedulePage"
5656
),
5757
)
58+
const WorkspaceParametersPage = lazy(
59+
() =>
60+
import(
61+
"./pages/WorkspaceSettingsPage/WorkspaceParametersPage/WorkspaceParametersPage"
62+
),
63+
)
5864
const TerminalPage = lazy(() => import("./pages/TerminalPage/TerminalPage"))
5965
const TemplatePermissionsPage = lazy(
6066
() =>
@@ -291,6 +297,10 @@ export const AppRouter: FC = () => {
291297
/>
292298
<Route path="settings" element={<WorkspaceSettingsLayout />}>
293299
<Route index element={<WorkspaceSettingsPage />} />
300+
<Route
301+
path="parameters"
302+
element={<WorkspaceParametersPage />}
303+
/>
294304
<Route
295305
path="schedule"
296306
element={<WorkspaceSchedulePage />}

site/src/pages/WorkspaceSettingsPage/Sidebar.tsx

+7
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { FC, ElementType, PropsWithChildren, ReactNode } from "react"
66
import { Link, NavLink } from "react-router-dom"
77
import { combineClasses } from "utils/combineClasses"
88
import GeneralIcon from "@material-ui/icons/SettingsOutlined"
9+
import ParameterIcon from "@material-ui/icons/CodeOutlined"
910
import { Avatar } from "components/Avatar/Avatar"
1011

1112
const SidebarNavItem: FC<
@@ -65,6 +66,12 @@ export const Sidebar: React.FC<{ username: string; workspace: Workspace }> = ({
6566
<SidebarNavItem href="" icon={<SidebarNavItemIcon icon={GeneralIcon} />}>
6667
General
6768
</SidebarNavItem>
69+
<SidebarNavItem
70+
href="parameters"
71+
icon={<SidebarNavItemIcon icon={ParameterIcon} />}
72+
>
73+
Parameters
74+
</SidebarNavItem>
6875
<SidebarNavItem
6976
href="schedule"
7077
icon={<SidebarNavItemIcon icon={ScheduleIcon} />}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
import {
2+
FormFields,
3+
FormFooter,
4+
FormSection,
5+
HorizontalForm,
6+
} from "components/Form/Form"
7+
import { RichParameterInput } from "components/RichParameterInput/RichParameterInput"
8+
import { useFormik } from "formik"
9+
import { FC } from "react"
10+
import { useTranslation } from "react-i18next"
11+
import {
12+
useValidationSchemaForRichParameters,
13+
workspaceBuildParameterValue,
14+
} from "utils/richParameters"
15+
import * as Yup from "yup"
16+
import { getFormHelpers } from "utils/formUtils"
17+
import {
18+
TemplateVersionParameter,
19+
WorkspaceBuildParameter,
20+
} from "api/typesGenerated"
21+
22+
export type WorkspaceParametersFormValues = {
23+
rich_parameter_values: WorkspaceBuildParameter[]
24+
}
25+
26+
export const WorkspaceParametersForm: FC<{
27+
isSubmitting: boolean
28+
templateVersionRichParameters: TemplateVersionParameter[]
29+
buildParameters: WorkspaceBuildParameter[]
30+
error: unknown
31+
onCancel: () => void
32+
onSubmit: (values: WorkspaceParametersFormValues) => void
33+
}> = ({
34+
onCancel,
35+
onSubmit,
36+
templateVersionRichParameters,
37+
buildParameters,
38+
error,
39+
isSubmitting,
40+
}) => {
41+
const { t } = useTranslation("workspaceSettingsPage")
42+
const mutableParameters = templateVersionRichParameters.filter(
43+
(param) => param.mutable === true,
44+
)
45+
const immutableParameters = templateVersionRichParameters.filter(
46+
(param) => param.mutable === false,
47+
)
48+
const form = useFormik<WorkspaceParametersFormValues>({
49+
onSubmit,
50+
initialValues: {
51+
rich_parameter_values: mutableParameters.map((parameter) => {
52+
const buildParameter = buildParameters.find(
53+
(p) => p.name === parameter.name,
54+
)
55+
if (!buildParameter) {
56+
return {
57+
name: parameter.name,
58+
value: parameter.default_value,
59+
}
60+
}
61+
return buildParameter
62+
}),
63+
},
64+
validationSchema: Yup.object({
65+
rich_parameter_values: useValidationSchemaForRichParameters(
66+
"createWorkspacePage",
67+
templateVersionRichParameters,
68+
),
69+
}),
70+
})
71+
const getFieldHelpers = getFormHelpers<WorkspaceParametersFormValues>(
72+
form,
73+
error,
74+
)
75+
76+
return (
77+
<HorizontalForm onSubmit={form.handleSubmit} data-testid="form">
78+
{mutableParameters.length > 0 && (
79+
<FormSection
80+
title={t("parameters")}
81+
description={t("parametersDescription")}
82+
>
83+
<FormFields>
84+
{mutableParameters.map((parameter, index) => (
85+
<RichParameterInput
86+
{...getFieldHelpers(
87+
"rich_parameter_values[" + index + "].value",
88+
)}
89+
disabled={isSubmitting}
90+
index={index}
91+
key={parameter.name}
92+
onChange={async (value) => {
93+
await form.setFieldValue("rich_parameter_values." + index, {
94+
name: parameter.name,
95+
value: value,
96+
})
97+
}}
98+
parameter={parameter}
99+
initialValue={workspaceBuildParameterValue(
100+
buildParameters,
101+
parameter,
102+
)}
103+
/>
104+
))}
105+
</FormFields>
106+
</FormSection>
107+
)}
108+
{/* They are displayed here only for visibility purposes */}
109+
{immutableParameters.length > 0 && (
110+
<FormSection
111+
title="Immutable parameters"
112+
description={
113+
<>
114+
These parameters are also provided by your Terraform configuration
115+
but they{" "}
116+
<strong>cannot be changed after creating the workspace.</strong>
117+
</>
118+
}
119+
>
120+
<FormFields>
121+
{immutableParameters.map((parameter, index) => (
122+
<RichParameterInput
123+
disabled
124+
{...getFieldHelpers(
125+
"rich_parameter_values[" + index + "].value",
126+
)}
127+
index={index}
128+
key={parameter.name}
129+
onChange={async () => {
130+
throw new Error(
131+
"Cannot change immutable parameter after creation",
132+
)
133+
}}
134+
parameter={parameter}
135+
initialValue={workspaceBuildParameterValue(
136+
buildParameters,
137+
parameter,
138+
)}
139+
/>
140+
))}
141+
</FormFields>
142+
</FormSection>
143+
)}
144+
<FormFooter onCancel={onCancel} isLoading={isSubmitting} />
145+
</HorizontalForm>
146+
)
147+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import { ComponentMeta, Story } from "@storybook/react"
2+
import {
3+
WorkspaceParametersPageView,
4+
WorkspaceParametersPageViewProps,
5+
} from "./WorkspaceParametersPage"
6+
import { action } from "@storybook/addon-actions"
7+
import {
8+
MockWorkspaceBuildParameter1,
9+
MockWorkspaceBuildParameter2,
10+
MockTemplateVersionParameter1,
11+
MockTemplateVersionParameter2,
12+
MockTemplateVersionParameter3,
13+
MockWorkspaceBuildParameter3,
14+
} from "testHelpers/entities"
15+
16+
export default {
17+
title: "pages/WorkspaceParametersPageView",
18+
component: WorkspaceParametersPageView,
19+
args: {
20+
submitError: undefined,
21+
isSubmitting: false,
22+
onCancel: action("cancel"),
23+
data: {
24+
buildParameters: [
25+
MockWorkspaceBuildParameter1,
26+
MockWorkspaceBuildParameter2,
27+
MockWorkspaceBuildParameter3,
28+
],
29+
templateVersionRichParameters: [
30+
MockTemplateVersionParameter1,
31+
MockTemplateVersionParameter2,
32+
{
33+
...MockTemplateVersionParameter3,
34+
mutable: false,
35+
},
36+
],
37+
},
38+
},
39+
} as ComponentMeta<typeof WorkspaceParametersPageView>
40+
41+
const Template: Story<WorkspaceParametersPageViewProps> = (args) => (
42+
<WorkspaceParametersPageView {...args} />
43+
)
44+
45+
export const Example = Template.bind({})
46+
Example.args = {}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
import userEvent from "@testing-library/user-event"
2+
import {
3+
renderWithWorkspaceSettingsLayout,
4+
waitForLoaderToBeRemoved,
5+
} from "testHelpers/renderHelpers"
6+
import WorkspaceParametersPage from "./WorkspaceParametersPage"
7+
import { screen, waitFor, within } from "@testing-library/react"
8+
import * as api from "api/api"
9+
import {
10+
MockWorkspace,
11+
MockTemplateVersionParameter1,
12+
MockTemplateVersionParameter2,
13+
MockWorkspaceBuildParameter1,
14+
MockWorkspaceBuildParameter2,
15+
MockWorkspaceBuild,
16+
} from "testHelpers/entities"
17+
18+
test("Submit the workspace settings page successfully", async () => {
19+
// Mock the API calls that loads data
20+
jest
21+
.spyOn(api, "getWorkspaceByOwnerAndName")
22+
.mockResolvedValueOnce(MockWorkspace)
23+
jest
24+
.spyOn(api, "getTemplateVersionRichParameters")
25+
.mockResolvedValueOnce([
26+
MockTemplateVersionParameter1,
27+
MockTemplateVersionParameter2,
28+
])
29+
jest
30+
.spyOn(api, "getWorkspaceBuildParameters")
31+
.mockResolvedValueOnce([
32+
MockWorkspaceBuildParameter1,
33+
MockWorkspaceBuildParameter2,
34+
])
35+
// Mock the API calls that submit data
36+
const postWorkspaceBuildSpy = jest
37+
.spyOn(api, "postWorkspaceBuild")
38+
.mockResolvedValue(MockWorkspaceBuild)
39+
// Setup event and rendering
40+
const user = userEvent.setup()
41+
renderWithWorkspaceSettingsLayout(<WorkspaceParametersPage />, {
42+
route: "/@test-user/test-workspace/settings",
43+
path: "/@:username/:workspace/settings",
44+
// Need this because after submit the user is redirected
45+
extraRoutes: [{ path: "/@:username/:workspace", element: <div /> }],
46+
})
47+
await waitForLoaderToBeRemoved()
48+
// Fill the form and submit
49+
const form = screen.getByTestId("form")
50+
const parameter1 = within(form).getByLabelText(
51+
MockWorkspaceBuildParameter1.name,
52+
{ exact: false },
53+
)
54+
await user.clear(parameter1)
55+
await user.type(parameter1, "new-value")
56+
const parameter2 = within(form).getByLabelText(
57+
MockWorkspaceBuildParameter2.name,
58+
{ exact: false },
59+
)
60+
await user.clear(parameter2)
61+
await user.type(parameter2, "1")
62+
await user.click(within(form).getByRole("button", { name: "Submit" }))
63+
// Assert that the API calls were made with the correct data
64+
await waitFor(() => {
65+
expect(postWorkspaceBuildSpy).toHaveBeenCalledWith(MockWorkspace.id, {
66+
transition: "start",
67+
rich_parameter_values: [
68+
{ name: MockTemplateVersionParameter1.name, value: "new-value" },
69+
{ name: MockTemplateVersionParameter2.name, value: "1" },
70+
],
71+
})
72+
})
73+
})

0 commit comments

Comments
 (0)