coder/site/src/pages/CreateWorkspacePage/CreateWorkspacePageView.tsx

290 lines
9.0 KiB
TypeScript

import { type Interpolation, type Theme } from "@emotion/react";
import TextField from "@mui/material/TextField";
import type * as TypesGen from "api/typesGenerated";
import { UserAutocomplete } from "components/UserAutocomplete/UserAutocomplete";
import { FormikContextType, useFormik } from "formik";
import { type FC, useEffect, useState, useMemo } from "react";
import {
getFormHelpers,
nameValidator,
onChangeTrimmed,
} from "utils/formUtils";
import * as Yup from "yup";
import {
FormFields,
FormSection,
FormFooter,
HorizontalForm,
} from "components/Form/Form";
import {
AutofillBuildParameter,
AutofillSource,
getInitialRichParameterValues,
useValidationSchemaForRichParameters,
} from "utils/richParameters";
import { ErrorAlert } from "components/Alert/ErrorAlert";
import { Stack } from "components/Stack/Stack";
import {
CreateWorkspaceMode,
ExternalAuthPollingState,
} from "./CreateWorkspacePage";
import { useSearchParams } from "react-router-dom";
import { CreateWSPermissions } from "./permissions";
import { Alert } from "components/Alert/Alert";
import { ExternalAuthBanner } from "./ExternalAuthBanner/ExternalAuthBanner";
import { Margins } from "components/Margins/Margins";
import Button from "@mui/material/Button";
import { Avatar } from "components/Avatar/Avatar";
import {
PageHeader,
PageHeaderTitle,
PageHeaderSubtitle,
} from "components/PageHeader/PageHeader";
import { Pill } from "components/Pill/Pill";
import { RichParameterInput } from "components/RichParameterInput/RichParameterInput";
export const Language = {
duplicationWarning:
"Duplicating a workspace only copies its parameters. No state from the old workspace is copied over.",
} as const;
export interface CreateWorkspacePageViewProps {
mode: CreateWorkspaceMode;
error: unknown;
resetMutation: () => void;
defaultName: string;
defaultOwner: TypesGen.User;
template: TypesGen.Template;
versionId?: string;
externalAuth: TypesGen.TemplateVersionExternalAuth[];
externalAuthPollingState: ExternalAuthPollingState;
startPollingExternalAuth: () => void;
parameters: TypesGen.TemplateVersionParameter[];
autofillParameters: AutofillBuildParameter[];
permissions: CreateWSPermissions;
creatingWorkspace: boolean;
onCancel: () => void;
onSubmit: (
req: TypesGen.CreateWorkspaceRequest,
owner: TypesGen.User,
) => void;
}
export const CreateWorkspacePageView: FC<CreateWorkspacePageViewProps> = ({
mode,
error,
resetMutation,
defaultName,
defaultOwner,
template,
versionId,
externalAuth,
externalAuthPollingState,
startPollingExternalAuth,
parameters,
autofillParameters,
permissions,
creatingWorkspace,
onSubmit,
onCancel,
}) => {
const [owner, setOwner] = useState(defaultOwner);
const [searchParams] = useSearchParams();
const disabledParamsList = searchParams?.get("disable_params")?.split(",");
const requiresExternalAuth = externalAuth.some((auth) => !auth.authenticated);
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 (requiresExternalAuth) {
return;
}
onSubmit(request, owner);
},
});
useEffect(() => {
if (error) {
window.scrollTo(0, 0);
}
}, [error]);
const getFieldHelpers = getFormHelpers<TypesGen.CreateWorkspaceRequest>(
form,
error,
);
const autofillSources = useMemo(() => {
return autofillParameters.reduce(
(acc, param) => {
acc[param.name] = param.source;
return acc;
},
{} as Record<string, AutofillSource>,
);
}, [autofillParameters]);
return (
<Margins size="medium">
<PageHeader actions={<Button onClick={onCancel}>Cancel</Button>}>
<Stack direction="row" spacing={3} alignItems="center">
{template.icon !== "" ? (
<Avatar size="xl" src={template.icon} variant="square" fitImage />
) : (
<Avatar size="xl">{template.name}</Avatar>
)}
<div>
<PageHeaderTitle>
{template.display_name.length > 0
? template.display_name
: template.name}
</PageHeaderTitle>
<PageHeaderSubtitle condensed>New workspace</PageHeaderSubtitle>
</div>
{template.deprecated && <Pill type="warning">Deprecated</Pill>}
</Stack>
</PageHeader>
{requiresExternalAuth ? (
<ExternalAuthBanner
providers={externalAuth}
pollingState={externalAuthPollingState}
onStartPolling={startPollingExternalAuth}
/>
) : (
<HorizontalForm
name="create-workspace-form"
onSubmit={form.handleSubmit}
css={{ padding: "16px 0" }}
>
{Boolean(error) && <ErrorAlert error={error} />}
{mode === "duplicate" && (
<Alert severity="info" dismissible>
{Language.duplicationWarning}
</Alert>
)}
{/* General info */}
<FormSection
title="General"
description={
permissions.createWorkspaceForUser
? "The name of the workspace and its owner. Only admins can create workspace for other users."
: "The name of your new workspace."
}
>
<FormFields>
{versionId && versionId !== template.active_version_id && (
<Stack spacing={1} css={styles.hasDescription}>
<TextField
disabled
fullWidth
value={versionId}
label="Version ID"
/>
<span css={styles.description}>
This parameter has been preset, and cannot be modified.
</span>
</Stack>
)}
<TextField
{...getFieldHelpers("name")}
disabled={creatingWorkspace}
// resetMutation facilitates the clearing of validation errors
onChange={onChangeTrimmed(form, resetMutation)}
autoFocus
fullWidth
label="Workspace Name"
/>
{permissions.createWorkspaceForUser && (
<UserAutocomplete
value={owner}
onChange={(user) => {
setOwner(user ?? defaultOwner);
}}
label="Owner"
size="medium"
/>
)}
</FormFields>
</FormSection>
{parameters.length > 0 && (
<FormSection
title="Parameters"
description="These are the settings used by your template. Please note that immutable parameters cannot be modified once the workspace is created."
>
{/*
Opted not to use FormFields in order to increase spacing.
This decision was made because rich parameter inputs are more visually dense than standard text fields.
*/}
<div css={{ display: "flex", flexDirection: "column", gap: 36 }}>
{parameters.map((parameter, index) => {
const parameterField = `rich_parameter_values.${index}`;
const parameterInputName = `${parameterField}.value`;
const isDisabled =
disabledParamsList?.includes(
parameter.name.toLowerCase().replace(/ /g, "_"),
) || creatingWorkspace;
return (
<RichParameterInput
{...getFieldHelpers(parameterInputName)}
onChange={async (value) => {
await form.setFieldValue(parameterField, {
name: parameter.name,
value,
});
}}
autofillSource={autofillSources[parameter.name]}
key={parameter.name}
parameter={parameter}
disabled={isDisabled}
/>
);
})}
</div>
</FormSection>
)}
<FormFooter
onCancel={onCancel}
isLoading={creatingWorkspace}
submitLabel="Create Workspace"
/>
</HorizontalForm>
)}
</Margins>
);
};
const styles = {
hasDescription: {
paddingBottom: 16,
},
description: (theme) => ({
fontSize: 13,
color: theme.palette.text.secondary,
}),
} satisfies Record<string, Interpolation<Theme>>;