diff --git a/site/src/modules/workspaces/generateWorkspaceName.ts b/site/src/modules/workspaces/generateWorkspaceName.ts new file mode 100644 index 0000000000000..cb9113b772315 --- /dev/null +++ b/site/src/modules/workspaces/generateWorkspaceName.ts @@ -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", + }); +}; diff --git a/site/src/pages/CreateWorkspacePage/CreateWorkspacePage.test.tsx b/site/src/pages/CreateWorkspacePage/CreateWorkspacePage.test.tsx index 4dc65ed64c8fa..d301e21bbe839 100644 --- a/site/src/pages/CreateWorkspacePage/CreateWorkspacePage.test.tsx +++ b/site/src/pages/CreateWorkspacePage/CreateWorkspacePage.test.tsx @@ -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, }); diff --git a/site/src/pages/CreateWorkspacePage/CreateWorkspacePage.tsx b/site/src/pages/CreateWorkspacePage/CreateWorkspacePage.tsx index 036e05548e3c9..2136e6c3600cb 100644 --- a/site/src/pages/CreateWorkspacePage/CreateWorkspacePage.tsx +++ b/site/src/pages/CreateWorkspacePage/CreateWorkspacePage.tsx @@ -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 { diff --git a/site/src/pages/CreateWorkspacePage/CreateWorkspacePageView.stories.tsx b/site/src/pages/CreateWorkspacePage/CreateWorkspacePageView.stories.tsx index fc0a23810b939..5662f96338ef6 100644 --- a/site/src/pages/CreateWorkspacePage/CreateWorkspacePageView.stories.tsx +++ b/site/src/pages/CreateWorkspacePage/CreateWorkspacePageView.stories.tsx @@ -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: [ diff --git a/site/src/pages/CreateWorkspacePage/CreateWorkspacePageView.tsx b/site/src/pages/CreateWorkspacePage/CreateWorkspacePageView.tsx index 7737a79161b42..1d2cdcf660a4f 100644 --- a/site/src/pages/CreateWorkspacePage/CreateWorkspacePageView.tsx +++ b/site/src/pages/CreateWorkspacePage/CreateWorkspacePageView.tsx @@ -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 = ({ 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 = useFormik({ initialValues: { - name: defaultName, + name: defaultName ?? "", template_id: template.id, rich_parameter_values: getInitialRichParameterValues( parameters, @@ -205,16 +214,29 @@ export const CreateWorkspacePageView: FC = ({ )} - - +
+ + + Need a suggestion?{" "} + + +
{permissions.createWorkspaceForUser && ( = ({ }; const styles = { + nameSuggestion: (theme) => ({ + color: theme.roles.info.fill.solid, + padding: "4px 8px", + lineHeight: "inherit", + fontSize: "inherit", + height: "unset", + }), hasDescription: { paddingBottom: 16, }, diff --git a/site/src/pages/CreateWorkspacePage/useWorkspaceDuplication.test.tsx b/site/src/pages/CreateWorkspacePage/useWorkspaceDuplication.test.tsx index a286aa8311149..d4a72e694e03e 100644 --- a/site/src/pages/CreateWorkspacePage/useWorkspaceDuplication.test.tsx +++ b/site/src/pages/CreateWorkspacePage/useWorkspaceDuplication.test.tsx @@ -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; diff --git a/site/src/pages/CreateWorkspacePage/useWorkspaceDuplication.ts b/site/src/pages/CreateWorkspacePage/useWorkspaceDuplication.ts index 6e929dd6f092f..c1777eb3d2b5c 100644 --- a/site/src/pages/CreateWorkspacePage/useWorkspaceDuplication.ts +++ b/site/src/pages/CreateWorkspacePage/useWorkspaceDuplication.ts @@ -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[], @@ -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, }); } diff --git a/site/src/theme/dark/roles.ts b/site/src/theme/dark/roles.ts index 1831fe65367ea..6a116863fd448 100644 --- a/site/src/theme/dark/roles.ts +++ b/site/src/theme/dark/roles.ts @@ -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, }, diff --git a/site/src/theme/darkBlue/roles.ts b/site/src/theme/darkBlue/roles.ts index effff5a65a8d1..d83eab54e0e28 100644 --- a/site/src/theme/darkBlue/roles.ts +++ b/site/src/theme/darkBlue/roles.ts @@ -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, }, }, diff --git a/site/src/theme/light/roles.ts b/site/src/theme/light/roles.ts index 983ebea2b501e..a26fd5fe7ecc2 100644 --- a/site/src/theme/light/roles.ts +++ b/site/src/theme/light/roles.ts @@ -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, },