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 (
-