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 }) => ( +
{children}
+ )), + SideNavLink: jest.fn(({ href, current, className, children }) => ( + + {children} + + )), + SideNavMenu: jest.fn(({ title, expanded, className, children }) => ( +
+ {title} + {children} +
+ )), +})); + +describe('SideNavMenuItem Component', () => { + beforeEach(() => { + // Simple in-memory storage for the test + const mockSessionStorage = { + getItem: (key) => + mockSessionStorage[key] ? mockSessionStorage[key] : null, + setItem: (key, value) => (mockSessionStorage[key] = value), + removeItem: (key) => delete mockSessionStorage[key], + clear: () => + Object.keys(mockSessionStorage).forEach( + (key) => delete mockSessionStorage[key], + ), + }; + Object.defineProperty(window, 'sessionStorage', { + value: mockSessionStorage, + writable: true, + }); + }); + + afterEach(() => { + delete window.sessionStorage; + }); + + const items = [ + { + title: 'Item 1', + children: [ + { + title: 'SubItem 1', + href: '/item1/subitem1', + }, + ], + }, + { + title: 'Item 2', + href: '/item2', + children: [], + }, + ]; + + it('should render SideNavMenuItem with given className', () => { + const wrapper = shallow( + , + ); + expect(wrapper.exists()).toBeTruthy(); + expect(wrapper.find('.SideNavMenuItem__nav').exists()).toBeTruthy(); + expect(wrapper.find('.test-class').exists()).toBeTruthy(); + }); + + it('should render SideNavMenu for items with children', () => { + const wrapper = shallow(); + expect(wrapper.find(SideNavMenu).exists()).toBeTruthy(); + expect(wrapper.find(SideNavMenu).prop('title')).toEqual( +
Item 1
, + ); + }); + + it('should render SideNavLink for items without children', () => { + const wrapper = shallow(); + expect(wrapper.find(SideNavLink).exists()).toBeTruthy(); + const sideNavLinks = wrapper.find(SideNavLink); + expect(sideNavLinks.at(0).prop('href')).toEqual('/item1/subitem1'); + expect(sideNavLinks.at(1).prop('href')).toEqual('/item2'); + }); + + it('should pass children to SideNavMenu', () => { + const wrapper = shallow(); + const sideNavMenu = wrapper.find(SideNavMenu).at(0); + const children = sideNavMenu.prop('children'); + expect(children).toBeDefined(); + expect(children).toHaveLength(1); + }); +}); diff --git a/__tests__/components/__snapshots__/Header.test.js.snap b/__tests__/components/__snapshots__/Header.test.js.snap index f7329da..509d8e8 100644 --- a/__tests__/components/__snapshots__/Header.test.js.snap +++ b/__tests__/components/__snapshots__/Header.test.js.snap @@ -12,7 +12,6 @@ exports[`Header Rendering Render Header component with default props 1`] = `
-
@@ -28,7 +27,6 @@ exports[`Header Rendering Render Header component with default props 1`] = `
- `; @@ -45,7 +43,6 @@ exports[`Header Rendering Render Header component with props 1`] = `
-
@@ -61,7 +58,6 @@ exports[`Header Rendering Render Header component with props 1`] = `
- `; diff --git a/__tests__/components/__snapshots__/HeaderAuthentication.test.js.snap b/__tests__/components/__snapshots__/HeaderAuthentication.test.js.snap index 71dc8dc..3f3dee9 100644 --- a/__tests__/components/__snapshots__/HeaderAuthentication.test.js.snap +++ b/__tests__/components/__snapshots__/HeaderAuthentication.test.js.snap @@ -6,23 +6,19 @@ exports[`HeaderAuthentication Rendering Render HeaderAuthentication component wi className="HeaderAuthentication__authentication" > - + BAM -   - -   - + : + + BAM-9.5.0 - - + +
@@ -34,23 +30,19 @@ exports[`HeaderAuthentication Rendering Render HeaderAuthentication component wi className="HeaderAuthentication__authentication varClassName" > - + BAM -   - -   - + : + + BAM-9.5.0 - - + +
@@ -62,23 +54,19 @@ exports[`HeaderAuthentication Rendering Render HeaderAuthentication component wi className="HeaderAuthentication__authentication" > - + Micetro -   - -   - + : + + Micetro-10 - - + + diff --git a/__tests__/components/__snapshots__/HeaderLogo.test.js.snap b/__tests__/components/__snapshots__/HeaderLogo.test.js.snap index 666684c..5a36a17 100644 --- a/__tests__/components/__snapshots__/HeaderLogo.test.js.snap +++ b/__tests__/components/__snapshots__/HeaderLogo.test.js.snap @@ -1,35 +1,39 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`HeaderLogo Rendering Render HeaderLogo component with default props 1`] = ` - - Gateway logo + + + Gateway logo + - Gateway + Gateway - + `; exports[`HeaderLogo Rendering Render HeaderLogo component with props 1`] = ` - - Gateway logo + + + Gateway logo + - Gateway + Gateway - + `; diff --git a/__tests__/components/__snapshots__/SideMenu.test.js.snap b/__tests__/components/__snapshots__/SideMenu.test.js.snap new file mode 100644 index 0000000..928b3fd --- /dev/null +++ b/__tests__/components/__snapshots__/SideMenu.test.js.snap @@ -0,0 +1,109 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`SideMenu Rendering Render SideMenu component active highlight 1`] = ` +
+
+ + +
+ +
+
+ +
+
+
+`; + +exports[`SideMenu Rendering Render SideMenu component with default 1`] = ` +
+
+ + +
+ +
+
+ +
+
+
+`; 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'>
-
@@ -72,7 +67,6 @@ const Header = ({ className }) => {
- )} diff --git a/src/header/Header.less b/src/header/Header.less index ceae3b0..1ad606a 100644 --- a/src/header/Header.less +++ b/src/header/Header.less @@ -1,4 +1,4 @@ -// 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,12 +19,7 @@ // SOFTWARE. @import '~@bluecateng/pelagos/less/spacing'; @import '~@bluecateng/pelagos/less/colors'; - -.SideNavMenu__hamburgerIcon { - .Hamburger { - color: var(--icon-interactive); - } -} +@import '~@bluecateng/pelagos/less/breakpoints'; .Header { background-color: var(--background); @@ -43,20 +38,18 @@ border-bottom: 1px solid var(--border-subtle); } - #navToggle { - grid-row: 1; - } - &__leftSideMenu { display: flex; flex-direction: row; justify-content: start; align-items: center; gap: @sp-16; + margin-left: @sp-16; &__info { - display: flex; flex-direction: row; + display: none; + .breakpoint(md, {display: flex}); } } diff --git a/src/header/HeaderAuthentication.js b/src/header/HeaderAuthentication.js index 75d1e15..72f8ef7 100644 --- a/src/header/HeaderAuthentication.js +++ b/src/header/HeaderAuthentication.js @@ -19,7 +19,6 @@ 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 { Link } from '@carbon/icons-react'; import PropTypes from 'prop-types'; import usePlatformData from '../hooks/usePlatformData'; @@ -46,22 +45,14 @@ const HeaderAuthentication = ({ className }) => { }`}> {authenticationAlias ? ( - <> - {authenticationService}   - -   - - {authenticationAlias} - - + + {authenticationService}:{' '} + {authenticationAlias} + ) : ( authenticationService )} diff --git a/src/header/HeaderAuthentication.less b/src/header/HeaderAuthentication.less index cd72867..2108228 100644 --- a/src/header/HeaderAuthentication.less +++ b/src/header/HeaderAuthentication.less @@ -35,13 +35,11 @@ a { text-decoration: none; - color: var(--tag-color-teal); - padding-left: @sp-02; - max-width: @sp-160; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; + color: var(--link-primary); } } diff --git a/src/header/HeaderLogo.js b/src/header/HeaderLogo.js index 893dde2..48f8fba 100644 --- a/src/header/HeaderLogo.js +++ b/src/header/HeaderLogo.js @@ -25,15 +25,24 @@ import usePlatformData from '../hooks/usePlatformData'; const HeaderLogo = ({ className }) => { const { data } = usePlatformData(); - const headerLogoPath = data?.platform?.header_logo_path; + const { + header_logo_path: headerLogoPath, + gateway_version: gatewayVersion, + } = data?.platform ?? {}; return ( - - Gateway logo - Gateway - + <> + + Gateway logo + + + Gateway {gatewayVersion} + + ); }; diff --git a/src/l10n/en.po b/src/l10n/en.po index f1b4e41..cebfe56 100644 --- a/src/l10n/en.po +++ b/src/l10n/en.po @@ -1,4 +1,4 @@ -# 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 @@ -32,6 +32,10 @@ msgstr "" "Language-Team: \n" "Plural-Forms: \n" +#: 443d +#~ msgid "{0} pages" +#~ msgstr "" + #: 85df msgid "Account" msgstr "" @@ -44,6 +48,22 @@ msgstr "" msgid "Close" msgstr "" +#: 12d7 +msgid "Close side menu" +msgstr "" + +#: b38e +msgid "Custom workflows" +msgstr "" + +#: 8eb6 +msgid "Custom Workflows" +msgstr "" + +#: 397f +#~ msgid "CustomWorkflows" +#~ msgstr "" + #: 3752 msgid "Download all logs" msgstr "" @@ -84,6 +104,10 @@ msgstr "" #~ msgid "Path" #~ msgstr "" +#: f106 +msgid "Quick access to default workflows" +msgstr "" + #: efc0 #~ msgid "Save" #~ msgstr "" @@ -92,14 +116,26 @@ msgstr "" msgid "Save changes" msgstr "" +#: c7f7 +msgid "Settings" +msgstr "" + +#: b657 +msgid "Side menu" +msgstr "" + #: bc07 msgid "System" msgstr "" +#: b3bf +msgid "User configured home page" +msgstr "" + #: 4c39 msgid "View logs" msgstr "" #: 825c -#~ msgid "Workflows" -#~ msgstr "" +msgid "Workflows" +msgstr "" diff --git a/src/l10n/zz.po b/src/l10n/zz.po index 513a32d..09b3fb6 100644 --- a/src/l10n/zz.po +++ b/src/l10n/zz.po @@ -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 @@ -32,6 +32,10 @@ msgstr "" "Mime-Version: 1.0\n" "X-Generator: BlueCat pseudo-translation tool v0.0.2\n" +#: 443d +#~ msgid "{0} pages" +#~ msgstr "" + #: 85df msgid "Account" msgstr "ÂÂççççôôùùnt" @@ -44,6 +48,22 @@ msgstr "ÇÇâânççéél" msgid "Close" msgstr "ÇÇlôôséé" +#: 12d7 +msgid "Close side menu" +msgstr "ÇÇlôôséé sîîdéé ménûû" + +#: b38e +msgid "Custom workflows" +msgstr "ÇÇûûstôôm wôôrkflôôws" + +#: 8eb6 +msgid "Custom Workflows" +msgstr "" + +#: 397f +#~ msgid "CustomWorkflows" +#~ msgstr "ÇÇûûstôômWôôrkflôôws" + #: 3752 msgid "Download all logs" msgstr "Dôôwnlôôââd ââll lôôgs" @@ -84,6 +104,10 @@ msgstr "Lôôgôôùùt" #~ msgid "Path" #~ msgstr "Pââth" +#: f106 +msgid "Quick access to default workflows" +msgstr "Rââpîîd açççéès àû dêéfââùùlt wôôrkflôôws" + #: efc0 #~ msgid "Save" #~ msgstr "Sââvéé" @@ -92,13 +116,25 @@ msgstr "Lôôgôôùùt" msgid "Save changes" msgstr "Sââvéé ççhâângéés" +#: c7f7 +msgid "Settings" +msgstr "Sééttinggs" + +#: b657 +msgid "Side menu" +msgstr "Sîîdéé ménûû" + #: bc07 msgid "System" msgstr "Systéém" +#: b3bf +msgid "User configured home page" +msgstr "Uséér côônfîîgurééd hôôméé pââgéé" + #: 4c39 msgid "View logs" msgstr "Vîîééw lôôgs" -#~ msgid "Workflows" -#~ msgstr "Wôôrkflôôws" +msgid "Workflows" +msgstr "Wôôrkflôôws" diff --git a/src/pageLayout/PageBody.js b/src/pageLayout/PageBody.js index 5074576..4d5423a 100644 --- a/src/pageLayout/PageBody.js +++ b/src/pageLayout/PageBody.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 @@ -20,14 +20,16 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ import PropTypes from 'prop-types'; +import '../../less/main.less'; import Header from '../header/Header'; +import SideMenu from '../sideNav/SideMenu'; import PageContentShell from './PageContentShell'; -import '../../less/main.less'; const PageBody = ({ children }) => { return ( <>
+ {children} ); diff --git a/src/pageLayout/PageContentShell.js b/src/pageLayout/PageContentShell.js index a5b3f50..906fcd0 100644 --- a/src/pageLayout/PageContentShell.js +++ b/src/pageLayout/PageContentShell.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 @@ -22,17 +22,12 @@ SOFTWARE. import PropTypes from 'prop-types'; import Error from '../error/Error'; import usePageError from '../error/usePageError'; -import useSideNav from '../sideNav/useSideNav'; import './PageContentShell.less'; const PageContentShell = ({ className, children }) => { - const { isExpanded } = useSideNav(); const { error } = usePageError(); const classNames = ['PageContentShell']; - if (isExpanded) { - classNames.push('PageContentShell--leftNavIsOpen'); - } // TODO: Add 'PageContentShell--rightPanelIsOpen' conditionally. if (className) { classNames.push(className); diff --git a/src/pageLayout/PageContentShell.less b/src/pageLayout/PageContentShell.less index e66a6f9..3407226 100644 --- a/src/pageLayout/PageContentShell.less +++ b/src/pageLayout/PageContentShell.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 @@ -18,15 +18,14 @@ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. // eslint-disable-next-line max-len + +@import '~@bluecateng/pelagos/less/spacing'; + .PageContentShell { flex-direction: column; flex: 1; overflow: hidden; -} -.PageContentShell--leftNavIsOpen { - // NOTE: The left margin matches the width of the element used for navigation. - // stylelint-disable-next-line @bluecateng/property-strict-value - margin-left: 264px; + margin-left: @sp-80; // SideNavMenu } .PageContentShell--rightPanelIsOpen { // NOTE: The right margin matches the width of the element used for side/details panel. diff --git a/src/sideNav/SideNavMenuSwitcher.js b/src/sideNav/Category.js similarity index 51% rename from src/sideNav/SideNavMenuSwitcher.js rename to src/sideNav/Category.js index 2252ec7..b7c4358 100644 --- a/src/sideNav/SideNavMenuSwitcher.js +++ b/src/sideNav/Category.js @@ -1,5 +1,5 @@ /* -Copyright 2023-2024 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,31 +19,41 @@ 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 { Hamburger } from '@bluecateng/pelagos'; -import useSideNav from './useSideNav'; -import { useLayoutEffect } from 'react'; -const SideNavMenuSwitcher = () => { - const { isExpanded, setExpanded } = useSideNav(); +import { t } from '@bluecateng/l10n.macro'; +import { + DocumentWordProcessor, + DocumentWordProcessorReference, + Home, + Settings, +} from '@carbon/icons-react'; - useLayoutEffect(() => { - if (isExpanded) { - const button = document.getElementById('navToggle'); - const { bottom } = button.getBoundingClientRect(); - const menu = document.getElementById('sideNav'); - menu.style.top = `${bottom}px`; - } - }, [isExpanded]); - - return ( -
- setExpanded(!isExpanded)} - /> -
- ); +const Category = { + Home: { + buttonId: 'homeCategoryButton', + title: t`Home`, + icon: Home, + tooltipText: t`User configured home page`, + }, + Workflows: { + buttonId: 'workflowsCategoryButton', + title: t`Workflows`, + icon: DocumentWordProcessor, + href: '/index', + tooltipText: t`Quick access to default workflows`, + }, + CustomWorkflows: { + buttonId: 'customWorkflowsCategoryButton', + title: t`Custom Workflows`, + icon: DocumentWordProcessorReference, + tooltipText: t`Custom workflows`, + }, + Settings: { + buttonId: 'settingsCategoryButton', + title: t`Settings`, + icon: Settings, + tooltipText: t`Settings`, + }, }; -export default SideNavMenuSwitcher; +export default Category; diff --git a/src/sideNav/SideMenu.js b/src/sideNav/SideMenu.js new file mode 100644 index 0000000..3d215c0 --- /dev/null +++ b/src/sideNav/SideMenu.js @@ -0,0 +1,242 @@ +/* +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 { Layer, SideNav } from '@bluecateng/pelagos'; +import { useCallback, useEffect, useRef, useState } from 'react'; +import usePlatformData from '../hooks/usePlatformData'; +import Category from './Category'; +import './SideMenu.less'; +import SideMenuCategoryButton from './SideMenuCategoryButton'; +import SideMenuHeader from './SideMenuHeader'; +import SideNavMenuItem from './SideNavMenuItem'; + +const isCurrentPathOfCategory = (pathname, category) => { + const flattenedNavLinks = category?.reduce((acc, link) => { + if (link.href === pathname) { + return true; + } + if (link.children) { + return acc || isCurrentPathOfCategory(pathname, link.children); + } + return acc; + }, false); + return flattenedNavLinks; +}; + +const getStates = () => + JSON.parse(sessionStorage.getItem('gw-leftnav-states')) ?? {}; + +const saveStates = (states) => + sessionStorage.setItem('gw-leftnav-states', JSON.stringify(states)); + +const defaultPages = ['', '/', '/home', '/index', null, undefined]; + +const categorizeWorkflows = (navLinks) => { + const result = { + customWorkflows: [], + settings: [], + }; + + navLinks?.forEach((link) => { + if (link?.is_default_workflow) { + result.settings.push(link); + } else { + result.customWorkflows.push(link); + } + }); + + return result; +}; + +const SideMenu = () => { + const [, setOpen] = useState(getStates()); + const [settings, setSettings] = useState([]); + const [customWorkflows, setCustomWorkflows] = useState([]); + const [currentCategoryKey] = useState(null); + const [expandedCategoryKey, setExpandedCategoryKey] = useState(null); + const animationRef = useRef(null); + const menuRef = useRef(null); + const { data } = usePlatformData(); + + const homeUrl = data?.user?.home_url; + const landingPageActivated = !defaultPages.includes(homeUrl); + const mainPageActivated = defaultPages.some( + (path) => path === window.location.pathname, + ); + const customWorkflowsActivated = isCurrentPathOfCategory( + window.location.pathname, + customWorkflows, + ); + const settingsActivated = isCurrentPathOfCategory( + window.location.pathname, + settings, + ); + + useEffect(() => { + const result = categorizeWorkflows(data?.user?.nav_links); + setCustomWorkflows(result.customWorkflows); + setSettings(result.settings); + }, [data?.user?.nav_links]); + + useEffect(() => { + if (menuRef?.current) { + animationRef?.current?.pause(); + const currentTime = animationRef?.current?.currentTime ?? 0; + animationRef.current = + menuRef.current.animate( + [ + { transform: 'translateX(-100%)' }, + { transform: 'translateX(0)' }, + ], + { + duration: 250, + fill: 'both', + easing: 'ease-out', + }, + ) ?? null; + + animationRef.current.pause(); + animationRef.current.currentTime = currentTime; + if (currentTime !== 250 && expandedCategoryKey) { + animationRef.current.play(); + } + } + }, [expandedCategoryKey]); + + const handleCategoryButtonChange = useCallback( + (categoryKey) => { + if (expandedCategoryKey === categoryKey && animationRef?.current) { + animationRef.current.onfinish = + animationRef.current.playbackRate > 0 + ? () => { + setExpandedCategoryKey(null); + } + : null; + document + .getElementById(Category[categoryKey].buttonId) + ?.focus(); + animationRef.current.reverse(); + } else { + setExpandedCategoryKey(categoryKey); + } + }, + [expandedCategoryKey], + ); + + const handleClick = useCallback( + (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 })); + }, + [handleCategoryButtonChange, expandedCategoryKey], + ); + + const handleKeyDown = useCallback( + (event) => { + if ( + event.code === 'Escape' && + expandedCategoryKey && + // Only close the menu if the menu is not already closing + (animationRef?.current?.playState !== 'running' || + animationRef?.current?.playbackRate > 0) + ) { + handleCategoryButtonChange(expandedCategoryKey); + } + }, + [handleCategoryButtonChange, expandedCategoryKey], + ); + const renderSideMenuCategoryMenu = ( + key, + current, + items, + className = null, + ) => ( +
+ handleCategoryButtonChange(key)} + /> + {(expandedCategoryKey === key || + (!expandedCategoryKey && currentCategoryKey === key)) && ( + + + + + + + )} +
+ ); + + return ( +
+
+ {landingPageActivated && ( + + )} + + {renderSideMenuCategoryMenu( + 'CustomWorkflows', + customWorkflowsActivated, + customWorkflows, + null, + )} + {renderSideMenuCategoryMenu( + 'Settings', + settingsActivated, + settings, + 'SideMenu__categories__fromBottom', + )} +
+
+ ); +}; + +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 (
{/* // eslint-disable-next-line max-len */} - - - +
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;