feat: split management settings sidebar into deployment/organization sidebars (#15388)

resolves coder/internal#199

Currently the admin settings has a combined sidebar for deployment
settings and organization settings, this PR separates the sidebar in 2
sidebars for deployment and organization settings.

This is preparation for the redesign work of the organization settings
sidebar where a dropdown will be user to select and create new
organizations.

see figma:
https://www.figma.com/design/OR75XeUI0Z3ksqt1mHsNQw/Dashboard-v1?node-id=684-5287&m=dev

This also does some of the initial redesign work for the sidebars using
Tailwind.


<img width="1172" alt="Screenshot 2024-11-05 at 5 28 47 PM"
src="https://github.com/user-attachments/assets/5ede14fa-1da9-4e74-a967-81f4e5772c68">
<img width="1169" alt="Screenshot 2024-11-05 at 5 28 56 PM"
src="https://github.com/user-attachments/assets/459339ba-f3bf-4ef2-b86a-143c922108b7">
This commit is contained in:
Jaayden Halko
2024-12-18 17:18:46 -05:00
committed by GitHub
parent 2c7f8ac65f
commit 8e61e4a0be
23 changed files with 322 additions and 328 deletions
+31 -5
View File
@@ -4,13 +4,14 @@ import { Stack } from "components/Stack/Stack";
import { type ClassName, useClassName } from "hooks/useClassName";
import type { ElementType, FC, ReactNode } from "react";
import { Link, NavLink } from "react-router-dom";
import { cn } from "utils/cn";
interface SidebarProps {
children?: ReactNode;
}
export const Sidebar: FC<SidebarProps> = ({ children }) => {
return <nav css={styles.sidebar}>{children}</nav>;
return <nav className="w-60 flex-shrink-0">{children}</nav>;
};
interface SidebarHeaderProps {
@@ -49,6 +50,35 @@ export const SidebarHeader: FC<SidebarHeaderProps> = ({
);
};
interface SettingsSidebarNavItemProps {
children?: ReactNode;
href: string;
end?: boolean;
}
export const SettingsSidebarNavItem: FC<SettingsSidebarNavItemProps> = ({
children,
href,
end,
}) => {
return (
<NavLink
end={end}
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 ",
{
"font-semibold text-content-primary": isActive,
},
)
}
>
{children}
</NavLink>
);
};
interface SidebarNavItemProps {
children?: ReactNode;
icon: ElementType;
@@ -78,10 +108,6 @@ export const SidebarNavItem: FC<SidebarNavItemProps> = ({
};
const styles = {
sidebar: {
width: 245,
flexShrink: 0,
},
info: (theme) => ({
...(theme.typography.body2 as CSSObject),
marginBottom: 16,
-1
View File
@@ -60,7 +60,6 @@ export const ThemeProvider: FC<PropsWithChildren> = ({ children }) => {
useEffect(() => {
const root = document.documentElement;
if (themePreference === "auto") {
root.classList.add(preferredColorScheme);
} else {
@@ -0,0 +1,34 @@
import { Loader } from "components/Loader/Loader";
import { useAuthenticated } from "contexts/auth/RequireAuth";
import { RequirePermission } from "contexts/auth/RequirePermission";
import { type FC, Suspense } from "react";
import { Outlet } from "react-router-dom";
import { DeploymentSidebar } from "./DeploymentSidebar";
const DeploymentSettingsLayout: FC = () => {
const { permissions } = useAuthenticated();
// The deployment settings page also contains users, audit logs, and groups
// so this page must be visible if you can see any of these.
const canViewDeploymentSettingsPage =
permissions.viewDeploymentValues ||
permissions.viewAllUsers ||
permissions.viewAnyAuditLog;
return (
<RequirePermission isFeatureVisible={canViewDeploymentSettingsPage}>
<div className="px-10 max-w-screen-2xl">
<div className="flex flex-row gap-12 py-10">
<DeploymentSidebar />
<main css={{ flexGrow: 1 }}>
<Suspense fallback={<Loader />}>
<Outlet />
</Suspense>
</main>
</div>
</div>
</RequirePermission>
);
};
export default DeploymentSettingsLayout;
@@ -31,12 +31,11 @@ const DeploymentSettingsProvider: FC = () => {
const { permissions } = useAuthenticated();
const deploymentConfigQuery = useQuery(deploymentConfig());
// The deployment settings page also contains users, audit logs, groups and
// organizations, so this page must be visible if you can see any of these.
// The deployment settings page also contains users, audit logs, and groups
// so this page must be visible if you can see any of these.
const canViewDeploymentSettingsPage =
permissions.viewDeploymentValues ||
permissions.viewAllUsers ||
permissions.editAnyOrganization ||
permissions.viewAnyAuditLog;
// Not a huge problem to unload the content in the event of an error,
@@ -0,0 +1,12 @@
import { useAuthenticated } from "contexts/auth/RequireAuth";
import type { FC } from "react";
import { DeploymentSidebarView } from "./DeploymentSidebarView";
/**
* A sidebar for deployment settings.
*/
export const DeploymentSidebar: FC = () => {
const { permissions } = useAuthenticated();
return <DeploymentSidebarView permissions={permissions} />;
};
@@ -0,0 +1,60 @@
import type { Meta, StoryObj } from "@storybook/react";
import { MockNoPermissions, MockPermissions } from "testHelpers/entities";
import { withDashboardProvider } from "testHelpers/storybook";
import { DeploymentSidebarView } from "./DeploymentSidebarView";
const meta: Meta<typeof DeploymentSidebarView> = {
title: "modules/management/SidebarView",
component: DeploymentSidebarView,
decorators: [withDashboardProvider],
parameters: { showOrganizations: true },
args: {
permissions: MockPermissions,
},
};
export default meta;
type Story = StoryObj<typeof DeploymentSidebarView>;
export const NoViewUsers: Story = {
args: {
permissions: {
...MockPermissions,
viewAllUsers: false,
},
},
};
export const NoAuditLog: Story = {
args: {
permissions: {
...MockPermissions,
viewAnyAuditLog: false,
},
},
};
export const NoLicenses: Story = {
args: {
permissions: {
...MockPermissions,
viewAllLicenses: false,
},
},
};
export const NoDeploymentValues: Story = {
args: {
permissions: {
...MockPermissions,
viewDeploymentValues: false,
editDeploymentValues: false,
},
},
};
export const NoPermissions: Story = {
args: {
permissions: MockNoPermissions,
},
};
@@ -0,0 +1,108 @@
import type { AuthorizationResponse, Organization } from "api/typesGenerated";
import { FeatureStageBadge } from "components/FeatureStageBadge/FeatureStageBadge";
import {
Sidebar as BaseSidebar,
SettingsSidebarNavItem as SidebarNavItem,
} from "components/Sidebar/Sidebar";
import type { Permissions } from "contexts/auth/permissions";
import { useFeatureVisibility } from "modules/dashboard/useFeatureVisibility";
import type { FC } from "react";
export interface OrganizationWithPermissions extends Organization {
permissions: AuthorizationResponse;
}
interface DeploymentSidebarProps {
/** Site-wide permissions. */
permissions: Permissions;
}
/**
* A combined deployment settings and organization menu.
*/
export const DeploymentSidebarView: FC<DeploymentSidebarProps> = ({
permissions,
}) => {
const { multiple_organizations: hasPremiumLicense } = useFeatureVisibility();
return (
<BaseSidebar>
<DeploymentSettingsNavigation
permissions={permissions}
isPremium={hasPremiumLicense}
/>
</BaseSidebar>
);
};
interface DeploymentSettingsNavigationProps {
/** Site-wide permissions. */
permissions: Permissions;
isPremium: boolean;
}
/**
* Displays navigation for deployment settings. If active, highlight the main
* menu heading.
*
* Menu items are shown based on the permissions. If organizations can be
* viewed, groups are skipped since they will show under each org instead.
*/
const DeploymentSettingsNavigation: FC<DeploymentSettingsNavigationProps> = ({
permissions,
isPremium,
}) => {
return (
<div>
<div className="flex flex-col gap-1">
{permissions.viewDeploymentValues && (
<SidebarNavItem href="general">General</SidebarNavItem>
)}
{permissions.viewAllLicenses && (
<SidebarNavItem href="licenses">Licenses</SidebarNavItem>
)}
{permissions.editDeploymentValues && (
<SidebarNavItem href="appearance">Appearance</SidebarNavItem>
)}
{permissions.viewDeploymentValues && (
<SidebarNavItem href="userauth">User Authentication</SidebarNavItem>
)}
{permissions.viewDeploymentValues && (
<SidebarNavItem href="external-auth">
External Authentication
</SidebarNavItem>
)}
{/* Not exposing this yet since token exchange is not finished yet.
<SidebarNavItem href="oauth2-provider/ap>
OAuth2 Applications
</SidebarNavItem>*/}
{permissions.viewDeploymentValues && (
<SidebarNavItem href="network">Network</SidebarNavItem>
)}
{permissions.readWorkspaceProxies && (
<SidebarNavItem href="workspace-proxies">
Workspace Proxies
</SidebarNavItem>
)}
{permissions.viewDeploymentValues && (
<SidebarNavItem href="security">Security</SidebarNavItem>
)}
{permissions.viewDeploymentValues && (
<SidebarNavItem href="observability">Observability</SidebarNavItem>
)}
{permissions.viewAllUsers && (
<SidebarNavItem href="users">Users</SidebarNavItem>
)}
{permissions.viewNotificationTemplate && (
<SidebarNavItem href="notifications">
<div className="flex flex-row items-center gap-2">
<span>Notifications</span>
<FeatureStageBadge contentType="beta" size="sm" />
</div>
</SidebarNavItem>
)}
{!isPremium && <SidebarNavItem href="premium">Premium</SidebarNavItem>}
</div>
</div>
);
};
@@ -1,28 +1,26 @@
import type { AuthorizationResponse, Organization } from "api/typesGenerated";
import { Loader } from "components/Loader/Loader";
import { Margins } from "components/Margins/Margins";
import { Stack } from "components/Stack/Stack";
import { useAuthenticated } from "contexts/auth/RequireAuth";
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 { Sidebar } from "./Sidebar";
import { OrganizationSidebar } from "./OrganizationSidebar";
export const ManagementSettingsContext = createContext<
ManagementSettingsValue | undefined
export const OrganizationSettingsContext = createContext<
OrganizationSettingsValue | undefined
>(undefined);
type ManagementSettingsValue = Readonly<{
type OrganizationSettingsValue = Readonly<{
organizations: readonly Organization[];
organization?: Organization;
}>;
export const useManagementSettings = (): ManagementSettingsValue => {
const context = useContext(ManagementSettingsContext);
export const useOrganizationSettings = (): OrganizationSettingsValue => {
const context = useContext(OrganizationSettingsContext);
if (!context) {
throw new Error(
"useManagementSettings should be used inside of ManagementSettingsLayout",
"useOrganizationSettings should be used inside of OrganizationSettingsLayout",
);
}
@@ -43,20 +41,15 @@ export const canEditOrganization = (
);
};
const ManagementSettingsLayout: FC = () => {
const OrganizationSettingsLayout: FC = () => {
const { permissions } = useAuthenticated();
const { organizations } = useDashboard();
const { organization: orgName } = useParams() as {
organization?: string;
};
// The deployment settings page also contains users, audit logs, groups and
// organizations, so this page must be visible if you can see any of these.
const canViewDeploymentSettingsPage =
permissions.viewDeploymentValues ||
permissions.viewAllUsers ||
permissions.editAnyOrganization ||
permissions.viewAnyAuditLog;
const canViewOrganizationSettingsPage =
permissions.viewDeploymentValues || permissions.editAnyOrganization;
const organization =
organizations && orgName
@@ -64,26 +57,26 @@ const ManagementSettingsLayout: FC = () => {
: undefined;
return (
<RequirePermission isFeatureVisible={canViewDeploymentSettingsPage}>
<ManagementSettingsContext.Provider
<RequirePermission isFeatureVisible={canViewOrganizationSettingsPage}>
<OrganizationSettingsContext.Provider
value={{
organizations,
organization,
}}
>
<Margins>
<Stack css={{ padding: "48px 0" }} direction="row" spacing={6}>
<Sidebar />
<div className="px-10 max-w-screen-2xl">
<div className="flex flex-row gap-12 py-10">
<OrganizationSidebar />
<main css={{ flexGrow: 1 }}>
<Suspense fallback={<Loader />}>
<Outlet />
</Suspense>
</main>
</Stack>
</Margins>
</ManagementSettingsContext.Provider>
</div>
</div>
</OrganizationSettingsContext.Provider>
</RequirePermission>
);
};
export default ManagementSettingsLayout;
export default OrganizationSettingsLayout;
@@ -1,14 +1,16 @@
import { organizationsPermissions } from "api/queries/organizations";
import { useAuthenticated } from "contexts/auth/RequireAuth";
import { useDashboard } from "modules/dashboard/useDashboard";
import {
canEditOrganization,
useManagementSettings,
} from "modules/management/ManagementSettingsLayout";
useOrganizationSettings,
} from "modules/management/OrganizationSettingsLayout";
import type { FC } from "react";
import { useQuery } from "react-query";
import { useLocation, useParams } from "react-router-dom";
import { type OrganizationWithPermissions, SidebarView } from "./SidebarView";
import { useParams } from "react-router-dom";
import {
OrganizationSidebarView,
type OrganizationWithPermissions,
} from "./OrganizationSidebarView";
/**
* A combined deployment settings and organization menu.
@@ -17,10 +19,9 @@ import { type OrganizationWithPermissions, SidebarView } from "./SidebarView";
* disabled or not licensed, this is the wrong sidebar to use. See
* DeploySettingsPage/Sidebar instead.
*/
export const Sidebar: FC = () => {
const location = useLocation();
export const OrganizationSidebar: FC = () => {
const { permissions } = useAuthenticated();
const { organizations } = useManagementSettings();
const { organizations } = useOrganizationSettings();
const { organization: organizationName } = useParams() as {
organization?: string;
};
@@ -47,11 +48,7 @@ export const Sidebar: FC = () => {
});
return (
<SidebarView
// Both activeSettings and activeOrganizationName could be be falsey if
// the user is on /organizations but has no editable organizations to
// which we can redirect.
activeSettings={location.pathname.startsWith("/deployment")}
<OrganizationSidebarView
activeOrganizationName={organizationName}
organizations={editableOrgs}
permissions={permissions}
@@ -6,15 +6,14 @@ import {
MockPermissions,
} from "testHelpers/entities";
import { withDashboardProvider } from "testHelpers/storybook";
import { SidebarView } from "./SidebarView";
import { OrganizationSidebarView } from "./OrganizationSidebarView";
const meta: Meta<typeof SidebarView> = {
title: "modules/management/SidebarView",
component: SidebarView,
const meta: Meta<typeof OrganizationSidebarView> = {
title: "modules/management/OrganizationSidebarView",
component: OrganizationSidebarView,
decorators: [withDashboardProvider],
parameters: { showOrganizations: true },
args: {
activeSettings: true,
activeOrganizationName: undefined,
organizations: [
{
@@ -41,7 +40,7 @@ const meta: Meta<typeof SidebarView> = {
};
export default meta;
type Story = StoryObj<typeof SidebarView>;
type Story = StoryObj<typeof OrganizationSidebarView>;
export const LoadingOrganizations: Story = {
args: {
@@ -58,55 +57,12 @@ export const NoCreateOrg: Story = {
},
};
export const NoViewUsers: Story = {
args: {
permissions: {
...MockPermissions,
viewAllUsers: false,
},
},
};
export const NoAuditLog: Story = {
args: {
permissions: {
...MockPermissions,
viewAnyAuditLog: false,
},
},
};
export const NoLicenses: Story = {
args: {
permissions: {
...MockPermissions,
viewAllLicenses: false,
},
},
};
export const NoDeploymentValues: Story = {
args: {
permissions: {
...MockPermissions,
viewDeploymentValues: false,
editDeploymentValues: false,
},
},
};
export const NoPermissions: Story = {
args: {
permissions: MockNoPermissions,
},
};
export const NoSelected: Story = {
args: {
activeSettings: false,
},
};
export const SelectedOrgNoMatch: Story = {
args: {
activeOrganizationName: MockOrganization.name,
@@ -1,17 +1,16 @@
import { cx } from "@emotion/css";
import type { Interpolation, Theme } from "@emotion/react";
import AddIcon from "@mui/icons-material/Add";
import SettingsIcon from "@mui/icons-material/Settings";
import type { AuthorizationResponse, Organization } from "api/typesGenerated";
import { FeatureStageBadge } from "components/FeatureStageBadge/FeatureStageBadge";
import { Loader } from "components/Loader/Loader";
import { Sidebar as BaseSidebar } from "components/Sidebar/Sidebar";
import {
Sidebar as BaseSidebar,
SettingsSidebarNavItem as SidebarNavSubItem,
} from "components/Sidebar/Sidebar";
import { Stack } from "components/Stack/Stack";
import { UserAvatar } from "components/UserAvatar/UserAvatar";
import type { Permissions } from "contexts/auth/permissions";
import { type ClassName, useClassName } from "hooks/useClassName";
import { useDashboard } from "modules/dashboard/useDashboard";
import { useFeatureVisibility } from "modules/dashboard/useFeatureVisibility";
import type { FC, ReactNode } from "react";
import { Link, NavLink } from "react-router-dom";
@@ -20,8 +19,6 @@ export interface OrganizationWithPermissions extends Organization {
}
interface SidebarProps {
/** True if a settings page is being viewed. */
activeSettings: boolean;
/** The active org name, if any. Overrides activeSettings. */
activeOrganizationName: string | undefined;
/** Organizations and their permissions or undefined if still fetching. */
@@ -31,31 +28,17 @@ interface SidebarProps {
}
/**
* A combined deployment settings and organization menu.
* Organization settings left sidebar menu.
*/
export const SidebarView: FC<SidebarProps> = ({
activeSettings,
export const OrganizationSidebarView: FC<SidebarProps> = ({
activeOrganizationName,
organizations,
permissions,
}) => {
const { showOrganizations } = useDashboard();
const { multiple_organizations: hasPremiumLicense } = useFeatureVisibility();
// TODO: Do something nice to scroll to the active org.
return (
<BaseSidebar>
{showOrganizations && (
<header>
<h2 css={styles.sidebarHeader}>Deployment</h2>
</header>
)}
<DeploymentSettingsNavigation
active={!activeOrganizationName && activeSettings}
permissions={permissions}
isPremium={hasPremiumLicense}
/>
{showOrganizations && (
<OrganizationsSettingsNavigation
activeOrganizationName={activeOrganizationName}
@@ -67,108 +50,6 @@ export const SidebarView: FC<SidebarProps> = ({
);
};
interface DeploymentSettingsNavigationProps {
/** Whether a deployment setting page is being viewed. */
active: boolean;
/** Site-wide permissions. */
permissions: Permissions;
isPremium: boolean;
}
/**
* Displays navigation for deployment settings. If active, highlight the main
* menu heading.
*
* Menu items are shown based on the permissions. If organizations can be
* viewed, groups are skipped since they will show under each org instead.
*/
const DeploymentSettingsNavigation: FC<DeploymentSettingsNavigationProps> = ({
active,
permissions,
isPremium,
}) => {
return (
<div css={{ paddingBottom: 12 }}>
<SidebarNavItem
active={active}
href={
permissions.viewDeploymentValues
? "/deployment/general"
: "/deployment/workspace-proxies"
}
// 24px matches the width of the organization icons, and the component
// is smart enough to keep the icon itself square. It looks too big if
// it's 24x24.
icon={<SettingsIcon css={{ width: 24, height: 20 }} />}
>
Deployment
</SidebarNavItem>
{active && (
<Stack spacing={0.5} css={{ marginBottom: 8, marginTop: 8 }}>
{permissions.viewDeploymentValues && (
<SidebarNavSubItem href="general">General</SidebarNavSubItem>
)}
{permissions.viewAllLicenses && (
<SidebarNavSubItem href="licenses">Licenses</SidebarNavSubItem>
)}
{permissions.editDeploymentValues && (
<SidebarNavSubItem href="appearance">Appearance</SidebarNavSubItem>
)}
{permissions.viewDeploymentValues && (
<SidebarNavSubItem href="userauth">
User Authentication
</SidebarNavSubItem>
)}
{permissions.viewDeploymentValues && (
<SidebarNavSubItem href="external-auth">
External Authentication
</SidebarNavSubItem>
)}
{/* Not exposing this yet since token exchange is not finished yet.
<SidebarNavSubItem href="oauth2-provider/ap>
OAuth2 Applications
</SidebarNavSubItem>*/}
{permissions.viewDeploymentValues && (
<SidebarNavSubItem href="network">Network</SidebarNavSubItem>
)}
{permissions.readWorkspaceProxies && (
<SidebarNavSubItem href="workspace-proxies">
Workspace Proxies
</SidebarNavSubItem>
)}
{permissions.viewDeploymentValues && (
<SidebarNavSubItem href="security">Security</SidebarNavSubItem>
)}
{permissions.viewDeploymentValues && (
<SidebarNavSubItem href="observability">
Observability
</SidebarNavSubItem>
)}
{permissions.viewAllUsers && (
<SidebarNavSubItem href="users">Users</SidebarNavSubItem>
)}
{permissions.viewNotificationTemplate && (
<SidebarNavSubItem href="notifications">
<Stack direction="row" alignItems="center" spacing={1}>
<span>Notifications</span>
<FeatureStageBadge contentType="beta" size="sm" />
</Stack>
</SidebarNavSubItem>
)}
{permissions.viewOrganizationIDPSyncSettings && (
<SidebarNavSubItem href="idp-org-sync">
IdP Organization Sync
</SidebarNavSubItem>
)}
{!isPremium && (
<SidebarNavSubItem href="premium">Premium</SidebarNavSubItem>
)}
</Stack>
)}
</div>
);
};
function urlForSubpage(organizationName: string, subpage = ""): string {
return `/organizations/${organizationName}/${subpage}`;
}
@@ -204,18 +85,6 @@ const OrganizationsSettingsNavigation: FC<
return (
<>
<header
css={{
display: "flex",
flexFlow: "row wrap",
columnGap: "8px",
alignItems: "baseline",
}}
>
<h2 css={styles.sidebarHeader}>Organizations</h2>
<FeatureStageBadge contentType="beta" size="sm" />
</header>
{permissions.createOrganization && (
<SidebarNavItem
active="auto"
@@ -270,7 +139,7 @@ const OrganizationSettingsNavigation: FC<
{organization.display_name}
</SidebarNavItem>
{active && (
<Stack spacing={0.5} css={{ marginBottom: 8, marginTop: 8 }}>
<div className="flex flex-col gap-1 my-2 ml-11">
{organization.permissions.editOrganization && (
<SidebarNavSubItem end href={urlForSubpage(organization.name)}>
Settings
@@ -309,7 +178,7 @@ const OrganizationSettingsNavigation: FC<
IdP Sync
</SidebarNavSubItem>
)}
</Stack>
</div>
)}
</>
);
@@ -356,42 +225,6 @@ const SidebarNavItem: FC<SidebarNavItemProps> = ({
);
};
interface SidebarNavSubItemProps {
children?: ReactNode;
href: string;
end?: boolean;
}
const SidebarNavSubItem: FC<SidebarNavSubItemProps> = ({
children,
href,
end,
}) => {
const link = useClassName(classNames.subLink, []);
const activeLink = useClassName(classNames.activeSubLink, []);
return (
<NavLink
end={end}
to={href}
className={({ isActive }) => cx([link, isActive && activeLink])}
>
{children}
</NavLink>
);
};
const styles = {
sidebarHeader: {
textTransform: "uppercase",
letterSpacing: "0.1em",
margin: 0,
fontSize: 11,
fontWeight: 500,
paddingBottom: 4,
},
} satisfies Record<string, Interpolation<Theme>>;
const classNames = {
link: (css, theme) => css`
color: inherit;
@@ -414,29 +247,5 @@ const classNames = {
border-left-color: ${theme.palette.primary.main};
border-top-left-radius: 0;
border-bottom-left-radius: 0;
`,
subLink: (css, theme) => css`
color: ${theme.palette.text.secondary};
text-decoration: none;
display: block;
font-size: 13px;
margin-left: 44px;
padding: 4px 12px;
border-radius: 4px;
transition: background-color 0.15s ease-in-out;
margin-bottom: 1px;
position: relative;
&:hover {
color: ${theme.palette.text.primary};
background-color: ${theme.palette.action.hover};
}
`,
activeSubLink: (css, theme) => css`
color: ${theme.palette.text.primary};
font-weight: 600;
`,
} satisfies Record<string, ClassName>;
@@ -8,7 +8,7 @@ import {
import type { CustomRoleRequest } from "api/typesGenerated";
import { displayError } from "components/GlobalSnackbar/utils";
import { Loader } from "components/Loader/Loader";
import { useManagementSettings } from "modules/management/ManagementSettingsLayout";
import { useOrganizationSettings } from "modules/management/OrganizationSettingsLayout";
import type { FC } from "react";
import { Helmet } from "react-helmet-async";
import { useMutation, useQuery, useQueryClient } from "react-query";
@@ -24,7 +24,7 @@ export const CreateEditRolePage: FC = () => {
organization: string;
roleName: string;
};
const { organizations } = useManagementSettings();
const { organizations } = useOrganizationSettings();
const organization = organizations?.find((o) => o.name === organizationName);
const permissionsQuery = useQuery(organizationPermissions(organization?.id));
const createOrganizationRoleMutation = useMutation(
@@ -8,7 +8,7 @@ import { Loader } from "components/Loader/Loader";
import { SettingsHeader } from "components/SettingsHeader/SettingsHeader";
import { Stack } from "components/Stack/Stack";
import { useFeatureVisibility } from "modules/dashboard/useFeatureVisibility";
import { useManagementSettings } from "modules/management/ManagementSettingsLayout";
import { useOrganizationSettings } from "modules/management/OrganizationSettingsLayout";
import { type FC, useEffect, useState } from "react";
import { Helmet } from "react-helmet-async";
import { useMutation, useQuery, useQueryClient } from "react-query";
@@ -22,7 +22,7 @@ export const CustomRolesPage: FC = () => {
const { organization: organizationName } = useParams() as {
organization: string;
};
const { organizations } = useManagementSettings();
const { organizations } = useOrganizationSettings();
const organization = organizations?.find((o) => o.name === organizationName);
const permissionsQuery = useQuery(organizationPermissions(organization?.id));
const deleteRoleMutation = useMutation(
@@ -10,7 +10,7 @@ import { Loader } from "components/Loader/Loader";
import { SettingsHeader } from "components/SettingsHeader/SettingsHeader";
import { Stack } from "components/Stack/Stack";
import { useFeatureVisibility } from "modules/dashboard/useFeatureVisibility";
import { useManagementSettings } from "modules/management/ManagementSettingsLayout";
import { useOrganizationSettings } from "modules/management/OrganizationSettingsLayout";
import { type FC, useEffect } from "react";
import { Helmet } from "react-helmet-async";
import { useQuery } from "react-query";
@@ -24,7 +24,7 @@ export const GroupsPage: FC = () => {
organization: string;
};
const groupsQuery = useQuery(groupsByOrganization(organizationName));
const { organizations } = useManagementSettings();
const { organizations } = useOrganizationSettings();
const organization = organizations?.find((o) => o.name === organizationName);
const permissionsQuery = useQuery(organizationPermissions(organization?.id));
@@ -11,7 +11,7 @@ import { Paywall } from "components/Paywall/Paywall";
import { SettingsHeader } from "components/SettingsHeader/SettingsHeader";
import { Stack } from "components/Stack/Stack";
import { useFeatureVisibility } from "modules/dashboard/useFeatureVisibility";
import { useManagementSettings } from "modules/management/ManagementSettingsLayout";
import { useOrganizationSettings } from "modules/management/OrganizationSettingsLayout";
import type { FC } from "react";
import { Helmet } from "react-helmet-async";
import { useQueries } from "react-query";
@@ -27,7 +27,7 @@ export const IdpSyncPage: FC = () => {
};
// IdP sync does not have its own entitlement and is based on templace_rbac
const { template_rbac: isIdpSyncEnabled } = useFeatureVisibility();
const { organizations } = useManagementSettings();
const { organizations } = useOrganizationSettings();
const organization = organizations?.find((o) => o.name === organizationName);
const [groupIdpSyncSettingsQuery, roleIdpSyncSettingsQuery, groupsQuery] =
@@ -9,7 +9,7 @@ import {
MockUser,
} from "testHelpers/entities";
import {
renderWithManagementSettingsLayout,
renderWithOrganizationSettingsLayout,
waitForLoaderToBeRemoved,
} from "testHelpers/renderHelpers";
import { server } from "testHelpers/server";
@@ -32,7 +32,7 @@ beforeEach(() => {
});
const renderPage = async () => {
renderWithManagementSettingsLayout(<OrganizationMembersPage />, {
renderWithOrganizationSettingsLayout(<OrganizationMembersPage />, {
route: `/organizations/${MockOrganization.name}/members`,
path: "/organizations/:organization/members",
});
@@ -15,7 +15,7 @@ import { displayError, displaySuccess } from "components/GlobalSnackbar/utils";
import { Loader } from "components/Loader/Loader";
import { Stack } from "components/Stack/Stack";
import { useAuthenticated } from "contexts/auth/RequireAuth";
import { useManagementSettings } from "modules/management/ManagementSettingsLayout";
import { useOrganizationSettings } from "modules/management/OrganizationSettingsLayout";
import { type FC, useState } from "react";
import { Helmet } from "react-helmet-async";
import { useMutation, useQuery, useQueryClient } from "react-query";
@@ -52,7 +52,8 @@ const OrganizationMembersPage: FC = () => {
updateOrganizationMemberRoles(queryClient, organizationName),
);
const { organization } = useManagementSettings();
const { organizations } = useOrganizationSettings();
const organization = organizations?.find((o) => o.name === organizationName);
const permissionsQuery = useQuery(organizationPermissions(organization?.id));
const [memberToDelete, setMemberToDelete] =
@@ -3,7 +3,7 @@ import { provisionerDaemonGroups } from "api/queries/organizations";
import { EmptyState } from "components/EmptyState/EmptyState";
import { useEmbeddedMetadata } from "hooks/useEmbeddedMetadata";
import { useDashboard } from "modules/dashboard/useDashboard";
import { useManagementSettings } from "modules/management/ManagementSettingsLayout";
import { useOrganizationSettings } from "modules/management/OrganizationSettingsLayout";
import type { FC } from "react";
import { Helmet } from "react-helmet-async";
import { useQuery } from "react-query";
@@ -15,7 +15,7 @@ const OrganizationProvisionersPage: FC = () => {
const { organization: organizationName } = useParams() as {
organization: string;
};
const { organization } = useManagementSettings();
const { organization } = useOrganizationSettings();
const { entitlements } = useDashboard();
const { metadata } = useEmbeddedMetadata();
const buildInfoQuery = useQuery(buildInfo(metadata["build-info"]));
@@ -6,7 +6,7 @@ import {
MockOrganization2,
} from "testHelpers/entities";
import {
renderWithManagementSettingsLayout,
renderWithOrganizationSettingsLayout,
waitForLoaderToBeRemoved,
} from "testHelpers/renderHelpers";
import { server } from "testHelpers/server";
@@ -15,7 +15,7 @@ import OrganizationSettingsPage from "./OrganizationSettingsPage";
jest.spyOn(console, "error").mockImplementation(() => {});
const renderPage = async () => {
renderWithManagementSettingsLayout(<OrganizationSettingsPage />, {
renderWithOrganizationSettingsLayout(<OrganizationSettingsPage />, {
route: "/organizations",
path: "/organizations/:organization?",
});
@@ -3,16 +3,13 @@ import {
organizationsPermissions,
updateOrganization,
} from "api/queries/organizations";
import type { Organization } from "api/typesGenerated";
import { ErrorAlert } from "components/Alert/ErrorAlert";
import { EmptyState } from "components/EmptyState/EmptyState";
import { displaySuccess } from "components/GlobalSnackbar/utils";
import { Loader } from "components/Loader/Loader";
import { useFeatureVisibility } from "modules/dashboard/useFeatureVisibility";
import {
canEditOrganization,
useManagementSettings,
} from "modules/management/ManagementSettingsLayout";
import { canEditOrganization } from "modules/management/OrganizationSettingsLayout";
import { useOrganizationSettings } from "modules/management/OrganizationSettingsLayout";
import type { FC } from "react";
import { useMutation, useQuery, useQueryClient } from "react-query";
import { Navigate, useNavigate, useParams } from "react-router-dom";
@@ -23,7 +20,7 @@ const OrganizationSettingsPage: FC = () => {
const { organization: organizationName } = useParams() as {
organization?: string;
};
const { organizations } = useManagementSettings();
const { organizations } = useOrganizationSettings();
const feats = useFeatureVisibility();
const navigate = useNavigate();
+7 -4
View File
@@ -27,12 +27,15 @@ import WorkspacesPage from "./pages/WorkspacesPage/WorkspacesPage";
// - Pages that are secondary, not in the main navigation or not usually accessed
// - Pages that use heavy dependencies like charts or time libraries
const NotFoundPage = lazy(() => import("./pages/404Page/404Page"));
const ManagementSettingsLayout = lazy(
() => import("./modules/management/ManagementSettingsLayout"),
const DeploymentSettingsLayout = lazy(
() => import("./modules/management/DeploymentSettingsLayout"),
);
const DeploymentSettingsProvider = lazy(
() => import("./modules/management/DeploymentSettingsProvider"),
);
const OrganizationSettingsLayout = lazy(
() => import("./modules/management/OrganizationSettingsLayout"),
);
const CliAuthenticationPage = lazy(
() => import("./pages/CliAuthPage/CliAuthPage"),
);
@@ -414,7 +417,7 @@ export const router = createBrowserRouter(
<Route path="/audit" element={<AuditPage />} />
<Route path="/organizations" element={<ManagementSettingsLayout />}>
<Route path="/organizations" element={<OrganizationSettingsLayout />}>
<Route path="new" element={<CreateOrganizationPage />} />
{/* General settings for the default org can omit the organization name */}
@@ -437,7 +440,7 @@ export const router = createBrowserRouter(
</Route>
</Route>
<Route path="/deployment" element={<ManagementSettingsLayout />}>
<Route path="/deployment" element={<DeploymentSettingsLayout />}>
<Route element={<DeploymentSettingsProvider />}>
<Route path="general" element={<GeneralSettingsPage />} />
<Route path="security" element={<SecuritySettingsPage />} />
+3 -3
View File
@@ -9,7 +9,7 @@ import { ThemeProvider } from "contexts/ThemeProvider";
import { RequireAuth } from "contexts/auth/RequireAuth";
import { DashboardLayout } from "modules/dashboard/DashboardLayout";
import type { DashboardProvider } from "modules/dashboard/DashboardProvider";
import ManagementSettingsLayout from "modules/management/ManagementSettingsLayout";
import OrganizationSettingsLayout from "modules/management/OrganizationSettingsLayout";
import { TemplateSettingsLayout } from "pages/TemplateSettingsPage/TemplateSettingsLayout";
import { WorkspaceSettingsLayout } from "pages/WorkspaceSettingsPage/WorkspaceSettingsLayout";
import type { ReactNode } from "react";
@@ -195,7 +195,7 @@ export function renderWithWorkspaceSettingsLayout(
};
}
export function renderWithManagementSettingsLayout(
export function renderWithOrganizationSettingsLayout(
element: JSX.Element,
{
path = "/",
@@ -212,7 +212,7 @@ export function renderWithManagementSettingsLayout(
element: <DashboardLayout />,
children: [
{
element: <ManagementSettingsLayout />,
element: <OrganizationSettingsLayout />,
children: [{ element, path }, ...extraRoutes],
},
],
+3 -3
View File
@@ -9,7 +9,7 @@ import { AuthProvider } from "contexts/auth/AuthProvider";
import { permissionsToCheck } from "contexts/auth/permissions";
import { DashboardContext } from "modules/dashboard/DashboardProvider";
import { DeploymentSettingsContext } from "modules/management/DeploymentSettingsProvider";
import { ManagementSettingsContext } from "modules/management/ManagementSettingsLayout";
import { OrganizationSettingsContext } from "modules/management/OrganizationSettingsLayout";
import type { FC } from "react";
import { useQueryClient } from "react-query";
import {
@@ -155,7 +155,7 @@ export const withGlobalSnackbar = (Story: FC) => (
export const withManagementSettingsProvider = (Story: FC) => {
return (
<ManagementSettingsContext.Provider
<OrganizationSettingsContext.Provider
value={{
organizations: [MockDefaultOrganization],
organization: MockDefaultOrganization,
@@ -166,6 +166,6 @@ export const withManagementSettingsProvider = (Story: FC) => {
>
<Story />
</DeploymentSettingsContext.Provider>
</ManagementSettingsContext.Provider>
</OrganizationSettingsContext.Provider>
);
};