From 136f23fb4ca77591b43d0f395e8afae6f3785018 Mon Sep 17 00:00:00 2001 From: Bruno Quaresma Date: Mon, 6 Mar 2023 15:36:19 -0300 Subject: [PATCH] refactor(site): Suport template version variables on template creation (#6434) --- site/jest.setup.ts | 17 + site/package.json | 1 + .../RequireAuth/RequireAuth.test.tsx | 8 +- .../CreateTemplateForm.stories.tsx | 364 +++++++++++++ .../CreateTemplatePage/CreateTemplateForm.tsx | 500 ++++++++---------- .../CreateTemplatePage.test.tsx | 122 +++++ .../CreateTemplatePage/CreateTemplatePage.tsx | 12 +- .../CreateTemplatePage/VariableInput.tsx | 137 +++++ .../CreateWorkspacePageView.stories.tsx | 37 +- .../TemplateSettingsPage.test.tsx | 38 +- .../TemplateVariablesPage.test.tsx | 5 +- site/src/testHelpers/entities.ts | 62 ++- site/src/testHelpers/renderHelpers.tsx | 62 ++- site/src/util/formUtils.ts | 1 + .../createTemplate/createTemplateXService.ts | 152 ++++-- site/yarn.lock | 69 ++- 16 files changed, 1166 insertions(+), 421 deletions(-) create mode 100644 site/src/pages/CreateTemplatePage/CreateTemplateForm.stories.tsx create mode 100644 site/src/pages/CreateTemplatePage/CreateTemplatePage.test.tsx create mode 100644 site/src/pages/CreateTemplatePage/VariableInput.tsx diff --git a/site/jest.setup.ts b/site/jest.setup.ts index d6eb950443..2a93969f6d 100644 --- a/site/jest.setup.ts +++ b/site/jest.setup.ts @@ -5,6 +5,7 @@ import { server } from "./src/testHelpers/server" import "jest-location-mock" import { TextEncoder, TextDecoder } from "util" import { Blob } from "buffer" +import { fetch, Request, Response, Headers } from "@remix-run/web-fetch" global.TextEncoder = TextEncoder // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Polyfill for jsdom @@ -12,6 +13,22 @@ global.TextDecoder = TextDecoder as any // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Polyfill for jsdom global.Blob = Blob as any +// From REMIX https://github.com/remix-run/react-router/blob/main/packages/react-router-dom/__tests__/setup.ts +if (!global.fetch) { + // Built-in lib.dom.d.ts expects `fetch(Request | string, ...)` but the web + // fetch API allows a URL so @remix-run/web-fetch defines + // `fetch(string | URL | Request, ...)` + // @ts-expect-error -- Polyfill for jsdom + global.fetch = fetch + // Same as above, lib.dom.d.ts doesn't allow a URL to the Request constructor + // @ts-expect-error -- Polyfill for jsdom + global.Request = Request + // web-std/fetch Response does not currently implement Response.error() + // @ts-expect-error -- Polyfill for jsdom + global.Response = Response + global.Headers = Headers +} + // Polyfill the getRandomValues that is used on utils/random.ts Object.defineProperty(global.self, "crypto", { value: { diff --git a/site/package.json b/site/package.json index 0460dea495..21854772d2 100644 --- a/site/package.json +++ b/site/package.json @@ -36,6 +36,7 @@ "@material-ui/icons": "4.5.1", "@material-ui/lab": "4.0.0-alpha.42", "@monaco-editor/react": "4.4.6", + "@remix-run/web-fetch": "4.3.2", "@tanstack/react-query": "4.22.4", "@testing-library/react-hooks": "8.0.1", "@types/color-convert": "2.0.0", diff --git a/site/src/components/RequireAuth/RequireAuth.test.tsx b/site/src/components/RequireAuth/RequireAuth.test.tsx index a71dd7aedc..a6abeead64 100644 --- a/site/src/components/RequireAuth/RequireAuth.test.tsx +++ b/site/src/components/RequireAuth/RequireAuth.test.tsx @@ -1,6 +1,5 @@ import { screen } from "@testing-library/react" import { rest } from "msw" -import { Route } from "react-router-dom" import { renderWithAuth } from "testHelpers/renderHelpers" import { server } from "testHelpers/server" @@ -20,7 +19,12 @@ describe("RequireAuth", () => { ) renderWithAuth(

Test

, { - routes: Setup} />, + nonAuthenticatedRoutes: [ + { + path: "setup", + element:

Setup

, + }, + ], }) await screen.findByText("Setup") diff --git a/site/src/pages/CreateTemplatePage/CreateTemplateForm.stories.tsx b/site/src/pages/CreateTemplatePage/CreateTemplateForm.stories.tsx new file mode 100644 index 0000000000..a25384e9f4 --- /dev/null +++ b/site/src/pages/CreateTemplatePage/CreateTemplateForm.stories.tsx @@ -0,0 +1,364 @@ +import { ComponentMeta, Story } from "@storybook/react" +import { + MockParameterSchemas, + MockTemplateExample, + MockTemplateVersionVariable1, + MockTemplateVersionVariable2, + MockTemplateVersionVariable3, + MockTemplateVersionVariable4, + MockTemplateVersionVariable5, +} from "testHelpers/entities" +import { + CreateTemplateForm, + CreateTemplateFormProps, +} from "./CreateTemplateForm" + +export default { + title: "components/CreateTemplateForm", + component: CreateTemplateForm, + args: { + isSubmitting: false, + }, +} as ComponentMeta + +const Template: Story = (args) => ( + +) + +export const Initial = Template.bind({}) +Initial.args = {} + +export const WithStarterTemplate = Template.bind({}) +WithStarterTemplate.args = { + starterTemplate: MockTemplateExample, +} + +export const WithParameters = Template.bind({}) +WithParameters.args = { + parameters: MockParameterSchemas, +} + +export const WithVariables = Template.bind({}) +WithVariables.args = { + variables: [ + MockTemplateVersionVariable1, + MockTemplateVersionVariable2, + MockTemplateVersionVariable3, + MockTemplateVersionVariable4, + MockTemplateVersionVariable5, + ], +} + +export const WithJobError = Template.bind({}) +WithJobError.args = { + jobError: + "template import provision for start: recv import provision: plan terraform: terraform plan: exit status 1", + logs: [ + { + id: 461061, + created_at: "2023-03-06T14:47:32.501Z", + log_source: "provisioner_daemon", + log_level: "info", + stage: "Adding README.md...", + output: "", + }, + { + id: 461062, + created_at: "2023-03-06T14:47:32.501Z", + log_source: "provisioner_daemon", + log_level: "info", + stage: "Setting up", + output: "", + }, + { + id: 461063, + created_at: "2023-03-06T14:47:32.528Z", + log_source: "provisioner_daemon", + log_level: "info", + stage: "Parsing template parameters", + output: "", + }, + { + id: 461064, + created_at: "2023-03-06T14:47:32.552Z", + log_source: "provisioner_daemon", + log_level: "info", + stage: "Detecting persistent resources", + output: "", + }, + { + id: 461065, + created_at: "2023-03-06T14:47:32.633Z", + log_source: "provisioner", + log_level: "debug", + stage: "Detecting persistent resources", + output: "", + }, + { + id: 461066, + created_at: "2023-03-06T14:47:32.633Z", + log_source: "provisioner", + log_level: "debug", + stage: "Detecting persistent resources", + output: "Initializing the backend...", + }, + { + id: 461067, + created_at: "2023-03-06T14:47:32.71Z", + log_source: "provisioner", + log_level: "debug", + stage: "Detecting persistent resources", + output: "", + }, + { + id: 461068, + created_at: "2023-03-06T14:47:32.711Z", + log_source: "provisioner", + log_level: "debug", + stage: "Detecting persistent resources", + output: "Initializing provider plugins...", + }, + { + id: 461069, + created_at: "2023-03-06T14:47:32.712Z", + log_source: "provisioner", + log_level: "debug", + stage: "Detecting persistent resources", + output: '- Finding coder/coder versions matching "~\u003e 0.6.12"...', + }, + { + id: 461070, + created_at: "2023-03-06T14:47:32.922Z", + log_source: "provisioner", + log_level: "debug", + stage: "Detecting persistent resources", + output: '- Finding hashicorp/aws versions matching "~\u003e 4.55"...', + }, + { + id: 461071, + created_at: "2023-03-06T14:47:33.132Z", + log_source: "provisioner", + log_level: "debug", + stage: "Detecting persistent resources", + output: "- Installing hashicorp/aws v4.57.0...", + }, + { + id: 461072, + created_at: "2023-03-06T14:47:37.364Z", + log_source: "provisioner", + log_level: "debug", + stage: "Detecting persistent resources", + output: "- Installed hashicorp/aws v4.57.0 (signed by HashiCorp)", + }, + { + id: 461073, + created_at: "2023-03-06T14:47:38.142Z", + log_source: "provisioner", + log_level: "debug", + stage: "Detecting persistent resources", + output: "- Installing coder/coder v0.6.15...", + }, + { + id: 461074, + created_at: "2023-03-06T14:47:39.083Z", + log_source: "provisioner", + log_level: "debug", + stage: "Detecting persistent resources", + output: + "- Installed coder/coder v0.6.15 (signed by a HashiCorp partner, key ID 93C75807601AA0EC)", + }, + { + id: 461075, + created_at: "2023-03-06T14:47:39.394Z", + log_source: "provisioner", + log_level: "debug", + stage: "Detecting persistent resources", + output: "", + }, + { + id: 461076, + created_at: "2023-03-06T14:47:39.394Z", + log_source: "provisioner", + log_level: "debug", + stage: "Detecting persistent resources", + output: "Partner and community providers are signed by their developers.", + }, + { + id: 461077, + created_at: "2023-03-06T14:47:39.394Z", + log_source: "provisioner", + log_level: "debug", + stage: "Detecting persistent resources", + output: + "If you'd like to know more about provider signing, you can read about it here:", + }, + { + id: 461078, + created_at: "2023-03-06T14:47:39.394Z", + log_source: "provisioner", + log_level: "debug", + stage: "Detecting persistent resources", + output: "https://www.terraform.io/docs/cli/plugins/signing.html", + }, + { + id: 461079, + created_at: "2023-03-06T14:47:39.394Z", + log_source: "provisioner", + log_level: "debug", + stage: "Detecting persistent resources", + output: "", + }, + { + id: 461080, + created_at: "2023-03-06T14:47:39.394Z", + log_source: "provisioner", + log_level: "debug", + stage: "Detecting persistent resources", + output: + "Terraform has created a lock file .terraform.lock.hcl to record the provider", + }, + { + id: 461081, + created_at: "2023-03-06T14:47:39.394Z", + log_source: "provisioner", + log_level: "debug", + stage: "Detecting persistent resources", + output: + "selections it made above. Include this file in your version control repository", + }, + { + id: 461082, + created_at: "2023-03-06T14:47:39.394Z", + log_source: "provisioner", + log_level: "debug", + stage: "Detecting persistent resources", + output: + "so that Terraform can guarantee to make the same selections by default when", + }, + { + id: 461083, + created_at: "2023-03-06T14:47:39.395Z", + log_source: "provisioner", + log_level: "debug", + stage: "Detecting persistent resources", + output: 'you run "terraform init" in the future.', + }, + { + id: 461084, + created_at: "2023-03-06T14:47:39.395Z", + log_source: "provisioner", + log_level: "debug", + stage: "Detecting persistent resources", + output: "", + }, + { + id: 461085, + created_at: "2023-03-06T14:47:39.395Z", + log_source: "provisioner", + log_level: "debug", + stage: "Detecting persistent resources", + output: "Terraform has been successfully initialized!", + }, + { + id: 461086, + created_at: "2023-03-06T14:47:39.395Z", + log_source: "provisioner", + log_level: "debug", + stage: "Detecting persistent resources", + output: "", + }, + { + id: 461087, + created_at: "2023-03-06T14:47:39.395Z", + log_source: "provisioner", + log_level: "debug", + stage: "Detecting persistent resources", + output: + 'You may now begin working with Terraform. Try running "terraform plan" to see', + }, + { + id: 461088, + created_at: "2023-03-06T14:47:39.395Z", + log_source: "provisioner", + log_level: "debug", + stage: "Detecting persistent resources", + output: + "any changes that are required for your infrastructure. All Terraform commands", + }, + { + id: 461089, + created_at: "2023-03-06T14:47:39.395Z", + log_source: "provisioner", + log_level: "debug", + stage: "Detecting persistent resources", + output: "should now work.", + }, + { + id: 461090, + created_at: "2023-03-06T14:47:39.397Z", + log_source: "provisioner", + log_level: "debug", + stage: "Detecting persistent resources", + output: "", + }, + { + id: 461091, + created_at: "2023-03-06T14:47:39.397Z", + log_source: "provisioner", + log_level: "debug", + stage: "Detecting persistent resources", + output: + "If you ever set or change modules or backend configuration for Terraform,", + }, + { + id: 461092, + created_at: "2023-03-06T14:47:39.397Z", + log_source: "provisioner", + log_level: "debug", + stage: "Detecting persistent resources", + output: + "rerun this command to reinitialize your working directory. If you forget, other", + }, + { + id: 461093, + created_at: "2023-03-06T14:47:39.397Z", + log_source: "provisioner", + log_level: "debug", + stage: "Detecting persistent resources", + output: "commands will detect it and remind you to do so if necessary.", + }, + { + id: 461094, + created_at: "2023-03-06T14:47:39.431Z", + log_source: "provisioner", + log_level: "info", + stage: "Detecting persistent resources", + output: "Terraform 1.1.9", + }, + { + id: 461095, + created_at: "2023-03-06T14:47:43.759Z", + log_source: "provisioner", + log_level: "error", + stage: "Detecting persistent resources", + output: + "Error: configuring Terraform AWS Provider: no valid credential sources for Terraform AWS Provider found.\n\nPlease see https://registry.terraform.io/providers/hashicorp/aws\nfor more information about providing credentials.\n\nError: failed to refresh cached credentials, no EC2 IMDS role found, operation error ec2imds: GetMetadata, http response error StatusCode: 404, request to EC2 IMDS failed\n", + }, + { + id: 461096, + created_at: "2023-03-06T14:47:43.759Z", + log_source: "provisioner", + log_level: "error", + stage: "Detecting persistent resources", + output: "", + }, + { + id: 461097, + created_at: "2023-03-06T14:47:43.777Z", + log_source: "provisioner_daemon", + log_level: "info", + stage: "Cleaning Up", + output: "", + }, + ], +} diff --git a/site/src/pages/CreateTemplatePage/CreateTemplateForm.tsx b/site/src/pages/CreateTemplatePage/CreateTemplateForm.tsx index 48fe9dd55f..5e0a8043e1 100644 --- a/site/src/pages/CreateTemplatePage/CreateTemplateForm.tsx +++ b/site/src/pages/CreateTemplatePage/CreateTemplateForm.tsx @@ -5,8 +5,8 @@ import { ParameterSchema, ProvisionerJobLog, TemplateExample, + TemplateVersionVariable, } from "api/typesGenerated" -import { FormFooter } from "components/FormFooter/FormFooter" import { ParameterInput } from "components/ParameterInput/ParameterInput" import { Stack } from "components/Stack/Stack" import { @@ -17,21 +17,30 @@ import { useFormik } from "formik" import { SelectedTemplate } from "pages/CreateWorkspacePage/SelectedTemplate" import { FC } from "react" import { useTranslation } from "react-i18next" -import { nameValidator, getFormHelpers, onChangeTrimmed } from "util/formUtils" +import { + nameValidator, + getFormHelpers, + onChangeTrimmed, + templateDisplayNameValidator, +} from "util/formUtils" import { CreateTemplateData } from "xServices/createTemplate/createTemplateXService" import * as Yup from "yup" import { WorkspaceBuildLogs } from "components/WorkspaceBuildLogs/WorkspaceBuildLogs" import { HelpTooltip, HelpTooltipText } from "components/Tooltips/HelpTooltip" import { LazyIconField } from "components/IconField/LazyIconField" +import { VariableInput } from "./VariableInput" +import { + FormFields, + FormFooter, + FormSection, + HorizontalForm, +} from "components/HorizontalForm/HorizontalForm" +import camelCase from "lodash/camelCase" +import capitalize from "lodash/capitalize" const validationSchema = Yup.object({ name: nameValidator("Name"), - display_name: Yup.string().optional(), - description: Yup.string().optional(), - icon: Yup.string().optional(), - default_ttl_hours: Yup.number(), - allow_user_cancel_workspace_jobs: Yup.boolean(), - parameter_values_by_name: Yup.object().optional(), + display_name: templateDisplayNameValidator("Display name"), }) const defaultInitialValues: CreateTemplateData = { @@ -41,7 +50,6 @@ const defaultInitialValues: CreateTemplateData = { icon: "", default_ttl_hours: 24, allow_user_cancel_workspace_jobs: false, - parameter_values_by_name: undefined, } const getInitialValues = (starterTemplate?: TemplateExample) => { @@ -58,31 +66,32 @@ const getInitialValues = (starterTemplate?: TemplateExample) => { } } -interface CreateTemplateFormProps { - starterTemplate?: TemplateExample - error?: unknown - parameters?: ParameterSchema[] - isSubmitting: boolean +export interface CreateTemplateFormProps { onCancel: () => void onSubmit: (data: CreateTemplateData) => void + isSubmitting: boolean upload: TemplateUploadProps + starterTemplate?: TemplateExample + parameters?: ParameterSchema[] + variables?: TemplateVersionVariable[] + error?: unknown jobError?: string logs?: ProvisionerJobLog[] } export const CreateTemplateForm: FC = ({ - starterTemplate, - error, - parameters, - isSubmitting, onCancel, onSubmit, + starterTemplate, + parameters, + variables, + isSubmitting, upload, + error, jobError, logs, }) => { const styles = useStyles() - const formFooterStyles = useFormFooterStyles() const form = useFormik({ initialValues: getInitialValues(starterTemplate), validationSchema, @@ -92,258 +101,223 @@ export const CreateTemplateForm: FC = ({ const { t } = useTranslation("createTemplatePage") return ( -
- - {/* General info */} -
-
-

- {t("form.generalInfo.title")} -

-

- {t("form.generalInfo.description")} -

-
- - - {starterTemplate ? ( - - ) : ( - - )} - - + {/* General info */} + + + {starterTemplate ? ( + + ) : ( + { + await fillNameAndDisplayWithFilename(file.name, form) + upload.onUpload(file) + }} /> - -
+ )} - {/* Display info */} -
-
-

- {t("form.displayInfo.title")} -

-

- {t("form.displayInfo.description")} -

-
+ + + - - + {/* Display info */} + + + - + - form.setFieldValue("icon", value)} - /> - -
+ form.setFieldValue("icon", value)} + /> + + - {/* Schedule */} -
-
-

- {t("form.schedule.title")} -

-

- {t("form.schedule.description")} -

-
+ {/* Schedule */} + + + + + - - - -
+ {/* Operations */} + + +