From 54ff17bec6b05f3f452ae367117bafc2a3a45d22 Mon Sep 17 00:00:00 2001 From: Jaayden Halko Date: Thu, 3 Apr 2025 21:39:12 +0100 Subject: [PATCH] feat: create experimental CreateWorkspacePage and dynamic-parameters experiment (#17240) The purpose of the PR is to make a copy of the CreateWorkspacePage and create an experimental version that will use when the dynamic-parameters experiment is enabled. The Figma designs for this page are still in progress but this first PR will start to move to the new designs. Figma design: https://www.figma.com/design/SMg6H8VKXnPSkE6h9KPoAD/UX-Presets?node-id=2121-2383&t=CtgPUz8eNsTI5b1t-1 Much of the existing code will be left behind and will slowly migrated over the course of several PRs to make sure no existing functionality is forgotten in the migration to dynamic paramaters. --- coderd/apidoc/docs.go | 7 +- coderd/apidoc/swagger.json | 7 +- codersdk/deployment.go | 1 + docs/reference/api/schemas.md | 1 + site/src/api/typesGenerated.ts | 1 + .../CreateWorkspaceExperimentRouter.tsx | 18 + .../CreateWorkspacePageExperimental.tsx | 327 +++++++++++++ .../CreateWorkspacePageViewExperimental.tsx | 446 ++++++++++++++++++ site/src/router.tsx | 6 +- 9 files changed, 807 insertions(+), 7 deletions(-) create mode 100644 site/src/pages/CreateWorkspacePage/CreateWorkspaceExperimentRouter.tsx create mode 100644 site/src/pages/CreateWorkspacePage/CreateWorkspacePageExperimental.tsx create mode 100644 site/src/pages/CreateWorkspacePage/CreateWorkspacePageViewExperimental.tsx diff --git a/coderd/apidoc/docs.go b/coderd/apidoc/docs.go index 134031a2fa..c93af6a64a 100644 --- a/coderd/apidoc/docs.go +++ b/coderd/apidoc/docs.go @@ -12097,10 +12097,12 @@ const docTemplate = `{ "auto-fill-parameters", "notifications", "workspace-usage", - "web-push" + "web-push", + "dynamic-parameters" ], "x-enum-comments": { "ExperimentAutoFillParameters": "This should not be taken out of experiments until we have redesigned the feature.", + "ExperimentDynamicParameters": "Enables dynamic parameters when creating a workspace.", "ExperimentExample": "This isn't used for anything.", "ExperimentNotifications": "Sends notifications via SMTP and webhooks following certain events.", "ExperimentWebPush": "Enables web push notifications through the browser.", @@ -12111,7 +12113,8 @@ const docTemplate = `{ "ExperimentAutoFillParameters", "ExperimentNotifications", "ExperimentWorkspaceUsage", - "ExperimentWebPush" + "ExperimentWebPush", + "ExperimentDynamicParameters" ] }, "codersdk.ExternalAuth": { diff --git a/coderd/apidoc/swagger.json b/coderd/apidoc/swagger.json index 66821355e7..da4d7a4fcf 100644 --- a/coderd/apidoc/swagger.json +++ b/coderd/apidoc/swagger.json @@ -10833,10 +10833,12 @@ "auto-fill-parameters", "notifications", "workspace-usage", - "web-push" + "web-push", + "dynamic-parameters" ], "x-enum-comments": { "ExperimentAutoFillParameters": "This should not be taken out of experiments until we have redesigned the feature.", + "ExperimentDynamicParameters": "Enables dynamic parameters when creating a workspace.", "ExperimentExample": "This isn't used for anything.", "ExperimentNotifications": "Sends notifications via SMTP and webhooks following certain events.", "ExperimentWebPush": "Enables web push notifications through the browser.", @@ -10847,7 +10849,8 @@ "ExperimentAutoFillParameters", "ExperimentNotifications", "ExperimentWorkspaceUsage", - "ExperimentWebPush" + "ExperimentWebPush", + "ExperimentDynamicParameters" ] }, "codersdk.ExternalAuth": { diff --git a/codersdk/deployment.go b/codersdk/deployment.go index dc0bc36a85..a67682489f 100644 --- a/codersdk/deployment.go +++ b/codersdk/deployment.go @@ -3194,6 +3194,7 @@ const ( ExperimentNotifications Experiment = "notifications" // Sends notifications via SMTP and webhooks following certain events. ExperimentWorkspaceUsage Experiment = "workspace-usage" // Enables the new workspace usage tracking. ExperimentWebPush Experiment = "web-push" // Enables web push notifications through the browser. + ExperimentDynamicParameters Experiment = "dynamic-parameters" // Enables dynamic parameters when creating a workspace. ) // ExperimentsAll should include all experiments that are safe for diff --git a/docs/reference/api/schemas.md b/docs/reference/api/schemas.md index f8af45a5e6..4791967b53 100644 --- a/docs/reference/api/schemas.md +++ b/docs/reference/api/schemas.md @@ -2845,6 +2845,7 @@ CreateWorkspaceRequest provides options for creating a new workspace. Only one o | `notifications` | | `workspace-usage` | | `web-push` | +| `dynamic-parameters` | ## codersdk.ExternalAuth diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts index ab8e58d457..2df1c351d9 100644 --- a/site/src/api/typesGenerated.ts +++ b/site/src/api/typesGenerated.ts @@ -749,6 +749,7 @@ export const EntitlementsWarningHeader = "X-Coder-Entitlements-Warning"; // From codersdk/deployment.go export type Experiment = | "auto-fill-parameters" + | "dynamic-parameters" | "example" | "notifications" | "web-push" diff --git a/site/src/pages/CreateWorkspacePage/CreateWorkspaceExperimentRouter.tsx b/site/src/pages/CreateWorkspacePage/CreateWorkspaceExperimentRouter.tsx new file mode 100644 index 0000000000..36cd921e28 --- /dev/null +++ b/site/src/pages/CreateWorkspacePage/CreateWorkspaceExperimentRouter.tsx @@ -0,0 +1,18 @@ +import { useDashboard } from "modules/dashboard/useDashboard"; +import type { FC } from "react"; +import CreateWorkspacePage from "./CreateWorkspacePage"; +import CreateWorkspacePageExperimental from "./CreateWorkspacePageExperimental"; + +const CreateWorkspaceExperimentRouter: FC = () => { + const { experiments } = useDashboard(); + + const dynamicParametersEnabled = experiments.includes("dynamic-parameters"); + + if (dynamicParametersEnabled) { + return ; + } + + return ; +}; + +export default CreateWorkspaceExperimentRouter; diff --git a/site/src/pages/CreateWorkspacePage/CreateWorkspacePageExperimental.tsx b/site/src/pages/CreateWorkspacePage/CreateWorkspacePageExperimental.tsx new file mode 100644 index 0000000000..cc843798b1 --- /dev/null +++ b/site/src/pages/CreateWorkspacePage/CreateWorkspacePageExperimental.tsx @@ -0,0 +1,327 @@ +import { API } from "api/api"; +import type { ApiErrorResponse } from "api/errors"; +import { checkAuthorization } from "api/queries/authCheck"; +import { + richParameters, + templateByName, + templateVersionExternalAuth, + templateVersionPresets, +} from "api/queries/templates"; +import { autoCreateWorkspace, createWorkspace } from "api/queries/workspaces"; +import type { + TemplateVersionParameter, + UserParameter, + Workspace, +} from "api/typesGenerated"; +import { Loader } from "components/Loader/Loader"; +import { useAuthenticated } from "contexts/auth/RequireAuth"; +import { useEffectEvent } from "hooks/hookPolyfills"; +import { useDashboard } from "modules/dashboard/useDashboard"; +import { + type WorkspacePermissions, + workspacePermissionChecks, +} from "modules/permissions/workspaces"; +import { generateWorkspaceName } from "modules/workspaces/generateWorkspaceName"; +import { type FC, useCallback, useEffect, useRef, useState } from "react"; +import { Helmet } from "react-helmet-async"; +import { useMutation, useQuery, useQueryClient } from "react-query"; +import { useNavigate, useParams, useSearchParams } from "react-router-dom"; +import { pageTitle } from "utils/page"; +import type { AutofillBuildParameter } from "utils/richParameters"; +import { paramsUsedToCreateWorkspace } from "utils/workspace"; +import { CreateWorkspacePageViewExperimental } from "./CreateWorkspacePageViewExperimental"; +export const createWorkspaceModes = ["form", "auto", "duplicate"] as const; +export type CreateWorkspaceMode = (typeof createWorkspaceModes)[number]; + +export type ExternalAuthPollingState = "idle" | "polling" | "abandoned"; + +const CreateWorkspacePageExperimental: FC = () => { + const { organization: organizationName = "default", template: templateName } = + useParams() as { organization?: string; template: string }; + const { user: me } = useAuthenticated(); + const navigate = useNavigate(); + const [searchParams] = useSearchParams(); + const { experiments } = useDashboard(); + + const customVersionId = searchParams.get("version") ?? undefined; + const defaultName = searchParams.get("name"); + const disabledParams = searchParams.get("disable_params")?.split(","); + const [mode, setMode] = useState(() => getWorkspaceMode(searchParams)); + const [autoCreateError, setAutoCreateError] = + useState(null); + + const queryClient = useQueryClient(); + const autoCreateWorkspaceMutation = useMutation( + autoCreateWorkspace(queryClient), + ); + const createWorkspaceMutation = useMutation(createWorkspace(queryClient)); + + const templateQuery = useQuery( + templateByName(organizationName, templateName), + ); + const templateVersionPresetsQuery = useQuery({ + ...templateVersionPresets(templateQuery.data?.active_version_id ?? ""), + enabled: templateQuery.data !== undefined, + }); + const permissionsQuery = useQuery( + templateQuery.data + ? checkAuthorization({ + checks: workspacePermissionChecks(templateQuery.data.organization_id), + }) + : { enabled: false }, + ); + const realizedVersionId = + customVersionId ?? templateQuery.data?.active_version_id; + const organizationId = templateQuery.data?.organization_id; + const richParametersQuery = useQuery({ + ...richParameters(realizedVersionId ?? ""), + enabled: realizedVersionId !== undefined, + }); + const realizedParameters = richParametersQuery.data + ? richParametersQuery.data.filter(paramsUsedToCreateWorkspace) + : undefined; + + const { + externalAuth, + externalAuthPollingState, + startPollingExternalAuth, + isLoadingExternalAuth, + } = useExternalAuth(realizedVersionId); + + const isLoadingFormData = + templateQuery.isLoading || + permissionsQuery.isLoading || + richParametersQuery.isLoading; + const loadFormDataError = + templateQuery.error ?? permissionsQuery.error ?? richParametersQuery.error; + + const title = autoCreateWorkspaceMutation.isLoading + ? "Creating workspace..." + : "Create workspace"; + + const onCreateWorkspace = useCallback( + (workspace: Workspace) => { + navigate(`/@${workspace.owner_name}/${workspace.name}`); + }, + [navigate], + ); + + // Auto fill parameters + const autofillEnabled = experiments.includes("auto-fill-parameters"); + const userParametersQuery = useQuery({ + queryKey: ["userParameters"], + queryFn: () => API.getUserParameters(templateQuery.data!.id), + enabled: autofillEnabled && templateQuery.isSuccess, + }); + const autofillParameters = getAutofillParameters( + searchParams, + userParametersQuery.data ? userParametersQuery.data : [], + ); + + const autoCreationStartedRef = useRef(false); + const automateWorkspaceCreation = useEffectEvent(async () => { + if (autoCreationStartedRef.current || !organizationId) { + return; + } + + try { + autoCreationStartedRef.current = true; + const newWorkspace = await autoCreateWorkspaceMutation.mutateAsync({ + organizationId, + templateName, + buildParameters: autofillParameters, + workspaceName: defaultName ?? generateWorkspaceName(), + templateVersionId: realizedVersionId, + match: searchParams.get("match"), + }); + + onCreateWorkspace(newWorkspace); + } catch { + setMode("form"); + } + }); + + const hasAllRequiredExternalAuth = Boolean( + !isLoadingExternalAuth && + externalAuth?.every((auth) => auth.optional || auth.authenticated), + ); + + let autoCreateReady = + mode === "auto" && + (!autofillEnabled || userParametersQuery.isSuccess) && + hasAllRequiredExternalAuth; + + // `mode=auto` was set, but a prerequisite has failed, and so auto-mode should be abandoned. + if ( + mode === "auto" && + !isLoadingExternalAuth && + !hasAllRequiredExternalAuth + ) { + // Prevent suddenly resuming auto-mode if the user connects to all of the required + // external auth providers. + setMode("form"); + // Ensure this is always false, so that we don't ever let `automateWorkspaceCreation` + // fire when we're trying to disable it. + autoCreateReady = false; + // Show an error message to explain _why_ the workspace was not created automatically. + const subject = + externalAuth?.length === 1 + ? "an external authentication provider that is" + : "external authentication providers that are"; + setAutoCreateError({ + message: `This template requires ${subject} not connected.`, + detail: + "Auto-creation has been disabled. Please connect all required external authentication providers before continuing.", + }); + } + + useEffect(() => { + if (autoCreateReady) { + void automateWorkspaceCreation(); + } + }, [automateWorkspaceCreation, autoCreateReady]); + + return ( + <> + + {pageTitle(title)} + + {isLoadingFormData || isLoadingExternalAuth || autoCreateReady ? ( + + ) : ( + { + navigate(-1); + }} + onSubmit={async (request, owner) => { + if (realizedVersionId) { + request = { + ...request, + template_id: undefined, + template_version_id: realizedVersionId, + }; + } + + const workspace = await createWorkspaceMutation.mutateAsync({ + ...request, + userId: owner.id, + }); + onCreateWorkspace(workspace); + }} + /> + )} + + ); +}; + +const useExternalAuth = (versionId: string | undefined) => { + const [externalAuthPollingState, setExternalAuthPollingState] = + useState("idle"); + + const startPollingExternalAuth = useCallback(() => { + setExternalAuthPollingState("polling"); + }, []); + + const { data: externalAuth, isLoading: isLoadingExternalAuth } = useQuery( + versionId + ? { + ...templateVersionExternalAuth(versionId), + refetchInterval: + externalAuthPollingState === "polling" ? 1000 : false, + } + : { enabled: false }, + ); + + const allSignedIn = externalAuth?.every((it) => it.authenticated); + + useEffect(() => { + if (allSignedIn) { + setExternalAuthPollingState("idle"); + return; + } + + if (externalAuthPollingState !== "polling") { + return; + } + + // Poll for a maximum of one minute + const quitPolling = setTimeout( + () => setExternalAuthPollingState("abandoned"), + 60_000, + ); + return () => { + clearTimeout(quitPolling); + }; + }, [externalAuthPollingState, allSignedIn]); + + return { + startPollingExternalAuth, + externalAuth, + externalAuthPollingState, + isLoadingExternalAuth, + }; +}; + +const getAutofillParameters = ( + urlSearchParams: URLSearchParams, + userParameters: UserParameter[], +): AutofillBuildParameter[] => { + const userParamMap = userParameters.reduce((acc, param) => { + acc.set(param.name, param); + return acc; + }, new Map()); + + const buildValues: AutofillBuildParameter[] = Array.from( + urlSearchParams.keys(), + ) + .filter((key) => key.startsWith("param.")) + .map((key) => { + const name = key.replace("param.", ""); + const value = urlSearchParams.get(key) ?? ""; + // URL should take precedence over user parameters + userParamMap.delete(name); + return { name, value, source: "url" }; + }); + + for (const param of userParamMap.values()) { + buildValues.push({ + name: param.name, + value: param.value, + source: "user_history", + }); + } + return buildValues; +}; + +export default CreateWorkspacePageExperimental; + +function getWorkspaceMode(params: URLSearchParams): CreateWorkspaceMode { + const paramMode = params.get("mode"); + if (createWorkspaceModes.includes(paramMode as CreateWorkspaceMode)) { + return paramMode as CreateWorkspaceMode; + } + + return "form"; +} diff --git a/site/src/pages/CreateWorkspacePage/CreateWorkspacePageViewExperimental.tsx b/site/src/pages/CreateWorkspacePage/CreateWorkspacePageViewExperimental.tsx new file mode 100644 index 0000000000..2eb58f515e --- /dev/null +++ b/site/src/pages/CreateWorkspacePage/CreateWorkspacePageViewExperimental.tsx @@ -0,0 +1,446 @@ +import type { Interpolation, Theme } from "@emotion/react"; +import type * as TypesGen from "api/typesGenerated"; +import { Alert } from "components/Alert/Alert"; +import { ErrorAlert } from "components/Alert/ErrorAlert"; +import { Avatar } from "components/Avatar/Avatar"; +import { Button } from "components/Button/Button"; +import { FeatureStageBadge } from "components/FeatureStageBadge/FeatureStageBadge"; +import { SelectFilter } from "components/Filter/SelectFilter"; +import { Input } from "components/Input/Input"; +import { Label } from "components/Label/Label"; +import { Pill } from "components/Pill/Pill"; +import { RichParameterInput } from "components/RichParameterInput/RichParameterInput"; +import { Spinner } from "components/Spinner/Spinner"; +import { Stack } from "components/Stack/Stack"; +import { UserAutocomplete } from "components/UserAutocomplete/UserAutocomplete"; +import { type FormikContextType, useFormik } from "formik"; +import { ArrowLeft } from "lucide-react"; +import type { WorkspacePermissions } from "modules/permissions/workspaces"; +import { generateWorkspaceName } from "modules/workspaces/generateWorkspaceName"; +import { + type FC, + useCallback, + useEffect, + useId, + useMemo, + useState, +} from "react"; +import { Link } from "react-router-dom"; +import { + getFormHelpers, + nameValidator, + onChangeTrimmed, +} from "utils/formUtils"; +import { + type AutofillBuildParameter, + getInitialRichParameterValues, + useValidationSchemaForRichParameters, +} from "utils/richParameters"; +import * as Yup from "yup"; +import type { + CreateWorkspaceMode, + ExternalAuthPollingState, +} from "./CreateWorkspacePage"; +import { ExternalAuthButton } from "./ExternalAuthButton"; + +export const Language = { + duplicationWarning: + "Duplicating a workspace only copies its parameters. No state from the old workspace is copied over.", +} as const; + +export interface CreateWorkspacePageViewExperimentalProps { + mode: CreateWorkspaceMode; + defaultName?: string | null; + disabledParams?: string[]; + error: unknown; + resetMutation: () => void; + defaultOwner: TypesGen.User; + template: TypesGen.Template; + versionId?: string; + externalAuth: TypesGen.TemplateVersionExternalAuth[]; + externalAuthPollingState: ExternalAuthPollingState; + startPollingExternalAuth: () => void; + hasAllRequiredExternalAuth: boolean; + parameters: TypesGen.TemplateVersionParameter[]; + autofillParameters: AutofillBuildParameter[]; + presets: TypesGen.Preset[]; + permissions: WorkspacePermissions; + creatingWorkspace: boolean; + onCancel: () => void; + onSubmit: ( + req: TypesGen.CreateWorkspaceRequest, + owner: TypesGen.User, + ) => void; +} + +export const CreateWorkspacePageViewExperimental: FC< + CreateWorkspacePageViewExperimentalProps +> = ({ + mode, + defaultName, + disabledParams, + error, + resetMutation, + defaultOwner, + template, + versionId, + externalAuth, + externalAuthPollingState, + startPollingExternalAuth, + hasAllRequiredExternalAuth, + parameters, + autofillParameters, + presets = [], + permissions, + creatingWorkspace, + onSubmit, + onCancel, +}) => { + const [owner, setOwner] = useState(defaultOwner); + const [suggestedName, setSuggestedName] = useState(() => + generateWorkspaceName(), + ); + const id = useId(); + + const rerollSuggestedName = useCallback(() => { + setSuggestedName(() => generateWorkspaceName()); + }, []); + + const form: FormikContextType = + useFormik({ + initialValues: { + name: defaultName ?? "", + template_id: template.id, + rich_parameter_values: getInitialRichParameterValues( + parameters, + autofillParameters, + ), + }, + validationSchema: Yup.object({ + name: nameValidator("Workspace Name"), + rich_parameter_values: useValidationSchemaForRichParameters(parameters), + }), + enableReinitialize: true, + onSubmit: (request) => { + if (!hasAllRequiredExternalAuth) { + return; + } + + onSubmit(request, owner); + }, + }); + + useEffect(() => { + if (error) { + window.scrollTo(0, 0); + } + }, [error]); + + const getFieldHelpers = getFormHelpers( + form, + error, + ); + + const autofillByName = useMemo( + () => + Object.fromEntries( + autofillParameters.map((param) => [param.name, param]), + ), + [autofillParameters], + ); + + const [presetOptions, setPresetOptions] = useState([ + { label: "None", value: "" }, + ]); + useEffect(() => { + setPresetOptions([ + { label: "None", value: "" }, + ...presets.map((preset) => ({ + label: preset.Name, + value: preset.ID, + })), + ]); + }, [presets]); + + const [selectedPresetIndex, setSelectedPresetIndex] = useState(0); + const [presetParameterNames, setPresetParameterNames] = useState( + [], + ); + + useEffect(() => { + const selectedPresetOption = presetOptions[selectedPresetIndex]; + let selectedPreset: TypesGen.Preset | undefined; + for (const preset of presets) { + if (preset.ID === selectedPresetOption.value) { + selectedPreset = preset; + break; + } + } + + if (!selectedPreset || !selectedPreset.Parameters) { + setPresetParameterNames([]); + return; + } + + setPresetParameterNames(selectedPreset.Parameters.map((p) => p.Name)); + + for (const presetParameter of selectedPreset.Parameters) { + const parameterIndex = parameters.findIndex( + (p) => p.name === presetParameter.Name, + ); + if (parameterIndex === -1) continue; + + const parameterField = `rich_parameter_values.${parameterIndex}`; + + form.setFieldValue(parameterField, { + name: presetParameter.Name, + value: presetParameter.Value, + }); + } + }, [ + presetOptions, + selectedPresetIndex, + presets, + parameters, + form.setFieldValue, + ]); + + return ( + <> +
+ +
+
+
+
+ +

+ {template.display_name.length > 0 + ? template.display_name + : template.name} +

+
+

New workspace

+ + {template.deprecated && Deprecated} +
+ +
+ {Boolean(error) && } + + {mode === "duplicate" && ( + + {Language.duplicationWarning} + + )} + +
+
+

General

+

+ {permissions.createWorkspaceForUser + ? "Only admins can create workspaces for other users." + : "The name of your new workspace."} +

+
+
+ {versionId && versionId !== template.active_version_id && ( +
+ + + + This parameter has been preset, and cannot be modified. + +
+ )} +
+
+ +
+ { + form.setFieldValue("name", e.target.value.trim()); + resetMutation(); + }} + disabled={creatingWorkspace} + /> +
+ Need a suggestion? + +
+
+
+ {permissions.createWorkspaceForUser && ( +
+ + { + setOwner(user ?? defaultOwner); + }} + size="medium" + /> +
+ )} +
+
+
+ + {externalAuth && externalAuth.length > 0 && ( +
+
+

+ External Authentication +

+

+ This template uses external services for authentication. +

+
+
+ {Boolean(error) && !hasAllRequiredExternalAuth && ( + + To create a workspace using this template, please connect to + all required external authentication providers listed below. + + )} + {externalAuth.map((auth) => ( + + ))} +
+
+ )} + + {parameters.length > 0 && ( +
+
+

Parameters

+

+ These are the settings used by your template. Please note that + immutable parameters cannot be modified once the workspace is + created. +

+
+ {presets.length > 0 && ( + +
+
+ + +
+
+ { + const index = presetOptions.findIndex( + (preset) => preset.value === option?.value, + ); + if (index === -1) { + return; + } + setSelectedPresetIndex(index); + }} + placeholder="Select a preset" + selectedOption={presetOptions[selectedPresetIndex]} + /> +
+
+
+ )} + +
+ {parameters.map((parameter, index) => { + const parameterField = `rich_parameter_values.${index}`; + const parameterInputName = `${parameterField}.value`; + const isDisabled = + disabledParams?.includes( + parameter.name.toLowerCase().replace(/ /g, "_"), + ) || + creatingWorkspace || + presetParameterNames.includes(parameter.name); + + return ( + { + await form.setFieldValue(parameterField, { + name: parameter.name, + value, + }); + }} + key={parameter.name} + parameter={parameter} + parameterAutofill={autofillByName[parameter.name]} + disabled={isDisabled} + /> + ); + })} +
+
+ )} + +
+ +
+ +
+ + ); +}; + +const styles = { + description: (theme) => ({ + fontSize: 13, + color: theme.palette.text.secondary, + }), +} satisfies Record>; diff --git a/site/src/router.tsx b/site/src/router.tsx index d1e3e903eb..4f9ba95d1e 100644 --- a/site/src/router.tsx +++ b/site/src/router.tsx @@ -95,8 +95,8 @@ const TemplatePermissionsPage = lazy( const TemplateSummaryPage = lazy( () => import("./pages/TemplatePage/TemplateSummaryPage/TemplateSummaryPage"), ); -const CreateWorkspacePage = lazy( - () => import("./pages/CreateWorkspacePage/CreateWorkspacePage"), +const CreateWorkspaceExperimentRouter = lazy( + () => import("./pages/CreateWorkspacePage/CreateWorkspaceExperimentRouter"), ); const OverviewPage = lazy( () => import("./pages/DeploymentSettingsPage/OverviewPage/OverviewPage"), @@ -334,7 +334,7 @@ const templateRouter = () => { } /> - } /> + } /> }> } />