mirror of
https://github.com/coder/coder.git
synced 2025-07-15 22:20:27 +00:00
refactor(site): Suport template version variables on template creation (#6434)
This commit is contained in:
@ -5,6 +5,7 @@ import { server } from "./src/testHelpers/server"
|
|||||||
import "jest-location-mock"
|
import "jest-location-mock"
|
||||||
import { TextEncoder, TextDecoder } from "util"
|
import { TextEncoder, TextDecoder } from "util"
|
||||||
import { Blob } from "buffer"
|
import { Blob } from "buffer"
|
||||||
|
import { fetch, Request, Response, Headers } from "@remix-run/web-fetch"
|
||||||
|
|
||||||
global.TextEncoder = TextEncoder
|
global.TextEncoder = TextEncoder
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Polyfill for jsdom
|
// 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
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Polyfill for jsdom
|
||||||
global.Blob = Blob as any
|
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
|
// Polyfill the getRandomValues that is used on utils/random.ts
|
||||||
Object.defineProperty(global.self, "crypto", {
|
Object.defineProperty(global.self, "crypto", {
|
||||||
value: {
|
value: {
|
||||||
|
@ -36,6 +36,7 @@
|
|||||||
"@material-ui/icons": "4.5.1",
|
"@material-ui/icons": "4.5.1",
|
||||||
"@material-ui/lab": "4.0.0-alpha.42",
|
"@material-ui/lab": "4.0.0-alpha.42",
|
||||||
"@monaco-editor/react": "4.4.6",
|
"@monaco-editor/react": "4.4.6",
|
||||||
|
"@remix-run/web-fetch": "4.3.2",
|
||||||
"@tanstack/react-query": "4.22.4",
|
"@tanstack/react-query": "4.22.4",
|
||||||
"@testing-library/react-hooks": "8.0.1",
|
"@testing-library/react-hooks": "8.0.1",
|
||||||
"@types/color-convert": "2.0.0",
|
"@types/color-convert": "2.0.0",
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
import { screen } from "@testing-library/react"
|
import { screen } from "@testing-library/react"
|
||||||
import { rest } from "msw"
|
import { rest } from "msw"
|
||||||
import { Route } from "react-router-dom"
|
|
||||||
import { renderWithAuth } from "testHelpers/renderHelpers"
|
import { renderWithAuth } from "testHelpers/renderHelpers"
|
||||||
import { server } from "testHelpers/server"
|
import { server } from "testHelpers/server"
|
||||||
|
|
||||||
@ -20,7 +19,12 @@ describe("RequireAuth", () => {
|
|||||||
)
|
)
|
||||||
|
|
||||||
renderWithAuth(<h1>Test</h1>, {
|
renderWithAuth(<h1>Test</h1>, {
|
||||||
routes: <Route path="setup" element={<h1>Setup</h1>} />,
|
nonAuthenticatedRoutes: [
|
||||||
|
{
|
||||||
|
path: "setup",
|
||||||
|
element: <h1>Setup</h1>,
|
||||||
|
},
|
||||||
|
],
|
||||||
})
|
})
|
||||||
|
|
||||||
await screen.findByText("Setup")
|
await screen.findByText("Setup")
|
||||||
|
364
site/src/pages/CreateTemplatePage/CreateTemplateForm.stories.tsx
Normal file
364
site/src/pages/CreateTemplatePage/CreateTemplateForm.stories.tsx
Normal file
@ -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<typeof CreateTemplateForm>
|
||||||
|
|
||||||
|
const Template: Story<CreateTemplateFormProps> = (args) => (
|
||||||
|
<CreateTemplateForm {...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: "",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
@ -5,8 +5,8 @@ import {
|
|||||||
ParameterSchema,
|
ParameterSchema,
|
||||||
ProvisionerJobLog,
|
ProvisionerJobLog,
|
||||||
TemplateExample,
|
TemplateExample,
|
||||||
|
TemplateVersionVariable,
|
||||||
} from "api/typesGenerated"
|
} from "api/typesGenerated"
|
||||||
import { FormFooter } from "components/FormFooter/FormFooter"
|
|
||||||
import { ParameterInput } from "components/ParameterInput/ParameterInput"
|
import { ParameterInput } from "components/ParameterInput/ParameterInput"
|
||||||
import { Stack } from "components/Stack/Stack"
|
import { Stack } from "components/Stack/Stack"
|
||||||
import {
|
import {
|
||||||
@ -17,21 +17,30 @@ import { useFormik } from "formik"
|
|||||||
import { SelectedTemplate } from "pages/CreateWorkspacePage/SelectedTemplate"
|
import { SelectedTemplate } from "pages/CreateWorkspacePage/SelectedTemplate"
|
||||||
import { FC } from "react"
|
import { FC } from "react"
|
||||||
import { useTranslation } from "react-i18next"
|
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 { CreateTemplateData } from "xServices/createTemplate/createTemplateXService"
|
||||||
import * as Yup from "yup"
|
import * as Yup from "yup"
|
||||||
import { WorkspaceBuildLogs } from "components/WorkspaceBuildLogs/WorkspaceBuildLogs"
|
import { WorkspaceBuildLogs } from "components/WorkspaceBuildLogs/WorkspaceBuildLogs"
|
||||||
import { HelpTooltip, HelpTooltipText } from "components/Tooltips/HelpTooltip"
|
import { HelpTooltip, HelpTooltipText } from "components/Tooltips/HelpTooltip"
|
||||||
import { LazyIconField } from "components/IconField/LazyIconField"
|
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({
|
const validationSchema = Yup.object({
|
||||||
name: nameValidator("Name"),
|
name: nameValidator("Name"),
|
||||||
display_name: Yup.string().optional(),
|
display_name: templateDisplayNameValidator("Display name"),
|
||||||
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(),
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const defaultInitialValues: CreateTemplateData = {
|
const defaultInitialValues: CreateTemplateData = {
|
||||||
@ -41,7 +50,6 @@ const defaultInitialValues: CreateTemplateData = {
|
|||||||
icon: "",
|
icon: "",
|
||||||
default_ttl_hours: 24,
|
default_ttl_hours: 24,
|
||||||
allow_user_cancel_workspace_jobs: false,
|
allow_user_cancel_workspace_jobs: false,
|
||||||
parameter_values_by_name: undefined,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const getInitialValues = (starterTemplate?: TemplateExample) => {
|
const getInitialValues = (starterTemplate?: TemplateExample) => {
|
||||||
@ -58,31 +66,32 @@ const getInitialValues = (starterTemplate?: TemplateExample) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
interface CreateTemplateFormProps {
|
export interface CreateTemplateFormProps {
|
||||||
starterTemplate?: TemplateExample
|
|
||||||
error?: unknown
|
|
||||||
parameters?: ParameterSchema[]
|
|
||||||
isSubmitting: boolean
|
|
||||||
onCancel: () => void
|
onCancel: () => void
|
||||||
onSubmit: (data: CreateTemplateData) => void
|
onSubmit: (data: CreateTemplateData) => void
|
||||||
|
isSubmitting: boolean
|
||||||
upload: TemplateUploadProps
|
upload: TemplateUploadProps
|
||||||
|
starterTemplate?: TemplateExample
|
||||||
|
parameters?: ParameterSchema[]
|
||||||
|
variables?: TemplateVersionVariable[]
|
||||||
|
error?: unknown
|
||||||
jobError?: string
|
jobError?: string
|
||||||
logs?: ProvisionerJobLog[]
|
logs?: ProvisionerJobLog[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export const CreateTemplateForm: FC<CreateTemplateFormProps> = ({
|
export const CreateTemplateForm: FC<CreateTemplateFormProps> = ({
|
||||||
starterTemplate,
|
|
||||||
error,
|
|
||||||
parameters,
|
|
||||||
isSubmitting,
|
|
||||||
onCancel,
|
onCancel,
|
||||||
onSubmit,
|
onSubmit,
|
||||||
|
starterTemplate,
|
||||||
|
parameters,
|
||||||
|
variables,
|
||||||
|
isSubmitting,
|
||||||
upload,
|
upload,
|
||||||
|
error,
|
||||||
jobError,
|
jobError,
|
||||||
logs,
|
logs,
|
||||||
}) => {
|
}) => {
|
||||||
const styles = useStyles()
|
const styles = useStyles()
|
||||||
const formFooterStyles = useFormFooterStyles()
|
|
||||||
const form = useFormik<CreateTemplateData>({
|
const form = useFormik<CreateTemplateData>({
|
||||||
initialValues: getInitialValues(starterTemplate),
|
initialValues: getInitialValues(starterTemplate),
|
||||||
validationSchema,
|
validationSchema,
|
||||||
@ -92,24 +101,23 @@ export const CreateTemplateForm: FC<CreateTemplateFormProps> = ({
|
|||||||
const { t } = useTranslation("createTemplatePage")
|
const { t } = useTranslation("createTemplatePage")
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form onSubmit={form.handleSubmit}>
|
<HorizontalForm onSubmit={form.handleSubmit}>
|
||||||
<Stack direction="column" spacing={10} className={styles.formSections}>
|
|
||||||
{/* General info */}
|
{/* General info */}
|
||||||
<div className={styles.formSection}>
|
<FormSection
|
||||||
<div className={styles.formSectionInfo}>
|
title={t("form.generalInfo.title")}
|
||||||
<h2 className={styles.formSectionInfoTitle}>
|
description={t("form.generalInfo.description")}
|
||||||
{t("form.generalInfo.title")}
|
>
|
||||||
</h2>
|
<FormFields>
|
||||||
<p className={styles.formSectionInfoDescription}>
|
|
||||||
{t("form.generalInfo.description")}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Stack direction="column" className={styles.formSectionFields}>
|
|
||||||
{starterTemplate ? (
|
{starterTemplate ? (
|
||||||
<SelectedTemplate template={starterTemplate} />
|
<SelectedTemplate template={starterTemplate} />
|
||||||
) : (
|
) : (
|
||||||
<TemplateUpload {...upload} />
|
<TemplateUpload
|
||||||
|
{...upload}
|
||||||
|
onUpload={async (file) => {
|
||||||
|
await fillNameAndDisplayWithFilename(file.name, form)
|
||||||
|
upload.onUpload(file)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<TextField
|
<TextField
|
||||||
@ -118,24 +126,19 @@ export const CreateTemplateForm: FC<CreateTemplateFormProps> = ({
|
|||||||
onChange={onChangeTrimmed(form)}
|
onChange={onChangeTrimmed(form)}
|
||||||
autoFocus
|
autoFocus
|
||||||
fullWidth
|
fullWidth
|
||||||
|
required
|
||||||
label={t("form.fields.name")}
|
label={t("form.fields.name")}
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
/>
|
/>
|
||||||
</Stack>
|
</FormFields>
|
||||||
</div>
|
</FormSection>
|
||||||
|
|
||||||
{/* Display info */}
|
{/* Display info */}
|
||||||
<div className={styles.formSection}>
|
<FormSection
|
||||||
<div className={styles.formSectionInfo}>
|
title={t("form.displayInfo.title")}
|
||||||
<h2 className={styles.formSectionInfoTitle}>
|
description={t("form.displayInfo.description")}
|
||||||
{t("form.displayInfo.title")}
|
>
|
||||||
</h2>
|
<FormFields>
|
||||||
<p className={styles.formSectionInfoDescription}>
|
|
||||||
{t("form.displayInfo.description")}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Stack direction="column" className={styles.formSectionFields}>
|
|
||||||
<TextField
|
<TextField
|
||||||
{...getFieldHelpers("display_name")}
|
{...getFieldHelpers("display_name")}
|
||||||
disabled={isSubmitting}
|
disabled={isSubmitting}
|
||||||
@ -163,21 +166,15 @@ export const CreateTemplateForm: FC<CreateTemplateFormProps> = ({
|
|||||||
variant="outlined"
|
variant="outlined"
|
||||||
onPickEmoji={(value) => form.setFieldValue("icon", value)}
|
onPickEmoji={(value) => form.setFieldValue("icon", value)}
|
||||||
/>
|
/>
|
||||||
</Stack>
|
</FormFields>
|
||||||
</div>
|
</FormSection>
|
||||||
|
|
||||||
{/* Schedule */}
|
{/* Schedule */}
|
||||||
<div className={styles.formSection}>
|
<FormSection
|
||||||
<div className={styles.formSectionInfo}>
|
title={t("form.schedule.title")}
|
||||||
<h2 className={styles.formSectionInfoTitle}>
|
description={t("form.schedule.description")}
|
||||||
{t("form.schedule.title")}
|
>
|
||||||
</h2>
|
<FormFields>
|
||||||
<p className={styles.formSectionInfoDescription}>
|
|
||||||
{t("form.schedule.description")}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Stack direction="column" className={styles.formSectionFields}>
|
|
||||||
<TextField
|
<TextField
|
||||||
{...getFieldHelpers("default_ttl_hours")}
|
{...getFieldHelpers("default_ttl_hours")}
|
||||||
disabled={isSubmitting}
|
disabled={isSubmitting}
|
||||||
@ -188,21 +185,15 @@ export const CreateTemplateForm: FC<CreateTemplateFormProps> = ({
|
|||||||
type="number"
|
type="number"
|
||||||
helperText={t("form.helperText.autoStop")}
|
helperText={t("form.helperText.autoStop")}
|
||||||
/>
|
/>
|
||||||
</Stack>
|
</FormFields>
|
||||||
</div>
|
</FormSection>
|
||||||
|
|
||||||
{/* Operations */}
|
{/* Operations */}
|
||||||
<div className={styles.formSection}>
|
<FormSection
|
||||||
<div className={styles.formSectionInfo}>
|
title={t("form.operations.title")}
|
||||||
<h2 className={styles.formSectionInfoTitle}>
|
description={t("form.operations.description")}
|
||||||
{t("form.operations.title")}
|
>
|
||||||
</h2>
|
<FormFields>
|
||||||
<p className={styles.formSectionInfoDescription}>
|
|
||||||
{t("form.operations.description")}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Stack direction="column" className={styles.formSectionFields}>
|
|
||||||
<label htmlFor="allow_user_cancel_workspace_jobs">
|
<label htmlFor="allow_user_cancel_workspace_jobs">
|
||||||
<Stack direction="row" spacing={1}>
|
<Stack direction="row" spacing={1}>
|
||||||
<Checkbox
|
<Checkbox
|
||||||
@ -235,22 +226,16 @@ export const CreateTemplateForm: FC<CreateTemplateFormProps> = ({
|
|||||||
</Stack>
|
</Stack>
|
||||||
</Stack>
|
</Stack>
|
||||||
</label>
|
</label>
|
||||||
</Stack>
|
</FormFields>
|
||||||
</div>
|
</FormSection>
|
||||||
|
|
||||||
{/* Parameters */}
|
{/* Parameters */}
|
||||||
{parameters && (
|
{parameters && (
|
||||||
<div className={styles.formSection}>
|
<FormSection
|
||||||
<div className={styles.formSectionInfo}>
|
title={t("form.parameters.title")}
|
||||||
<h2 className={styles.formSectionInfoTitle}>
|
description={t("form.parameters.description")}
|
||||||
{t("form.parameters.title")}
|
>
|
||||||
</h2>
|
<FormFields>
|
||||||
<p className={styles.formSectionInfoDescription}>
|
|
||||||
{t("form.parameters.description")}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Stack direction="column" className={styles.formSectionFields}>
|
|
||||||
{parameters.map((schema) => (
|
{parameters.map((schema) => (
|
||||||
<ParameterInput
|
<ParameterInput
|
||||||
schema={schema}
|
schema={schema}
|
||||||
@ -264,8 +249,32 @@ export const CreateTemplateForm: FC<CreateTemplateFormProps> = ({
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</Stack>
|
</FormFields>
|
||||||
</div>
|
</FormSection>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Variables */}
|
||||||
|
{variables && (
|
||||||
|
<FormSection
|
||||||
|
title="Variables"
|
||||||
|
description="Input variables allow you to customize templates without altering their source code."
|
||||||
|
>
|
||||||
|
<FormFields>
|
||||||
|
{variables.map((variable, index) => (
|
||||||
|
<VariableInput
|
||||||
|
variable={variable}
|
||||||
|
disabled={isSubmitting}
|
||||||
|
key={variable.name}
|
||||||
|
onChange={async (value) => {
|
||||||
|
await form.setFieldValue("user_variable_values." + index, {
|
||||||
|
name: variable.name,
|
||||||
|
value: value,
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</FormFields>
|
||||||
|
</FormSection>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{jobError && (
|
{jobError && (
|
||||||
@ -273,8 +282,8 @@ export const CreateTemplateForm: FC<CreateTemplateFormProps> = ({
|
|||||||
<div className={styles.error}>
|
<div className={styles.error}>
|
||||||
<h5 className={styles.errorTitle}>Error during provisioning</h5>
|
<h5 className={styles.errorTitle}>Error during provisioning</h5>
|
||||||
<p className={styles.errorDescription}>
|
<p className={styles.errorDescription}>
|
||||||
Looks like we found an error during the template provisioning.
|
Looks like we found an error during the template provisioning. You
|
||||||
You can see the logs bellow.
|
can see the logs bellow.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<code className={styles.errorDetails}>{jobError}</code>
|
<code className={styles.errorDetails}>{jobError}</code>
|
||||||
@ -285,65 +294,30 @@ export const CreateTemplateForm: FC<CreateTemplateFormProps> = ({
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<FormFooter
|
<FormFooter
|
||||||
styles={formFooterStyles}
|
|
||||||
onCancel={onCancel}
|
onCancel={onCancel}
|
||||||
isLoading={isSubmitting}
|
isLoading={isSubmitting}
|
||||||
submitLabel={jobError ? "Retry" : "Create template"}
|
submitLabel={jobError ? "Retry" : "Create template"}
|
||||||
/>
|
/>
|
||||||
</Stack>
|
</HorizontalForm>
|
||||||
</form>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const fillNameAndDisplayWithFilename = async (
|
||||||
|
filename: string,
|
||||||
|
form: ReturnType<typeof useFormik<CreateTemplateData>>,
|
||||||
|
) => {
|
||||||
|
const [name, _extension] = filename.split(".")
|
||||||
|
await Promise.all([
|
||||||
|
form.setFieldValue(
|
||||||
|
"name",
|
||||||
|
// Camel case will remove special chars and spaces
|
||||||
|
camelCase(name).toLowerCase(),
|
||||||
|
),
|
||||||
|
form.setFieldValue("display_name", capitalize(name)),
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
const useStyles = makeStyles((theme) => ({
|
const useStyles = makeStyles((theme) => ({
|
||||||
formSections: {
|
|
||||||
[theme.breakpoints.down("sm")]: {
|
|
||||||
gap: theme.spacing(8),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
formSection: {
|
|
||||||
display: "flex",
|
|
||||||
alignItems: "flex-start",
|
|
||||||
gap: theme.spacing(15),
|
|
||||||
|
|
||||||
[theme.breakpoints.down("sm")]: {
|
|
||||||
flexDirection: "column",
|
|
||||||
gap: theme.spacing(2),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
formSectionInfo: {
|
|
||||||
width: 312,
|
|
||||||
flexShrink: 0,
|
|
||||||
position: "sticky",
|
|
||||||
top: theme.spacing(3),
|
|
||||||
|
|
||||||
[theme.breakpoints.down("sm")]: {
|
|
||||||
width: "100%",
|
|
||||||
position: "initial",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
formSectionInfoTitle: {
|
|
||||||
fontSize: 20,
|
|
||||||
color: theme.palette.text.primary,
|
|
||||||
fontWeight: 400,
|
|
||||||
margin: 0,
|
|
||||||
marginBottom: theme.spacing(1),
|
|
||||||
},
|
|
||||||
|
|
||||||
formSectionInfoDescription: {
|
|
||||||
fontSize: 14,
|
|
||||||
color: theme.palette.text.secondary,
|
|
||||||
lineHeight: "160%",
|
|
||||||
margin: 0,
|
|
||||||
},
|
|
||||||
|
|
||||||
formSectionFields: {
|
|
||||||
width: "100%",
|
|
||||||
},
|
|
||||||
|
|
||||||
optionText: {
|
optionText: {
|
||||||
fontSize: theme.spacing(2),
|
fontSize: theme.spacing(2),
|
||||||
color: theme.palette.text.primary,
|
color: theme.palette.text.primary,
|
||||||
@ -379,25 +353,3 @@ const useStyles = makeStyles((theme) => ({
|
|||||||
fontSize: theme.spacing(2),
|
fontSize: theme.spacing(2),
|
||||||
},
|
},
|
||||||
}))
|
}))
|
||||||
|
|
||||||
const useFormFooterStyles = makeStyles((theme) => ({
|
|
||||||
button: {
|
|
||||||
minWidth: theme.spacing(23),
|
|
||||||
|
|
||||||
[theme.breakpoints.down("sm")]: {
|
|
||||||
width: "100%",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
footer: {
|
|
||||||
display: "flex",
|
|
||||||
alignItems: "center",
|
|
||||||
justifyContent: "flex-start",
|
|
||||||
flexDirection: "row-reverse",
|
|
||||||
gap: theme.spacing(2),
|
|
||||||
|
|
||||||
[theme.breakpoints.down("sm")]: {
|
|
||||||
flexDirection: "column",
|
|
||||||
gap: theme.spacing(1),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}))
|
|
||||||
|
122
site/src/pages/CreateTemplatePage/CreateTemplatePage.test.tsx
Normal file
122
site/src/pages/CreateTemplatePage/CreateTemplatePage.test.tsx
Normal file
@ -0,0 +1,122 @@
|
|||||||
|
import {
|
||||||
|
MockOrganization,
|
||||||
|
MockProvisionerJob,
|
||||||
|
MockTemplate,
|
||||||
|
MockTemplateExample,
|
||||||
|
MockTemplateVersion,
|
||||||
|
MockTemplateVersionVariable1,
|
||||||
|
MockTemplateVersionVariable2,
|
||||||
|
MockTemplateVersionVariable3,
|
||||||
|
MockTemplateVersionVariable4,
|
||||||
|
MockTemplateVersionVariable5,
|
||||||
|
renderWithAuth,
|
||||||
|
} from "testHelpers/renderHelpers"
|
||||||
|
import CreateTemplatePage from "./CreateTemplatePage"
|
||||||
|
import { screen, waitFor } from "@testing-library/react"
|
||||||
|
import userEvent from "@testing-library/user-event"
|
||||||
|
import * as API from "api/api"
|
||||||
|
|
||||||
|
const renderPage = async () => {
|
||||||
|
// Render with the example ID so we don't need to upload a file
|
||||||
|
const result = renderWithAuth(<CreateTemplatePage />, {
|
||||||
|
route: `/templates/new?exampleId=${MockTemplateExample.id}`,
|
||||||
|
path: "/templates/new",
|
||||||
|
// We need this because after creation, the user will be redirected to here
|
||||||
|
extraRoutes: [{ path: "templates/:template", element: <></> }],
|
||||||
|
})
|
||||||
|
// It is lazy loaded, so we have to wait for it to be rendered to not get an
|
||||||
|
// act error
|
||||||
|
await screen.findByLabelText("Icon")
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
test("Create template with variables", async () => {
|
||||||
|
// Return pending when creating the first template version
|
||||||
|
jest.spyOn(API, "createTemplateVersion").mockResolvedValueOnce({
|
||||||
|
...MockTemplateVersion,
|
||||||
|
job: {
|
||||||
|
...MockTemplateVersion.job,
|
||||||
|
status: "pending",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
// Return an error requesting for template variables
|
||||||
|
jest.spyOn(API, "getTemplateVersion").mockResolvedValue({
|
||||||
|
...MockTemplateVersion,
|
||||||
|
job: {
|
||||||
|
...MockTemplateVersion.job,
|
||||||
|
status: "failed",
|
||||||
|
error: "required template variables",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
// Return the template variables
|
||||||
|
jest
|
||||||
|
.spyOn(API, "getTemplateVersionVariables")
|
||||||
|
.mockResolvedValue([
|
||||||
|
MockTemplateVersionVariable1,
|
||||||
|
MockTemplateVersionVariable2,
|
||||||
|
MockTemplateVersionVariable3,
|
||||||
|
MockTemplateVersionVariable4,
|
||||||
|
MockTemplateVersionVariable5,
|
||||||
|
])
|
||||||
|
|
||||||
|
// Render page, fill the name and submit
|
||||||
|
const { router } = await renderPage()
|
||||||
|
await userEvent.type(screen.getByLabelText(/Name/), "my-template")
|
||||||
|
await userEvent.click(
|
||||||
|
screen.getByRole("button", { name: /create template/i }),
|
||||||
|
)
|
||||||
|
|
||||||
|
// Wait for the variables form to be rendered and fill it
|
||||||
|
await screen.findByText(/Variables/)
|
||||||
|
// Type first variable
|
||||||
|
await userEvent.clear(screen.getByLabelText(/var.first_variable/))
|
||||||
|
await userEvent.type(
|
||||||
|
screen.getByLabelText(/var.first_variable/),
|
||||||
|
"First value",
|
||||||
|
)
|
||||||
|
// Type second variable
|
||||||
|
await userEvent.clear(screen.getByLabelText(/var.second_variable/))
|
||||||
|
await userEvent.type(screen.getByLabelText(/var.second_variable/), "2")
|
||||||
|
// Select third variable on radio
|
||||||
|
await userEvent.click(screen.getByLabelText(/True/))
|
||||||
|
// Type fourth variable
|
||||||
|
await userEvent.clear(screen.getByLabelText(/var.fourth_variable/))
|
||||||
|
await userEvent.type(
|
||||||
|
screen.getByLabelText(/var.fourth_variable/),
|
||||||
|
"Fourth value",
|
||||||
|
)
|
||||||
|
// Type fifth variable
|
||||||
|
await userEvent.clear(screen.getByLabelText(/var.fifth_variable/))
|
||||||
|
await userEvent.type(
|
||||||
|
screen.getByLabelText(/var.fifth_variable/),
|
||||||
|
"Fifth value",
|
||||||
|
)
|
||||||
|
// Setup the mock for the second template version creation before submit the form
|
||||||
|
jest.clearAllMocks()
|
||||||
|
jest
|
||||||
|
.spyOn(API, "createTemplateVersion")
|
||||||
|
.mockResolvedValue(MockTemplateVersion)
|
||||||
|
jest.spyOn(API, "createTemplate").mockResolvedValue(MockTemplate)
|
||||||
|
await userEvent.click(
|
||||||
|
screen.getByRole("button", { name: /create template/i }),
|
||||||
|
)
|
||||||
|
|
||||||
|
await waitFor(() => expect(API.createTemplate).toBeCalledTimes(1))
|
||||||
|
expect(router.state.location.pathname).toEqual(
|
||||||
|
`/templates/${MockTemplate.name}`,
|
||||||
|
)
|
||||||
|
expect(API.createTemplateVersion).toHaveBeenCalledWith(MockOrganization.id, {
|
||||||
|
file_id: MockProvisionerJob.file_id,
|
||||||
|
parameter_values: [],
|
||||||
|
provisioner: "terraform",
|
||||||
|
storage_method: "file",
|
||||||
|
tags: {},
|
||||||
|
user_variable_values: [
|
||||||
|
{ name: "first_variable", value: "First value" },
|
||||||
|
{ name: "second_variable", value: "2" },
|
||||||
|
{ name: "third_variable", value: "true" },
|
||||||
|
{ name: "fourth_variable", value: "Fourth value" },
|
||||||
|
{ name: "fifth_variable", value: "Fifth value" },
|
||||||
|
],
|
||||||
|
})
|
||||||
|
})
|
@ -30,8 +30,15 @@ const CreateTemplatePage: FC = () => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
const { starterTemplate, parameters, error, file, jobError, jobLogs } =
|
const {
|
||||||
state.context
|
starterTemplate,
|
||||||
|
parameters,
|
||||||
|
error,
|
||||||
|
file,
|
||||||
|
jobError,
|
||||||
|
jobLogs,
|
||||||
|
variables,
|
||||||
|
} = state.context
|
||||||
const shouldDisplayForm = !state.hasTag("loading")
|
const shouldDisplayForm = !state.hasTag("loading")
|
||||||
|
|
||||||
const onCancel = () => {
|
const onCancel = () => {
|
||||||
@ -59,6 +66,7 @@ const CreateTemplatePage: FC = () => {
|
|||||||
error={error}
|
error={error}
|
||||||
starterTemplate={starterTemplate}
|
starterTemplate={starterTemplate}
|
||||||
isSubmitting={state.hasTag("submitting")}
|
isSubmitting={state.hasTag("submitting")}
|
||||||
|
variables={variables}
|
||||||
parameters={parameters}
|
parameters={parameters}
|
||||||
onCancel={onCancel}
|
onCancel={onCancel}
|
||||||
onSubmit={(data) => {
|
onSubmit={(data) => {
|
||||||
|
137
site/src/pages/CreateTemplatePage/VariableInput.tsx
Normal file
137
site/src/pages/CreateTemplatePage/VariableInput.tsx
Normal file
@ -0,0 +1,137 @@
|
|||||||
|
import FormControlLabel from "@material-ui/core/FormControlLabel"
|
||||||
|
import Radio from "@material-ui/core/Radio"
|
||||||
|
import RadioGroup from "@material-ui/core/RadioGroup"
|
||||||
|
import { makeStyles } from "@material-ui/core/styles"
|
||||||
|
import TextField from "@material-ui/core/TextField"
|
||||||
|
import { Stack } from "components/Stack/Stack"
|
||||||
|
import { FC } from "react"
|
||||||
|
import { TemplateVersionVariable } from "../../api/typesGenerated"
|
||||||
|
|
||||||
|
const isBoolean = (variable: TemplateVersionVariable) => {
|
||||||
|
return variable.type === "bool"
|
||||||
|
}
|
||||||
|
|
||||||
|
const VariableLabel: React.FC<{ variable: TemplateVersionVariable }> = ({
|
||||||
|
variable,
|
||||||
|
}) => {
|
||||||
|
const styles = useStyles()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<label htmlFor={variable.name}>
|
||||||
|
<span className={styles.labelName}>
|
||||||
|
var.{variable.name}
|
||||||
|
{!variable.required && " (optional)"}
|
||||||
|
</span>
|
||||||
|
<span className={styles.labelDescription}>{variable.description}</span>
|
||||||
|
</label>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface VariableInputProps {
|
||||||
|
disabled?: boolean
|
||||||
|
variable: TemplateVersionVariable
|
||||||
|
onChange: (value: string) => void
|
||||||
|
defaultValue?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export const VariableInput: FC<VariableInputProps> = ({
|
||||||
|
disabled,
|
||||||
|
onChange,
|
||||||
|
variable,
|
||||||
|
defaultValue,
|
||||||
|
}) => {
|
||||||
|
const styles = useStyles()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Stack direction="column" spacing={0.75}>
|
||||||
|
<VariableLabel variable={variable} />
|
||||||
|
<div className={styles.input}>
|
||||||
|
<VariableField
|
||||||
|
disabled={disabled}
|
||||||
|
onChange={onChange}
|
||||||
|
variable={variable}
|
||||||
|
defaultValue={defaultValue}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Stack>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const VariableField: React.FC<VariableInputProps> = ({
|
||||||
|
disabled,
|
||||||
|
onChange,
|
||||||
|
variable,
|
||||||
|
defaultValue,
|
||||||
|
}) => {
|
||||||
|
if (isBoolean(variable)) {
|
||||||
|
return (
|
||||||
|
<RadioGroup
|
||||||
|
id={variable.name}
|
||||||
|
defaultValue={variable.default_value}
|
||||||
|
onChange={(event) => {
|
||||||
|
onChange(event.target.value)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<FormControlLabel
|
||||||
|
disabled={disabled}
|
||||||
|
value="true"
|
||||||
|
control={<Radio color="primary" size="small" disableRipple />}
|
||||||
|
label="True"
|
||||||
|
/>
|
||||||
|
<FormControlLabel
|
||||||
|
disabled={disabled}
|
||||||
|
value="false"
|
||||||
|
control={<Radio color="primary" size="small" disableRipple />}
|
||||||
|
label="False"
|
||||||
|
/>
|
||||||
|
</RadioGroup>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TextField
|
||||||
|
id={variable.name}
|
||||||
|
size="small"
|
||||||
|
disabled={disabled}
|
||||||
|
placeholder={variable.sensitive ? "" : variable.default_value}
|
||||||
|
required={variable.required}
|
||||||
|
defaultValue={
|
||||||
|
variable.sensitive ? "" : defaultValue ?? variable.default_value
|
||||||
|
}
|
||||||
|
onChange={(event) => {
|
||||||
|
onChange(event.target.value)
|
||||||
|
}}
|
||||||
|
type={
|
||||||
|
variable.type === "number"
|
||||||
|
? "number"
|
||||||
|
: variable.sensitive
|
||||||
|
? "password"
|
||||||
|
: "string"
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const useStyles = makeStyles((theme) => ({
|
||||||
|
labelName: {
|
||||||
|
fontSize: 14,
|
||||||
|
color: theme.palette.text.secondary,
|
||||||
|
display: "block",
|
||||||
|
marginBottom: theme.spacing(0.5),
|
||||||
|
},
|
||||||
|
labelDescription: {
|
||||||
|
fontSize: 16,
|
||||||
|
color: theme.palette.text.primary,
|
||||||
|
display: "block",
|
||||||
|
fontWeight: 600,
|
||||||
|
},
|
||||||
|
input: {
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
},
|
||||||
|
checkbox: {
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
gap: theme.spacing(1),
|
||||||
|
},
|
||||||
|
}))
|
@ -2,6 +2,7 @@ import { ComponentMeta, Story } from "@storybook/react"
|
|||||||
import {
|
import {
|
||||||
makeMockApiError,
|
makeMockApiError,
|
||||||
mockParameterSchema,
|
mockParameterSchema,
|
||||||
|
MockParameterSchemas,
|
||||||
MockTemplate,
|
MockTemplate,
|
||||||
MockTemplateVersionParameter1,
|
MockTemplateVersionParameter1,
|
||||||
MockTemplateVersionParameter2,
|
MockTemplateVersionParameter2,
|
||||||
@ -34,41 +35,7 @@ export const Parameters = Template.bind({})
|
|||||||
Parameters.args = {
|
Parameters.args = {
|
||||||
templates: [MockTemplate],
|
templates: [MockTemplate],
|
||||||
selectedTemplate: MockTemplate,
|
selectedTemplate: MockTemplate,
|
||||||
templateSchema: [
|
templateSchema: MockParameterSchemas,
|
||||||
mockParameterSchema({
|
|
||||||
name: "region",
|
|
||||||
default_source_value: "🏈 US Central",
|
|
||||||
description: "Where would you like your workspace to live?",
|
|
||||||
redisplay_value: true,
|
|
||||||
validation_contains: [
|
|
||||||
"🏈 US Central",
|
|
||||||
"⚽ Brazil East",
|
|
||||||
"💶 EU West",
|
|
||||||
"🦘 Australia South",
|
|
||||||
],
|
|
||||||
}),
|
|
||||||
mockParameterSchema({
|
|
||||||
name: "instance_size",
|
|
||||||
default_source_value: "Big",
|
|
||||||
description: "How large should you instance be?",
|
|
||||||
validation_contains: ["Small", "Medium", "Big"],
|
|
||||||
redisplay_value: true,
|
|
||||||
}),
|
|
||||||
mockParameterSchema({
|
|
||||||
name: "instance_size",
|
|
||||||
default_source_value: "Big",
|
|
||||||
description: "How large should your instance be?",
|
|
||||||
validation_contains: ["Small", "Medium", "Big"],
|
|
||||||
redisplay_value: true,
|
|
||||||
}),
|
|
||||||
mockParameterSchema({
|
|
||||||
name: "disable_docker",
|
|
||||||
description: "Disable Docker?",
|
|
||||||
validation_value_type: "bool",
|
|
||||||
default_source_value: "false",
|
|
||||||
redisplay_value: true,
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
createWorkspaceErrors: {},
|
createWorkspaceErrors: {},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -11,17 +11,6 @@ import i18next from "i18next"
|
|||||||
|
|
||||||
const { t } = i18next
|
const { t } = i18next
|
||||||
|
|
||||||
const renderTemplateSettingsPage = async () => {
|
|
||||||
const renderResult = renderWithAuth(<TemplateSettingsPage />, {
|
|
||||||
route: `/templates/${MockTemplate.name}/settings`,
|
|
||||||
path: `/templates/:templateId/settings`,
|
|
||||||
})
|
|
||||||
// Wait the form to be rendered
|
|
||||||
const label = t("nameLabel", { ns: "templateSettingsPage" })
|
|
||||||
await screen.findAllByLabelText(label)
|
|
||||||
return renderResult
|
|
||||||
}
|
|
||||||
|
|
||||||
const validFormValues = {
|
const validFormValues = {
|
||||||
name: "Name",
|
name: "Name",
|
||||||
display_name: "A display name",
|
display_name: "A display name",
|
||||||
@ -31,6 +20,17 @@ const validFormValues = {
|
|||||||
allow_user_cancel_workspace_jobs: false,
|
allow_user_cancel_workspace_jobs: false,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const renderTemplateSettingsPage = async () => {
|
||||||
|
renderWithAuth(<TemplateSettingsPage />, {
|
||||||
|
route: `/templates/${MockTemplate.name}/settings`,
|
||||||
|
path: `/templates/:template/settings`,
|
||||||
|
extraRoutes: [{ path: "templates/:template", element: <></> }],
|
||||||
|
})
|
||||||
|
// Wait the form to be rendered
|
||||||
|
const label = t("nameLabel", { ns: "templateSettingsPage" })
|
||||||
|
await screen.findAllByLabelText(label)
|
||||||
|
}
|
||||||
|
|
||||||
const fillAndSubmitForm = async ({
|
const fillAndSubmitForm = async ({
|
||||||
name,
|
name,
|
||||||
display_name,
|
display_name,
|
||||||
@ -109,17 +109,13 @@ describe("TemplateSettingsPage", () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
await fillAndSubmitForm(validFormValues)
|
await fillAndSubmitForm(validFormValues)
|
||||||
expect(screen.getByDisplayValue(1)).toBeInTheDocument() // the default_ttl_ms
|
|
||||||
await waitFor(() => expect(API.updateTemplateMeta).toBeCalledTimes(1))
|
await waitFor(() => expect(API.updateTemplateMeta).toBeCalledTimes(1))
|
||||||
|
|
||||||
await waitFor(() =>
|
|
||||||
expect(API.updateTemplateMeta).toBeCalledWith(
|
expect(API.updateTemplateMeta).toBeCalledWith(
|
||||||
"test-template",
|
"test-template",
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
...validFormValues,
|
...validFormValues,
|
||||||
default_ttl_ms: 3600000, // the default_ttl_ms to ms
|
default_ttl_ms: 3600000, // the default_ttl_ms to ms
|
||||||
}),
|
}),
|
||||||
),
|
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -13,7 +13,6 @@ import * as API from "api/api"
|
|||||||
import i18next from "i18next"
|
import i18next from "i18next"
|
||||||
import TemplateVariablesPage from "./TemplateVariablesPage"
|
import TemplateVariablesPage from "./TemplateVariablesPage"
|
||||||
import { Language as FooterFormLanguage } from "components/FormFooter/FormFooter"
|
import { Language as FooterFormLanguage } from "components/FormFooter/FormFooter"
|
||||||
import { Route } from "react-router-dom"
|
|
||||||
import * as router from "react-router"
|
import * as router from "react-router"
|
||||||
|
|
||||||
const navigate = jest.fn()
|
const navigate = jest.fn()
|
||||||
@ -35,9 +34,7 @@ const renderTemplateVariablesPage = () => {
|
|||||||
return renderWithAuth(<TemplateVariablesPage />, {
|
return renderWithAuth(<TemplateVariablesPage />, {
|
||||||
route: `/templates/${MockTemplate.name}/variables`,
|
route: `/templates/${MockTemplate.name}/variables`,
|
||||||
path: `/templates/:template/variables`,
|
path: `/templates/:template/variables`,
|
||||||
routes: (
|
extraRoutes: [{ path: `/templates/${MockTemplate.name}`, element: <></> }],
|
||||||
<Route path={`/templates/${MockTemplate.name}`} element={<></>}></Route>
|
|
||||||
),
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -8,6 +8,13 @@ import { Permissions } from "xServices/auth/authXService"
|
|||||||
import { TemplateVersionFiles } from "util/templateVersion"
|
import { TemplateVersionFiles } from "util/templateVersion"
|
||||||
import { FileTree } from "util/filetree"
|
import { FileTree } from "util/filetree"
|
||||||
|
|
||||||
|
export const MockOrganization: TypesGen.Organization = {
|
||||||
|
id: "fc0774ce-cc9e-48d4-80ae-88f7a4d4a8b0",
|
||||||
|
name: "Test Organization",
|
||||||
|
created_at: "",
|
||||||
|
updated_at: "",
|
||||||
|
}
|
||||||
|
|
||||||
export const MockTemplateDAUResponse: TypesGen.TemplateDAUsResponse = {
|
export const MockTemplateDAUResponse: TypesGen.TemplateDAUsResponse = {
|
||||||
entries: [
|
entries: [
|
||||||
{ date: "2022-08-27T00:00:00Z", amount: 1 },
|
{ date: "2022-08-27T00:00:00Z", amount: 1 },
|
||||||
@ -140,7 +147,7 @@ export const MockUser: TypesGen.User = {
|
|||||||
email: "test@coder.com",
|
email: "test@coder.com",
|
||||||
created_at: "",
|
created_at: "",
|
||||||
status: "active",
|
status: "active",
|
||||||
organization_ids: ["fc0774ce-cc9e-48d4-80ae-88f7a4d4a8b0"],
|
organization_ids: [MockOrganization.id],
|
||||||
roles: [MockOwnerRole],
|
roles: [MockOwnerRole],
|
||||||
avatar_url: "https://avatars.githubusercontent.com/u/95932066?s=200&v=4",
|
avatar_url: "https://avatars.githubusercontent.com/u/95932066?s=200&v=4",
|
||||||
last_seen_at: "",
|
last_seen_at: "",
|
||||||
@ -152,7 +159,7 @@ export const MockUserAdmin: TypesGen.User = {
|
|||||||
email: "test@coder.com",
|
email: "test@coder.com",
|
||||||
created_at: "",
|
created_at: "",
|
||||||
status: "active",
|
status: "active",
|
||||||
organization_ids: ["fc0774ce-cc9e-48d4-80ae-88f7a4d4a8b0"],
|
organization_ids: [MockOrganization.id],
|
||||||
roles: [MockUserAdminRole],
|
roles: [MockUserAdminRole],
|
||||||
avatar_url: "",
|
avatar_url: "",
|
||||||
last_seen_at: "",
|
last_seen_at: "",
|
||||||
@ -164,7 +171,7 @@ export const MockUser2: TypesGen.User = {
|
|||||||
email: "test2@coder.com",
|
email: "test2@coder.com",
|
||||||
created_at: "",
|
created_at: "",
|
||||||
status: "active",
|
status: "active",
|
||||||
organization_ids: ["fc0774ce-cc9e-48d4-80ae-88f7a4d4a8b0"],
|
organization_ids: [MockOrganization.id],
|
||||||
roles: [],
|
roles: [],
|
||||||
avatar_url: "",
|
avatar_url: "",
|
||||||
last_seen_at: "2022-09-14T19:12:21Z",
|
last_seen_at: "2022-09-14T19:12:21Z",
|
||||||
@ -176,19 +183,12 @@ export const SuspendedMockUser: TypesGen.User = {
|
|||||||
email: "iamsuspendedsad!@coder.com",
|
email: "iamsuspendedsad!@coder.com",
|
||||||
created_at: "",
|
created_at: "",
|
||||||
status: "suspended",
|
status: "suspended",
|
||||||
organization_ids: ["fc0774ce-cc9e-48d4-80ae-88f7a4d4a8b0"],
|
organization_ids: [MockOrganization.id],
|
||||||
roles: [],
|
roles: [],
|
||||||
avatar_url: "",
|
avatar_url: "",
|
||||||
last_seen_at: "",
|
last_seen_at: "",
|
||||||
}
|
}
|
||||||
|
|
||||||
export const MockOrganization: TypesGen.Organization = {
|
|
||||||
id: "test-org",
|
|
||||||
name: "Test Organization",
|
|
||||||
created_at: "",
|
|
||||||
updated_at: "",
|
|
||||||
}
|
|
||||||
|
|
||||||
export const MockProvisioner: TypesGen.ProvisionerDaemon = {
|
export const MockProvisioner: TypesGen.ProvisionerDaemon = {
|
||||||
created_at: "",
|
created_at: "",
|
||||||
id: "test-provisioner",
|
id: "test-provisioner",
|
||||||
@ -201,7 +201,7 @@ export const MockProvisionerJob: TypesGen.ProvisionerJob = {
|
|||||||
created_at: "",
|
created_at: "",
|
||||||
id: "test-provisioner-job",
|
id: "test-provisioner-job",
|
||||||
status: "succeeded",
|
status: "succeeded",
|
||||||
file_id: "fc0774ce-cc9e-48d4-80ae-88f7a4d4a8b0",
|
file_id: MockOrganization.id,
|
||||||
completed_at: "2022-05-17T17:39:01.382927298Z",
|
completed_at: "2022-05-17T17:39:01.382927298Z",
|
||||||
tags: {},
|
tags: {},
|
||||||
}
|
}
|
||||||
@ -1240,7 +1240,7 @@ export const MockAuditLog: TypesGen.AuditLog = {
|
|||||||
id: "fbd2116a-8961-4954-87ae-e4575bd29ce0",
|
id: "fbd2116a-8961-4954-87ae-e4575bd29ce0",
|
||||||
request_id: "53bded77-7b9d-4e82-8771-991a34d759f9",
|
request_id: "53bded77-7b9d-4e82-8771-991a34d759f9",
|
||||||
time: "2022-05-19T16:45:57.122Z",
|
time: "2022-05-19T16:45:57.122Z",
|
||||||
organization_id: "fc0774ce-cc9e-48d4-80ae-88f7a4d4a8b0",
|
organization_id: MockOrganization.id,
|
||||||
ip: "127.0.0.1",
|
ip: "127.0.0.1",
|
||||||
user_agent:
|
user_agent:
|
||||||
'"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/104.0.0.0 Safari/537.36"',
|
'"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/104.0.0.0 Safari/537.36"',
|
||||||
@ -1462,6 +1462,42 @@ export const mockParameterSchema = (
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const MockParameterSchemas: TypesGen.ParameterSchema[] = [
|
||||||
|
mockParameterSchema({
|
||||||
|
name: "region",
|
||||||
|
default_source_value: "🏈 US Central",
|
||||||
|
description: "Where would you like your workspace to live?",
|
||||||
|
redisplay_value: true,
|
||||||
|
validation_contains: [
|
||||||
|
"🏈 US Central",
|
||||||
|
"⚽ Brazil East",
|
||||||
|
"💶 EU West",
|
||||||
|
"🦘 Australia South",
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
mockParameterSchema({
|
||||||
|
name: "instance_size",
|
||||||
|
default_source_value: "Big",
|
||||||
|
description: "How large should you instance be?",
|
||||||
|
validation_contains: ["Small", "Medium", "Big"],
|
||||||
|
redisplay_value: true,
|
||||||
|
}),
|
||||||
|
mockParameterSchema({
|
||||||
|
name: "instance_size",
|
||||||
|
default_source_value: "Big",
|
||||||
|
description: "How large should your instance be?",
|
||||||
|
validation_contains: ["Small", "Medium", "Big"],
|
||||||
|
redisplay_value: true,
|
||||||
|
}),
|
||||||
|
mockParameterSchema({
|
||||||
|
name: "disable_docker",
|
||||||
|
description: "Disable Docker?",
|
||||||
|
validation_value_type: "bool",
|
||||||
|
default_source_value: "false",
|
||||||
|
redisplay_value: true,
|
||||||
|
}),
|
||||||
|
]
|
||||||
|
|
||||||
export const MockTemplateVersionGitAuth: TypesGen.TemplateVersionGitAuth = {
|
export const MockTemplateVersionGitAuth: TypesGen.TemplateVersionGitAuth = {
|
||||||
id: "github",
|
id: "github",
|
||||||
type: "github",
|
type: "github",
|
||||||
|
@ -11,10 +11,10 @@ import { i18n } from "i18n"
|
|||||||
import { FC, ReactElement } from "react"
|
import { FC, ReactElement } from "react"
|
||||||
import { I18nextProvider } from "react-i18next"
|
import { I18nextProvider } from "react-i18next"
|
||||||
import {
|
import {
|
||||||
MemoryRouter,
|
|
||||||
Route,
|
|
||||||
Routes,
|
|
||||||
unstable_HistoryRouter as HistoryRouter,
|
unstable_HistoryRouter as HistoryRouter,
|
||||||
|
RouterProvider,
|
||||||
|
createMemoryRouter,
|
||||||
|
RouteObject,
|
||||||
} from "react-router-dom"
|
} from "react-router-dom"
|
||||||
import { RequireAuth } from "../components/RequireAuth/RequireAuth"
|
import { RequireAuth } from "../components/RequireAuth/RequireAuth"
|
||||||
import { MockUser } from "./entities"
|
import { MockUser } from "./entities"
|
||||||
@ -35,41 +35,53 @@ export const render = (component: ReactElement): RenderResult => {
|
|||||||
return wrappedRender(<WrapperComponent>{component}</WrapperComponent>)
|
return wrappedRender(<WrapperComponent>{component}</WrapperComponent>)
|
||||||
}
|
}
|
||||||
|
|
||||||
type RenderWithAuthResult = RenderResult & { user: typeof MockUser }
|
type RenderWithAuthOptions = {
|
||||||
|
// The current URL, /workspaces/123
|
||||||
|
route?: string
|
||||||
|
// The route path, /workspaces/:workspaceId
|
||||||
|
path?: string
|
||||||
|
// Extra routes to add to the router. It is helpful when having redirecting
|
||||||
|
// routes or multiple routes during the test flow
|
||||||
|
extraRoutes?: RouteObject[]
|
||||||
|
// The same as extraRoutes but for routes that don't require authentication
|
||||||
|
nonAuthenticatedRoutes?: RouteObject[]
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
* @param ui The component to render and test
|
|
||||||
* @param options Can contain `route`, the URL to use, such as /users/user1, and `path`,
|
|
||||||
* such as /users/:userid. When there are no parameters, they are the same and you can just supply `route`.
|
|
||||||
*/
|
|
||||||
export function renderWithAuth(
|
export function renderWithAuth(
|
||||||
ui: JSX.Element,
|
element: JSX.Element,
|
||||||
{
|
{
|
||||||
|
path = "/",
|
||||||
route = "/",
|
route = "/",
|
||||||
path,
|
extraRoutes = [],
|
||||||
routes,
|
nonAuthenticatedRoutes = [],
|
||||||
}: { route?: string; path?: string; routes?: JSX.Element } = {},
|
}: RenderWithAuthOptions = {},
|
||||||
): RenderWithAuthResult {
|
) {
|
||||||
|
const routes: RouteObject[] = [
|
||||||
|
{
|
||||||
|
element: <RequireAuth />,
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
element: <DashboardLayout />,
|
||||||
|
children: [{ path, element }, ...extraRoutes],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
...nonAuthenticatedRoutes,
|
||||||
|
]
|
||||||
|
|
||||||
|
const router = createMemoryRouter(routes, { initialEntries: [route] })
|
||||||
|
|
||||||
const renderResult = wrappedRender(
|
const renderResult = wrappedRender(
|
||||||
<I18nextProvider i18n={i18n}>
|
<I18nextProvider i18n={i18n}>
|
||||||
<AppProviders>
|
<AppProviders>
|
||||||
<MemoryRouter initialEntries={[route]}>
|
<RouterProvider router={router} />
|
||||||
<Routes>
|
|
||||||
<Route element={<RequireAuth />}>
|
|
||||||
<Route element={<DashboardLayout />}>
|
|
||||||
<Route path={path ?? route} element={ui} />
|
|
||||||
</Route>
|
|
||||||
</Route>
|
|
||||||
{routes}
|
|
||||||
</Routes>
|
|
||||||
</MemoryRouter>
|
|
||||||
</AppProviders>
|
</AppProviders>
|
||||||
</I18nextProvider>,
|
</I18nextProvider>,
|
||||||
)
|
)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
user: MockUser,
|
user: MockUser,
|
||||||
|
router,
|
||||||
...renderResult,
|
...renderResult,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -100,3 +100,4 @@ export const templateDisplayNameValidator = (
|
|||||||
templateDisplayNameMaxLength,
|
templateDisplayNameMaxLength,
|
||||||
Language.nameTooLong(displayName, templateDisplayNameMaxLength),
|
Language.nameTooLong(displayName, templateDisplayNameMaxLength),
|
||||||
)
|
)
|
||||||
|
.optional()
|
||||||
|
@ -6,15 +6,19 @@ import {
|
|||||||
getTemplateVersionSchema,
|
getTemplateVersionSchema,
|
||||||
uploadTemplateFile,
|
uploadTemplateFile,
|
||||||
getTemplateVersionLogs,
|
getTemplateVersionLogs,
|
||||||
|
getTemplateVersionVariables,
|
||||||
} from "api/api"
|
} from "api/api"
|
||||||
import {
|
import {
|
||||||
CreateTemplateVersionRequest,
|
CreateTemplateVersionRequest,
|
||||||
ParameterSchema,
|
ParameterSchema,
|
||||||
|
ProvisionerJob,
|
||||||
ProvisionerJobLog,
|
ProvisionerJobLog,
|
||||||
Template,
|
Template,
|
||||||
TemplateExample,
|
TemplateExample,
|
||||||
TemplateVersion,
|
TemplateVersion,
|
||||||
|
TemplateVersionVariable,
|
||||||
UploadResponse,
|
UploadResponse,
|
||||||
|
VariableValue,
|
||||||
} from "api/typesGenerated"
|
} from "api/typesGenerated"
|
||||||
import { displayError } from "components/GlobalSnackbar/utils"
|
import { displayError } from "components/GlobalSnackbar/utils"
|
||||||
import { delay } from "util/delay"
|
import { delay } from "util/delay"
|
||||||
@ -24,7 +28,7 @@ import { assign, createMachine } from "xstate"
|
|||||||
// 1. upload template tar or use the example ID
|
// 1. upload template tar or use the example ID
|
||||||
// 2. create template version
|
// 2. create template version
|
||||||
// 3. wait for it to complete
|
// 3. wait for it to complete
|
||||||
// 4. if the job failed with the missing parameter error then:
|
// 4. verify if template has missing parameters or variables
|
||||||
// a. prompt for params
|
// a. prompt for params
|
||||||
// b. create template version again with the same file hash
|
// b. create template version again with the same file hash
|
||||||
// c. wait for it to complete
|
// c. wait for it to complete
|
||||||
@ -39,6 +43,7 @@ export interface CreateTemplateData {
|
|||||||
default_ttl_hours: number
|
default_ttl_hours: number
|
||||||
allow_user_cancel_workspace_jobs: boolean
|
allow_user_cancel_workspace_jobs: boolean
|
||||||
parameter_values_by_name?: Record<string, string>
|
parameter_values_by_name?: Record<string, string>
|
||||||
|
user_variable_values?: VariableValue[]
|
||||||
}
|
}
|
||||||
interface CreateTemplateContext {
|
interface CreateTemplateContext {
|
||||||
organizationId: string
|
organizationId: string
|
||||||
@ -50,6 +55,7 @@ interface CreateTemplateContext {
|
|||||||
version?: TemplateVersion
|
version?: TemplateVersion
|
||||||
templateData?: CreateTemplateData
|
templateData?: CreateTemplateData
|
||||||
parameters?: ParameterSchema[]
|
parameters?: ParameterSchema[]
|
||||||
|
variables?: TemplateVersionVariable[]
|
||||||
// file is used in the FE to show the filename and some other visual stuff
|
// file is used in the FE to show the filename and some other visual stuff
|
||||||
// uploadedFile is the response from the server to use in the API
|
// uploadedFile is the response from the server to use in the API
|
||||||
file?: File
|
file?: File
|
||||||
@ -78,7 +84,7 @@ export const createTemplateMachine =
|
|||||||
createFirstVersion: {
|
createFirstVersion: {
|
||||||
data: TemplateVersion
|
data: TemplateVersion
|
||||||
}
|
}
|
||||||
createVersionWithParameters: {
|
createVersionWithParametersAndVariables: {
|
||||||
data: TemplateVersion
|
data: TemplateVersion
|
||||||
}
|
}
|
||||||
waitForJobToBeCompleted: {
|
waitForJobToBeCompleted: {
|
||||||
@ -87,6 +93,12 @@ export const createTemplateMachine =
|
|||||||
loadParameterSchema: {
|
loadParameterSchema: {
|
||||||
data: ParameterSchema[]
|
data: ParameterSchema[]
|
||||||
}
|
}
|
||||||
|
checkParametersAndVariables: {
|
||||||
|
data: {
|
||||||
|
parameters?: ParameterSchema[]
|
||||||
|
variables?: TemplateVersionVariable[]
|
||||||
|
}
|
||||||
|
}
|
||||||
createTemplate: {
|
createTemplate: {
|
||||||
data: Template
|
data: Template
|
||||||
}
|
}
|
||||||
@ -170,17 +182,15 @@ export const createTemplateMachine =
|
|||||||
invoke: {
|
invoke: {
|
||||||
src: "waitForJobToBeCompleted",
|
src: "waitForJobToBeCompleted",
|
||||||
onDone: [
|
onDone: [
|
||||||
{
|
|
||||||
target: "loadingMissingParameters",
|
|
||||||
cond: "hasMissingParameters",
|
|
||||||
actions: ["assignVersion"],
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
target: "loadingVersionLogs",
|
target: "loadingVersionLogs",
|
||||||
actions: ["assignJobError", "assignVersion"],
|
actions: ["assignJobError", "assignVersion"],
|
||||||
cond: "hasFailed",
|
cond: "hasFailed",
|
||||||
},
|
},
|
||||||
{ target: "creatingTemplate", actions: ["assignVersion"] },
|
{
|
||||||
|
target: "checkingParametersAndVariables",
|
||||||
|
actions: ["assignVersion"],
|
||||||
|
},
|
||||||
],
|
],
|
||||||
onError: {
|
onError: {
|
||||||
target: "#createTemplate.idle",
|
target: "#createTemplate.idle",
|
||||||
@ -189,26 +199,19 @@ export const createTemplateMachine =
|
|||||||
},
|
},
|
||||||
tags: ["submitting"],
|
tags: ["submitting"],
|
||||||
},
|
},
|
||||||
loadingVersionLogs: {
|
checkingParametersAndVariables: {
|
||||||
invoke: {
|
invoke: {
|
||||||
src: "loadVersionLogs",
|
src: "checkParametersAndVariables",
|
||||||
onDone: {
|
onDone: [
|
||||||
target: "#createTemplate.idle",
|
{
|
||||||
actions: ["assignJobLogs"],
|
target: "creatingTemplate",
|
||||||
|
cond: "hasNoParametersOrVariables",
|
||||||
},
|
},
|
||||||
onError: {
|
{
|
||||||
target: "#createTemplate.idle",
|
target: "promptParametersAndVariables",
|
||||||
actions: ["assignError"],
|
actions: ["assignParametersAndVariables"],
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
loadingMissingParameters: {
|
|
||||||
invoke: {
|
|
||||||
src: "loadParameterSchema",
|
|
||||||
onDone: {
|
|
||||||
target: "promptParameters",
|
|
||||||
actions: ["assignParameters"],
|
|
||||||
},
|
},
|
||||||
|
],
|
||||||
onError: {
|
onError: {
|
||||||
target: "#createTemplate.idle",
|
target: "#createTemplate.idle",
|
||||||
actions: ["assignError"],
|
actions: ["assignError"],
|
||||||
@ -216,24 +219,24 @@ export const createTemplateMachine =
|
|||||||
},
|
},
|
||||||
tags: ["submitting"],
|
tags: ["submitting"],
|
||||||
},
|
},
|
||||||
promptParameters: {
|
promptParametersAndVariables: {
|
||||||
on: {
|
on: {
|
||||||
CREATE: {
|
CREATE: {
|
||||||
target: "creatingVersionWithParameters",
|
target: "creatingVersionWithParametersAndVariables",
|
||||||
actions: ["assignTemplateData"],
|
actions: ["assignTemplateData"],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
creatingVersionWithParameters: {
|
creatingVersionWithParametersAndVariables: {
|
||||||
invoke: {
|
invoke: {
|
||||||
src: "createVersionWithParameters",
|
src: "createVersionWithParametersAndVariables",
|
||||||
onDone: {
|
onDone: {
|
||||||
target: "waitingForJobToBeCompleted",
|
target: "waitingForJobToBeCompleted",
|
||||||
actions: ["assignVersion"],
|
actions: ["assignVersion"],
|
||||||
},
|
},
|
||||||
onError: {
|
onError: {
|
||||||
actions: ["assignError"],
|
actions: ["assignError"],
|
||||||
target: "promptParameters",
|
target: "promptParametersAndVariables",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
tags: ["submitting"],
|
tags: ["submitting"],
|
||||||
@ -255,6 +258,19 @@ export const createTemplateMachine =
|
|||||||
created: {
|
created: {
|
||||||
type: "final",
|
type: "final",
|
||||||
},
|
},
|
||||||
|
loadingVersionLogs: {
|
||||||
|
invoke: {
|
||||||
|
src: "loadVersionLogs",
|
||||||
|
onDone: {
|
||||||
|
target: "#createTemplate.idle",
|
||||||
|
actions: ["assignJobLogs"],
|
||||||
|
},
|
||||||
|
onError: {
|
||||||
|
target: "#createTemplate.idle",
|
||||||
|
actions: ["assignError"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -300,7 +316,7 @@ export const createTemplateMachine =
|
|||||||
|
|
||||||
throw new Error("No file or example provided")
|
throw new Error("No file or example provided")
|
||||||
},
|
},
|
||||||
createVersionWithParameters: async ({
|
createVersionWithParametersAndVariables: async ({
|
||||||
organizationId,
|
organizationId,
|
||||||
parameters,
|
parameters,
|
||||||
templateData,
|
templateData,
|
||||||
@ -313,11 +329,11 @@ export const createTemplateMachine =
|
|||||||
throw new Error("No template data defined")
|
throw new Error("No template data defined")
|
||||||
}
|
}
|
||||||
|
|
||||||
const { parameter_values_by_name } = templateData
|
|
||||||
// Get parameter values if they are needed/present
|
// Get parameter values if they are needed/present
|
||||||
const parameterValues: CreateTemplateVersionRequest["parameter_values"] =
|
const parameterValues: CreateTemplateVersionRequest["parameter_values"] =
|
||||||
[]
|
[]
|
||||||
if (parameters) {
|
if (parameters) {
|
||||||
|
const { parameter_values_by_name } = templateData
|
||||||
parameters.forEach((schema) => {
|
parameters.forEach((schema) => {
|
||||||
const value = parameter_values_by_name?.[schema.name]
|
const value = parameter_values_by_name?.[schema.name]
|
||||||
parameterValues.push({
|
parameterValues.push({
|
||||||
@ -334,6 +350,7 @@ export const createTemplateMachine =
|
|||||||
file_id: version.job.file_id,
|
file_id: version.job.file_id,
|
||||||
provisioner: "terraform",
|
provisioner: "terraform",
|
||||||
parameter_values: parameterValues,
|
parameter_values: parameterValues,
|
||||||
|
user_variable_values: templateData.user_variable_values,
|
||||||
tags: {},
|
tags: {},
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
@ -342,24 +359,48 @@ export const createTemplateMachine =
|
|||||||
throw new Error("Version not defined")
|
throw new Error("Version not defined")
|
||||||
}
|
}
|
||||||
|
|
||||||
let status = version.job.status
|
let job = version.job
|
||||||
while (["pending", "running"].includes(status)) {
|
while (isPendingOrRunning(job)) {
|
||||||
version = await getTemplateVersion(version.id)
|
version = await getTemplateVersion(version.id)
|
||||||
status = version.job.status
|
job = version.job
|
||||||
|
|
||||||
// Delay the verification in two seconds to not overload the server
|
// Delay the verification in two seconds to not overload the server
|
||||||
// with too many requests Maybe at some point we could have a
|
// with too many requests Maybe at some point we could have a
|
||||||
// websocket for template version Also, preferred doing this way to
|
// websocket for template version Also, preferred doing this way to
|
||||||
// avoid a new state since we don't need to reflect it on the UI
|
// avoid a new state since we don't need to reflect it on the UI
|
||||||
|
if (isPendingOrRunning(job)) {
|
||||||
await delay(2_000)
|
await delay(2_000)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
return version
|
return version
|
||||||
},
|
},
|
||||||
loadParameterSchema: async ({ version }) => {
|
checkParametersAndVariables: async ({ version }) => {
|
||||||
if (!version) {
|
if (!version) {
|
||||||
throw new Error("Version not defined")
|
throw new Error("Version not defined")
|
||||||
}
|
}
|
||||||
|
|
||||||
return getTemplateVersionSchema(version.id)
|
let promiseParameter: Promise<ParameterSchema[]> | undefined =
|
||||||
|
undefined
|
||||||
|
let promiseVariables: Promise<TemplateVersionVariable[]> | undefined =
|
||||||
|
undefined
|
||||||
|
|
||||||
|
if (isMissingParameter(version)) {
|
||||||
|
promiseParameter = getTemplateVersionSchema(version.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isMissingVariables(version)) {
|
||||||
|
promiseVariables = getTemplateVersionVariables(version.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
const [parameters, variables] = await Promise.all([
|
||||||
|
promiseParameter,
|
||||||
|
promiseVariables,
|
||||||
|
])
|
||||||
|
|
||||||
|
return {
|
||||||
|
parameters,
|
||||||
|
variables,
|
||||||
|
}
|
||||||
},
|
},
|
||||||
createTemplate: async ({ organizationId, version, templateData }) => {
|
createTemplate: async ({ organizationId, version, templateData }) => {
|
||||||
if (!version) {
|
if (!version) {
|
||||||
@ -401,7 +442,10 @@ export const createTemplateMachine =
|
|||||||
}),
|
}),
|
||||||
assignVersion: assign({ version: (_, { data }) => data }),
|
assignVersion: assign({ version: (_, { data }) => data }),
|
||||||
assignTemplateData: assign({ templateData: (_, { data }) => data }),
|
assignTemplateData: assign({ templateData: (_, { data }) => data }),
|
||||||
assignParameters: assign({ parameters: (_, { data }) => data }),
|
assignParametersAndVariables: assign({
|
||||||
|
parameters: (_, { data }) => data.parameters,
|
||||||
|
variables: (_, { data }) => data.variables,
|
||||||
|
}),
|
||||||
assignFile: assign({ file: (_, { file }) => file }),
|
assignFile: assign({ file: (_, { file }) => file }),
|
||||||
assignUploadResponse: assign({ uploadResponse: (_, { data }) => data }),
|
assignUploadResponse: assign({ uploadResponse: (_, { data }) => data }),
|
||||||
removeFile: assign({
|
removeFile: assign({
|
||||||
@ -414,11 +458,31 @@ export const createTemplateMachine =
|
|||||||
isExampleProvided: ({ exampleId }) => Boolean(exampleId),
|
isExampleProvided: ({ exampleId }) => Boolean(exampleId),
|
||||||
isNotUsingExample: ({ exampleId }) => !exampleId,
|
isNotUsingExample: ({ exampleId }) => !exampleId,
|
||||||
hasFile: ({ file }) => Boolean(file),
|
hasFile: ({ file }) => Boolean(file),
|
||||||
hasFailed: (_, { data }) => data.job.status === "failed",
|
hasFailed: (_, { data }) =>
|
||||||
hasMissingParameters: (_, { data }) =>
|
|
||||||
Boolean(
|
Boolean(
|
||||||
data.job.error && data.job.error.includes("missing parameter"),
|
data.job.status === "failed" &&
|
||||||
|
!isMissingParameter(data) &&
|
||||||
|
!isMissingVariables(data),
|
||||||
),
|
),
|
||||||
|
hasNoParametersOrVariables: (_, { data }) =>
|
||||||
|
data.parameters === undefined && data.variables === undefined,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const isMissingParameter = (version: TemplateVersion) => {
|
||||||
|
return Boolean(
|
||||||
|
version.job.error && version.job.error.includes("missing parameter"),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const isMissingVariables = (version: TemplateVersion) => {
|
||||||
|
return Boolean(
|
||||||
|
version.job.error &&
|
||||||
|
version.job.error.includes("required template variables"),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const isPendingOrRunning = (job: ProvisionerJob) => {
|
||||||
|
return job.status === "pending" || job.status === "running"
|
||||||
|
}
|
||||||
|
@ -1844,6 +1844,41 @@
|
|||||||
resolved "https://registry.yarnpkg.com/@remix-run/router/-/router-1.0.1.tgz#88d7ac31811ab0cef14aaaeae2a0474923b278bc"
|
resolved "https://registry.yarnpkg.com/@remix-run/router/-/router-1.0.1.tgz#88d7ac31811ab0cef14aaaeae2a0474923b278bc"
|
||||||
integrity sha512-eBV5rvW4dRFOU1eajN7FmYxjAIVz/mRHgUE9En9mBn6m3mulK3WTR5C3iQhL9MZ14rWAq+xOlEaCkDiW0/heOg==
|
integrity sha512-eBV5rvW4dRFOU1eajN7FmYxjAIVz/mRHgUE9En9mBn6m3mulK3WTR5C3iQhL9MZ14rWAq+xOlEaCkDiW0/heOg==
|
||||||
|
|
||||||
|
"@remix-run/web-blob@^3.0.4":
|
||||||
|
version "3.0.4"
|
||||||
|
resolved "https://registry.yarnpkg.com/@remix-run/web-blob/-/web-blob-3.0.4.tgz#99c67b9d0fb641bd0c07d267fd218ae5aa4ae5ed"
|
||||||
|
integrity sha512-AfegzZvSSDc+LwnXV+SwROTrDtoLiPxeFW+jxgvtDAnkuCX1rrzmVJ6CzqZ1Ai0bVfmJadkG5GxtAfYclpPmgw==
|
||||||
|
dependencies:
|
||||||
|
"@remix-run/web-stream" "^1.0.0"
|
||||||
|
web-encoding "1.1.5"
|
||||||
|
|
||||||
|
"@remix-run/web-fetch@4.3.2":
|
||||||
|
version "4.3.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/@remix-run/web-fetch/-/web-fetch-4.3.2.tgz#193758bb7a301535540f0e3a86c743283f81cf56"
|
||||||
|
integrity sha512-aRNaaa0Fhyegv/GkJ/qsxMhXvyWGjPNgCKrStCvAvV1XXphntZI0nQO/Fl02LIQg3cGL8lDiOXOS1gzqDOlG5w==
|
||||||
|
dependencies:
|
||||||
|
"@remix-run/web-blob" "^3.0.4"
|
||||||
|
"@remix-run/web-form-data" "^3.0.3"
|
||||||
|
"@remix-run/web-stream" "^1.0.3"
|
||||||
|
"@web3-storage/multipart-parser" "^1.0.0"
|
||||||
|
abort-controller "^3.0.0"
|
||||||
|
data-uri-to-buffer "^3.0.1"
|
||||||
|
mrmime "^1.0.0"
|
||||||
|
|
||||||
|
"@remix-run/web-form-data@^3.0.3":
|
||||||
|
version "3.0.4"
|
||||||
|
resolved "https://registry.yarnpkg.com/@remix-run/web-form-data/-/web-form-data-3.0.4.tgz#18c5795edaffbc88c320a311766dc04644125bab"
|
||||||
|
integrity sha512-UMF1jg9Vu9CLOf8iHBdY74Mm3PUvMW8G/XZRJE56SxKaOFWGSWlfxfG+/a3boAgHFLTkP7K4H1PxlRugy1iQtw==
|
||||||
|
dependencies:
|
||||||
|
web-encoding "1.1.5"
|
||||||
|
|
||||||
|
"@remix-run/web-stream@^1.0.0", "@remix-run/web-stream@^1.0.3":
|
||||||
|
version "1.0.3"
|
||||||
|
resolved "https://registry.yarnpkg.com/@remix-run/web-stream/-/web-stream-1.0.3.tgz#3284a6a45675d1455c4d9c8f31b89225c9006438"
|
||||||
|
integrity sha512-wlezlJaA5NF6SsNMiwQnnAW6tnPzQ5I8qk0Y0pSohm0eHKa2FQ1QhEKLVVcDDu02TmkfHgnux0igNfeYhDOXiA==
|
||||||
|
dependencies:
|
||||||
|
web-streams-polyfill "^3.1.1"
|
||||||
|
|
||||||
"@sinclair/typebox@^0.24.1":
|
"@sinclair/typebox@^0.24.1":
|
||||||
version "0.24.51"
|
version "0.24.51"
|
||||||
resolved "https://registry.yarnpkg.com/@sinclair/typebox/-/typebox-0.24.51.tgz#645f33fe4e02defe26f2f5c0410e1c094eac7f5f"
|
resolved "https://registry.yarnpkg.com/@sinclair/typebox/-/typebox-0.24.51.tgz#645f33fe4e02defe26f2f5c0410e1c094eac7f5f"
|
||||||
@ -3623,6 +3658,11 @@
|
|||||||
magic-string "^0.26.2"
|
magic-string "^0.26.2"
|
||||||
react-refresh "^0.14.0"
|
react-refresh "^0.14.0"
|
||||||
|
|
||||||
|
"@web3-storage/multipart-parser@^1.0.0":
|
||||||
|
version "1.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/@web3-storage/multipart-parser/-/multipart-parser-1.0.0.tgz#6b69dc2a32a5b207ba43e556c25cc136a56659c4"
|
||||||
|
integrity sha512-BEO6al7BYqcnfX15W2cnGR+Q566ACXAT9UQykORCWW80lmkpWsnEob6zJS1ZVBKsSJC8+7vJkHwlp+lXG1UCdw==
|
||||||
|
|
||||||
"@webassemblyjs/ast@1.11.1":
|
"@webassemblyjs/ast@1.11.1":
|
||||||
version "1.11.1"
|
version "1.11.1"
|
||||||
resolved "https://registry.yarnpkg.com/@webassemblyjs/ast/-/ast-1.11.1.tgz#2bfd767eae1a6996f432ff7e8d7fc75679c0b6a7"
|
resolved "https://registry.yarnpkg.com/@webassemblyjs/ast/-/ast-1.11.1.tgz#2bfd767eae1a6996f432ff7e8d7fc75679c0b6a7"
|
||||||
@ -4055,6 +4095,13 @@ abbrev@1:
|
|||||||
resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.1.1.tgz#f8f2c887ad10bf67f634f005b6987fed3179aac8"
|
resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.1.1.tgz#f8f2c887ad10bf67f634f005b6987fed3179aac8"
|
||||||
integrity sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==
|
integrity sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==
|
||||||
|
|
||||||
|
abort-controller@^3.0.0:
|
||||||
|
version "3.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/abort-controller/-/abort-controller-3.0.0.tgz#eaf54d53b62bae4138e809ca225c8439a6efb392"
|
||||||
|
integrity sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==
|
||||||
|
dependencies:
|
||||||
|
event-target-shim "^5.0.0"
|
||||||
|
|
||||||
accepts@~1.3.5, accepts@~1.3.8:
|
accepts@~1.3.5, accepts@~1.3.8:
|
||||||
version "1.3.8"
|
version "1.3.8"
|
||||||
resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.8.tgz#0bf0be125b67014adcb0b0921e62db7bffe16b2e"
|
resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.8.tgz#0bf0be125b67014adcb0b0921e62db7bffe16b2e"
|
||||||
@ -5887,6 +5934,11 @@ damerau-levenshtein@^1.0.8:
|
|||||||
resolved "https://registry.yarnpkg.com/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz#b43d286ccbd36bc5b2f7ed41caf2d0aba1f8a6e7"
|
resolved "https://registry.yarnpkg.com/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz#b43d286ccbd36bc5b2f7ed41caf2d0aba1f8a6e7"
|
||||||
integrity sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==
|
integrity sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==
|
||||||
|
|
||||||
|
data-uri-to-buffer@^3.0.1:
|
||||||
|
version "3.0.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/data-uri-to-buffer/-/data-uri-to-buffer-3.0.1.tgz#594b8973938c5bc2c33046535785341abc4f3636"
|
||||||
|
integrity sha512-WboRycPNsVw3B3TL559F7kuBUM4d8CgMEvk6xEJlOp7OBPjt6G7z8WMWlD2rOFZLk6OYfFIUGsCOWzcQH9K2og==
|
||||||
|
|
||||||
data-urls@^2.0.0:
|
data-urls@^2.0.0:
|
||||||
version "2.0.0"
|
version "2.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/data-urls/-/data-urls-2.0.0.tgz#156485a72963a970f5d5821aaf642bef2bf2db9b"
|
resolved "https://registry.yarnpkg.com/data-urls/-/data-urls-2.0.0.tgz#156485a72963a970f5d5821aaf642bef2bf2db9b"
|
||||||
@ -6978,6 +7030,11 @@ event-loop-spinner@^2.0.0, event-loop-spinner@^2.1.0:
|
|||||||
dependencies:
|
dependencies:
|
||||||
tslib "^2.1.0"
|
tslib "^2.1.0"
|
||||||
|
|
||||||
|
event-target-shim@^5.0.0:
|
||||||
|
version "5.0.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/event-target-shim/-/event-target-shim-5.0.1.tgz#5d4d3ebdf9583d63a5333ce2deb7480ab2b05789"
|
||||||
|
integrity sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==
|
||||||
|
|
||||||
events@^3.0.0, events@^3.2.0, events@^3.3.0:
|
events@^3.0.0, events@^3.2.0, events@^3.3.0:
|
||||||
version "3.3.0"
|
version "3.3.0"
|
||||||
resolved "https://registry.yarnpkg.com/events/-/events-3.3.0.tgz#31a95ad0a924e2d2c419a813aeb2c4e878ea7400"
|
resolved "https://registry.yarnpkg.com/events/-/events-3.3.0.tgz#31a95ad0a924e2d2c419a813aeb2c4e878ea7400"
|
||||||
@ -10862,6 +10919,11 @@ mri@^1.1.0:
|
|||||||
resolved "https://registry.yarnpkg.com/mri/-/mri-1.2.0.tgz#6721480fec2a11a4889861115a48b6cbe7cc8f0b"
|
resolved "https://registry.yarnpkg.com/mri/-/mri-1.2.0.tgz#6721480fec2a11a4889861115a48b6cbe7cc8f0b"
|
||||||
integrity sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==
|
integrity sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==
|
||||||
|
|
||||||
|
mrmime@^1.0.0:
|
||||||
|
version "1.0.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/mrmime/-/mrmime-1.0.1.tgz#5f90c825fad4bdd41dc914eff5d1a8cfdaf24f27"
|
||||||
|
integrity sha512-hzzEagAgDyoU1Q6yg5uI+AorQgdvMCur3FcKf7NhMKWsaYg+RnbTyHRa/9IlLF9rf455MOCtcqqrQQ83pPP7Uw==
|
||||||
|
|
||||||
ms@2.0.0:
|
ms@2.0.0:
|
||||||
version "2.0.0"
|
version "2.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8"
|
resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8"
|
||||||
@ -14653,7 +14715,7 @@ wcwidth@^1.0.1:
|
|||||||
dependencies:
|
dependencies:
|
||||||
defaults "^1.0.3"
|
defaults "^1.0.3"
|
||||||
|
|
||||||
web-encoding@^1.1.5:
|
web-encoding@1.1.5, web-encoding@^1.1.5:
|
||||||
version "1.1.5"
|
version "1.1.5"
|
||||||
resolved "https://registry.yarnpkg.com/web-encoding/-/web-encoding-1.1.5.tgz#fc810cf7667364a6335c939913f5051d3e0c4864"
|
resolved "https://registry.yarnpkg.com/web-encoding/-/web-encoding-1.1.5.tgz#fc810cf7667364a6335c939913f5051d3e0c4864"
|
||||||
integrity sha512-HYLeVCdJ0+lBYV2FvNZmv3HJ2Nt0QYXqZojk3d9FJOLkwnuhzM9tmamh8d7HPM8QqjKH8DeHkFTx+CFlWpZZDA==
|
integrity sha512-HYLeVCdJ0+lBYV2FvNZmv3HJ2Nt0QYXqZojk3d9FJOLkwnuhzM9tmamh8d7HPM8QqjKH8DeHkFTx+CFlWpZZDA==
|
||||||
@ -14667,6 +14729,11 @@ web-namespaces@^1.0.0:
|
|||||||
resolved "https://registry.yarnpkg.com/web-namespaces/-/web-namespaces-1.1.4.tgz#bc98a3de60dadd7faefc403d1076d529f5e030ec"
|
resolved "https://registry.yarnpkg.com/web-namespaces/-/web-namespaces-1.1.4.tgz#bc98a3de60dadd7faefc403d1076d529f5e030ec"
|
||||||
integrity sha512-wYxSGajtmoP4WxfejAPIr4l0fVh+jeMXZb08wNc0tMg6xsfZXj3cECqIK0G7ZAqUq0PP8WlMDtaOGVBTAWztNw==
|
integrity sha512-wYxSGajtmoP4WxfejAPIr4l0fVh+jeMXZb08wNc0tMg6xsfZXj3cECqIK0G7ZAqUq0PP8WlMDtaOGVBTAWztNw==
|
||||||
|
|
||||||
|
web-streams-polyfill@^3.1.1:
|
||||||
|
version "3.2.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/web-streams-polyfill/-/web-streams-polyfill-3.2.1.tgz#71c2718c52b45fd49dbeee88634b3a60ceab42a6"
|
||||||
|
integrity sha512-e0MO3wdXWKrLbL0DgGnUV7WHVuw9OUvL4hjgnPkIeEvESk74gAITi5G606JtZPp39cd8HA9VQzCIvA49LpPN5Q==
|
||||||
|
|
||||||
webidl-conversions@^3.0.0:
|
webidl-conversions@^3.0.0:
|
||||||
version "3.0.1"
|
version "3.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871"
|
resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871"
|
||||||
|
Reference in New Issue
Block a user