Skip to content

Commit 6f62204

Browse files
feat(site): Add template embed page (coder#7501)
1 parent 049e557 commit 6f62204

File tree

11 files changed

+408
-85
lines changed

11 files changed

+408
-85
lines changed

site/.storybook/preview.jsx

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import CssBaseline from "@mui/material/CssBaseline"
22
import { StyledEngineProvider, ThemeProvider } from "@mui/material/styles"
33
import { createMemoryHistory } from "history"
44
import { unstable_HistoryRouter as HistoryRouter } from "react-router-dom"
5+
import { HelmetProvider } from "react-helmet-async"
56
import { dark } from "../src/theme"
67
import "../src/theme/globalFonts"
78
import "../src/i18n"
@@ -24,6 +25,13 @@ export const decorators = [
2425
</HistoryRouter>
2526
)
2627
},
28+
(Story) => {
29+
return (
30+
<HelmetProvider>
31+
<Story />
32+
</HelmetProvider>
33+
)
34+
},
2735
]
2836

2937
export const parameters = {

site/src/AppRouter.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,9 @@ const AddNewLicensePage = lazy(
176176
() =>
177177
import("./pages/DeploySettingsPage/LicensesSettingsPage/AddNewLicensePage"),
178178
)
179+
const TemplateEmbedPage = lazy(
180+
() => import("./pages/TemplatePage/TemplateEmbedPage/TemplateEmbedPage"),
181+
)
179182

180183
export const AppRouter: FC = () => {
181184
return (
@@ -208,6 +211,7 @@ export const AppRouter: FC = () => {
208211
<Route path="docs" element={<TemplateDocsPage />} />
209212
<Route path="files" element={<TemplateFilesPage />} />
210213
<Route path="versions" element={<TemplateVersionsPage />} />
214+
<Route path="embed" element={<TemplateEmbedPage />} />
211215
</Route>
212216

213217
<Route path="workspace" element={<CreateWorkspacePage />} />

site/src/components/Dashboard/DashboardLayout.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ const useStyles = makeStyles((theme) => ({
7272
// It also give a more pleasant distance to the site content when
7373
// the banner is visible.
7474
marginTop: theme.spacing(2),
75-
marginBottom: -theme.spacing(2),
75+
marginBottom: theme.spacing(-2),
7676
},
7777
siteContent: {
7878
flex: 1,

site/src/components/RichParameterInput/RichParameterInput.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ const ParameterLabel: FC<ParameterLabelProps> = ({ id, parameter }) => {
5454
)
5555
}
5656

57-
export type RichParameterInputProps = TextFieldProps & {
57+
export type RichParameterInputProps = Omit<TextFieldProps, "onChange"> & {
5858
index: number
5959
parameter: TemplateVersionParameter
6060
onChange: (value: string) => void

site/src/components/TabSidebar/TabSidebar.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ const useStyles = makeStyles((theme) => ({
7373
},
7474

7575
menuItem: {
76-
letterSpacing: -theme.spacing(0.0375),
76+
letterSpacing: theme.spacing(-0.0375),
7777
padding: 0,
7878
fontSize: 18,
7979
color: theme.palette.text.secondary,

site/src/components/TemplateLayout/TemplateLayout.tsx

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,17 @@ export const TemplateLayout: FC<{ children?: JSX.Element }> = ({
146146
>
147147
Versions
148148
</NavLink>
149+
<NavLink
150+
to={`/templates/${templateName}/embed`}
151+
className={({ isActive }) =>
152+
combineClasses([
153+
styles.tabItem,
154+
isActive ? styles.tabItemActive : undefined,
155+
])
156+
}
157+
>
158+
Embed
159+
</NavLink>
149160
</Stack>
150161
</Margins>
151162
</div>
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
import { TemplateVersionParameter } from "api/typesGenerated"
2+
import { FormSection, FormFields } from "components/Form/Form"
3+
import {
4+
RichParameterInput,
5+
RichParameterInputProps,
6+
} from "components/RichParameterInput/RichParameterInput"
7+
import { ComponentProps, FC } from "react"
8+
9+
export type TemplateParametersSectionProps = {
10+
templateParameters: TemplateVersionParameter[]
11+
getInputProps: (
12+
parameter: TemplateVersionParameter,
13+
index: number,
14+
) => Omit<RichParameterInputProps, "parameter" | "index">
15+
} & Pick<ComponentProps<typeof FormSection>, "classes">
16+
17+
export const MutableTemplateParametersSection: FC<
18+
TemplateParametersSectionProps
19+
> = ({ templateParameters, getInputProps, ...formSectionProps }) => {
20+
const hasMutableParameters =
21+
templateParameters.filter((p) => p.mutable).length > 0
22+
23+
return (
24+
<>
25+
{hasMutableParameters && (
26+
<FormSection
27+
{...formSectionProps}
28+
title="Parameters"
29+
description="These parameters are provided by your template's Terraform configuration and can be changed after creating the workspace."
30+
>
31+
<FormFields>
32+
{templateParameters.map(
33+
(parameter, index) =>
34+
parameter.mutable && (
35+
<RichParameterInput
36+
{...getInputProps(parameter, index)}
37+
index={index}
38+
key={parameter.name}
39+
parameter={parameter}
40+
/>
41+
),
42+
)}
43+
</FormFields>
44+
</FormSection>
45+
)}
46+
</>
47+
)
48+
}
49+
50+
export const ImmutableTemplateParametersSection: FC<
51+
TemplateParametersSectionProps
52+
> = ({ templateParameters, getInputProps, ...formSectionProps }) => {
53+
const hasImmutableParameters =
54+
templateParameters.filter((p) => !p.mutable).length > 0
55+
56+
return (
57+
<>
58+
{hasImmutableParameters && (
59+
<FormSection
60+
{...formSectionProps}
61+
title="Immutable parameters"
62+
description={
63+
<>
64+
These parameters are also provided by your Terraform configuration
65+
but they{" "}
66+
<strong>cannot be changed after creating the workspace.</strong>
67+
</>
68+
}
69+
>
70+
<FormFields>
71+
{templateParameters.map(
72+
(parameter, index) =>
73+
!parameter.mutable && (
74+
<RichParameterInput
75+
{...getInputProps(parameter, index)}
76+
index={index}
77+
key={parameter.name}
78+
parameter={parameter}
79+
/>
80+
),
81+
)}
82+
</FormFields>
83+
</FormSection>
84+
)}
85+
</>
86+
)
87+
}

site/src/pages/CreateWorkspacePage/CreateWorkspacePageView.tsx

Lines changed: 52 additions & 82 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import TextField from "@mui/material/TextField"
22
import * as TypesGen from "api/typesGenerated"
33
import { ParameterInput } from "components/ParameterInput/ParameterInput"
4-
import { RichParameterInput } from "components/RichParameterInput/RichParameterInput"
54
import { Stack } from "components/Stack/Stack"
65
import { UserAutocomplete } from "components/UserAutocomplete/UserAutocomplete"
76
import { FormikContextType, FormikTouched, useFormik } from "formik"
@@ -26,6 +25,10 @@ import {
2625
useValidationSchemaForRichParameters,
2726
workspaceBuildParameterValue,
2827
} from "utils/richParameters"
28+
import {
29+
ImmutableTemplateParametersSection,
30+
MutableTemplateParametersSection,
31+
} from "components/TemplateParameters/TemplateParameters"
2932

3033
export enum CreateWorkspaceErrors {
3134
GET_TEMPLATES_ERROR = "getTemplatesError",
@@ -308,86 +311,53 @@ export const CreateWorkspacePageView: FC<
308311
</FormSection>
309312
)}
310313

311-
{/* Mutable rich parameters */}
312-
{props.templateParameters &&
313-
props.templateParameters.filter((p) => p.mutable).length > 0 && (
314-
<FormSection
315-
title="Parameters"
316-
description="These parameters are provided by your template's Terraform configuration and can be changed after creating the workspace."
317-
>
318-
<FormFields>
319-
{props.templateParameters.map(
320-
(parameter, index) =>
321-
parameter.mutable && (
322-
<RichParameterInput
323-
{...getFieldHelpers(
324-
"rich_parameter_values[" + index + "].value",
325-
)}
326-
disabled={form.isSubmitting}
327-
index={index}
328-
key={parameter.name}
329-
onChange={(value) => {
330-
form.setFieldValue("rich_parameter_values." + index, {
331-
name: parameter.name,
332-
value: value,
333-
})
334-
}}
335-
parameter={parameter}
336-
initialValue={workspaceBuildParameterValue(
337-
initialRichParameterValues,
338-
parameter,
339-
)}
340-
/>
341-
),
342-
)}
343-
</FormFields>
344-
</FormSection>
345-
)}
346-
347-
{/* Immutable rich parameters */}
348-
{props.templateParameters &&
349-
props.templateParameters.filter((p) => !p.mutable).length > 0 && (
350-
<FormSection
351-
title="Immutable parameters"
314+
{props.templateParameters && (
315+
<>
316+
<MutableTemplateParametersSection
317+
templateParameters={props.templateParameters}
318+
getInputProps={(parameter, index) => {
319+
return {
320+
...getFieldHelpers(
321+
"rich_parameter_values[" + index + "].value",
322+
),
323+
onChange: (value) => {
324+
form.setFieldValue("rich_parameter_values." + index, {
325+
name: parameter.name,
326+
value: value,
327+
})
328+
},
329+
initialValue: workspaceBuildParameterValue(
330+
initialRichParameterValues,
331+
parameter,
332+
),
333+
disabled: form.isSubmitting,
334+
}
335+
}}
336+
/>
337+
<ImmutableTemplateParametersSection
338+
templateParameters={props.templateParameters}
352339
classes={{ root: styles.warningSection }}
353-
description={
354-
<>
355-
These parameters are also provided by your Terraform
356-
configuration but they{" "}
357-
<strong className={styles.warningText}>
358-
cannot be changed after creating the workspace.
359-
</strong>
360-
</>
361-
}
362-
>
363-
<FormFields>
364-
{props.templateParameters.map(
365-
(parameter, index) =>
366-
!parameter.mutable && (
367-
<RichParameterInput
368-
{...getFieldHelpers(
369-
"rich_parameter_values[" + index + "].value",
370-
)}
371-
disabled={form.isSubmitting}
372-
index={index}
373-
key={parameter.name}
374-
onChange={(value) => {
375-
form.setFieldValue("rich_parameter_values." + index, {
376-
name: parameter.name,
377-
value: value,
378-
})
379-
}}
380-
parameter={parameter}
381-
initialValue={workspaceBuildParameterValue(
382-
initialRichParameterValues,
383-
parameter,
384-
)}
385-
/>
386-
),
387-
)}
388-
</FormFields>
389-
</FormSection>
390-
)}
340+
getInputProps={(parameter, index) => {
341+
return {
342+
...getFieldHelpers(
343+
"rich_parameter_values[" + index + "].value",
344+
),
345+
onChange: (value) => {
346+
form.setFieldValue("rich_parameter_values." + index, {
347+
name: parameter.name,
348+
value: value,
349+
})
350+
},
351+
initialValue: workspaceBuildParameterValue(
352+
initialRichParameterValues,
353+
parameter,
354+
),
355+
disabled: form.isSubmitting,
356+
}
357+
}}
358+
/>
359+
</>
360+
)}
391361

392362
<FormFooter
393363
onCancel={props.onCancel}
@@ -408,7 +378,7 @@ const useStyles = makeStyles((theme) => ({
408378
borderRadius: 8,
409379
backgroundColor: theme.palette.background.paper,
410380
padding: theme.spacing(10),
411-
marginLeft: -theme.spacing(10),
412-
marginRight: -theme.spacing(10),
381+
marginLeft: theme.spacing(-10),
382+
marginRight: theme.spacing(-10),
413383
},
414384
}))
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import {
2+
renderWithAuth,
3+
waitForLoaderToBeRemoved,
4+
} from "testHelpers/renderHelpers"
5+
import TemplateEmbedPage from "./TemplateEmbedPage"
6+
import { TemplateLayout } from "components/TemplateLayout/TemplateLayout"
7+
import {
8+
MockTemplate,
9+
MockTemplateVersionParameter1 as parameter1,
10+
MockTemplateVersionParameter2 as parameter2,
11+
} from "testHelpers/entities"
12+
import * as API from "api/api"
13+
import userEvent from "@testing-library/user-event"
14+
import { screen } from "@testing-library/react"
15+
16+
test("Users can fill the parameters and copy the open in coder url", async () => {
17+
jest
18+
.spyOn(API, "getTemplateVersionRichParameters")
19+
.mockResolvedValue([parameter1, parameter2])
20+
21+
renderWithAuth(
22+
<TemplateLayout>
23+
<TemplateEmbedPage />
24+
</TemplateLayout>,
25+
{
26+
route: `/templates/${MockTemplate.name}/embed`,
27+
path: "/templates/:template/embed",
28+
},
29+
)
30+
await waitForLoaderToBeRemoved()
31+
32+
const user = userEvent.setup()
33+
const firstParameterField = screen.getByLabelText(
34+
parameter1.display_name ?? parameter1.name,
35+
{ exact: false },
36+
)
37+
await user.clear(firstParameterField)
38+
await user.type(firstParameterField, "firstParameterValue")
39+
const secondParameterField = screen.getByLabelText(
40+
parameter2.display_name ?? parameter2.name,
41+
{ exact: false },
42+
)
43+
await user.clear(secondParameterField)
44+
await user.type(secondParameterField, "123456")
45+
46+
jest.spyOn(window.navigator.clipboard, "writeText")
47+
const copyButton = screen.getByRole("button", { name: /copy/i })
48+
await userEvent.click(copyButton)
49+
expect(window.navigator.clipboard.writeText).toBeCalledWith(
50+
`[![Open in Coder](http://localhost/open-in-coder.svg)](http://localhost/templates/test-template/workspace?param.first_parameter=firstParameterValue&param.second_parameter=123456)`,
51+
)
52+
})

0 commit comments

Comments
 (0)