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

This commit is contained in:
Kayla Washburn-Love
2024-02-05 10:40:15 -07:00
committed by GitHub
parent 52ec3edd5d
commit b73e66e9a9
10 changed files with 105 additions and 75 deletions
@@ -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,
});
}
+1 -1
View File
@@ -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,
},
+1 -1
View File
@@ -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,
},
},
+1 -1
View File
@@ -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,
},