From 8a0094ce70daf3e159be919fa433399fb406d06a Mon Sep 17 00:00:00 2001 From: Jaayden Halko Date: Wed, 8 Jan 2025 13:01:00 +0000 Subject: [PATCH] feat: add redesigned organization settings sidebar (#15932) resolves coder/internal#173, coder/internal#175 This PR does the following 1. Updates the left sidebar for organizations to use a dropdown to select the organization 2. Move the create organization button inside the dropdown 3. Update the design of the create organization page 4. Do not display the organization in the dropdown if there is only 1 org to display Figma: https://www.figma.com/design/OR75XeUI0Z3ksqt1mHsNQw/Dashboard-v1?node-id=139-1380&m=dev The loading state for the save button in the create organization form will be handled separately after #14978 is completed. Note: Since the dropdown is based off the cmdk component, navigation in the dropdown is handled by the arrow keys, https://cmdk.paco.me/ Screenshot 2025-01-03 at 21 11 26 Screenshot 2025-01-03 at 21 11 39 Screenshot 2025-01-03 at 21 12 05 Screenshot 2025-01-03 at 21 12 39 --- pnpm-lock.yaml | 16 +- site/e2e/tests/organizationGroups.spec.ts | 2 +- site/e2e/tests/organizationMembers.spec.ts | 2 +- site/e2e/tests/organizations.spec.ts | 12 +- site/src/components/Command/Command.tsx | 6 +- site/src/components/Popover/Popover.tsx | 2 +- site/src/components/Sidebar/Sidebar.tsx | 2 +- .../management/DeploymentSettingsLayout.tsx | 4 +- .../DeploymentSidebarView.stories.tsx | 2 +- .../management/OrganizationSettingsLayout.tsx | 21 +- .../management/OrganizationSidebar.tsx | 4 +- .../management/OrganizationSidebarLayout.tsx | 19 ++ .../OrganizationSidebarView.stories.tsx | 216 ++++++++---- .../management/OrganizationSidebarView.tsx | 318 ++++++++---------- .../CreateOrganizationPage.tsx | 20 +- .../CreateOrganizationPageView.tsx | 161 ++++----- .../OrganizationSettingsPage.tsx | 2 +- site/src/router.tsx | 9 +- 18 files changed, 465 insertions(+), 353 deletions(-) create mode 100644 site/src/modules/management/OrganizationSidebarLayout.tsx diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c97cbb1a8d..eb8fcb06d8 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -106,15 +106,15 @@ packages: resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} engines: {node: '>=0.12'} - fast-glob@3.3.2: - resolution: {integrity: sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==} + fast-glob@3.3.3: + resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==} engines: {node: '>=8.6.0'} fast-levenshtein@2.0.6: resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} - fastq@1.17.1: - resolution: {integrity: sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==} + fastq@1.18.0: + resolution: {integrity: sha512-QKHXPW0hD8g4UET03SdOdunzSouc9N4AuHdsX8XNcTsuz+yYFILVNIX4l9yHABMhiEI9Db0JTTIpu0wB+Y1QQw==} fill-range@7.1.1: resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} @@ -376,7 +376,7 @@ snapshots: '@nodelib/fs.walk@1.2.8': dependencies: '@nodelib/fs.scandir': 2.1.5 - fastq: 1.17.1 + fastq: 1.18.0 '@pkgjs/parseargs@0.11.0': optional: true @@ -431,7 +431,7 @@ snapshots: entities@4.5.0: {} - fast-glob@3.3.2: + fast-glob@3.3.3: dependencies: '@nodelib/fs.stat': 2.0.5 '@nodelib/fs.walk': 1.2.8 @@ -441,7 +441,7 @@ snapshots: fast-levenshtein@2.0.6: {} - fastq@1.17.1: + fastq@1.18.0: dependencies: reusify: 1.0.4 @@ -478,7 +478,7 @@ snapshots: globby@14.0.2: dependencies: '@sindresorhus/merge-streams': 2.3.0 - fast-glob: 3.3.2 + fast-glob: 3.3.3 ignore: 5.3.2 path-type: 5.0.0 slash: 5.1.0 diff --git a/site/e2e/tests/organizationGroups.spec.ts b/site/e2e/tests/organizationGroups.spec.ts index e774de2a74..2d0a41acaf 100644 --- a/site/e2e/tests/organizationGroups.spec.ts +++ b/site/e2e/tests/organizationGroups.spec.ts @@ -23,7 +23,7 @@ test("create group", async ({ page }) => { await page.goto(`/organizations/${org.name}`); // Navigate to groups page - await page.getByText("Groups").click(); + await page.getByRole("link", { name: "Groups" }).click(); await expect(page).toHaveTitle(`Groups - Org ${org.name} - Coder`); // Create a new group diff --git a/site/e2e/tests/organizationMembers.spec.ts b/site/e2e/tests/organizationMembers.spec.ts index de20ebd977..9edb2eb922 100644 --- a/site/e2e/tests/organizationMembers.spec.ts +++ b/site/e2e/tests/organizationMembers.spec.ts @@ -21,7 +21,7 @@ test("add and remove organization member", async ({ page }) => { const { displayName } = await createOrganization(page); // Navigate to members page - await page.getByText("Members").click(); + await page.getByRole("link", { name: "Members" }).click(); await expect(page).toHaveTitle(`Members - ${displayName} - Coder`); // Add a user to the org diff --git a/site/e2e/tests/organizations.spec.ts b/site/e2e/tests/organizations.spec.ts index 268640f28f..5a1cf4ba82 100644 --- a/site/e2e/tests/organizations.spec.ts +++ b/site/e2e/tests/organizations.spec.ts @@ -29,15 +29,25 @@ test("create and delete organization", async ({ page }) => { await expectUrl(page).toHavePathName(`/organizations/${name}`); await expect(page.getByText("Organization created.")).toBeVisible(); + await page.goto(`/organizations/${name}/settings`, { + waitUntil: "domcontentloaded", + }); + const newName = randomName(); await page.getByLabel("Slug").fill(newName); await page.getByLabel("Description").fill(`Org description ${newName}`); await page.getByRole("button", { name: /save/i }).click(); // Expect to be redirected when renaming the organization - await expectUrl(page).toHavePathName(`/organizations/${newName}`); + await expectUrl(page).toHavePathName(`/organizations/${newName}/settings`); await expect(page.getByText("Organization settings updated.")).toBeVisible(); + await page.goto(`/organizations/${newName}/settings`, { + waitUntil: "domcontentloaded", + }); + // Expect to be redirected when renaming the organization + await expectUrl(page).toHavePathName(`/organizations/${newName}/settings`); + await page.getByRole("button", { name: "Delete this organization" }).click(); const dialog = page.getByTestId("dialog"); await dialog.getByLabel("Name").fill(newName); diff --git a/site/src/components/Command/Command.tsx b/site/src/components/Command/Command.tsx index d101e8159f..bbdc5684cb 100644 --- a/site/src/components/Command/Command.tsx +++ b/site/src/components/Command/Command.tsx @@ -69,7 +69,7 @@ export const CommandList = forwardRef< >(({ className, ...props }, ref) => ( )); @@ -92,7 +92,7 @@ export const CommandGroup = forwardRef< = ({ to={href} className={({ isActive }) => cn( - "relative text-sm text-content-secondary no-underline font-medium py-2 px-3 hover:bg-surface-secondary rounded-md transition ease-in-out duration-150 ", + "relative text-sm text-content-secondary no-underline font-medium py-2 px-3 hover:bg-surface-secondary rounded-md transition ease-in-out duration-150", { "font-semibold text-content-primary": isActive, }, diff --git a/site/src/modules/management/DeploymentSettingsLayout.tsx b/site/src/modules/management/DeploymentSettingsLayout.tsx index 65c2e70ea3..676a24c936 100644 --- a/site/src/modules/management/DeploymentSettingsLayout.tsx +++ b/site/src/modules/management/DeploymentSettingsLayout.tsx @@ -39,8 +39,8 @@ const DeploymentSettingsLayout: FC = () => {
-
-
+
+
}> diff --git a/site/src/modules/management/DeploymentSidebarView.stories.tsx b/site/src/modules/management/DeploymentSidebarView.stories.tsx index 443a5cfd41..5bda860b4b 100644 --- a/site/src/modules/management/DeploymentSidebarView.stories.tsx +++ b/site/src/modules/management/DeploymentSidebarView.stories.tsx @@ -4,7 +4,7 @@ import { withDashboardProvider } from "testHelpers/storybook"; import { DeploymentSidebarView } from "./DeploymentSidebarView"; const meta: Meta = { - title: "modules/management/SidebarView", + title: "modules/management/DeploymentSidebarView", component: DeploymentSidebarView, decorators: [withDashboardProvider], parameters: { showOrganizations: true }, diff --git a/site/src/modules/management/OrganizationSettingsLayout.tsx b/site/src/modules/management/OrganizationSettingsLayout.tsx index aa586e877d..d2d25cc4a4 100644 --- a/site/src/modules/management/OrganizationSettingsLayout.tsx +++ b/site/src/modules/management/OrganizationSettingsLayout.tsx @@ -15,7 +15,6 @@ import { RequirePermission } from "contexts/auth/RequirePermission"; import { useDashboard } from "modules/dashboard/useDashboard"; import { type FC, Suspense, createContext, useContext } from "react"; import { Outlet, useParams } from "react-router-dom"; -import { OrganizationSidebar } from "./OrganizationSidebar"; export const OrganizationSettingsContext = createContext< OrganizationSettingsValue | undefined @@ -82,13 +81,10 @@ const OrganizationSettingsLayout: FC = () => { - + Organizations - + {organization && ( <> @@ -109,15 +105,10 @@ const OrganizationSettingsLayout: FC = () => {
-
-
- -
- }> - - -
-
+
+ }> + +
diff --git a/site/src/modules/management/OrganizationSidebar.tsx b/site/src/modules/management/OrganizationSidebar.tsx index 902085052e..8ef14f9baf 100644 --- a/site/src/modules/management/OrganizationSidebar.tsx +++ b/site/src/modules/management/OrganizationSidebar.tsx @@ -47,9 +47,11 @@ export const OrganizationSidebar: FC = () => { return canEditOrganization(org.permissions); }); + const organization = editableOrgs?.find((o) => o.name === organizationName); + return ( diff --git a/site/src/modules/management/OrganizationSidebarLayout.tsx b/site/src/modules/management/OrganizationSidebarLayout.tsx new file mode 100644 index 0000000000..279ed61186 --- /dev/null +++ b/site/src/modules/management/OrganizationSidebarLayout.tsx @@ -0,0 +1,19 @@ +import { Loader } from "components/Loader/Loader"; +import { type FC, Suspense } from "react"; +import { Outlet } from "react-router-dom"; +import { OrganizationSidebar } from "./OrganizationSidebar"; + +const OrganizationSidebarLayout: FC = () => { + return ( +
+ +
+ }> + + +
+
+ ); +}; + +export default OrganizationSidebarLayout; diff --git a/site/src/modules/management/OrganizationSidebarView.stories.tsx b/site/src/modules/management/OrganizationSidebarView.stories.tsx index fd9313ebe6..4f1b17a27c 100644 --- a/site/src/modules/management/OrganizationSidebarView.stories.tsx +++ b/site/src/modules/management/OrganizationSidebarView.stories.tsx @@ -1,4 +1,5 @@ import type { Meta, StoryObj } from "@storybook/react"; +import { expect, userEvent, waitFor, within } from "@storybook/test"; import { MockNoPermissions, MockOrganization, @@ -14,7 +15,7 @@ const meta: Meta = { decorators: [withDashboardProvider], parameters: { showOrganizations: true }, args: { - activeOrganizationName: undefined, + activeOrganization: undefined, organizations: [ { ...MockOrganization, @@ -50,29 +51,163 @@ export const LoadingOrganizations: Story = { export const NoCreateOrg: Story = { args: { + activeOrganization: { + ...MockOrganization, + permissions: { createOrganization: false }, + }, permissions: { ...MockPermissions, createOrganization: false, }, }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + await userEvent.click( + canvas.getByRole("button", { name: /My Organization/i }), + ); + await waitFor(() => + expect(canvas.queryByText("Create Organization")).not.toBeInTheDocument(), + ); + }, +}; + +export const OverflowDropdown: Story = { + args: { + activeOrganization: { + ...MockOrganization, + permissions: { createOrganization: true }, + }, + permissions: { + ...MockPermissions, + createOrganization: true, + }, + organizations: [ + { + ...MockOrganization, + permissions: {}, + }, + { + ...MockOrganization2, + permissions: {}, + }, + { + id: "my-organization-3-id", + name: "my-organization-3", + display_name: "My Organization 3", + description: "Another organization that gets used for stuff.", + icon: "/emojis/1f957.png", + created_at: "", + updated_at: "", + is_default: false, + permissions: {}, + }, + { + id: "my-organization-4-id", + name: "my-organization-4", + display_name: "My Organization 4", + description: "Another organization that gets used for stuff.", + icon: "/emojis/1f957.png", + created_at: "", + updated_at: "", + is_default: false, + permissions: {}, + }, + { + id: "my-organization-5-id", + name: "my-organization-5", + display_name: "My Organization 5", + description: "Another organization that gets used for stuff.", + icon: "/emojis/1f957.png", + created_at: "", + updated_at: "", + is_default: false, + permissions: {}, + }, + { + id: "my-organization-6-id", + name: "my-organization-6", + display_name: "My Organization 6", + description: "Another organization that gets used for stuff.", + icon: "/emojis/1f957.png", + created_at: "", + updated_at: "", + is_default: false, + permissions: {}, + }, + { + id: "my-organization-7-id", + name: "my-organization-7", + display_name: "My Organization 7", + description: "Another organization that gets used for stuff.", + icon: "/emojis/1f957.png", + created_at: "", + updated_at: "", + is_default: false, + permissions: {}, + }, + ], + }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + await userEvent.click( + canvas.getByRole("button", { name: /My Organization/i }), + ); + }, }; export const NoPermissions: Story = { args: { + activeOrganization: { + ...MockOrganization, + permissions: MockNoPermissions, + }, permissions: MockNoPermissions, }, }; -export const SelectedOrgNoMatch: Story = { +export const AllPermissions: Story = { args: { - activeOrganizationName: MockOrganization.name, - organizations: [], + activeOrganization: { + ...MockOrganization, + permissions: { + editOrganization: true, + editMembers: true, + editGroups: true, + auditOrganization: true, + assignOrgRole: true, + viewProvisioners: true, + viewIdpSyncSettings: true, + }, + }, + organizations: [ + { + ...MockOrganization, + permissions: { + editOrganization: true, + editMembers: true, + editGroups: true, + auditOrganization: true, + assignOrgRole: true, + viewProvisioners: true, + viewIdpSyncSettings: true, + }, + }, + ], }, }; export const SelectedOrgAdmin: Story = { args: { - activeOrganizationName: MockOrganization.name, + activeOrganization: { + ...MockOrganization, + permissions: { + editOrganization: true, + editMembers: true, + editGroups: true, + auditOrganization: true, + assignOrgRole: true, + }, + }, organizations: [ { ...MockOrganization, @@ -90,7 +225,15 @@ export const SelectedOrgAdmin: Story = { export const SelectedOrgAuditor: Story = { args: { - activeOrganizationName: MockOrganization.name, + activeOrganization: { + ...MockOrganization, + permissions: { + editOrganization: false, + editMembers: false, + editGroups: false, + auditOrganization: true, + }, + }, permissions: { ...MockPermissions, createOrganization: false, @@ -111,7 +254,15 @@ export const SelectedOrgAuditor: Story = { export const SelectedOrgUserAdmin: Story = { args: { - activeOrganizationName: MockOrganization.name, + activeOrganization: { + ...MockOrganization, + permissions: { + editOrganization: false, + editMembers: true, + editGroups: true, + auditOrganization: false, + }, + }, permissions: { ...MockPermissions, createOrganization: false, @@ -130,57 +281,6 @@ export const SelectedOrgUserAdmin: Story = { }, }; -export const MultiOrgAdminAndUserAdmin: Story = { - args: { - organizations: [ - { - ...MockOrganization, - permissions: { - editOrganization: false, - editMembers: false, - editGroups: false, - auditOrganization: true, - }, - }, - { - ...MockOrganization2, - permissions: { - editOrganization: false, - editMembers: true, - editGroups: true, - auditOrganization: false, - }, - }, - ], - }, -}; - -export const SelectedMultiOrgAdminAndUserAdmin: Story = { - args: { - activeOrganizationName: MockOrganization2.name, - organizations: [ - { - ...MockOrganization, - permissions: { - editOrganization: false, - editMembers: false, - editGroups: false, - auditOrganization: true, - }, - }, - { - ...MockOrganization2, - permissions: { - editOrganization: false, - editMembers: true, - editGroups: true, - auditOrganization: false, - }, - }, - ], - }, -}; - export const OrgsDisabled: Story = { parameters: { showOrganizations: false, diff --git a/site/src/modules/management/OrganizationSidebarView.tsx b/site/src/modules/management/OrganizationSidebarView.tsx index 104bf28c87..d52b740d4a 100644 --- a/site/src/modules/management/OrganizationSidebarView.tsx +++ b/site/src/modules/management/OrganizationSidebarView.tsx @@ -1,18 +1,27 @@ -import { cx } from "@emotion/css"; -import AddIcon from "@mui/icons-material/Add"; import type { AuthorizationResponse, Organization } from "api/typesGenerated"; import { Avatar } from "components/Avatar/Avatar"; +import { Button } from "components/Button/Button"; +import { + Command, + CommandGroup, + CommandItem, + CommandList, +} from "components/Command/Command"; import { Loader } from "components/Loader/Loader"; +import { + Popover, + PopoverContent, + PopoverTrigger, +} from "components/Popover/Popover"; import { Sidebar as BaseSidebar, - SettingsSidebarNavItem as SidebarNavSubItem, + SettingsSidebarNavItem, } from "components/Sidebar/Sidebar"; -import { Stack } from "components/Stack/Stack"; import type { Permissions } from "contexts/auth/permissions"; -import { type ClassName, useClassName } from "hooks/useClassName"; +import { ChevronDown, Plus } from "lucide-react"; import { useDashboard } from "modules/dashboard/useDashboard"; -import type { FC, ReactNode } from "react"; -import { Link, NavLink } from "react-router-dom"; +import { type FC, useState } from "react"; +import { useNavigate } from "react-router-dom"; export interface OrganizationWithPermissions extends Organization { permissions: AuthorizationResponse; @@ -20,7 +29,7 @@ export interface OrganizationWithPermissions extends Organization { interface SidebarProps { /** The active org name, if any. Overrides activeSettings. */ - activeOrganizationName: string | undefined; + activeOrganization: OrganizationWithPermissions | undefined; /** Organizations and their permissions or undefined if still fetching. */ organizations: OrganizationWithPermissions[] | undefined; /** Site-wide permissions. */ @@ -31,7 +40,7 @@ interface SidebarProps { * Organization settings left sidebar menu. */ export const OrganizationSidebarView: FC = ({ - activeOrganizationName, + activeOrganization, organizations, permissions, }) => { @@ -41,7 +50,7 @@ export const OrganizationSidebarView: FC = ({ {showOrganizations && ( @@ -56,7 +65,7 @@ function urlForSubpage(organizationName: string, subpage = ""): string { interface OrganizationsSettingsNavigationProps { /** The active org name if an org is being viewed. */ - activeOrganizationName: string | undefined; + activeOrganization: OrganizationWithPermissions | undefined; /** Organizations and their permissions or undefined if still fetching. */ organizations: OrganizationWithPermissions[] | undefined; /** Site-wide permissions. */ @@ -64,187 +73,158 @@ interface OrganizationsSettingsNavigationProps { } /** - * Displays navigation for all organizations and a create organization link. + * Displays navigation items for the active organization and a combobox to + * switch between organizations. * * If organizations or their permissions are still loading, show a loader. - * - * If there are no organizations and the user does not have the create org - * permission, nothing is displayed. */ const OrganizationsSettingsNavigation: FC< OrganizationsSettingsNavigationProps -> = ({ activeOrganizationName, organizations, permissions }) => { - // Wait for organizations and their permissions to load in. - if (!organizations) { +> = ({ activeOrganization, organizations, permissions }) => { + // Wait for organizations and their permissions to load + if (!organizations || !activeOrganization) { return ; } - if (organizations.length <= 0 && !permissions.createOrganization) { - return null; - } + const [isPopoverOpen, setIsPopoverOpen] = useState(false); + const navigate = useNavigate(); return ( <> - {permissions.createOrganization && ( - } - > - New organization - - )} - {organizations.map((org) => ( - - ))} + + + + + + + + + {organizations.length > 1 && ( +
+ {organizations.map((organization) => ( + { + setIsPopoverOpen(false); + navigate(urlForSubpage(organization.name)); + }} + // There is currently an issue with the cmdk component for keyboard navigation + // https://github.com/pacocoursey/cmdk/issues/322 + tabIndex={0} + > + + + {organization?.display_name || organization?.name} + + + ))} +
+ )} + {permissions.createOrganization && ( + <> + {organizations.length > 1 && ( +
+ )} + { + setIsPopoverOpen(false); + setTimeout(() => { + navigate("/organizations/new"); + }, 200); + }} + > + Create Organization + + + )} +
+
+
+
+
+ ); }; interface OrganizationSettingsNavigationProps { - /** Whether this organization is currently selected. */ - active: boolean; - /** The organization to display in the navigation. */ organization: OrganizationWithPermissions; } -/** - * Displays navigation for a single organization. - * - * If inactive, no sub-menu items will be shown, just the organization name. - * - * If active, it will show sub-menu items based on the permissions. - */ const OrganizationSettingsNavigation: FC< OrganizationSettingsNavigationProps -> = ({ active, organization }) => { +> = ({ organization }) => { return ( <> - - } - > - {organization.display_name} - - {active && ( -
- {organization.permissions.editOrganization && ( - - Settings - - )} - {organization.permissions.editMembers && ( - - Members - - )} - {organization.permissions.editGroups && ( - - Groups - - )} - {organization.permissions.assignOrgRole && ( - - Roles - - )} - {organization.permissions.viewProvisioners && ( - - Provisioners - - )} - {organization.permissions.viewIdpSyncSettings && ( - - IdP Sync - - )} -
- )} +
+ {organization.permissions.editMembers && ( + + Members + + )} + {organization.permissions.editGroups && ( + + Groups + + )} + {organization.permissions.assignOrgRole && ( + + Roles + + )} + {organization.permissions.viewProvisioners && ( + + Provisioners + + )} + {organization.permissions.viewIdpSyncSettings && ( + + IdP Sync + + )} + {organization.permissions.editOrganization && ( + + Settings + + )} +
); }; - -interface SidebarNavItemProps { - active?: boolean | "auto"; - children?: ReactNode; - icon?: ReactNode; - href: string; -} - -const SidebarNavItem: FC = ({ - active, - children, - href, - icon, -}) => { - const link = useClassName(classNames.link, []); - const activeLink = useClassName(classNames.activeLink, []); - - const content = ( - - {icon} - {children} - - ); - - if (active === "auto") { - return ( - cx([link, isActive && activeLink])} - > - {content} - - ); - } - - return ( - - {content} - - ); -}; - -const classNames = { - link: (css, theme) => css` - color: inherit; - display: block; - font-size: 14px; - text-decoration: none; - padding: 10px 12px 10px 16px; - border-radius: 4px; - transition: background-color 0.15s ease-in-out; - position: relative; - - &:hover { - background-color: ${theme.palette.action.hover}; - } - - border-left: 3px solid transparent; - `, - - activeLink: (css, theme) => css` - border-left-color: ${theme.palette.primary.main}; - border-top-left-radius: 0; - border-bottom-left-radius: 0; - `, -} satisfies Record; diff --git a/site/src/pages/ManagementSettingsPage/CreateOrganizationPage.tsx b/site/src/pages/ManagementSettingsPage/CreateOrganizationPage.tsx index 8f9c967040..f49f9db79e 100644 --- a/site/src/pages/ManagementSettingsPage/CreateOrganizationPage.tsx +++ b/site/src/pages/ManagementSettingsPage/CreateOrganizationPage.tsx @@ -18,15 +18,17 @@ const CreateOrganizationPage: FC = () => { const error = createOrganizationMutation.error; return ( - { - await createOrganizationMutation.mutateAsync(values); - displaySuccess("Organization created."); - navigate(`/organizations/${values.name}`); - }} - /> +
+ { + await createOrganizationMutation.mutateAsync(values); + displaySuccess("Organization created."); + navigate(`/organizations/${values.name}`); + }} + /> +
); }; diff --git a/site/src/pages/ManagementSettingsPage/CreateOrganizationPageView.tsx b/site/src/pages/ManagementSettingsPage/CreateOrganizationPageView.tsx index 172c537643..705c0be2eb 100644 --- a/site/src/pages/ManagementSettingsPage/CreateOrganizationPageView.tsx +++ b/site/src/pages/ManagementSettingsPage/CreateOrganizationPageView.tsx @@ -5,25 +5,20 @@ import { ErrorAlert } from "components/Alert/ErrorAlert"; import { Badges, PremiumBadge } from "components/Badges/Badges"; import { Button } from "components/Button/Button"; import { ChooseOne, Cond } from "components/Conditionals/ChooseOne"; -import { - FormFields, - FormFooter, - FormSection, - HorizontalForm, -} from "components/Form/Form"; import { IconField } from "components/IconField/IconField"; import { Paywall } from "components/Paywall/Paywall"; import { PopoverPaywall } from "components/Paywall/PopoverPaywall"; -import { SettingsHeader } from "components/SettingsHeader/SettingsHeader"; import { Spinner } from "components/Spinner/Spinner"; -import { Stack } from "components/Stack/Stack"; import { Popover, PopoverContent, PopoverTrigger, } from "components/deprecated/Popover/Popover"; import { useFormik } from "formik"; +import { ArrowLeft } from "lucide-react"; import type { FC } from "react"; +import { useNavigate } from "react-router-dom"; +import { Link } from "react-router-dom"; import { docs } from "utils/docs"; import { displayNameValidator, @@ -64,74 +59,80 @@ export const CreateOrganizationPageView: FC< validationSchema, onSubmit, }); + const navigate = useNavigate(); const getFieldHelpers = getFormHelpers(form, error); return ( - -
- +
+
+ + + Go Back + +
+
+
+ {Boolean(error) && !isApiValidationError(error) && ( +
+ +
+ )} - {Boolean(error) && !isApiValidationError(error) && ( -
- -
- )} + + + {isEntitled && ( + + + + + + )} - - - {isEntitled && ( - - - - - - )} + + + + + - - +

New Organization

+

+ Organize your deployment into multiple platform teams with unique + provisioners, templates, groups, and members. +

+ +
+ + +
+ - - - -
- - - - - - - - -
+ + +
+
- +
@@ -143,29 +144,33 @@ export const CreateOrganizationPageView: FC< form.setFieldValue("icon", value)} /> - -
- - - - - - - - - +
+
+ + +
+ +
+ + +
+
); }; diff --git a/site/src/pages/ManagementSettingsPage/OrganizationSettingsPage.tsx b/site/src/pages/ManagementSettingsPage/OrganizationSettingsPage.tsx index 9b80db4503..698f2ee758 100644 --- a/site/src/pages/ManagementSettingsPage/OrganizationSettingsPage.tsx +++ b/site/src/pages/ManagementSettingsPage/OrganizationSettingsPage.tsx @@ -89,7 +89,7 @@ const OrganizationSettingsPage: FC = () => { organizationId: organization.id, req: values, }); - navigate(`/organizations/${updatedOrganization.name}`); + navigate(`/organizations/${updatedOrganization.name}/settings`); displaySuccess("Organization settings updated."); }} onDeleteOrganization={() => { diff --git a/site/src/router.tsx b/site/src/router.tsx index 6e6fe630f7..5ee3537575 100644 --- a/site/src/router.tsx +++ b/site/src/router.tsx @@ -37,6 +37,9 @@ const DeploymentSettingsProvider = lazy( const OrganizationSettingsLayout = lazy( () => import("./modules/management/OrganizationSettingsLayout"), ); +const OrganizationSidebarLayout = lazy( + () => import("./modules/management/OrganizationSidebarLayout"), +); const CliAuthenticationPage = lazy( () => import("./pages/CliAuthPage/CliAuthPage"), ); @@ -427,9 +430,8 @@ export const router = createBrowserRouter( {/* General settings for the default org can omit the organization name */} } /> - - } /> - } /> + }> + } /> {groupsRouter()} } /> @@ -441,6 +443,7 @@ export const router = createBrowserRouter( element={} /> } /> + } />