Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions changelog/7488.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
type: Changed
description: Reorganized nav menu Settings section into Core configuration, Compliance, and Settings groups with new Carbon icons, added collapsible sidebar with smooth transitions and logo crossfade, styled flyout menus, and fixed content width to expand when nav collapses
pr: 7488
labels: []
2 changes: 1 addition & 1 deletion clients/admin-ui/cypress/e2e/domains.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ describe("Domains page", () => {

it("can navigate to the Domains page", () => {
cy.visit("/");
cy.getByTestId("Settings-nav-group").click();
cy.getByTestId("Core configuration-nav-group").click();
cy.getByTestId("Domains-nav-link").click();
cy.getByTestId("management-domains");
});
Expand Down
16 changes: 12 additions & 4 deletions clients/admin-ui/cypress/e2e/nav-bar.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ describe("Nav Bar", () => {
it("renders all navigation groups with links inside", () => {
cy.visit("/");

cy.get(".ant-menu-submenu-title").should("have.length", 4);
// Without Plus: Overview, Data inventory, Privacy requests, Core configuration, Settings (Compliance hidden)
cy.get(".ant-menu-submenu-title").should("have.length", 5);
cy.getByTestId("Overview-nav-group")
.click()
.parents(".ant-menu-submenu")
Expand All @@ -30,14 +31,19 @@ describe("Nav Bar", () => {
cy.getByTestId("Request manager-nav-link");
cy.getByTestId("Connection manager-nav-link");
});
cy.getByTestId("Core configuration-nav-group")
.click()
.parents(".ant-menu-submenu")
.within(() => {
cy.getByTestId("Taxonomy-nav-link");
});
cy.getByTestId("Settings-nav-group")
.click()
.parents(".ant-menu-submenu")
.within(() => {
cy.getByTestId("Privacy requests-nav-link");
cy.getByTestId("Users-nav-link");
cy.getByTestId("Organization-nav-link");
cy.getByTestId("Taxonomy-nav-link");
cy.getByTestId("About Fides-nav-link");
});
});
Expand All @@ -46,18 +52,20 @@ describe("Nav Bar", () => {
stubPlus(true);
cy.visit("/");

cy.get(".ant-menu-submenu-title").should("have.length", 6);
// With Plus: Overview, Detection & Discovery, Data inventory, Privacy requests, Consent, Core configuration, Compliance, Settings
cy.get(".ant-menu-submenu-title").should("have.length", 8);
cy.getByTestId("Detection & Discovery-nav-group")
.click()
.parents(".ant-menu-submenu")
.within(() => {
cy.getByTestId("Action center-nav-link").should("exist");
});
cy.getByTestId("Core configuration-nav-group").should("exist");
cy.getByTestId("Compliance-nav-group").should("exist");
});

it("styles the active navigation link based on the current route", () => {
const ACTIVE_COLOR = "rgb(43, 46, 53)";
// Start on the Home page
cy.visit("/");

// The nav should reflect the active page.
Expand Down
2 changes: 1 addition & 1 deletion clients/admin-ui/cypress/e2e/taxonomies.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ describe("Taxonomy management page", () => {

it("Can navigate to the taxonomy page", () => {
cy.visit("/");
cy.getByTestId("Settings-nav-group").click();
cy.getByTestId("Core configuration-nav-group").click();
cy.getByTestId("Taxonomy-nav-link").click();
cy.getByTestId("taxonomy-type-selector");
});
Expand Down
3 changes: 3 additions & 0 deletions clients/admin-ui/public/logo-collapsed.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
11 changes: 11 additions & 0 deletions clients/admin-ui/public/logo-expanded.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion clients/admin-ui/src/features/common/FixedLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ const FixedLayout = ({
data-testid={title}
direction="column"
height={fullHeight ? "100vh" : "calc(100vh - 48px)"}
width={fullWidth ? "100vw" : "calc(100vw - 240px)"}
width={fullWidth ? "100vw" : "100%"}
>
<Head>
<title>Fides Admin UI - {title}</title>
Expand Down
200 changes: 149 additions & 51 deletions clients/admin-ui/src/features/common/nav/MainSideNav.tsx
Comment thread
jack-gale-ethyca marked this conversation as resolved.
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,10 @@ import {
import palette from "fidesui/src/palette/palette.module.scss";
import NextLink from "next/link";
import { useRouter } from "next/router";
import React from "react";

import logoImage from "~/../public/logo-white.svg";
import logoCollapsed from "~/../public/logo-collapsed.svg";
import logoExpanded from "~/../public/logo-expanded.svg";
import { useAppDispatch } from "~/app/hooks";
import { LOGIN_ROUTE } from "~/constants";
import { logout, useLogoutMutation } from "~/features/auth";
Expand All @@ -19,45 +21,71 @@ import AccountDropdownMenu from "./AccountDropdownMenu";
import { useNav } from "./hooks";
import { ActiveNav, NavGroup } from "./nav-config";
import { NavMenu } from "./NavMenu";
import { INDEX_ROUTE } from "./routes";
import styles from "./NavMenu.module.scss";

const NAV_BACKGROUND_COLOR = palette.FIDESUI_MINOS;
const NAV_WIDTH = "240px";
const COLLAPSED_WIDTH = "80px";
const OPENED_TOGGLES_LOCAL_STORAGE_KEY = "mainSideNavOpenKeys";
const COLLAPSED_LOCAL_STORAGE_KEY = "mainSideNavCollapsed";

const LOGO_FULL_WIDTH = 107;
const LOGO_HEIGHT = 24;
const LOGO_ICON_SIZE = 24;

interface UnconnectedMainSideNavProps {
groups: NavGroup[];
active: ActiveNav | undefined;
handleLogout: () => Promise<void>;
collapsed?: boolean;
onCollapseToggle?: () => void;
}

/** Inner component which we export for component testing */
export const UnconnectedMainSideNav = ({
groups,
active,
handleLogout,
}: {
groups: NavGroup[];
active: ActiveNav | undefined;
handleLogout: any;
}) => {
collapsed = false,
onCollapseToggle,
}: UnconnectedMainSideNavProps) => {
const navMenuItems = groups
.filter((group) => group.children.some((child) => !child.hidden)) // Only include groups with visible children
.map((group) => ({
key: group.title,
icon: group.icon,
popupClassName: styles.flyout,
popupOffset: [12, 0],
label: (
<span data-testid={`${group.title}-nav-group`}>{group.title}</span>
),
children: group.children
.filter((child) => !child.hidden) // Filter out hidden routes from UI
.map((child) => ({
key: child.path,
// child label needs left margin/padding to align with group title
label: (
<NextLink
href={child.path}
data-testid={`${child.title}-nav-link`}
className="ml-4 pl-0.5"
>
{child.title}
</NextLink>
),
})),
children: [
...(collapsed
? [
{
key: `${group.title}-header`,
label: (
<span className={styles.flyoutHeader}>{group.title}</span>
),
disabled: true,
},
]
: []),
...group.children
.filter((child) => !child.hidden)
.map((child) => ({
key: child.path,
label: (
<NextLink
href={child.path}
data-testid={`${child.title}-nav-link`}
className="ml-4 pl-0.5"
>
{child.title}
</NextLink>
),
})),
],
}));

const getActiveKeyFromUrl = () => {
Expand Down Expand Up @@ -111,56 +139,98 @@ export const UnconnectedMainSideNav = ({
);
};

const navWidth = collapsed ? COLLAPSED_WIDTH : NAV_WIDTH;

return (
<Box
px={2}
pb={0}
pt={4}
minWidth={NAV_WIDTH}
maxWidth={NAV_WIDTH}
backgroundColor={NAV_BACKGROUND_COLOR}
height="100%"
overflow="auto"
<div
className={`${styles.navContainer} ${collapsed ? styles.navContainerCollapsed : styles.navContainerExpanded}`}
style={{
minWidth: navWidth,
maxWidth: navWidth,
backgroundColor: NAV_BACKGROUND_COLOR,
}}
>
<VStack
as="nav"
alignItems="start"
alignItems="stretch"
color="white"
height="100%"
justifyContent="space-between"
>
<Box width="100%">
<Box pb={6}>
<Box px={2}>
<NextLink href={INDEX_ROUTE}>
{/* this image gets priority because it's the largest contentful paint and above the fold
see https://nextjs.org/docs/pages/api-reference/components/image#priority */}
<Image src={logoImage} alt="Fides Logo" width={116} priority />
</NextLink>
</Box>
</Box>
<div
className={`${styles.logoWrapper} ${collapsed ? styles.logoWrapperCollapsed : styles.logoWrapperExpanded}`}
>
<button
type="button"
className={`inline-flex cursor-pointer p-0 ${styles.collapseToggle}`}
onClick={onCollapseToggle}
aria-label={
collapsed
? "Expand navigation menu"
: "Collapse navigation menu"
}
data-testid="nav-collapse-toggle"
>
<div
className={styles.logoContainer}
style={{
width: collapsed
? `${LOGO_ICON_SIZE}px`
: `${LOGO_FULL_WIDTH}px`,
height: `${LOGO_HEIGHT}px`,
}}
>
<div
className={`${styles.logoImage} ${collapsed ? styles.logoImageHidden : styles.logoImageVisible}`}
>
<Image
src={logoExpanded}
alt="Fides Logo"
width={LOGO_FULL_WIDTH}
height={LOGO_HEIGHT}
priority
/>
</div>
<div
className={`${styles.logoImage} ${collapsed ? styles.logoImageVisible : styles.logoImageHidden}`}
>
<Image
src={logoCollapsed}
alt="Fides Logo"
width={LOGO_ICON_SIZE}
height={LOGO_ICON_SIZE}
priority
/>
</div>
</div>
</button>
</div>
<NavMenu
items={navMenuItems}
selectedKeys={activeKey ? [activeKey] : []}
onOpenChange={handleOpenChange}
defaultOpenKeys={getStartupOpenKeys()}
inlineCollapsed={collapsed}
/>
</Box>
<Box alignItems="center" pb={4}>
<div
className={`${styles.bottomBar} ${collapsed ? styles.bottomBarCollapsed : styles.bottomBarExpanded}`}
>
<Button
type="primary"
href="https://docs.ethyca.com"
target="_blank"
className="border-none bg-transparent hover:!bg-gray-700"
className={styles.helpButton}
icon={<Icons.Help />}
aria-label="Help"
/>
<div className="inline-block">
<AccountDropdownMenu onLogout={handleLogout} />
</div>
</Box>
</div>
</VStack>
</Box>
</div>
);
};

Expand All @@ -171,6 +241,25 @@ const MainSideNav = () => {
const dispatch = useAppDispatch();
const plusQuery = useGetHealthQuery();

const [collapsed, setCollapsed] = React.useState(false);

React.useEffect(() => {
const stored = localStorage.getItem(COLLAPSED_LOCAL_STORAGE_KEY);
if (stored === "true") {
setCollapsed(true);
}
}, []);
Comment on lines +244 to +251
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Collapsed preference is applied late, causing initial width flicker.

Line 244 starts collapsed as false, then Line 247 applies persisted state after mount. Users who saved collapsed mode will see a brief expanded nav. Line 279-280 also hardcodes expanded width during loading.

Proposed fix
-  const [collapsed, setCollapsed] = React.useState(false);
-
-  React.useEffect(() => {
-    const stored = localStorage.getItem(COLLAPSED_LOCAL_STORAGE_KEY);
-    if (stored === "true") {
-      setCollapsed(true);
-    }
-  }, []);
+  const [collapsed, setCollapsed] = React.useState<boolean>(() => {
+    if (typeof window === "undefined") {
+      return false;
+    }
+    return localStorage.getItem(COLLAPSED_LOCAL_STORAGE_KEY) === "true";
+  });
...
-  if (plusQuery.isLoading) {
+  const loadingNavWidth = collapsed ? COLLAPSED_WIDTH : NAV_WIDTH;
+  if (plusQuery.isLoading) {
     return (
       <div
         style={{
-          minWidth: NAV_WIDTH,
-          maxWidth: NAV_WIDTH,
+          minWidth: loadingNavWidth,
+          maxWidth: loadingNavWidth,
           backgroundColor: NAV_BACKGROUND_COLOR,
           height: "100%",
         }}
       />
     );
   }

Also applies to: 277-283

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@clients/admin-ui/src/features/common/nav/MainSideNav.tsx` around lines 244 -
251, The nav's collapsed preference is read after mount causing a visible
flicker; change the collapsed state initialization to read localStorage
synchronously (e.g., useState(() => { try { return
localStorage.getItem(COLLAPSED_LOCAL_STORAGE_KEY) === "true"; } catch { return
false; } })) so the initial render uses the persisted value, and update the
render logic around the width fallback (the code around where
collapsed/setCollapsed are used and the hardcoded expanded-width path at lines
~277-283) to base the initial width on the collapsed state instead of forcing an
expanded width during loading; ensure you keep the COLLAPSED_LOCAL_STORAGE_KEY,
collapsed and setCollapsed symbols and add a safe try/catch for environments
where localStorage is unavailable.


const toggleCollapsed = React.useCallback(() => {
setCollapsed((prev) => {
const next = !prev;
if (typeof window !== "undefined") {
localStorage.setItem(COLLAPSED_LOCAL_STORAGE_KEY, String(next));
}
return next;
});
}, []);

const handleLogout = async () => {
await logoutMutation({});
// Go to Login page first, then dispatch logout so that ProtectedRoute does not
Expand All @@ -185,16 +274,25 @@ const MainSideNav = () => {
// version of the nav during load, so that when the nav does load, it is fully featured.
if (plusQuery.isLoading) {
return (
<Box
minWidth={NAV_WIDTH}
maxWidth={NAV_WIDTH}
backgroundColor={NAV_BACKGROUND_COLOR}
height="100%"
<div
style={{
minWidth: NAV_WIDTH,
maxWidth: NAV_WIDTH,
backgroundColor: NAV_BACKGROUND_COLOR,
height: "100%",
}}
/>
);
}

return <UnconnectedMainSideNav {...nav} handleLogout={handleLogout} />;
return (
<UnconnectedMainSideNav
{...nav}
handleLogout={handleLogout}
collapsed={collapsed}
onCollapseToggle={toggleCollapsed}
/>
);
};

export default MainSideNav;
Loading
Loading