mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
feat: show workspace name suggestions below the name field (#12001)
This commit is contained in:
@@ -0,0 +1,16 @@
|
||||
import {
|
||||
NumberDictionary,
|
||||
animals,
|
||||
colors,
|
||||
uniqueNamesGenerator,
|
||||
} from "unique-names-generator";
|
||||
|
||||
export const generateWorkspaceName = () => {
|
||||
const numberDictionary = NumberDictionary.generate({ min: 0, max: 99 });
|
||||
return uniqueNamesGenerator({
|
||||
dictionaries: [colors, animals, numberDictionary],
|
||||
separator: "-",
|
||||
length: 3,
|
||||
style: "lowerCase",
|
||||
});
|
||||
};
|
||||
@@ -214,7 +214,7 @@ describe("CreateWorkspacePage", () => {
|
||||
it("Detects when a workspace is being created with the 'duplicate' mode", async () => {
|
||||
const params = new URLSearchParams({
|
||||
mode: "duplicate",
|
||||
name: MockWorkspace.name,
|
||||
name: `${MockWorkspace.name}-copy`,
|
||||
version: MockWorkspace.template_active_version_id,
|
||||
});
|
||||
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
import { type FC, useCallback, useEffect, 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 { getUserParameters } from "api/api";
|
||||
import { checkAuthorization } from "api/queries/authCheck";
|
||||
import {
|
||||
@@ -6,7 +10,7 @@ import {
|
||||
templateVersionExternalAuth,
|
||||
} from "api/queries/templates";
|
||||
import { autoCreateWorkspace, createWorkspace } from "api/queries/workspaces";
|
||||
import {
|
||||
import type {
|
||||
TemplateVersionParameter,
|
||||
UserParameter,
|
||||
Workspace,
|
||||
@@ -16,21 +20,12 @@ import { Loader } from "components/Loader/Loader";
|
||||
import { useMe } from "contexts/auth/useMe";
|
||||
import { useOrganizationId } from "contexts/auth/useOrganizationId";
|
||||
import { useEffectEvent } from "hooks/hookPolyfills";
|
||||
import { useCallback, useEffect, useMemo, useState, type FC } from "react";
|
||||
import { Helmet } from "react-helmet-async";
|
||||
import { useMutation, useQuery, useQueryClient } from "react-query";
|
||||
import { useNavigate, useParams, useSearchParams } from "react-router-dom";
|
||||
import {
|
||||
NumberDictionary,
|
||||
animals,
|
||||
colors,
|
||||
uniqueNamesGenerator,
|
||||
} from "unique-names-generator";
|
||||
import { pageTitle } from "utils/page";
|
||||
import { AutofillBuildParameter } from "utils/richParameters";
|
||||
import { paramsUsedToCreateWorkspace } from "utils/workspace";
|
||||
import { CreateWorkspacePageView } from "./CreateWorkspacePageView";
|
||||
import { CreateWSPermissions, createWorkspaceChecks } from "./permissions";
|
||||
import { generateWorkspaceName } from "modules/workspaces/generateWorkspaceName";
|
||||
|
||||
export const createWorkspaceModes = ["form", "auto", "duplicate"] as const;
|
||||
export type CreateWorkspaceMode = (typeof createWorkspaceModes)[number];
|
||||
@@ -46,14 +41,7 @@ const CreateWorkspacePage: FC = () => {
|
||||
const mode = getWorkspaceMode(searchParams);
|
||||
const customVersionId = searchParams.get("version") ?? undefined;
|
||||
|
||||
const defaultName = useMemo(() => {
|
||||
const paramsName = searchParams.get("name");
|
||||
if (mode === "duplicate" && paramsName) {
|
||||
return `${paramsName}-copy`;
|
||||
}
|
||||
|
||||
return paramsName ?? generateUniqueName();
|
||||
}, [mode, searchParams]);
|
||||
const defaultName = searchParams.get("name");
|
||||
|
||||
const queryClient = useQueryClient();
|
||||
const autoCreateWorkspaceMutation = useMutation(
|
||||
@@ -63,13 +51,11 @@ const CreateWorkspacePage: FC = () => {
|
||||
|
||||
const templateQuery = useQuery(templateByName(organizationId, templateName));
|
||||
|
||||
const userParametersQuery = useQuery(
|
||||
["userParameters"],
|
||||
() => getUserParameters(templateQuery.data!.id),
|
||||
{
|
||||
enabled: templateQuery.isSuccess,
|
||||
},
|
||||
);
|
||||
const userParametersQuery = useQuery({
|
||||
queryKey: ["userParameters"],
|
||||
queryFn: () => getUserParameters(templateQuery.data!.id),
|
||||
enabled: templateQuery.isSuccess,
|
||||
});
|
||||
|
||||
const permissionsQuery = useQuery(
|
||||
checkAuthorization({
|
||||
@@ -122,7 +108,7 @@ const CreateWorkspacePage: FC = () => {
|
||||
templateName,
|
||||
organizationId,
|
||||
defaultBuildParameters: autofillParameters,
|
||||
defaultName,
|
||||
defaultName: defaultName ?? generateWorkspaceName(),
|
||||
versionId: realizedVersionId,
|
||||
});
|
||||
|
||||
@@ -269,16 +255,6 @@ const getAutofillParameters = (
|
||||
return buildValues;
|
||||
};
|
||||
|
||||
const generateUniqueName = () => {
|
||||
const numberDictionary = NumberDictionary.generate({ min: 0, max: 99 });
|
||||
return uniqueNamesGenerator({
|
||||
dictionaries: [colors, animals, numberDictionary],
|
||||
separator: "-",
|
||||
length: 3,
|
||||
style: "lowerCase",
|
||||
});
|
||||
};
|
||||
|
||||
export default CreateWorkspacePage;
|
||||
|
||||
function getWorkspaceMode(params: URLSearchParams): CreateWorkspaceMode {
|
||||
|
||||
@@ -48,6 +48,18 @@ export const CreateWorkspaceError: Story = {
|
||||
},
|
||||
};
|
||||
|
||||
export const SpecificVersion: Story = {
|
||||
args: {
|
||||
versionId: "specific-version",
|
||||
},
|
||||
};
|
||||
|
||||
export const Duplicate: Story = {
|
||||
args: {
|
||||
mode: "duplicate",
|
||||
},
|
||||
};
|
||||
|
||||
export const Parameters: Story = {
|
||||
args: {
|
||||
parameters: [
|
||||
|
||||
@@ -1,21 +1,24 @@
|
||||
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 Button from "@mui/material/Button";
|
||||
import FormHelperText from "@mui/material/FormHelperText";
|
||||
import { FormikContextType, useFormik } from "formik";
|
||||
import { type FC, useEffect, useState, useMemo } from "react";
|
||||
import { type FC, useEffect, useState, useMemo, useCallback } from "react";
|
||||
import { useSearchParams } from "react-router-dom";
|
||||
import * as Yup from "yup";
|
||||
import type * as TypesGen from "api/typesGenerated";
|
||||
import {
|
||||
getFormHelpers,
|
||||
nameValidator,
|
||||
onChangeTrimmed,
|
||||
} from "utils/formUtils";
|
||||
import * as Yup from "yup";
|
||||
import {
|
||||
FormFields,
|
||||
FormSection,
|
||||
FormFooter,
|
||||
HorizontalForm,
|
||||
} from "components/Form/Form";
|
||||
import { UserAutocomplete } from "components/UserAutocomplete/UserAutocomplete";
|
||||
import {
|
||||
AutofillBuildParameter,
|
||||
AutofillSource,
|
||||
@@ -24,16 +27,8 @@ import {
|
||||
} 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,
|
||||
@@ -42,6 +37,13 @@ import {
|
||||
} from "components/PageHeader/PageHeader";
|
||||
import { Pill } from "components/Pill/Pill";
|
||||
import { RichParameterInput } from "components/RichParameterInput/RichParameterInput";
|
||||
import { generateWorkspaceName } from "modules/workspaces/generateWorkspaceName";
|
||||
import {
|
||||
CreateWorkspaceMode,
|
||||
ExternalAuthPollingState,
|
||||
} from "./CreateWorkspacePage";
|
||||
import { ExternalAuthBanner } from "./ExternalAuthBanner/ExternalAuthBanner";
|
||||
import { CreateWSPermissions } from "./permissions";
|
||||
|
||||
export const Language = {
|
||||
duplicationWarning:
|
||||
@@ -52,7 +54,7 @@ export interface CreateWorkspacePageViewProps {
|
||||
mode: CreateWorkspaceMode;
|
||||
error: unknown;
|
||||
resetMutation: () => void;
|
||||
defaultName: string;
|
||||
defaultName?: string | null;
|
||||
defaultOwner: TypesGen.User;
|
||||
template: TypesGen.Template;
|
||||
versionId?: string;
|
||||
@@ -92,11 +94,18 @@ export const CreateWorkspacePageView: FC<CreateWorkspacePageViewProps> = ({
|
||||
const [searchParams] = useSearchParams();
|
||||
const disabledParamsList = searchParams?.get("disable_params")?.split(",");
|
||||
const requiresExternalAuth = externalAuth.some((auth) => !auth.authenticated);
|
||||
const [suggestedName, setSuggestedName] = useState(() =>
|
||||
generateWorkspaceName(),
|
||||
);
|
||||
|
||||
const rerollSuggestedName = useCallback(() => {
|
||||
setSuggestedName(() => generateWorkspaceName());
|
||||
}, []);
|
||||
|
||||
const form: FormikContextType<TypesGen.CreateWorkspaceRequest> =
|
||||
useFormik<TypesGen.CreateWorkspaceRequest>({
|
||||
initialValues: {
|
||||
name: defaultName,
|
||||
name: defaultName ?? "",
|
||||
template_id: template.id,
|
||||
rich_parameter_values: getInitialRichParameterValues(
|
||||
parameters,
|
||||
@@ -205,16 +214,29 @@ export const CreateWorkspacePageView: FC<CreateWorkspacePageViewProps> = ({
|
||||
</span>
|
||||
</Stack>
|
||||
)}
|
||||
|
||||
<TextField
|
||||
{...getFieldHelpers("name")}
|
||||
disabled={creatingWorkspace}
|
||||
// resetMutation facilitates the clearing of validation errors
|
||||
onChange={onChangeTrimmed(form, resetMutation)}
|
||||
autoFocus
|
||||
fullWidth
|
||||
label="Workspace Name"
|
||||
/>
|
||||
<div>
|
||||
<TextField
|
||||
{...getFieldHelpers("name")}
|
||||
disabled={creatingWorkspace}
|
||||
// resetMutation facilitates the clearing of validation errors
|
||||
onChange={onChangeTrimmed(form, resetMutation)}
|
||||
fullWidth
|
||||
label="Workspace Name"
|
||||
/>
|
||||
<FormHelperText data-chromatic="ignore">
|
||||
Need a suggestion?{" "}
|
||||
<Button
|
||||
variant="text"
|
||||
css={styles.nameSuggestion}
|
||||
onClick={async () => {
|
||||
await form.setFieldValue("name", suggestedName);
|
||||
rerollSuggestedName();
|
||||
}}
|
||||
>
|
||||
{suggestedName}
|
||||
</Button>
|
||||
</FormHelperText>
|
||||
</div>
|
||||
|
||||
{permissions.createWorkspaceForUser && (
|
||||
<UserAutocomplete
|
||||
@@ -279,6 +301,13 @@ export const CreateWorkspacePageView: FC<CreateWorkspacePageViewProps> = ({
|
||||
};
|
||||
|
||||
const styles = {
|
||||
nameSuggestion: (theme) => ({
|
||||
color: theme.roles.info.fill.solid,
|
||||
padding: "4px 8px",
|
||||
lineHeight: "inherit",
|
||||
fontSize: "inherit",
|
||||
height: "unset",
|
||||
}),
|
||||
hasDescription: {
|
||||
paddingBottom: 16,
|
||||
},
|
||||
|
||||
@@ -88,7 +88,7 @@ describe(`${useWorkspaceDuplication.name}`, () => {
|
||||
const parsedParams = new URLSearchParams(router.state.location.search);
|
||||
const extraMetadataEntries = [
|
||||
["mode", "duplicate"],
|
||||
["name", MockWorkspace.name],
|
||||
["name", `${MockWorkspace.name}-copy`],
|
||||
["version", MockWorkspace.template_active_version_id],
|
||||
] as const;
|
||||
|
||||
|
||||
@@ -1,12 +1,9 @@
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { useQuery } from "react-query";
|
||||
import { type CreateWorkspaceMode } from "./CreateWorkspacePage";
|
||||
import {
|
||||
type Workspace,
|
||||
type WorkspaceBuildParameter,
|
||||
} from "api/typesGenerated";
|
||||
import { workspaceBuildParameters } from "api/queries/workspaceBuilds";
|
||||
import { useCallback } from "react";
|
||||
import { useQuery } from "react-query";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import type { Workspace, WorkspaceBuildParameter } from "api/typesGenerated";
|
||||
import { workspaceBuildParameters } from "api/queries/workspaceBuilds";
|
||||
import { type CreateWorkspaceMode } from "./CreateWorkspacePage";
|
||||
|
||||
function getDuplicationUrlParams(
|
||||
workspaceParams: readonly WorkspaceBuildParameter[],
|
||||
@@ -23,7 +20,7 @@ function getDuplicationUrlParams(
|
||||
return new URLSearchParams({
|
||||
...consolidatedParams,
|
||||
mode: "duplicate" satisfies CreateWorkspaceMode,
|
||||
name: workspace.name,
|
||||
name: `${workspace.name}-copy`,
|
||||
version: workspace.template_active_version_id,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -67,7 +67,7 @@ export default {
|
||||
outline: colors.blue[400],
|
||||
text: colors.blue[50],
|
||||
fill: {
|
||||
solid: colors.blue[600],
|
||||
solid: colors.blue[500],
|
||||
outline: colors.blue[600],
|
||||
text: colors.white,
|
||||
},
|
||||
|
||||
@@ -68,7 +68,7 @@ export default {
|
||||
text: colors.blue[50],
|
||||
fill: {
|
||||
solid: colors.blue[500],
|
||||
outline: colors.blue[500],
|
||||
outline: colors.blue[600],
|
||||
text: colors.white,
|
||||
},
|
||||
},
|
||||
|
||||
@@ -67,7 +67,7 @@ export default {
|
||||
outline: colors.blue[400],
|
||||
text: colors.blue[950],
|
||||
fill: {
|
||||
solid: colors.blue[600],
|
||||
solid: colors.blue[700],
|
||||
outline: colors.blue[600],
|
||||
text: colors.white,
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user