diff --git a/__tests__/components/SideMenu.test.js b/__tests__/components/SideMenu.test.js
new file mode 100644
index 0000000..a98a2fd
--- /dev/null
+++ b/__tests__/components/SideMenu.test.js
@@ -0,0 +1,79 @@
+/*
+Copyright 2023-2025 BlueCat Networks Inc.
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+*/
+
+import { shallow } from 'enzyme';
+import SideMenu from '../../src/sideNav/SideMenu';
+
+jest.unmock('../../src/sideNav/SideMenu');
+
+jest.mock('../../src/hooks/usePlatformData', () =>
+ jest.fn(() => {
+ return {
+ data: {
+ user: {
+ home_url: '/landing_page',
+ nav_links: {
+ custom_workflows: [
+ {
+ 'title': 'Custom workflow',
+ 'href': '/custom_workflow/page',
+ 'children': [],
+ },
+ ],
+ default_workflows: [
+ {
+ 'title': 'Workflow management',
+ 'href': '/admin/workflow_export_import',
+ 'children': [],
+ },
+ ],
+ },
+ },
+ },
+ };
+ }),
+);
+
+Object.defineProperty(window, 'location', {
+ value: {
+ pathname: '',
+ },
+});
+
+describe('SideMenu', () => {
+ describe('Rendering', () => {
+ it('Render SideMenu component with default', () => {
+ sessionStorage.getItem.mockReturnValueOnce(null);
+ sessionStorage.getItem.mockReturnValueOnce(null);
+ const wrapper = shallow();
+ expect(wrapper.getElement()).toMatchSnapshot();
+ });
+
+ it('Render SideMenu component active highlight', () => {
+ window.location.pathname = '/admin/workflow_export_import';
+ sessionStorage.getItem.mockReturnValueOnce(null);
+ sessionStorage.getItem.mockReturnValueOnce(null);
+ const wrapper = shallow();
+ expect(wrapper.getElement()).toMatchSnapshot();
+ });
+ });
+});
diff --git a/__tests__/components/SideNavMenu.test.js b/__tests__/components/SideNavMenu.test.js
deleted file mode 100644
index 1043fab..0000000
--- a/__tests__/components/SideNavMenu.test.js
+++ /dev/null
@@ -1,111 +0,0 @@
-/*
-Copyright 2023 BlueCat Networks Inc.
-
-Permission is hereby granted, free of charge, to any person obtaining a copy
-of this software and associated documentation files (the "Software"), to deal
-in the Software without restriction, including without limitation the rights
-to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-copies of the Software, and to permit persons to whom the Software is
-furnished to do so, subject to the following conditions:
-
-The above copyright notice and this permission notice shall be included in all
-copies or substantial portions of the Software.
-
-THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
-SOFTWARE.
-*/
-
-import { shallow } from 'enzyme';
-import SideNavMenu from '../../src/sideNav/SideNavMenu';
-import useSideNav from '../../src/sideNav/useSideNav';
-
-jest.unmock('../../src/sideNav/SideNavMenu');
-
-jest.mock('../../src/hooks/usePlatformData', () =>
- jest.fn(() => {
- return {
- data: {
- user: {
- nav_links: [
- {
- 'title': 'Administration',
- 'href': null,
- 'children': [
- {
- 'title': 'Workflow Management',
- 'href': '/admin/workflow_export_import',
- 'children': [],
- },
- {
- 'title': 'Create Workflow',
- 'href': '/create_workflow/page',
- 'children': [],
- },
- ],
- },
- ],
- },
- },
- };
- }),
-);
-
-const anyFunction = expect.any(Function);
-
-const mockUseSideNav = {
- isExpanded: true,
- setExpanded: jest.fn(),
-};
-jest.mock('../../src/sideNav/useSideNav', () =>
- jest.fn(() => {
- return mockUseSideNav;
- }),
-);
-
-Object.defineProperty(window, 'location', {
- value: {
- pathname: '',
- },
-});
-
-describe('SideNavMenu', () => {
- describe('Rendering', () => {
- it('Render SideNavMenu component with menu open', () => {
- sessionStorage.getItem.mockReturnValueOnce(null);
- sessionStorage.getItem.mockReturnValueOnce(null);
- const wrapper = shallow();
- expect(wrapper.getElement()).toMatchSnapshot();
- });
-
- it('Render SideNavMenu component with menu open and active highlight', () => {
- window.location.pathname = '/admin/workflow_export_import';
- sessionStorage.getItem.mockReturnValueOnce(null);
- sessionStorage.getItem.mockReturnValueOnce(null);
- const wrapper = shallow();
- expect(wrapper.getElement()).toMatchSnapshot();
- });
-
- it('Render SideNavMenu component with menu closed', () => {
- sessionStorage.getItem.mockReturnValueOnce(null);
- sessionStorage.getItem.mockReturnValueOnce(null);
- useSideNav.mockReturnValue({
- isExpanded: false,
- setExpanded: jest.fn(),
- });
- const wrapper = shallow();
- expect(wrapper.getElement()).toMatchSnapshot();
- });
-
- it('Render SideNavMenu component with menu closed without nav context', () => {
- sessionStorage.getItem.mockReturnValueOnce(null);
- sessionStorage.getItem.mockReturnValueOnce(null);
- const wrapper = shallow();
- expect(wrapper.getElement()).toMatchSnapshot();
- });
- });
-});
diff --git a/__tests__/components/SideNavMenuItem.test.js b/__tests__/components/SideNavMenuItem.test.js
new file mode 100644
index 0000000..bfc4f9f
--- /dev/null
+++ b/__tests__/components/SideNavMenuItem.test.js
@@ -0,0 +1,117 @@
+/*
+Copyright 2025 BlueCat Networks Inc.
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+*/
+
+import { shallow } from 'enzyme';
+import SideNavMenuItem from '../../src/sideNav/SideNavMenuItem';
+import { SideNavLink, SideNavMenu } from '@bluecateng/pelagos';
+
+jest.mock('@bluecateng/pelagos', () => ({
+ useTooltip: () => [false, jest.fn()],
+ SideNavItems: jest.fn(({ children, className }) => (
+
+`;
diff --git a/__tests__/components/__snapshots__/SideNavMenu.test.js.snap b/__tests__/components/__snapshots__/SideNavMenu.test.js.snap
deleted file mode 100644
index 79d858e..0000000
--- a/__tests__/components/__snapshots__/SideNavMenu.test.js.snap
+++ /dev/null
@@ -1,79 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`SideNavMenu Rendering Render SideNavMenu component with menu closed 1`] = `null`;
-
-exports[`SideNavMenu Rendering Render SideNavMenu component with menu closed without nav context 1`] = `null`;
-
-exports[`SideNavMenu Rendering Render SideNavMenu component with menu open 1`] = `
-
-
-
-
- Workflow Management
-
-
- Create Workflow
-
-
-
-
-`;
-
-exports[`SideNavMenu Rendering Render SideNavMenu component with menu open and active highlight 1`] = `
-
-
-
-
- Workflow Management
-
-
- Create Workflow
-
-
-
-
-`;
diff --git a/__tests__/components/__snapshots__/ThemeSwitch.test.js.snap b/__tests__/components/__snapshots__/ThemeSwitch.test.js.snap
index 93d96d0..85bafa1 100644
--- a/__tests__/components/__snapshots__/ThemeSwitch.test.js.snap
+++ b/__tests__/components/__snapshots__/ThemeSwitch.test.js.snap
@@ -3,10 +3,8 @@
exports[`PageToolkit with ThemeSwitch Rendering Render PageToolkit component to verify ThemeSwitch Component 1`] = `
-
-
-
-
+
+
`;
diff --git a/src/components/PageToolkit.js b/src/components/PageToolkit.js
index 1adb12c..e1de4a0 100644
--- a/src/components/PageToolkit.js
+++ b/src/components/PageToolkit.js
@@ -23,7 +23,6 @@ import { useEffect, useState } from 'react';
import PropTypes from 'prop-types';
import PageToolkitBase from './PageToolkitBase';
import PlatformData from './PlatformData';
-import SideNavContextProvider from '../sideNav/SideNavContextProvider';
import useLanguage from '../hooks/useLanguage';
import setLanguage from '../functions/setLanguage';
import ThemeSwitch from './ThemeSwitch';
@@ -58,10 +57,8 @@ const PageToolkit = ({ onLanguageChange, children }) => {
return (
-
-
- {children}
-
+
+ {children}
);
diff --git a/src/header/Header.js b/src/header/Header.js
index 3776473..083bb7b 100644
--- a/src/header/Header.js
+++ b/src/header/Header.js
@@ -1,5 +1,5 @@
/*
-Copyright 2023-2024 BlueCat Networks Inc.
+Copyright 2023-2025 BlueCat Networks Inc.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
@@ -19,17 +19,15 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/
+import { Layer } from '@bluecateng/pelagos';
import PropTypes from 'prop-types';
import { useEffect } from 'react';
-import { Layer } from '@bluecateng/pelagos';
+import usePlatformData from '../hooks/usePlatformData';
import HeaderAccountMenu from './HeaderAccountMenu';
import HeaderAuthentication from './HeaderAuthentication';
import HeaderHelpMenu from './HeaderHelpMenu';
import HeaderLogo from './HeaderLogo';
import HeaderSystemMenu from './HeaderSystemMenu';
-import SideNavMenu from '../sideNav/SideNavMenu';
-import usePlatformData from '../hooks/usePlatformData';
-import SideNavMenuSwitcher from '../sideNav/SideNavMenuSwitcher';
import './Header.less';
@@ -37,8 +35,6 @@ import './Header.less';
* Header component presents the top navbar and the side navbar.
* An example of this component could be found on the index page
* of gateway after login.
- * This component needs to be wrapped with SideNavContext
- * to set initial expansion setting.
* This component is intended to be nested inside the PlatformDataContext
* as it will require access to PlatformData.
* The component should always be wrapped inside parent element
@@ -60,7 +56,6 @@ const Header = ({ className }) => {
data-theme='dark'>
+ );
+};
+
+export default SideMenu;
diff --git a/src/sideNav/SideMenu.less b/src/sideNav/SideMenu.less
new file mode 100644
index 0000000..f7c44e6
--- /dev/null
+++ b/src/sideNav/SideMenu.less
@@ -0,0 +1,100 @@
+/*
+Copyright 2025 BlueCat Networks Inc.
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+*/
+@import '~@bluecateng/pelagos/less/breakpoints';
+@import '~@bluecateng/pelagos/less/colors';
+@import '~@bluecateng/pelagos/less/fonts';
+@import '~@bluecateng/pelagos/less/modal';
+@import '~@bluecateng/pelagos/less/spacing';
+
+.SideMenu {
+ z-index: 2;
+ position: absolute;
+ display: flex;
+ flex-direction: row;
+ top: @sp-48; // Height of header
+ bottom: 0;
+ left: 0;
+ border-right: 1px solid var(--border-subtle);
+
+ &__secondaryNavigation {
+ display: flex;
+ flex-direction: column;
+ width: 240px;
+ top: 0;
+ bottom: 0;
+ left: @sp-80;
+ position: absolute;
+ will-change: transform;
+ transform: translateX(-100%);
+ overflow: hidden;
+ background-color: var(--layer);
+ border-right: 1px solid var(--border-subtle);
+ z-index: -1;
+ &[aria-hidden='false'] {
+ box-shadow: var(--shadow-12);
+ }
+ }
+
+ &__categories {
+ list-style: none;
+ margin: 0;
+ padding: 0;
+ background-color: var(--background);
+ display: flex;
+ flex-direction: column;
+ overflow-y: auto;
+
+ &__fromBottom {
+ margin-top: auto;
+ }
+ }
+
+ .SideNav__items {
+ padding: 0;
+ }
+}
+
+@media print {
+ .SideMenu {
+ display: none;
+ }
+}
+
+// The side-menu always uses a theme of g100, but the shadow it creates should be based on the
+// theme of the document. This redefines pelagos's --shadow-umbra, --shadow-penumbra,
+// and --shadow-ambient based on the theme of the document. Without this, the shadow used is always
+// for g100.
+html[data-theme='white'] {
+ .SideMenu__secondaryNavigation {
+ --shadow-umbra: fade(@black, 20%);
+ --shadow-penumbra: fade(@black, 14%);
+ --shadow-ambient: fade(@black, 12%);
+ }
+}
+
+html[data-theme='g100'] {
+ .SideMenu__secondaryNavigation {
+ --shadow-umbra: fade(@black, 30%);
+ --shadow-penumbra: fade(@black, 21%);
+ --shadow-ambient: fade(@black, 18%);
+ }
+}
diff --git a/src/sideNav/SideMenuCategoryButton.js b/src/sideNav/SideMenuCategoryButton.js
new file mode 100644
index 0000000..b7a0fa2
--- /dev/null
+++ b/src/sideNav/SideMenuCategoryButton.js
@@ -0,0 +1,78 @@
+/*
+Copyright 2025 BlueCat Networks Inc.
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+*/
+
+import { t } from '@bluecateng/l10n.macro';
+import { useTooltip } from '@bluecateng/pelagos';
+import SidePanelClose from '@carbon/icons-react/es/SidePanelClose';
+import { useMemo } from 'react';
+import './SideMenuCategoryButton.less';
+
+const SideMenuCategoryButton = ({
+ buttonId,
+ current,
+ expanded,
+ href,
+ icon: Icon,
+ title,
+ toggleMenu,
+ tooltipText,
+}) => {
+ const buttonProps = useMemo(
+ () => ({
+ 'role': href ? 'link' : 'button',
+ 'aria-current': current,
+ 'aria-expanded': expanded,
+ 'aria-controls': 'secondaryNavigation',
+ 'aria-label': expanded ? t`Close side menu` : undefined,
+ }),
+ [expanded, current],
+ );
+ const tooltipRef = useTooltip(tooltipText, 'right');
+
+ const onClick = href
+ ? { onClick: () => window.location.replace(href) }
+ : null;
+
+ return (
+
+ );
+};
+
+export default SideMenuCategoryButton;
diff --git a/src/sideNav/SideMenuCategoryButton.less b/src/sideNav/SideMenuCategoryButton.less
new file mode 100644
index 0000000..1ba12be
--- /dev/null
+++ b/src/sideNav/SideMenuCategoryButton.less
@@ -0,0 +1,102 @@
+/*
+Copyright 2025 BlueCat Networks Inc.
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+*/
+@import '~@bluecateng/pelagos/less/focus';
+@import '~@bluecateng/pelagos/less/fonts';
+@import '~@bluecateng/pelagos/less/spacing';
+@import '~@bluecateng/pelagos/less/utils';
+
+.SideMenuCategoryButton {
+ display: flex;
+ flex-direction: column;
+ padding: @sp-04;
+ height: @sp-80;
+ width: @sp-80;
+ border: 0;
+ gap: @sp-04;
+ cursor: pointer;
+ position: relative;
+ align-items: center;
+ justify-content: center;
+ transition: background-color 0.15s ease-out;
+ background-color: var(--background);
+
+ @focus-visible();
+ &:focus-visible {
+ box-shadow: inset 0 0 0 2px var(--focus-inset);
+ }
+
+ &__icon {
+ color: var(--icon-secondary);
+ }
+
+ &__title {
+ @body-compact-01();
+ color: var(--text-secondary);
+ white-space: normal;
+ }
+
+ &:hover,
+ &[aria-expanded='true'],
+ &[aria-current='true'] {
+ .SideMenuCategoryButton__icon {
+ color: var(--icon-primary);
+ }
+ .SideMenuCategoryButton__title {
+ color: var(--text-primary);
+ }
+ }
+
+ &:hover {
+ background-color: var(--background-hover);
+ }
+
+ &[aria-expanded='true'] {
+ background-color: var(--layer-01);
+ &:hover {
+ background-color: var(--layer-hover-01);
+ }
+ }
+
+ &[role='link'] {
+ &[aria-current='true']::before {
+ content: '';
+ position: absolute;
+ left: 0;
+ top: 0;
+ bottom: 0;
+ width: @sp-04;
+ background-color: var(--border-interactive);
+ }
+ }
+
+ &[aria-expanded='false'] {
+ &[aria-current='true']::before {
+ content: '';
+ position: absolute;
+ left: 0;
+ top: 0;
+ bottom: 0;
+ width: @sp-04;
+ background-color: var(--border-interactive);
+ }
+ }
+}
diff --git a/src/sideNav/useSideNav.js b/src/sideNav/SideMenuHeader.js
similarity index 75%
rename from src/sideNav/useSideNav.js
rename to src/sideNav/SideMenuHeader.js
index 5804f9f..c77249c 100644
--- a/src/sideNav/useSideNav.js
+++ b/src/sideNav/SideMenuHeader.js
@@ -1,5 +1,5 @@
/*
-Copyright 2023 BlueCat Networks Inc.
+Copyright 2025 BlueCat Networks Inc.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
@@ -19,9 +19,14 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/
-import { useContext } from 'react';
-import SideNavContext from './SideNavContext';
-export default () => {
- return useContext(SideNavContext);
-};
+import './SideMenuHeader.less';
+
+const SideMenuHeader = ({ title, icon: Icon }) => (
+
+
+ {title}
+
+);
+
+export default SideMenuHeader;
diff --git a/src/sideNav/SideNavContext.js b/src/sideNav/SideMenuHeader.less
similarity index 70%
rename from src/sideNav/SideNavContext.js
rename to src/sideNav/SideMenuHeader.less
index 4092791..0871732 100644
--- a/src/sideNav/SideNavContext.js
+++ b/src/sideNav/SideMenuHeader.less
@@ -1,5 +1,5 @@
/*
-Copyright 2023 BlueCat Networks Inc.
+Copyright 2025 BlueCat Networks Inc.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
@@ -19,9 +19,22 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/
-import { createContext } from 'react';
+@import '~@bluecateng/pelagos/less/spacing';
-export default createContext({
- isExpanded: true,
- setExpanded: () => {},
-});
+.SideMenuHeader {
+ height: @sp-80; // same as the height of a SideMenuCategoryButton
+ padding: @sp-24 0 @sp-24 @sp-24;
+ display: flex;
+ flex-direction: row;
+ gap: @sp-12;
+
+ &__icon {
+ color: var(--icon-primary);
+ align-self: center;
+ }
+
+ &__title {
+ color: var(--text-primary);
+ align-self: center;
+ }
+}
diff --git a/src/sideNav/SideNavContextProvider.js b/src/sideNav/SideNavContextProvider.js
deleted file mode 100644
index 339524a..0000000
--- a/src/sideNav/SideNavContextProvider.js
+++ /dev/null
@@ -1,63 +0,0 @@
-/*
-Copyright 2023 BlueCat Networks Inc.
-
-Permission is hereby granted, free of charge, to any person obtaining a copy
-of this software and associated documentation files (the "Software"), to deal
-in the Software without restriction, including without limitation the rights
-to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-copies of the Software, and to permit persons to whom the Software is
-furnished to do so, subject to the following conditions:
-
-The above copyright notice and this permission notice shall be included in all
-copies or substantial portions of the Software.
-
-THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
-SOFTWARE.
-*/
-import { useEffect, useState } from 'react';
-import PropTypes from 'prop-types';
-import SideNavContext from './SideNavContext';
-
-const SideNavContextProvider = ({ children }) => {
- const [isExpanded, setExpanded] = useState(true);
-
- useEffect(() => {
- const parseNav = sessionStorage.getItem('gw-sidenav-isExpanded');
- if (isExpanded === undefined || parseNav === null || parseNav === '') {
- setExpanded(true); // default to open if state unknown
- } else {
- setExpanded(JSON.parse(parseNav));
- }
- }, [setExpanded]);
-
- useEffect(() => {
- sessionStorage.setItem(
- 'gw-sidenav-isExpanded',
- JSON.stringify(isExpanded),
- );
- }, [isExpanded]);
-
- const value = {
- isExpanded,
- setExpanded,
- };
- return (
-
- {children}
-
- );
-};
-
-SideNavContextProvider.displayName = 'SideNav';
-
-SideNavContextProvider.propTypes = {
- /** The child elements. */
- children: PropTypes.oneOfType([PropTypes.node, PropTypes.array]),
-};
-
-export default SideNavContextProvider;
diff --git a/src/sideNav/SideNavMenu.js b/src/sideNav/SideNavMenu.js
deleted file mode 100644
index 542e694..0000000
--- a/src/sideNav/SideNavMenu.js
+++ /dev/null
@@ -1,165 +0,0 @@
-/*
-Copyright 2023-2024 BlueCat Networks Inc.
-
-Permission is hereby granted, free of charge, to any person obtaining a copy
-of this software and associated documentation files (the "Software"), to deal
-in the Software without restriction, including without limitation the rights
-to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-copies of the Software, and to permit persons to whom the Software is
-furnished to do so, subject to the following conditions:
-
-The above copyright notice and this permission notice shall be included in all
-copies or substantial portions of the Software.
-
-THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
-SOFTWARE.
-*/
-import { useState } from 'react';
-import PropTypes from 'prop-types';
-import {
- SideNav,
- SideNavItems,
- SideNavMenu as _SideNavMenu, // Avoid name collision.
- SideNavLink,
-} from '@bluecateng/pelagos';
-import usePlatformData from '../hooks/usePlatformData';
-import useSideNav from './useSideNav';
-import './SideNavMenu.less';
-
-const getStates = () =>
- JSON.parse(sessionStorage.getItem('gw-leftnav-states')) ?? {};
-
-const saveStates = (states) =>
- sessionStorage.setItem('gw-leftnav-states', JSON.stringify(states));
-
-const nameForState = (title) => {
- const sep = '///';
- return sep + title;
-};
-
-const keyForState = (title, parents) => {
- const navPath = parents.concat([nameForState(title)]);
- return navPath.join('');
-};
-
-/* This checks for the /index page link. Without a landing page
- pathname '/home' and '/' do point to the /index page if no
- landing page is enabled in Gateway */
-const validateCurrentHomePageLink = (href, landingPageActivated) => {
- if (href !== '/index' || landingPageActivated) {
- return false;
- }
-
- return ['/', '/home', ''].includes(window.location.pathname);
-};
-
-const renderItem = (item, parents, isNavActive, landingPageActivated) => {
- const classNames = ['SideNavMenu__item--level' + (parents.length + 1)];
-
- if (item.children.length > 0) {
- const key = keyForState(item.title, parents);
- const states = getStates();
- const expanded = states[key] === true;
- const newParents = parents.concat([nameForState(item.title)]);
-
- return (
- <_SideNavMenu
- title={item.title}
- expanded={expanded}
- className={classNames.join(' ')}
- sideNavActive={isNavActive}
- data-state-key={key}>
- {renderItems(item.children, newParents)}
-
- );
- } else {
- return (
-
- {item.title}
-
- );
- }
-};
-
-const renderItems = (items, parents, isNavActive, landingPageActivated) => {
- return items.map((item) =>
- renderItem(item, parents, isNavActive, landingPageActivated),
- );
-};
-
-/**
- * SideNavMenu is a component to display navigation links to the left.
- * This component needs to be wrapped with SideNavContext
- * to set initial expansion setting.
- * This component is intended to be nested inside the PlatformDataContext
- * as it will require access to PlatformData.
- */
-
-const SideNavMenu = ({ className }) => {
- const { data } = usePlatformData();
- const { isExpanded } = useSideNav();
- const links = data?.user?.nav_links ?? [];
- const homeUrl = data?.user?.home_url;
- const landingPageActivated = ![null, '', '/', '/home', undefined].includes(
- homeUrl,
- );
-
- const [, setOpen] = useState(getStates());
-
- const handleClick = (event) => {
- const target = event.target;
- const item = target.closest('[data-state-key]');
- if (!item) {
- // Only groups have a state key.
- return;
- }
- const key = item.getAttribute('data-state-key');
- if (!key) {
- // Only groups have a state key.
- return;
- }
- const expanded = item.getAttribute('aria-expanded') === 'true';
-
- item.setAttribute('aria-expanded', !expanded);
- const states = getStates();
- states[key] = !expanded;
- saveStates(states);
- setOpen((open) => ({ ...open, [key]: !expanded }));
- };
-
- const classNames = ['SideNavMenu__nav'];
- if (className) {
- classNames.push(className);
- }
- return isExpanded ? (
-
-
- {renderItems(links, [], isExpanded, landingPageActivated)}
-
-
- ) : null;
-};
-
-SideNavMenu.propTypes = {
- /** The component class name(s). */
- className: PropTypes.string,
-};
-
-export default SideNavMenu;
diff --git a/src/sideNav/SideNavMenuItem.js b/src/sideNav/SideNavMenuItem.js
new file mode 100644
index 0000000..b630013
--- /dev/null
+++ b/src/sideNav/SideNavMenuItem.js
@@ -0,0 +1,109 @@
+/*
+Copyright 2023-2025 BlueCat Networks Inc.
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+*/
+import {
+ SideNavItems,
+ SideNavLink,
+ SideNavMenu,
+ useTooltip,
+} from '@bluecateng/pelagos';
+import PropTypes from 'prop-types';
+import './SideNavMenuItem.less';
+
+const getStates = () =>
+ JSON.parse(sessionStorage.getItem('gw-leftnav-states')) ?? {};
+
+const nameForState = (title) => {
+ const sep = '///';
+ return sep + title;
+};
+
+const keyForState = (title, parents) => {
+ const navPath = parents.concat([nameForState(title)]);
+ return navPath.join('');
+};
+
+const renderItem = (item, parents) => {
+ const classNames = ['SideNavMenuItem__item--level' + (parents.length + 1)];
+ const key = keyForState(item.title, parents);
+ const tooltipRef = useTooltip(item?.title, 'right');
+
+ if (item?.children?.length > 0) {
+ const states = getStates();
+ const expanded = states[key] === true;
+ const newParents = parents.concat([nameForState(item.title)]);
+
+ return (
+ {item.title}
}>
+ {renderItems(item.children, newParents)}
+
+ );
+ } else {
+ return (
+
+ {/* Do not use value as string, it will trigger the tooltips */}
+
{item.title}
+
+ );
+ }
+};
+
+const renderItems = (items, parents) => {
+ return items?.map((item) => renderItem(item, parents));
+};
+
+/**
+ * SideNav is a component to display navigation links to the left.
+ * This component is intended to be nested inside the PlatformDataContext
+ * as it will require access to PlatformData.
+ */
+
+const SideNavMenuItem = ({ className, items }) => {
+ const classNames = ['SideNavMenuItem__nav'];
+ if (className) {
+ classNames.push(className);
+ }
+
+ return (
+
+ {renderItems(items, [])}
+
+ );
+};
+
+SideNavMenuItem.propTypes = {
+ /** The component class name(s). */
+ className: PropTypes.string,
+};
+
+export default SideNavMenuItem;
diff --git a/src/sideNav/SideNavMenu.less b/src/sideNav/SideNavMenuItem.less
similarity index 75%
rename from src/sideNav/SideNavMenu.less
rename to src/sideNav/SideNavMenuItem.less
index 670cd84..340f0a7 100644
--- a/src/sideNav/SideNavMenu.less
+++ b/src/sideNav/SideNavMenuItem.less
@@ -1,4 +1,4 @@
-// Copyright 2023 BlueCat Networks Inc.
+// Copyright 2023-2025 BlueCat Networks Inc.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
@@ -22,10 +22,9 @@
@import '~@bluecateng/pelagos/less/fonts';
@import '~@bluecateng/pelagos/less/colors';
-.SideNavMenu__nav {
- position: fixed;
+.SideNavMenuItem__nav {
height: 100%;
- width: 264px;
+ width: inherit;
background-color: var(--layer);
border-right: 1px solid var(--border-subtle);
@@ -34,28 +33,19 @@
height: @sp-40; // Override pelagos' decision.
}
- @levels: {
- 2: @sp-32;
- 3: @sp-48;
- 4: @sp-64;
- 5: @sp-80;
- };
- each(@levels, {
- .SideNavMenu__item--level@{key} > .SideNav__submenu,
- .SideNavMenu__item--level@{key} > .SideNav__link {
- padding-left: @value;
- }
- })
-
+ .SideNavMenuItem__item--level2 > .SideNav__submenu,
+ .SideNavMenuItem__item--level2 > .SideNav__link {
+ padding-left: @sp-32;
+ }
// Allow links one level deeper than groups.
- .SideNavMenu__item--level6 > .SideNav__link {
- padding-left: @sp-96;
+ .SideNavMenuItem__item--level3 > .SideNav__link {
+ padding-left: @sp-48;
}
}
@media print {
- .SideNavMenu__nav {
+ .SideNavMenuItem__nav {
display: none;
}
}
diff --git a/src/sideNav/index.js b/src/sideNav/index.js
index 6bec98e..16758bb 100644
--- a/src/sideNav/index.js
+++ b/src/sideNav/index.js
@@ -1,5 +1,5 @@
/*
-Copyright 2023 BlueCat Networks Inc.
+Copyright 2023-2025 BlueCat Networks Inc.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
@@ -19,6 +19,5 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/
-export { default as SideNavMenu } from './SideNavMenu';
-export { default as SideNavMenuSwitcher } from './SideNavMenuSwitcher';
-export { default as useSideNav } from './useSideNav';
+export { default as SideMenu } from './SideMenu';
+export { default as SideNavMenuItem } from './SideNavMenuItem';
diff --git a/stories/Header.stories.js b/stories/Header.stories.js
index 3c937f2..6f80573 100644
--- a/stories/Header.stories.js
+++ b/stories/Header.stories.js
@@ -21,8 +21,6 @@ SOFTWARE.
*/
import Header from '../src/header/Header';
import PlatformDataContext from '../src/components/PlatformDataContext';
-import SideNavContext from '../src/sideNav/SideNavContext';
-import { useState } from 'react';
import PageContentShell from '../src/pageLayout/PageContentShell';
import './Header.stories.less';
@@ -33,6 +31,7 @@ const platformMockValue = {
header_logo_path:
'https://cdn.pixabay.com/photo/2017/05/09/03/46/alberta-2297204_1280.jpg',
language: 'en',
+ gateway_version: '25.2.1',
},
user: {
authentication_info: {
@@ -98,21 +97,12 @@ export default {
component: Header,
decorators: [
(Story) => {
- const [isExpanded, setExpanded] = useState(false);
- const sideNavMockValue = {
- isExpanded: isExpanded,
- setExpanded: setExpanded,
- };
return (
diff --git a/stories/SideNavMenu.stories.js b/stories/SideMenu.stories.js
similarity index 50%
rename from stories/SideNavMenu.stories.js
rename to stories/SideMenu.stories.js
index ddf02c5..0874ff5 100644
--- a/stories/SideNavMenu.stories.js
+++ b/stories/SideMenu.stories.js
@@ -1,5 +1,5 @@
/*
-Copyright 2023 BlueCat Networks Inc.
+Copyright 2025 BlueCat Networks Inc.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
@@ -19,44 +19,70 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/
+/* eslint-disable camelcase, max-len */
+
import React from 'react';
import PlatformDataContext from '../src/components/PlatformDataContext';
-import SideNavMenu from '../src/sideNav/SideNavMenu';
-import SideNavContext from '../src/sideNav/SideNavContext';
+import SideMenu from '../src/sideNav/SideMenu';
const mockDataValue = {
data: {
user: {
- // eslint-disable-next-line camelcase
nav_links: [
{
- 'title': 'Administration',
- 'href': null,
+ 'title': 'Custom workflow',
+ 'href': '/custom_workflow/page',
+ 'children': [],
+ 'is_default_workflow': false,
+ },
+ {
+ 'title': 'Create a new workflow',
+ 'href': '/create_workflow/page',
+ 'children': [],
+ 'is_default_workflow': true,
+ },
+ {
+ 'title': 'Workflow management',
+ 'href': '/admin/workflow_export_import',
+ 'children': [],
+ 'is_default_workflow': true,
+ },
+ {
+ 'title': 'Configurations',
'children': [
{
- 'title': 'Workflow management',
- 'href': '/admin/workflow_export_import',
+ 'title': 'General configuration',
+ 'href': '/admin/general_configuration',
'children': [],
+ 'is_default_workflow': true,
},
{
- 'title': 'Create workflow',
- 'href': '/create_workflow/page',
- 'children': [],
+ 'title': 'SSO configuration',
+ 'href': '/admin/sso_configuration',
+ 'children': [
+ {
+ 'title': 'SSO configuration1',
+ 'href': '/admin/sso_configuration1',
+ 'children': [],
+ 'is_default_workflow': true,
+ },
+ ],
+ 'is_default_workflow': true,
},
],
+ 'is_default_workflow': true,
},
],
},
},
};
-const mockExpanded = {
- isExpanded: true,
-};
-
export default {
- title: 'Components/SideNavMenu',
- component: SideNavMenu,
+ title: 'Components/SideMenu',
+ component: SideMenu,
+ parameters: {
+ layout: 'fullscreen',
+ },
};
export const Normal = {
@@ -65,11 +91,9 @@ export const Normal = {
},
decorators: [
(Story) => (
-
+
-
-
-
+
),
diff --git a/stories/SimplePage.stories.js b/stories/SimplePage.stories.js
index 8c2d66a..29dea18 100644
--- a/stories/SimplePage.stories.js
+++ b/stories/SimplePage.stories.js
@@ -1,5 +1,5 @@
/*
-Copyright 2023-2024 BlueCat Networks Inc.
+Copyright 2023-2025 BlueCat Networks Inc.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
@@ -19,17 +19,18 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/
+/* eslint-disable camelcase, max-len */
import { http, HttpResponse } from 'msw';
import { SimplePage } from '../src/pageLayout';
import './SimplePage.stories.less';
const platformMockValue = {
- /* eslint-disable camelcase, max-len */
platform: {
header_logo_path:
'https://cdn.pixabay.com/photo/2017/05/09/03/46/alberta-2297204_1280.jpg',
language: 'en',
+ gateway_version: '25.2.1',
},
user: {
authentication_info: {
@@ -58,23 +59,50 @@ const platformMockValue = {
},
],
},
- home_url: '#',
+ home_url: '/landing_page',
nav_links: [
{
- 'title': 'Administration',
- 'href': null,
+ 'title': 'Custom workflow',
+ 'href': '/custom_workflow/page',
+ 'children': [],
+ 'is_default_workflow': false,
+ },
+ {
+ 'title': 'Create a new workflow',
+ 'href': '/create_workflow/page',
+ 'children': [],
+ 'is_default_workflow': true,
+ },
+ {
+ 'title': 'Workflow management',
+ 'href': '/admin/workflow_export_import',
+ 'children': [],
+ 'is_default_workflow': true,
+ },
+ {
+ 'title': 'Configurations',
'children': [
{
- 'title': 'Workflow management',
- 'href': '/admin/workflow_export_import',
+ 'title': 'General configuration',
+ 'href': '/admin/general_configuration',
'children': [],
+ 'is_default_workflow': true,
},
{
- 'title': 'Create workflow',
- 'href': '/create_workflow/page',
- 'children': [],
+ 'title': 'SSO configuration',
+ 'href': '/admin/sso_configuration',
+ 'children': [
+ {
+ 'title': 'SSO configuration1',
+ 'href': '/admin/sso_configuration1',
+ 'children': [],
+ 'is_default_workflow': true,
+ },
+ ],
+ 'is_default_workflow': true,
},
],
+ 'is_default_workflow': true,
},
],
permissions: {
@@ -88,7 +116,6 @@ const platformMockValue = {
triggerFetchData: () => {},
/* eslint-enable camelcase, max-len */
};
-
export default {
title: 'Components/SimplePage',
component: SimplePage,
@@ -100,10 +127,11 @@ export default {
}),
],
},
+ layout: 'fullscreen',
},
decorators: [
(Story) => (
-
+
),
diff --git a/stories/SimplePage.stories.less b/stories/SimplePage.stories.less
index edd0628..739c64e 100644
--- a/stories/SimplePage.stories.less
+++ b/stories/SimplePage.stories.less
@@ -19,11 +19,6 @@
// SOFTWARE.
@import '~@bluecateng/pelagos/less/spacing';
-.SideNavMenu__nav {
- height: 330px;
- top: 58px !important; // Adjust the top position of the side nav to fix the display issue in Storybook, not fully optimized
-}
-
.PageContent__customTitle {
background-color: var(--layer);
padding: @sp-32 @sp-32 @sp-16 @sp-32;