Skip to content

feat: show workspace name suggestions below the name field #12001

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
Feb 5, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions site/src/modules/workspaces/generateWorkspaceName.ts
Original file line number Diff line number Diff line change
@@ -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",
});
};
Original file line number Diff line number Diff line change
Expand Up @@ -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,
});

Expand Down
50 changes: 13 additions & 37 deletions site/src/pages/CreateWorkspacePage/CreateWorkspacePage.tsx
Original file line number Diff line number Diff line change
@@ -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 {
Expand All @@ -6,7 +10,7 @@ import {
templateVersionExternalAuth,
} from "api/queries/templates";
import { autoCreateWorkspace, createWorkspace } from "api/queries/workspaces";
import {
import type {
TemplateVersionParameter,
UserParameter,
Workspace,
Expand All @@ -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];
Expand All @@ -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(
Expand All @@ -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({
Expand Down Expand Up @@ -122,7 +108,7 @@ const CreateWorkspacePage: FC = () => {
templateName,
organizationId,
defaultBuildParameters: autofillParameters,
defaultName,
defaultName: defaultName ?? generateWorkspaceName(),
versionId: realizedVersionId,
});

Expand Down Expand Up @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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: [
Expand Down
77 changes: 53 additions & 24 deletions site/src/pages/CreateWorkspacePage/CreateWorkspacePageView.tsx
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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,
Expand All @@ -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:
Expand All @@ -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;
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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,
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
13 changes: 5 additions & 8 deletions site/src/pages/CreateWorkspacePage/useWorkspaceDuplication.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,9 @@
import { useNavigate } from "react-router-dom";
import { useCallback } from "react";
import { useQuery } from "react-query";
import { type CreateWorkspaceMode } from "./CreateWorkspacePage";
import {
type Workspace,
type WorkspaceBuildParameter,
} from "api/typesGenerated";
import { useNavigate } from "react-router-dom";
import type { Workspace, WorkspaceBuildParameter } from "api/typesGenerated";
import { workspaceBuildParameters } from "api/queries/workspaceBuilds";
import { useCallback } from "react";
import { type CreateWorkspaceMode } from "./CreateWorkspacePage";

function getDuplicationUrlParams(
workspaceParams: readonly WorkspaceBuildParameter[],
Expand All @@ -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,
});
}
Expand Down
2 changes: 1 addition & 1 deletion site/src/theme/dark/roles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
},
Expand Down
2 changes: 1 addition & 1 deletion site/src/theme/darkBlue/roles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
},
},
Expand Down
2 changes: 1 addition & 1 deletion site/src/theme/light/roles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
},
Expand Down