From aaa05397b0349c590af7ce8eaccea3dec20d5cfa Mon Sep 17 00:00:00 2001 From: Marcin Tojek Date: Fri, 17 Feb 2023 16:57:53 +0100 Subject: [PATCH 01/25] WIP --- site/src/AppRouter.tsx | 4 + site/src/api/api.ts | 9 ++ .../TemplateLayout/TemplateLayout.tsx | 31 ++++- .../TemplateSummaryPage.tsx | 8 +- .../TemplateVariablesPage.tsx | 37 ++++++ .../template/templateVariablesXService.ts | 123 ++++++++++++++++++ .../xServices/template/templateXService.ts | 35 +++++ 7 files changed, 244 insertions(+), 3 deletions(-) create mode 100644 site/src/pages/TemplateVariablesPage/TemplateVariablesPage.tsx create mode 100644 site/src/xServices/template/templateVariablesXService.ts diff --git a/site/src/AppRouter.tsx b/site/src/AppRouter.tsx index a1cd2ab296f0f..e2a184cc599cf 100644 --- a/site/src/AppRouter.tsx +++ b/site/src/AppRouter.tsx @@ -123,6 +123,9 @@ const StarterTemplatePage = lazy( const CreateTemplatePage = lazy( () => import("./pages/CreateTemplatePage/CreateTemplatePage"), ) +const TemplateVariablesPage = lazy( + () => import("./pages/TemplateVariablesPage/TemplateVariablesPage"), +) export const AppRouter: FC = () => { return ( @@ -160,6 +163,7 @@ export const AppRouter: FC = () => { } /> } /> + } /> } /> diff --git a/site/src/api/api.ts b/site/src/api/api.ts index 37d89bf22288f..5bd556718fa1c 100644 --- a/site/src/api/api.ts +++ b/site/src/api/api.ts @@ -220,6 +220,15 @@ export const getTemplateVersionResources = async ( return response.data } +export const getTemplateVersionVariables = async ( + versionId: string, +): Promise => { + const response = await axios.get( + `/api/v2/templateversions/${versionId}/variables`, + ) + return response.data +} + export const getTemplateVersions = async ( templateId: string, ): Promise => { diff --git a/site/src/components/TemplateLayout/TemplateLayout.tsx b/site/src/components/TemplateLayout/TemplateLayout.tsx index fa02caff789ac..9cf2d80c037a7 100644 --- a/site/src/components/TemplateLayout/TemplateLayout.tsx +++ b/site/src/components/TemplateLayout/TemplateLayout.tsx @@ -2,6 +2,7 @@ import Button from "@material-ui/core/Button" import Link from "@material-ui/core/Link" import { makeStyles } from "@material-ui/core/styles" import AddCircleOutline from "@material-ui/icons/AddCircleOutline" +import CodeOutlined from "@material-ui/icons/CodeOutlined" import SettingsOutlined from "@material-ui/icons/SettingsOutlined" import { useMachine } from "@xstate/react" import { @@ -31,7 +32,7 @@ import { Avatar } from "components/Avatar/Avatar" const Language = { settingsButton: "Settings", - editButton: "Edit", + variablesButton: "Variables", createButton: "Create workspace", noDescription: "", } @@ -79,6 +80,20 @@ const TemplateSettingsButton: FC<{ templateName: string }> = ({ ) +const TemplateVariablesButton: FC<{ templateName: string }> = ({ + templateName, +}) => ( + + + +) + const CreateWorkspaceButton: FC<{ templateName: string className?: string @@ -106,7 +121,11 @@ export const TemplateLayout: FC<{ children?: JSX.Element }> = ({ organizationId, }, }) - const { template, permissions: templatePermissions } = templateState.context + const { + template, + permissions: templatePermissions, + templateVersionVariables, + } = templateState.context const permissions = usePermissions() const hasIcon = template && template.icon && template.icon !== "" @@ -121,6 +140,14 @@ export const TemplateLayout: FC<{ children?: JSX.Element }> = ({ pageActions.push() } + if ( + templatePermissions?.canUpdateTemplate && + templateVersionVariables && + templateVersionVariables.length > 0 + ) { + pageActions.push() + } + pageActions.push() return pageActions diff --git a/site/src/pages/TemplatePage/TemplateSummaryPage/TemplateSummaryPage.tsx b/site/src/pages/TemplatePage/TemplateSummaryPage/TemplateSummaryPage.tsx index 42cbd7dd63ae8..fe41fa4373c72 100644 --- a/site/src/pages/TemplatePage/TemplateSummaryPage/TemplateSummaryPage.tsx +++ b/site/src/pages/TemplatePage/TemplateSummaryPage/TemplateSummaryPage.tsx @@ -12,10 +12,16 @@ export const TemplateSummaryPage: FC = () => { activeTemplateVersion, templateResources, templateVersions, + templateVersionVariables, templateDAUs, } = context - if (!template || !activeTemplateVersion || !templateResources) { + if ( + !template || + !activeTemplateVersion || + !templateResources || + !templateVersionVariables + ) { return } diff --git a/site/src/pages/TemplateVariablesPage/TemplateVariablesPage.tsx b/site/src/pages/TemplateVariablesPage/TemplateVariablesPage.tsx new file mode 100644 index 0000000000000..44443d9588fdb --- /dev/null +++ b/site/src/pages/TemplateVariablesPage/TemplateVariablesPage.tsx @@ -0,0 +1,37 @@ +import { useMachine } from "@xstate/react" +import { useOrganizationId } from "hooks/useOrganizationId" +import { FC } from "react" +import { Helmet } from "react-helmet-async" +import { useTranslation } from "react-i18next" +import { useParams } from "react-router-dom" +import { templateVariablesMachine } from "xServices/template/templateVariablesXService" +import { pageTitle } from "../../util/page" + +export const TemplateVariablesPage: FC = () => { + const { t } = useTranslation("templateVariablesPage") + + const { template: templateName } = useParams() as { + organization: string + template: string + } + const organizationId = useOrganizationId() + + const [state] = useMachine(templateVariablesMachine, { + context: { + organizationId, + templateName, + }, + }) + + return ( + <> + + Codestin Search App + + +
TODO
+ + ) +} + +export default TemplateVariablesPage diff --git a/site/src/xServices/template/templateVariablesXService.ts b/site/src/xServices/template/templateVariablesXService.ts new file mode 100644 index 0000000000000..a1b7fa69f91b4 --- /dev/null +++ b/site/src/xServices/template/templateVariablesXService.ts @@ -0,0 +1,123 @@ +import { getTemplateByName, getTemplateVersionVariables } from "api/api" +import { + CreateWorkspaceBuildRequest, + Template, + TemplateVersionVariable, +} from "api/typesGenerated" +import { assign, createMachine } from "xstate" + +type TemplateVariablesContext = { + organizationId: string + templateName: string + + selectedTemplate?: Template + templateVariables?: TemplateVersionVariable[] + + getTemplateError?: Error | unknown + getTemplateVariablesError?: Error | unknown +} + +type UpdateTemplateEvent = { + type: "UPDATE_TEMPLATE" + request: CreateWorkspaceBuildRequest // FIXME +} + +export const templateVariablesMachine = createMachine( + { + id: "templateVariablesState", + predictableActionArguments: true, + tsTypes: {} as import("./templateVariablesXService.typegen").Typegen0, + schema: { + context: {} as TemplateVariablesContext, + events: {} as UpdateTemplateEvent, + services: {} as { + getTemplate: { + data: Template + } + getTemplateVariables: { + data: TemplateVersionVariable[] + } + }, + }, + initial: "gettingTemplate", + states: { + gettingTemplate: { + entry: "clearGetTemplateError", + invoke: { + src: "getTemplate", + onDone: [ + { + actions: ["assignTemplate"], + target: "gettingTemplateVariables", + }, + ], + onError: { + actions: ["assignGetTemplateError"], + target: "error", + }, + }, + }, + gettingTemplateVariables: { + entry: "clearGetTemplateVariablesError", + invoke: { + src: "getTemplateVariables", + onDone: [ + { + actions: ["assignTemplateVariables"], + target: "fillingParams", + }, + ], + onError: { + actions: ["assignGetTemplateVariablesError"], + target: "error", + }, + }, + }, + fillingParams: { + // FIXME on + }, + updated: { + entry: "onUpdateTemplate", + type: "final", + }, + error: {}, + }, + }, + { + services: { + getTemplate: (context) => { + const { organizationId, templateName } = context + return getTemplateByName(organizationId, templateName) + }, + getTemplateVariables: (context) => { + const { selectedTemplate } = context + + if (!selectedTemplate) { + throw new Error("No template selected") + } + + return getTemplateVersionVariables(selectedTemplate.active_version_id) + }, + }, + actions: { + assignTemplate: assign({ + selectedTemplate: (_, event) => event.data, + }), + assignTemplateVariables: assign({ + templateVariables: (_, event) => event.data, + }), + assignGetTemplateError: assign({ + getTemplateError: (_, event) => event.data, + }), + clearGetTemplateError: assign({ + getTemplateError: (_) => undefined, + }), + assignGetTemplateVariablesError: assign({ + getTemplateVariablesError: (_, event) => event.data, + }), + clearGetTemplateVariablesError: assign({ + getTemplateVariablesError: (_) => undefined, + }), + }, + }, +) diff --git a/site/src/xServices/template/templateXService.ts b/site/src/xServices/template/templateXService.ts index b7b836e4565ef..da99c24c5d3fc 100644 --- a/site/src/xServices/template/templateXService.ts +++ b/site/src/xServices/template/templateXService.ts @@ -6,12 +6,14 @@ import { getTemplateVersion, getTemplateVersionResources, getTemplateVersions, + getTemplateVersionVariables, } from "api/api" import { AuthorizationResponse, Template, TemplateDAUsResponse, TemplateVersion, + TemplateVersionVariable, WorkspaceResource, } from "api/typesGenerated" @@ -21,6 +23,7 @@ export interface TemplateContext { template?: Template activeTemplateVersion?: TemplateVersion templateResources?: WorkspaceResource[] + templateVersionVariables?: TemplateVersionVariable[] templateVersions?: TemplateVersion[] templateDAUs?: TemplateDAUsResponse permissions?: AuthorizationResponse @@ -53,6 +56,9 @@ export const templateMachine = getActiveTemplateVersion: { data: TemplateVersion } + getTemplateVersionVariables: { + data: TemplateVersionVariable[] + } getTemplateResources: { data: WorkspaceResource[] } @@ -127,6 +133,25 @@ export const templateMachine = }, }, }, + templateVersionVariables: { + initial: "gettingTemplateVersionVariables", + states: { + gettingTemplateVersionVariables: { + invoke: { + src: "getTemplateVersionVariables", + onDone: [ + { + actions: "assignTemplateVersionVariables", + target: "success", + }, + ], + }, + }, + success: { + type: "final", + }, + }, + }, templateVersions: { initial: "gettingTemplateVersions", states: { @@ -220,6 +245,13 @@ export const templateMachine = return getTemplateVersion(ctx.template.active_version_id) }, + getTemplateVersionVariables: (ctx) => { + if (!ctx.template) { + throw new Error("Active template version not loaded") + } + + return getTemplateVersionVariables(ctx.template.active_version_id) + }, getTemplateResources: (ctx) => { if (!ctx.template) { throw new Error("Template not loaded") @@ -265,6 +297,9 @@ export const templateMachine = assignTemplateVersions: assign({ templateVersions: (_, event) => event.data, }), + assignTemplateVersionVariables: assign({ + templateVersionVariables: (_, event) => event.data, + }), assignTemplateDAUs: assign({ templateDAUs: (_, event) => event.data, }), From 247ee882044030c724d173ea6896a1e03b7204cd Mon Sep 17 00:00:00 2001 From: Marcin Tojek Date: Mon, 27 Feb 2023 14:25:14 +0100 Subject: [PATCH 02/25] Fix --- site/src/components/TemplateLayout/TemplateLayout.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/site/src/components/TemplateLayout/TemplateLayout.tsx b/site/src/components/TemplateLayout/TemplateLayout.tsx index cd6e73dd00691..734e34a2c6f2c 100644 --- a/site/src/components/TemplateLayout/TemplateLayout.tsx +++ b/site/src/components/TemplateLayout/TemplateLayout.tsx @@ -64,7 +64,7 @@ export const TemplateLayout: FC<{ children?: JSX.Element }> = ({ } = templateState.context const permissions = usePermissions() - if (!template || !templatePermissions) { + if (!template || !templatePermissions || !templateVersionVariables) { return } From 2f3b663962d3e684dc6fe0472249bdcc03edac35 Mon Sep 17 00:00:00 2001 From: Marcin Tojek Date: Mon, 27 Feb 2023 14:27:35 +0100 Subject: [PATCH 03/25] fmt --- site/src/components/TemplateLayout/TemplatePageHeader.tsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/site/src/components/TemplateLayout/TemplatePageHeader.tsx b/site/src/components/TemplateLayout/TemplatePageHeader.tsx index 4bd2826da73cc..393dded7dbde3 100644 --- a/site/src/components/TemplateLayout/TemplatePageHeader.tsx +++ b/site/src/components/TemplateLayout/TemplatePageHeader.tsx @@ -3,7 +3,11 @@ import DeleteOutlined from "@material-ui/icons/DeleteOutlined" import AddCircleOutline from "@material-ui/icons/AddCircleOutline" import SettingsOutlined from "@material-ui/icons/SettingsOutlined" import CodeOutlined from "@material-ui/icons/CodeOutlined" -import { AuthorizationResponse, Template, TemplateVersionVariable } from "api/typesGenerated" +import { + AuthorizationResponse, + Template, + TemplateVersionVariable, +} from "api/typesGenerated" import { Avatar } from "components/Avatar/Avatar" import { Maybe } from "components/Conditionals/Maybe" import { DeleteDialog } from "components/Dialogs/DeleteDialog/DeleteDialog" From 1fbb2ef38ce11a0e2823266b80828d2c552bf4fa Mon Sep 17 00:00:00 2001 From: Marcin Tojek Date: Mon, 27 Feb 2023 14:59:11 +0100 Subject: [PATCH 04/25] Tests work after merge --- site/src/i18n/en/index.ts | 2 ++ site/src/i18n/en/templateVariablesPage.json | 3 +++ .../TemplateSummaryPage.test.tsx | 15 +++++++++++++++ 3 files changed, 20 insertions(+) create mode 100644 site/src/i18n/en/templateVariablesPage.json diff --git a/site/src/i18n/en/index.ts b/site/src/i18n/en/index.ts index e838753608850..58f6530cab049 100644 --- a/site/src/i18n/en/index.ts +++ b/site/src/i18n/en/index.ts @@ -9,6 +9,7 @@ import buildPage from "./buildPage.json" import workspacesPage from "./workspacesPage.json" import usersPage from "./usersPage.json" import templateSettingsPage from "./templateSettingsPage.json" +import templateVariablesPage from "./templateVariablesPage.json" import templateVersionPage from "./templateVersionPage.json" import loginPage from "./loginPage.json" import workspaceBuildParametersPage from "./workspaceBuildParametersPage.json" @@ -33,6 +34,7 @@ export const en = { workspacesPage, usersPage, templateSettingsPage, + templateVariablesPage, templateVersionPage, loginPage, workspaceBuildParametersPage, diff --git a/site/src/i18n/en/templateVariablesPage.json b/site/src/i18n/en/templateVariablesPage.json new file mode 100644 index 0000000000000..7d626bc2a3108 --- /dev/null +++ b/site/src/i18n/en/templateVariablesPage.json @@ -0,0 +1,3 @@ +{ + "title": "Variables" +} diff --git a/site/src/pages/TemplatePage/TemplateSummaryPage/TemplateSummaryPage.test.tsx b/site/src/pages/TemplatePage/TemplateSummaryPage/TemplateSummaryPage.test.tsx index bd2ab35e06ba2..a022d30e0c759 100644 --- a/site/src/pages/TemplatePage/TemplateSummaryPage/TemplateSummaryPage.test.tsx +++ b/site/src/pages/TemplatePage/TemplateSummaryPage/TemplateSummaryPage.test.tsx @@ -36,6 +36,15 @@ describe("TemplateSummaryPage", () => { const mock = jest.spyOn(CreateDayString, "createDayString") mock.mockImplementation(() => "a minute ago") + server.use( + rest.get( + "/api/v2/templateversions/:templateVersion/variables", + (req, res, ctx) => { + return res(ctx.status(200), ctx.json([])) + }, + ), + ) + renderPage() await screen.findByText(MockTemplate.display_name) await screen.findByTestId("markdown") @@ -45,6 +54,12 @@ describe("TemplateSummaryPage", () => { it("does not allow a member to delete a template", () => { // get member-level permissions server.use( + rest.get( + "/api/v2/templateversions/:templateVersion/variables", + (req, res, ctx) => { + return res(ctx.status(200), ctx.json([])) + }, + ), rest.post("/api/v2/authcheck", async (req, res, ctx) => { return res(ctx.status(200), ctx.json(MockMemberPermissions)) }), From 19478de02990d10fe0fbe530352abeded66e6b70 Mon Sep 17 00:00:00 2001 From: Marcin Tojek Date: Mon, 27 Feb 2023 18:55:04 +0100 Subject: [PATCH 05/25] WIP --- .../TemplateVariablesForm.tsx | 181 ++++++++++++++++++ .../TemplateVariablesPage.tsx | 32 +++- .../TemplateVariablesPageView.tsx | 66 +++++++ .../template/templateVariablesXService.ts | 57 ++++-- 4 files changed, 318 insertions(+), 18 deletions(-) create mode 100644 site/src/pages/TemplateVariablesPage/TemplateVariablesForm.tsx create mode 100644 site/src/pages/TemplateVariablesPage/TemplateVariablesPageView.tsx diff --git a/site/src/pages/TemplateVariablesPage/TemplateVariablesForm.tsx b/site/src/pages/TemplateVariablesPage/TemplateVariablesForm.tsx new file mode 100644 index 0000000000000..d554feedd6c2a --- /dev/null +++ b/site/src/pages/TemplateVariablesPage/TemplateVariablesForm.tsx @@ -0,0 +1,181 @@ +import TextField from "@material-ui/core/TextField" +import { CreateTemplateVersionRequest, Template, TemplateVersionVariable } from "api/typesGenerated" +import { FormikContextType, FormikTouched, useFormik } from "formik" +import { FC } from "react" +import { + getFormHelpers, + onChangeTrimmed, +} from "util/formUtils" +import * as Yup from "yup" +import { useTranslation } from "react-i18next" +import { LazyIconField } from "components/IconField/LazyIconField" +import { + FormFields, + FormSection, + HorizontalForm, + FormFooter, +} from "components/HorizontalForm/HorizontalForm" +import { Stack } from "components/Stack/Stack" +import Checkbox from "@material-ui/core/Checkbox" +import { makeStyles } from "@material-ui/core/styles" + +export const getValidationSchema = (): Yup.AnyObjectSchema => + Yup.object() + +export interface TemplateVariablesForm { + template: Template + templateVariables: TemplateVersionVariable[] + onSubmit: (data: CreateTemplateVersionRequest) => void + onCancel: () => void + isSubmitting: boolean + error?: unknown + // Helpful to show field errors on Storybook + initialTouched?: FormikTouched +} + +export const TemplateVariablesForm: FC = ({ + template, + templateVariables, + onSubmit, + onCancel, + error, + isSubmitting, + initialTouched, +}) => { + const validationSchema = getValidationSchema() + const form: FormikContextType = + useFormik({ + initialValues: { + name: template.name, + }, + validationSchema, + onSubmit: onSubmit, + initialTouched, + }) + const getFieldHelpers = getFormHelpers(form, error) + const { t } = useTranslation("TemplateVariablesPage") + const styles = useStyles() + + return ( + + + + + + + + + + + + + + form.setFieldValue("icon", value)} + /> + + + + + + + + + + + + + + ) +} + +const useStyles = makeStyles((theme) => ({ + optionText: { + fontSize: theme.spacing(2), + color: theme.palette.text.primary, + }, + + optionHelperText: { + fontSize: theme.spacing(1.5), + color: theme.palette.text.secondary, + }, +})) diff --git a/site/src/pages/TemplateVariablesPage/TemplateVariablesPage.tsx b/site/src/pages/TemplateVariablesPage/TemplateVariablesPage.tsx index 44443d9588fdb..aded522074029 100644 --- a/site/src/pages/TemplateVariablesPage/TemplateVariablesPage.tsx +++ b/site/src/pages/TemplateVariablesPage/TemplateVariablesPage.tsx @@ -3,33 +3,55 @@ import { useOrganizationId } from "hooks/useOrganizationId" import { FC } from "react" import { Helmet } from "react-helmet-async" import { useTranslation } from "react-i18next" -import { useParams } from "react-router-dom" +import { useNavigate, useParams } from "react-router-dom" import { templateVariablesMachine } from "xServices/template/templateVariablesXService" import { pageTitle } from "../../util/page" +import { TemplateVariablesPageView } from "./TemplateVariablesPageView" export const TemplateVariablesPage: FC = () => { - const { t } = useTranslation("templateVariablesPage") - const { template: templateName } = useParams() as { organization: string template: string } const organizationId = useOrganizationId() - const [state] = useMachine(templateVariablesMachine, { + const [state, send] = useMachine(templateVariablesMachine, { context: { organizationId, templateName, }, }) + const { + templateVariables, + getTemplateError, + getTemplateVariablesError, + // FIXME saveTemplateVariablesError, + } = state.context + const { t } = useTranslation("templateVariablesPage") + const navigate = useNavigate() return ( <> Codestin Search App -
TODO
+ { + navigate(`/templates/${templateName}`) + }} + onSubmit={(templateVariables) => { + send({ type: "UPDATE_TEMPLATE", templateVariables }) + }} + /> + ) } diff --git a/site/src/pages/TemplateVariablesPage/TemplateVariablesPageView.tsx b/site/src/pages/TemplateVariablesPage/TemplateVariablesPageView.tsx new file mode 100644 index 0000000000000..55202ed0b4c7a --- /dev/null +++ b/site/src/pages/TemplateVariablesPage/TemplateVariablesPageView.tsx @@ -0,0 +1,66 @@ +import { Template, TemplateVersionVariable, UpdateTemplateMeta } from "api/typesGenerated" +import { AlertBanner } from "components/AlertBanner/AlertBanner" +import { Loader } from "components/Loader/Loader" +import { ComponentProps, FC } from "react" +import { TemplateVariablesForm } from "./TemplateVariablesForm" +import { Stack } from "components/Stack/Stack" +import { makeStyles } from "@material-ui/core/styles" +import { useTranslation } from "react-i18next" +import { FullPageHorizontalForm } from "components/FullPageForm/FullPageHorizontalForm" + +export interface TemplateVariablesPageViewProps { + template?: Template + templateVariables?: TemplateVersionVariable[] + onSubmit: (data: UpdateTemplateMeta) => void + onCancel: () => void + isSubmitting: boolean + errors?: { + getTemplateError?: unknown + getTemplateVariablesError?: unknown + } + initialTouched?: ComponentProps["initialTouched"] +} + +export const TemplateVariablesPageView: FC = ({ + template, + templateVariables, + onCancel, + onSubmit, + isSubmitting, + errors = {}, + initialTouched, +}) => { + const classes = useStyles() + const isLoading = !template && !errors.getTemplateError && !errors.getTemplateVariablesError + const { t } = useTranslation("TemplateVariablesPage") + + return ( + + {Boolean(errors.getTemplateError) && ( + + + + )} + {isLoading && } + {template && templateVariables && ( + <> + + + )} + + ) +} + +const useStyles = makeStyles((theme) => ({ + errorContainer: { + marginBottom: theme.spacing(2), + }, +})) diff --git a/site/src/xServices/template/templateVariablesXService.ts b/site/src/xServices/template/templateVariablesXService.ts index a1b7fa69f91b4..cb92cfd5c03ca 100644 --- a/site/src/xServices/template/templateVariablesXService.ts +++ b/site/src/xServices/template/templateVariablesXService.ts @@ -1,7 +1,8 @@ -import { getTemplateByName, getTemplateVersionVariables } from "api/api" +import { getTemplateByName, getTemplateVersion, getTemplateVersionVariables } from "api/api" import { - CreateWorkspaceBuildRequest, + CreateTemplateVersionRequest, Template, + TemplateVersion, TemplateVersionVariable, } from "api/typesGenerated" import { assign, createMachine } from "xstate" @@ -10,16 +11,18 @@ type TemplateVariablesContext = { organizationId: string templateName: string - selectedTemplate?: Template + template?: Template + activeTemplateVersion?: TemplateVersion templateVariables?: TemplateVersionVariable[] getTemplateError?: Error | unknown + getActiveTemplateVersionError?: Error | unknown getTemplateVariablesError?: Error | unknown } -type UpdateTemplateEvent = { - type: "UPDATE_TEMPLATE" - request: CreateWorkspaceBuildRequest // FIXME +type CreateTemplateVersionEvent = { + type: "CREATE_TEMPLATE_VERSION" + request: CreateTemplateVersionRequest // FIXME } export const templateVariablesMachine = createMachine( @@ -29,11 +32,14 @@ export const templateVariablesMachine = createMachine( tsTypes: {} as import("./templateVariablesXService.typegen").Typegen0, schema: { context: {} as TemplateVariablesContext, - events: {} as UpdateTemplateEvent, + events: {} as CreateTemplateVersionEvent, services: {} as { getTemplate: { data: Template } + getActiveTemplateVersion: { + data: TemplateVersion + } getTemplateVariables: { data: TemplateVersionVariable[] } @@ -57,6 +63,22 @@ export const templateVariablesMachine = createMachine( }, }, }, + gettingActiveTemplateVersion: { + entry: "clearGetActiveTemplateVersionError", + invoke: { + src: "getActiveTemplateVersion", + onDone: [ + { + actions: ["assignActiveTemplateVersion"], + target: "gettingTemplateVariables", + }, + ], + onError: { + actions: ["assignGetActiveTemplateVersionError"], + target: "error", + }, + }, + }, gettingTemplateVariables: { entry: "clearGetTemplateVariablesError", invoke: { @@ -89,19 +111,28 @@ export const templateVariablesMachine = createMachine( const { organizationId, templateName } = context return getTemplateByName(organizationId, templateName) }, - getTemplateVariables: (context) => { - const { selectedTemplate } = context - - if (!selectedTemplate) { + getActiveTemplateVersion: (context) => { + const { template } = context + if (!template) { throw new Error("No template selected") } + return getTemplateVersion(template.active_version_id) - return getTemplateVersionVariables(selectedTemplate.active_version_id) + }, + getTemplateVariables: (context) => { + const { template } = context + if (!template) { + throw new Error("No template selected") + } + return getTemplateVersionVariables(template.active_version_id) }, }, actions: { assignTemplate: assign({ - selectedTemplate: (_, event) => event.data, + template: (_, event) => event.data, + }), + assignActiveTemplateVersion: assign({ + activeTemplateVersion: (_, event) => event.data, }), assignTemplateVariables: assign({ templateVariables: (_, event) => event.data, From 76cd96f8d9c5780933efdea071985a927ba597a1 Mon Sep 17 00:00:00 2001 From: Marcin Tojek Date: Tue, 28 Feb 2023 13:08:41 +0100 Subject: [PATCH 06/25] WIP --- .../TemplateVariablesForm.tsx | 60 ++++++++++++++----- .../TemplateVariablesPage.tsx | 21 ++++--- .../TemplateVariablesPageView.tsx | 30 +++++++--- .../template/templateVariablesXService.ts | 59 +++++++++++++++--- 4 files changed, 132 insertions(+), 38 deletions(-) diff --git a/site/src/pages/TemplateVariablesPage/TemplateVariablesForm.tsx b/site/src/pages/TemplateVariablesPage/TemplateVariablesForm.tsx index d554feedd6c2a..8846933755670 100644 --- a/site/src/pages/TemplateVariablesPage/TemplateVariablesForm.tsx +++ b/site/src/pages/TemplateVariablesPage/TemplateVariablesForm.tsx @@ -1,11 +1,13 @@ import TextField from "@material-ui/core/TextField" -import { CreateTemplateVersionRequest, Template, TemplateVersionVariable } from "api/typesGenerated" +import { + CreateTemplateVersionRequest, + TemplateVersion, + TemplateVersionVariable, + VariableValue, +} from "api/typesGenerated" import { FormikContextType, FormikTouched, useFormik } from "formik" import { FC } from "react" -import { - getFormHelpers, - onChangeTrimmed, -} from "util/formUtils" +import { getFormHelpers, onChangeTrimmed } from "util/formUtils" import * as Yup from "yup" import { useTranslation } from "react-i18next" import { LazyIconField } from "components/IconField/LazyIconField" @@ -19,11 +21,10 @@ import { Stack } from "components/Stack/Stack" import Checkbox from "@material-ui/core/Checkbox" import { makeStyles } from "@material-ui/core/styles" -export const getValidationSchema = (): Yup.AnyObjectSchema => - Yup.object() +export const getValidationSchema = (): Yup.AnyObjectSchema => Yup.object() export interface TemplateVariablesForm { - template: Template + templateVersion: TemplateVersion templateVariables: TemplateVersionVariable[] onSubmit: (data: CreateTemplateVersionRequest) => void onCancel: () => void @@ -34,7 +35,7 @@ export interface TemplateVariablesForm { } export const TemplateVariablesForm: FC = ({ - template, + templateVersion, templateVariables, onSubmit, onCancel, @@ -46,13 +47,22 @@ export const TemplateVariablesForm: FC = ({ const form: FormikContextType = useFormik({ initialValues: { - name: template.name, + template_id: templateVersion.template_id, + provisioner: "terraform", + storage_method: "file", + tags: {}, + // FIXME file_id: null, + user_variable_values: + selectInitialUserVariableValues(templateVariables), }, validationSchema, onSubmit: onSubmit, initialTouched, }) - const getFieldHelpers = getFormHelpers(form, error) + const getFieldHelpers = getFormHelpers( + form, + error, + ) const { t } = useTranslation("TemplateVariablesPage") const styles = useStyles() @@ -118,9 +128,7 @@ export const TemplateVariablesForm: FC = ({ description={t("schedule.description")} > = ({ className={styles.optionText} > {t("allowUserCancelWorkspaceJobsLabel")} - {t("allowUsersCancelHelperText")} @@ -179,3 +186,26 @@ const useStyles = makeStyles((theme) => ({ color: theme.palette.text.secondary, }, })) + +export const selectInitialUserVariableValues = ( + templateVariables: TemplateVersionVariable[], +): VariableValue[] => { + const defaults: VariableValue[] = [] + templateVariables.forEach((templateVariable) => { + if ( + templateVariable.value === "" && + templateVariable.default_value !== "" + ) { + defaults.push({ + name: templateVariable.name, + value: templateVariable.default_value, + }) + return + } + defaults.push({ + name: templateVariable.name, + value: templateVariable.value, + }) + }) + return defaults +} diff --git a/site/src/pages/TemplateVariablesPage/TemplateVariablesPage.tsx b/site/src/pages/TemplateVariablesPage/TemplateVariablesPage.tsx index aded522074029..59c75bec55895 100644 --- a/site/src/pages/TemplateVariablesPage/TemplateVariablesPage.tsx +++ b/site/src/pages/TemplateVariablesPage/TemplateVariablesPage.tsx @@ -14,22 +14,28 @@ export const TemplateVariablesPage: FC = () => { template: string } const organizationId = useOrganizationId() - + const navigate = useNavigate() const [state, send] = useMachine(templateVariablesMachine, { context: { organizationId, templateName, }, + actions: { + onUpdateTemplate: () => { + navigate(`/templates/${templateName}`) + }, + }, }) const { + activeTemplateVersion, templateVariables, getTemplateError, + getActiveTemplateVersionError, getTemplateVariablesError, - // FIXME saveTemplateVariablesError, + updateTemplateError, } = state.context const { t } = useTranslation("templateVariablesPage") - const navigate = useNavigate() return ( <> @@ -38,20 +44,21 @@ export const TemplateVariablesPage: FC = () => { { navigate(`/templates/${templateName}`) }} - onSubmit={(templateVariables) => { - send({ type: "UPDATE_TEMPLATE", templateVariables }) + onSubmit={(formData) => { + send({ type: "UPDATE_TEMPLATE_EVENT", request: formData }) }} /> - ) } diff --git a/site/src/pages/TemplateVariablesPage/TemplateVariablesPageView.tsx b/site/src/pages/TemplateVariablesPage/TemplateVariablesPageView.tsx index 55202ed0b4c7a..2f3cd8a697ded 100644 --- a/site/src/pages/TemplateVariablesPage/TemplateVariablesPageView.tsx +++ b/site/src/pages/TemplateVariablesPage/TemplateVariablesPageView.tsx @@ -1,4 +1,8 @@ -import { Template, TemplateVersionVariable, UpdateTemplateMeta } from "api/typesGenerated" +import { + CreateTemplateVersionRequest, + TemplateVersion, + TemplateVersionVariable, +} from "api/typesGenerated" import { AlertBanner } from "components/AlertBanner/AlertBanner" import { Loader } from "components/Loader/Loader" import { ComponentProps, FC } from "react" @@ -9,20 +13,24 @@ import { useTranslation } from "react-i18next" import { FullPageHorizontalForm } from "components/FullPageForm/FullPageHorizontalForm" export interface TemplateVariablesPageViewProps { - template?: Template + templateVersion?: TemplateVersion templateVariables?: TemplateVersionVariable[] - onSubmit: (data: UpdateTemplateMeta) => void + onSubmit: (data: CreateTemplateVersionRequest) => void onCancel: () => void isSubmitting: boolean errors?: { getTemplateError?: unknown + getActiveTemplateVersionError?: unknown getTemplateVariablesError?: unknown + updateTemplateError?: unknown } - initialTouched?: ComponentProps["initialTouched"] + initialTouched?: ComponentProps< + typeof TemplateVariablesForm + >["initialTouched"] } export const TemplateVariablesPageView: FC = ({ - template, + templateVersion, templateVariables, onCancel, onSubmit, @@ -31,7 +39,11 @@ export const TemplateVariablesPageView: FC = ({ initialTouched, }) => { const classes = useStyles() - const isLoading = !template && !errors.getTemplateError && !errors.getTemplateVariablesError + const isLoading = + !templateVersion && + !templateVariables && + !errors.getTemplateError && + !errors.getTemplateVariablesError const { t } = useTranslation("TemplateVariablesPage") return ( @@ -42,16 +54,16 @@ export const TemplateVariablesPageView: FC = ({ )} {isLoading && } - {template && templateVariables && ( + {templateVersion && templateVariables && ( <> )} diff --git a/site/src/xServices/template/templateVariablesXService.ts b/site/src/xServices/template/templateVariablesXService.ts index cb92cfd5c03ca..e840072fee703 100644 --- a/site/src/xServices/template/templateVariablesXService.ts +++ b/site/src/xServices/template/templateVariablesXService.ts @@ -1,4 +1,8 @@ -import { getTemplateByName, getTemplateVersion, getTemplateVersionVariables } from "api/api" +import { + getTemplateByName, + getTemplateVersion, + getTemplateVersionVariables, +} from "api/api" import { CreateTemplateVersionRequest, Template, @@ -15,13 +19,16 @@ type TemplateVariablesContext = { activeTemplateVersion?: TemplateVersion templateVariables?: TemplateVersionVariable[] + createTemplateVersionRequest?: CreateTemplateVersionRequest + getTemplateError?: Error | unknown getActiveTemplateVersionError?: Error | unknown getTemplateVariablesError?: Error | unknown + updateTemplateError?: Error | unknown } -type CreateTemplateVersionEvent = { - type: "CREATE_TEMPLATE_VERSION" +type UpdateTemplateEvent = { + type: "UPDATE_TEMPLATE_EVENT" request: CreateTemplateVersionRequest // FIXME } @@ -32,7 +39,7 @@ export const templateVariablesMachine = createMachine( tsTypes: {} as import("./templateVariablesXService.typegen").Typegen0, schema: { context: {} as TemplateVariablesContext, - events: {} as CreateTemplateVersionEvent, + events: {} as UpdateTemplateEvent, services: {} as { getTemplate: { data: Template @@ -43,6 +50,9 @@ export const templateVariablesMachine = createMachine( getTemplateVariables: { data: TemplateVersionVariable[] } + updateTemplate: { + data: CreateTemplateVersionRequest + } }, }, initial: "gettingTemplate", @@ -54,7 +64,7 @@ export const templateVariablesMachine = createMachine( onDone: [ { actions: ["assignTemplate"], - target: "gettingTemplateVariables", + target: "gettingActiveTemplateVersion", }, ], onError: { @@ -96,7 +106,27 @@ export const templateVariablesMachine = createMachine( }, }, fillingParams: { - // FIXME on + on: { + UPDATE_TEMPLATE_EVENT: { + actions: ["assignCreateTemplateVersionRequest"], + target: "updatingTemplate", + }, + }, + }, + updatingTemplate: { + entry: "clearUpdateTemplateError", + invoke: { + src: "updateTemplate", + onDone: { + actions: ["onUpdateTemplate"], + target: "updated", + }, + onError: { + actions: ["assignUpdateTemplateError"], + target: "fillingParams", + }, + }, + tags: ["submitting"], }, updated: { entry: "onUpdateTemplate", @@ -117,7 +147,6 @@ export const templateVariablesMachine = createMachine( throw new Error("No template selected") } return getTemplateVersion(template.active_version_id) - }, getTemplateVariables: (context) => { const { template } = context @@ -126,6 +155,7 @@ export const templateVariablesMachine = createMachine( } return getTemplateVersionVariables(template.active_version_id) }, + updateTemplate: (context) => {}, }, actions: { assignTemplate: assign({ @@ -137,6 +167,9 @@ export const templateVariablesMachine = createMachine( assignTemplateVariables: assign({ templateVariables: (_, event) => event.data, }), + assignCreateTemplateVersionRequest: assign({ + createTemplateVersionRequest: (_, event) => event.request, + }), assignGetTemplateError: assign({ getTemplateError: (_, event) => event.data, }), @@ -149,6 +182,18 @@ export const templateVariablesMachine = createMachine( clearGetTemplateVariablesError: assign({ getTemplateVariablesError: (_) => undefined, }), + assignGetActiveTemplateVersionError: assign({ + getActiveTemplateVersionError: (_, event) => event.data, + }), + clearGetActiveTemplateVersionError: assign({ + getActiveTemplateVersionError: (_) => undefined, + }), + assignUpdateTemplateError: assign({ + updateTemplateError: (_, event) => event.data, + }), + clearUpdateTemplateError: assign({ + updateTemplateError: (_) => undefined, + }), }, }, ) From dd9404f8fa87bb362b4d7d61ee89c87a2dd77c18 Mon Sep 17 00:00:00 2001 From: Marcin Tojek Date: Tue, 28 Feb 2023 14:17:22 +0100 Subject: [PATCH 07/25] WIP --- .../TemplateVariablesForm.tsx | 142 ++++-------------- .../TemplateVariablesPageView.tsx | 2 +- 2 files changed, 28 insertions(+), 116 deletions(-) diff --git a/site/src/pages/TemplateVariablesPage/TemplateVariablesForm.tsx b/site/src/pages/TemplateVariablesPage/TemplateVariablesForm.tsx index 8846933755670..789a193b4f7e2 100644 --- a/site/src/pages/TemplateVariablesPage/TemplateVariablesForm.tsx +++ b/site/src/pages/TemplateVariablesPage/TemplateVariablesForm.tsx @@ -7,19 +7,15 @@ import { } from "api/typesGenerated" import { FormikContextType, FormikTouched, useFormik } from "formik" import { FC } from "react" -import { getFormHelpers, onChangeTrimmed } from "util/formUtils" +import { getFormHelpers } from "util/formUtils" import * as Yup from "yup" import { useTranslation } from "react-i18next" -import { LazyIconField } from "components/IconField/LazyIconField" import { FormFields, FormSection, HorizontalForm, FormFooter, } from "components/HorizontalForm/HorizontalForm" -import { Stack } from "components/Stack/Stack" -import Checkbox from "@material-ui/core/Checkbox" -import { makeStyles } from "@material-ui/core/styles" export const getValidationSchema = (): Yup.AnyObjectSchema => Yup.object() @@ -63,135 +59,50 @@ export const TemplateVariablesForm: FC = ({ form, error, ) - const { t } = useTranslation("TemplateVariablesPage") - const styles = useStyles() + const { t } = useTranslation("templateVariablesPage") return ( - - - - - - - - - - - - - form.setFieldValue("icon", value)} - /> - - - - - - - - - - + + + ))} ) } -const useStyles = makeStyles((theme) => ({ - optionText: { - fontSize: theme.spacing(2), - color: theme.palette.text.primary, - }, - - optionHelperText: { - fontSize: theme.spacing(1.5), - color: theme.palette.text.secondary, - }, -})) - export const selectInitialUserVariableValues = ( templateVariables: TemplateVersionVariable[], ): VariableValue[] => { const defaults: VariableValue[] = [] templateVariables.forEach((templateVariable) => { + if (templateVariable.sensitive) { + defaults.push({ + name: templateVariable.name, + value: "", + }) + return + } + if ( templateVariable.value === "" && templateVariable.default_value !== "" @@ -202,6 +113,7 @@ export const selectInitialUserVariableValues = ( }) return } + defaults.push({ name: templateVariable.name, value: templateVariable.value, diff --git a/site/src/pages/TemplateVariablesPage/TemplateVariablesPageView.tsx b/site/src/pages/TemplateVariablesPage/TemplateVariablesPageView.tsx index 2f3cd8a697ded..3fa40bdca427f 100644 --- a/site/src/pages/TemplateVariablesPage/TemplateVariablesPageView.tsx +++ b/site/src/pages/TemplateVariablesPage/TemplateVariablesPageView.tsx @@ -44,7 +44,7 @@ export const TemplateVariablesPageView: FC = ({ !templateVariables && !errors.getTemplateError && !errors.getTemplateVariablesError - const { t } = useTranslation("TemplateVariablesPage") + const { t } = useTranslation("templateVariablesPage") return ( From 1fb043f5820d84ffc8010b96ac471794b45b76d3 Mon Sep 17 00:00:00 2001 From: Marcin Tojek Date: Tue, 28 Feb 2023 16:22:23 +0100 Subject: [PATCH 08/25] TemplateVariableField --- .../TemplateVariableField.tsx | 77 +++++++++++++++++++ .../TemplateVariablesForm.tsx | 14 ++-- 2 files changed, 86 insertions(+), 5 deletions(-) create mode 100644 site/src/components/TemplateVariableField/TemplateVariableField.tsx diff --git a/site/src/components/TemplateVariableField/TemplateVariableField.tsx b/site/src/components/TemplateVariableField/TemplateVariableField.tsx new file mode 100644 index 0000000000000..cc9c8275fe3b3 --- /dev/null +++ b/site/src/components/TemplateVariableField/TemplateVariableField.tsx @@ -0,0 +1,77 @@ +import FormControlLabel from "@material-ui/core/FormControlLabel" +import Radio from "@material-ui/core/Radio" +import RadioGroup from "@material-ui/core/RadioGroup" +import TextField from "@material-ui/core/TextField" +import { TemplateVersionVariable } from "api/typesGenerated" +import { FC, useState } from "react" + +export interface TemplateVariableFieldProps { + templateVersionVariable: TemplateVersionVariable + disabled: boolean + onChange: (value: string) => void +} + +const isBoolean = (variable: TemplateVersionVariable) => { + return variable.type === "bool" +} + +export const TemplateVariableField: FC = ({ + templateVersionVariable, + disabled, + onChange, + ...props +}) => { + const [variableValue, setVariableValue] = useState( + templateVersionVariable.sensitive + ? "" + : templateVersionVariable.default_value, + ) + if (isBoolean(templateVersionVariable)) { + return ( + { + onChange(event.target.value) + }} + > + } + label="True" + /> + } + label="False" + /> + + ) + } + + // TODO Sensitive + // TODO Required + return ( + { + setVariableValue(event.target.value) + onChange(event.target.value) + }} + variant="outlined" + /> + ) +} diff --git a/site/src/pages/TemplateVariablesPage/TemplateVariablesForm.tsx b/site/src/pages/TemplateVariablesPage/TemplateVariablesForm.tsx index 789a193b4f7e2..499e782059db5 100644 --- a/site/src/pages/TemplateVariablesPage/TemplateVariablesForm.tsx +++ b/site/src/pages/TemplateVariablesPage/TemplateVariablesForm.tsx @@ -16,6 +16,7 @@ import { HorizontalForm, FormFooter, } from "components/HorizontalForm/HorizontalForm" +import { TemplateVariableField } from "components/TemplateVariableField/TemplateVariableField" export const getValidationSchema = (): Yup.AnyObjectSchema => Yup.object() @@ -73,13 +74,16 @@ export const TemplateVariablesForm: FC = ({ description={templateVariable.description} > - { + form.setFieldValue("rich_parameter_values." + index, { + name: templateVariable.name, + value: value, + }) + }} /> From 92fb4de3f25fd5256d01d45cac807e2913548f83 Mon Sep 17 00:00:00 2001 From: Marcin Tojek Date: Tue, 28 Feb 2023 16:24:38 +0100 Subject: [PATCH 09/25] i18n --- site/src/i18n/en/templateVariablesPage.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/site/src/i18n/en/templateVariablesPage.json b/site/src/i18n/en/templateVariablesPage.json index 7d626bc2a3108..5fd58e07187dc 100644 --- a/site/src/i18n/en/templateVariablesPage.json +++ b/site/src/i18n/en/templateVariablesPage.json @@ -1,3 +1,3 @@ { - "title": "Variables" + "title": "Template variables" } From 8514cc7c74d0be354b356140764151f0cc291645 Mon Sep 17 00:00:00 2001 From: Marcin Tojek Date: Tue, 28 Feb 2023 16:57:22 +0100 Subject: [PATCH 10/25] WIP --- .../TemplateVariableField.tsx | 16 ++++-- site/src/i18n/en/templateVariablesPage.json | 3 +- .../TemplateVariablesForm.tsx | 55 +++++++++++-------- 3 files changed, 47 insertions(+), 27 deletions(-) diff --git a/site/src/components/TemplateVariableField/TemplateVariableField.tsx b/site/src/components/TemplateVariableField/TemplateVariableField.tsx index cc9c8275fe3b3..f18a72dd3803d 100644 --- a/site/src/components/TemplateVariableField/TemplateVariableField.tsx +++ b/site/src/components/TemplateVariableField/TemplateVariableField.tsx @@ -4,6 +4,14 @@ import RadioGroup from "@material-ui/core/RadioGroup" import TextField from "@material-ui/core/TextField" import { TemplateVersionVariable } from "api/typesGenerated" import { FC, useState } from "react" +import { useTranslation } from "react-i18next" + +export const SensitiveVariableHelperText = () => { + const { t } = useTranslation("templateVariablesPage") + return ( + {t("sensitiveVariableHelperText") } + ) +} export interface TemplateVariableFieldProps { templateVersionVariable: TemplateVersionVariable @@ -11,10 +19,6 @@ export interface TemplateVariableFieldProps { onChange: (value: string) => void } -const isBoolean = (variable: TemplateVersionVariable) => { - return variable.type === "bool" -} - export const TemplateVariableField: FC = ({ templateVersionVariable, disabled, @@ -75,3 +79,7 @@ export const TemplateVariableField: FC = ({ /> ) } + +const isBoolean = (variable: TemplateVersionVariable) => { + return variable.type === "bool" +} diff --git a/site/src/i18n/en/templateVariablesPage.json b/site/src/i18n/en/templateVariablesPage.json index 5fd58e07187dc..bb0c627efa919 100644 --- a/site/src/i18n/en/templateVariablesPage.json +++ b/site/src/i18n/en/templateVariablesPage.json @@ -1,3 +1,4 @@ { - "title": "Template variables" + "title": "Template variables", + "sensitiveVariableHelperText": "This variable is sensitive." } diff --git a/site/src/pages/TemplateVariablesPage/TemplateVariablesForm.tsx b/site/src/pages/TemplateVariablesPage/TemplateVariablesForm.tsx index 499e782059db5..de40986e7d14d 100644 --- a/site/src/pages/TemplateVariablesPage/TemplateVariablesForm.tsx +++ b/site/src/pages/TemplateVariablesPage/TemplateVariablesForm.tsx @@ -16,7 +16,8 @@ import { HorizontalForm, FormFooter, } from "components/HorizontalForm/HorizontalForm" -import { TemplateVariableField } from "components/TemplateVariableField/TemplateVariableField" +import { SensitiveVariableHelperText, TemplateVariableField } from "components/TemplateVariableField/TemplateVariableField" +import { SensitiveValue } from "components/Resources/SensitiveValue" export const getValidationSchema = (): Yup.AnyObjectSchema => Yup.object() @@ -67,27 +68,37 @@ export const TemplateVariablesForm: FC = ({ onSubmit={form.handleSubmit} aria-label={t("formAriaLabel")} > - {templateVariables.map((templateVariable, index) => ( - - - { - form.setFieldValue("rich_parameter_values." + index, { - name: templateVariable.name, - value: value, - }) - }} - /> - - - ))} + {templateVariables.map((templateVariable, index) => { + let fieldHelpers; + if (templateVariable.sensitive) { + fieldHelpers = getFieldHelpers("user_variable_values[" + index + "].value", + ) + } else { + fieldHelpers = getFieldHelpers("user_variable_values[" + index + "].value") + } + + return( + + + { + form.setFieldValue("user_variable_values." + index, { + name: templateVariable.name, + value: value, + }) + }} + /> + + + ) + })} From 44a8e863daa0c2ca3875bbacafcd1a7842b5c10a Mon Sep 17 00:00:00 2001 From: Marcin Tojek Date: Tue, 28 Feb 2023 18:26:14 +0100 Subject: [PATCH 11/25] fix: initialValue --- .../TemplateVariableField.tsx | 17 +++-- site/src/i18n/en/templateVariablesPage.json | 3 +- .../TemplateVariablesForm.tsx | 73 ++++++++++++++----- .../TemplateVariablesPageView.tsx | 21 +++--- 4 files changed, 76 insertions(+), 38 deletions(-) diff --git a/site/src/components/TemplateVariableField/TemplateVariableField.tsx b/site/src/components/TemplateVariableField/TemplateVariableField.tsx index f18a72dd3803d..3daaec6980fae 100644 --- a/site/src/components/TemplateVariableField/TemplateVariableField.tsx +++ b/site/src/components/TemplateVariableField/TemplateVariableField.tsx @@ -8,28 +8,24 @@ import { useTranslation } from "react-i18next" export const SensitiveVariableHelperText = () => { const { t } = useTranslation("templateVariablesPage") - return ( - {t("sensitiveVariableHelperText") } - ) + return {t("sensitiveVariableHelperText")} } export interface TemplateVariableFieldProps { templateVersionVariable: TemplateVersionVariable + initialValue: string disabled: boolean onChange: (value: string) => void } export const TemplateVariableField: FC = ({ templateVersionVariable, + initialValue, disabled, onChange, ...props }) => { - const [variableValue, setVariableValue] = useState( - templateVersionVariable.sensitive - ? "" - : templateVersionVariable.default_value, - ) + const [variableValue, setVariableValue] = useState(initialValue) if (isBoolean(templateVersionVariable)) { return ( = ({ fullWidth label={templateVersionVariable.name} value={variableValue} + placeholder={ + templateVersionVariable.sensitive + ? "" + : templateVersionVariable.default_value + } onChange={(event) => { setVariableValue(event.target.value) onChange(event.target.value) diff --git a/site/src/i18n/en/templateVariablesPage.json b/site/src/i18n/en/templateVariablesPage.json index bb0c627efa919..3e8f239fac6d4 100644 --- a/site/src/i18n/en/templateVariablesPage.json +++ b/site/src/i18n/en/templateVariablesPage.json @@ -1,4 +1,5 @@ { "title": "Template variables", - "sensitiveVariableHelperText": "This variable is sensitive." + "sensitiveVariableHelperText": "This variable is sensitive. The previous value will be used if empty.", + "validationRequiredVariable": "Variable is required." } diff --git a/site/src/pages/TemplateVariablesPage/TemplateVariablesForm.tsx b/site/src/pages/TemplateVariablesPage/TemplateVariablesForm.tsx index de40986e7d14d..de9fd60b2c817 100644 --- a/site/src/pages/TemplateVariablesPage/TemplateVariablesForm.tsx +++ b/site/src/pages/TemplateVariablesPage/TemplateVariablesForm.tsx @@ -1,4 +1,3 @@ -import TextField from "@material-ui/core/TextField" import { CreateTemplateVersionRequest, TemplateVersion, @@ -16,8 +15,10 @@ import { HorizontalForm, FormFooter, } from "components/HorizontalForm/HorizontalForm" -import { SensitiveVariableHelperText, TemplateVariableField } from "components/TemplateVariableField/TemplateVariableField" -import { SensitiveValue } from "components/Resources/SensitiveValue" +import { + SensitiveVariableHelperText, + TemplateVariableField, +} from "components/TemplateVariableField/TemplateVariableField" export const getValidationSchema = (): Yup.AnyObjectSchema => Yup.object() @@ -31,7 +32,6 @@ export interface TemplateVariablesForm { // Helpful to show field errors on Storybook initialTouched?: FormikTouched } - export const TemplateVariablesForm: FC = ({ templateVersion, templateVariables, @@ -41,7 +41,8 @@ export const TemplateVariablesForm: FC = ({ isSubmitting, initialTouched, }) => { - const validationSchema = getValidationSchema() + const initialUserVariableValues = + selectInitialUserVariableValues(templateVariables) const form: FormikContextType = useFormik({ initialValues: { @@ -50,10 +51,14 @@ export const TemplateVariablesForm: FC = ({ storage_method: "file", tags: {}, // FIXME file_id: null, - user_variable_values: - selectInitialUserVariableValues(templateVariables), + user_variable_values: initialUserVariableValues, }, - validationSchema, + validationSchema: Yup.object({ + user_variable_values: ValidationSchemaForTemplateVariables( + "templateVariablesPage", + templateVariables, + ), + }), onSubmit: onSubmit, initialTouched, }) @@ -69,15 +74,19 @@ export const TemplateVariablesForm: FC = ({ aria-label={t("formAriaLabel")} > {templateVariables.map((templateVariable, index) => { - let fieldHelpers; + let fieldHelpers if (templateVariable.sensitive) { - fieldHelpers = getFieldHelpers("user_variable_values[" + index + "].value", - ) + fieldHelpers = getFieldHelpers( + "user_variable_values[" + index + "].value", + , + ) } else { - fieldHelpers = getFieldHelpers("user_variable_values[" + index + "].value") + fieldHelpers = getFieldHelpers( + "user_variable_values[" + index + "].value", + ) } - return( + return ( = ({ { form.setFieldValue("user_variable_values." + index, { @@ -97,7 +107,7 @@ export const TemplateVariablesForm: FC = ({ /> - ) + ) })} @@ -118,10 +128,7 @@ export const selectInitialUserVariableValues = ( return } - if ( - templateVariable.value === "" && - templateVariable.default_value !== "" - ) { + if (templateVariable.required && templateVariable.value === "") { defaults.push({ name: templateVariable.name, value: templateVariable.default_value, @@ -136,3 +143,33 @@ export const selectInitialUserVariableValues = ( }) return defaults } + +export const ValidationSchemaForTemplateVariables = ( + ns: string, + templateVariables: TemplateVersionVariable[], +): Yup.AnySchema => { + const { t } = useTranslation(ns) + + return Yup.array() + .of( + Yup.object().shape({ + name: Yup.string().required(), + value: Yup.string().test("verify with template", (val, ctx) => { + const name = ctx.parent.name + const templateVariable = templateVariables.find( + (variable) => variable.name === name, + ) + if (templateVariable && templateVariable.required) { + if (!val || val.length === 0) { + return ctx.createError({ + path: ctx.path, + message: t("validationRequiredVariable"), + }) + } + } + return true + }), + }), + ) + .required() +} diff --git a/site/src/pages/TemplateVariablesPage/TemplateVariablesPageView.tsx b/site/src/pages/TemplateVariablesPage/TemplateVariablesPageView.tsx index 3fa40bdca427f..ad24dff807beb 100644 --- a/site/src/pages/TemplateVariablesPage/TemplateVariablesPageView.tsx +++ b/site/src/pages/TemplateVariablesPage/TemplateVariablesPageView.tsx @@ -46,6 +46,7 @@ export const TemplateVariablesPageView: FC = ({ !errors.getTemplateVariablesError const { t } = useTranslation("templateVariablesPage") + // TODO stack alert banners return ( {Boolean(errors.getTemplateError) && ( @@ -55,17 +56,15 @@ export const TemplateVariablesPageView: FC = ({ )} {isLoading && } {templateVersion && templateVariables && ( - <> - - + )} ) From 579a30a97319cde47cade1641d487d26cbd159e7 Mon Sep 17 00:00:00 2001 From: Marcin Tojek Date: Wed, 1 Mar 2023 10:40:52 +0100 Subject: [PATCH 12/25] Stacked errors --- .../TemplateVariableField/TemplateVariableField.tsx | 2 -- .../TemplateVariablesPageView.tsx | 10 ++++++++++ 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/site/src/components/TemplateVariableField/TemplateVariableField.tsx b/site/src/components/TemplateVariableField/TemplateVariableField.tsx index 3daaec6980fae..3920896ea8353 100644 --- a/site/src/components/TemplateVariableField/TemplateVariableField.tsx +++ b/site/src/components/TemplateVariableField/TemplateVariableField.tsx @@ -50,8 +50,6 @@ export const TemplateVariableField: FC = ({ ) } - // TODO Sensitive - // TODO Required return ( = ({ )} + {Boolean(errors.getActiveTemplateVersionError) && ( + + + + )} + {Boolean(errors.getTemplateVariablesError) && ( + + + + )} {isLoading && } {templateVersion && templateVariables && ( Date: Wed, 1 Mar 2023 12:17:32 +0100 Subject: [PATCH 13/25] Prepare to trigger API --- coderd/apidoc/docs.go | 71 ++++++++++++++++++- coderd/apidoc/swagger.json | 59 ++++++++++++++- coderd/templateversions.go | 2 +- docs/api/schemas.md | 69 ++++++++++++++++++ docs/api/templates.md | 27 +++---- .../TemplateVariablesForm.tsx | 11 ++- .../TemplateVariablesPage.tsx | 43 ++++++++++- .../TemplateVariablesPageView.tsx | 10 ++- .../template/templateVariablesXService.ts | 4 +- 9 files changed, 274 insertions(+), 22 deletions(-) diff --git a/coderd/apidoc/docs.go b/coderd/apidoc/docs.go index 5912070e2e322..ffc3a073ae2dc 100644 --- a/coderd/apidoc/docs.go +++ b/coderd/apidoc/docs.go @@ -1473,7 +1473,7 @@ const docTemplate = `{ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/codersdk.CreateTemplateVersionDryRunRequest" + "$ref": "#/definitions/codersdk.CreateTemplateVersionRequest" } } ], @@ -5808,6 +5808,66 @@ const docTemplate = `{ } } }, + "codersdk.CreateTemplateVersionRequest": { + "type": "object", + "required": [ + "provisioner", + "storage_method" + ], + "properties": { + "example_id": { + "type": "string" + }, + "file_id": { + "type": "string", + "format": "uuid" + }, + "name": { + "type": "string" + }, + "parameter_values": { + "description": "ParameterValues allows for additional parameters to be provided\nduring the dry-run provision stage.", + "type": "array", + "items": { + "$ref": "#/definitions/codersdk.CreateParameterRequest" + } + }, + "provisioner": { + "type": "string", + "enum": [ + "terraform", + "echo" + ] + }, + "storage_method": { + "enum": [ + "file" + ], + "allOf": [ + { + "$ref": "#/definitions/codersdk.ProvisionerStorageMethod" + } + ] + }, + "tags": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "template_id": { + "description": "TemplateID optionally associates a version with a template.", + "type": "string", + "format": "uuid" + }, + "user_variable_values": { + "type": "array", + "items": { + "$ref": "#/definitions/codersdk.VariableValue" + } + } + } + }, "codersdk.CreateTestAuditLogRequest": { "type": "object", "properties": { @@ -7295,6 +7355,15 @@ const docTemplate = `{ "ProvisionerJobFailed" ] }, + "codersdk.ProvisionerStorageMethod": { + "type": "string", + "enum": [ + "file" + ], + "x-enum-varnames": [ + "ProvisionerStorageMethodFile" + ] + }, "codersdk.PutExtendWorkspaceRequest": { "type": "object", "required": [ diff --git a/coderd/apidoc/swagger.json b/coderd/apidoc/swagger.json index b21f7e6b86166..00109f36735f6 100644 --- a/coderd/apidoc/swagger.json +++ b/coderd/apidoc/swagger.json @@ -1287,7 +1287,7 @@ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/codersdk.CreateTemplateVersionDryRunRequest" + "$ref": "#/definitions/codersdk.CreateTemplateVersionRequest" } } ], @@ -5156,6 +5156,58 @@ } } }, + "codersdk.CreateTemplateVersionRequest": { + "type": "object", + "required": ["provisioner", "storage_method"], + "properties": { + "example_id": { + "type": "string" + }, + "file_id": { + "type": "string", + "format": "uuid" + }, + "name": { + "type": "string" + }, + "parameter_values": { + "description": "ParameterValues allows for additional parameters to be provided\nduring the dry-run provision stage.", + "type": "array", + "items": { + "$ref": "#/definitions/codersdk.CreateParameterRequest" + } + }, + "provisioner": { + "type": "string", + "enum": ["terraform", "echo"] + }, + "storage_method": { + "enum": ["file"], + "allOf": [ + { + "$ref": "#/definitions/codersdk.ProvisionerStorageMethod" + } + ] + }, + "tags": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "template_id": { + "description": "TemplateID optionally associates a version with a template.", + "type": "string", + "format": "uuid" + }, + "user_variable_values": { + "type": "array", + "items": { + "$ref": "#/definitions/codersdk.VariableValue" + } + } + } + }, "codersdk.CreateTestAuditLogRequest": { "type": "object", "properties": { @@ -6543,6 +6595,11 @@ "ProvisionerJobFailed" ] }, + "codersdk.ProvisionerStorageMethod": { + "type": "string", + "enum": ["file"], + "x-enum-varnames": ["ProvisionerStorageMethodFile"] + }, "codersdk.PutExtendWorkspaceRequest": { "type": "object", "required": ["deadline"], diff --git a/coderd/templateversions.go b/coderd/templateversions.go index 04d7e25070e1f..4500a15022894 100644 --- a/coderd/templateversions.go +++ b/coderd/templateversions.go @@ -1077,7 +1077,7 @@ func (api *API) patchActiveTemplateVersion(rw http.ResponseWriter, r *http.Reque // @Produce json // @Tags Templates // @Param organization path string true "Organization ID" format(uuid) -// @Param request body codersdk.CreateTemplateVersionDryRunRequest true "Create template version request" +// @Param request body codersdk.CreateTemplateVersionRequest true "Create template version request" // @Success 201 {object} codersdk.TemplateVersion // @Router /organizations/{organization}/templateversions [post] func (api *API) postTemplateVersionsByOrganization(rw http.ResponseWriter, r *http.Request) { diff --git a/docs/api/schemas.md b/docs/api/schemas.md index 9ca068a81aacb..6404d3bfbc8c3 100644 --- a/docs/api/schemas.md +++ b/docs/api/schemas.md @@ -1027,6 +1027,61 @@ CreateParameterRequest is a structure used to create a new parameter value for a | `user_variable_values` | array of [codersdk.VariableValue](#codersdkvariablevalue) | false | | | | `workspace_name` | string | false | | | +## codersdk.CreateTemplateVersionRequest + +```json +{ + "example_id": "string", + "file_id": "8a0cfb4f-ddc9-436d-91bb-75133c583767", + "name": "string", + "parameter_values": [ + { + "copy_from_parameter": "000e07d6-021d-446c-be14-48a9c20bca0b", + "destination_scheme": "none", + "name": "string", + "source_scheme": "none", + "source_value": "string" + } + ], + "provisioner": "terraform", + "storage_method": "file", + "tags": { + "property1": "string", + "property2": "string" + }, + "template_id": "c6d67e98-83ea-49f0-8812-e4abae2b68bc", + "user_variable_values": [ + { + "name": "string", + "value": "string" + } + ] +} +``` + +### Properties + +| Name | Type | Required | Restrictions | Description | +| ---------------------- | --------------------------------------------------------------------------- | -------- | ------------ | ---------------------------------------------------------------------------------------------------- | +| `example_id` | string | false | | | +| `file_id` | string | false | | | +| `name` | string | false | | | +| `parameter_values` | array of [codersdk.CreateParameterRequest](#codersdkcreateparameterrequest) | false | | Parameter values allows for additional parameters to be provided during the dry-run provision stage. | +| `provisioner` | string | true | | | +| `storage_method` | [codersdk.ProvisionerStorageMethod](#codersdkprovisionerstoragemethod) | true | | | +| `tags` | object | false | | | +| ยป `[any property]` | string | false | | | +| `template_id` | string | false | | Template ID optionally associates a version with a template. | +| `user_variable_values` | array of [codersdk.VariableValue](#codersdkvariablevalue) | false | | | + +#### Enumerated Values + +| Property | Value | +| ---------------- | ----------- | +| `provisioner` | `terraform` | +| `provisioner` | `echo` | +| `storage_method` | `file` | + ## codersdk.CreateTestAuditLogRequest ```json @@ -4059,6 +4114,20 @@ Parameter represents a set value for the scope. | `canceled` | | `failed` | +## codersdk.ProvisionerStorageMethod + +```json +"file" +``` + +### Properties + +#### Enumerated Values + +| Value | +| ------ | +| `file` | + ## codersdk.PutExtendWorkspaceRequest ```json diff --git a/docs/api/templates.md b/docs/api/templates.md index b48b805d7c58e..a8bb6ce443908 100644 --- a/docs/api/templates.md +++ b/docs/api/templates.md @@ -545,6 +545,9 @@ curl -X POST http://coder-server:8080/api/v2/organizations/{organization}/templa ```json { + "example_id": "string", + "file_id": "8a0cfb4f-ddc9-436d-91bb-75133c583767", + "name": "string", "parameter_values": [ { "copy_from_parameter": "000e07d6-021d-446c-be14-48a9c20bca0b", @@ -554,28 +557,28 @@ curl -X POST http://coder-server:8080/api/v2/organizations/{organization}/templa "source_value": "string" } ], - "rich_parameter_values": [ - { - "name": "string", - "value": "string" - } - ], + "provisioner": "terraform", + "storage_method": "file", + "tags": { + "property1": "string", + "property2": "string" + }, + "template_id": "c6d67e98-83ea-49f0-8812-e4abae2b68bc", "user_variable_values": [ { "name": "string", "value": "string" } - ], - "workspace_name": "string" + ] } ``` ### Parameters -| Name | In | Type | Required | Description | -| -------------- | ---- | ---------------------------------------------------------------------------------------------------- | -------- | ------------------------------- | -| `organization` | path | string(uuid) | true | Organization ID | -| `body` | body | [codersdk.CreateTemplateVersionDryRunRequest](schemas.md#codersdkcreatetemplateversiondryrunrequest) | true | Create template version request | +| Name | In | Type | Required | Description | +| -------------- | ---- | ---------------------------------------------------------------------------------------- | -------- | ------------------------------- | +| `organization` | path | string(uuid) | true | Organization ID | +| `body` | body | [codersdk.CreateTemplateVersionRequest](schemas.md#codersdkcreatetemplateversionrequest) | true | Create template version request | ### Example responses diff --git a/site/src/pages/TemplateVariablesPage/TemplateVariablesForm.tsx b/site/src/pages/TemplateVariablesPage/TemplateVariablesForm.tsx index de9fd60b2c817..f2c41ea5e60a3 100644 --- a/site/src/pages/TemplateVariablesPage/TemplateVariablesForm.tsx +++ b/site/src/pages/TemplateVariablesPage/TemplateVariablesForm.tsx @@ -49,8 +49,8 @@ export const TemplateVariablesForm: FC = ({ template_id: templateVersion.template_id, provisioner: "terraform", storage_method: "file", - tags: {}, - // FIXME file_id: null, + tags: templateVersion.job.tags, + file_id: templateVersion.job.file_id, user_variable_values: initialUserVariableValues, }, validationSchema: Yup.object({ @@ -144,7 +144,7 @@ export const selectInitialUserVariableValues = ( return defaults } -export const ValidationSchemaForTemplateVariables = ( +const ValidationSchemaForTemplateVariables = ( ns: string, templateVariables: TemplateVersionVariable[], ): Yup.AnySchema => { @@ -159,6 +159,11 @@ export const ValidationSchemaForTemplateVariables = ( const templateVariable = templateVariables.find( (variable) => variable.name === name, ) + if (templateVariable && templateVariable.sensitive) { + // It's possible that the secret is already stored in database, + // so we can't properly verify the "required" condition. + return true + } if (templateVariable && templateVariable.required) { if (!val || val.length === 0) { return ctx.createError({ diff --git a/site/src/pages/TemplateVariablesPage/TemplateVariablesPage.tsx b/site/src/pages/TemplateVariablesPage/TemplateVariablesPage.tsx index 59c75bec55895..b21639d7610da 100644 --- a/site/src/pages/TemplateVariablesPage/TemplateVariablesPage.tsx +++ b/site/src/pages/TemplateVariablesPage/TemplateVariablesPage.tsx @@ -1,4 +1,9 @@ import { useMachine } from "@xstate/react" +import { + CreateTemplateVersionRequest, + TemplateVersionVariable, + VariableValue, +} from "api/typesGenerated" import { useOrganizationId } from "hooks/useOrganizationId" import { FC } from "react" import { Helmet } from "react-helmet-async" @@ -56,11 +61,47 @@ export const TemplateVariablesPage: FC = () => { navigate(`/templates/${templateName}`) }} onSubmit={(formData) => { - send({ type: "UPDATE_TEMPLATE_EVENT", request: formData }) + const request = filterEmptySensitiveVariables( + formData, + templateVariables, + ) + send({ type: "UPDATE_TEMPLATE_EVENT", request: request }) }} /> ) } +const filterEmptySensitiveVariables = ( + request: CreateTemplateVersionRequest, + templateVariables?: TemplateVersionVariable[], +): CreateTemplateVersionRequest => { + const filtered: VariableValue[] = [] + + if (!templateVariables) { + return request + } + + if (request.user_variable_values) { + request.user_variable_values.forEach((variableValue) => { + const templateVariable = templateVariables.find( + (t) => t.name === variableValue.name, + ) + if ( + templateVariable && + templateVariable.sensitive && + variableValue.value === "" + ) { + return + } + filtered.push(variableValue) + }) + } + + return { + ...request, + user_variable_values: filtered, + } +} + export default TemplateVariablesPage diff --git a/site/src/pages/TemplateVariablesPage/TemplateVariablesPageView.tsx b/site/src/pages/TemplateVariablesPage/TemplateVariablesPageView.tsx index f66f585da19c2..aa521d156f021 100644 --- a/site/src/pages/TemplateVariablesPage/TemplateVariablesPageView.tsx +++ b/site/src/pages/TemplateVariablesPage/TemplateVariablesPageView.tsx @@ -56,12 +56,18 @@ export const TemplateVariablesPageView: FC = ({ )} {Boolean(errors.getActiveTemplateVersionError) && ( - + )} {Boolean(errors.getTemplateVariablesError) && ( - + )} {isLoading && } diff --git a/site/src/xServices/template/templateVariablesXService.ts b/site/src/xServices/template/templateVariablesXService.ts index e840072fee703..7aa82391c0658 100644 --- a/site/src/xServices/template/templateVariablesXService.ts +++ b/site/src/xServices/template/templateVariablesXService.ts @@ -155,7 +155,9 @@ export const templateVariablesMachine = createMachine( } return getTemplateVersionVariables(template.active_version_id) }, - updateTemplate: (context) => {}, + updateTemplate: (context) => { + console.log(context.createTemplateVersionRequest) + }, }, actions: { assignTemplate: assign({ From 1064122996e956b4f3d8a3ee7c5e3b6d0f26d2c4 Mon Sep 17 00:00:00 2001 From: Marcin Tojek Date: Wed, 1 Mar 2023 13:55:10 +0100 Subject: [PATCH 14/25] Created --- .../template/templateVariablesXService.ts | 91 +++++++++++++++++-- 1 file changed, 84 insertions(+), 7 deletions(-) diff --git a/site/src/xServices/template/templateVariablesXService.ts b/site/src/xServices/template/templateVariablesXService.ts index 7aa82391c0658..dc483a5f233eb 100644 --- a/site/src/xServices/template/templateVariablesXService.ts +++ b/site/src/xServices/template/templateVariablesXService.ts @@ -1,7 +1,9 @@ import { + createTemplateVersion, getTemplateByName, getTemplateVersion, getTemplateVersionVariables, + updateActiveTemplateVersion, } from "api/api" import { CreateTemplateVersionRequest, @@ -10,6 +12,9 @@ import { TemplateVersionVariable, } from "api/typesGenerated" import { assign, createMachine } from "xstate" +import { delay } from "util/delay" +import { template } from "lodash" +import { Message } from "api/types" type TemplateVariablesContext = { organizationId: string @@ -20,6 +25,7 @@ type TemplateVariablesContext = { templateVariables?: TemplateVersionVariable[] createTemplateVersionRequest?: CreateTemplateVersionRequest + newTemplateVersion?: TemplateVersion getTemplateError?: Error | unknown getActiveTemplateVersionError?: Error | unknown @@ -29,7 +35,7 @@ type TemplateVariablesContext = { type UpdateTemplateEvent = { type: "UPDATE_TEMPLATE_EVENT" - request: CreateTemplateVersionRequest // FIXME + request: CreateTemplateVersionRequest } export const templateVariablesMachine = createMachine( @@ -50,8 +56,14 @@ export const templateVariablesMachine = createMachine( getTemplateVariables: { data: TemplateVersionVariable[] } + createNewTemplateVersion: { + data: TemplateVersion + } + waitForJobToBeCompleted: { + data: TemplateVersion + } updateTemplate: { - data: CreateTemplateVersionRequest + data: Message } }, }, @@ -109,17 +121,47 @@ export const templateVariablesMachine = createMachine( on: { UPDATE_TEMPLATE_EVENT: { actions: ["assignCreateTemplateVersionRequest"], - target: "updatingTemplate", + target: "creatingTemplateVersion", }, }, }, - updatingTemplate: { + creatingTemplateVersion: { entry: "clearUpdateTemplateError", + invoke: { + src: "createNewTemplateVersion", + onDone: { + actions: ["assignNewTemplateVersion"], + target: "waitingForJobToBeCompleted", + }, + onError: { + actions: ["assignUpdateTemplateError"], + target: "fillingParams", + }, + }, + tags: ["submitting"], + }, + waitingForJobToBeCompleted: { + invoke: { + src: "waitForJobToBeCompleted", + onDone: [ + { + actions: ["assignNewTemplateVersion"], + target: "updatingTemplate", + }, + ], + onError: { + actions: ["assignUpdateTemplateError"], + target: "fillingParams", + }, + }, + tags: ["submitting"], + }, + updatingTemplate: { invoke: { src: "updateTemplate", onDone: { - actions: ["onUpdateTemplate"], target: "updated", + actions: ["onUpdateTemplate"], }, onError: { actions: ["assignUpdateTemplateError"], @@ -155,8 +197,40 @@ export const templateVariablesMachine = createMachine( } return getTemplateVersionVariables(template.active_version_id) }, - updateTemplate: (context) => { - console.log(context.createTemplateVersionRequest) + createNewTemplateVersion: (context) => { + if (!context.createTemplateVersionRequest) { + throw new Error("Missing request body") + } + return createTemplateVersion( + context.organizationId, + context.createTemplateVersionRequest, + ) + }, + waitForJobToBeCompleted: async ({ newTemplateVersion }) => { + if (!newTemplateVersion) { + throw new Error("Template version is undefined") + } + + let status = newTemplateVersion.job.status + while (["pending", "running"].includes(status)) { + newTemplateVersion = await getTemplateVersion(newTemplateVersion.id) + status = newTemplateVersion.job.status + await delay(2_000) + } + return newTemplateVersion + }, + updateTemplate: async ({ template, newTemplateVersion }) => { + if (!template) { + throw new Error("No template selected") + } + + if (!newTemplateVersion) { + throw new Error("New template version is undefined") + } + + return updateActiveTemplateVersion(template.id, { + id: newTemplateVersion.id, + }) }, }, actions: { @@ -172,6 +246,9 @@ export const templateVariablesMachine = createMachine( assignCreateTemplateVersionRequest: assign({ createTemplateVersionRequest: (_, event) => event.request, }), + assignNewTemplateVersion: assign({ + newTemplateVersion: (_, event) => event.data, + }), assignGetTemplateError: assign({ getTemplateError: (_, event) => event.data, }), From 4e4b9354a532c4f4ab1a11c43af51f4d5cee811f Mon Sep 17 00:00:00 2001 From: Marcin Tojek Date: Wed, 1 Mar 2023 14:06:03 +0100 Subject: [PATCH 15/25] Fixes --- .../template/templateVariablesXService.ts | 23 +++++++++---------- 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/site/src/xServices/template/templateVariablesXService.ts b/site/src/xServices/template/templateVariablesXService.ts index dc483a5f233eb..3b29d21e0099c 100644 --- a/site/src/xServices/template/templateVariablesXService.ts +++ b/site/src/xServices/template/templateVariablesXService.ts @@ -13,7 +13,6 @@ import { } from "api/typesGenerated" import { assign, createMachine } from "xstate" import { delay } from "util/delay" -import { template } from "lodash" import { Message } from "api/types" type TemplateVariablesContext = { @@ -179,31 +178,31 @@ export const templateVariablesMachine = createMachine( }, { services: { - getTemplate: (context) => { - const { organizationId, templateName } = context + getTemplate: ({ organizationId, templateName }) => { return getTemplateByName(organizationId, templateName) }, - getActiveTemplateVersion: (context) => { - const { template } = context + getActiveTemplateVersion: ({ template }) => { if (!template) { throw new Error("No template selected") } return getTemplateVersion(template.active_version_id) }, - getTemplateVariables: (context) => { - const { template } = context + getTemplateVariables: ({ template }) => { if (!template) { throw new Error("No template selected") } return getTemplateVersionVariables(template.active_version_id) }, - createNewTemplateVersion: (context) => { - if (!context.createTemplateVersionRequest) { + createNewTemplateVersion: ({ + organizationId, + createTemplateVersionRequest, + }) => { + if (!createTemplateVersionRequest) { throw new Error("Missing request body") } return createTemplateVersion( - context.organizationId, - context.createTemplateVersionRequest, + organizationId, + createTemplateVersionRequest, ) }, waitForJobToBeCompleted: async ({ newTemplateVersion }) => { @@ -219,7 +218,7 @@ export const templateVariablesMachine = createMachine( } return newTemplateVersion }, - updateTemplate: async ({ template, newTemplateVersion }) => { + updateTemplate: ({ template, newTemplateVersion }) => { if (!template) { throw new Error("No template selected") } From b8b295a98886c0af1ca3f7e21619314b2cf40586 Mon Sep 17 00:00:00 2001 From: Marcin Tojek Date: Wed, 1 Mar 2023 14:28:28 +0100 Subject: [PATCH 16/25] Fixes --- site/src/components/TemplateLayout/TemplatePageHeader.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/site/src/components/TemplateLayout/TemplatePageHeader.tsx b/site/src/components/TemplateLayout/TemplatePageHeader.tsx index 393dded7dbde3..5076cded7aebb 100644 --- a/site/src/components/TemplateLayout/TemplatePageHeader.tsx +++ b/site/src/components/TemplateLayout/TemplatePageHeader.tsx @@ -101,7 +101,7 @@ export const TemplatePageHeader: FC = ({ onClick={deleteTemplate.openDeleteConfirmation} /> - 0}> + 0}> From e922349bbebab0556e953370a33e7d7fd1651488 Mon Sep 17 00:00:00 2001 From: Marcin Tojek Date: Wed, 1 Mar 2023 14:32:44 +0100 Subject: [PATCH 17/25] Fix: fmt --- site/src/components/TemplateLayout/TemplatePageHeader.tsx | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/site/src/components/TemplateLayout/TemplatePageHeader.tsx b/site/src/components/TemplateLayout/TemplatePageHeader.tsx index 5076cded7aebb..2ae3dd7e5868c 100644 --- a/site/src/components/TemplateLayout/TemplatePageHeader.tsx +++ b/site/src/components/TemplateLayout/TemplatePageHeader.tsx @@ -101,7 +101,12 @@ export const TemplatePageHeader: FC = ({ onClick={deleteTemplate.openDeleteConfirmation} /> - 0}> + 0 + } + > From 1798c9ed5a7efb9b3292c515a979482e1e473180 Mon Sep 17 00:00:00 2001 From: Marcin Tojek Date: Wed, 1 Mar 2023 14:36:03 +0100 Subject: [PATCH 18/25] storybook: WithVariables --- .../TemplateLayout/TemplatePageHeader.stories.tsx | 10 +++++++++- site/src/testHelpers/entities.ts | 10 ++++++++++ 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/site/src/components/TemplateLayout/TemplatePageHeader.stories.tsx b/site/src/components/TemplateLayout/TemplatePageHeader.stories.tsx index 07d5334961ea6..80590577990ab 100644 --- a/site/src/components/TemplateLayout/TemplatePageHeader.stories.tsx +++ b/site/src/components/TemplateLayout/TemplatePageHeader.stories.tsx @@ -1,5 +1,8 @@ import { ComponentMeta, Story } from "@storybook/react" -import { MockTemplate } from "testHelpers/entities" +import { + MockTemplate, + MockTemplateVersionVariable1, +} from "testHelpers/entities" import { TemplatePageHeader, TemplatePageHeaderProps, @@ -33,3 +36,8 @@ CanNotUpdate.args = { canUpdateTemplate: false, }, } + +export const WithVariables = Template.bind({}) +WithVariables.args = { + templateVersionVariables: [MockTemplateVersionVariable1], +} diff --git a/site/src/testHelpers/entities.ts b/site/src/testHelpers/entities.ts index 8881682f94d38..0f1ca8c0feb3e 100644 --- a/site/src/testHelpers/entities.ts +++ b/site/src/testHelpers/entities.ts @@ -787,6 +787,16 @@ export const MockTemplateVersionParameter5: TypesGen.TemplateVersionParameter = validation_monotonic: "decreasing", } +export const MockTemplateVersionVariable1: TypesGen.TemplateVersionVariable = { + name: "first_variable", + description: "This is first variable", + type: "string", + value: "", + default_value: "abc", + required: false, + sensitive: false, +} + // requests the MockWorkspace export const MockWorkspaceRequest: TypesGen.CreateWorkspaceRequest = { name: "test", From de0e48d123da28dcc97989643f2ef2866551dbf1 Mon Sep 17 00:00:00 2001 From: Marcin Tojek Date: Wed, 1 Mar 2023 15:48:32 +0100 Subject: [PATCH 19/25] Storybook --- .../TemplateSettingsPageView.stories.tsx | 2 +- .../TemplateVariablesForm.tsx | 11 ++- .../TemplateVariablesPageView.stories.tsx | 78 +++++++++++++++++++ .../TemplateVariablesPageView.tsx | 9 ++- site/src/testHelpers/entities.ts | 42 +++++++++- 5 files changed, 137 insertions(+), 5 deletions(-) create mode 100644 site/src/pages/TemplateVariablesPage/TemplateVariablesPageView.stories.tsx diff --git a/site/src/pages/TemplateSettingsPage/TemplateSettingsPageView.stories.tsx b/site/src/pages/TemplateSettingsPage/TemplateSettingsPageView.stories.tsx index bd1baeca97622..0f17b2ff20bf6 100644 --- a/site/src/pages/TemplateSettingsPage/TemplateSettingsPageView.stories.tsx +++ b/site/src/pages/TemplateSettingsPage/TemplateSettingsPageView.stories.tsx @@ -51,7 +51,7 @@ SaveTemplateSettingsError.args = { }), }, initialTouched: { - name: true, + allow_user_cancel_workspace_jobs: true, }, onSubmit: action("onSubmit"), onCancel: action("cancel"), diff --git a/site/src/pages/TemplateVariablesPage/TemplateVariablesForm.tsx b/site/src/pages/TemplateVariablesPage/TemplateVariablesForm.tsx index f2c41ea5e60a3..cbea09ee748f5 100644 --- a/site/src/pages/TemplateVariablesPage/TemplateVariablesForm.tsx +++ b/site/src/pages/TemplateVariablesPage/TemplateVariablesForm.tsx @@ -59,7 +59,7 @@ export const TemplateVariablesForm: FC = ({ templateVariables, ), }), - onSubmit: onSubmit, + onSubmit, initialTouched, }) const getFieldHelpers = getFormHelpers( @@ -120,6 +120,15 @@ export const selectInitialUserVariableValues = ( ): VariableValue[] => { const defaults: VariableValue[] = [] templateVariables.forEach((templateVariable) => { + // Boolean variables must be always either "true" or "false" + if (templateVariable.type === "bool" && templateVariable.value === "") { + defaults.push({ + name: templateVariable.name, + value: templateVariable.default_value, + }) + return + } + if (templateVariable.sensitive) { defaults.push({ name: templateVariable.name, diff --git a/site/src/pages/TemplateVariablesPage/TemplateVariablesPageView.stories.tsx b/site/src/pages/TemplateVariablesPage/TemplateVariablesPageView.stories.tsx new file mode 100644 index 0000000000000..00fa2aff55d4c --- /dev/null +++ b/site/src/pages/TemplateVariablesPage/TemplateVariablesPageView.stories.tsx @@ -0,0 +1,78 @@ +import { action } from "@storybook/addon-actions" +import { Story } from "@storybook/react" +import { + makeMockApiError, + MockTemplateVersion, + MockTemplateVersionVariable1, + MockTemplateVersionVariable2, + MockTemplateVersionVariable3, + MockTemplateVersionVariable4, + MockTemplateVersionVariable5, +} from "testHelpers/entities" +import { + TemplateVariablesPageView, + TemplateVariablesPageViewProps, +} from "./TemplateVariablesPageView" + +export default { + title: "pages/TemplateVariablesPageView", + component: TemplateVariablesPageView, +} + +const TemplateVariables: Story = (args) => ( + +) + +export const Loading = TemplateVariables.bind({}) +Loading.args = { + onSubmit: action("onSubmit"), + onCancel: action("cancel"), +} + +export const Basic = TemplateVariables.bind({}) +Basic.args = { + templateVersion: MockTemplateVersion, + templateVariables: [ + MockTemplateVersionVariable1, + MockTemplateVersionVariable2, + MockTemplateVersionVariable3, + MockTemplateVersionVariable4, + ], + onSubmit: action("onSubmit"), + onCancel: action("cancel"), +} + +// This example isn't fully supported. As "user_variable_values" is an array, +// FormikTouched can't properly handle this. +// See: https://github.com/jaredpalmer/formik/issues/2022 +export const RequiredVariable = TemplateVariables.bind({}) +RequiredVariable.args = { + templateVersion: MockTemplateVersion, + templateVariables: [ + MockTemplateVersionVariable4, + MockTemplateVersionVariable5, + ], + onSubmit: action("onSubmit"), + onCancel: action("cancel"), + initialTouched: { + user_variable_values: true, + }, +} + +export const WithUpdateTemplateError = TemplateVariables.bind({}) +WithUpdateTemplateError.args = { + templateVersion: MockTemplateVersion, + templateVariables: [ + MockTemplateVersionVariable1, + MockTemplateVersionVariable2, + MockTemplateVersionVariable3, + MockTemplateVersionVariable4, + ], + errors: { + updateTemplateError: makeMockApiError({ + message: "Something went wrong.", + }), + }, + onSubmit: action("onSubmit"), + onCancel: action("cancel"), +} diff --git a/site/src/pages/TemplateVariablesPage/TemplateVariablesPageView.tsx b/site/src/pages/TemplateVariablesPage/TemplateVariablesPageView.tsx index aa521d156f021..27cff41434933 100644 --- a/site/src/pages/TemplateVariablesPage/TemplateVariablesPageView.tsx +++ b/site/src/pages/TemplateVariablesPage/TemplateVariablesPageView.tsx @@ -43,10 +43,10 @@ export const TemplateVariablesPageView: FC = ({ !templateVersion && !templateVariables && !errors.getTemplateError && - !errors.getTemplateVariablesError + !errors.getTemplateVariablesError && + !errors.updateTemplateError const { t } = useTranslation("templateVariablesPage") - // TODO stack alert banners return ( {Boolean(errors.getTemplateError) && ( @@ -70,6 +70,11 @@ export const TemplateVariablesPageView: FC = ({ /> )} + {Boolean(errors.updateTemplateError) && ( + + + + )} {isLoading && } {templateVersion && templateVariables && ( Date: Wed, 1 Mar 2023 17:46:37 +0100 Subject: [PATCH 20/25] unit tests --- .../TemplateVariablesPage.test.tsx | 177 ++++++++++++++++++ 1 file changed, 177 insertions(+) create mode 100644 site/src/pages/TemplateVariablesPage/TemplateVariablesPage.test.tsx diff --git a/site/src/pages/TemplateVariablesPage/TemplateVariablesPage.test.tsx b/site/src/pages/TemplateVariablesPage/TemplateVariablesPage.test.tsx new file mode 100644 index 0000000000000..962eacea7bf5c --- /dev/null +++ b/site/src/pages/TemplateVariablesPage/TemplateVariablesPage.test.tsx @@ -0,0 +1,177 @@ +import { screen, waitFor } from "@testing-library/react" +import userEvent from "@testing-library/user-event" +import { + history, + MockTemplate, + MockTemplateVersion2, + MockTemplateVersion, + MockTemplateVersionVariable1, + MockTemplateVersionVariable2, + renderWithAuth, + MockTemplateVersionVariable5, +} from "testHelpers/renderHelpers" +import * as API from "api/api" +import i18next from "i18next" +import TemplateVariablesPage from "./TemplateVariablesPage" +import { Language as FooterFormLanguage } from "components/FormFooter/FormFooter" +import { Route } from "react-router-dom" +import * as router from "react-router" + +const navigate = jest.fn() + +const { t } = i18next + +const validFormValues = { + first_variable: "Hello world", + second_variable: "123", +} + +const pageTitleText = t("title", { ns: "templateVariablesPage" }) + +const validationRequiredField = t("validationRequiredVariable", { + ns: "templateVariablesPage", +}) + +const renderTemplateVariablesPage = () => { + return renderWithAuth(, { + route: `/templates/${MockTemplate.name}/variables`, + path: `/templates/:template/variables`, + routes: ( + }> + ), + }) +} + +describe("TemplateVariablesPage", () => { + it("renders with variables", async () => { + jest.spyOn(API, "getTemplateByName").mockResolvedValueOnce(MockTemplate) + jest + .spyOn(API, "getTemplateVersion") + .mockResolvedValueOnce(MockTemplateVersion) + jest + .spyOn(API, "getTemplateVersionVariables") + .mockResolvedValueOnce([ + MockTemplateVersionVariable1, + MockTemplateVersionVariable2, + ]) + + renderTemplateVariablesPage() + + const element = await screen.findByText(pageTitleText) + expect(element).toBeDefined() + + const firstVariable = await screen.findByLabelText( + MockTemplateVersionVariable1.name, + ) + expect(firstVariable).toBeDefined() + + const secondVariable = await screen.findByLabelText( + MockTemplateVersionVariable2.name, + ) + expect(secondVariable).toBeDefined() + }) + + it("user submits the form successfully", async () => { + jest.spyOn(API, "getTemplateByName").mockResolvedValueOnce(MockTemplate) + jest + .spyOn(API, "getTemplateVersion") + .mockResolvedValueOnce(MockTemplateVersion) + jest + .spyOn(API, "getTemplateVersionVariables") + .mockResolvedValueOnce([ + MockTemplateVersionVariable1, + MockTemplateVersionVariable2, + ]) + jest + .spyOn(API, "createTemplateVersion") + .mockResolvedValueOnce(MockTemplateVersion2) + jest.spyOn(API, "updateActiveTemplateVersion").mockResolvedValueOnce({ + message: "done", + }) + jest.spyOn(router, "useNavigate").mockImplementation(() => navigate) + + renderTemplateVariablesPage() + + const element = await screen.findByText(pageTitleText) + expect(element).toBeDefined() + + const firstVariable = await screen.findByLabelText( + MockTemplateVersionVariable1.name, + ) + expect(firstVariable).toBeDefined() + + const secondVariable = await screen.findByLabelText( + MockTemplateVersionVariable2.name, + ) + expect(secondVariable).toBeDefined() + + // Fill the form + const firstVariableField = await screen.findByLabelText( + MockTemplateVersionVariable1.name, + ) + await userEvent.clear(firstVariableField) + await userEvent.type(firstVariableField, validFormValues.first_variable) + + const secondVariableField = await screen.findByLabelText( + MockTemplateVersionVariable2.name, + ) + await userEvent.clear(secondVariableField) + await userEvent.type(secondVariableField, validFormValues.second_variable) + + // Submit the form + const submitButton = await screen.findByText( + FooterFormLanguage.defaultSubmitLabel, + ) + await userEvent.click(submitButton) + + // Wait for redirect + await waitFor(() => + expect(navigate).toHaveBeenCalledWith(`/templates/${MockTemplate.name}`), + ) + }) + + it("user forgets to fill the required field", async () => { + jest.spyOn(API, "getTemplateByName").mockResolvedValueOnce(MockTemplate) + jest + .spyOn(API, "getTemplateVersion") + .mockResolvedValueOnce(MockTemplateVersion) + jest + .spyOn(API, "getTemplateVersionVariables") + .mockResolvedValueOnce([ + MockTemplateVersionVariable1, + MockTemplateVersionVariable5, + ]) + jest + .spyOn(API, "createTemplateVersion") + .mockResolvedValueOnce(MockTemplateVersion2) + jest.spyOn(API, "updateActiveTemplateVersion").mockResolvedValueOnce({ + message: "done", + }) + jest.spyOn(router, "useNavigate").mockImplementation(() => navigate) + + renderTemplateVariablesPage() + + const element = await screen.findByText(pageTitleText) + expect(element).toBeDefined() + + const firstVariable = await screen.findByLabelText( + MockTemplateVersionVariable1.name, + ) + expect(firstVariable).toBeDefined() + + const fifthVariable = await screen.findByLabelText( + MockTemplateVersionVariable5.name, + ) + expect(fifthVariable).toBeDefined() + + // Submit the form + const submitButton = await screen.findByText( + FooterFormLanguage.defaultSubmitLabel, + ) + await userEvent.click(submitButton) + + // Check validation error + const validationError = await screen.findByText(validationRequiredField) + expect(validationError).toBeDefined() + }) +}) From 5cbd670ad13e340b37d5bcb0a4defdf4ff6f1208 Mon Sep 17 00:00:00 2001 From: Marcin Tojek Date: Wed, 1 Mar 2023 17:52:24 +0100 Subject: [PATCH 21/25] Fix: lint --- .../pages/TemplateVariablesPage/TemplateVariablesPage.test.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/site/src/pages/TemplateVariablesPage/TemplateVariablesPage.test.tsx b/site/src/pages/TemplateVariablesPage/TemplateVariablesPage.test.tsx index 962eacea7bf5c..d3b88a55f5834 100644 --- a/site/src/pages/TemplateVariablesPage/TemplateVariablesPage.test.tsx +++ b/site/src/pages/TemplateVariablesPage/TemplateVariablesPage.test.tsx @@ -1,7 +1,6 @@ import { screen, waitFor } from "@testing-library/react" import userEvent from "@testing-library/user-event" import { - history, MockTemplate, MockTemplateVersion2, MockTemplateVersion, From 380e3e142722ad25d290c4d0c81e304b6ac6ec8a Mon Sep 17 00:00:00 2001 From: Marcin Tojek Date: Thu, 2 Mar 2023 13:08:15 +0100 Subject: [PATCH 22/25] Use getTemplateDataError --- .../TemplateVariablesPage.tsx | 8 +--- .../TemplateVariablesPageView.tsx | 27 ++----------- .../template/templateVariablesXService.ts | 38 ++++++------------- 3 files changed, 18 insertions(+), 55 deletions(-) diff --git a/site/src/pages/TemplateVariablesPage/TemplateVariablesPage.tsx b/site/src/pages/TemplateVariablesPage/TemplateVariablesPage.tsx index b21639d7610da..5a3e94e0af978 100644 --- a/site/src/pages/TemplateVariablesPage/TemplateVariablesPage.tsx +++ b/site/src/pages/TemplateVariablesPage/TemplateVariablesPage.tsx @@ -34,9 +34,7 @@ export const TemplateVariablesPage: FC = () => { const { activeTemplateVersion, templateVariables, - getTemplateError, - getActiveTemplateVersionError, - getTemplateVariablesError, + getTemplateDataError, updateTemplateError, } = state.context @@ -52,9 +50,7 @@ export const TemplateVariablesPage: FC = () => { templateVersion={activeTemplateVersion} templateVariables={templateVariables} errors={{ - getTemplateError, - getActiveTemplateVersionError, - getTemplateVariablesError, + getTemplateDataError, updateTemplateError, }} onCancel={() => { diff --git a/site/src/pages/TemplateVariablesPage/TemplateVariablesPageView.tsx b/site/src/pages/TemplateVariablesPage/TemplateVariablesPageView.tsx index 27cff41434933..8bd8bdf2b6978 100644 --- a/site/src/pages/TemplateVariablesPage/TemplateVariablesPageView.tsx +++ b/site/src/pages/TemplateVariablesPage/TemplateVariablesPageView.tsx @@ -19,9 +19,7 @@ export interface TemplateVariablesPageViewProps { onCancel: () => void isSubmitting: boolean errors?: { - getTemplateError?: unknown - getActiveTemplateVersionError?: unknown - getTemplateVariablesError?: unknown + getTemplateDataError?: unknown updateTemplateError?: unknown } initialTouched?: ComponentProps< @@ -42,32 +40,15 @@ export const TemplateVariablesPageView: FC = ({ const isLoading = !templateVersion && !templateVariables && - !errors.getTemplateError && - !errors.getTemplateVariablesError && + !errors.getTemplateDataError && !errors.updateTemplateError const { t } = useTranslation("templateVariablesPage") return ( - {Boolean(errors.getTemplateError) && ( + {Boolean(errors.getTemplateDataError) && ( - - - )} - {Boolean(errors.getActiveTemplateVersionError) && ( - - - - )} - {Boolean(errors.getTemplateVariablesError) && ( - - + )} {Boolean(errors.updateTemplateError) && ( diff --git a/site/src/xServices/template/templateVariablesXService.ts b/site/src/xServices/template/templateVariablesXService.ts index 3b29d21e0099c..898670c8bf5b5 100644 --- a/site/src/xServices/template/templateVariablesXService.ts +++ b/site/src/xServices/template/templateVariablesXService.ts @@ -26,9 +26,7 @@ type TemplateVariablesContext = { createTemplateVersionRequest?: CreateTemplateVersionRequest newTemplateVersion?: TemplateVersion - getTemplateError?: Error | unknown - getActiveTemplateVersionError?: Error | unknown - getTemplateVariablesError?: Error | unknown + getTemplateDataError?: Error | unknown updateTemplateError?: Error | unknown } @@ -69,7 +67,7 @@ export const templateVariablesMachine = createMachine( initial: "gettingTemplate", states: { gettingTemplate: { - entry: "clearGetTemplateError", + entry: "clearGetTemplateDataError", invoke: { src: "getTemplate", onDone: [ @@ -79,13 +77,13 @@ export const templateVariablesMachine = createMachine( }, ], onError: { - actions: ["assignGetTemplateError"], + actions: ["assignGetTemplateDataError"], target: "error", }, }, }, gettingActiveTemplateVersion: { - entry: "clearGetActiveTemplateVersionError", + entry: "clearGetTemplateDataError", invoke: { src: "getActiveTemplateVersion", onDone: [ @@ -95,13 +93,13 @@ export const templateVariablesMachine = createMachine( }, ], onError: { - actions: ["assignGetActiveTemplateVersionError"], + actions: ["assignGetTemplateDataError"], target: "error", }, }, }, gettingTemplateVariables: { - entry: "clearGetTemplateVariablesError", + entry: "clearGetTemplateDataError", invoke: { src: "getTemplateVariables", onDone: [ @@ -111,7 +109,7 @@ export const templateVariablesMachine = createMachine( }, ], onError: { - actions: ["assignGetTemplateVariablesError"], + actions: ["assignGetTemplateDataError"], target: "error", }, }, @@ -133,7 +131,7 @@ export const templateVariablesMachine = createMachine( target: "waitingForJobToBeCompleted", }, onError: { - actions: ["assignUpdateTemplateError"], + actions: ["assignGetTemplateDataError"], target: "fillingParams", }, }, @@ -248,23 +246,11 @@ export const templateVariablesMachine = createMachine( assignNewTemplateVersion: assign({ newTemplateVersion: (_, event) => event.data, }), - assignGetTemplateError: assign({ - getTemplateError: (_, event) => event.data, - }), - clearGetTemplateError: assign({ - getTemplateError: (_) => undefined, - }), - assignGetTemplateVariablesError: assign({ - getTemplateVariablesError: (_, event) => event.data, - }), - clearGetTemplateVariablesError: assign({ - getTemplateVariablesError: (_) => undefined, - }), - assignGetActiveTemplateVersionError: assign({ - getActiveTemplateVersionError: (_, event) => event.data, + assignGetTemplateDataError: assign({ + getTemplateDataError: (_, event) => event.data, }), - clearGetActiveTemplateVersionError: assign({ - getActiveTemplateVersionError: (_) => undefined, + clearGetTemplateDataError: assign({ + getTemplateDataError: (_) => undefined, }), assignUpdateTemplateError: assign({ updateTemplateError: (_, event) => event.data, From 6814d9f5054eccf269374edb71f3ec5ad84316b3 Mon Sep 17 00:00:00 2001 From: Marcin Tojek Date: Thu, 2 Mar 2023 13:18:00 +0100 Subject: [PATCH 23/25] Always show Variables button --- .../TemplateLayout/TemplateLayout.tsx | 9 ++--- .../TemplatePageHeader.stories.tsx | 10 +----- .../TemplateLayout/TemplatePageHeader.tsx | 11 +----- .../TemplateSummaryPage.tsx | 8 +---- .../xServices/template/templateXService.ts | 35 ------------------- 5 files changed, 5 insertions(+), 68 deletions(-) diff --git a/site/src/components/TemplateLayout/TemplateLayout.tsx b/site/src/components/TemplateLayout/TemplateLayout.tsx index 734e34a2c6f2c..4716698baa392 100644 --- a/site/src/components/TemplateLayout/TemplateLayout.tsx +++ b/site/src/components/TemplateLayout/TemplateLayout.tsx @@ -57,14 +57,10 @@ export const TemplateLayout: FC<{ children?: JSX.Element }> = ({ organizationId, }, }) - const { - template, - permissions: templatePermissions, - templateVersionVariables, - } = templateState.context + const { template, permissions: templatePermissions } = templateState.context const permissions = usePermissions() - if (!template || !templatePermissions || !templateVersionVariables) { + if (!template || !templatePermissions) { return } @@ -73,7 +69,6 @@ export const TemplateLayout: FC<{ children?: JSX.Element }> = ({ { navigate("/templates") }} diff --git a/site/src/components/TemplateLayout/TemplatePageHeader.stories.tsx b/site/src/components/TemplateLayout/TemplatePageHeader.stories.tsx index 80590577990ab..07d5334961ea6 100644 --- a/site/src/components/TemplateLayout/TemplatePageHeader.stories.tsx +++ b/site/src/components/TemplateLayout/TemplatePageHeader.stories.tsx @@ -1,8 +1,5 @@ import { ComponentMeta, Story } from "@storybook/react" -import { - MockTemplate, - MockTemplateVersionVariable1, -} from "testHelpers/entities" +import { MockTemplate } from "testHelpers/entities" import { TemplatePageHeader, TemplatePageHeaderProps, @@ -36,8 +33,3 @@ CanNotUpdate.args = { canUpdateTemplate: false, }, } - -export const WithVariables = Template.bind({}) -WithVariables.args = { - templateVersionVariables: [MockTemplateVersionVariable1], -} diff --git a/site/src/components/TemplateLayout/TemplatePageHeader.tsx b/site/src/components/TemplateLayout/TemplatePageHeader.tsx index 2ae3dd7e5868c..f02c0f0317bb1 100644 --- a/site/src/components/TemplateLayout/TemplatePageHeader.tsx +++ b/site/src/components/TemplateLayout/TemplatePageHeader.tsx @@ -78,14 +78,12 @@ const DeleteTemplateButton: FC<{ onClick: () => void }> = ({ onClick }) => ( export type TemplatePageHeaderProps = { template: Template permissions: AuthorizationResponse - templateVersionVariables: TemplateVersionVariable[] onDeleteTemplate: () => void } export const TemplatePageHeader: FC = ({ template, permissions, - templateVersionVariables, onDeleteTemplate, }) => { const hasIcon = template.icon && template.icon !== "" @@ -101,14 +99,7 @@ export const TemplatePageHeader: FC = ({ onClick={deleteTemplate.openDeleteConfirmation} /> - 0 - } - > - - + diff --git a/site/src/pages/TemplatePage/TemplateSummaryPage/TemplateSummaryPage.tsx b/site/src/pages/TemplatePage/TemplateSummaryPage/TemplateSummaryPage.tsx index fe41fa4373c72..42cbd7dd63ae8 100644 --- a/site/src/pages/TemplatePage/TemplateSummaryPage/TemplateSummaryPage.tsx +++ b/site/src/pages/TemplatePage/TemplateSummaryPage/TemplateSummaryPage.tsx @@ -12,16 +12,10 @@ export const TemplateSummaryPage: FC = () => { activeTemplateVersion, templateResources, templateVersions, - templateVersionVariables, templateDAUs, } = context - if ( - !template || - !activeTemplateVersion || - !templateResources || - !templateVersionVariables - ) { + if (!template || !activeTemplateVersion || !templateResources) { return } diff --git a/site/src/xServices/template/templateXService.ts b/site/src/xServices/template/templateXService.ts index da99c24c5d3fc..b7b836e4565ef 100644 --- a/site/src/xServices/template/templateXService.ts +++ b/site/src/xServices/template/templateXService.ts @@ -6,14 +6,12 @@ import { getTemplateVersion, getTemplateVersionResources, getTemplateVersions, - getTemplateVersionVariables, } from "api/api" import { AuthorizationResponse, Template, TemplateDAUsResponse, TemplateVersion, - TemplateVersionVariable, WorkspaceResource, } from "api/typesGenerated" @@ -23,7 +21,6 @@ export interface TemplateContext { template?: Template activeTemplateVersion?: TemplateVersion templateResources?: WorkspaceResource[] - templateVersionVariables?: TemplateVersionVariable[] templateVersions?: TemplateVersion[] templateDAUs?: TemplateDAUsResponse permissions?: AuthorizationResponse @@ -56,9 +53,6 @@ export const templateMachine = getActiveTemplateVersion: { data: TemplateVersion } - getTemplateVersionVariables: { - data: TemplateVersionVariable[] - } getTemplateResources: { data: WorkspaceResource[] } @@ -133,25 +127,6 @@ export const templateMachine = }, }, }, - templateVersionVariables: { - initial: "gettingTemplateVersionVariables", - states: { - gettingTemplateVersionVariables: { - invoke: { - src: "getTemplateVersionVariables", - onDone: [ - { - actions: "assignTemplateVersionVariables", - target: "success", - }, - ], - }, - }, - success: { - type: "final", - }, - }, - }, templateVersions: { initial: "gettingTemplateVersions", states: { @@ -245,13 +220,6 @@ export const templateMachine = return getTemplateVersion(ctx.template.active_version_id) }, - getTemplateVersionVariables: (ctx) => { - if (!ctx.template) { - throw new Error("Active template version not loaded") - } - - return getTemplateVersionVariables(ctx.template.active_version_id) - }, getTemplateResources: (ctx) => { if (!ctx.template) { throw new Error("Template not loaded") @@ -297,9 +265,6 @@ export const templateMachine = assignTemplateVersions: assign({ templateVersions: (_, event) => event.data, }), - assignTemplateVersionVariables: assign({ - templateVersionVariables: (_, event) => event.data, - }), assignTemplateDAUs: assign({ templateDAUs: (_, event) => event.data, }), From 507bf8e7acd1dc6dcd87238c8f30eae61ce58ff7 Mon Sep 17 00:00:00 2001 From: Marcin Tojek Date: Thu, 2 Mar 2023 13:20:35 +0100 Subject: [PATCH 24/25] Fix: test --- .../TemplateSummaryPage.test.tsx | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/site/src/pages/TemplatePage/TemplateSummaryPage/TemplateSummaryPage.test.tsx b/site/src/pages/TemplatePage/TemplateSummaryPage/TemplateSummaryPage.test.tsx index a022d30e0c759..bd2ab35e06ba2 100644 --- a/site/src/pages/TemplatePage/TemplateSummaryPage/TemplateSummaryPage.test.tsx +++ b/site/src/pages/TemplatePage/TemplateSummaryPage/TemplateSummaryPage.test.tsx @@ -36,15 +36,6 @@ describe("TemplateSummaryPage", () => { const mock = jest.spyOn(CreateDayString, "createDayString") mock.mockImplementation(() => "a minute ago") - server.use( - rest.get( - "/api/v2/templateversions/:templateVersion/variables", - (req, res, ctx) => { - return res(ctx.status(200), ctx.json([])) - }, - ), - ) - renderPage() await screen.findByText(MockTemplate.display_name) await screen.findByTestId("markdown") @@ -54,12 +45,6 @@ describe("TemplateSummaryPage", () => { it("does not allow a member to delete a template", () => { // get member-level permissions server.use( - rest.get( - "/api/v2/templateversions/:templateVersion/variables", - (req, res, ctx) => { - return res(ctx.status(200), ctx.json([])) - }, - ), rest.post("/api/v2/authcheck", async (req, res, ctx) => { return res(ctx.status(200), ctx.json(MockMemberPermissions)) }), From 96a813cb8dc7374f4ae280ab8f3d38940f99ecda Mon Sep 17 00:00:00 2001 From: Marcin Tojek Date: Thu, 2 Mar 2023 13:52:19 +0100 Subject: [PATCH 25/25] unusedVariablesNotice --- .../TemplateLayout/TemplatePageHeader.tsx | 6 +----- site/src/i18n/en/templateVariablesPage.json | 3 ++- .../TemplateVariablesPage.test.tsx | 16 ++++++++++++++++ .../TemplateVariablesPageView.tsx | 16 +++++++++++++++- 4 files changed, 34 insertions(+), 7 deletions(-) diff --git a/site/src/components/TemplateLayout/TemplatePageHeader.tsx b/site/src/components/TemplateLayout/TemplatePageHeader.tsx index f02c0f0317bb1..fdb43aa4875f3 100644 --- a/site/src/components/TemplateLayout/TemplatePageHeader.tsx +++ b/site/src/components/TemplateLayout/TemplatePageHeader.tsx @@ -3,11 +3,7 @@ import DeleteOutlined from "@material-ui/icons/DeleteOutlined" import AddCircleOutline from "@material-ui/icons/AddCircleOutline" import SettingsOutlined from "@material-ui/icons/SettingsOutlined" import CodeOutlined from "@material-ui/icons/CodeOutlined" -import { - AuthorizationResponse, - Template, - TemplateVersionVariable, -} from "api/typesGenerated" +import { AuthorizationResponse, Template } from "api/typesGenerated" import { Avatar } from "components/Avatar/Avatar" import { Maybe } from "components/Conditionals/Maybe" import { DeleteDialog } from "components/Dialogs/DeleteDialog/DeleteDialog" diff --git a/site/src/i18n/en/templateVariablesPage.json b/site/src/i18n/en/templateVariablesPage.json index 3e8f239fac6d4..6a885a8bde64d 100644 --- a/site/src/i18n/en/templateVariablesPage.json +++ b/site/src/i18n/en/templateVariablesPage.json @@ -1,5 +1,6 @@ { "title": "Template variables", "sensitiveVariableHelperText": "This variable is sensitive. The previous value will be used if empty.", - "validationRequiredVariable": "Variable is required." + "validationRequiredVariable": "Variable is required.", + "unusedVariablesNotice": "This template does not use managed variables." } diff --git a/site/src/pages/TemplateVariablesPage/TemplateVariablesPage.test.tsx b/site/src/pages/TemplateVariablesPage/TemplateVariablesPage.test.tsx index d3b88a55f5834..240ef507a86d2 100644 --- a/site/src/pages/TemplateVariablesPage/TemplateVariablesPage.test.tsx +++ b/site/src/pages/TemplateVariablesPage/TemplateVariablesPage.test.tsx @@ -173,4 +173,20 @@ describe("TemplateVariablesPage", () => { const validationError = await screen.findByText(validationRequiredField) expect(validationError).toBeDefined() }) + + it("no managed variables", async () => { + jest.spyOn(API, "getTemplateByName").mockResolvedValueOnce(MockTemplate) + jest + .spyOn(API, "getTemplateVersion") + .mockResolvedValueOnce(MockTemplateVersion) + jest.spyOn(API, "getTemplateVersionVariables").mockResolvedValueOnce([]) + + renderTemplateVariablesPage() + + const element = await screen.findByText(pageTitleText) + expect(element).toBeDefined() + + const goBackButton = await screen.findByText("Go back") + expect(goBackButton).toBeDefined() + }) }) diff --git a/site/src/pages/TemplateVariablesPage/TemplateVariablesPageView.tsx b/site/src/pages/TemplateVariablesPage/TemplateVariablesPageView.tsx index 8bd8bdf2b6978..9048c6bdcfe69 100644 --- a/site/src/pages/TemplateVariablesPage/TemplateVariablesPageView.tsx +++ b/site/src/pages/TemplateVariablesPage/TemplateVariablesPageView.tsx @@ -11,6 +11,7 @@ import { Stack } from "components/Stack/Stack" import { makeStyles } from "@material-ui/core/styles" import { useTranslation } from "react-i18next" import { FullPageHorizontalForm } from "components/FullPageForm/FullPageHorizontalForm" +import { GoBackButton } from "components/GoBackButton/GoBackButton" export interface TemplateVariablesPageViewProps { templateVersion?: TemplateVersion @@ -57,7 +58,7 @@ export const TemplateVariablesPageView: FC = ({ )} {isLoading && } - {templateVersion && templateVariables && ( + {templateVersion && templateVariables && templateVariables.length > 0 && ( = ({ error={errors.updateTemplateError} /> )} + {templateVariables && templateVariables.length === 0 && ( +
+ +
+ +
+
+ )}
) } @@ -76,4 +85,9 @@ const useStyles = makeStyles((theme) => ({ errorContainer: { marginBottom: theme.spacing(2), }, + goBackSection: { + display: "flex", + width: "100%", + marginTop: 32, + }, }))