mirror of
https://github.com/coder/coder.git
synced 2026-09-22 21:22:17 +08:00
feat(site): add auto mode on create workspace form (#8651)
This commit is contained in:
@@ -95,6 +95,7 @@
|
||||
"ts-prune": "0.10.3",
|
||||
"tzdata": "1.0.30",
|
||||
"ua-parser-js": "1.0.33",
|
||||
"unique-names-generator": "4.7.1",
|
||||
"uuid": "9.0.0",
|
||||
"vite": "4.4.2",
|
||||
"xstate": "4.38.1",
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
MockTemplateVersionParameter2,
|
||||
MockTemplateVersionParameter3,
|
||||
MockTemplateVersionGitAuth,
|
||||
MockOrganization,
|
||||
} from "testHelpers/entities"
|
||||
import {
|
||||
renderWithAuth,
|
||||
@@ -217,4 +218,29 @@ describe("CreateWorkspacePage", () => {
|
||||
|
||||
await screen.findByText("You must authenticate to create a workspace!")
|
||||
})
|
||||
|
||||
it("auto create a workspace if uses mode=auto", async () => {
|
||||
const param = "first_parameter"
|
||||
const paramValue = "It works!"
|
||||
const createWorkspaceSpy = jest.spyOn(API, "createWorkspace")
|
||||
|
||||
renderWithAuth(<CreateWorkspacePage />, {
|
||||
route:
|
||||
"/templates/" +
|
||||
MockTemplate.name +
|
||||
`/workspace?param.${param}=${paramValue}&mode=auto`,
|
||||
path: "/templates/:template/workspace",
|
||||
})
|
||||
|
||||
await waitFor(() => {
|
||||
expect(createWorkspaceSpy).toBeCalledWith(
|
||||
MockOrganization.id,
|
||||
"me",
|
||||
expect.objectContaining({
|
||||
template_id: MockTemplate.id,
|
||||
rich_parameter_values: [{ name: param, value: paramValue }],
|
||||
}),
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,27 +1,47 @@
|
||||
import { useMachine } from "@xstate/react"
|
||||
import { TemplateVersionParameter } from "api/typesGenerated"
|
||||
import {
|
||||
Template,
|
||||
TemplateVersionGitAuth,
|
||||
TemplateVersionParameter,
|
||||
WorkspaceBuildParameter,
|
||||
} from "api/typesGenerated"
|
||||
import { useMe } from "hooks/useMe"
|
||||
import { useOrganizationId } from "hooks/useOrganizationId"
|
||||
import { FC } from "react"
|
||||
import { Helmet } from "react-helmet-async"
|
||||
import { useNavigate, useParams, useSearchParams } from "react-router-dom"
|
||||
import { pageTitle } from "utils/page"
|
||||
import { createWorkspaceMachine } from "xServices/createWorkspace/createWorkspaceXService"
|
||||
import {
|
||||
CreateWorkspaceErrors,
|
||||
CreateWorkspacePageView,
|
||||
} from "./CreateWorkspacePageView"
|
||||
CreateWSPermissions,
|
||||
CreateWorkspaceMode,
|
||||
createWorkspaceMachine,
|
||||
} from "xServices/createWorkspace/createWorkspaceXService"
|
||||
import { CreateWorkspacePageView } from "./CreateWorkspacePageView"
|
||||
import { Loader } from "components/Loader/Loader"
|
||||
import { ErrorAlert } from "components/Alert/ErrorAlert"
|
||||
import {
|
||||
uniqueNamesGenerator,
|
||||
animals,
|
||||
colors,
|
||||
NumberDictionary,
|
||||
} from "unique-names-generator"
|
||||
|
||||
const CreateWorkspacePage: FC = () => {
|
||||
const organizationId = useOrganizationId()
|
||||
const { template: templateName } = useParams() as { template: string }
|
||||
const navigate = useNavigate()
|
||||
const me = useMe()
|
||||
const navigate = useNavigate()
|
||||
const [searchParams] = useSearchParams()
|
||||
const defaultBuildParameters = getDefaultBuildParameters(searchParams)
|
||||
const mode = (searchParams.get("mode") ?? "form") as CreateWorkspaceMode
|
||||
const [createWorkspaceState, send] = useMachine(createWorkspaceMachine, {
|
||||
context: {
|
||||
organizationId,
|
||||
templateName,
|
||||
owner: me,
|
||||
mode,
|
||||
defaultBuildParameters,
|
||||
defaultName:
|
||||
mode === "auto" ? generateUniqueName() : searchParams.get("name") ?? "",
|
||||
},
|
||||
actions: {
|
||||
onCreateWorkspace: (_, event) => {
|
||||
@@ -29,83 +49,65 @@ const CreateWorkspacePage: FC = () => {
|
||||
},
|
||||
},
|
||||
})
|
||||
const {
|
||||
templates,
|
||||
templateParameters,
|
||||
templateGitAuth,
|
||||
selectedTemplate,
|
||||
getTemplateGitAuthError,
|
||||
getTemplatesError,
|
||||
createWorkspaceError,
|
||||
permissions,
|
||||
owner,
|
||||
} = createWorkspaceState.context
|
||||
const [searchParams] = useSearchParams()
|
||||
const defaultParameterValues = getDefaultParameterValues(searchParams)
|
||||
const name = getName(searchParams)
|
||||
const { template, error, parameters, permissions, gitAuth, defaultName } =
|
||||
createWorkspaceState.context
|
||||
const title = createWorkspaceState.matches("autoCreating")
|
||||
? "Creating workspace..."
|
||||
: "Create Workspace"
|
||||
|
||||
return (
|
||||
<>
|
||||
<Helmet>
|
||||
<title>{pageTitle("Create Workspace")}</title>
|
||||
<title>{pageTitle(title)}</title>
|
||||
</Helmet>
|
||||
<CreateWorkspacePageView
|
||||
name={name}
|
||||
defaultParameterValues={defaultParameterValues}
|
||||
loadingTemplates={createWorkspaceState.matches("gettingTemplates")}
|
||||
creatingWorkspace={createWorkspaceState.matches("creatingWorkspace")}
|
||||
hasTemplateErrors={createWorkspaceState.matches("error")}
|
||||
templateName={templateName}
|
||||
templates={templates}
|
||||
selectedTemplate={selectedTemplate}
|
||||
templateParameters={orderedTemplateParameters(templateParameters)}
|
||||
templateGitAuth={templateGitAuth}
|
||||
createWorkspaceErrors={{
|
||||
[CreateWorkspaceErrors.GET_TEMPLATES_ERROR]: getTemplatesError,
|
||||
[CreateWorkspaceErrors.CREATE_WORKSPACE_ERROR]: createWorkspaceError,
|
||||
[CreateWorkspaceErrors.GET_TEMPLATE_GITAUTH_ERROR]:
|
||||
getTemplateGitAuthError,
|
||||
}}
|
||||
canCreateForUser={permissions?.createWorkspaceForUser}
|
||||
owner={owner}
|
||||
setOwner={(user) => {
|
||||
send({
|
||||
type: "SELECT_OWNER",
|
||||
owner: user,
|
||||
})
|
||||
}}
|
||||
onCancel={() => {
|
||||
// Go back
|
||||
navigate(-1)
|
||||
}}
|
||||
onSubmit={(request) => {
|
||||
send({
|
||||
type: "CREATE_WORKSPACE",
|
||||
request,
|
||||
owner,
|
||||
})
|
||||
}}
|
||||
/>
|
||||
{Boolean(
|
||||
createWorkspaceState.matches("loadingFormData") ||
|
||||
createWorkspaceState.matches("autoCreating"),
|
||||
) && <Loader />}
|
||||
{createWorkspaceState.matches("loadError") && (
|
||||
<ErrorAlert error={error} />
|
||||
)}
|
||||
{createWorkspaceState.matches("idle") && (
|
||||
<CreateWorkspacePageView
|
||||
defaultName={defaultName}
|
||||
defaultOwner={me}
|
||||
defaultBuildParameters={defaultBuildParameters}
|
||||
error={error}
|
||||
template={template as Template}
|
||||
gitAuth={gitAuth as TemplateVersionGitAuth[]}
|
||||
permissions={permissions as CreateWSPermissions}
|
||||
parameters={parameters as TemplateVersionParameter[]}
|
||||
creatingWorkspace={createWorkspaceState.matches("creatingWorkspace")}
|
||||
onCancel={() => {
|
||||
navigate(-1)
|
||||
}}
|
||||
onSubmit={(request, owner) => {
|
||||
send({
|
||||
type: "CREATE_WORKSPACE",
|
||||
request,
|
||||
owner,
|
||||
})
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
const getName = (urlSearchParams: URLSearchParams): string => {
|
||||
return urlSearchParams.get("name") ?? ""
|
||||
}
|
||||
export default CreateWorkspacePage
|
||||
|
||||
const getDefaultParameterValues = (
|
||||
const getDefaultBuildParameters = (
|
||||
urlSearchParams: URLSearchParams,
|
||||
): Record<string, string> => {
|
||||
const paramValues: Record<string, string> = {}
|
||||
): WorkspaceBuildParameter[] => {
|
||||
const buildValues: WorkspaceBuildParameter[] = []
|
||||
Array.from(urlSearchParams.keys())
|
||||
.filter((key) => key.startsWith("param."))
|
||||
.forEach((key) => {
|
||||
const paramName = key.replace("param.", "")
|
||||
const paramValue = urlSearchParams.get(key)
|
||||
paramValues[paramName] = paramValue ?? ""
|
||||
const name = key.replace("param.", "")
|
||||
const value = urlSearchParams.get(key) ?? ""
|
||||
buildValues.push({ name, value })
|
||||
})
|
||||
return paramValues
|
||||
return buildValues
|
||||
}
|
||||
|
||||
export const orderedTemplateParameters = (
|
||||
@@ -122,4 +124,12 @@ export const orderedTemplateParameters = (
|
||||
return [...immutables, ...mutables]
|
||||
}
|
||||
|
||||
export default CreateWorkspacePage
|
||||
const generateUniqueName = () => {
|
||||
const numberDictionary = NumberDictionary.generate({ min: 0, max: 99 })
|
||||
return uniqueNamesGenerator({
|
||||
dictionaries: [colors, animals, numberDictionary],
|
||||
separator: "-",
|
||||
length: 3,
|
||||
style: "lowerCase",
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,64 +1,38 @@
|
||||
import { ComponentMeta, Story } from "@storybook/react"
|
||||
import { Meta, StoryObj } from "@storybook/react"
|
||||
import {
|
||||
mockApiError,
|
||||
MockTemplate,
|
||||
MockTemplateVersionParameter1,
|
||||
MockTemplateVersionParameter2,
|
||||
MockTemplateVersionParameter3,
|
||||
MockUser,
|
||||
} from "../../testHelpers/entities"
|
||||
import {
|
||||
CreateWorkspaceErrors,
|
||||
CreateWorkspacePageView,
|
||||
CreateWorkspacePageViewProps,
|
||||
} from "./CreateWorkspacePageView"
|
||||
import { CreateWorkspacePageView } from "./CreateWorkspacePageView"
|
||||
|
||||
export default {
|
||||
title: "pages/CreateWorkspacePageView",
|
||||
const meta: Meta<typeof CreateWorkspacePageView> = {
|
||||
title: "components/Alert",
|
||||
component: CreateWorkspacePageView,
|
||||
} as ComponentMeta<typeof CreateWorkspacePageView>
|
||||
|
||||
const Template: Story<CreateWorkspacePageViewProps> = (args) => (
|
||||
<CreateWorkspacePageView {...args} />
|
||||
)
|
||||
|
||||
export const NoParameters = Template.bind({})
|
||||
NoParameters.args = {
|
||||
templates: [MockTemplate],
|
||||
selectedTemplate: MockTemplate,
|
||||
createWorkspaceErrors: {},
|
||||
}
|
||||
|
||||
export const Parameters = Template.bind({})
|
||||
Parameters.args = {
|
||||
templates: [MockTemplate],
|
||||
selectedTemplate: MockTemplate,
|
||||
createWorkspaceErrors: {},
|
||||
}
|
||||
|
||||
export const RedisplayParameters = Template.bind({})
|
||||
RedisplayParameters.args = {
|
||||
templates: [MockTemplate],
|
||||
selectedTemplate: MockTemplate,
|
||||
createWorkspaceErrors: {},
|
||||
}
|
||||
|
||||
export const GetTemplatesError = Template.bind({})
|
||||
GetTemplatesError.args = {
|
||||
...Parameters.args,
|
||||
createWorkspaceErrors: {
|
||||
[CreateWorkspaceErrors.GET_TEMPLATES_ERROR]: mockApiError({
|
||||
message: "Failed to fetch templates.",
|
||||
detail: "You do not have permission to access this resource.",
|
||||
}),
|
||||
args: {
|
||||
defaultName: "",
|
||||
defaultOwner: MockUser,
|
||||
defaultBuildParameters: [],
|
||||
template: MockTemplate,
|
||||
parameters: [],
|
||||
gitAuth: [],
|
||||
permissions: {
|
||||
createWorkspaceForUser: true,
|
||||
},
|
||||
},
|
||||
hasTemplateErrors: true,
|
||||
}
|
||||
|
||||
export const CreateWorkspaceError = Template.bind({})
|
||||
CreateWorkspaceError.args = {
|
||||
...Parameters.args,
|
||||
createWorkspaceErrors: {
|
||||
[CreateWorkspaceErrors.CREATE_WORKSPACE_ERROR]: mockApiError({
|
||||
export default meta
|
||||
type Story = StoryObj<typeof CreateWorkspacePageView>
|
||||
|
||||
export const NoParameters: Story = {}
|
||||
|
||||
export const CreateWorkspaceError: Story = {
|
||||
args: {
|
||||
error: mockApiError({
|
||||
message:
|
||||
'Workspace "test" already exists in the "docker-amd64" template.',
|
||||
validations: [
|
||||
@@ -69,72 +43,64 @@ CreateWorkspaceError.args = {
|
||||
],
|
||||
}),
|
||||
},
|
||||
initialTouched: {
|
||||
name: true,
|
||||
}
|
||||
|
||||
export const Parameters: Story = {
|
||||
args: {
|
||||
parameters: [
|
||||
MockTemplateVersionParameter1,
|
||||
MockTemplateVersionParameter2,
|
||||
MockTemplateVersionParameter3,
|
||||
{
|
||||
name: "Region",
|
||||
required: false,
|
||||
description: "",
|
||||
description_plaintext: "",
|
||||
type: "string",
|
||||
mutable: false,
|
||||
default_value: "",
|
||||
icon: "/emojis/1f30e.png",
|
||||
options: [
|
||||
{
|
||||
name: "Pittsburgh",
|
||||
description: "",
|
||||
value: "us-pittsburgh",
|
||||
icon: "/emojis/1f1fa-1f1f8.png",
|
||||
},
|
||||
{
|
||||
name: "Helsinki",
|
||||
description: "",
|
||||
value: "eu-helsinki",
|
||||
icon: "/emojis/1f1eb-1f1ee.png",
|
||||
},
|
||||
{
|
||||
name: "Sydney",
|
||||
description: "",
|
||||
value: "ap-sydney",
|
||||
icon: "/emojis/1f1e6-1f1fa.png",
|
||||
},
|
||||
],
|
||||
ephemeral: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
||||
|
||||
export const RichParameters = Template.bind({})
|
||||
RichParameters.args = {
|
||||
templates: [MockTemplate],
|
||||
selectedTemplate: MockTemplate,
|
||||
templateParameters: [
|
||||
MockTemplateVersionParameter1,
|
||||
MockTemplateVersionParameter2,
|
||||
MockTemplateVersionParameter3,
|
||||
{
|
||||
name: "Region",
|
||||
required: false,
|
||||
description: "",
|
||||
description_plaintext: "",
|
||||
type: "string",
|
||||
mutable: false,
|
||||
default_value: "",
|
||||
icon: "/emojis/1f30e.png",
|
||||
options: [
|
||||
{
|
||||
name: "Pittsburgh",
|
||||
description: "",
|
||||
value: "us-pittsburgh",
|
||||
icon: "/emojis/1f1fa-1f1f8.png",
|
||||
},
|
||||
{
|
||||
name: "Helsinki",
|
||||
description: "",
|
||||
value: "eu-helsinki",
|
||||
icon: "/emojis/1f1eb-1f1ee.png",
|
||||
},
|
||||
{
|
||||
name: "Sydney",
|
||||
description: "",
|
||||
value: "ap-sydney",
|
||||
icon: "/emojis/1f1e6-1f1fa.png",
|
||||
},
|
||||
],
|
||||
ephemeral: false,
|
||||
},
|
||||
],
|
||||
createWorkspaceErrors: {},
|
||||
}
|
||||
|
||||
export const GitAuth = Template.bind({})
|
||||
GitAuth.args = {
|
||||
templates: [MockTemplate],
|
||||
selectedTemplate: MockTemplate,
|
||||
createWorkspaceErrors: {},
|
||||
templateParameters: [],
|
||||
templateGitAuth: [
|
||||
{
|
||||
id: "github",
|
||||
type: "github",
|
||||
authenticated: false,
|
||||
authenticate_url: "",
|
||||
},
|
||||
{
|
||||
id: "gitlab",
|
||||
type: "gitlab",
|
||||
authenticated: true,
|
||||
authenticate_url: "",
|
||||
},
|
||||
],
|
||||
export const GitAuth: Story = {
|
||||
args: {
|
||||
gitAuth: [
|
||||
{
|
||||
id: "github",
|
||||
type: "github",
|
||||
authenticated: false,
|
||||
authenticate_url: "",
|
||||
},
|
||||
{
|
||||
id: "gitlab",
|
||||
type: "gitlab",
|
||||
authenticated: true,
|
||||
authenticate_url: "",
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
||||
|
||||
@@ -1,16 +1,13 @@
|
||||
import TextField from "@mui/material/TextField"
|
||||
import * as TypesGen from "api/typesGenerated"
|
||||
import { Stack } from "components/Stack/Stack"
|
||||
import { UserAutocomplete } from "components/UserAutocomplete/UserAutocomplete"
|
||||
import { FormikContextType, FormikTouched, useFormik } from "formik"
|
||||
import { FormikContextType, useFormik } from "formik"
|
||||
import { FC, useEffect, useState } from "react"
|
||||
import { useTranslation } from "react-i18next"
|
||||
import { getFormHelpers, nameValidator, onChangeTrimmed } from "utils/formUtils"
|
||||
import * as Yup from "yup"
|
||||
import { FullPageHorizontalForm } from "components/FullPageForm/FullPageHorizontalForm"
|
||||
import { SelectedTemplate } from "./SelectedTemplate"
|
||||
import { Loader } from "components/Loader/Loader"
|
||||
import { GitAuth } from "components/GitAuth/GitAuth"
|
||||
import {
|
||||
FormFields,
|
||||
FormSection,
|
||||
@@ -27,171 +24,92 @@ import {
|
||||
ImmutableTemplateParametersSection,
|
||||
MutableTemplateParametersSection,
|
||||
} from "components/TemplateParameters/TemplateParameters"
|
||||
import { CreateWSPermissions } from "xServices/createWorkspace/createWorkspaceXService"
|
||||
import { GitAuth } from "components/GitAuth/GitAuth"
|
||||
import { ErrorAlert } from "components/Alert/ErrorAlert"
|
||||
import { paramUsedToCreateWorkspace } from "utils/workspace"
|
||||
|
||||
export enum CreateWorkspaceErrors {
|
||||
GET_TEMPLATES_ERROR = "getTemplatesError",
|
||||
GET_TEMPLATE_GITAUTH_ERROR = "getTemplateGitAuthError",
|
||||
CREATE_WORKSPACE_ERROR = "createWorkspaceError",
|
||||
}
|
||||
|
||||
export interface CreateWorkspacePageViewProps {
|
||||
name: string
|
||||
loadingTemplates: boolean
|
||||
error: unknown
|
||||
defaultName: string
|
||||
defaultOwner: TypesGen.User
|
||||
template: TypesGen.Template
|
||||
gitAuth: TypesGen.TemplateVersionGitAuth[]
|
||||
parameters: TypesGen.TemplateVersionParameter[]
|
||||
defaultBuildParameters: TypesGen.WorkspaceBuildParameter[]
|
||||
permissions: CreateWSPermissions
|
||||
creatingWorkspace: boolean
|
||||
hasTemplateErrors: boolean
|
||||
templateName: string
|
||||
templates?: TypesGen.Template[]
|
||||
selectedTemplate?: TypesGen.Template
|
||||
templateParameters?: TypesGen.TemplateVersionParameter[]
|
||||
templateGitAuth?: TypesGen.TemplateVersionGitAuth[]
|
||||
createWorkspaceErrors: Partial<Record<CreateWorkspaceErrors, Error | unknown>>
|
||||
canCreateForUser?: boolean
|
||||
owner: TypesGen.User | null
|
||||
setOwner: (arg0: TypesGen.User | null) => void
|
||||
onCancel: () => void
|
||||
onSubmit: (req: TypesGen.CreateWorkspaceRequest) => void
|
||||
// initialTouched is only used for testing the error state of the form.
|
||||
initialTouched?: FormikTouched<TypesGen.CreateWorkspaceRequest>
|
||||
defaultParameterValues?: Record<string, string>
|
||||
onSubmit: (req: TypesGen.CreateWorkspaceRequest, owner: TypesGen.User) => void
|
||||
}
|
||||
|
||||
export const CreateWorkspacePageView: FC<
|
||||
React.PropsWithChildren<CreateWorkspacePageViewProps>
|
||||
> = (props) => {
|
||||
const templateParameters = props.templateParameters?.filter(
|
||||
paramUsedToCreateWorkspace,
|
||||
)
|
||||
export const CreateWorkspacePageView: FC<CreateWorkspacePageViewProps> = ({
|
||||
error,
|
||||
defaultName,
|
||||
defaultOwner,
|
||||
template,
|
||||
gitAuth,
|
||||
parameters,
|
||||
defaultBuildParameters,
|
||||
permissions,
|
||||
creatingWorkspace,
|
||||
onSubmit,
|
||||
onCancel,
|
||||
}) => {
|
||||
const initialRichParameterValues = selectInitialRichParametersValues(
|
||||
templateParameters,
|
||||
props.defaultParameterValues,
|
||||
parameters,
|
||||
defaultBuildParameters,
|
||||
)
|
||||
const [gitAuthErrors, setGitAuthErrors] = useState<Record<string, string>>({})
|
||||
useEffect(() => {
|
||||
// templateGitAuth is refreshed automatically using a BroadcastChannel
|
||||
// which may change the `authenticated` property.
|
||||
//
|
||||
// If the provider becomes authenticated, we want the error message
|
||||
// to disappear.
|
||||
setGitAuthErrors({})
|
||||
}, [props.templateGitAuth])
|
||||
const workspaceErrors =
|
||||
props.createWorkspaceErrors[CreateWorkspaceErrors.CREATE_WORKSPACE_ERROR]
|
||||
// Scroll to top of page if errors are present
|
||||
useEffect(() => {
|
||||
if (props.hasTemplateErrors || Boolean(workspaceErrors)) {
|
||||
window.scrollTo(0, 0)
|
||||
}
|
||||
}, [props.hasTemplateErrors, workspaceErrors])
|
||||
const { t } = useTranslation("createWorkspacePage")
|
||||
const styles = useStyles()
|
||||
const [owner, setOwner] = useState(defaultOwner)
|
||||
const { verifyGitAuth, gitAuthErrors } = useGitAuthVerification(gitAuth)
|
||||
const form: FormikContextType<TypesGen.CreateWorkspaceRequest> =
|
||||
useFormik<TypesGen.CreateWorkspaceRequest>({
|
||||
initialValues: {
|
||||
name: props.name,
|
||||
template_id: props.selectedTemplate ? props.selectedTemplate.id : "",
|
||||
name: defaultName,
|
||||
template_id: template.id,
|
||||
rich_parameter_values: initialRichParameterValues,
|
||||
},
|
||||
validationSchema: Yup.object({
|
||||
name: nameValidator(t("nameLabel", { ns: "createWorkspacePage" })),
|
||||
rich_parameter_values: useValidationSchemaForRichParameters(
|
||||
"createWorkspacePage",
|
||||
templateParameters,
|
||||
parameters,
|
||||
),
|
||||
}),
|
||||
enableReinitialize: true,
|
||||
initialTouched: props.initialTouched,
|
||||
onSubmit: (request) => {
|
||||
for (let i = 0; i < (props.templateGitAuth?.length || 0); i++) {
|
||||
const auth = props.templateGitAuth?.[i]
|
||||
if (!auth) {
|
||||
continue
|
||||
}
|
||||
if (!auth.authenticated) {
|
||||
setGitAuthErrors({
|
||||
[auth.id]: "You must authenticate to create a workspace!",
|
||||
})
|
||||
form.setSubmitting(false)
|
||||
return
|
||||
}
|
||||
if (!verifyGitAuth()) {
|
||||
form.setSubmitting(false)
|
||||
return
|
||||
}
|
||||
props.onSubmit({
|
||||
...request,
|
||||
})
|
||||
form.setSubmitting(false)
|
||||
|
||||
onSubmit(request, owner)
|
||||
},
|
||||
})
|
||||
|
||||
const isLoading = props.loadingTemplates
|
||||
useEffect(() => {
|
||||
if (error) {
|
||||
window.scrollTo(0, 0)
|
||||
}
|
||||
}, [error])
|
||||
|
||||
const getFieldHelpers = getFormHelpers<TypesGen.CreateWorkspaceRequest>(
|
||||
form,
|
||||
props.createWorkspaceErrors[CreateWorkspaceErrors.CREATE_WORKSPACE_ERROR],
|
||||
error,
|
||||
)
|
||||
|
||||
if (isLoading) {
|
||||
return <Loader />
|
||||
}
|
||||
|
||||
return (
|
||||
<FullPageHorizontalForm title="New workspace" onCancel={props.onCancel}>
|
||||
<FullPageHorizontalForm title="New workspace" onCancel={onCancel}>
|
||||
<HorizontalForm onSubmit={form.handleSubmit}>
|
||||
{Boolean(props.hasTemplateErrors) && (
|
||||
<Stack>
|
||||
{Boolean(
|
||||
props.createWorkspaceErrors[
|
||||
CreateWorkspaceErrors.GET_TEMPLATES_ERROR
|
||||
],
|
||||
) && (
|
||||
<ErrorAlert
|
||||
error={
|
||||
props.createWorkspaceErrors[
|
||||
CreateWorkspaceErrors.GET_TEMPLATES_ERROR
|
||||
]
|
||||
}
|
||||
/>
|
||||
)}
|
||||
{Boolean(
|
||||
props.createWorkspaceErrors[
|
||||
CreateWorkspaceErrors.GET_TEMPLATE_GITAUTH_ERROR
|
||||
],
|
||||
) && (
|
||||
<ErrorAlert
|
||||
error={
|
||||
props.createWorkspaceErrors[
|
||||
CreateWorkspaceErrors.GET_TEMPLATE_GITAUTH_ERROR
|
||||
]
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</Stack>
|
||||
)}
|
||||
|
||||
{Boolean(
|
||||
props.createWorkspaceErrors[
|
||||
CreateWorkspaceErrors.CREATE_WORKSPACE_ERROR
|
||||
],
|
||||
) && (
|
||||
<ErrorAlert
|
||||
error={
|
||||
props.createWorkspaceErrors[
|
||||
CreateWorkspaceErrors.CREATE_WORKSPACE_ERROR
|
||||
]
|
||||
}
|
||||
/>
|
||||
)}
|
||||
|
||||
{Boolean(error) && <ErrorAlert error={error} />}
|
||||
{/* General info */}
|
||||
<FormSection
|
||||
title="General"
|
||||
description="The template and name of your new workspace."
|
||||
>
|
||||
<FormFields>
|
||||
{props.selectedTemplate && (
|
||||
<SelectedTemplate template={props.selectedTemplate} />
|
||||
)}
|
||||
|
||||
<SelectedTemplate template={template} />
|
||||
<TextField
|
||||
{...getFieldHelpers("name")}
|
||||
disabled={form.isSubmitting}
|
||||
@@ -203,16 +121,17 @@ export const CreateWorkspacePageView: FC<
|
||||
</FormFields>
|
||||
</FormSection>
|
||||
|
||||
{/* Workspace owner */}
|
||||
{props.canCreateForUser && (
|
||||
{permissions.createWorkspaceForUser && (
|
||||
<FormSection
|
||||
title="Workspace Owner"
|
||||
description="Only admins can create workspace for other users."
|
||||
>
|
||||
<FormFields>
|
||||
<UserAutocomplete
|
||||
value={props.owner}
|
||||
onChange={props.setOwner}
|
||||
value={owner}
|
||||
onChange={(user) => {
|
||||
setOwner(user ?? defaultOwner)
|
||||
}}
|
||||
label={t("ownerLabel").toString()}
|
||||
size="medium"
|
||||
/>
|
||||
@@ -220,14 +139,13 @@ export const CreateWorkspacePageView: FC<
|
||||
</FormSection>
|
||||
)}
|
||||
|
||||
{/* Template git auth */}
|
||||
{props.templateGitAuth && props.templateGitAuth.length > 0 && (
|
||||
{gitAuth && gitAuth.length > 0 && (
|
||||
<FormSection
|
||||
title="Git Authentication"
|
||||
description="This template requires authentication to automatically perform Git operations on create."
|
||||
>
|
||||
<FormFields>
|
||||
{props.templateGitAuth.map((auth, index) => (
|
||||
{gitAuth.map((auth, index) => (
|
||||
<GitAuth
|
||||
key={index}
|
||||
authenticateURL={auth.authenticate_url}
|
||||
@@ -240,10 +158,10 @@ export const CreateWorkspacePageView: FC<
|
||||
</FormSection>
|
||||
)}
|
||||
|
||||
{templateParameters && (
|
||||
{parameters && (
|
||||
<>
|
||||
<MutableTemplateParametersSection
|
||||
templateParameters={templateParameters}
|
||||
templateParameters={parameters}
|
||||
getInputProps={(parameter, index) => {
|
||||
return {
|
||||
...getFieldHelpers(
|
||||
@@ -264,7 +182,7 @@ export const CreateWorkspacePageView: FC<
|
||||
}}
|
||||
/>
|
||||
<ImmutableTemplateParametersSection
|
||||
templateParameters={templateParameters}
|
||||
templateParameters={parameters}
|
||||
classes={{ root: styles.warningSection }}
|
||||
getInputProps={(parameter, index) => {
|
||||
return {
|
||||
@@ -289,8 +207,8 @@ export const CreateWorkspacePageView: FC<
|
||||
)}
|
||||
|
||||
<FormFooter
|
||||
onCancel={props.onCancel}
|
||||
isLoading={props.creatingWorkspace}
|
||||
onCancel={onCancel}
|
||||
isLoading={creatingWorkspace}
|
||||
submitLabel={t("createWorkspace").toString()}
|
||||
/>
|
||||
</HorizontalForm>
|
||||
@@ -298,6 +216,44 @@ export const CreateWorkspacePageView: FC<
|
||||
)
|
||||
}
|
||||
|
||||
type GitAuthErrors = Record<string, string>
|
||||
|
||||
const useGitAuthVerification = (gitAuth: TypesGen.TemplateVersionGitAuth[]) => {
|
||||
const [gitAuthErrors, setGitAuthErrors] = useState<GitAuthErrors>({})
|
||||
|
||||
useEffect(() => {
|
||||
// templateGitAuth is refreshed automatically using a BroadcastChannel
|
||||
// which may change the `authenticated` property.
|
||||
//
|
||||
// If the provider becomes authenticated, we want the error message
|
||||
// to disappear.
|
||||
setGitAuthErrors({})
|
||||
}, [gitAuth])
|
||||
|
||||
const verifyGitAuth = () => {
|
||||
const errors: GitAuthErrors = {}
|
||||
|
||||
for (let i = 0; i < gitAuth.length; i++) {
|
||||
const auth = gitAuth.at(i)
|
||||
if (!auth) {
|
||||
continue
|
||||
}
|
||||
if (!auth.authenticated) {
|
||||
errors[auth.id] = "You must authenticate to create a workspace!"
|
||||
}
|
||||
}
|
||||
|
||||
setGitAuthErrors(errors)
|
||||
const isValid = Object.keys(errors).length === 0
|
||||
return isValid
|
||||
}
|
||||
|
||||
return {
|
||||
gitAuthErrors,
|
||||
verifyGitAuth,
|
||||
}
|
||||
}
|
||||
|
||||
const useStyles = makeStyles((theme) => ({
|
||||
warningText: {
|
||||
color: theme.palette.warning.light,
|
||||
|
||||
@@ -7,10 +7,10 @@ import {
|
||||
import { usePermissions } from "hooks"
|
||||
import { FC, useEffect } from "react"
|
||||
import { useParams } from "react-router-dom"
|
||||
import { REFRESH_GITAUTH_BROADCAST_CHANNEL } from "xServices/createWorkspace/createWorkspaceXService"
|
||||
import GitAuthPageView from "./GitAuthPageView"
|
||||
import { ApiErrorResponse } from "api/errors"
|
||||
import { isAxiosError } from "axios"
|
||||
import { REFRESH_GITAUTH_BROADCAST_CHANNEL } from "utils/gitAuth"
|
||||
|
||||
const GitAuthPage: FC = () => {
|
||||
const { provider } = useParams()
|
||||
@@ -58,7 +58,6 @@ const GitAuthPage: FC = () => {
|
||||
}
|
||||
// This is used to notify the parent window that the Git auth token has been refreshed.
|
||||
// It's critical in the create workspace flow!
|
||||
// eslint-disable-next-line compat/compat -- It actually is supported... not sure why it's complaining.
|
||||
const bc = new BroadcastChannel(REFRESH_GITAUTH_BROADCAST_CHANNEL)
|
||||
// The message doesn't matter, any message refreshes the page!
|
||||
bc.postMessage("noop")
|
||||
|
||||
@@ -12,7 +12,7 @@ import { CopyButton } from "components/CopyButton/CopyButton"
|
||||
import { SignInLayout } from "components/SignInLayout/SignInLayout"
|
||||
import { Welcome } from "components/Welcome/Welcome"
|
||||
import { FC, useEffect } from "react"
|
||||
import { REFRESH_GITAUTH_BROADCAST_CHANNEL } from "xServices/createWorkspace/createWorkspaceXService"
|
||||
import { REFRESH_GITAUTH_BROADCAST_CHANNEL } from "utils/gitAuth"
|
||||
|
||||
export interface GitAuthPageViewProps {
|
||||
gitAuth: GitAuth
|
||||
|
||||
@@ -47,6 +47,6 @@ test("Users can fill the parameters and copy the open in coder url", async () =>
|
||||
const copyButton = screen.getByRole("button", { name: /copy/i })
|
||||
await userEvent.click(copyButton)
|
||||
expect(window.navigator.clipboard.writeText).toBeCalledWith(
|
||||
`[](http://localhost/templates/test-template/workspace?param.first_parameter=firstParameterValue¶m.second_parameter=123456)`,
|
||||
`[](http://localhost/templates/test-template/workspace?mode=manual¶m.first_parameter=firstParameterValue¶m.second_parameter=123456)`,
|
||||
)
|
||||
})
|
||||
|
||||
@@ -2,10 +2,13 @@ import CheckOutlined from "@mui/icons-material/CheckOutlined"
|
||||
import FileCopyOutlined from "@mui/icons-material/FileCopyOutlined"
|
||||
import Box from "@mui/material/Box"
|
||||
import Button from "@mui/material/Button"
|
||||
import FormControlLabel from "@mui/material/FormControlLabel"
|
||||
import Radio from "@mui/material/Radio"
|
||||
import RadioGroup from "@mui/material/RadioGroup"
|
||||
import { useQuery } from "@tanstack/react-query"
|
||||
import { getTemplateVersionRichParameters } from "api/api"
|
||||
import { Template, TemplateVersionParameter } from "api/typesGenerated"
|
||||
import { VerticalForm } from "components/Form/Form"
|
||||
import { FormSection, VerticalForm } from "components/Form/Form"
|
||||
import { Loader } from "components/Loader/Loader"
|
||||
import { useTemplateLayoutContext } from "components/TemplateLayout/TemplateLayout"
|
||||
import {
|
||||
@@ -21,7 +24,7 @@ import {
|
||||
selectInitialRichParametersValues,
|
||||
workspaceBuildParameterValue,
|
||||
} from "utils/richParameters"
|
||||
import { paramUsedToCreateWorkspace } from "utils/workspace"
|
||||
import { paramsUsedToCreateWorkspace } from "utils/workspace"
|
||||
|
||||
type ButtonValues = Record<string, string>
|
||||
|
||||
@@ -40,7 +43,7 @@ const TemplateEmbedPage = () => {
|
||||
<TemplateEmbedPageView
|
||||
template={template}
|
||||
templateParameters={templateParameters?.filter(
|
||||
paramUsedToCreateWorkspace,
|
||||
paramsUsedToCreateWorkspace,
|
||||
)}
|
||||
/>
|
||||
</>
|
||||
@@ -51,7 +54,9 @@ export const TemplateEmbedPageView: FC<{
|
||||
template: Template
|
||||
templateParameters?: TemplateVersionParameter[]
|
||||
}> = ({ template, templateParameters }) => {
|
||||
const [buttonValues, setButtonValues] = useState<ButtonValues>({})
|
||||
const [buttonValues, setButtonValues] = useState<ButtonValues>({
|
||||
mode: "manual",
|
||||
})
|
||||
const initialRichParametersValues = templateParameters
|
||||
? selectInitialRichParametersValues(templateParameters)
|
||||
: undefined
|
||||
@@ -92,20 +97,48 @@ export const TemplateEmbedPageView: FC<{
|
||||
<Loader />
|
||||
) : (
|
||||
<Box display="flex" alignItems="flex-start" gap={6}>
|
||||
{templateParameters.length > 0 && (
|
||||
<Box flex={1} maxWidth={400}>
|
||||
<VerticalForm>
|
||||
<MutableTemplateParametersSection
|
||||
templateParameters={templateParameters}
|
||||
getInputProps={getInputProps}
|
||||
/>
|
||||
<ImmutableTemplateParametersSection
|
||||
templateParameters={templateParameters}
|
||||
getInputProps={getInputProps}
|
||||
/>
|
||||
</VerticalForm>
|
||||
</Box>
|
||||
)}
|
||||
<Box flex={1} maxWidth={400}>
|
||||
<VerticalForm>
|
||||
<FormSection
|
||||
title="Creation mode"
|
||||
description="By changing the mode to automatic, when the user clicks the button, the workspace will be created automatically instead of showing a form to the user."
|
||||
>
|
||||
<RadioGroup
|
||||
defaultValue={buttonValues.mode}
|
||||
onChange={(_, v) => {
|
||||
setButtonValues((buttonValues) => ({
|
||||
...buttonValues,
|
||||
mode: v,
|
||||
}))
|
||||
}}
|
||||
>
|
||||
<FormControlLabel
|
||||
value="manual"
|
||||
control={<Radio size="small" />}
|
||||
label="Manual"
|
||||
/>
|
||||
<FormControlLabel
|
||||
value="auto"
|
||||
control={<Radio size="small" />}
|
||||
label="Automatic"
|
||||
/>
|
||||
</RadioGroup>
|
||||
</FormSection>
|
||||
|
||||
{templateParameters.length > 0 && (
|
||||
<>
|
||||
<MutableTemplateParametersSection
|
||||
templateParameters={templateParameters}
|
||||
getInputProps={getInputProps}
|
||||
/>
|
||||
<ImmutableTemplateParametersSection
|
||||
templateParameters={templateParameters}
|
||||
getInputProps={getInputProps}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</VerticalForm>
|
||||
</Box>
|
||||
<Box
|
||||
display="flex"
|
||||
height={{
|
||||
|
||||
@@ -20,7 +20,7 @@ const meta: Meta<typeof TemplateEmbedPageView> = {
|
||||
export default meta
|
||||
type Story = StoryObj<typeof TemplateEmbedPageView>
|
||||
|
||||
export const Empty: Story = {
|
||||
export const NoParameters: Story = {
|
||||
args: {
|
||||
templateParameters: [],
|
||||
},
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
export const REFRESH_GITAUTH_BROADCAST_CHANNEL = "gitauth_refresh"
|
||||
@@ -0,0 +1,53 @@
|
||||
import { TemplateVersionParameter } from "api/typesGenerated"
|
||||
import { selectInitialRichParametersValues } from "./richParameters"
|
||||
|
||||
test("selectInitialRichParametersValues return default value when default build parameter is not valid", () => {
|
||||
const templateParameters: TemplateVersionParameter[] = [
|
||||
{
|
||||
name: "cpu",
|
||||
display_name: "CPU",
|
||||
description: "The number of CPU cores",
|
||||
description_plaintext: "The number of CPU cores",
|
||||
type: "string",
|
||||
mutable: true,
|
||||
default_value: "2",
|
||||
icon: "/icon/memory.svg",
|
||||
options: [
|
||||
{
|
||||
name: "2 Cores",
|
||||
description: "",
|
||||
value: "2",
|
||||
icon: "",
|
||||
},
|
||||
{
|
||||
name: "4 Cores",
|
||||
description: "",
|
||||
value: "4",
|
||||
icon: "",
|
||||
},
|
||||
{
|
||||
name: "6 Cores",
|
||||
description: "",
|
||||
value: "6",
|
||||
icon: "",
|
||||
},
|
||||
{
|
||||
name: "8 Cores",
|
||||
description: "",
|
||||
value: "8",
|
||||
icon: "",
|
||||
},
|
||||
],
|
||||
required: false,
|
||||
ephemeral: false,
|
||||
},
|
||||
]
|
||||
|
||||
const cpuParameter = templateParameters[0]
|
||||
const [cpuParameterInitialValue] = selectInitialRichParametersValues(
|
||||
templateParameters,
|
||||
[{ name: cpuParameter.name, value: "100" }],
|
||||
)
|
||||
|
||||
expect(cpuParameterInitialValue.value).toBe(cpuParameter.default_value)
|
||||
})
|
||||
@@ -7,7 +7,7 @@ import * as Yup from "yup"
|
||||
|
||||
export const selectInitialRichParametersValues = (
|
||||
templateParameters?: TemplateVersionParameter[],
|
||||
defaultValuesFromQuery?: Record<string, string>,
|
||||
defaultBuildParameters?: WorkspaceBuildParameter[],
|
||||
): WorkspaceBuildParameter[] => {
|
||||
const defaults: WorkspaceBuildParameter[] = []
|
||||
if (!templateParameters) {
|
||||
@@ -19,9 +19,20 @@ export const selectInitialRichParametersValues = (
|
||||
|
||||
if (parameter.options.length > 0) {
|
||||
parameterValue = parameterValue ?? parameter.options[0].value
|
||||
const validValues = parameter.options.map((option) => option.value)
|
||||
|
||||
if (defaultValuesFromQuery && defaultValuesFromQuery[parameter.name]) {
|
||||
parameterValue = defaultValuesFromQuery[parameter.name]
|
||||
if (defaultBuildParameters) {
|
||||
const defaultBuildParameter = defaultBuildParameters.find(
|
||||
(p) => p.name === parameter.name,
|
||||
)
|
||||
|
||||
// We don't want invalid values from default parameters to be set
|
||||
if (
|
||||
defaultBuildParameter &&
|
||||
validValues.includes(defaultBuildParameter.value)
|
||||
) {
|
||||
parameterValue = defaultBuildParameter?.value
|
||||
}
|
||||
}
|
||||
|
||||
const buildParameter: WorkspaceBuildParameter = {
|
||||
@@ -36,8 +47,14 @@ export const selectInitialRichParametersValues = (
|
||||
parameterValue = parameter.default_value
|
||||
}
|
||||
|
||||
if (defaultValuesFromQuery && defaultValuesFromQuery[parameter.name]) {
|
||||
parameterValue = defaultValuesFromQuery[parameter.name]
|
||||
if (defaultBuildParameters) {
|
||||
const buildParameter = defaultBuildParameters.find(
|
||||
(p) => p.name === parameter.name,
|
||||
)
|
||||
|
||||
if (buildParameter) {
|
||||
parameterValue = buildParameter?.value
|
||||
}
|
||||
}
|
||||
|
||||
const buildParameter: WorkspaceBuildParameter = {
|
||||
|
||||
@@ -286,6 +286,6 @@ export const hasJobError = (workspace: TypesGen.Workspace) => {
|
||||
return workspace.latest_build.job.error !== undefined
|
||||
}
|
||||
|
||||
export const paramUsedToCreateWorkspace = (
|
||||
export const paramsUsedToCreateWorkspace = (
|
||||
param: TypesGen.TemplateVersionParameter,
|
||||
) => !param.ephemeral
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import {
|
||||
checkAuthorization,
|
||||
createWorkspace,
|
||||
getTemplates,
|
||||
getTemplateByName,
|
||||
getTemplateVersionGitAuth,
|
||||
getTemplateVersionRichParameters,
|
||||
} from "api/api"
|
||||
@@ -12,38 +12,33 @@ import {
|
||||
TemplateVersionParameter,
|
||||
User,
|
||||
Workspace,
|
||||
WorkspaceBuildParameter,
|
||||
} from "api/typesGenerated"
|
||||
import { assign, createMachine } from "xstate"
|
||||
import { paramsUsedToCreateWorkspace } from "utils/workspace"
|
||||
import { REFRESH_GITAUTH_BROADCAST_CHANNEL } from "utils/gitAuth"
|
||||
|
||||
export const REFRESH_GITAUTH_BROADCAST_CHANNEL = "gitauth_refresh"
|
||||
export type CreateWorkspaceMode = "form" | "auto"
|
||||
|
||||
type CreateWorkspaceContext = {
|
||||
organizationId: string
|
||||
owner: User | null
|
||||
templateName: string
|
||||
templates?: Template[]
|
||||
selectedTemplate?: Template
|
||||
templateParameters?: TemplateVersionParameter[]
|
||||
templateGitAuth?: TemplateVersionGitAuth[]
|
||||
createWorkspaceRequest?: CreateWorkspaceRequest
|
||||
createdWorkspace?: Workspace
|
||||
createWorkspaceError?: Error | unknown
|
||||
getTemplatesError?: Error | unknown
|
||||
getTemplateParametersError?: Error | unknown
|
||||
getTemplateGitAuthError?: Error | unknown
|
||||
mode: CreateWorkspaceMode
|
||||
defaultName: string
|
||||
error?: Error | unknown
|
||||
// Form
|
||||
template?: Template
|
||||
parameters?: TemplateVersionParameter[]
|
||||
permissions?: Record<string, boolean>
|
||||
checkPermissionsError?: Error | unknown
|
||||
gitAuth?: TemplateVersionGitAuth[]
|
||||
// Used on auto-create
|
||||
defaultBuildParameters?: WorkspaceBuildParameter[]
|
||||
}
|
||||
|
||||
type CreateWorkspaceEvent = {
|
||||
type: "CREATE_WORKSPACE"
|
||||
request: CreateWorkspaceRequest
|
||||
owner: User | null
|
||||
}
|
||||
|
||||
type SelectOwnerEvent = {
|
||||
type: "SELECT_OWNER"
|
||||
owner: User | null
|
||||
owner: User
|
||||
}
|
||||
|
||||
type RefreshGitAuthEvent = {
|
||||
@@ -59,117 +54,79 @@ export const createWorkspaceMachine =
|
||||
tsTypes: {} as import("./createWorkspaceXService.typegen").Typegen0,
|
||||
schema: {
|
||||
context: {} as CreateWorkspaceContext,
|
||||
events: {} as
|
||||
| CreateWorkspaceEvent
|
||||
| SelectOwnerEvent
|
||||
| RefreshGitAuthEvent,
|
||||
events: {} as CreateWorkspaceEvent | RefreshGitAuthEvent,
|
||||
services: {} as {
|
||||
getTemplates: {
|
||||
data: Template[]
|
||||
}
|
||||
getTemplateGitAuth: {
|
||||
data: TemplateVersionGitAuth[]
|
||||
}
|
||||
getTemplateParameters: {
|
||||
data: TemplateVersionParameter[]
|
||||
loadFormData: {
|
||||
data: {
|
||||
template: Template
|
||||
permissions: CreateWSPermissions
|
||||
parameters: TemplateVersionParameter[]
|
||||
gitAuth: TemplateVersionGitAuth[]
|
||||
}
|
||||
}
|
||||
createWorkspace: {
|
||||
data: Workspace
|
||||
}
|
||||
autoCreateWorkspace: {
|
||||
data: Workspace
|
||||
}
|
||||
},
|
||||
},
|
||||
initial: "gettingTemplates",
|
||||
initial: "checkingMode",
|
||||
states: {
|
||||
gettingTemplates: {
|
||||
entry: "clearGetTemplatesError",
|
||||
checkingMode: {
|
||||
always: [
|
||||
{
|
||||
target: "autoCreating",
|
||||
cond: ({ mode }) => mode === "auto",
|
||||
},
|
||||
{ target: "loadingFormData" },
|
||||
],
|
||||
},
|
||||
autoCreating: {
|
||||
invoke: {
|
||||
src: "getTemplates",
|
||||
onDone: [
|
||||
{
|
||||
actions: ["assignTemplates"],
|
||||
cond: "areTemplatesEmpty",
|
||||
src: "autoCreateWorkspace",
|
||||
onDone: {
|
||||
actions: ["onCreateWorkspace"],
|
||||
},
|
||||
onError: {
|
||||
actions: ["assignError"],
|
||||
target: "loadingFormData",
|
||||
},
|
||||
},
|
||||
},
|
||||
loadingFormData: {
|
||||
invoke: {
|
||||
src: "loadFormData",
|
||||
onDone: {
|
||||
target: "idle",
|
||||
actions: ["assignFormData"],
|
||||
},
|
||||
onError: {
|
||||
target: "loadError",
|
||||
actions: ["assignError"],
|
||||
},
|
||||
},
|
||||
},
|
||||
idle: {
|
||||
invoke: [
|
||||
{
|
||||
src: () => (callback) => {
|
||||
const channel = watchGitAuthRefresh(() => {
|
||||
callback("REFRESH_GITAUTH")
|
||||
})
|
||||
return () => channel.close()
|
||||
},
|
||||
{
|
||||
actions: ["assignTemplates", "assignSelectedTemplate"],
|
||||
target: "gettingTemplateParameters",
|
||||
},
|
||||
],
|
||||
onError: {
|
||||
actions: ["assignGetTemplatesError"],
|
||||
target: "error",
|
||||
},
|
||||
},
|
||||
},
|
||||
gettingTemplateParameters: {
|
||||
entry: "clearGetTemplateParametersError",
|
||||
invoke: {
|
||||
src: "getTemplateParameters",
|
||||
onDone: {
|
||||
actions: ["assignTemplateParameters"],
|
||||
target: "checkingPermissions",
|
||||
},
|
||||
onError: {
|
||||
actions: ["assignGetTemplateParametersError"],
|
||||
target: "error",
|
||||
},
|
||||
},
|
||||
},
|
||||
checkingPermissions: {
|
||||
entry: "clearCheckPermissionsError",
|
||||
invoke: {
|
||||
src: "checkPermissions",
|
||||
id: "checkPermissions",
|
||||
onDone: {
|
||||
actions: "assignPermissions",
|
||||
target: "gettingTemplateGitAuth",
|
||||
},
|
||||
onError: {
|
||||
actions: ["assignCheckPermissionsError"],
|
||||
},
|
||||
},
|
||||
},
|
||||
gettingTemplateGitAuth: {
|
||||
entry: "clearTemplateGitAuthError",
|
||||
invoke: {
|
||||
src: "getTemplateGitAuth",
|
||||
onDone: {
|
||||
actions: ["assignTemplateGitAuth"],
|
||||
target: "fillingParams",
|
||||
},
|
||||
onError: {
|
||||
actions: ["assignTemplateGitAuthError"],
|
||||
target: "error",
|
||||
},
|
||||
},
|
||||
},
|
||||
fillingParams: {
|
||||
invoke: {
|
||||
id: "listenForRefreshGitAuth",
|
||||
src: () => (callback) => {
|
||||
// eslint-disable-next-line compat/compat -- It actually is supported... not sure why eslint is complaining.
|
||||
const bc = new BroadcastChannel(REFRESH_GITAUTH_BROADCAST_CHANNEL)
|
||||
bc.addEventListener("message", () => {
|
||||
callback("REFRESH_GITAUTH")
|
||||
})
|
||||
return () => bc.close()
|
||||
},
|
||||
},
|
||||
],
|
||||
on: {
|
||||
CREATE_WORKSPACE: {
|
||||
actions: ["assignCreateWorkspaceRequest", "assignOwner"],
|
||||
target: "creatingWorkspace",
|
||||
},
|
||||
SELECT_OWNER: {
|
||||
actions: ["assignOwner"],
|
||||
target: ["fillingParams"],
|
||||
},
|
||||
REFRESH_GITAUTH: {
|
||||
target: "gettingTemplateGitAuth",
|
||||
},
|
||||
},
|
||||
},
|
||||
creatingWorkspace: {
|
||||
entry: "clearCreateWorkspaceError",
|
||||
entry: "clearError",
|
||||
invoke: {
|
||||
src: "createWorkspace",
|
||||
onDone: {
|
||||
@@ -177,137 +134,100 @@ export const createWorkspaceMachine =
|
||||
target: "created",
|
||||
},
|
||||
onError: {
|
||||
actions: ["assignCreateWorkspaceError"],
|
||||
target: "fillingParams",
|
||||
actions: ["assignError"],
|
||||
target: "idle",
|
||||
},
|
||||
},
|
||||
},
|
||||
created: {
|
||||
type: "final",
|
||||
},
|
||||
error: {},
|
||||
loadError: {
|
||||
type: "final",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
services: {
|
||||
getTemplates: (context) => getTemplates(context.organizationId),
|
||||
getTemplateGitAuth: (context) => {
|
||||
const { selectedTemplate } = context
|
||||
|
||||
if (!selectedTemplate) {
|
||||
throw new Error("No selected template")
|
||||
}
|
||||
|
||||
return getTemplateVersionGitAuth(selectedTemplate.active_version_id)
|
||||
createWorkspace: ({ organizationId }, { request, owner }) => {
|
||||
return createWorkspace(organizationId, owner.id, request)
|
||||
},
|
||||
getTemplateParameters: (context) => {
|
||||
const { selectedTemplate } = context
|
||||
|
||||
if (!selectedTemplate) {
|
||||
throw new Error("No selected template")
|
||||
}
|
||||
|
||||
return getTemplateVersionRichParameters(
|
||||
selectedTemplate.active_version_id,
|
||||
)
|
||||
},
|
||||
checkPermissions: async (context) => {
|
||||
if (!context.organizationId) {
|
||||
throw new Error("No organization ID")
|
||||
}
|
||||
|
||||
// HACK: below, we pass in * for the owner_id, which is a hacky way of checking if the
|
||||
// current user can create a workspace on behalf of anyone within the org (only org owners should be able to do this).
|
||||
// This pattern should not be replicated outside of this narrow use case.
|
||||
const permissionsToCheck = {
|
||||
createWorkspaceForUser: {
|
||||
object: {
|
||||
resource_type: "workspace",
|
||||
organization_id: `${context.organizationId}`,
|
||||
owner_id: "*",
|
||||
},
|
||||
action: "create",
|
||||
},
|
||||
} as const
|
||||
|
||||
return checkAuthorization({
|
||||
checks: permissionsToCheck,
|
||||
autoCreateWorkspace: async ({
|
||||
templateName,
|
||||
organizationId,
|
||||
defaultBuildParameters,
|
||||
defaultName,
|
||||
}) => {
|
||||
const template = await getTemplateByName(organizationId, templateName)
|
||||
return createWorkspace(organizationId, "me", {
|
||||
template_id: template.id,
|
||||
name: defaultName,
|
||||
rich_parameter_values: defaultBuildParameters,
|
||||
})
|
||||
},
|
||||
createWorkspace: (context) => {
|
||||
const { createWorkspaceRequest, organizationId, owner } = context
|
||||
loadFormData: async ({ templateName, organizationId }) => {
|
||||
const [template, permissions] = await Promise.all([
|
||||
getTemplateByName(organizationId, templateName),
|
||||
checkCreateWSPermissions(organizationId),
|
||||
])
|
||||
const [parameters, gitAuth] = await Promise.all([
|
||||
getTemplateVersionRichParameters(template.active_version_id).then(
|
||||
(p) => p.filter(paramsUsedToCreateWorkspace),
|
||||
),
|
||||
getTemplateVersionGitAuth(template.active_version_id),
|
||||
])
|
||||
|
||||
if (!createWorkspaceRequest) {
|
||||
throw new Error("No create workspace request")
|
||||
return {
|
||||
template,
|
||||
permissions,
|
||||
parameters,
|
||||
gitAuth,
|
||||
}
|
||||
|
||||
return createWorkspace(
|
||||
organizationId,
|
||||
owner?.id ?? "me",
|
||||
createWorkspaceRequest,
|
||||
)
|
||||
},
|
||||
},
|
||||
guards: {
|
||||
areTemplatesEmpty: (_, event) => event.data.length === 0,
|
||||
},
|
||||
actions: {
|
||||
assignTemplates: assign({
|
||||
templates: (_, event) => event.data,
|
||||
assignFormData: assign((ctx, event) => {
|
||||
return {
|
||||
...ctx,
|
||||
...event.data,
|
||||
}
|
||||
}),
|
||||
assignSelectedTemplate: assign({
|
||||
selectedTemplate: (ctx, event) => {
|
||||
const templates = event.data.filter(
|
||||
(template) => template.name === ctx.templateName,
|
||||
)
|
||||
return templates.length > 0 ? templates[0] : undefined
|
||||
},
|
||||
assignError: assign({
|
||||
error: (_, event) => event.data,
|
||||
}),
|
||||
assignTemplateParameters: assign({
|
||||
templateParameters: (_, event) => event.data,
|
||||
}),
|
||||
assignPermissions: assign({
|
||||
permissions: (_, event) => event.data as Record<string, boolean>,
|
||||
}),
|
||||
assignCheckPermissionsError: assign({
|
||||
checkPermissionsError: (_, event) => event.data,
|
||||
}),
|
||||
clearCheckPermissionsError: assign({
|
||||
checkPermissionsError: (_) => undefined,
|
||||
}),
|
||||
assignCreateWorkspaceRequest: assign({
|
||||
createWorkspaceRequest: (_, event) => event.request,
|
||||
}),
|
||||
assignOwner: assign({
|
||||
owner: (_, event) => event.owner,
|
||||
}),
|
||||
assignCreateWorkspaceError: assign({
|
||||
createWorkspaceError: (_, event) => event.data,
|
||||
}),
|
||||
clearCreateWorkspaceError: assign({
|
||||
createWorkspaceError: (_) => undefined,
|
||||
}),
|
||||
assignGetTemplatesError: assign({
|
||||
getTemplatesError: (_, event) => event.data,
|
||||
}),
|
||||
clearGetTemplatesError: assign({
|
||||
getTemplatesError: (_) => undefined,
|
||||
}),
|
||||
assignGetTemplateParametersError: assign({
|
||||
getTemplateParametersError: (_, event) => event.data,
|
||||
}),
|
||||
clearGetTemplateParametersError: assign({
|
||||
getTemplateParametersError: (_) => undefined,
|
||||
}),
|
||||
clearTemplateGitAuthError: assign({
|
||||
getTemplateGitAuthError: (_) => undefined,
|
||||
}),
|
||||
assignTemplateGitAuthError: assign({
|
||||
getTemplateGitAuthError: (_, event) => event.data,
|
||||
}),
|
||||
assignTemplateGitAuth: assign({
|
||||
templateGitAuth: (_, event) => event.data,
|
||||
clearError: assign({
|
||||
error: (_) => undefined,
|
||||
}),
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
const checkCreateWSPermissions = async (organizationId: string) => {
|
||||
// HACK: below, we pass in * for the owner_id, which is a hacky way of checking if the
|
||||
// current user can create a workspace on behalf of anyone within the org (only org owners should be able to do this).
|
||||
// This pattern should not be replicated outside of this narrow use case.
|
||||
const permissionsToCheck = {
|
||||
createWorkspaceForUser: {
|
||||
object: {
|
||||
resource_type: "workspace",
|
||||
organization_id: organizationId,
|
||||
owner_id: "*",
|
||||
},
|
||||
action: "create",
|
||||
},
|
||||
} as const
|
||||
|
||||
return checkAuthorization({
|
||||
checks: permissionsToCheck,
|
||||
}) as Promise<Record<keyof typeof permissionsToCheck, boolean>>
|
||||
}
|
||||
|
||||
export const watchGitAuthRefresh = (callback: () => void) => {
|
||||
const bc = new BroadcastChannel(REFRESH_GITAUTH_BROADCAST_CHANNEL)
|
||||
bc.addEventListener("message", callback)
|
||||
return bc
|
||||
}
|
||||
|
||||
export type CreateWSPermissions = Awaited<
|
||||
ReturnType<typeof checkCreateWSPermissions>
|
||||
>
|
||||
|
||||
@@ -11497,6 +11497,11 @@ unified@^10.0.0:
|
||||
trough "^2.0.0"
|
||||
vfile "^5.0.0"
|
||||
|
||||
unique-names-generator@4.7.1:
|
||||
version "4.7.1"
|
||||
resolved "https://registry.yarnpkg.com/unique-names-generator/-/unique-names-generator-4.7.1.tgz#966407b12ba97f618928f77322cfac8c80df5597"
|
||||
integrity sha512-lMx9dX+KRmG8sq6gulYYpKWZc9RlGsgBR6aoO8Qsm3qvkSJ+3rAymr+TnV8EDMrIrwuFJ4kruzMWM/OpYzPoow==
|
||||
|
||||
unique-string@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/unique-string/-/unique-string-2.0.0.tgz#39c6451f81afb2749de2b233e3f7c5e8843bd89d"
|
||||
|
||||
Reference in New Issue
Block a user