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.
This commit is contained in:
Jaayden Halko
2025-04-03 21:39:12 +01:00
committed by GitHub
parent ae44ecfc07
commit 54ff17bec6
9 changed files with 807 additions and 7 deletions

7
coderd/apidoc/docs.go generated
View File

@ -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": {

View File

@ -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": {

View File

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

View File

@ -2845,6 +2845,7 @@ CreateWorkspaceRequest provides options for creating a new workspace. Only one o
| `notifications` |
| `workspace-usage` |
| `web-push` |
| `dynamic-parameters` |
## codersdk.ExternalAuth

View File

@ -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"

View File

@ -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 <CreateWorkspacePageExperimental />;
}
return <CreateWorkspacePage />;
};
export default CreateWorkspaceExperimentRouter;

View File

@ -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<ApiErrorResponse | null>(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 (
<>
<Helmet>
<title>{pageTitle(title)}</title>
</Helmet>
{isLoadingFormData || isLoadingExternalAuth || autoCreateReady ? (
<Loader />
) : (
<CreateWorkspacePageViewExperimental
mode={mode}
defaultName={defaultName}
disabledParams={disabledParams}
defaultOwner={me}
autofillParameters={autofillParameters}
error={
createWorkspaceMutation.error ||
autoCreateError ||
loadFormDataError ||
autoCreateWorkspaceMutation.error
}
resetMutation={createWorkspaceMutation.reset}
template={templateQuery.data!}
versionId={realizedVersionId}
externalAuth={externalAuth ?? []}
externalAuthPollingState={externalAuthPollingState}
startPollingExternalAuth={startPollingExternalAuth}
hasAllRequiredExternalAuth={hasAllRequiredExternalAuth}
permissions={permissionsQuery.data as WorkspacePermissions}
parameters={realizedParameters as TemplateVersionParameter[]}
presets={templateVersionPresetsQuery.data ?? []}
creatingWorkspace={createWorkspaceMutation.isLoading}
onCancel={() => {
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<ExternalAuthPollingState>("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<string, UserParameter>());
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";
}

View File

@ -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<TypesGen.CreateWorkspaceRequest> =
useFormik<TypesGen.CreateWorkspaceRequest>({
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<TypesGen.CreateWorkspaceRequest>(
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<string[]>(
[],
);
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 (
<>
<div className="absolute sticky top-5 ml-10">
<button
onClick={onCancel}
type="button"
className="flex items-center gap-2 bg-transparent border-none text-content-secondary hover:text-content-primary translate-y-12"
>
<ArrowLeft size={20} />
Go back
</button>
</div>
<div className="flex flex-col gap-6 max-w-screen-sm mx-auto">
<header className="flex flex-col gap-2 mt-10">
<div className="flex items-center gap-2">
<Avatar
variant="icon"
size="md"
src={template.icon}
fallback={template.name}
/>
<p className="text-base font-medium m-0">
{template.display_name.length > 0
? template.display_name
: template.name}
</p>
</div>
<h1 className="text-3xl font-semibold m-0">New workspace</h1>
{template.deprecated && <Pill type="warning">Deprecated</Pill>}
</header>
<form
onSubmit={form.handleSubmit}
aria-label="Create workspace form"
className="flex flex-col gap-6 w-full border border-border-default border-solid rounded-lg p-6"
>
{Boolean(error) && <ErrorAlert error={error} />}
{mode === "duplicate" && (
<Alert
severity="info"
dismissible
data-testid="duplication-warning"
>
{Language.duplicationWarning}
</Alert>
)}
<section className="flex flex-col gap-4">
<hgroup>
<h2 className="text-xl font-semibold m-0">General</h2>
<p className="text-sm text-content-secondary mt-0">
{permissions.createWorkspaceForUser
? "Only admins can create workspaces for other users."
: "The name of your new workspace."}
</p>
</hgroup>
<div>
{versionId && versionId !== template.active_version_id && (
<div className="flex flex-col gap-2 pb-4">
<Label className="text-sm" htmlFor={`${id}-version-id`}>
Version ID
</Label>
<Input id={`${id}-version-id`} value={versionId} disabled />
<span className="text-xs text-content-secondary">
This parameter has been preset, and cannot be modified.
</span>
</div>
)}
<div className="flex gap-4 flex-wrap">
<div className="flex flex-col gap-2 flex-1">
<Label className="text-sm" htmlFor={`${id}-workspace-name`}>
Workspace name
</Label>
<div>
<Input
id={`${id}-workspace-name`}
value={form.values.name}
onChange={(e) => {
form.setFieldValue("name", e.target.value.trim());
resetMutation();
}}
disabled={creatingWorkspace}
/>
<div className="flex gap-2 text-xs text-content-secondary items-center">
Need a suggestion?
<Button
variant="subtle"
size="sm"
onClick={async () => {
await form.setFieldValue("name", suggestedName);
rerollSuggestedName();
}}
>
{suggestedName}
</Button>
</div>
</div>
</div>
{permissions.createWorkspaceForUser && (
<div className="flex flex-col gap-2 flex-1">
<Label className="text-sm" htmlFor={`${id}-workspace-name`}>
Owner
</Label>
<UserAutocomplete
value={owner}
onChange={(user) => {
setOwner(user ?? defaultOwner);
}}
size="medium"
/>
</div>
)}
</div>
</div>
</section>
{externalAuth && externalAuth.length > 0 && (
<section>
<hgroup>
<h2 className="text-xl font-semibold mb-0">
External Authentication
</h2>
<p className="text-sm text-content-secondary mt-0">
This template uses external services for authentication.
</p>
</hgroup>
<div>
{Boolean(error) && !hasAllRequiredExternalAuth && (
<Alert severity="error">
To create a workspace using this template, please connect to
all required external authentication providers listed below.
</Alert>
)}
{externalAuth.map((auth) => (
<ExternalAuthButton
key={auth.id}
error={error}
auth={auth}
isLoading={externalAuthPollingState === "polling"}
onStartPolling={startPollingExternalAuth}
displayRetry={externalAuthPollingState === "abandoned"}
/>
))}
</div>
</section>
)}
{parameters.length > 0 && (
<section className="flex flex-col gap-6">
<hgroup>
<h2 className="text-xl font-semibold m-0">Parameters</h2>
<p className="text-sm text-content-secondary m-0">
These are the settings used by your template. Please note that
immutable parameters cannot be modified once the workspace is
created.
</p>
</hgroup>
{presets.length > 0 && (
<Stack direction="column" spacing={2}>
<div className="flex flex-col gap-2">
<div className="flex gap-2 items-center">
<Label className="text-sm">Preset</Label>
<FeatureStageBadge contentType={"beta"} size="md" />
</div>
<div className="flex">
<SelectFilter
label="Preset"
options={presetOptions}
onSelect={(option) => {
const index = presetOptions.findIndex(
(preset) => preset.value === option?.value,
);
if (index === -1) {
return;
}
setSelectedPresetIndex(index);
}}
placeholder="Select a preset"
selectedOption={presetOptions[selectedPresetIndex]}
/>
</div>
</div>
</Stack>
)}
<div className="flex flex-col gap-9">
{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 (
<RichParameterInput
{...getFieldHelpers(parameterInputName)}
onChange={async (value) => {
await form.setFieldValue(parameterField, {
name: parameter.name,
value,
});
}}
key={parameter.name}
parameter={parameter}
parameterAutofill={autofillByName[parameter.name]}
disabled={isDisabled}
/>
);
})}
</div>
</section>
)}
<div className="flex flex-row justify-end">
<Button
type="submit"
disabled={creatingWorkspace || !hasAllRequiredExternalAuth}
>
<Spinner loading={creatingWorkspace} />
Create workspace
</Button>
</div>
</form>
</div>
</>
);
};
const styles = {
description: (theme) => ({
fontSize: 13,
color: theme.palette.text.secondary,
}),
} satisfies Record<string, Interpolation<Theme>>;

View File

@ -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 = () => {
<Route path="insights" element={<TemplateInsightsPage />} />
</Route>
<Route path="workspace" element={<CreateWorkspacePage />} />
<Route path="workspace" element={<CreateWorkspaceExperimentRouter />} />
<Route path="settings" element={<TemplateSettingsLayout />}>
<Route index element={<TemplateSettingsPage />} />