diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c97cbb1a8dedd..eb8fcb06d8eb5 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 e774de2a7491f..2d0a41acafc02 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 de20ebd9778e7..9edb2eb922ab8 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 268640f28ff29..5a1cf4ba82c0c 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 d101e8159fa27..bbdc5684cb19d 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 65c2e70ea3333..676a24c936246 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 443a5cfd417ce..5bda860b4b93a 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 aa586e877d6e0..d2d25cc4a41bd 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 902085052e289..8ef14f9baf165 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 0000000000000..279ed61186bdc --- /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 fd9313ebe63f9..4f1b17a27c181 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,134 @@ 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 NoPermissions: Story = { +export const OverflowDropdown: Story = { args: { - permissions: MockNoPermissions, + 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 SelectedOrgNoMatch: Story = { +export const NoPermissions: Story = { args: { - activeOrganizationName: MockOrganization.name, - organizations: [], + activeOrganization: { + ...MockOrganization, + permissions: MockNoPermissions, + }, + permissions: MockNoPermissions, }, }; -export const SelectedOrgAdmin: Story = { +export const AllPermissions: Story = { args: { - activeOrganizationName: MockOrganization.name, + activeOrganization: { + ...MockOrganization, + permissions: { + editOrganization: true, + editMembers: true, + editGroups: true, + auditOrganization: true, + assignOrgRole: true, + viewProvisioners: true, + viewIdpSyncSettings: true, + }, + }, organizations: [ { ...MockOrganization, @@ -82,56 +188,56 @@ export const SelectedOrgAdmin: Story = { editGroups: true, auditOrganization: true, assignOrgRole: true, + viewProvisioners: true, + viewIdpSyncSettings: true, }, }, ], }, }; -export const SelectedOrgAuditor: Story = { +export const SelectedOrgAdmin: Story = { args: { - activeOrganizationName: MockOrganization.name, - permissions: { - ...MockPermissions, - createOrganization: false, + activeOrganization: { + ...MockOrganization, + permissions: { + editOrganization: true, + editMembers: true, + editGroups: true, + auditOrganization: true, + assignOrgRole: true, + }, }, organizations: [ { ...MockOrganization, permissions: { - editOrganization: false, - editMembers: false, - editGroups: false, + editOrganization: true, + editMembers: true, + editGroups: true, auditOrganization: true, + assignOrgRole: true, }, }, ], }, }; -export const SelectedOrgUserAdmin: Story = { +export const SelectedOrgAuditor: Story = { args: { - activeOrganizationName: MockOrganization.name, + activeOrganization: { + ...MockOrganization, + permissions: { + editOrganization: false, + editMembers: false, + editGroups: false, + auditOrganization: true, + }, + }, permissions: { ...MockPermissions, createOrganization: false, }, - organizations: [ - { - ...MockOrganization, - permissions: { - editOrganization: false, - editMembers: true, - editGroups: true, - auditOrganization: false, - }, - }, - ], - }, -}; - -export const MultiOrgAdminAndUserAdmin: Story = { - args: { organizations: [ { ...MockOrganization, @@ -142,34 +248,28 @@ export const MultiOrgAdminAndUserAdmin: Story = { auditOrganization: true, }, }, - { - ...MockOrganization2, - permissions: { - editOrganization: false, - editMembers: true, - editGroups: true, - auditOrganization: false, - }, - }, ], }, }; -export const SelectedMultiOrgAdminAndUserAdmin: Story = { +export const SelectedOrgUserAdmin: Story = { args: { - activeOrganizationName: MockOrganization2.name, + activeOrganization: { + ...MockOrganization, + permissions: { + editOrganization: false, + editMembers: true, + editGroups: true, + auditOrganization: false, + }, + }, + permissions: { + ...MockPermissions, + createOrganization: false, + }, organizations: [ { ...MockOrganization, - permissions: { - editOrganization: false, - editMembers: false, - editGroups: false, - auditOrganization: true, - }, - }, - { - ...MockOrganization2, permissions: { editOrganization: false, editMembers: true, diff --git a/site/src/modules/management/OrganizationSidebarView.tsx b/site/src/modules/management/OrganizationSidebarView.tsx index 104bf28c87b7c..d52b740d4a542 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 8f9c967040d5c..f49f9db79edf4 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 172c537643f69..705c0be2eb5a9 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 9b80db4503f44..698f2ee75822f 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 6e6fe630f7188..5ee3537575cb7 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={} /> } /> + } />