Skip to content

Commit b73e66e

Browse files
authored
feat: show workspace name suggestions below the name field (coder#12001)
1 parent 52ec3ed commit b73e66e

10 files changed

+104
-74
lines changed
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import {
2+
NumberDictionary,
3+
animals,
4+
colors,
5+
uniqueNamesGenerator,
6+
} from "unique-names-generator";
7+
8+
export const generateWorkspaceName = () => {
9+
const numberDictionary = NumberDictionary.generate({ min: 0, max: 99 });
10+
return uniqueNamesGenerator({
11+
dictionaries: [colors, animals, numberDictionary],
12+
separator: "-",
13+
length: 3,
14+
style: "lowerCase",
15+
});
16+
};

site/src/pages/CreateWorkspacePage/CreateWorkspacePage.test.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -214,7 +214,7 @@ describe("CreateWorkspacePage", () => {
214214
it("Detects when a workspace is being created with the 'duplicate' mode", async () => {
215215
const params = new URLSearchParams({
216216
mode: "duplicate",
217-
name: MockWorkspace.name,
217+
name: `${MockWorkspace.name}-copy`,
218218
version: MockWorkspace.template_active_version_id,
219219
});
220220

site/src/pages/CreateWorkspacePage/CreateWorkspacePage.tsx

Lines changed: 13 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
import { type FC, useCallback, useEffect, useState } from "react";
2+
import { Helmet } from "react-helmet-async";
3+
import { useMutation, useQuery, useQueryClient } from "react-query";
4+
import { useNavigate, useParams, useSearchParams } from "react-router-dom";
15
import { getUserParameters } from "api/api";
26
import { checkAuthorization } from "api/queries/authCheck";
37
import {
@@ -6,7 +10,7 @@ import {
610
templateVersionExternalAuth,
711
} from "api/queries/templates";
812
import { autoCreateWorkspace, createWorkspace } from "api/queries/workspaces";
9-
import {
13+
import type {
1014
TemplateVersionParameter,
1115
UserParameter,
1216
Workspace,
@@ -16,21 +20,12 @@ import { Loader } from "components/Loader/Loader";
1620
import { useMe } from "contexts/auth/useMe";
1721
import { useOrganizationId } from "contexts/auth/useOrganizationId";
1822
import { useEffectEvent } from "hooks/hookPolyfills";
19-
import { useCallback, useEffect, useMemo, useState, type FC } from "react";
20-
import { Helmet } from "react-helmet-async";
21-
import { useMutation, useQuery, useQueryClient } from "react-query";
22-
import { useNavigate, useParams, useSearchParams } from "react-router-dom";
23-
import {
24-
NumberDictionary,
25-
animals,
26-
colors,
27-
uniqueNamesGenerator,
28-
} from "unique-names-generator";
2923
import { pageTitle } from "utils/page";
3024
import { AutofillBuildParameter } from "utils/richParameters";
3125
import { paramsUsedToCreateWorkspace } from "utils/workspace";
3226
import { CreateWorkspacePageView } from "./CreateWorkspacePageView";
3327
import { CreateWSPermissions, createWorkspaceChecks } from "./permissions";
28+
import { generateWorkspaceName } from "modules/workspaces/generateWorkspaceName";
3429

3530
export const createWorkspaceModes = ["form", "auto", "duplicate"] as const;
3631
export type CreateWorkspaceMode = (typeof createWorkspaceModes)[number];
@@ -46,14 +41,7 @@ const CreateWorkspacePage: FC = () => {
4641
const mode = getWorkspaceMode(searchParams);
4742
const customVersionId = searchParams.get("version") ?? undefined;
4843

49-
const defaultName = useMemo(() => {
50-
const paramsName = searchParams.get("name");
51-
if (mode === "duplicate" && paramsName) {
52-
return `${paramsName}-copy`;
53-
}
54-
55-
return paramsName ?? generateUniqueName();
56-
}, [mode, searchParams]);
44+
const defaultName = searchParams.get("name");
5745

5846
const queryClient = useQueryClient();
5947
const autoCreateWorkspaceMutation = useMutation(
@@ -63,13 +51,11 @@ const CreateWorkspacePage: FC = () => {
6351

6452
const templateQuery = useQuery(templateByName(organizationId, templateName));
6553

66-
const userParametersQuery = useQuery(
67-
["userParameters"],
68-
() => getUserParameters(templateQuery.data!.id),
69-
{
70-
enabled: templateQuery.isSuccess,
71-
},
72-
);
54+
const userParametersQuery = useQuery({
55+
queryKey: ["userParameters"],
56+
queryFn: () => getUserParameters(templateQuery.data!.id),
57+
enabled: templateQuery.isSuccess,
58+
});
7359

7460
const permissionsQuery = useQuery(
7561
checkAuthorization({
@@ -122,7 +108,7 @@ const CreateWorkspacePage: FC = () => {
122108
templateName,
123109
organizationId,
124110
defaultBuildParameters: autofillParameters,
125-
defaultName,
111+
defaultName: defaultName ?? generateWorkspaceName(),
126112
versionId: realizedVersionId,
127113
});
128114

@@ -269,16 +255,6 @@ const getAutofillParameters = (
269255
return buildValues;
270256
};
271257

272-
const generateUniqueName = () => {
273-
const numberDictionary = NumberDictionary.generate({ min: 0, max: 99 });
274-
return uniqueNamesGenerator({
275-
dictionaries: [colors, animals, numberDictionary],
276-
separator: "-",
277-
length: 3,
278-
style: "lowerCase",
279-
});
280-
};
281-
282258
export default CreateWorkspacePage;
283259

284260
function getWorkspaceMode(params: URLSearchParams): CreateWorkspaceMode {

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

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,18 @@ export const CreateWorkspaceError: Story = {
4848
},
4949
};
5050

51+
export const SpecificVersion: Story = {
52+
args: {
53+
versionId: "specific-version",
54+
},
55+
};
56+
57+
export const Duplicate: Story = {
58+
args: {
59+
mode: "duplicate",
60+
},
61+
};
62+
5163
export const Parameters: Story = {
5264
args: {
5365
parameters: [

site/src/pages/CreateWorkspacePage/CreateWorkspacePageView.tsx

Lines changed: 53 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,24 @@
11
import { type Interpolation, type Theme } from "@emotion/react";
22
import TextField from "@mui/material/TextField";
3-
import type * as TypesGen from "api/typesGenerated";
4-
import { UserAutocomplete } from "components/UserAutocomplete/UserAutocomplete";
3+
import Button from "@mui/material/Button";
4+
import FormHelperText from "@mui/material/FormHelperText";
55
import { FormikContextType, useFormik } from "formik";
6-
import { type FC, useEffect, useState, useMemo } from "react";
6+
import { type FC, useEffect, useState, useMemo, useCallback } from "react";
7+
import { useSearchParams } from "react-router-dom";
8+
import * as Yup from "yup";
9+
import type * as TypesGen from "api/typesGenerated";
710
import {
811
getFormHelpers,
912
nameValidator,
1013
onChangeTrimmed,
1114
} from "utils/formUtils";
12-
import * as Yup from "yup";
1315
import {
1416
FormFields,
1517
FormSection,
1618
FormFooter,
1719
HorizontalForm,
1820
} from "components/Form/Form";
21+
import { UserAutocomplete } from "components/UserAutocomplete/UserAutocomplete";
1922
import {
2023
AutofillBuildParameter,
2124
AutofillSource,
@@ -24,16 +27,8 @@ import {
2427
} from "utils/richParameters";
2528
import { ErrorAlert } from "components/Alert/ErrorAlert";
2629
import { Stack } from "components/Stack/Stack";
27-
import {
28-
CreateWorkspaceMode,
29-
ExternalAuthPollingState,
30-
} from "./CreateWorkspacePage";
31-
import { useSearchParams } from "react-router-dom";
32-
import { CreateWSPermissions } from "./permissions";
3330
import { Alert } from "components/Alert/Alert";
34-
import { ExternalAuthBanner } from "./ExternalAuthBanner/ExternalAuthBanner";
3531
import { Margins } from "components/Margins/Margins";
36-
import Button from "@mui/material/Button";
3732
import { Avatar } from "components/Avatar/Avatar";
3833
import {
3934
PageHeader,
@@ -42,6 +37,13 @@ import {
4237
} from "components/PageHeader/PageHeader";
4338
import { Pill } from "components/Pill/Pill";
4439
import { RichParameterInput } from "components/RichParameterInput/RichParameterInput";
40+
import { generateWorkspaceName } from "modules/workspaces/generateWorkspaceName";
41+
import {
42+
CreateWorkspaceMode,
43+
ExternalAuthPollingState,
44+
} from "./CreateWorkspacePage";
45+
import { ExternalAuthBanner } from "./ExternalAuthBanner/ExternalAuthBanner";
46+
import { CreateWSPermissions } from "./permissions";
4547

4648
export const Language = {
4749
duplicationWarning:
@@ -52,7 +54,7 @@ export interface CreateWorkspacePageViewProps {
5254
mode: CreateWorkspaceMode;
5355
error: unknown;
5456
resetMutation: () => void;
55-
defaultName: string;
57+
defaultName?: string | null;
5658
defaultOwner: TypesGen.User;
5759
template: TypesGen.Template;
5860
versionId?: string;
@@ -92,11 +94,18 @@ export const CreateWorkspacePageView: FC<CreateWorkspacePageViewProps> = ({
9294
const [searchParams] = useSearchParams();
9395
const disabledParamsList = searchParams?.get("disable_params")?.split(",");
9496
const requiresExternalAuth = externalAuth.some((auth) => !auth.authenticated);
97+
const [suggestedName, setSuggestedName] = useState(() =>
98+
generateWorkspaceName(),
99+
);
100+
101+
const rerollSuggestedName = useCallback(() => {
102+
setSuggestedName(() => generateWorkspaceName());
103+
}, []);
95104

96105
const form: FormikContextType<TypesGen.CreateWorkspaceRequest> =
97106
useFormik<TypesGen.CreateWorkspaceRequest>({
98107
initialValues: {
99-
name: defaultName,
108+
name: defaultName ?? "",
100109
template_id: template.id,
101110
rich_parameter_values: getInitialRichParameterValues(
102111
parameters,
@@ -205,16 +214,29 @@ export const CreateWorkspacePageView: FC<CreateWorkspacePageViewProps> = ({
205214
</span>
206215
</Stack>
207216
)}
208-
209-
<TextField
210-
{...getFieldHelpers("name")}
211-
disabled={creatingWorkspace}
212-
// resetMutation facilitates the clearing of validation errors
213-
onChange={onChangeTrimmed(form, resetMutation)}
214-
autoFocus
215-
fullWidth
216-
label="Workspace Name"
217-
/>
217+
<div>
218+
<TextField
219+
{...getFieldHelpers("name")}
220+
disabled={creatingWorkspace}
221+
// resetMutation facilitates the clearing of validation errors
222+
onChange={onChangeTrimmed(form, resetMutation)}
223+
fullWidth
224+
label="Workspace Name"
225+
/>
226+
<FormHelperText data-chromatic="ignore">
227+
Need a suggestion?{" "}
228+
<Button
229+
variant="text"
230+
css={styles.nameSuggestion}
231+
onClick={async () => {
232+
await form.setFieldValue("name", suggestedName);
233+
rerollSuggestedName();
234+
}}
235+
>
236+
{suggestedName}
237+
</Button>
238+
</FormHelperText>
239+
</div>
218240

219241
{permissions.createWorkspaceForUser && (
220242
<UserAutocomplete
@@ -279,6 +301,13 @@ export const CreateWorkspacePageView: FC<CreateWorkspacePageViewProps> = ({
279301
};
280302

281303
const styles = {
304+
nameSuggestion: (theme) => ({
305+
color: theme.roles.info.fill.solid,
306+
padding: "4px 8px",
307+
lineHeight: "inherit",
308+
fontSize: "inherit",
309+
height: "unset",
310+
}),
282311
hasDescription: {
283312
paddingBottom: 16,
284313
},

site/src/pages/CreateWorkspacePage/useWorkspaceDuplication.test.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ describe(`${useWorkspaceDuplication.name}`, () => {
8888
const parsedParams = new URLSearchParams(router.state.location.search);
8989
const extraMetadataEntries = [
9090
["mode", "duplicate"],
91-
["name", MockWorkspace.name],
91+
["name", `${MockWorkspace.name}-copy`],
9292
["version", MockWorkspace.template_active_version_id],
9393
] as const;
9494

site/src/pages/CreateWorkspacePage/useWorkspaceDuplication.ts

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,9 @@
1-
import { useNavigate } from "react-router-dom";
1+
import { useCallback } from "react";
22
import { useQuery } from "react-query";
3-
import { type CreateWorkspaceMode } from "./CreateWorkspacePage";
4-
import {
5-
type Workspace,
6-
type WorkspaceBuildParameter,
7-
} from "api/typesGenerated";
3+
import { useNavigate } from "react-router-dom";
4+
import type { Workspace, WorkspaceBuildParameter } from "api/typesGenerated";
85
import { workspaceBuildParameters } from "api/queries/workspaceBuilds";
9-
import { useCallback } from "react";
6+
import { type CreateWorkspaceMode } from "./CreateWorkspacePage";
107

118
function getDuplicationUrlParams(
129
workspaceParams: readonly WorkspaceBuildParameter[],
@@ -23,7 +20,7 @@ function getDuplicationUrlParams(
2320
return new URLSearchParams({
2421
...consolidatedParams,
2522
mode: "duplicate" satisfies CreateWorkspaceMode,
26-
name: workspace.name,
23+
name: `${workspace.name}-copy`,
2724
version: workspace.template_active_version_id,
2825
});
2926
}

site/src/theme/dark/roles.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ export default {
6767
outline: colors.blue[400],
6868
text: colors.blue[50],
6969
fill: {
70-
solid: colors.blue[600],
70+
solid: colors.blue[500],
7171
outline: colors.blue[600],
7272
text: colors.white,
7373
},

site/src/theme/darkBlue/roles.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ export default {
6868
text: colors.blue[50],
6969
fill: {
7070
solid: colors.blue[500],
71-
outline: colors.blue[500],
71+
outline: colors.blue[600],
7272
text: colors.white,
7373
},
7474
},

site/src/theme/light/roles.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ export default {
6767
outline: colors.blue[400],
6868
text: colors.blue[950],
6969
fill: {
70-
solid: colors.blue[600],
70+
solid: colors.blue[700],
7171
outline: colors.blue[600],
7272
text: colors.white,
7373
},

0 commit comments

Comments
 (0)