mirror of
https://github.com/coder/coder.git
synced 2026-08-31 01:03:45 +08:00
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/ <img width="560" alt="Screenshot 2025-01-03 at 21 11 26" src="https://github.com/user-attachments/assets/ff6e61ab-c8d4-4f97-b603-306492e9bfec" /> <img width="641" alt="Screenshot 2025-01-03 at 21 11 39" src="https://github.com/user-attachments/assets/fedb28e0-9ef3-4b0f-8665-06215338f351" /> <img width="1178" alt="Screenshot 2025-01-03 at 21 12 05" src="https://github.com/user-attachments/assets/ee672533-2689-4b2e-a7bf-471ea72e1095" /> <img width="1177" alt="Screenshot 2025-01-03 at 21 12 39" src="https://github.com/user-attachments/assets/f13824a6-2581-4bff-b5b6-2024c2e145a4" />
This commit is contained in:
Generated
+8
-8
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -69,7 +69,7 @@ export const CommandList = forwardRef<
|
||||
>(({ className, ...props }, ref) => (
|
||||
<CommandPrimitive.List
|
||||
ref={ref}
|
||||
className={cn("max-h-[300px] overflow-y-auto overflow-x-hidden", className)}
|
||||
className={cn("max-h-96 overflow-y-auto overflow-x-hidden", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
@@ -92,7 +92,7 @@ export const CommandGroup = forwardRef<
|
||||
<CommandPrimitive.Group
|
||||
ref={ref}
|
||||
className={cn(
|
||||
`overflow-hidden p-1 text-content-primary
|
||||
`overflow-hidden p-2 text-content-primary
|
||||
[&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs
|
||||
[&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-content-secondary`,
|
||||
className,
|
||||
@@ -119,7 +119,7 @@ export const CommandItem = forwardRef<
|
||||
<CommandPrimitive.Item
|
||||
ref={ref}
|
||||
className={cn(
|
||||
`relative flex cursor-default gap-2 select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none
|
||||
`relative flex cursor-default gap-2 select-none text-content-secondary items-center rounded-sm px-2 py-2 text-sm font-medium outline-none
|
||||
data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-50
|
||||
data-[selected=true]:bg-surface-secondary data-[selected=true]:text-content-primary
|
||||
[&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0`,
|
||||
|
||||
@@ -24,7 +24,7 @@ export const PopoverContent = forwardRef<
|
||||
align={align}
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
`z-50 w-72 rounded-md border border-solid bg-surface-primary p-4
|
||||
`z-50 w-72 rounded-md border border-solid bg-surface-primary
|
||||
text-content-primary shadow-md outline-none
|
||||
data-[state=open]:animate-in data-[state=closed]:animate-out
|
||||
data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0
|
||||
|
||||
@@ -67,7 +67,7 @@ export const SettingsSidebarNavItem: FC<SettingsSidebarNavItemProps> = ({
|
||||
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,
|
||||
},
|
||||
|
||||
@@ -39,8 +39,8 @@ const DeploymentSettingsLayout: FC = () => {
|
||||
</BreadcrumbList>
|
||||
</Breadcrumb>
|
||||
<hr className="h-px border-none bg-border" />
|
||||
<div className="px-6 max-w-screen-2xl">
|
||||
<div className="flex flex-row gap-12 py-10">
|
||||
<div className="px-10 max-w-screen-2xl">
|
||||
<div className="flex flex-row gap-28 py-10">
|
||||
<DeploymentSidebar />
|
||||
<main css={{ flexGrow: 1 }}>
|
||||
<Suspense fallback={<Loader />}>
|
||||
|
||||
@@ -4,7 +4,7 @@ import { withDashboardProvider } from "testHelpers/storybook";
|
||||
import { DeploymentSidebarView } from "./DeploymentSidebarView";
|
||||
|
||||
const meta: Meta<typeof DeploymentSidebarView> = {
|
||||
title: "modules/management/SidebarView",
|
||||
title: "modules/management/DeploymentSidebarView",
|
||||
component: DeploymentSidebarView,
|
||||
decorators: [withDashboardProvider],
|
||||
parameters: { showOrganizations: true },
|
||||
|
||||
@@ -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 = () => {
|
||||
</BreadcrumbItem>
|
||||
<BreadcrumbSeparator />
|
||||
<BreadcrumbItem>
|
||||
<BreadcrumbLink
|
||||
href="/organizations"
|
||||
className="flex items-center gap-2"
|
||||
>
|
||||
<BreadcrumbPage className="flex items-center gap-2">
|
||||
Organizations
|
||||
<FeatureStageBadge contentType="beta" size="sm" />
|
||||
</BreadcrumbLink>
|
||||
</BreadcrumbPage>
|
||||
</BreadcrumbItem>
|
||||
{organization && (
|
||||
<>
|
||||
@@ -109,15 +105,10 @@ const OrganizationSettingsLayout: FC = () => {
|
||||
</BreadcrumbList>
|
||||
</Breadcrumb>
|
||||
<hr className="h-px border-none bg-border" />
|
||||
<div className="px-6 max-w-screen-2xl">
|
||||
<div className="flex flex-row gap-12 py-10">
|
||||
<OrganizationSidebar />
|
||||
<main css={{ flexGrow: 1 }}>
|
||||
<Suspense fallback={<Loader />}>
|
||||
<Outlet />
|
||||
</Suspense>
|
||||
</main>
|
||||
</div>
|
||||
<div className="px-10 max-w-screen-2xl">
|
||||
<Suspense fallback={<Loader />}>
|
||||
<Outlet />
|
||||
</Suspense>
|
||||
</div>
|
||||
</div>
|
||||
</OrganizationSettingsContext.Provider>
|
||||
|
||||
@@ -47,9 +47,11 @@ export const OrganizationSidebar: FC = () => {
|
||||
return canEditOrganization(org.permissions);
|
||||
});
|
||||
|
||||
const organization = editableOrgs?.find((o) => o.name === organizationName);
|
||||
|
||||
return (
|
||||
<OrganizationSidebarView
|
||||
activeOrganizationName={organizationName}
|
||||
activeOrganization={organization}
|
||||
organizations={editableOrgs}
|
||||
permissions={permissions}
|
||||
/>
|
||||
|
||||
@@ -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 (
|
||||
<div className="flex flex-row gap-28 py-10">
|
||||
<OrganizationSidebar />
|
||||
<main css={{ flexGrow: 1 }}>
|
||||
<Suspense fallback={<Loader />}>
|
||||
<Outlet />
|
||||
</Suspense>
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default OrganizationSidebarLayout;
|
||||
@@ -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<typeof OrganizationSidebarView> = {
|
||||
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,
|
||||
|
||||
@@ -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<SidebarProps> = ({
|
||||
activeOrganizationName,
|
||||
activeOrganization,
|
||||
organizations,
|
||||
permissions,
|
||||
}) => {
|
||||
@@ -41,7 +50,7 @@ export const OrganizationSidebarView: FC<SidebarProps> = ({
|
||||
<BaseSidebar>
|
||||
{showOrganizations && (
|
||||
<OrganizationsSettingsNavigation
|
||||
activeOrganizationName={activeOrganizationName}
|
||||
activeOrganization={activeOrganization}
|
||||
organizations={organizations}
|
||||
permissions={permissions}
|
||||
/>
|
||||
@@ -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 <Loader />;
|
||||
}
|
||||
|
||||
if (organizations.length <= 0 && !permissions.createOrganization) {
|
||||
return null;
|
||||
}
|
||||
const [isPopoverOpen, setIsPopoverOpen] = useState(false);
|
||||
const navigate = useNavigate();
|
||||
|
||||
return (
|
||||
<>
|
||||
{permissions.createOrganization && (
|
||||
<SidebarNavItem
|
||||
active="auto"
|
||||
href="/organizations/new"
|
||||
icon={<AddIcon />}
|
||||
>
|
||||
New organization
|
||||
</SidebarNavItem>
|
||||
)}
|
||||
{organizations.map((org) => (
|
||||
<OrganizationSettingsNavigation
|
||||
key={org.id}
|
||||
organization={org}
|
||||
active={org.name === activeOrganizationName}
|
||||
/>
|
||||
))}
|
||||
<Popover open={isPopoverOpen} onOpenChange={setIsPopoverOpen}>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
aria-expanded={isPopoverOpen}
|
||||
className="w-60 justify-between p-2 h-11"
|
||||
>
|
||||
<div className="flex flex-row gap-2 items-center p-2 truncate">
|
||||
{activeOrganization && (
|
||||
<Avatar
|
||||
size="sm"
|
||||
src={activeOrganization.icon}
|
||||
fallback={activeOrganization.display_name}
|
||||
/>
|
||||
)}
|
||||
<span className="truncate">
|
||||
{activeOrganization?.display_name || activeOrganization?.name}
|
||||
</span>
|
||||
</div>
|
||||
<ChevronDown />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent align="start" className="w-60">
|
||||
<Command loop>
|
||||
<CommandList>
|
||||
<CommandGroup className="pb-2">
|
||||
{organizations.length > 1 && (
|
||||
<div className="flex flex-col max-h-[260px] overflow-y-auto">
|
||||
{organizations.map((organization) => (
|
||||
<CommandItem
|
||||
key={organization.id}
|
||||
value={organization.name}
|
||||
onSelect={() => {
|
||||
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}
|
||||
>
|
||||
<Avatar
|
||||
size="sm"
|
||||
src={organization.icon}
|
||||
fallback={organization.display_name}
|
||||
/>
|
||||
<span className="truncate">
|
||||
{organization?.display_name || organization?.name}
|
||||
</span>
|
||||
</CommandItem>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
{permissions.createOrganization && (
|
||||
<>
|
||||
{organizations.length > 1 && (
|
||||
<hr className="h-px my-2 border-none bg-border -mx-2" />
|
||||
)}
|
||||
<CommandItem
|
||||
className="flex justify-center data-[selected=true]:bg-transparent"
|
||||
onSelect={() => {
|
||||
setIsPopoverOpen(false);
|
||||
setTimeout(() => {
|
||||
navigate("/organizations/new");
|
||||
}, 200);
|
||||
}}
|
||||
>
|
||||
<Plus /> Create Organization
|
||||
</CommandItem>
|
||||
</>
|
||||
)}
|
||||
</CommandGroup>
|
||||
</CommandList>
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
<OrganizationSettingsNavigation
|
||||
key={activeOrganization.id}
|
||||
organization={activeOrganization}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
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 (
|
||||
<>
|
||||
<SidebarNavItem
|
||||
active={active}
|
||||
href={urlForSubpage(organization.name)}
|
||||
icon={
|
||||
<Avatar
|
||||
variant="icon"
|
||||
src={organization.icon}
|
||||
fallback={organization.display_name}
|
||||
/>
|
||||
}
|
||||
>
|
||||
{organization.display_name}
|
||||
</SidebarNavItem>
|
||||
{active && (
|
||||
<div className="flex flex-col gap-1 my-2 ml-11">
|
||||
{organization.permissions.editOrganization && (
|
||||
<SidebarNavSubItem end href={urlForSubpage(organization.name)}>
|
||||
Settings
|
||||
</SidebarNavSubItem>
|
||||
)}
|
||||
{organization.permissions.editMembers && (
|
||||
<SidebarNavSubItem
|
||||
href={urlForSubpage(organization.name, "members")}
|
||||
>
|
||||
Members
|
||||
</SidebarNavSubItem>
|
||||
)}
|
||||
{organization.permissions.editGroups && (
|
||||
<SidebarNavSubItem
|
||||
href={urlForSubpage(organization.name, "groups")}
|
||||
>
|
||||
Groups
|
||||
</SidebarNavSubItem>
|
||||
)}
|
||||
{organization.permissions.assignOrgRole && (
|
||||
<SidebarNavSubItem href={urlForSubpage(organization.name, "roles")}>
|
||||
Roles
|
||||
</SidebarNavSubItem>
|
||||
)}
|
||||
{organization.permissions.viewProvisioners && (
|
||||
<SidebarNavSubItem
|
||||
href={urlForSubpage(organization.name, "provisioners")}
|
||||
>
|
||||
Provisioners
|
||||
</SidebarNavSubItem>
|
||||
)}
|
||||
{organization.permissions.viewIdpSyncSettings && (
|
||||
<SidebarNavSubItem
|
||||
href={urlForSubpage(organization.name, "idp-sync")}
|
||||
>
|
||||
IdP Sync
|
||||
</SidebarNavSubItem>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<div className="flex flex-col gap-1 my-2">
|
||||
{organization.permissions.editMembers && (
|
||||
<SettingsSidebarNavItem end href={urlForSubpage(organization.name)}>
|
||||
Members
|
||||
</SettingsSidebarNavItem>
|
||||
)}
|
||||
{organization.permissions.editGroups && (
|
||||
<SettingsSidebarNavItem
|
||||
href={urlForSubpage(organization.name, "groups")}
|
||||
>
|
||||
Groups
|
||||
</SettingsSidebarNavItem>
|
||||
)}
|
||||
{organization.permissions.assignOrgRole && (
|
||||
<SettingsSidebarNavItem
|
||||
href={urlForSubpage(organization.name, "roles")}
|
||||
>
|
||||
Roles
|
||||
</SettingsSidebarNavItem>
|
||||
)}
|
||||
{organization.permissions.viewProvisioners && (
|
||||
<SettingsSidebarNavItem
|
||||
href={urlForSubpage(organization.name, "provisioners")}
|
||||
>
|
||||
Provisioners
|
||||
</SettingsSidebarNavItem>
|
||||
)}
|
||||
{organization.permissions.viewIdpSyncSettings && (
|
||||
<SettingsSidebarNavItem
|
||||
href={urlForSubpage(organization.name, "idp-sync")}
|
||||
>
|
||||
IdP Sync
|
||||
</SettingsSidebarNavItem>
|
||||
)}
|
||||
{organization.permissions.editOrganization && (
|
||||
<SettingsSidebarNavItem
|
||||
href={urlForSubpage(organization.name, "settings")}
|
||||
>
|
||||
Settings
|
||||
</SettingsSidebarNavItem>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
interface SidebarNavItemProps {
|
||||
active?: boolean | "auto";
|
||||
children?: ReactNode;
|
||||
icon?: ReactNode;
|
||||
href: string;
|
||||
}
|
||||
|
||||
const SidebarNavItem: FC<SidebarNavItemProps> = ({
|
||||
active,
|
||||
children,
|
||||
href,
|
||||
icon,
|
||||
}) => {
|
||||
const link = useClassName(classNames.link, []);
|
||||
const activeLink = useClassName(classNames.activeLink, []);
|
||||
|
||||
const content = (
|
||||
<Stack alignItems="center" spacing={1.5} direction="row">
|
||||
{icon}
|
||||
{children}
|
||||
</Stack>
|
||||
);
|
||||
|
||||
if (active === "auto") {
|
||||
return (
|
||||
<NavLink
|
||||
to={href}
|
||||
className={({ isActive }) => cx([link, isActive && activeLink])}
|
||||
>
|
||||
{content}
|
||||
</NavLink>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Link to={href} className={cx([link, active && activeLink])}>
|
||||
{content}
|
||||
</Link>
|
||||
);
|
||||
};
|
||||
|
||||
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<string, ClassName>;
|
||||
|
||||
@@ -18,15 +18,17 @@ const CreateOrganizationPage: FC = () => {
|
||||
const error = createOrganizationMutation.error;
|
||||
|
||||
return (
|
||||
<CreateOrganizationPageView
|
||||
error={error}
|
||||
isEntitled={feats.multiple_organizations}
|
||||
onSubmit={async (values) => {
|
||||
await createOrganizationMutation.mutateAsync(values);
|
||||
displaySuccess("Organization created.");
|
||||
navigate(`/organizations/${values.name}`);
|
||||
}}
|
||||
/>
|
||||
<main className="py-20 sm:py-7">
|
||||
<CreateOrganizationPageView
|
||||
error={error}
|
||||
isEntitled={feats.multiple_organizations}
|
||||
onSubmit={async (values) => {
|
||||
await createOrganizationMutation.mutateAsync(values);
|
||||
displaySuccess("Organization created.");
|
||||
navigate(`/organizations/${values.name}`);
|
||||
}}
|
||||
/>
|
||||
</main>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -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 (
|
||||
<Stack>
|
||||
<div>
|
||||
<SettingsHeader
|
||||
title="New Organization"
|
||||
description="Organize your deployment into multiple platform teams with unique provisioners, templates, groups, and members."
|
||||
/>
|
||||
<div className="flex flex-row font-medium">
|
||||
<div className="absolute top-40 left-12">
|
||||
<Link
|
||||
to="/organizations"
|
||||
className="flex flex-row items-center gap-2 no-underline text-content-secondary hover:text-content-primary"
|
||||
>
|
||||
<ArrowLeft size={20} />
|
||||
Go Back
|
||||
</Link>
|
||||
</div>
|
||||
<div className="flex flex-col gap-4 w-full min-w-72 mx-auto">
|
||||
<div className="flex flex-col items-center">
|
||||
{Boolean(error) && !isApiValidationError(error) && (
|
||||
<div css={{ marginBottom: 32 }}>
|
||||
<ErrorAlert error={error} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{Boolean(error) && !isApiValidationError(error) && (
|
||||
<div css={{ marginBottom: 32 }}>
|
||||
<ErrorAlert error={error} />
|
||||
</div>
|
||||
)}
|
||||
<Badges>
|
||||
<Popover mode="hover">
|
||||
{isEntitled && (
|
||||
<PopoverTrigger>
|
||||
<span>
|
||||
<PremiumBadge />
|
||||
</span>
|
||||
</PopoverTrigger>
|
||||
)}
|
||||
|
||||
<Badges>
|
||||
<Popover mode="hover">
|
||||
{isEntitled && (
|
||||
<PopoverTrigger>
|
||||
<span>
|
||||
<PremiumBadge />
|
||||
</span>
|
||||
</PopoverTrigger>
|
||||
)}
|
||||
<PopoverContent css={{ transform: "translateY(-28px)" }}>
|
||||
<PopoverPaywall
|
||||
message="Organizations"
|
||||
description="Create multiple organizations within a single Coder deployment, allowing several platform teams to operate with isolated users, templates, and distinct underlying infrastructure."
|
||||
documentationLink={docs("/admin/users/organizations")}
|
||||
/>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</Badges>
|
||||
|
||||
<PopoverContent css={{ transform: "translateY(-28px)" }}>
|
||||
<PopoverPaywall
|
||||
<header className="flex flex-col items-center">
|
||||
<h1 className="text-3xl font-semibold m-0">New Organization</h1>
|
||||
<p className="max-w-md text-sm text-content-secondary text-center">
|
||||
Organize your deployment into multiple platform teams with unique
|
||||
provisioners, templates, groups, and members.
|
||||
</p>
|
||||
</header>
|
||||
</div>
|
||||
<ChooseOne>
|
||||
<Cond condition={!isEntitled}>
|
||||
<div className="min-w-fit mx-auto">
|
||||
<Paywall
|
||||
message="Organizations"
|
||||
description="Create multiple organizations within a single Coder deployment, allowing several platform teams to operate with isolated users, templates, and distinct underlying infrastructure."
|
||||
documentationLink={docs("/admin/users/organizations")}
|
||||
/>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</Badges>
|
||||
</div>
|
||||
|
||||
<ChooseOne>
|
||||
<Cond condition={!isEntitled}>
|
||||
<Paywall
|
||||
message="Organizations"
|
||||
description="Create multiple organizations within a single Coder deployment, allowing several platform teams to operate with isolated users, templates, and distinct underlying infrastructure."
|
||||
documentationLink={docs("/admin/users/organizations")}
|
||||
/>
|
||||
</Cond>
|
||||
<Cond>
|
||||
<HorizontalForm
|
||||
onSubmit={form.handleSubmit}
|
||||
aria-label="Organization settings form"
|
||||
>
|
||||
<FormSection
|
||||
title="General info"
|
||||
description="The name and description of the organization."
|
||||
>
|
||||
<fieldset
|
||||
disabled={form.isSubmitting}
|
||||
css={{
|
||||
border: "unset",
|
||||
padding: 0,
|
||||
margin: 0,
|
||||
width: "100%",
|
||||
}}
|
||||
</div>
|
||||
</Cond>
|
||||
<Cond>
|
||||
<div className="flex flex-col gap-4 w-full max-w-xl min-w-72 mx-auto">
|
||||
<form
|
||||
onSubmit={form.handleSubmit}
|
||||
aria-label="Organization settings form"
|
||||
className="flex flex-col gap-6 w-full"
|
||||
>
|
||||
<FormFields>
|
||||
<fieldset
|
||||
disabled={form.isSubmitting}
|
||||
className="flex flex-col gap-6 w-full border-none"
|
||||
>
|
||||
<TextField
|
||||
{...getFieldHelpers("name")}
|
||||
onChange={onChangeTrimmed(form)}
|
||||
autoFocus
|
||||
fullWidth
|
||||
label="Slug"
|
||||
/>
|
||||
@@ -143,29 +144,33 @@ export const CreateOrganizationPageView: FC<
|
||||
<TextField
|
||||
{...getFieldHelpers("description")}
|
||||
multiline
|
||||
fullWidth
|
||||
label="Description"
|
||||
rows={2}
|
||||
/>
|
||||
<IconField
|
||||
{...getFieldHelpers("icon")}
|
||||
onChange={onChangeTrimmed(form)}
|
||||
fullWidth
|
||||
onPickEmoji={(value) => form.setFieldValue("icon", value)}
|
||||
/>
|
||||
</FormFields>
|
||||
</fieldset>
|
||||
</FormSection>
|
||||
|
||||
<FormFooter>
|
||||
<Button type="submit" disabled={form.isSubmitting}>
|
||||
{form.isSubmitting && <Spinner />}
|
||||
Save
|
||||
</Button>
|
||||
</FormFooter>
|
||||
</HorizontalForm>
|
||||
</Cond>
|
||||
</ChooseOne>
|
||||
</Stack>
|
||||
</fieldset>
|
||||
<div className="flex flex-row gap-2">
|
||||
<Button type="submit" disabled={form.isSubmitting}>
|
||||
{form.isSubmitting && <Spinner />}
|
||||
Save
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
type="button"
|
||||
onClick={() => navigate("/organizations")}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</Cond>
|
||||
</ChooseOne>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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={() => {
|
||||
|
||||
+6
-3
@@ -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 */}
|
||||
<Route index element={<OrganizationSettingsPage />} />
|
||||
|
||||
<Route path=":organization">
|
||||
<Route index element={<OrganizationSettingsPage />} />
|
||||
<Route path="members" element={<OrganizationMembersPage />} />
|
||||
<Route path=":organization" element={<OrganizationSidebarLayout />}>
|
||||
<Route index element={<OrganizationMembersPage />} />
|
||||
{groupsRouter()}
|
||||
<Route path="roles">
|
||||
<Route index element={<OrganizationCustomRolesPage />} />
|
||||
@@ -441,6 +443,7 @@ export const router = createBrowserRouter(
|
||||
element={<OrganizationProvisionersPage />}
|
||||
/>
|
||||
<Route path="idp-sync" element={<OrganizationIdPSyncPage />} />
|
||||
<Route path="settings" element={<OrganizationSettingsPage />} />
|
||||
</Route>
|
||||
</Route>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user