From 0a389463f4b89ca3744d9c6be402c5c87c6b1710 Mon Sep 17 00:00:00 2001 From: Martin Hradil Date: Thu, 9 Apr 2020 04:40:14 +0000 Subject: [PATCH 01/57] Replace MainMenu with a carbon wrapper and adding a css hack to distinguish sub-sections from sections --- .../components/main-menu/main-menu.jsx | 83 ---------------- .../components/main-menu/recursive-props.js | 15 --- .../components/main-menu/second-level.jsx | 65 ------------- .../components/main-menu/third-level.jsx | 33 ------- .../components/main-menu/top-level.jsx | 95 ------------------- app/javascript/menu/main-menu.jsx | 81 ++++++++++++++++ app/stylesheet/carbon.scss | 10 ++ package.json | 1 + 8 files changed, 92 insertions(+), 291 deletions(-) delete mode 100644 app/javascript/components/main-menu/main-menu.jsx delete mode 100644 app/javascript/components/main-menu/recursive-props.js delete mode 100644 app/javascript/components/main-menu/second-level.jsx delete mode 100644 app/javascript/components/main-menu/third-level.jsx delete mode 100644 app/javascript/components/main-menu/top-level.jsx create mode 100644 app/javascript/menu/main-menu.jsx diff --git a/app/javascript/components/main-menu/main-menu.jsx b/app/javascript/components/main-menu/main-menu.jsx deleted file mode 100644 index c765e592bc4..00000000000 --- a/app/javascript/components/main-menu/main-menu.jsx +++ /dev/null @@ -1,83 +0,0 @@ -import React, { useState, useEffect } from 'react'; -import PropTypes from 'prop-types'; -import { Grid } from 'patternfly-react'; -import ClassNames from 'classnames'; -import { useSelector } from 'react-redux'; -import isEqual from 'lodash/isEqual'; -import TopLevel from './top-level'; -import SecondLevel from './second-level'; -import ThirdLevel from './third-level'; -import { menuProps, RecursiveMenuProps } from './recursive-props'; - -const Fallback = props => ; - -const getLevelComponent = level => ({ - 0: props => , - 1: props => , -})[level] || Fallback; - -export const MenuItem = ({ level, ...props }) => getLevelComponent(level)(props); - -export const HoverContext = React.createContext(); - -const MainMenu = ({ menu }) => { - const [activeIds, setActiveIds] = useState({}); - const isVerticalMenuCollapsed = useSelector(({ menuReducer: { isVerticalMenuCollapsed } }) => isVerticalMenuCollapsed); - - useEffect(() => { - const content = document.querySelector('.container-pf-nav-pf-vertical-with-sub-menus'); - if (! content) { - return; - } - - if (isVerticalMenuCollapsed) { - content.classList.add('collapsed-nav'); - } else { - content.classList.remove('collapsed-nav'); - } - }, [isVerticalMenuCollapsed]); - - const handleSetActiveIds = (value) => { - if (!isEqual(activeIds, { ...activeIds, ...value })) { - setActiveIds(prevState => ({ ...prevState, ...value })); - } - }; - - return ( - -
handleSetActiveIds({ topLevelId: undefined, secondLevelId: undefined })} - id="main-menu" - className={ClassNames( - 'nav-pf-vertical nav-pf-vertical-with-sub-menus nav-pf-vertical-collapsible-menus', - { - 'hover-secondary-nav-pf': activeIds.topLevelId, - 'hover-tertiary-nav-pf': activeIds.secondLevelId, - collapsed: isVerticalMenuCollapsed, - }, - ) - } - > -
    - - {menu.map(props => ( - - ))} - -
-
-
- ); -}; - -MainMenu.propTypes = { - menu: PropTypes.arrayOf(PropTypes.shape({ - ...menuProps, - items: PropTypes.arrayOf(PropTypes.shape({ - ...menuProps, - items: PropTypes.arrayOf(PropTypes.shape(RecursiveMenuProps())), - })), - })).isRequired, -}; - -export default MainMenu; diff --git a/app/javascript/components/main-menu/recursive-props.js b/app/javascript/components/main-menu/recursive-props.js deleted file mode 100644 index 7e98b154ba1..00000000000 --- a/app/javascript/components/main-menu/recursive-props.js +++ /dev/null @@ -1,15 +0,0 @@ -import PropTypes from 'prop-types'; - -export const menuProps = { - id: PropTypes.string.isRequired, - title: PropTypes.string.isRequired, - icon: PropTypes.string, - href: PropTypes.string.isRequired, - visible: PropTypes.bool, - active: PropTypes.bool, -}; - -export const RecursiveMenuProps = () => ({ - ...menuProps, - items: PropTypes.arrayOf(PropTypes.shape(menuProps)), -}); diff --git a/app/javascript/components/main-menu/second-level.jsx b/app/javascript/components/main-menu/second-level.jsx deleted file mode 100644 index fa49533dfe7..00000000000 --- a/app/javascript/components/main-menu/second-level.jsx +++ /dev/null @@ -1,65 +0,0 @@ -import React, { useContext } from 'react'; -import PropTypes from 'prop-types'; -import ClassNames from 'classnames'; -import { MenuItem, HoverContext } from './main-menu'; -import { menuProps } from './recursive-props'; -import { itemId, linkProps } from '../../menu/item-type'; - -const SecondLevel = ({ - id, - title, - href, - items, - level, - type, - active, - handleSetActiveIds, -}) => { - const hoveredSecondLevelId = useContext(HoverContext).secondLevelId; - const hasSubitems = items.length > 0; - - return ( -
  • (handleSetActiveIds(hasSubitems ? { secondLevelId: id } : undefined))} - onMouseLeave={() => handleSetActiveIds({ secondLevelId: undefined })} - > - - {title} - -
    - - {hasSubitems && ( -
      - {items.map(props => )} -
    - )} -
    -
  • - ); -}; - -SecondLevel.propTypes = { - ...menuProps, - items: PropTypes.arrayOf(PropTypes.shape({ - ...menuProps, - })), -}; - -SecondLevel.defaultProps = { - items: [], -}; - -export default SecondLevel; diff --git a/app/javascript/components/main-menu/third-level.jsx b/app/javascript/components/main-menu/third-level.jsx deleted file mode 100644 index a8c4f339d97..00000000000 --- a/app/javascript/components/main-menu/third-level.jsx +++ /dev/null @@ -1,33 +0,0 @@ -import React from 'react'; -import ClassNames from 'classnames'; -import { menuProps } from './recursive-props'; -import { itemId, linkProps } from '../../menu/item-type'; - -const ThirdLevel = ({ - id, - title, - href, - active, - visible, - type, -}) => (!visible ? null : ( -
  • - - {title} - -
  • -)); - -ThirdLevel.propTypes = { - ...menuProps, -}; - -export default ThirdLevel; diff --git a/app/javascript/components/main-menu/top-level.jsx b/app/javascript/components/main-menu/top-level.jsx deleted file mode 100644 index 4f0e1127796..00000000000 --- a/app/javascript/components/main-menu/top-level.jsx +++ /dev/null @@ -1,95 +0,0 @@ -import React, { useContext } from 'react'; -import PropTypes from 'prop-types'; -import ClassNames from 'classnames'; -import { MenuItem, HoverContext } from './main-menu'; -import { menuProps, RecursiveMenuProps } from './recursive-props'; -import { itemId, linkProps } from '../../menu/item-type'; - -const TopLevel = ({ - level, - id, - title, - icon, - href, - active, - items, - type, - handleSetActiveIds, -}) => { - const hoveredTopLevelId = useContext(HoverContext).topLevelId; - const isSection = items.length > 0; - if (isSection) { - return ( -
  • handleSetActiveIds({ topLevelId: id })} - onBlur={() => undefined} - > - - - {title} - - -
    - -
      - {items.map(props => )} -
    -
    -
    -
  • - ); - } - - return ( -
  • handleSetActiveIds({ topLevelId: undefined })} - onBlur={() => undefined} - > - - - {title} - -
  • - ); -}; - -TopLevel.propTypes = { - ...menuProps, - items: PropTypes.arrayOf(PropTypes.shape({ - ...menuProps, - items: PropTypes.arrayOf(PropTypes.shape(RecursiveMenuProps())), - })), -}; - -TopLevel.defaultProps = { - items: [], -}; - -export default TopLevel; diff --git a/app/javascript/menu/main-menu.jsx b/app/javascript/menu/main-menu.jsx new file mode 100644 index 00000000000..24149ee9d5f --- /dev/null +++ b/app/javascript/menu/main-menu.jsx @@ -0,0 +1,81 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { + SideNav, + SideNavItems, + SideNavLink, + SideNavMenu, + SideNavMenuItem, +} from 'carbon-components-react/es/components/UIShell'; + +import { itemId, linkProps } from './item-type'; + +const MenuItem = ({ active, href, id, title, type }) => ( + + {title} + +); + +const MenuSection = ({ active, id, items, title }) => ( + + {mapItems(items)} + +); + +const mapItems = (items) => items.map((item) => ( + item.items.length + ? + : +)); + +const MainMenu = ({ menu }) => { + return ( + + + {mapItems(menu)} + + + ); +}; + +const menuProps = { + id: PropTypes.string.isRequired, + title: PropTypes.string.isRequired, + icon: PropTypes.string, + href: PropTypes.string.isRequired, + visible: PropTypes.bool, + active: PropTypes.bool, +}; + +const recursiveMenuProps = () => ({ + ...menuProps, + items: PropTypes.arrayOf(PropTypes.shape(menuProps)), +}); + +MainMenu.propTypes = { + menu: PropTypes.arrayOf(PropTypes.shape({ + ...menuProps, + items: PropTypes.arrayOf(PropTypes.shape({ + ...menuProps, + items: PropTypes.arrayOf(PropTypes.shape({ + ...menuProps, + items: PropTypes.arrayOf(PropTypes.shape(menuProps)), + })), + })), + })).isRequired, +}; + +export default MainMenu; diff --git a/app/stylesheet/carbon.scss b/app/stylesheet/carbon.scss index 9d3e20baf7f..69ed4475fe8 100644 --- a/app/stylesheet/carbon.scss +++ b/app/stylesheet/carbon.scss @@ -23,3 +23,13 @@ $carbon--theme: $carbon--theme--g90; html { font-size: 100%; } + +// FIXME distinguish subsections +.bx--side-nav__item .bx--side-nav__item .bx--side-nav__submenu-title { + padding-left: 8px; +} + +// FIXME hack for size differences from original menu +.container-pf-nav-pf-vertical { + margin-left: 256px !important; +} diff --git a/package.json b/package.json index 73f4cfe9b78..d5040b743f3 100644 --- a/package.json +++ b/package.json @@ -54,6 +54,7 @@ "bootstrap-filestyle": "~1.2.1", "bootstrap-switch": "3.3.4", "carbon-components": "~10.12.0", + "carbon-components-react": "~7.12.0", "classnames": "~2.2.6", "codemirror": "~5.47.0", "connected-react-router": "~6.7.0", From f87c48ce361b729491d4d6b525311a6fcc4178ca Mon Sep 17 00:00:00 2001 From: Martin Hradil Date: Thu, 28 May 2020 23:44:41 +0000 Subject: [PATCH 02/57] Remove old specs --- .../__snapshots__/main-menu.spec.js.snap | 732 ------------------ .../spec/main-menu/main-menu.spec.js | 131 ---- 2 files changed, 863 deletions(-) delete mode 100644 app/javascript/spec/main-menu/__snapshots__/main-menu.spec.js.snap delete mode 100644 app/javascript/spec/main-menu/main-menu.spec.js diff --git a/app/javascript/spec/main-menu/__snapshots__/main-menu.spec.js.snap b/app/javascript/spec/main-menu/__snapshots__/main-menu.spec.js.snap deleted file mode 100644 index c5f17ac340a..00000000000 --- a/app/javascript/spec/main-menu/__snapshots__/main-menu.spec.js.snap +++ /dev/null @@ -1,732 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`Main menu test should render correctly 1`] = ` - - -
    - -
    -
    -
    -`; diff --git a/app/javascript/spec/main-menu/main-menu.spec.js b/app/javascript/spec/main-menu/main-menu.spec.js deleted file mode 100644 index a6c07512f4b..00000000000 --- a/app/javascript/spec/main-menu/main-menu.spec.js +++ /dev/null @@ -1,131 +0,0 @@ -import React from 'react'; -import { mount } from 'enzyme'; -import { Provider } from 'react-redux'; -import toJson from 'enzyme-to-json'; -import configureStore from 'redux-mock-store'; -import MainMenu from '../../components/main-menu/main-menu'; -import TopLevel from '../../components/main-menu/top-level'; -import SecondLevel from '../../components/main-menu/second-level'; -import ThirdLevel from '../../components/main-menu/third-level'; - -describe('Main menu test', () => { - let mockNavItems; - const mockStore = configureStore(); - let store; - - beforeEach(() => { - store = mockStore({ - menuReducer: { - isVerticalMenuCollapsed: false, - }, - }); - mockNavItems = [{ - id: 'vi', - title: 'Cloud Intel', - iconClass: 'fa fa-dashboard', - href: '/dashboard/maintab/?tab=vi', - visible: true, - active: true, - type: 'default', - items: [{ - id: 'dashboard', - title: 'Dashboard', - iconClass: null, - href: '/dashboard/show', - visible: true, - active: true, - type: 'modal', - items: [], - }, { - id: 'chargeback', - title: 'Chargeback', - iconClass: null, - href: '/chargeback/explorer', - visible: true, - active: false, - type: 'big_iframe', - items: [], - }], - }, { - id: 'compute', - title: 'Compute', - iconClass: 'pficon pficon-cpu', - href: '/dashboard/maintab/?tab=compute', - visible: true, - active: false, - type: 'new_window', - items: [{ - id: 'clo', - title: 'Clouds', - iconClass: 'fa fa-plus', - href: '/dashboard/maintab/?tab=clo', - visible: true, - active: false, - type: 'new_window', - items: [{ - id: 'ems_cloud', - title: 'Providers', - iconClass: null, - href: '/ems_cloud/show_list', - visible: true, - active: true, - type: 'new_window', - items: [], - }, { - id: 'availability_zone', - title: 'Availability Zones', - iconClass: null, - href: '/availability_zone/show_list', - visible: true, - active: false, - type: 'default', - items: [], - }, { - id: 'host_aggregate', - title: 'Host Aggregates', - iconClass: null, - href: '/host_aggregate/show_list', - visible: false, - active: false, - type: 'new_window', - items: [], - }], - }], - }]; - }); - - it('should render correctly', () => { - const wrapper = mount().find(MainMenu); - expect(toJson(wrapper)).toMatchSnapshot(); - }); - - it('should render proper level components', () => { - const wrapper = mount(); - expect(wrapper.find(TopLevel)).toHaveLength(2); - expect(wrapper.find(SecondLevel)).toHaveLength(3); - expect(wrapper.find(ThirdLevel)).toHaveLength(3); - }); - - it('should render active third level components properly', () => { - const wrapper = mount(); - expect(wrapper.find(ThirdLevel).find('li.menu-list-group-item.active')) - .toHaveLength(1); - }); - - it('should render not collapsed vertical navbar with proper classNames', () => { - const wrapper = mount(); - expect(wrapper.find('#main-menu').instance().className) - .toEqual('nav-pf-vertical nav-pf-vertical-with-sub-menus nav-pf-vertical-collapsible-menus'); - }); - - it('should render collapsed vertical navbar with proper classNames', () => { - const collapsedStore = mockStore({ - menuReducer: { - isVerticalMenuCollapsed: true, - }, - }); - const wrapper = mount(); - expect(wrapper.find('#main-menu').instance().className) - .toEqual('nav-pf-vertical nav-pf-vertical-with-sub-menus nav-pf-vertical-collapsible-menus collapsed'); - }); -}); From 873fcd9fa65594b5b05ac140f182c708bf582db2 Mon Sep 17 00:00:00 2001 From: Martin Hradil Date: Thu, 9 Apr 2020 04:41:27 +0000 Subject: [PATCH 03/57] Navbar component - wraps navbar, menu, notifications/toasts todo the navbar/notification/toasts parts --- .../components/top-navbar/right-section.jsx | 4 +- app/javascript/menu/navbar.jsx | 56 +++++++++++++++++++ .../packs/component-definitions-common.js | 18 +----- app/views/layouts/_header.html.haml | 35 +++++------- 4 files changed, 76 insertions(+), 37 deletions(-) create mode 100644 app/javascript/menu/navbar.jsx diff --git a/app/javascript/components/top-navbar/right-section.jsx b/app/javascript/components/top-navbar/right-section.jsx index 74fe39d56ee..b16df628f29 100644 --- a/app/javascript/components/top-navbar/right-section.jsx +++ b/app/javascript/components/top-navbar/right-section.jsx @@ -12,7 +12,7 @@ import { const RightSection = ({ customLogo, currentUser, helpMenu, opsExplorerAllowed, applianceName, miqGroups, currentGroup, userMenu, }) => ( - +
      @@ -22,7 +22,7 @@ const RightSection = ({ )} - +
    ); RightSection.propTypes = { diff --git a/app/javascript/menu/navbar.jsx b/app/javascript/menu/navbar.jsx new file mode 100644 index 00000000000..398593fdbcc --- /dev/null +++ b/app/javascript/menu/navbar.jsx @@ -0,0 +1,56 @@ +import React from 'react'; +import { + Header, + HeaderContainer, + HeaderName, + HeaderNavigation, + HeaderMenuItem, + HeaderMenuButton, + HeaderGlobalBar, + HeaderGlobalAction, + SkipToContent, +} from 'carbon-components-react/es/components/UIShell'; + +import MainMenu from './main-menu'; +import NotificationDrawer from '../components/notification-drawer/notification-drawer'; +import ToastList from '../components/toast-list/toast-list'; +import navbar from '../components/top-navbar'; + +export const Navbar = (props) => { + const { customBrand, imagePath } = props; + const { rightSection } = props; + const { notificationDrawer, toastList } = props; + const { menu } = props; + + return ( + ( +
    + + + + + + ManageIQ + + + { menu && () } + + + // %nav.collapse.navbar-collapse + { rightSection && () } + + + Repositories + + + { notificationDrawer && () } + { toastList && () } + + +
    + )} /> + ); +}; diff --git a/app/javascript/packs/component-definitions-common.js b/app/javascript/packs/component-definitions-common.js index dfe66b29fe3..eb050147eba 100644 --- a/app/javascript/packs/component-definitions-common.js +++ b/app/javascript/packs/component-definitions-common.js @@ -18,11 +18,9 @@ import GenericGroupWrapper from '../react/generic_group_wrapper'; import { HierarchicalTreeView } from '../components/tree-view'; import FonticonPicker from '../components/fonticon-picker'; import ImportDatastoreViaGit from '../components/automate-import-export-form/import-datastore-via-git'; -import MainMenu from '../components/main-menu/main-menu'; import MiqAboutModal from '../components/miq-about-modal'; import MiqToolbar from '../components/miq-toolbar'; -import navbar from '../components/top-navbar'; -import NotificationDrawer from '../components/notification-drawer/notification-drawer'; +import { Navbar } from '../menu/navbar'; import OptimizationList from '../optimization/optimization_list'; import OpsTenantForm from '../components/ops-tenant-form/ops-tenant-form'; import OrcherstrationTemplateForm from '../components/orchestration-template/orcherstration-template-form'; @@ -39,7 +37,6 @@ import TableListViewWrapper from '../react/table_list_view_wrapper'; import TaggingWrapperConnected from '../components/taggingWrapper'; import { TagView } from '../tagging'; import TextualSummaryWrapper from '../react/textual_summary_wrapper'; -import ToastList from '../components/toast-list/toast-list'; import VmServerRelationshipForm from '../components/vm-server-relationship-form'; import VmSnapshotForm from '../components/vm-snapshot-form/vm-snapshot-form'; import WorkersForm from '../components/workers-form/workers-form'; @@ -55,28 +52,22 @@ ManageIQ.component.addReact('Breadcrumbs', Breadcrumbs); ManageIQ.component.addReact('CatalogForm', CatalogForm); ManageIQ.component.addReact('CloudNetworkForm', CloudNetworkForm); ManageIQ.component.addReact('CloudTenantForm', CloudTenantForm); -ManageIQ.component.addReact('navbar.Configuration', navbar.Configuration); ManageIQ.component.addReact('ProviderForm', ProviderForm); ManageIQ.component.addReact('CopyCatalogForm', CopyCatalogForm); ManageIQ.component.addReact('CopyDashboardForm', CopyDashboardForm); -ManageIQ.component.addReact('navbar.CustomLogo', navbar.CustomLogo); ManageIQ.component.addReact('FirmwareRegistryForm', FirmwareRegistryForm); ManageIQ.component.addReact('FlavorForm', FlavorForm); ManageIQ.component.addReact('FormButtonsRedux', FormButtonsRedux); ManageIQ.component.addReact('GenericGroup', GenericGroup); ManageIQ.component.addReact('GenericGroupWrapper', GenericGroupWrapper); -ManageIQ.component.addReact('navbar.Help', navbar.Help); ManageIQ.component.addReact('HierarchicalTreeView', HierarchicalTreeView); ManageIQ.component.addReact('FonticonPicker', FonticonPicker); ManageIQ.component.addReact('ImportDatastoreViaGit', ImportDatastoreViaGit); -ManageIQ.component.addReact('MainMenu', MainMenu); ManageIQ.component.addReact('MiqAboutModal', MiqAboutModal); ManageIQ.component.addReact('MiqToolbar', MiqToolbar); -ManageIQ.component.addReact('navbar.NavbarHeader', navbar.NavbarHeader); -ManageIQ.component.addReact('navbar.Notifications', navbar.Notifications); -ManageIQ.component.addReact('NotificationDrawer', NotificationDrawer); -ManageIQ.component.addReact('OptimizationList', OptimizationList); +ManageIQ.component.addReact('Navbar', Navbar); ManageIQ.component.addReact('OpsTenantForm', OpsTenantForm); +ManageIQ.component.addReact('OptimizationList', OptimizationList); ManageIQ.component.addReact('OrcherstrationTemplateForm', OrcherstrationTemplateForm); ManageIQ.component.addReact('PxeServersForm', PxeServersForm); ManageIQ.component.addReact('Quadicon', Quadicon); @@ -84,7 +75,6 @@ ManageIQ.component.addReact('RegionForm', RegionForm); ManageIQ.component.addReact('RemoveCatalogItemModal', RemoveCatalogItemModal); ManageIQ.component.addReact('RemoveGenericItemModal', RemoveGenericItemModal); ManageIQ.component.addReact('ReportDataTable', ReportDataTable); -ManageIQ.component.addReact('navbar.RightSection', navbar.RightSection); ManageIQ.component.addReact('ServiceDialogFromForm', ServiceDialogFromForm); ManageIQ.component.addReact('ServiceForm', ServiceForm); ManageIQ.component.addReact('SetOwnershipForm', SetOwnershipForm); @@ -94,9 +84,7 @@ ManageIQ.component.addReact('TagGroup', props => ); ManageIQ.component.addReact('TagView', TagView); ManageIQ.component.addReact('TaggingWrapperConnected', TaggingWrapperConnected); ManageIQ.component.addReact('TextualSummaryWrapper', TextualSummaryWrapper); -ManageIQ.component.addReact('ToastList', ToastList); ManageIQ.component.addReact('Toolbar', Toolbar); -ManageIQ.component.addReact('navbar.UserOptions', navbar.UserOptions); ManageIQ.component.addReact('VmServerRelationshipForm', VmServerRelationshipForm); ManageIQ.component.addReact('VmSnapshotForm', VmSnapshotForm); ManageIQ.component.addReact('WorkersForm', WorkersForm); diff --git a/app/views/layouts/_header.html.haml b/app/views/layouts/_header.html.haml index 91e0ced32c2..db1562c5a5a 100644 --- a/app/views/layouts/_header.html.haml +++ b/app/views/layouts/_header.html.haml @@ -1,23 +1,18 @@ -= react('MiqAboutModal', :dialogClassName => Settings.server.custom_login_logo ? 'whitelabel' : '') - -%nav.navbar.navbar-pf-vertical - = react('navbar.NavbarHeader', { :customBrand => ::Settings.server.custom_brand, :imagePath => image_path("layout/brand.svg") }) - %nav.collapse.navbar-collapse - = react('navbar.RightSection', - {:customLogo => ::Settings.server.custom_logo, - :currentUser => current_user, - :helpMenu => menu_to_json(:help), - :opsExplorerAllowed => ApplicationHelper.role_allows?(:feature => 'ops_explorer',:any => true), - :applianceName => appliance_name, - :miqGroups => current_user.miq_groups.map { |group| { :id => group.id.to_s, :description => group.description } }, - :currentGroup => { :id => current_group.id.to_s, :description => current_group.description }, - :userMenu => menu_to_json(:top_right)}, - {:class => "nav navbar-nav navbar-right navbar-iconic"}, - 'ul') - = react('NotificationDrawer') - = react('ToastList') += react('Navbar', {:customBrand => ::Settings.server.custom_brand, + :imagePath => image_path("layout/brand.svg"), + :rightSection => {:customLogo => ::Settings.server.custom_logo, + :currentUser => current_user, + :helpMenu => menu_to_json(:help), + :opsExplorerAllowed => ApplicationHelper.role_allows?(:feature => 'ops_explorer',:any => true), + :applianceName => appliance_name, + :miqGroups => current_user.miq_groups.map { |group| { :id => group.id.to_s, :description => group.description } }, + :currentGroup => {:id => current_group.id.to_s, + :description => current_group.description}, + :userMenu => menu_to_json(:top_right)}, + :notificationDrawer => true, + :toastList => true, + :menu => menu_to_json}) += react('MiqAboutModal', :dialogClassName => Settings.server.custom_login_logo ? 'whitelabel' : '') = render :partial => "layouts/spinner" = render :partial => "layouts/lightbox_panel" - -= react('MainMenu', { :menu => menu_to_json }) From e143594573608a46eff41486a6e17b3610865890 Mon Sep 17 00:00:00 2001 From: Martin Hradil Date: Thu, 9 Apr 2020 11:24:41 +0000 Subject: [PATCH 04/57] Menu icons - display, cleanup 2nd level previous menu only displayed icons for first-level sections, some 2nd level sections have icons which don't exist, or are the same as every other icon in the section, removing --- app/javascript/menu/main-menu.jsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/app/javascript/menu/main-menu.jsx b/app/javascript/menu/main-menu.jsx index 24149ee9d5f..8cf4fc7dfa8 100644 --- a/app/javascript/menu/main-menu.jsx +++ b/app/javascript/menu/main-menu.jsx @@ -10,6 +10,8 @@ import { import { itemId, linkProps } from './item-type'; +const carbonizeIcon = (classname) => (props) => (); + const MenuItem = ({ active, href, id, title, type }) => ( ( ); -const MenuSection = ({ active, id, items, title }) => ( +const MenuSection = ({ active, id, items, title, icon }) => ( {mapItems(items)} From e5c42183b18e3ae644f79bfb185240f284e7e12c Mon Sep 17 00:00:00 2001 From: Martin Hradil Date: Thu, 9 Apr 2020 11:53:21 +0000 Subject: [PATCH 05/57] MainMenu - simplify props check what we need in the individual items and sections, removing the need for overly specific shapes on MainMenu --- app/javascript/menu/main-menu.jsx | 62 ++++++++++++++----------------- app/javascript/menu/navbar.jsx | 2 +- 2 files changed, 29 insertions(+), 35 deletions(-) diff --git a/app/javascript/menu/main-menu.jsx b/app/javascript/menu/main-menu.jsx index 8cf4fc7dfa8..e287613da70 100644 --- a/app/javascript/menu/main-menu.jsx +++ b/app/javascript/menu/main-menu.jsx @@ -3,15 +3,22 @@ import PropTypes from 'prop-types'; import { SideNav, SideNavItems, - SideNavLink, SideNavMenu, SideNavMenuItem, } from 'carbon-components-react/es/components/UIShell'; import { itemId, linkProps } from './item-type'; + const carbonizeIcon = (classname) => (props) => (); +const mapItems = (items) => items.map((item) => ( + item.items.length + ? + : +)); + + const MenuItem = ({ active, href, id, title, type }) => ( ( ); +MenuItem.props = { + active: PropTypes.bool, + href: PropTypes.string.isRequired, + id: PropTypes.string.isRequired, + title: PropTypes.string.isRequired, + type: PropTypes.string.isRequired, +}; + + const MenuSection = ({ active, id, items, title, icon }) => ( ( ); -const mapItems = (items) => items.map((item) => ( - item.items.length - ? - : -)); +MenuSection.props = { + active: PropTypes.bool, + icon: PropTypes.string, + id: PropTypes.string.isRequired, + items: PropTypes.arrayOf(PropTypes.any).isRequired, + title: PropTypes.string.isRequired, +}; -const MainMenu = ({ menu }) => { + +export const MainMenu = ({ menu }) => { return ( @@ -54,31 +73,6 @@ const MainMenu = ({ menu }) => { ); }; -const menuProps = { - id: PropTypes.string.isRequired, - title: PropTypes.string.isRequired, - icon: PropTypes.string, - href: PropTypes.string.isRequired, - visible: PropTypes.bool, - active: PropTypes.bool, -}; - -const recursiveMenuProps = () => ({ - ...menuProps, - items: PropTypes.arrayOf(PropTypes.shape(menuProps)), -}); - MainMenu.propTypes = { - menu: PropTypes.arrayOf(PropTypes.shape({ - ...menuProps, - items: PropTypes.arrayOf(PropTypes.shape({ - ...menuProps, - items: PropTypes.arrayOf(PropTypes.shape({ - ...menuProps, - items: PropTypes.arrayOf(PropTypes.shape(menuProps)), - })), - })), - })).isRequired, + menu: PropTypes.arrayOf(PropTypes.any).isRequired, }; - -export default MainMenu; diff --git a/app/javascript/menu/navbar.jsx b/app/javascript/menu/navbar.jsx index 398593fdbcc..326f96c0526 100644 --- a/app/javascript/menu/navbar.jsx +++ b/app/javascript/menu/navbar.jsx @@ -11,7 +11,7 @@ import { SkipToContent, } from 'carbon-components-react/es/components/UIShell'; -import MainMenu from './main-menu'; +import { MainMenu } from './main-menu'; import NotificationDrawer from '../components/notification-drawer/notification-drawer'; import ToastList from '../components/toast-list/toast-list'; import navbar from '../components/top-navbar'; From 3fe58974c59e0244789615f24553b0394a51564d Mon Sep 17 00:00:00 2001 From: Martin Hradil Date: Thu, 9 Apr 2020 12:13:57 +0000 Subject: [PATCH 06/57] NotificationDrawer, ToastList - both can live outside Navbar ToastList renders completely independently NotifiactionDrawer inherits height, so we can either override height and position down to align with the bottom of navbar, or move it outside and position up to align, better. --- app/javascript/menu/navbar.jsx | 5 ----- app/javascript/packs/component-definitions-common.js | 4 ++++ app/stylesheet/carbon.scss | 9 +++++++-- app/views/layouts/_header.html.haml | 7 ++++--- 4 files changed, 15 insertions(+), 10 deletions(-) diff --git a/app/javascript/menu/navbar.jsx b/app/javascript/menu/navbar.jsx index 326f96c0526..099f166026b 100644 --- a/app/javascript/menu/navbar.jsx +++ b/app/javascript/menu/navbar.jsx @@ -12,8 +12,6 @@ import { } from 'carbon-components-react/es/components/UIShell'; import { MainMenu } from './main-menu'; -import NotificationDrawer from '../components/notification-drawer/notification-drawer'; -import ToastList from '../components/toast-list/toast-list'; import navbar from '../components/top-navbar'; export const Navbar = (props) => { @@ -46,9 +44,6 @@ export const Navbar = (props) => { Repositories - { notificationDrawer && () } - { toastList && () } - )} /> diff --git a/app/javascript/packs/component-definitions-common.js b/app/javascript/packs/component-definitions-common.js index eb050147eba..0028b2a704c 100644 --- a/app/javascript/packs/component-definitions-common.js +++ b/app/javascript/packs/component-definitions-common.js @@ -21,6 +21,7 @@ import ImportDatastoreViaGit from '../components/automate-import-export-form/imp import MiqAboutModal from '../components/miq-about-modal'; import MiqToolbar from '../components/miq-toolbar'; import { Navbar } from '../menu/navbar'; +import NotificationDrawer from '../components/notification-drawer/notification-drawer'; import OptimizationList from '../optimization/optimization_list'; import OpsTenantForm from '../components/ops-tenant-form/ops-tenant-form'; import OrcherstrationTemplateForm from '../components/orchestration-template/orcherstration-template-form'; @@ -36,6 +37,7 @@ import SetOwnershipForm from '../components/set-ownership-form'; import TableListViewWrapper from '../react/table_list_view_wrapper'; import TaggingWrapperConnected from '../components/taggingWrapper'; import { TagView } from '../tagging'; +import ToastList from '../components/toast-list/toast-list'; import TextualSummaryWrapper from '../react/textual_summary_wrapper'; import VmServerRelationshipForm from '../components/vm-server-relationship-form'; import VmSnapshotForm from '../components/vm-snapshot-form/vm-snapshot-form'; @@ -66,6 +68,7 @@ ManageIQ.component.addReact('ImportDatastoreViaGit', ImportDatastoreViaGit); ManageIQ.component.addReact('MiqAboutModal', MiqAboutModal); ManageIQ.component.addReact('MiqToolbar', MiqToolbar); ManageIQ.component.addReact('Navbar', Navbar); +ManageIQ.component.addReact('NotificationDrawer', NotificationDrawer); ManageIQ.component.addReact('OpsTenantForm', OpsTenantForm); ManageIQ.component.addReact('OptimizationList', OptimizationList); ManageIQ.component.addReact('OrcherstrationTemplateForm', OrcherstrationTemplateForm); @@ -84,6 +87,7 @@ ManageIQ.component.addReact('TagGroup', props => ); ManageIQ.component.addReact('TagView', TagView); ManageIQ.component.addReact('TaggingWrapperConnected', TaggingWrapperConnected); ManageIQ.component.addReact('TextualSummaryWrapper', TextualSummaryWrapper); +ManageIQ.component.addReact('ToastList', ToastList); ManageIQ.component.addReact('Toolbar', Toolbar); ManageIQ.component.addReact('VmServerRelationshipForm', VmServerRelationshipForm); ManageIQ.component.addReact('VmSnapshotForm', VmSnapshotForm); diff --git a/app/stylesheet/carbon.scss b/app/stylesheet/carbon.scss index 69ed4475fe8..0f427a9a13a 100644 --- a/app/stylesheet/carbon.scss +++ b/app/stylesheet/carbon.scss @@ -24,12 +24,17 @@ html { font-size: 100%; } -// FIXME distinguish subsections +// distinguish subsections .bx--side-nav__item .bx--side-nav__item .bx--side-nav__submenu-title { padding-left: 8px; } -// FIXME hack for size differences from original menu +// hack for size differences from original menu .container-pf-nav-pf-vertical { margin-left: 256px !important; } + +// fix notification drawer 0 height +.drawer-pf.drawer-alt-pf.drawer-pf-notifications { + height: 100%; +} diff --git a/app/views/layouts/_header.html.haml b/app/views/layouts/_header.html.haml index db1562c5a5a..a108560283b 100644 --- a/app/views/layouts/_header.html.haml +++ b/app/views/layouts/_header.html.haml @@ -9,10 +9,11 @@ :currentGroup => {:id => current_group.id.to_s, :description => current_group.description}, :userMenu => menu_to_json(:top_right)}, - :notificationDrawer => true, - :toastList => true, :menu => menu_to_json}) -= react('MiqAboutModal', :dialogClassName => Settings.server.custom_login_logo ? 'whitelabel' : '') += react('NotificationDrawer') += react('ToastList') += react('MiqAboutModal', {:dialogClassName => Settings.server.custom_login_logo ? 'whitelabel' : ''}) + = render :partial => "layouts/spinner" = render :partial => "layouts/lightbox_panel" From 0d2b1aeff226dee865edd34f83db7a092a923042 Mon Sep 17 00:00:00 2001 From: Martin Hradil Date: Thu, 9 Apr 2020 12:21:50 +0000 Subject: [PATCH 07/57] NotificationDrawer sizing fix --- app/stylesheet/carbon.scss | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/app/stylesheet/carbon.scss b/app/stylesheet/carbon.scss index 0f427a9a13a..7eca0c43b1b 100644 --- a/app/stylesheet/carbon.scss +++ b/app/stylesheet/carbon.scss @@ -34,7 +34,8 @@ html { margin-left: 256px !important; } -// fix notification drawer 0 height +// fix notification drawer 0 height, hide "padding" under header .drawer-pf.drawer-alt-pf.drawer-pf-notifications { - height: 100%; + top: 48px; + height: calc(100% - 48px); } From 7b1fe7910a94eb16cd15b0b376a0d79592cef6f9 Mon Sep 17 00:00:00 2001 From: Martin Hradil Date: Thu, 9 Apr 2020 13:41:41 +0000 Subject: [PATCH 08/57] Navbar - integrate navbar header --- app/javascript/components/top-navbar/index.js | 2 - .../components/top-navbar/navbar-header.jsx | 48 ------------------- app/javascript/menu/navbar.jsx | 33 ++++++++++--- 3 files changed, 26 insertions(+), 57 deletions(-) delete mode 100644 app/javascript/components/top-navbar/navbar-header.jsx diff --git a/app/javascript/components/top-navbar/index.js b/app/javascript/components/top-navbar/index.js index 67b9b25c32b..ad2850eb1bf 100644 --- a/app/javascript/components/top-navbar/index.js +++ b/app/javascript/components/top-navbar/index.js @@ -2,7 +2,6 @@ import RightSection from './right-section'; import Configuration from './configuration'; import CustomLogo from './custom-logo'; import Help from './help'; -import NavbarHeader from './navbar-header'; import Notifications from './notifications'; import UserOptions from './user-options'; @@ -11,7 +10,6 @@ export default { Configuration, CustomLogo, Help, - NavbarHeader, Notifications, UserOptions, }; diff --git a/app/javascript/components/top-navbar/navbar-header.jsx b/app/javascript/components/top-navbar/navbar-header.jsx deleted file mode 100644 index 9818e08e7dc..00000000000 --- a/app/javascript/components/top-navbar/navbar-header.jsx +++ /dev/null @@ -1,48 +0,0 @@ -import React, { useEffect } from 'react'; -import PropTypes from 'prop-types'; -import { useDispatch, useSelector } from 'react-redux'; -import { toggleVerticalMenuCollapsed } from '../../miq-redux/menu-reducer'; - -const NavbarHeader = ({ - customBrand, - imagePath, -}) => { - const dispatch = useDispatch(); - const isVerticalMenuCollapsed = useSelector(({ menuReducer: { isVerticalMenuCollapsed } }) => isVerticalMenuCollapsed); - useEffect(() => { - window.localStorage.setItem('patternfly-navigation-primary', isVerticalMenuCollapsed ? 'collapsed' : 'expanded'); - }, [isVerticalMenuCollapsed]); - - return ( -
    - - - ManageIQ - -
    - ); -}; - -NavbarHeader.propTypes = { - customBrand: PropTypes.bool.isRequired, - imagePath: PropTypes.string.isRequired, -}; - -export default NavbarHeader; diff --git a/app/javascript/menu/navbar.jsx b/app/javascript/menu/navbar.jsx index 099f166026b..d072bab663a 100644 --- a/app/javascript/menu/navbar.jsx +++ b/app/javascript/menu/navbar.jsx @@ -1,4 +1,5 @@ import React from 'react'; +import PropTypes from 'prop-types'; import { Header, HeaderContainer, @@ -23,29 +24,47 @@ export const Navbar = (props) => { return ( (
    + {/* screenreader link to #main-content */} + {/* mobile, FIXME: use the render params HeaderContainer gives, pass to MainMenu */} - - ManageIQ + {/* FIXME navbar.NavbarHeader */} + + ManageIQ { menu && () } - - // %nav.collapse.navbar-collapse - { rightSection && () } - + {/* TODO... Repositories + */} + + {/* %nav.collapse.navbar-collapse */} + { rightSection && () }
    )} /> ); }; + +Navbar.propTypes = { + customBrand: PropTypes.bool.isRequired, + imagePath: PropTypes.string.isRequired, + //FIXME: the rest +}; From 32c46c0588afba99cbcea1018810b621effbf088 Mon Sep 17 00:00:00 2001 From: Martin Hradil Date: Wed, 29 Apr 2020 19:49:05 +0000 Subject: [PATCH 09/57] _header - no separate helpMenu or userMenu --- app/views/layouts/_header.html.haml | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/app/views/layouts/_header.html.haml b/app/views/layouts/_header.html.haml index a108560283b..e4709350636 100644 --- a/app/views/layouts/_header.html.haml +++ b/app/views/layouts/_header.html.haml @@ -2,13 +2,11 @@ :imagePath => image_path("layout/brand.svg"), :rightSection => {:customLogo => ::Settings.server.custom_logo, :currentUser => current_user, - :helpMenu => menu_to_json(:help), :opsExplorerAllowed => ApplicationHelper.role_allows?(:feature => 'ops_explorer',:any => true), :applianceName => appliance_name, :miqGroups => current_user.miq_groups.map { |group| { :id => group.id.to_s, :description => group.description } }, :currentGroup => {:id => current_group.id.to_s, - :description => current_group.description}, - :userMenu => menu_to_json(:top_right)}, + :description => current_group.description}}, :menu => menu_to_json}) = react('NotificationDrawer') From 1abac90544e14bd0826b5901090a756ac9d0ab2d Mon Sep 17 00:00:00 2001 From: Martin Hradil Date: Wed, 29 Apr 2020 22:07:58 +0000 Subject: [PATCH 10/57] Menu - merge help and settings sections also adds the Configuration button back to the menu, this time as Application Settings --- app/presenters/menu/default_menu.rb | 22 +++++++++++++++------- app/views/layouts/_header.html.haml | 1 - 2 files changed, 15 insertions(+), 8 deletions(-) diff --git a/app/presenters/menu/default_menu.rb b/app/presenters/menu/default_menu.rb index 8ad8ca8c5e4..e89f33a97a6 100644 --- a/app/presenters/menu/default_menu.rb +++ b/app/presenters/menu/default_menu.rb @@ -19,7 +19,7 @@ def default_menu monitor_menu_section, settings_menu_section, - help_menu_section, + logout_item, ] end @@ -265,10 +265,14 @@ def monitor_menu_section end def settings_menu_section - Menu::Section.new(:set, N_("User Settings"), 'pficon pficon-settings', [ - Menu::Item.new('configuration', N_('My Settings'), 'my_settings', {:feature => 'my_settings', :any => true}, '/configuration/index'), - Menu::Item.new('my_tasks', N_('Tasks'), 'tasks', {:feature => 'tasks', :any => true}, '/miq_task/index?jobs_tab=tasks') - ], :top_right) + Menu::Section.new(:set, N_("Settings"), 'pficon pficon-settings', [ + Menu::Item.new('configuration', N_('My Settings'), 'my_settings', {:feature => 'my_settings', :any => true}, '/configuration/index'), + Menu::Item.new('my_tasks', N_('Tasks'), 'tasks', {:feature => 'tasks', :any => true}, '/miq_task/index?jobs_tab=tasks'), + help_documentation, + Menu::Item.new('ops_explorer', N_('Application Settings'), 'ops_explorer', {:feature => 'ops_explorer', :any => true}, '/ops/explorer'), + help_product, + help_about, + ]) end def help_documentation @@ -293,8 +297,12 @@ def help_menu_items ] end - def help_menu_section - Menu::Section.new(:help, N_('Help'), 'pficon pficon-help', help_menu_items, :help) + def logout_item + Menu::Item.new(:logout, + N_("Logout"), + 'logout', + nil, + '/dashboard/logout') end private diff --git a/app/views/layouts/_header.html.haml b/app/views/layouts/_header.html.haml index e4709350636..5aa57a5a8c0 100644 --- a/app/views/layouts/_header.html.haml +++ b/app/views/layouts/_header.html.haml @@ -2,7 +2,6 @@ :imagePath => image_path("layout/brand.svg"), :rightSection => {:customLogo => ::Settings.server.custom_logo, :currentUser => current_user, - :opsExplorerAllowed => ApplicationHelper.role_allows?(:feature => 'ops_explorer',:any => true), :applianceName => appliance_name, :miqGroups => current_user.miq_groups.map { |group| { :id => group.id.to_s, :description => group.description } }, :currentGroup => {:id => current_group.id.to_s, From 00474816d925e0f7d4d49b00dcc7bf2c3ddf1189 Mon Sep 17 00:00:00 2001 From: Martin Hradil Date: Wed, 29 Apr 2020 22:21:15 +0000 Subject: [PATCH 11/57] TopNavbar - remove old configuration, help menu, user menu code --- .../components/top-navbar/configuration.jsx | 24 - app/javascript/components/top-navbar/help.jsx | 67 -- app/javascript/components/top-navbar/index.js | 4 - .../components/top-navbar/recursive-props.js | 43 - .../components/top-navbar/right-section.jsx | 30 +- .../components/top-navbar/user-options.jsx | 34 +- app/javascript/menu/navbar.jsx | 1 - .../__snapshots__/top-navbar.spec.js.snap | 874 ------------------ .../spec/top-navbar/top-navbar.spec.js | 211 ----- 9 files changed, 10 insertions(+), 1278 deletions(-) delete mode 100644 app/javascript/components/top-navbar/configuration.jsx delete mode 100644 app/javascript/components/top-navbar/help.jsx delete mode 100644 app/javascript/components/top-navbar/recursive-props.js delete mode 100644 app/javascript/spec/top-navbar/__snapshots__/top-navbar.spec.js.snap delete mode 100644 app/javascript/spec/top-navbar/top-navbar.spec.js diff --git a/app/javascript/components/top-navbar/configuration.jsx b/app/javascript/components/top-navbar/configuration.jsx deleted file mode 100644 index 084ec4dcae3..00000000000 --- a/app/javascript/components/top-navbar/configuration.jsx +++ /dev/null @@ -1,24 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; - -const Configuration = ({ - opsExplorerAllowed, -}) => ( - opsExplorerAllowed && ( -
  • - - - -
  • - ) -); - -Configuration.propTypes = { - opsExplorerAllowed: PropTypes.bool.isRequired, -}; - -export default Configuration; diff --git a/app/javascript/components/top-navbar/help.jsx b/app/javascript/components/top-navbar/help.jsx deleted file mode 100644 index b45bab78865..00000000000 --- a/app/javascript/components/top-navbar/help.jsx +++ /dev/null @@ -1,67 +0,0 @@ -import React from 'react'; -import { Dropdown, Icon, MenuItem } from 'patternfly-react'; -import PropTypes from 'prop-types'; -import { helpMenuProps, recursiveHelpMenuProps } from './recursive-props'; -import { linkProps } from '../../menu/item-type'; - -const Help = ({ - helpMenu, -}) => ( - helpMenu.map(section => ( - section.visible && ( - ( -
  • - {children} -
  • - ) - } - > - ( - - {children} - - ) - } - > - - - - {section.items.map(item => ( - item.visible && ( - - {item.title} - - ) - ))} - -
    - ))) -); - -Help.propTypes = { - helpMenu: PropTypes.arrayOf(PropTypes.shape({ - ...helpMenuProps, - items: PropTypes.arrayOf(PropTypes.shape(recursiveHelpMenuProps)), - })).isRequired, -}; - -export default Help; diff --git a/app/javascript/components/top-navbar/index.js b/app/javascript/components/top-navbar/index.js index ad2850eb1bf..8aaf812fcd9 100644 --- a/app/javascript/components/top-navbar/index.js +++ b/app/javascript/components/top-navbar/index.js @@ -1,15 +1,11 @@ import RightSection from './right-section'; -import Configuration from './configuration'; import CustomLogo from './custom-logo'; -import Help from './help'; import Notifications from './notifications'; import UserOptions from './user-options'; export default { RightSection, - Configuration, CustomLogo, - Help, Notifications, UserOptions, }; diff --git a/app/javascript/components/top-navbar/recursive-props.js b/app/javascript/components/top-navbar/recursive-props.js deleted file mode 100644 index f7060665fdb..00000000000 --- a/app/javascript/components/top-navbar/recursive-props.js +++ /dev/null @@ -1,43 +0,0 @@ -import PropTypes from 'prop-types'; - -export const groupProps = { - description: PropTypes.string.isRequired, - id: PropTypes.string.isRequired, -}; - -export const helpMenuProps = { - id: PropTypes.string.isRequired, - title: PropTypes.string.isRequired, - type: PropTypes.string.isRequired, - visible: PropTypes.bool, - href: PropTypes.string.isRequired, -}; - -export const recursiveHelpMenuProps = { - ...helpMenuProps, - items: PropTypes.arrayOf( - PropTypes.shape( - { - ...helpMenuProps, - }, - ), - ), -}; - -export const userMenuProps = { - id: PropTypes.string.isRequired, - title: PropTypes.string.isRequired, - visible: PropTypes.bool, - href: PropTypes.string, -}; - -export const recursiveUserMenuProps = { - ...userMenuProps, - items: PropTypes.arrayOf( - PropTypes.shape( - { - ...helpMenuProps, - }, - ), - ), -}; diff --git a/app/javascript/components/top-navbar/right-section.jsx b/app/javascript/components/top-navbar/right-section.jsx index b16df628f29..4b195d018e3 100644 --- a/app/javascript/components/top-navbar/right-section.jsx +++ b/app/javascript/components/top-navbar/right-section.jsx @@ -2,24 +2,17 @@ import React from 'react'; import PropTypes from 'prop-types'; import CustomLogo from './custom-logo'; import Notifications from './notifications'; -import Help from './help'; -import Configuration from './configuration'; import UserOptions from './user-options'; -import { - groupProps, helpMenuProps, recursiveHelpMenuProps, userMenuProps, recursiveUserMenuProps, -} from './recursive-props'; const RightSection = ({ - customLogo, currentUser, helpMenu, opsExplorerAllowed, applianceName, miqGroups, currentGroup, userMenu, + customLogo, currentUser, applianceName, miqGroups, currentGroup }) => (
      - { currentUser && ( - - + )}
    @@ -31,24 +24,9 @@ RightSection.propTypes = { name: PropTypes.string.isRequired, userid: PropTypes.string.isRequired, }).isRequired, - opsExplorerAllowed: PropTypes.bool.isRequired, applianceName: PropTypes.string.isRequired, - miqGroups: PropTypes.arrayOf( - PropTypes.shape({ - ...groupProps, - }).isRequired, - ).isRequired, - currentGroup: PropTypes.shape({ - ...groupProps, - }).isRequired, - helpMenu: PropTypes.arrayOf(PropTypes.shape({ - ...helpMenuProps, - items: PropTypes.arrayOf(PropTypes.shape(recursiveHelpMenuProps)), - })).isRequired, - userMenu: PropTypes.arrayOf(PropTypes.shape({ - ...userMenuProps, - items: PropTypes.arrayOf(PropTypes.shape(recursiveUserMenuProps)), - })).isRequired, + miqGroups: PropTypes.any, + currentGroup: PropTypes.any, }; export default RightSection; diff --git a/app/javascript/components/top-navbar/user-options.jsx b/app/javascript/components/top-navbar/user-options.jsx index 07a12cc5e3e..03b6565d7de 100644 --- a/app/javascript/components/top-navbar/user-options.jsx +++ b/app/javascript/components/top-navbar/user-options.jsx @@ -1,13 +1,9 @@ import React from 'react'; import { Dropdown, Icon, MenuItem } from 'patternfly-react'; import PropTypes from 'prop-types'; -import { - groupProps, recursiveUserMenuProps, userMenuProps, -} from './recursive-props'; - const UserOptions = ({ - currentUser, applianceName, miqGroups, currentGroup, userMenu, + currentUser, applianceName, miqGroups, currentGroup, }) => ( {(currentGroup).description}
    ) } - {userMenu.map(section => ( - section.visible && ( - - - { section.items.map(item => ( - item.visible && ( - !miqCheckForChanges() && event.preventDefault()} - > - {item.title} - - ) - ))} - - ) - ))} ); +const groupProps = { + description: PropTypes.string.isRequired, + id: PropTypes.string.isRequired, +}; + UserOptions.propTypes = { currentUser: PropTypes.shape({ name: PropTypes.string.isRequired, @@ -133,10 +115,6 @@ UserOptions.propTypes = { currentGroup: PropTypes.shape({ ...groupProps, }).isRequired, - userMenu: PropTypes.arrayOf(PropTypes.shape({ - ...userMenuProps, - items: PropTypes.arrayOf(PropTypes.shape(recursiveUserMenuProps)), - })).isRequired, }; export default UserOptions; diff --git a/app/javascript/menu/navbar.jsx b/app/javascript/menu/navbar.jsx index d072bab663a..d2939eabbfd 100644 --- a/app/javascript/menu/navbar.jsx +++ b/app/javascript/menu/navbar.jsx @@ -18,7 +18,6 @@ import navbar from '../components/top-navbar'; export const Navbar = (props) => { const { customBrand, imagePath } = props; const { rightSection } = props; - const { notificationDrawer, toastList } = props; const { menu } = props; return ( diff --git a/app/javascript/spec/top-navbar/__snapshots__/top-navbar.spec.js.snap b/app/javascript/spec/top-navbar/__snapshots__/top-navbar.spec.js.snap deleted file mode 100644 index 0aed93c86c3..00000000000 --- a/app/javascript/spec/top-navbar/__snapshots__/top-navbar.spec.js.snap +++ /dev/null @@ -1,874 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`Top navbar tests should render correctly 1`] = ` - - - -
  • - - -
  • - - - - - - - - - - -
  • - - - - - - -
  • - - - - - - - - -
  • -
    -
    -
    -
    -
    - -
  • - - - -
  • -
    - - - - - -
  • - - - - - - - - -
  • -
    -
    -
    -
    -
    -
    -
    -`; diff --git a/app/javascript/spec/top-navbar/top-navbar.spec.js b/app/javascript/spec/top-navbar/top-navbar.spec.js deleted file mode 100644 index 4a087e096af..00000000000 --- a/app/javascript/spec/top-navbar/top-navbar.spec.js +++ /dev/null @@ -1,211 +0,0 @@ -import React from 'react'; -import { mount } from 'enzyme'; -import toJson from 'enzyme-to-json'; -import configureStore from 'redux-mock-store'; -import { Provider } from 'react-redux'; -import RightSection from '../../components/top-navbar/right-section'; -import Help from '../../components/top-navbar/help'; -import Notifications from '../../components/top-navbar/notifications'; -import CustomLogo from '../../components/top-navbar/custom-logo'; -import UserOptions from '../../components/top-navbar/user-options'; -import '../helpers/sprintf'; -import '../helpers/sendDataWithRx'; -import '../helpers/miqCheckForChanges'; -import '../helpers/miqChangeGroup'; - -describe('Top navbar tests', () => { - let customLogo; - let currentUser; - let helpMenu; - let opsExplorerAllowed; - let applianceName; - let miqGroups; - let currentGroup; - let userMenu; - const initialState = { - notificationReducer: { - unreadCount: 1, - isDrawerVisible: false, - }, - }; - const mockStore = configureStore(); - const sendDataWithRxSpy = jest.spyOn(window, 'sendDataWithRx'); - const miqCheckForChangesSpy = jest.spyOn(window, 'miqCheckForChanges'); - const miqChangeGroupSpy = jest.spyOn(window, 'miqChangeGroup'); - - beforeEach(() => { - customLogo = true; - currentUser = { - name: 'Administrator', - userid: 'admin', - }; - helpMenu = [ - { - id: 'help', - title: 'Help', - type: 'default', - items: [ - { - id: 'documentation', - title: 'Documentation', - type: 'default', - items: [], - visible: true, - href: '/support/index?support_tab=about', - }, - { - id: 'about', - title: 'About', - type: 'modal', - items: [], - visible: true, - href: 'javascript:void(0);', - }, - ], - visible: true, - href: '/dashboard/maintab/?tab=help', - }, - ]; - opsExplorerAllowed = true; - applianceName = 'EVM'; - miqGroups = [ - { - id: '10000000000002', - description: 'EvmGroup-super_administrator', - }, - { - id: '10000000000003', - description: 'EvmGroup-dev', - }, - ]; - currentGroup = { - id: '10000000000002', - description: 'EvmGroup-super_administrator', - }; - userMenu = [ - { - id: 'set', - title: 'User Settings', - href: null, - items: [ - { - id: 'configuration', - title: 'My Settings', - href: '/configuration/index', - items: [], - visible: true, - }, - { - id: 'my_tasks', - title: 'Tasks', - href: '/miq_task/index?jobs_tab=tasks', - items: [], - visible: true, - }, - ], - visible: true, - }, - ]; - }); - - afterEach(() => { - sendDataWithRxSpy.mockReset(); - miqCheckForChangesSpy.mockReset(); - miqChangeGroupSpy.mockReset(); - }); - - it('should render correctly', () => { - const store = mockStore({ ...initialState }); - const wrapper = mount( - - - , - ); - expect(toJson(wrapper)).toMatchSnapshot(); - }); - - it('should call sendDataWithRx after click on help menu item with type "modal"', () => { - const wrapper = mount( - , - ); - wrapper.find('a#help-menu-about').simulate('click'); - expect(sendDataWithRxSpy).toHaveBeenCalledWith({ type: 'showAboutModal' }); - }); - - it('should call miqCheckForChanges after click on help menu item with type "default"', () => { - const wrapper = mount( - , - ); - wrapper.find('a#help-menu-documentation').simulate('click'); - expect(miqCheckForChangesSpy).toHaveBeenCalled(); - }); - - it('should dispatch toggleDrawerVisibility after click on notiffication button', () => { - const store = mockStore({ ...initialState }); - const wrapper = mount( - - - , - ); - wrapper.find('a#notifications-btn').simulate('click'); - const expectedPayload = { type: '@@notifications/toggleDrawerVisibility' }; - expect(store.getActions()).toEqual([expectedPayload]); - }); - - it('should not render custom logo when disabled', () => { - const wrapper = mount( - , - ); - expect(wrapper.find('li.dropdown.brand-white-label')).toHaveLength(1); - }); - - it('should not render badge when no unread notifications', () => { - const store = mockStore({ ...initialState, notificationReducer: { unreadCount: 0 } }); - const wrapper = mount( - - - , - ); - expect(wrapper.find('span.badge.badge-pf-bordered').text()).toEqual(''); - }); - - it('should call miqChangeGroup after click on inactive group', () => { - const wrapper = mount( - , - ); - wrapper.find('a#EvmGroup-dev').simulate('click'); - expect(miqChangeGroupSpy).toHaveBeenCalled(); - }); - - it('should render disabled item in case of one group', () => { - const wrapper = mount( - , - ); - expect(wrapper.find('a#single-group-item')).toHaveLength(1); - }); - - it('should call miqCheckForChanges after click on user menu item', () => { - const wrapper = mount( - , - ); - wrapper.find('a#user-menu-tasks').simulate('click'); - expect(miqCheckForChangesSpy).toHaveBeenCalled(); - }); - - it('should call miqCheckForChanges before logout', () => { - const wrapper = mount( - , - ); - wrapper.find('a#logout-btn').simulate('click'); - expect(miqCheckForChangesSpy).toHaveBeenCalled(); - }); -}); From f26ea5441fd4be147d6fb6c60076314a1c5fbe67 Mon Sep 17 00:00:00 2001 From: Martin Hradil Date: Fri, 1 May 2020 17:24:15 +0000 Subject: [PATCH 12/57] Notifications - move toggle to breadcrumbs, fix styling make the breadcrumbs narrower, use the rest for the notification button, the badge styling didn't work, replaced with color for the bell when unread no margins would be nice but we have no such buttons --- app/assets/stylesheets/notifications.scss | 22 +++++++++++++ .../components/breadcrumbs/index.jsx | 20 ++++++----- .../breadcrumbs/notifications-toggle.jsx | 33 +++++++++++++++++++ .../components/notification-drawer/helpers.js | 8 ----- .../components/top-navbar/notifications.jsx | 32 ------------------ 5 files changed, 67 insertions(+), 48 deletions(-) create mode 100644 app/javascript/components/breadcrumbs/notifications-toggle.jsx delete mode 100644 app/javascript/components/top-navbar/notifications.jsx diff --git a/app/assets/stylesheets/notifications.scss b/app/assets/stylesheets/notifications.scss index 5ee6e32e181..efe9b858178 100644 --- a/app/assets/stylesheets/notifications.scss +++ b/app/assets/stylesheets/notifications.scss @@ -52,3 +52,25 @@ } } } + +// display under button +.drawer-pf.drawer-alt-pf.drawer-pf-notifications { + top: 38px; + height: calc(100% - 44px); +} + +#notifications-toggle { + display: inline-block; + // FIXME flex? + width: 140px; + + .unread { + color: #39a5dc; + } +} + +#breadcrumbs .breadcrumb { + display: inline-block; + margin-bottom: 0; + width: calc(100% - 140px); +} diff --git a/app/javascript/components/breadcrumbs/index.jsx b/app/javascript/components/breadcrumbs/index.jsx index 150a1aa762e..99643ddfd3d 100644 --- a/app/javascript/components/breadcrumbs/index.jsx +++ b/app/javascript/components/breadcrumbs/index.jsx @@ -3,6 +3,7 @@ import PropTypes from 'prop-types'; import { Breadcrumb } from 'patternfly-react'; import { unescape } from 'lodash'; +import { NotificationsToggle } from './notifications-toggle'; import { onClickTree, onClick, onClickToExplorer } from './on-click-functions'; // FIXME: don't parse html here @@ -45,14 +46,17 @@ const renderItems = ({ items, controllerName }) => { }; const Breadcrumbs = ({ items, title, controllerName }) => ( - - {items && renderItems({ items, controllerName })} - - - {items && items.length > 0 ? parsedText(items[items.length - 1].title) : parsedText(title)} - - - + <> + + {items && renderItems({ items, controllerName })} + + + {items && items.length > 0 ? parsedText(items[items.length - 1].title) : parsedText(title)} + + + + + ); Breadcrumbs.propTypes = { diff --git a/app/javascript/components/breadcrumbs/notifications-toggle.jsx b/app/javascript/components/breadcrumbs/notifications-toggle.jsx new file mode 100644 index 00000000000..d2a71db309c --- /dev/null +++ b/app/javascript/components/breadcrumbs/notifications-toggle.jsx @@ -0,0 +1,33 @@ +import React, { useEffect } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; +import { TOGGLE_DRAWER_VISIBILITY } from '../../miq-redux/actions/notifications-actions'; + +export const NotificationsToggle = () => { + const dispatch = useDispatch(); + const { isDrawerVisible, unreadCount } = useSelector(({ notificationReducer }) => notificationReducer); + + useEffect(() => { + window.localStorage.setItem('miq-notification-drawer-shown', isDrawerVisible ? 'true' : 'false'); + }, [isDrawerVisible]); + + const toggle = () => { + dispatch({ type: TOGGLE_DRAWER_VISIBILITY }); + }; + + const unreadCountText = function(count) { + return sprintf(n__('%d unread notification', '%d unread notifications', count), count); + }; + + return ( + + {__("Notifications")} +   + + + ); +}; diff --git a/app/javascript/components/notification-drawer/helpers.js b/app/javascript/components/notification-drawer/helpers.js index b44f72b5b6b..4496d8c279e 100644 --- a/app/javascript/components/notification-drawer/helpers.js +++ b/app/javascript/components/notification-drawer/helpers.js @@ -1,11 +1,3 @@ -export const saveNotificationDrawerVisibility = (isDrawerVisible) => { - window.localStorage.setItem('miq-notification-drawer-shown', isDrawerVisible ? 'true' : 'false'); -}; - -export const unreadCountText = function(count) { - return sprintf(n__('%d unread notification', '%d unread notifications', count), count); -}; - export const newCountText = function(count) { return sprintf(n__('%d New', '%d New', count), count); }; diff --git a/app/javascript/components/top-navbar/notifications.jsx b/app/javascript/components/top-navbar/notifications.jsx deleted file mode 100644 index 0578e8f316f..00000000000 --- a/app/javascript/components/top-navbar/notifications.jsx +++ /dev/null @@ -1,32 +0,0 @@ -import React, { useEffect } from 'react'; -import { Icon } from 'patternfly-react'; -import { useDispatch, useSelector } from 'react-redux'; -import { saveNotificationDrawerVisibility, unreadCountText } from '../notification-drawer/helpers'; -import { TOGGLE_DRAWER_VISIBILITY } from '../../miq-redux/actions/notifications-actions'; - -const Notifications = () => { - const dispatch = useDispatch(); - const { isDrawerVisible, unreadCount } = useSelector(({ notificationReducer }) => notificationReducer); - - useEffect(() => { - saveNotificationDrawerVisibility(isDrawerVisible); - }, [isDrawerVisible]); - - return ( -
  • - { - dispatch({ type: TOGGLE_DRAWER_VISIBILITY }); - }} - > - - {unreadCount > 0 ? ' ' : ''} - -
  • - ); -}; - -export default Notifications; From 63dfcc81ecff13929d191c2f6ab672d85122f71c Mon Sep 17 00:00:00 2001 From: Martin Hradil Date: Fri, 1 May 2020 17:37:22 +0000 Subject: [PATCH 13/57] menu.{Navbar,MainMenu} - separate, move everything to the right place --- .../components/top-navbar/custom-logo.jsx | 14 ---- app/javascript/components/top-navbar/index.js | 11 --- .../components/top-navbar/right-section.jsx | 32 -------- app/javascript/menu/index.js | 2 + app/javascript/menu/main-menu.jsx | 52 ++++++++++++- app/javascript/menu/miq-logo.jsx | 30 ++++++++ app/javascript/menu/navbar.jsx | 73 ++----------------- .../top-navbar => menu}/user-options.jsx | 50 +------------ .../packs/component-definitions-common.js | 5 +- app/views/layouts/_header.html.haml | 26 ++++--- 10 files changed, 112 insertions(+), 183 deletions(-) delete mode 100644 app/javascript/components/top-navbar/custom-logo.jsx delete mode 100644 app/javascript/components/top-navbar/index.js delete mode 100644 app/javascript/components/top-navbar/right-section.jsx create mode 100644 app/javascript/menu/index.js create mode 100644 app/javascript/menu/miq-logo.jsx rename app/javascript/{components/top-navbar => menu}/user-options.jsx (63%) diff --git a/app/javascript/components/top-navbar/custom-logo.jsx b/app/javascript/components/top-navbar/custom-logo.jsx deleted file mode 100644 index 2172f7f5676..00000000000 --- a/app/javascript/components/top-navbar/custom-logo.jsx +++ /dev/null @@ -1,14 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; - -const CustomLogo = ({ - customLogo, -}) => ( -
  • -); - -CustomLogo.propTypes = { - customLogo: PropTypes.bool.isRequired, -}; - -export default CustomLogo; diff --git a/app/javascript/components/top-navbar/index.js b/app/javascript/components/top-navbar/index.js deleted file mode 100644 index 8aaf812fcd9..00000000000 --- a/app/javascript/components/top-navbar/index.js +++ /dev/null @@ -1,11 +0,0 @@ -import RightSection from './right-section'; -import CustomLogo from './custom-logo'; -import Notifications from './notifications'; -import UserOptions from './user-options'; - -export default { - RightSection, - CustomLogo, - Notifications, - UserOptions, -}; diff --git a/app/javascript/components/top-navbar/right-section.jsx b/app/javascript/components/top-navbar/right-section.jsx deleted file mode 100644 index 4b195d018e3..00000000000 --- a/app/javascript/components/top-navbar/right-section.jsx +++ /dev/null @@ -1,32 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import CustomLogo from './custom-logo'; -import Notifications from './notifications'; -import UserOptions from './user-options'; - -const RightSection = ({ - customLogo, currentUser, applianceName, miqGroups, currentGroup -}) => ( -
      - - - { currentUser && ( - - - - )} -
    -); - -RightSection.propTypes = { - customLogo: PropTypes.bool.isRequired, - currentUser: PropTypes.shape({ - name: PropTypes.string.isRequired, - userid: PropTypes.string.isRequired, - }).isRequired, - applianceName: PropTypes.string.isRequired, - miqGroups: PropTypes.any, - currentGroup: PropTypes.any, -}; - -export default RightSection; diff --git a/app/javascript/menu/index.js b/app/javascript/menu/index.js new file mode 100644 index 00000000000..a19b811ca4d --- /dev/null +++ b/app/javascript/menu/index.js @@ -0,0 +1,2 @@ +export { Navbar } from './navbar'; +export { MainMenu } from './main-menu'; diff --git a/app/javascript/menu/main-menu.jsx b/app/javascript/menu/main-menu.jsx index e287613da70..fa401a254bc 100644 --- a/app/javascript/menu/main-menu.jsx +++ b/app/javascript/menu/main-menu.jsx @@ -7,6 +7,8 @@ import { SideNavMenuItem, } from 'carbon-components-react/es/components/UIShell'; +import { MiqLogo } from './miq-logo'; +import { UserOptions } from './user-options'; import { itemId, linkProps } from './item-type'; @@ -59,20 +61,66 @@ MenuSection.props = { }; -export const MainMenu = ({ menu }) => { +const MenuFind = () => ( + //TODO + +); + +const MenuCollapse = ({ collapsed }) => ( + // TODO +
    {collapsed ? '>' : '<'}
    +); + + +export const MainMenu = (props) => { + const { applianceName, currentGroup, currentUser, customBrand, customLogo, imagePath, menu, miqGroups } = props; + return ( + + + + +
    + {mapItems(menu)} + +
    ); }; +const propGroup = PropTypes.shape({ + description: PropTypes.string.isRequired, + id: PropTypes.string.isRequired, +}); + +const propUser = PropTypes.shape({ + name: PropTypes.string.isRequired, + userid: PropTypes.string.isRequired, +}); + MainMenu.propTypes = { + applianceName: PropTypes.string.isRequired, + currentGroup: propGroup.isRequired, + currentUser: propUser.isRequired, + customBrand: PropTypes.bool.isRequired, + customLogo: PropTypes.bool.isRequired, + imagePath: PropTypes.string.isRequired, menu: PropTypes.arrayOf(PropTypes.any).isRequired, + miqGroups: PropTypes.arrayOf(propGroup).isRequired, }; diff --git a/app/javascript/menu/miq-logo.jsx b/app/javascript/menu/miq-logo.jsx new file mode 100644 index 00000000000..566a257d4ca --- /dev/null +++ b/app/javascript/menu/miq-logo.jsx @@ -0,0 +1,30 @@ +import React from 'react'; +import PropTypes from 'prop-types'; + +export const MiqLogo = ({href, title, alt, customBrand, imagePath}) => ( + + {alt} + +); + +MiqLogo.propTypes = { + alt: PropTypes.string, + customBrand: PropTypes.bool, + href: PropTypes.string, + imagePath: PropTypes.string.isRequired, + title: PropTypes.string, +}; + +MiqLogo.defaultProps = { + alt: 'ManageIQ', + customBrand: false, + href: '/dashboard/start_url', + title: __('Go to my start page'), +}; diff --git a/app/javascript/menu/navbar.jsx b/app/javascript/menu/navbar.jsx index d2939eabbfd..6ca531d204c 100644 --- a/app/javascript/menu/navbar.jsx +++ b/app/javascript/menu/navbar.jsx @@ -1,69 +1,10 @@ import React from 'react'; import PropTypes from 'prop-types'; -import { - Header, - HeaderContainer, - HeaderName, - HeaderNavigation, - HeaderMenuItem, - HeaderMenuButton, - HeaderGlobalBar, - HeaderGlobalAction, - SkipToContent, -} from 'carbon-components-react/es/components/UIShell'; -import { MainMenu } from './main-menu'; -import navbar from '../components/top-navbar'; - -export const Navbar = (props) => { - const { customBrand, imagePath } = props; - const { rightSection } = props; - const { menu } = props; - - return ( - ( -
    - {/* screenreader link to #main-content */} - - - {/* mobile, FIXME: use the render params HeaderContainer gives, pass to MainMenu */} - - - {/* FIXME navbar.NavbarHeader */} - - ManageIQ - - - { menu && () } - - {/* TODO... - - Repositories - - */} - - - - {/* %nav.collapse.navbar-collapse */} - { rightSection && () } -
    - )} /> - ); -}; - -Navbar.propTypes = { - customBrand: PropTypes.bool.isRequired, - imagePath: PropTypes.string.isRequired, - //FIXME: the rest -}; +export const Navbar = () => ( + +); diff --git a/app/javascript/components/top-navbar/user-options.jsx b/app/javascript/menu/user-options.jsx similarity index 63% rename from app/javascript/components/top-navbar/user-options.jsx rename to app/javascript/menu/user-options.jsx index 03b6565d7de..81ad8a0dcf7 100644 --- a/app/javascript/components/top-navbar/user-options.jsx +++ b/app/javascript/menu/user-options.jsx @@ -1,12 +1,8 @@ import React from 'react'; -import { Dropdown, Icon, MenuItem } from 'patternfly-react'; -import PropTypes from 'prop-types'; +import { Dropdown, MenuItem } from 'patternfly-react'; -const UserOptions = ({ - currentUser, applianceName, miqGroups, currentGroup, -}) => ( +export const UserOptions = ({ currentUser, applianceName, miqGroups, currentGroup }) => ( (
  • - +

    {`${currentUser.name} | ${applianceName}`}

    {sprintf(__('Server: %s'), applianceName)} { miqGroups.length > 1 ? ( -
  • +
  • {(currentGroup).description} ) } - - { - if (miqCheckForChanges()) { - ManageIQ.logoutInProgress = true; - } else { - e.preventDefault(); - } - }} - title={__('Click to Logout')} - > - {__('Logout')} - ); - -const groupProps = { - description: PropTypes.string.isRequired, - id: PropTypes.string.isRequired, -}; - -UserOptions.propTypes = { - currentUser: PropTypes.shape({ - name: PropTypes.string.isRequired, - userid: PropTypes.string.isRequired, - }).isRequired, - applianceName: PropTypes.string.isRequired, - miqGroups: PropTypes.arrayOf( - PropTypes.shape({ - ...groupProps, - }).isRequired, - ).isRequired, - currentGroup: PropTypes.shape({ - ...groupProps, - }).isRequired, -}; - -export default UserOptions; diff --git a/app/javascript/packs/component-definitions-common.js b/app/javascript/packs/component-definitions-common.js index 0028b2a704c..f36e3534f4b 100644 --- a/app/javascript/packs/component-definitions-common.js +++ b/app/javascript/packs/component-definitions-common.js @@ -20,7 +20,7 @@ import FonticonPicker from '../components/fonticon-picker'; import ImportDatastoreViaGit from '../components/automate-import-export-form/import-datastore-via-git'; import MiqAboutModal from '../components/miq-about-modal'; import MiqToolbar from '../components/miq-toolbar'; -import { Navbar } from '../menu/navbar'; +import { MainMenu, Navbar } from '../menu'; import NotificationDrawer from '../components/notification-drawer/notification-drawer'; import OptimizationList from '../optimization/optimization_list'; import OpsTenantForm from '../components/ops-tenant-form/ops-tenant-form'; @@ -67,7 +67,8 @@ ManageIQ.component.addReact('FonticonPicker', FonticonPicker); ManageIQ.component.addReact('ImportDatastoreViaGit', ImportDatastoreViaGit); ManageIQ.component.addReact('MiqAboutModal', MiqAboutModal); ManageIQ.component.addReact('MiqToolbar', MiqToolbar); -ManageIQ.component.addReact('Navbar', Navbar); +ManageIQ.component.addReact('menu.MainMenu', MainMenu); +ManageIQ.component.addReact('menu.Navbar', Navbar); ManageIQ.component.addReact('NotificationDrawer', NotificationDrawer); ManageIQ.component.addReact('OpsTenantForm', OpsTenantForm); ManageIQ.component.addReact('OptimizationList', OptimizationList); diff --git a/app/views/layouts/_header.html.haml b/app/views/layouts/_header.html.haml index 5aa57a5a8c0..0b59baf1b5c 100644 --- a/app/views/layouts/_header.html.haml +++ b/app/views/layouts/_header.html.haml @@ -1,16 +1,22 @@ -= react('Navbar', {:customBrand => ::Settings.server.custom_brand, - :imagePath => image_path("layout/brand.svg"), - :rightSection => {:customLogo => ::Settings.server.custom_logo, - :currentUser => current_user, - :applianceName => appliance_name, - :miqGroups => current_user.miq_groups.map { |group| { :id => group.id.to_s, :description => group.description } }, - :currentGroup => {:id => current_group.id.to_s, - :description => current_group.description}}, - :menu => menu_to_json}) +- user = {:name => current_user.name, + :userid => current_user.userid} +- group = {:id => current_group.id.to_s, + :description => current_group.description} +- groups = current_user.miq_groups.map { |group| { :id => group.id.to_s, :description => group.description } } + += react('menu.Navbar') += react('menu.MainMenu', {:applianceName => appliance_name, + :currentGroup => group, + :currentUser => user, + :customBrand => ::Settings.server.custom_brand, + :customLogo => ::Settings.server.custom_logo, + :imagePath => image_path("layout/brand.svg"), + :menu => menu_to_json, + :miqGroups => groups}, :id => 'main-menu') = react('NotificationDrawer') = react('ToastList') -= react('MiqAboutModal', {:dialogClassName => Settings.server.custom_login_logo ? 'whitelabel' : ''}) += react('MiqAboutModal', {:dialogClassName => ::Settings.server.custom_login_logo ? 'whitelabel' : ''}) = render :partial => "layouts/spinner" = render :partial => "layouts/lightbox_panel" From 848608ad1fb8bed0f364431e6bb05a6bbf1bafd0 Mon Sep 17 00:00:00 2001 From: Martin Hradil Date: Fri, 1 May 2020 18:02:10 +0000 Subject: [PATCH 14/57] Remove obsolete styles - masthead, custom_logo, vertical_navigation and move the menu and notification styling to webpack --- app/assets/stylesheets/application.scss | 4 - app/assets/stylesheets/header_background.scss | 10 - app/assets/stylesheets/masthead.scss | 34 --- app/assets/stylesheets/service_dialogs.css | 0 .../stylesheets/vertical_navigation.scss | 211 ------------------ .../components/breadcrumbs/index.jsx | 2 +- app/javascript/menu/navbar.jsx | 9 +- app/stylesheet/application-webpack.scss | 3 + app/stylesheet/navbar.scss | 3 + .../notifications.scss | 0 10 files changed, 9 insertions(+), 267 deletions(-) delete mode 100644 app/assets/stylesheets/header_background.scss delete mode 100644 app/assets/stylesheets/masthead.scss delete mode 100644 app/assets/stylesheets/service_dialogs.css delete mode 100644 app/assets/stylesheets/vertical_navigation.scss create mode 100644 app/stylesheet/navbar.scss rename app/{assets/stylesheets => stylesheet}/notifications.scss (100%) diff --git a/app/assets/stylesheets/application.scss b/app/assets/stylesheets/application.scss index bf1e4192e55..2b63c477316 100644 --- a/app/assets/stylesheets/application.scss +++ b/app/assets/stylesheets/application.scss @@ -7,7 +7,6 @@ @import "patternfly"; @import "patternfly_overrides"; @import "icon_customizations"; -@import "header_background"; // sets a custom background image in the header @import "about_modal_background"; // sets a custom background image in the 'About' modal @import "alerts"; @import "aggregate_status_card"; @@ -19,10 +18,8 @@ @import "dual_list"; @import "dynamic_prefix_form_input"; @import "login"; -@import "masthead"; @import "metrics"; @import "miq_tree"; -@import "notifications"; @import "piecharts"; @import "report_colors"; @import "report-data-table"; @@ -30,4 +27,3 @@ @import "topology"; @import "topology-toolbar"; @import "remote_console"; -@import "vertical_navigation"; diff --git a/app/assets/stylesheets/header_background.scss b/app/assets/stylesheets/header_background.scss deleted file mode 100644 index 4fee20bab06..00000000000 --- a/app/assets/stylesheets/header_background.scss +++ /dev/null @@ -1,10 +0,0 @@ -/* Bootstrap overrides */ - -/* Header background image */ - -.navbar-pf-vertical { - /* background image attribute can be set in /public/custom.css */ - background-repeat: no-repeat; - background-position: 100% 0; - background-color: $navbar-pf-alt-bg-color; -} diff --git a/app/assets/stylesheets/masthead.scss b/app/assets/stylesheets/masthead.scss deleted file mode 100644 index 8f544646668..00000000000 --- a/app/assets/stylesheets/masthead.scss +++ /dev/null @@ -1,34 +0,0 @@ -.navbar-pf-vertical .navbar-toggle .icon-bar, -.navbar-pf-vertical .nav .nav-item-iconic .pficon, -.navbar-pf-vertical .nav .nav-item-iconic .fa { - color: #fff; -} - -.navbar-right { - .brand-white-label.whitelabeled { // custom logo styling - background: url(/upload/custom_logo.png) right top no-repeat; - background-size: auto 38px; - height: 38px; - margin-top: 10px; - width: 320px; - } - .nav-item-iconic { - .navbar__user-name { - color: #ffffff; - display: inline-block; - margin-left: 1ex; - } - } - .dropdown-submenu > a { // group switcher styling - padding-left: 20px !important; - } - .dropdown-submenu > a::after { - content: '\f104'; - left: 10px; - } - .scrollable-menu { // adds scrollbar to group switcher - height: auto; - max-height: 500px; - overflow-x: hidden; - } -} diff --git a/app/assets/stylesheets/service_dialogs.css b/app/assets/stylesheets/service_dialogs.css deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/app/assets/stylesheets/vertical_navigation.scss b/app/assets/stylesheets/vertical_navigation.scss deleted file mode 100644 index d80ed05e9e4..00000000000 --- a/app/assets/stylesheets/vertical_navigation.scss +++ /dev/null @@ -1,211 +0,0 @@ -.nav-pf-vertical { - .menu-list-group-item { - border-bottom: 1px solid rgb(3, 3, 3); - border-top: 1px solid rgb(3, 3, 3); - box-sizing: border-box; - padding: 0; - margin-bottom: -1px; - position: relative; - &:first-child { - border-top: none; - } - a { - background-color: transparent; - color: #d1d1d1; - cursor: pointer; - display: block; - font-size: 14px; - font-weight: 400; - height: 63px; - line-height: 26px; - padding: 17px 20px 17px 25px; - position: relative; - white-space: nowrap; - width: 225px; - text-decoration: none; - .pficon, .fa { - color: #72767b; - float: left; - font-size: 20px; - line-height: 26px; - margin-right: 10px; - text-align: center; - width: 24px; - } - } - .list-group-item-value { - flex: 1; - max-width: none; - padding-right: 15px; - line-height: 25px; - overflow: hidden; - text-overflow: ellipsis; - } - } - .menu-list-group-item.active { - a { - background-color: #393f44; - color: #fff; - font-weight: 600; - .pficon, .fa { - color: #39a5dc; - } - } - a:before { - background: #39a5dc; - content: " "; - height: 100%; - left: 0; - position: absolute; - top: 0; - width: 3px; - } - .nav-pf-secondary-nav { - .nav-item-pf-header { - a { - ::before { - opacity: 1; - } - } - } - } - } - .menu-list-group-item:hover { - a { - background-color: #393f44; - color: #fff; - font-weight: 600; - width: calc(225px + 1px); - z-index: 1031; - .pficon, .fa { - color: #39a5dc; - } - } - } - .nav-pf-secondary-nav { - .nav-item-pf-header { - a { - color: #fff; - opacity: 1; - font-size: 16px; - font-family: "Open Sans", Helvetica, Arial, sans-serif; - font-weight: 400; - cursor: pointer; - padding: 0; - height: auto; - background: transparent; - z-index: 0; - } - } - .menu-list-group-item { - border: none; - padding: 0 0 5px 0; - width: 225px; - margin-bottom: -1px; - &.tertiary-nav-item-pf { - a { - ::after { - color: #72767b; - content: "\f105"; - display: block; - font-family: "FontAwesome"; - font-size: 20px; - line-height: 20px; - padding: 0; - position: absolute; - right: 20px; - top: 4px; - } - } - } - a { - background-color: #393f44; - color: #d1d1d1; - font-size: 12px; - font-weight: inherit; - height: inherit; - margin-left: 20px; - width: calc(225px - 20px); - display: flex; - opacity: 1; - padding: 4px 0 2px 0; - .list-group-item-value { - padding-left: 5px; - flex: 1; - max-width: none; - padding-right: 15px; - } - } - } - .menu-list-group-item.active, .menu-list-group-item:hover { - a { - background-color: #4d5258; - color: #fff; - font-weight: 600; - } - } - a:before { - display: none; - } - .menu-list-group-item:hover { - a { - color: #fff; - font-weight: 600; - } - } - } - - .nav-pf-tertiary-nav{ - .nav-item-pf-header { - a { - font-size: 16px; - font-weight: 400 !important; - margin: 0; - padding: 0; - background-color: #4d5258; - color: #fff; - ::after { - display: none !important; - } - } - } - .menu-list-group-item { - a { - background-color: #4d5258; - span { - font-weight: 400; - color: #fff; - } - ::after { - display: none !important; - } - } - } - .menu-list-group-item.active, .menu-list-group-item:hover { - a { - background-color: #393f44; - span { - font-weight: 600; - } - } - } - } - - &.collapsed { - .menu-list-group-item, .menu-list-group-item.active { - >a.top-level-item { - >.list-group-item-value { - display: none; - } - padding: 17px 0 17px 25px; - width: 75px; - &::after { - right: 10px; - } - } - >a.top-level-item:hover { - width: calc(75px + 1px); - } - } - } -} diff --git a/app/javascript/components/breadcrumbs/index.jsx b/app/javascript/components/breadcrumbs/index.jsx index 99643ddfd3d..5fef9c4d387 100644 --- a/app/javascript/components/breadcrumbs/index.jsx +++ b/app/javascript/components/breadcrumbs/index.jsx @@ -47,7 +47,7 @@ const renderItems = ({ items, controllerName }) => { const Breadcrumbs = ({ items, title, controllerName }) => ( <> - + {items && renderItems({ items, controllerName })} diff --git a/app/javascript/menu/navbar.jsx b/app/javascript/menu/navbar.jsx index 6ca531d204c..018cb753c64 100644 --- a/app/javascript/menu/navbar.jsx +++ b/app/javascript/menu/navbar.jsx @@ -1,10 +1,5 @@ import React from 'react'; import PropTypes from 'prop-types'; -export const Navbar = () => ( - -); +// overriden in productization +export const Navbar = () => (<>); diff --git a/app/stylesheet/application-webpack.scss b/app/stylesheet/application-webpack.scss index 3ee7d025d80..8e65b6df4e7 100644 --- a/app/stylesheet/application-webpack.scss +++ b/app/stylesheet/application-webpack.scss @@ -7,4 +7,7 @@ @import '~@manageiq/react-ui-components/dist/textual_summary.css'; @import '~@manageiq/react-ui-components/dist/wooden-tree.css'; @import '~@manageiq/ui-components/dist/css/ui-components.css'; + @import './carbon.scss'; +@import './navbar.scss'; +@import './notifications.scss'; diff --git a/app/stylesheet/navbar.scss b/app/stylesheet/navbar.scss new file mode 100644 index 00000000000..b7c621b4df2 --- /dev/null +++ b/app/stylesheet/navbar.scss @@ -0,0 +1,3 @@ +.layout-pf.layout-pf-fixed body { + padding-top: 0px; +} diff --git a/app/assets/stylesheets/notifications.scss b/app/stylesheet/notifications.scss similarity index 100% rename from app/assets/stylesheets/notifications.scss rename to app/stylesheet/notifications.scss From 4831f3c875af83f225ebc32296c1cd48c2629ea4 Mon Sep 17 00:00:00 2001 From: Martin Hradil Date: Fri, 1 May 2020 19:03:33 +0000 Subject: [PATCH 15/57] Menu - add custom styling --- app/stylesheet/application-webpack.scss | 1 + app/stylesheet/menu.scss | 17 +++++++++++++++++ 2 files changed, 18 insertions(+) create mode 100644 app/stylesheet/menu.scss diff --git a/app/stylesheet/application-webpack.scss b/app/stylesheet/application-webpack.scss index 8e65b6df4e7..20001acb64f 100644 --- a/app/stylesheet/application-webpack.scss +++ b/app/stylesheet/application-webpack.scss @@ -9,5 +9,6 @@ @import '~@manageiq/ui-components/dist/css/ui-components.css'; @import './carbon.scss'; +@import './menu.scss'; @import './navbar.scss'; @import './notifications.scss'; diff --git a/app/stylesheet/menu.scss b/app/stylesheet/menu.scss new file mode 100644 index 00000000000..60e9859cd5b --- /dev/null +++ b/app/stylesheet/menu.scss @@ -0,0 +1,17 @@ +#main-menu { + .bx--side-nav--expanded { + width: 225px; + max-width: 225px; + } + + .bx--side-nav { + background-color: #262626; // Gray 90 + } + + .bx--side-nav, + .bx--side-nav__icon, + .bx--side-nav__link-text, + .bx--side-nav__submenu-title { + color: #f4f4f4; // Gray 10 + } +} From e9935bb895a8aee1c6532c013d8ddfc93a9db6c8 Mon Sep 17 00:00:00 2001 From: Martin Hradil Date: Fri, 1 May 2020 20:39:44 +0000 Subject: [PATCH 16/57] Menu - remember collapsed/expanded state, adjust body using the original patternfly localStorage logic, and added a miq-main-menu-{expanded,collapsed} body class that overrides patternfly default 225 px margin and made the menu component update all those :) --- app/javascript/menu/main-menu.jsx | 35 ++++++++++++++++++++---- app/javascript/miq-redux/menu-reducer.js | 19 ------------- app/javascript/miq-redux/store.js | 2 -- app/stylesheet/menu.scss | 22 +++++++++++++-- 4 files changed, 50 insertions(+), 28 deletions(-) delete mode 100644 app/javascript/miq-redux/menu-reducer.js diff --git a/app/javascript/menu/main-menu.jsx b/app/javascript/menu/main-menu.jsx index fa401a254bc..8c9638b9fc1 100644 --- a/app/javascript/menu/main-menu.jsx +++ b/app/javascript/menu/main-menu.jsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { useEffect, useState } from 'react'; import PropTypes from 'prop-types'; import { SideNav, @@ -66,20 +66,42 @@ const MenuFind = () => ( ); -const MenuCollapse = ({ collapsed }) => ( +const MenuCollapse = ({ expanded, toggle }) => ( // TODO -
    {collapsed ? '>' : '<'}
    +
    + {expanded ? '<' : '>'} +
    ); +const initialExpanded = window.localStorage.getItem('patternfly-navigation-primary') !== 'collapsed'; + export const MainMenu = (props) => { const { applianceName, currentGroup, currentUser, customBrand, customLogo, imagePath, menu, miqGroups } = props; + const [expanded, setExpanded] = useState(initialExpanded); + + useEffect(() => { + window.localStorage.setItem('patternfly-navigation-primary', expanded ? 'expanded' : 'collapsed'); + + const classNames = { + true: 'miq-main-menu-expanded', + false: 'miq-main-menu-collapsed', + }; + document.body.classList.remove(classNames[!expanded]); + document.body.classList.add(classNames[expanded]); + }, [expanded]); return ( { {mapItems(menu)} - + setExpanded(!expanded)} + /> ); }; diff --git a/app/javascript/miq-redux/menu-reducer.js b/app/javascript/miq-redux/menu-reducer.js deleted file mode 100644 index d4d65fcf66e..00000000000 --- a/app/javascript/miq-redux/menu-reducer.js +++ /dev/null @@ -1,19 +0,0 @@ -export const TOGGLE_VERTICAL_MENU_COLLAPSED = 'SET_VERTICAL_MENU_COLLAPSED'; - -export const toggleVerticalMenuCollapsed = isVerticalMenuCollapsed => ({ - type: TOGGLE_VERTICAL_MENU_COLLAPSED, - payload: { - isVerticalMenuCollapsed, - }, -}); - -export const menuReducer = (state = { - isVerticalMenuCollapsed: window.localStorage.getItem('patternfly-navigation-primary') === 'collapsed', -}, action) => { - switch (action.type) { - case TOGGLE_VERTICAL_MENU_COLLAPSED: - return { ...state, isVerticalMenuCollapsed: !state.isVerticalMenuCollapsed }; - default: - return state; - } -}; diff --git a/app/javascript/miq-redux/store.js b/app/javascript/miq-redux/store.js index a9578d1d249..c144cb44543 100644 --- a/app/javascript/miq-redux/store.js +++ b/app/javascript/miq-redux/store.js @@ -4,7 +4,6 @@ import createMiddlewares from './middleware'; import { history } from '../miq-component/react-history.js'; import { reducer as formReducer } from './form-reducer'; -import { menuReducer } from './menu-reducer'; import { notificationReducer } from './notification-reducer'; const initialState = {}; @@ -23,7 +22,6 @@ const initializeStore = () => { */ store.asyncReducers = { formReducer, - menuReducer, notificationReducer, }; diff --git a/app/stylesheet/menu.scss b/app/stylesheet/menu.scss index 60e9859cd5b..98c6d4ded22 100644 --- a/app/stylesheet/menu.scss +++ b/app/stylesheet/menu.scss @@ -1,7 +1,11 @@ #main-menu { + .bx--side-nav { + width: 48px; + max-width: 48px; + } .bx--side-nav--expanded { - width: 225px; - max-width: 225px; + width: 256px; + max-width: 256px; } .bx--side-nav { @@ -15,3 +19,17 @@ color: #f4f4f4; // Gray 10 } } + + +.layout-pf.layout-pf-fixed { + .miq-main-menu-collapsed { + .container-pf-nav-pf-vertical { + margin-left: 48px; + } + } + .miq-main-menu-expanded { + .container-pf-nav-pf-vertical { + margin-left: 256px; + } + } +} From f2fb9186cc8928d6e53ba6e05d2b0d11e6918c64 Mon Sep 17 00:00:00 2001 From: Martin Hradil Date: Fri, 1 May 2020 23:27:12 +0000 Subject: [PATCH 17/57] Menu - allow & use carbon icons updated menu definition to use carbon icons where available, and updated carbonizeIcon to recognize `carbon--Foobar` as `` --- app/javascript/menu/main-menu.jsx | 19 ++++++++++++++++++- app/presenters/menu/default_menu.rb | 27 ++++++++++++++++----------- package.json | 1 + 3 files changed, 35 insertions(+), 12 deletions(-) diff --git a/app/javascript/menu/main-menu.jsx b/app/javascript/menu/main-menu.jsx index 8c9638b9fc1..2432723228c 100644 --- a/app/javascript/menu/main-menu.jsx +++ b/app/javascript/menu/main-menu.jsx @@ -7,12 +7,29 @@ import { SideNavMenuItem, } from 'carbon-components-react/es/components/UIShell'; +import ChevronLeft20 from '@carbon/icons-react/es/chevron--left/20'; +import ChevronRight20 from '@carbon/icons-react/es/chevron--right/20'; +import * as icons from '@carbon/icons-react'; + + import { MiqLogo } from './miq-logo'; import { UserOptions } from './user-options'; import { itemId, linkProps } from './item-type'; -const carbonizeIcon = (classname) => (props) => (); +const carbonizeIcon = (classname, size = 20) => { + if (!classname) { + return null; + } + + if (!classname.startsWith('carbon--')) { + return (props) => (); + } + + let name = classname.replace(/^carbon--/, ''); + let key = `${name}${size}`; + return icons[key]; +}; const mapItems = (items) => items.map((item) => ( item.items.length diff --git a/app/presenters/menu/default_menu.rb b/app/presenters/menu/default_menu.rb index e89f33a97a6..90f3ee54ef0 100644 --- a/app/presenters/menu/default_menu.rb +++ b/app/presenters/menu/default_menu.rb @@ -24,7 +24,7 @@ def default_menu end def compute_menu_section - Menu::Section.new(:compute, N_("Compute"), 'pficon pficon-cpu', [ + Menu::Section.new(:compute, N_("Compute"), 'carbon--Chip', [ clouds_menu_section, infrastructure_menu_section, physical_infrastructure_menu_section, @@ -33,7 +33,7 @@ def compute_menu_section end def configuration_menu_section - Menu::Section.new(:conf, N_("Configuration"), 'fa fa-cog', [ + Menu::Section.new(:conf, N_("Configuration"), 'carbon--InventoryManagement', [ Menu::Item.new('ems_configuration', N_('Providers'), 'ems_configuration', {:feature => 'ems_configuration_show_list'}, '/ems_configuration/show_list'), Menu::Item.new('configuration_profile', N_('Profiles'), 'configuration_profile', {:feature => 'configuration_profile_show_list'}, '/configuration_profile/show_list'), Menu::Item.new('configured_system', N_('Configured Systems'), 'configured_system', {:feature => 'configured_system_show_list'}, '/configured_system/show_list'), @@ -41,7 +41,7 @@ def configuration_menu_section end def overview_menu_section - Menu::Section.new(:vi, N_("Overview"), 'fa fa-dashboard', [ + Menu::Section.new(:vi, N_("Overview"), 'carbon--Cloud', [ Menu::Item.new('dashboard', N_('Dashboard'), 'dashboard', {:feature => 'dashboard_view'}, '/dashboard/show'), Menu::Item.new('report', N_('Reports'), 'miq_report', {:feature => 'miq_report', :any => true}, '/report/explorer'), # Menu::Item.new('usage', N_('Usage'), 'usage', {:feature => 'usage'}, '/report/usage/'), # / Hiding usage for now - release 5.2 @@ -52,6 +52,7 @@ def overview_menu_section ]) end + # FIXME: remove? def consumption_menu_section Menu::Section.new(:cons, N_("Consumption"), nil, [ Menu::Item.new('consumption', N_('Dashboard'), 'consumption', {:feature => 'consumption', :any => true}, '/consumption/show') @@ -59,7 +60,7 @@ def consumption_menu_section end def services_menu_section - Menu::Section.new(:svc, N_("Services"), 'pficon pficon-service', [ + Menu::Section.new(:svc, N_("Services"), 'carbon--ToolBox', [ Menu::Item.new('services', N_('My Services'), 'service', {:feature => 'service', :any => true}, '/service/explorer'), Menu::Item.new('catalogs', N_('Catalogs'), 'catalog', {:feature => 'catalog', :any => true}, '/catalog/explorer'), Menu::Item.new('vm_or_template', N_('Workloads'), 'vm_explorer', {:feature => 'vm_explorer', :any => true}, '/vm_or_template/explorer'), @@ -138,7 +139,7 @@ def container_menu_section end def network_menu_section - Menu::Section.new(:net, N_("Network"), 'pficon pficon-network', [ + Menu::Section.new(:net, N_("Network"), 'carbon--Network_2', [ Menu::Item.new('ems_network', N_('Providers'), 'ems_network', {:feature => 'ems_network_show_list'}, '/ems_network/show_list'), Menu::Item.new('cloud_network', N_('Networks'), 'cloud_network', {:feature => 'cloud_network_show_list'}, '/cloud_network/show_list'), Menu::Item.new('cloud_subnet', N_('Subnets'), 'cloud_subnet', {:feature => 'cloud_subnet_show_list'}, '/cloud_subnet/show_list'), @@ -151,7 +152,7 @@ def network_menu_section end def storage_menu_section - Menu::Section.new(:sto, N_("Storage"), 'fa fa-database', [ + Menu::Section.new(:sto, N_("Storage"), 'carbon--Table', [ block_storage_menu_section, object_storage_menu_section, ]) @@ -208,7 +209,7 @@ def object_storage_menu_section end def control_menu_section - Menu::Section.new(:con, N_("Control"), 'fa fa-shield', [ + Menu::Section.new(:con, N_("Control"), 'carbon--IbmSecurity', [ Menu::Item.new('miq_policy', N_('Explorer'), 'control_explorer', {:feature => 'control_explorer_view'}, '/miq_policy/explorer'), Menu::Item.new('miq_policy_rsop', N_('Simulation'), 'policy_simulation', {:feature => 'policy_simulation'}, '/miq_policy/rsop'), Menu::Item.new('miq_policy_export', N_('Import / Export'), 'policy_import_export', {:feature => 'policy_import_export'}, '/miq_policy/export'), @@ -217,7 +218,7 @@ def control_menu_section end def automation_menu_section - Menu::Section.new(:aut, N_("Automation"), 'pficon pficon-automation', [ + Menu::Section.new(:aut, N_("Automation"), 'carbon--FlowData', [ ansible_menu_section, automation_manager_menu_section, automate_menu_section, @@ -259,13 +260,13 @@ def alerts_menu_section end def monitor_menu_section - Menu::Section.new(:monitor, N_("Monitor"), 'fa fa-heartbeat', [ + Menu::Section.new(:monitor, N_("Monitor"), 'carbon--Activity', [ alerts_menu_section, ]) end def settings_menu_section - Menu::Section.new(:set, N_("Settings"), 'pficon pficon-settings', [ + Menu::Section.new(:set, N_("Settings"), 'carbon--Settings', [ Menu::Item.new('configuration', N_('My Settings'), 'my_settings', {:feature => 'my_settings', :any => true}, '/configuration/index'), Menu::Item.new('my_tasks', N_('Tasks'), 'tasks', {:feature => 'tasks', :any => true}, '/miq_task/index?jobs_tab=tasks'), help_documentation, @@ -302,7 +303,11 @@ def logout_item N_("Logout"), 'logout', nil, - '/dashboard/logout') + '/dashboard/logout', + nil, + nil, + nil, + 'carbon--Logout') end private diff --git a/package.json b/package.json index d5040b743f3..fd45f89d9c3 100644 --- a/package.json +++ b/package.json @@ -30,6 +30,7 @@ }, "homepage": "https://github.com/ManageIQ/manageiq#readme", "dependencies": { + "@carbon/icons-react": "~10.11.0", "@carbon/themes": "~10.12.0", "@data-driven-forms/pf3-component-mapper": "~1.30.0", "@data-driven-forms/react-form-renderer": "~1.30.0", From 4d07ab43b7353b5b310dfebf3a8d4211fcfeb57c Mon Sep 17 00:00:00 2001 From: Martin Hradil Date: Fri, 1 May 2020 23:29:03 +0000 Subject: [PATCH 18/57] menu styling - rem fix, use SideNavHeader, SideNavItem, chevron icons in the right gray setting the html font size changes the rem units to the actual carbon defaults, so we get some menu sizing for free --- app/javascript/menu/main-menu.jsx | 36 ++++++++++++++++---------- app/stylesheet/carbon.scss | 16 ------------ app/stylesheet/menu.scss | 43 ++++++++++++++++++++++++------- 3 files changed, 55 insertions(+), 40 deletions(-) diff --git a/app/javascript/menu/main-menu.jsx b/app/javascript/menu/main-menu.jsx index 2432723228c..80da1643cf6 100644 --- a/app/javascript/menu/main-menu.jsx +++ b/app/javascript/menu/main-menu.jsx @@ -2,6 +2,8 @@ import React, { useEffect, useState } from 'react'; import PropTypes from 'prop-types'; import { SideNav, + SideNavHeader, + SideNavItem, SideNavItems, SideNavMenu, SideNavMenuItem, @@ -84,16 +86,14 @@ const MenuFind = () => ( ); const MenuCollapse = ({ expanded, toggle }) => ( - // TODO -
    - {expanded ? '<' : '>'} -
    + +
    + {expanded ? : } +
    +
    ); @@ -114,25 +114,33 @@ export const MainMenu = (props) => { document.body.classList.add(classNames[expanded]); }, [expanded]); + const renderIcon = () => ( + + ); + return ( - + + -
    +
    {mapItems(menu)} diff --git a/app/stylesheet/carbon.scss b/app/stylesheet/carbon.scss index 7eca0c43b1b..9d3e20baf7f 100644 --- a/app/stylesheet/carbon.scss +++ b/app/stylesheet/carbon.scss @@ -23,19 +23,3 @@ $carbon--theme: $carbon--theme--g90; html { font-size: 100%; } - -// distinguish subsections -.bx--side-nav__item .bx--side-nav__item .bx--side-nav__submenu-title { - padding-left: 8px; -} - -// hack for size differences from original menu -.container-pf-nav-pf-vertical { - margin-left: 256px !important; -} - -// fix notification drawer 0 height, hide "padding" under header -.drawer-pf.drawer-alt-pf.drawer-pf-notifications { - top: 48px; - height: calc(100% - 48px); -} diff --git a/app/stylesheet/menu.scss b/app/stylesheet/menu.scss index 98c6d4ded22..f20bdf18c4b 100644 --- a/app/stylesheet/menu.scss +++ b/app/stylesheet/menu.scss @@ -1,25 +1,48 @@ #main-menu { - .bx--side-nav { - width: 48px; - max-width: 48px; - } - .bx--side-nav--expanded { - width: 256px; - max-width: 256px; + list-style: none; + + svg { + fill: $gray-10; } .bx--side-nav { - background-color: #262626; // Gray 90 + background-color: $gray-90; } .bx--side-nav, .bx--side-nav__icon, .bx--side-nav__link-text, .bx--side-nav__submenu-title { - color: #f4f4f4; // Gray 10 + color: $gray-10; } -} + .bx--side-nav__link, + .bx--side-nav__submenu, + .menu-collapse, + .menu-collapse-button { + height: 48px; + } + + .bx--side-nav__hr { + color: $gray-60; + margin: 16px 0 0 0; + } + .bx--side-nav__expanded { + .bx--side-nav__hr { + margin: 16px 16px 0 16px; + } + } + + .menu-collapse { + border-top: solid 1px #3d3d3d; + + .menu-collapse-button { + margin-left: auto; + padding: 14px; + width: 48px; + } + } +} .layout-pf.layout-pf-fixed { .miq-main-menu-collapsed { From f24d42639f17a8c41c40e6677914b16a691e3ae3 Mon Sep 17 00:00:00 2001 From: Martin Hradil Date: Fri, 1 May 2020 23:31:00 +0000 Subject: [PATCH 19/57] SideNavMenuItem - support renderIcon, same as SideNavMenu not supported by default, but the logout item needs an icon, and custom items can use it too --- app/javascript/menu/SideNavMenuItem.jsx | 49 +++++++++++++++++++++++++ app/javascript/menu/main-menu.jsx | 7 +++- 2 files changed, 54 insertions(+), 2 deletions(-) create mode 100644 app/javascript/menu/SideNavMenuItem.jsx diff --git a/app/javascript/menu/SideNavMenuItem.jsx b/app/javascript/menu/SideNavMenuItem.jsx new file mode 100644 index 00000000000..78019084606 --- /dev/null +++ b/app/javascript/menu/SideNavMenuItem.jsx @@ -0,0 +1,49 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import cx from 'classnames'; +import Link from 'carbon-components-react/es/components/UIShell/Link'; +import { + SideNavIcon, + SideNavLinkText, +} from 'carbon-components-react/es/components/UIShell'; + +const prefix = 'bx'; + +// override Carbon SideNavMenuItem to support renderIcon +const SideNavMenuItem = React.forwardRef(function SideNavMenuItem(props, ref) { + const { + children, + className: customClassName, + isActive, + renderIcon: IconElement, + ...rest + } = props; + + const className = cx(`${prefix}--side-nav__menu-item`, customClassName); + const linkClassName = cx({ + [`${prefix}--side-nav__link`]: true, + [`${prefix}--side-nav__link--current`]: isActive, + }); + + return ( +
  • + + {IconElement && ( + + + + )} + {children} + +
  • + ); +}) + +SideNavMenuItem.propTypes = { + children: PropTypes.node, + className: PropTypes.string, + isActive: PropTypes.bool, + renderIcon: PropTypes.oneOfType([PropTypes.func, PropTypes.object]), +}; + +export default SideNavMenuItem; diff --git a/app/javascript/menu/main-menu.jsx b/app/javascript/menu/main-menu.jsx index 80da1643cf6..7e22de2e048 100644 --- a/app/javascript/menu/main-menu.jsx +++ b/app/javascript/menu/main-menu.jsx @@ -6,9 +6,10 @@ import { SideNavItem, SideNavItems, SideNavMenu, - SideNavMenuItem, } from 'carbon-components-react/es/components/UIShell'; +import SideNavMenuItem from './SideNavMenuItem'; + import ChevronLeft20 from '@carbon/icons-react/es/chevron--left/20'; import ChevronRight20 from '@carbon/icons-react/es/chevron--right/20'; import * as icons from '@carbon/icons-react'; @@ -40,10 +41,11 @@ const mapItems = (items) => items.map((item) => ( )); -const MenuItem = ({ active, href, id, title, type }) => ( +const MenuItem = ({ active, href, icon, id, title, type }) => ( {title} @@ -53,6 +55,7 @@ const MenuItem = ({ active, href, id, title, type }) => ( MenuItem.props = { active: PropTypes.bool, href: PropTypes.string.isRequired, + icon: PropTypes.string, id: PropTypes.string.isRequired, title: PropTypes.string.isRequired, type: PropTypes.string.isRequired, From 6cc557b87124be7087ee283fbe49c80602eac78b Mon Sep 17 00:00:00 2001 From: Martin Hradil Date: Fri, 1 May 2020 23:44:08 +0000 Subject: [PATCH 20/57] NotificationsToggle - use MiqIcon and carbon icons --- .../breadcrumbs/notifications-toggle.jsx | 5 +++-- app/javascript/menu/icon.jsx | 21 +++++++++++++++++++ app/javascript/menu/main-menu.jsx | 17 +-------------- app/stylesheet/notifications.scss | 6 ++++-- 4 files changed, 29 insertions(+), 20 deletions(-) create mode 100644 app/javascript/menu/icon.jsx diff --git a/app/javascript/components/breadcrumbs/notifications-toggle.jsx b/app/javascript/components/breadcrumbs/notifications-toggle.jsx index d2a71db309c..cb15ad4bd8e 100644 --- a/app/javascript/components/breadcrumbs/notifications-toggle.jsx +++ b/app/javascript/components/breadcrumbs/notifications-toggle.jsx @@ -1,6 +1,7 @@ import React, { useEffect } from 'react'; import { useDispatch, useSelector } from 'react-redux'; import { TOGGLE_DRAWER_VISIBILITY } from '../../miq-redux/actions/notifications-actions'; +import { MiqIcon } from '../../menu/icon'; export const NotificationsToggle = () => { const dispatch = useDispatch(); @@ -21,13 +22,13 @@ export const NotificationsToggle = () => { return ( {__("Notifications")}   - + ); }; diff --git a/app/javascript/menu/icon.jsx b/app/javascript/menu/icon.jsx new file mode 100644 index 00000000000..7cdc2ea5600 --- /dev/null +++ b/app/javascript/menu/icon.jsx @@ -0,0 +1,21 @@ +import React from 'react'; +import * as icons from '@carbon/icons-react'; + +export const carbonizeIcon = (classname, size = 20) => { + if (!classname) { + return null; + } + + if (!classname.startsWith('carbon--')) { + return (props) => (); + } + + let name = classname.replace(/^carbon--/, ''); + let key = `${name}${size}`; + return icons[key]; +}; + +export const MiqIcon = ({ icon, size = 16 }) => { + const IconElement = carbonizeIcon(icon, size); + return ; +}; diff --git a/app/javascript/menu/main-menu.jsx b/app/javascript/menu/main-menu.jsx index 7e22de2e048..69445877c1c 100644 --- a/app/javascript/menu/main-menu.jsx +++ b/app/javascript/menu/main-menu.jsx @@ -12,28 +12,13 @@ import SideNavMenuItem from './SideNavMenuItem'; import ChevronLeft20 from '@carbon/icons-react/es/chevron--left/20'; import ChevronRight20 from '@carbon/icons-react/es/chevron--right/20'; -import * as icons from '@carbon/icons-react'; - +import { carbonizeIcon } from './icon'; import { MiqLogo } from './miq-logo'; import { UserOptions } from './user-options'; import { itemId, linkProps } from './item-type'; -const carbonizeIcon = (classname, size = 20) => { - if (!classname) { - return null; - } - - if (!classname.startsWith('carbon--')) { - return (props) => (); - } - - let name = classname.replace(/^carbon--/, ''); - let key = `${name}${size}`; - return icons[key]; -}; - const mapItems = (items) => items.map((item) => ( item.items.length ? diff --git a/app/stylesheet/notifications.scss b/app/stylesheet/notifications.scss index efe9b858178..37b7364a92b 100644 --- a/app/stylesheet/notifications.scss +++ b/app/stylesheet/notifications.scss @@ -64,8 +64,10 @@ // FIXME flex? width: 140px; - .unread { - color: #39a5dc; + &.unread { + svg { + fill: #39a5dc; + } } } From 1c68ca872cc472a9701b26bebef95aeaa7962626 Mon Sep 17 00:00:00 2001 From: Martin Hradil Date: Sat, 2 May 2020 01:04:57 +0000 Subject: [PATCH 21/57] remove custom SideNavMenuItem, use SideNavLink for first level menu items that one can do icons by default --- app/javascript/menu/SideNavMenuItem.jsx | 49 ------------------------ app/javascript/menu/main-menu.jsx | 50 ++++++++++++++++++------- 2 files changed, 37 insertions(+), 62 deletions(-) delete mode 100644 app/javascript/menu/SideNavMenuItem.jsx diff --git a/app/javascript/menu/SideNavMenuItem.jsx b/app/javascript/menu/SideNavMenuItem.jsx deleted file mode 100644 index 78019084606..00000000000 --- a/app/javascript/menu/SideNavMenuItem.jsx +++ /dev/null @@ -1,49 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import cx from 'classnames'; -import Link from 'carbon-components-react/es/components/UIShell/Link'; -import { - SideNavIcon, - SideNavLinkText, -} from 'carbon-components-react/es/components/UIShell'; - -const prefix = 'bx'; - -// override Carbon SideNavMenuItem to support renderIcon -const SideNavMenuItem = React.forwardRef(function SideNavMenuItem(props, ref) { - const { - children, - className: customClassName, - isActive, - renderIcon: IconElement, - ...rest - } = props; - - const className = cx(`${prefix}--side-nav__menu-item`, customClassName); - const linkClassName = cx({ - [`${prefix}--side-nav__link`]: true, - [`${prefix}--side-nav__link--current`]: isActive, - }); - - return ( -
  • - - {IconElement && ( - - - - )} - {children} - -
  • - ); -}) - -SideNavMenuItem.propTypes = { - children: PropTypes.node, - className: PropTypes.string, - isActive: PropTypes.bool, - renderIcon: PropTypes.oneOfType([PropTypes.func, PropTypes.object]), -}; - -export default SideNavMenuItem; diff --git a/app/javascript/menu/main-menu.jsx b/app/javascript/menu/main-menu.jsx index 69445877c1c..581fd42ae34 100644 --- a/app/javascript/menu/main-menu.jsx +++ b/app/javascript/menu/main-menu.jsx @@ -5,11 +5,11 @@ import { SideNavHeader, SideNavItem, SideNavItems, + SideNavLink, SideNavMenu, + SideNavMenuItem, } from 'carbon-components-react/es/components/UIShell'; -import SideNavMenuItem from './SideNavMenuItem'; - import ChevronLeft20 from '@carbon/icons-react/es/chevron--left/20'; import ChevronRight20 from '@carbon/icons-react/es/chevron--right/20'; @@ -19,31 +19,55 @@ import { UserOptions } from './user-options'; import { itemId, linkProps } from './item-type'; -const mapItems = (items) => items.map((item) => ( +const mapItems = (items, root = true) => items.map((item) => ( item.items.length ? - : + : ( + root + ? + : + ) )); -const MenuItem = ({ active, href, icon, id, title, type }) => ( - ( + {title} + +); + +FirstLevelItem.props = { + ...menuItemProps, + icon: PropTypes.string, +}; + +// SideNavMenuItem can't render icon, but we only have first level icons +const MenuItem = ({ active, href, id, title, type }) => ( + + {title} ); MenuItem.props = { - active: PropTypes.bool, - href: PropTypes.string.isRequired, - icon: PropTypes.string, - id: PropTypes.string.isRequired, - title: PropTypes.string.isRequired, - type: PropTypes.string.isRequired, + ...menuItemProps, }; @@ -55,7 +79,7 @@ const MenuSection = ({ active, id, items, title, icon }) => ( renderIcon={carbonizeIcon(icon)} // only first level sections have it, but all need the prop for consistent padding title={title} > - {mapItems(items)} + {mapItems(items, false)}
    ); From 944906e97595e6cc11345c18ca9baf146fef0366 Mon Sep 17 00:00:00 2001 From: Martin Hradil Date: Mon, 4 May 2020 17:29:08 +0000 Subject: [PATCH 22/57] Menu carbonize UserOptions, split into user bit and GroupSwitcher GroupSwitcher should just be a SideNavSwitcher, except it doesn't handle options where value != label, and the chevron doesn't trigger it --- app/javascript/menu/group-switcher.jsx | 52 +++++++++++++++++ app/javascript/menu/main-menu.jsx | 25 ++++++--- app/javascript/menu/user-options.jsx | 78 -------------------------- app/stylesheet/menu.scss | 1 + 4 files changed, 70 insertions(+), 86 deletions(-) create mode 100644 app/javascript/menu/group-switcher.jsx delete mode 100644 app/javascript/menu/user-options.jsx diff --git a/app/javascript/menu/group-switcher.jsx b/app/javascript/menu/group-switcher.jsx new file mode 100644 index 00000000000..397ae7fd565 --- /dev/null +++ b/app/javascript/menu/group-switcher.jsx @@ -0,0 +1,52 @@ +import Dropdown from 'carbon-components-react/es/components/Dropdown'; +import React from 'react'; +import SideNavItem from 'carbon-components-react/es/components/UIShell/SideNavItem'; +import { Collaborate20 } from '@carbon/icons-react'; + +const { miqChangeGroup } = window; + +export const GroupSwitcher = ({ miqGroups, currentGroup, expanded }) => { + const options = miqGroups.map((g) => ({ + label: g.description, + value: g.id, + })); + + const currentOption = { + label: currentGroup.description, + value: currentGroup.id, + }; + + const groupChange = ({ selectedItem }) => { + const group_id = selectedItem.value; + if (group_id && group_id !== currentGroup.id) { + miqChangeGroup(group_id); + } + }; + + return ( +
    + { expanded ? ( + <> + { options.length > 1 ? ( + + ) : ( + + {currentOption.label} + + )} + + ) : ( + + + + )} +
    + ); +}; diff --git a/app/javascript/menu/main-menu.jsx b/app/javascript/menu/main-menu.jsx index 581fd42ae34..ff2c1fcf241 100644 --- a/app/javascript/menu/main-menu.jsx +++ b/app/javascript/menu/main-menu.jsx @@ -9,13 +9,11 @@ import { SideNavMenu, SideNavMenuItem, } from 'carbon-components-react/es/components/UIShell'; +import { ChevronLeft20, ChevronRight20 } from '@carbon/icons-react'; -import ChevronLeft20 from '@carbon/icons-react/es/chevron--left/20'; -import ChevronRight20 from '@carbon/icons-react/es/chevron--right/20'; - -import { carbonizeIcon } from './icon'; +import { GroupSwitcher } from './group-switcher'; import { MiqLogo } from './miq-logo'; -import { UserOptions } from './user-options'; +import { carbonizeIcon } from './icon'; import { itemId, linkProps } from './item-type'; @@ -133,6 +131,7 @@ export const MainMenu = (props) => { /> ); + return ( { renderIcon={renderIcon} /> - +

    + {currentUser.name} +

    + + + diff --git a/app/javascript/menu/user-options.jsx b/app/javascript/menu/user-options.jsx deleted file mode 100644 index 81ad8a0dcf7..00000000000 --- a/app/javascript/menu/user-options.jsx +++ /dev/null @@ -1,78 +0,0 @@ -import React from 'react'; -import { Dropdown, MenuItem } from 'patternfly-react'; - -export const UserOptions = ({ currentUser, applianceName, miqGroups, currentGroup }) => ( - ( -
  • - {children} -
  • - )} - > - ( - - {children} - - ) - } - > - -

    {`${currentUser.name} | ${applianceName}`}

    -
    - - {sprintf(__('Server: %s'), applianceName)} - { miqGroups.length > 1 ? ( -
  • - ( - - {children} - - ) - } - > - Change Group - - - {miqGroups.sort().map(group => ( - (currentGroup).id === group.id ? ( - - {`${group.description} (${__('Current Group')})`} - - ) : ( - { - e.preventDefault(); - miqChangeGroup(group.id); - }} - > - {group.description} - - ))) - } - -
  • - ) : ( - {(currentGroup).description} - ) - } -
    -
    -); diff --git a/app/stylesheet/menu.scss b/app/stylesheet/menu.scss index f20bdf18c4b..c341ebd60d4 100644 --- a/app/stylesheet/menu.scss +++ b/app/stylesheet/menu.scss @@ -16,6 +16,7 @@ color: $gray-10; } + .bx--side-nav__header, .bx--side-nav__link, .bx--side-nav__submenu, .menu-collapse, From 25c0932d479302450e564a8d02417e9a7f68ecf5 Mon Sep 17 00:00:00 2001 From: Martin Hradil Date: Mon, 4 May 2020 18:05:34 +0000 Subject: [PATCH 23/57] menu: use Search --- app/javascript/menu/main-menu.jsx | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/app/javascript/menu/main-menu.jsx b/app/javascript/menu/main-menu.jsx index ff2c1fcf241..18a0d854025 100644 --- a/app/javascript/menu/main-menu.jsx +++ b/app/javascript/menu/main-menu.jsx @@ -9,6 +9,7 @@ import { SideNavMenu, SideNavMenuItem, } from 'carbon-components-react/es/components/UIShell'; +import Search from 'carbon-components-react/es/components/Search'; import { ChevronLeft20, ChevronRight20 } from '@carbon/icons-react'; import { GroupSwitcher } from './group-switcher'; @@ -89,10 +90,13 @@ MenuSection.props = { title: PropTypes.string.isRequired, }; - const MenuFind = () => ( - //TODO - + + + ); const MenuCollapse = ({ expanded, toggle }) => ( From 163e94333f3af0f45a0df025cb9cab62759546e0 Mon Sep 17 00:00:00 2001 From: Martin Hradil Date: Mon, 4 May 2020 19:50:03 +0000 Subject: [PATCH 24/57] Menu - add second level leaving first-level sections and items in the first SideNav, but adding a second floaty SideNav when a section is selected, and moved second and third level menu there SideNav SideNavLink (section, level 1) SideNavLink (section, level 1) SideNavLink (item, level 1) SideNav.second SideNavMenu (section, level 2) SideNavMenuItem (item, level 3) SideNavMenuItem (item, level 3) SideNavMenu (section, level 2) SideNavMenuItem (item, level 2) (nothing does `setSection(null)` yet to hide the 2nd level nav) --- app/javascript/menu/main-menu.jsx | 61 ++++++++++++++++++++++++++----- app/stylesheet/menu.scss | 7 ++++ 2 files changed, 58 insertions(+), 10 deletions(-) diff --git a/app/javascript/menu/main-menu.jsx b/app/javascript/menu/main-menu.jsx index 18a0d854025..d28b57da290 100644 --- a/app/javascript/menu/main-menu.jsx +++ b/app/javascript/menu/main-menu.jsx @@ -18,9 +18,13 @@ import { carbonizeIcon } from './icon'; import { itemId, linkProps } from './item-type'; -const mapItems = (items, root = true) => items.map((item) => ( +const mapItems = (items, level = 0, root = true, setSection = null) => items.map((item) => ( item.items.length - ? + ? ( + level + ? + : + ) : ( root ? @@ -70,7 +74,31 @@ MenuItem.props = { }; -const MenuSection = ({ active, id, items, title, icon }) => ( +const menuSectionProps = { + active: PropTypes.bool, + icon: PropTypes.string, + id: PropTypes.string.isRequired, + items: PropTypes.arrayOf(PropTypes.any).isRequired, + title: PropTypes.string.isRequired, +}; + +// TODO items chevron +const FirstLevelSection = ({ active, id, items, title, icon, setSection }) => ( + setSection(items)} + > + {title} + +); + +FirstLevelSection.props = { + ...menuSectionProps, +}; + +const MenuSection = ({ active, id, items, title, icon, level }) => ( ( renderIcon={carbonizeIcon(icon)} // only first level sections have it, but all need the prop for consistent padding title={title} > - {mapItems(items, false)} + {mapItems(items, level + 1, false)} ); MenuSection.props = { - active: PropTypes.bool, - icon: PropTypes.string, - id: PropTypes.string.isRequired, - items: PropTypes.arrayOf(PropTypes.any).isRequired, - title: PropTypes.string.isRequired, + ...menuSectionProps, }; + const MenuFind = () => ( { /> ); + const [activeSectionItems, setSection] = useState(null); + return ( + <> {
    - {mapItems(menu)} + {mapItems(menu, 0, true, setSection)} { toggle={() => setExpanded(!expanded)} />
    + { activeSectionItems && ( + + + {mapItems(activeSectionItems, 1, true, setSection)} + + + )} + ); }; diff --git a/app/stylesheet/menu.scss b/app/stylesheet/menu.scss index c341ebd60d4..e3221180b03 100644 --- a/app/stylesheet/menu.scss +++ b/app/stylesheet/menu.scss @@ -9,6 +9,13 @@ background-color: $gray-90; } + .bx--side-nav.second { + background-color: $gray-80; + left: 16rem; + max-width: 14rem; + width: 14rem; + } + .bx--side-nav, .bx--side-nav__icon, .bx--side-nav__link-text, From a6373fa4387ff4dc573b0a90209124fcae91c31e Mon Sep 17 00:00:00 2001 From: Martin Hradil Date: Wed, 6 May 2020 18:07:08 +0000 Subject: [PATCH 25/57] menu - fix second level root item to use MenuItem, not Link --- app/javascript/menu/main-menu.jsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/javascript/menu/main-menu.jsx b/app/javascript/menu/main-menu.jsx index d28b57da290..088e0e0204b 100644 --- a/app/javascript/menu/main-menu.jsx +++ b/app/javascript/menu/main-menu.jsx @@ -26,9 +26,9 @@ const mapItems = (items, level = 0, root = true, setSection = null) => items.map : ) : ( - root - ? - : + level + ? + : ) )); From 251e864031124bacdbc28cb3b0792014a084cd37 Mon Sep 17 00:00:00 2001 From: Martin Hradil Date: Wed, 6 May 2020 18:09:44 +0000 Subject: [PATCH 26/57] menu - rebase colors on theme https://www.carbondesignsystem.com/guidelines/color/usage/ Gray 90 theme --- app/stylesheet/menu.scss | 39 +++++++++++++++++++++++++++------------ 1 file changed, 27 insertions(+), 12 deletions(-) diff --git a/app/stylesheet/menu.scss b/app/stylesheet/menu.scss index e3221180b03..aaed9b4d6e2 100644 --- a/app/stylesheet/menu.scss +++ b/app/stylesheet/menu.scss @@ -1,28 +1,44 @@ #main-menu { list-style: none; - svg { - fill: $gray-10; + ul ul { + list-style: none; + padding: 0; } + // colors .bx--side-nav { - background-color: $gray-90; + background-color: $ui-background; + + &.second { + background-color: $ui-01; + } + } + + .bx--side-nav__link-text, + .bx--side-nav__submenu-title { + color: $text-01; + } + + .bx--side-nav__icon > svg, + .menu-collapse-button > svg { + fill: $icon-01; + } + .bx--side-nav__icon > i { + color: $icon-01; + } + + .bx--side-nav__hr { + color: $text-03; } + // sizing .bx--side-nav.second { - background-color: $gray-80; left: 16rem; max-width: 14rem; width: 14rem; } - .bx--side-nav, - .bx--side-nav__icon, - .bx--side-nav__link-text, - .bx--side-nav__submenu-title { - color: $gray-10; - } - .bx--side-nav__header, .bx--side-nav__link, .bx--side-nav__submenu, @@ -32,7 +48,6 @@ } .bx--side-nav__hr { - color: $gray-60; margin: 16px 0 0 0; } .bx--side-nav__expanded { From 0d86a045d5195cb4b2d9906c1a94a015ff09f05a Mon Sep 17 00:00:00 2001 From: Martin Hradil Date: Thu, 7 May 2020 21:45:51 +0000 Subject: [PATCH 27/57] add new menu logos - expanded and collapsed manageiq-logo-inverse - logo+manageiq, horizontal, for dark background manageiq-logo-glyph-inverse - just the logo, for dark background brand - original brand, just minified minified through [svgomg](https://jakearchibald.github.io/svgomg/) --- app/assets/images/layout/brand.svg | 46 ++----------------- .../layout/manageiq-logo-glyph-inverse.svg | 6 +++ .../images/layout/manageiq-logo-inverse.svg | 7 +++ 3 files changed, 16 insertions(+), 43 deletions(-) create mode 100644 app/assets/images/layout/manageiq-logo-glyph-inverse.svg create mode 100644 app/assets/images/layout/manageiq-logo-inverse.svg diff --git a/app/assets/images/layout/brand.svg b/app/assets/images/layout/brand.svg index 86679df7619..0bfb8214235 100644 --- a/app/assets/images/layout/brand.svg +++ b/app/assets/images/layout/brand.svg @@ -1,43 +1,3 @@ - - - - - - - - - - - - + + + diff --git a/app/assets/images/layout/manageiq-logo-glyph-inverse.svg b/app/assets/images/layout/manageiq-logo-glyph-inverse.svg new file mode 100644 index 00000000000..a1053b4aadb --- /dev/null +++ b/app/assets/images/layout/manageiq-logo-glyph-inverse.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/app/assets/images/layout/manageiq-logo-inverse.svg b/app/assets/images/layout/manageiq-logo-inverse.svg new file mode 100644 index 00000000000..e4f26f06eea --- /dev/null +++ b/app/assets/images/layout/manageiq-logo-inverse.svg @@ -0,0 +1,7 @@ + + + + + + + From 8019aa7fc88931afc9eb7ffcbe391310e1b40bcc Mon Sep 17 00:00:00 2001 From: Martin Hradil Date: Thu, 7 May 2020 21:54:36 +0000 Subject: [PATCH 28/57] Menu - replace imagePath with logoLarge & logoSmall; add showLogo, showUser we're not using brand.svg in menu anymore, replaced by separate files added showLogo and showUser props, defaulting to true, can be overidden from a plugin --- app/javascript/menu/main-menu.jsx | 26 +++++++++++++++++--------- app/views/layouts/_header.html.haml | 3 ++- 2 files changed, 19 insertions(+), 10 deletions(-) diff --git a/app/javascript/menu/main-menu.jsx b/app/javascript/menu/main-menu.jsx index 088e0e0204b..c5ffe6e699e 100644 --- a/app/javascript/menu/main-menu.jsx +++ b/app/javascript/menu/main-menu.jsx @@ -139,7 +139,7 @@ const MenuCollapse = ({ expanded, toggle }) => ( const initialExpanded = window.localStorage.getItem('patternfly-navigation-primary') !== 'collapsed'; export const MainMenu = (props) => { - const { applianceName, currentGroup, currentUser, customBrand, customLogo, imagePath, menu, miqGroups } = props; + const { applianceName, currentGroup, currentUser, customBrand, customLogo, logoLarge, logoSmall, menu, miqGroups, showLogo, showUser } = props; const [expanded, setExpanded] = useState(initialExpanded); useEffect(() => { @@ -153,10 +153,10 @@ export const MainMenu = (props) => { document.body.classList.add(classNames[expanded]); }, [expanded]); - const renderIcon = () => ( + const miqLogo = () => ( ); @@ -170,12 +170,12 @@ export const MainMenu = (props) => { isChildOfHeader={false} expanded={expanded} > - + {showLogo && } {/* FIXME initials, collapsed.. */} - + {showUser &&

    { > {currentUser.name}

    -
    +
    } user, :customBrand => ::Settings.server.custom_brand, :customLogo => ::Settings.server.custom_logo, - :imagePath => image_path("layout/brand.svg"), + :logoLarge => image_path("layout/manageiq-logo-inverse.svg"), + :logoSmall => image_path("layout/manageiq-logo-glyph-inverse.svg"), :menu => menu_to_json, :miqGroups => groups}, :id => 'main-menu') From 9b2348d4444e190fedb8e106512fb6619698d063 Mon Sep 17 00:00:00 2001 From: Martin Hradil Date: Thu, 7 May 2020 21:56:31 +0000 Subject: [PATCH 29/57] Menu - appearExpanded vs expanded `expanded` is done explicitly by the user expanding or collapsing the whole menu, persisted in storage `appearExpanded` is what gets displayed The menu *appears* expanded when displaying search results, or when a menu section is expanded, even if it would be collapsed otherwise. --- app/javascript/menu/main-menu.jsx | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/app/javascript/menu/main-menu.jsx b/app/javascript/menu/main-menu.jsx index c5ffe6e699e..e8d29f683ad 100644 --- a/app/javascript/menu/main-menu.jsx +++ b/app/javascript/menu/main-menu.jsx @@ -141,34 +141,36 @@ const initialExpanded = window.localStorage.getItem('patternfly-navigation-prima export const MainMenu = (props) => { const { applianceName, currentGroup, currentUser, customBrand, customLogo, logoLarge, logoSmall, menu, miqGroups, showLogo, showUser } = props; const [expanded, setExpanded] = useState(initialExpanded); + const [activeSectionItems, setSection] = useState(null); + + let appearExpanded = expanded || !!activeSectionItems; useEffect(() => { window.localStorage.setItem('patternfly-navigation-primary', expanded ? 'expanded' : 'collapsed'); + }, [expanded]); + useEffect(() => { const classNames = { true: 'miq-main-menu-expanded', false: 'miq-main-menu-collapsed', }; - document.body.classList.remove(classNames[!expanded]); - document.body.classList.add(classNames[expanded]); - }, [expanded]); + document.body.classList.remove(classNames[!appearExpanded]); + document.body.classList.add(classNames[appearExpanded]); + }, [appearExpanded]); const miqLogo = () => ( ); - const [activeSectionItems, setSection] = useState(null); - - return ( <> {showLogo && { @@ -200,7 +202,7 @@ export const MainMenu = (props) => { setExpanded(!expanded)} /> From fc7cb69e3b7663009a1e1b04ed15eea64accb04f Mon Sep 17 00:00:00 2001 From: Martin Hradil Date: Fri, 8 May 2020 01:24:59 +0000 Subject: [PATCH 30/57] MenuSearch, SearchResults - search and display the results does a case insensitive substring search on each item title, including parent titles ("compute clouds providers") --- app/javascript/menu/main-menu.jsx | 32 +++++----- app/javascript/menu/search-results.jsx | 48 +++++++++++++++ app/javascript/menu/search.jsx | 81 ++++++++++++++++++++++++++ app/stylesheet/menu.scss | 8 +++ 4 files changed, 154 insertions(+), 15 deletions(-) create mode 100644 app/javascript/menu/search-results.jsx create mode 100644 app/javascript/menu/search.jsx diff --git a/app/javascript/menu/main-menu.jsx b/app/javascript/menu/main-menu.jsx index e8d29f683ad..b933335c261 100644 --- a/app/javascript/menu/main-menu.jsx +++ b/app/javascript/menu/main-menu.jsx @@ -9,10 +9,11 @@ import { SideNavMenu, SideNavMenuItem, } from 'carbon-components-react/es/components/UIShell'; -import Search from 'carbon-components-react/es/components/Search'; import { ChevronLeft20, ChevronRight20 } from '@carbon/icons-react'; import { GroupSwitcher } from './group-switcher'; +import { MenuSearch } from './search'; +import { SearchResults } from './search-results'; import { MiqLogo } from './miq-logo'; import { carbonizeIcon } from './icon'; import { itemId, linkProps } from './item-type'; @@ -115,15 +116,6 @@ MenuSection.props = { }; -const MenuFind = () => ( - - - -); - const MenuCollapse = ({ expanded, toggle }) => (
    { const { applianceName, currentGroup, currentUser, customBrand, customLogo, logoLarge, logoSmall, menu, miqGroups, showLogo, showUser } = props; const [expanded, setExpanded] = useState(initialExpanded); const [activeSectionItems, setSection] = useState(null); + const [searchResults, setSearch] = useState(null); - let appearExpanded = expanded || !!activeSectionItems; + let appearExpanded = expanded || !!activeSectionItems || !!searchResults; useEffect(() => { window.localStorage.setItem('patternfly-navigation-primary', expanded ? 'expanded' : 'collapsed'); @@ -193,13 +186,22 @@ export const MainMenu = (props) => { miqGroups={miqGroups} /> - +
    - - {mapItems(menu, 0, true, setSection)} - + { searchResults && ( + + )} + { !searchResults && ( + + {mapItems(menu, 0, true, setSection)} + + )} ( + +

    + {titles[titles.length - 1]} +

    +

    + {(titles.length > 1) && titles[0]} + {(titles.length > 2) && ' / '} + {(titles.length > 2) && titles[1]} +

    +
    +); + +// always expanded, or null +export const SearchResults = ({ results }) => ( + +

    + {__("Results")} +   + ({results.length}) +

    + + {results.map(({ item, titles }) => ( + + ))} +
    +); + +SearchResults.propTypes = { + results: PropTypes.arrayOf(PropTypes.shape({ + item: PropTypes.shape({}).isRequired, + titles: PropTypes.arrayOf(PropTypes.string).isRequired, + })).isRequired, +}; diff --git a/app/javascript/menu/search.jsx b/app/javascript/menu/search.jsx new file mode 100644 index 00000000000..497445c3292 --- /dev/null +++ b/app/javascript/menu/search.jsx @@ -0,0 +1,81 @@ +import React from 'react'; +import PropTypes from 'prop-types'; + +import Search from 'carbon-components-react/es/components/Search'; +import { Search20 } from '@carbon/icons-react'; +import { SideNavItem } from 'carbon-components-react/es/components/UIShell'; + +const flatten = (menuItems = []) => { + const flat = []; + + const process = (items, parents = []) => { + items.forEach((item) => { + const newParents = [...parents, item]; + + if (item.items && item.items.length) { + // section + process(item.items, newParents); + } else { + // item + const titles = newParents.map((p) => p.title); + const haystack = titles.join(' ').toLocaleLowerCase(); + + flat.push({ + haystack, + item, + titles, + }); + } + }); + } + + process(menuItems); + return flat; +}; + +export const MenuSearch = ({ expanded, menu, onSearch }) => { + if (! expanded) { + return ( + + + + ); + } + + const flatMenu = flatten(menu); + + const searchResults = (string) => { + if (!string || string.match(/^\s*$/)) { + onSearch(null); + return; + } + + const needle = string.toLocaleLowerCase(); + const results = flatMenu.filter((item) => item.haystack.includes(needle)); + + onSearch(results); + }; + + return ( + + + ); +}; + +MenuSearch.propTypes = { + expanded: PropTypes.bool.isRequired, + menu: PropTypes.arrayOf(PropTypes.shape({ + title: PropTypes.string.isRequired, + })).isRequired, + onSearch: PropTypes.func, +}; + +MenuSearch.defaultProps = { + onSearch: () => null, +}; diff --git a/app/stylesheet/menu.scss b/app/stylesheet/menu.scss index aaed9b4d6e2..2be5a180212 100644 --- a/app/stylesheet/menu.scss +++ b/app/stylesheet/menu.scss @@ -65,6 +65,14 @@ width: 48px; } } + + .menu-search-title { + font-size: small; + } + .menu-search-parent { + font-size: smaller; + font-weight: 400; + } } .layout-pf.layout-pf-fixed { From ab8c2d52f9bfe53f9a3e6ea886b264509d5191c0 Mon Sep 17 00:00:00 2001 From: Martin Hradil Date: Fri, 8 May 2020 01:27:28 +0000 Subject: [PATCH 31/57] menu Username - display username, convert to initials when collapsed --- app/javascript/menu/main-menu.jsx | 32 +++++++++++++++++++++---------- 1 file changed, 22 insertions(+), 10 deletions(-) diff --git a/app/javascript/menu/main-menu.jsx b/app/javascript/menu/main-menu.jsx index b933335c261..7b80951fe05 100644 --- a/app/javascript/menu/main-menu.jsx +++ b/app/javascript/menu/main-menu.jsx @@ -127,6 +127,23 @@ const MenuCollapse = ({ expanded, toggle }) => ( ); +const Username = ({ applianceName, currentUser, expanded }) => { + const title = `${currentUser.name} | ${currentUser.userid} | ${applianceName}`; + const initials = Array.from(currentUser.name).filter((x) => x.match(/\p{Upper}/u)).join('').substr(0, 3) || currentUser.name[0]; + + return ( + +

    + { expanded ? currentUser.name : initials } +

    +
    + ); +}; + const initialExpanded = window.localStorage.getItem('patternfly-navigation-primary') !== 'collapsed'; @@ -169,16 +186,11 @@ export const MainMenu = (props) => { renderIcon={miqLogo} />} - {/* FIXME initials, collapsed.. */} - {showUser && -

    - {currentUser.name} -

    -
    } + {showUser && } Date: Fri, 8 May 2020 01:28:07 +0000 Subject: [PATCH 32/57] menu color fixes --- app/stylesheet/menu.scss | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/app/stylesheet/menu.scss b/app/stylesheet/menu.scss index 2be5a180212..29ed2bbb581 100644 --- a/app/stylesheet/menu.scss +++ b/app/stylesheet/menu.scss @@ -12,9 +12,21 @@ &.second { background-color: $ui-01; + + .bx--side-nav__item--active > .bx--side-nav__submenu, + .bx--side-nav__link.bx--side-nav__link--current, + .bx--side-nav__link:hover, + .bx--side-nav__submenu[aria-expanded=false]:hover { + background-color: $active-ui; + } } } + .bx--side-nav__link:hover, + .bx--side-nav__link.bx--side-nav__link--current { + background-color: $active-secondary; + } + .bx--side-nav__link-text, .bx--side-nav__submenu-title { color: $text-01; @@ -37,6 +49,14 @@ left: 16rem; max-width: 14rem; width: 14rem; + + .bx--side-nav__link-text { + font-weight: 400; + } + } + + .bx--side-nav__item.padded { + padding: 0 1rem; } .bx--side-nav__header, @@ -50,7 +70,7 @@ .bx--side-nav__hr { margin: 16px 0 0 0; } - .bx--side-nav__expanded { + .bx--side-nav--expanded { .bx--side-nav__hr { margin: 16px 16px 0 16px; } From 145a36566d46fce0c2e10a3f045a3917a654d5ab Mon Sep 17 00:00:00 2001 From: Martin Hradil Date: Fri, 8 May 2020 01:44:11 +0000 Subject: [PATCH 33/57] Menu FirstLevelSection - add chevrons --- app/javascript/menu/main-menu.jsx | 45 +++++++++++++++++++++++-------- 1 file changed, 34 insertions(+), 11 deletions(-) diff --git a/app/javascript/menu/main-menu.jsx b/app/javascript/menu/main-menu.jsx index 7b80951fe05..5dc63f95687 100644 --- a/app/javascript/menu/main-menu.jsx +++ b/app/javascript/menu/main-menu.jsx @@ -1,14 +1,17 @@ import React, { useEffect, useState } from 'react'; +import cx from 'classnames'; import PropTypes from 'prop-types'; import { SideNav, SideNavHeader, + SideNavIcon, SideNavItem, SideNavItems, SideNavLink, SideNavMenu, SideNavMenuItem, } from 'carbon-components-react/es/components/UIShell'; +import Link from 'carbon-components-react/es/components/UIShell/Link'; import { ChevronLeft20, ChevronRight20 } from '@carbon/icons-react'; import { GroupSwitcher } from './group-switcher'; @@ -83,17 +86,37 @@ const menuSectionProps = { title: PropTypes.string.isRequired, }; -// TODO items chevron -const FirstLevelSection = ({ active, id, items, title, icon, setSection }) => ( - setSection(items)} - > - {title} - -); +// really a SideNavLink with a chevron from SideNavMenu instead of SideNavLinkText +const FirstLevelSection = ({ active, id, items, title, icon, setSection }) => { + const className = cx({ + 'bx--side-nav__link': true, + 'bx--side-nav__link--current': active, + }); + const IconElement = carbonizeIcon(icon); + + return ( + + setSection(items)} + > + {IconElement && ( + + + + )} + + + {title} + + + + + + + + ); +}; FirstLevelSection.props = { ...menuSectionProps, From 2be83e94cc0bb692a0b3e6b0274a32a8928edce1 Mon Sep 17 00:00:00 2001 From: Martin Hradil Date: Mon, 11 May 2020 13:59:42 +0000 Subject: [PATCH 34/57] Menu - add overlay, clicking in menu hides sections any click in the first level navigation that's not stopped will close the secondary navigation so will any click on the overlay and added the overlay --- app/javascript/menu/main-menu.jsx | 117 ++++++++++++++++-------------- app/stylesheet/menu.scss | 11 +++ app/stylesheet/navbar.scss | 6 +- 3 files changed, 80 insertions(+), 54 deletions(-) diff --git a/app/javascript/menu/main-menu.jsx b/app/javascript/menu/main-menu.jsx index 5dc63f95687..d5ef4dde0a1 100644 --- a/app/javascript/menu/main-menu.jsx +++ b/app/javascript/menu/main-menu.jsx @@ -22,7 +22,7 @@ import { carbonizeIcon } from './icon'; import { itemId, linkProps } from './item-type'; -const mapItems = (items, level = 0, root = true, setSection = null) => items.map((item) => ( +const mapItems = (items, level = 0, setSection = null) => items.map((item) => ( item.items.length ? ( level @@ -98,7 +98,7 @@ const FirstLevelSection = ({ active, id, items, title, icon, setSection }) => { setSection(items)} + onClick={(e) => {setSection(items); e.stopPropagation();}} > {IconElement && ( @@ -130,7 +130,7 @@ const MenuSection = ({ active, id, items, title, icon, level }) => ( renderIcon={carbonizeIcon(icon)} // only first level sections have it, but all need the prop for consistent padding title={title} > - {mapItems(items, level + 1, false)} + {mapItems(items, level + 1)} ); @@ -167,6 +167,10 @@ const Username = ({ applianceName, currentUser, expanded }) => { ); }; +const MiqOverlay = ({onClick}) => ( +
    +); + const initialExpanded = window.localStorage.getItem('patternfly-navigation-primary') !== 'collapsed'; @@ -176,6 +180,8 @@ export const MainMenu = (props) => { const [activeSectionItems, setSection] = useState(null); const [searchResults, setSearch] = useState(null); + const hideSecondary = (e) => setSection(null); + let appearExpanded = expanded || !!activeSectionItems || !!searchResults; useEffect(() => { @@ -200,60 +206,65 @@ export const MainMenu = (props) => { return ( <> - - {showLogo && } - - {showUser && } - - - - - -
    - - { searchResults && ( - - )} - { !searchResults && ( - - {mapItems(menu, 0, true, setSection)} - - )} - - setExpanded(!expanded)} - /> -
    - { activeSectionItems && ( +
    - - {mapItems(activeSectionItems, 1, true, setSection)} - + {showLogo && } + + {showUser && } + + + + + +
    + + { searchResults && ( + + )} + { !searchResults && ( + + {mapItems(menu, 0, setSection)} + + )} + + setExpanded(!expanded)} + />
    +
    + { activeSectionItems && ( + <> + + + {mapItems(activeSectionItems, 1)} + + + + )} ); diff --git a/app/stylesheet/menu.scss b/app/stylesheet/menu.scss index 29ed2bbb581..395a8ba81a6 100644 --- a/app/stylesheet/menu.scss +++ b/app/stylesheet/menu.scss @@ -107,3 +107,14 @@ } } } + +// only appears when secondary menu is expanded +.miq-main-menu-overlay { + background-color: #000; + bottom: 0; + left: 30rem; + opacity: 0.5; + position: fixed; + right: 0; + z-index: 10000; +} diff --git a/app/stylesheet/navbar.scss b/app/stylesheet/navbar.scss index b7c621b4df2..03a4e90f09d 100644 --- a/app/stylesheet/navbar.scss +++ b/app/stylesheet/navbar.scss @@ -1,3 +1,7 @@ .layout-pf.layout-pf-fixed body { - padding-top: 0px; + padding-top: 0; +} + +.miq-main-menu-overlay { + top: 0; } From 57a05ad22ec615a6d173a4db86250d77f5572577 Mon Sep 17 00:00:00 2001 From: Martin Hradil Date: Mon, 11 May 2020 14:36:58 +0000 Subject: [PATCH 35/57] menu positioning - icon --- app/stylesheet/menu.scss | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/app/stylesheet/menu.scss b/app/stylesheet/menu.scss index 395a8ba81a6..16b0009ed7a 100644 --- a/app/stylesheet/menu.scss +++ b/app/stylesheet/menu.scss @@ -55,6 +55,7 @@ } } + .bx--side-nav__header.padded, .bx--side-nav__item.padded { padding: 0 1rem; } @@ -118,3 +119,9 @@ right: 0; z-index: 10000; } + +.navbar-brand-name { + height: 32px; + // compensate for in-image padding + margin: -4px 0 0 -8px; +} From 7ce55c3bab941b461f6983a4c9231c19494f1fd2 Mon Sep 17 00:00:00 2001 From: Martin Hradil Date: Mon, 11 May 2020 16:25:42 +0000 Subject: [PATCH 36/57] menu - reorganize into files, simplify first&second level, document --- app/javascript/menu/README.md | 33 +++ app/javascript/menu/first-level.jsx | 61 +++++ app/javascript/menu/main-menu.jsx | 302 +++++---------------- app/javascript/menu/menu-collapse.jsx | 14 + app/javascript/menu/miq-logo.jsx | 49 +++- app/javascript/menu/second-level.jsx | 55 ++++ app/javascript/menu/side-nav-menu-link.jsx | 52 ++++ app/javascript/menu/username.jsx | 19 ++ 8 files changed, 342 insertions(+), 243 deletions(-) create mode 100644 app/javascript/menu/README.md create mode 100644 app/javascript/menu/first-level.jsx create mode 100644 app/javascript/menu/menu-collapse.jsx create mode 100644 app/javascript/menu/second-level.jsx create mode 100644 app/javascript/menu/side-nav-menu-link.jsx create mode 100644 app/javascript/menu/username.jsx diff --git a/app/javascript/menu/README.md b/app/javascript/menu/README.md new file mode 100644 index 00000000000..4d068ef2693 --- /dev/null +++ b/app/javascript/menu/README.md @@ -0,0 +1,33 @@ +## ManageIQ Main Menu & Navbar + +This code exports 2 registry components: + +* `menu.MainMenu` - the carbon main menu +* `menu.Navbar` - an empty navbar stub, overidable, + `app/stylesheet/navbar.scss` + + +### Main Menu + +The menu consists of a permanent SideNav, expanded or collapsed, +and a secondary pop-up SideNav with an overlay, always expanded. + +The primary pane remembers the collapsed state, but selecting a section or triggering the search or class selector forces it to expand. + +Document `body` additionally gets a `miq-main-menu-expanded` or `miq-main-menu-collapsed` class. + +Expanded state is persisted in `localStorage`, by setting `patternfly-navigation-primary` to either `'collapsed'` or `'expanded'`. + + +**Components** (in order): + +* `MiqLogo` - manageiq logo, uses `logoLarge` and `logoSmall` for the URLs, and is hidden when `showLogo` is false +* `Username` - username, appliance name; collapses to initials; uses `applianceName`, `currentUser`, is hidden when `showUser` is false +* `GroupSwitcher` - display or switch current group; uses `currentGroup`, `miqGroups` +* `MenuSearch` - search in menu component, `onSearch` provides an array of results or null +* when searching: `SearchResults` - list of menu items +* when not searching: `FirstLevel` - the actual menu +* `MenuCollapse` - bottom expand/collapse button + +Second level SideNav only contains `SecondLevel` which is a simple `SideNavItems` with `SideNavMenu` and `SideNavMenuItem` items. + +`FirstLevel` is a `SideNavItems` with `SideNavLink` for items, but needs a custom `SideNavMenuLink` to display the right chevron for sections. diff --git a/app/javascript/menu/first-level.jsx b/app/javascript/menu/first-level.jsx new file mode 100644 index 00000000000..91162cc5b1b --- /dev/null +++ b/app/javascript/menu/first-level.jsx @@ -0,0 +1,61 @@ +import React, { useEffect, useState } from 'react'; +import PropTypes from 'prop-types'; +import { SideNavItems, SideNavLink } from 'carbon-components-react/es/components/UIShell'; +import { SideNavMenuLink } from './side-nav-menu-link'; +import { carbonizeIcon } from './icon'; +import { itemId, linkProps } from './item-type'; + +const mapItems = (items, setSection) => items.map((item) => ( + item.items.length + ? + : +)); + + +// SideNavMenuItem can't render an icon, SideNavLink can +const MenuItem = ({ active, href, icon, id, title, type }) => ( + + {title} + +); + +MenuItem.props = { + active: PropTypes.bool, + href: PropTypes.string.isRequired, + icon: PropTypes.string, + id: PropTypes.string.isRequired, + title: PropTypes.string.isRequired, + type: PropTypes.string.isRequired, +}; + + +const MenuSection = ({ active, icon, id, items, title, setSection }) => ( + {setSection(items); e.stopPropagation();}} + renderIcon={carbonizeIcon(icon)} + title={title} + /> +); + +MenuSection.props = { + active: PropTypes.bool, + icon: PropTypes.string, + id: PropTypes.string.isRequired, + items: PropTypes.arrayOf(PropTypes.any).isRequired, + setSection: PropTypes.func, + title: PropTypes.string.isRequired, +}; + + +export const FirstLevel = ({ menu, setSection }) => ( + + {mapItems(menu, setSection)} + +); diff --git a/app/javascript/menu/main-menu.jsx b/app/javascript/menu/main-menu.jsx index d5ef4dde0a1..c8ab9be01a3 100644 --- a/app/javascript/menu/main-menu.jsx +++ b/app/javascript/menu/main-menu.jsx @@ -1,175 +1,15 @@ import React, { useEffect, useState } from 'react'; -import cx from 'classnames'; import PropTypes from 'prop-types'; -import { - SideNav, - SideNavHeader, - SideNavIcon, - SideNavItem, - SideNavItems, - SideNavLink, - SideNavMenu, - SideNavMenuItem, -} from 'carbon-components-react/es/components/UIShell'; -import Link from 'carbon-components-react/es/components/UIShell/Link'; -import { ChevronLeft20, ChevronRight20 } from '@carbon/icons-react'; +import { SideNav } from 'carbon-components-react/es/components/UIShell'; +import { FirstLevel } from './first-level'; import { GroupSwitcher } from './group-switcher'; +import { MenuCollapse } from './menu-collapse'; import { MenuSearch } from './search'; -import { SearchResults } from './search-results'; import { MiqLogo } from './miq-logo'; -import { carbonizeIcon } from './icon'; -import { itemId, linkProps } from './item-type'; - - -const mapItems = (items, level = 0, setSection = null) => items.map((item) => ( - item.items.length - ? ( - level - ? - : - ) - : ( - level - ? - : - ) -)); - - -const menuItemProps = { - active: PropTypes.bool, - href: PropTypes.string.isRequired, - id: PropTypes.string.isRequired, - title: PropTypes.string.isRequired, - type: PropTypes.string.isRequired, -}; - -// SideNavLink for first level - needed for icon -const FirstLevelItem = ({ active, href, icon, id, title, type }) => ( - - {title} - -); - -FirstLevelItem.props = { - ...menuItemProps, - icon: PropTypes.string, -}; - -// SideNavMenuItem can't render icon, but we only have first level icons -const MenuItem = ({ active, href, id, title, type }) => ( - - {title} - -); - -MenuItem.props = { - ...menuItemProps, -}; - - -const menuSectionProps = { - active: PropTypes.bool, - icon: PropTypes.string, - id: PropTypes.string.isRequired, - items: PropTypes.arrayOf(PropTypes.any).isRequired, - title: PropTypes.string.isRequired, -}; - -// really a SideNavLink with a chevron from SideNavMenu instead of SideNavLinkText -const FirstLevelSection = ({ active, id, items, title, icon, setSection }) => { - const className = cx({ - 'bx--side-nav__link': true, - 'bx--side-nav__link--current': active, - }); - const IconElement = carbonizeIcon(icon); - - return ( - - {setSection(items); e.stopPropagation();}} - > - {IconElement && ( - - - - )} - - - {title} - - - - - - - - ); -}; - -FirstLevelSection.props = { - ...menuSectionProps, -}; - -const MenuSection = ({ active, id, items, title, icon, level }) => ( - - {mapItems(items, level + 1)} - -); - -MenuSection.props = { - ...menuSectionProps, -}; - - -const MenuCollapse = ({ expanded, toggle }) => ( - -
    - {expanded ? : } -
    -
    -); - -const Username = ({ applianceName, currentUser, expanded }) => { - const title = `${currentUser.name} | ${currentUser.userid} | ${applianceName}`; - const initials = Array.from(currentUser.name).filter((x) => x.match(/\p{Upper}/u)).join('').substr(0, 3) || currentUser.name[0]; - - return ( - -

    - { expanded ? currentUser.name : initials } -

    -
    - ); -}; - -const MiqOverlay = ({onClick}) => ( -
    -); +import { SearchResults } from './search-results'; +import { SecondLevel } from './second-level'; +import { Username } from './username'; const initialExpanded = window.localStorage.getItem('patternfly-navigation-primary') !== 'collapsed'; @@ -197,76 +37,76 @@ export const MainMenu = (props) => { document.body.classList.add(classNames[appearExpanded]); }, [appearExpanded]); - const miqLogo = () => ( - - ); - return ( - <> -
    - +
    - {showLogo && } - - {showUser && } - - - - - -
    - - { searchResults && ( - - )} - { !searchResults && ( - - {mapItems(menu, 0, setSection)} - - )} - - setExpanded(!expanded)} - /> - -
    - { activeSectionItems && ( - <> - - {mapItems(activeSectionItems, 1)} - + {showLogo && } + + {showUser && } + + + + + +
    + + {searchResults && } + {!searchResults && } + + setExpanded(!expanded)} + />
    - - - )} - +
    + { activeSectionItems && ( + <> + + + +
    + + )} + ); }; @@ -284,7 +124,7 @@ MainMenu.propTypes = { applianceName: PropTypes.string.isRequired, currentGroup: propGroup.isRequired, currentUser: propUser.isRequired, - customBrand: PropTypes.bool.isRequired, + customBrand: PropTypes.bool, customLogo: PropTypes.bool.isRequired, logoLarge: PropTypes.string, logoSmall: PropTypes.string, diff --git a/app/javascript/menu/menu-collapse.jsx b/app/javascript/menu/menu-collapse.jsx new file mode 100644 index 00000000000..11c2ef00775 --- /dev/null +++ b/app/javascript/menu/menu-collapse.jsx @@ -0,0 +1,14 @@ +import React from 'react'; +import { ChevronLeft20, ChevronRight20 } from '@carbon/icons-react'; +import { SideNavItem } from 'carbon-components-react/es/components/UIShell'; + +export const MenuCollapse = ({ expanded, toggle }) => ( + +
    + {expanded ? : } +
    +
    +); diff --git a/app/javascript/menu/miq-logo.jsx b/app/javascript/menu/miq-logo.jsx index 566a257d4ca..eb23d3d0bd7 100644 --- a/app/javascript/menu/miq-logo.jsx +++ b/app/javascript/menu/miq-logo.jsx @@ -1,24 +1,48 @@ import React from 'react'; import PropTypes from 'prop-types'; +import { SideNavHeader } from 'carbon-components-react/es/components/UIShell'; -export const MiqLogo = ({href, title, alt, customBrand, imagePath}) => ( - - {alt} { + const url = customBrand ? logoCustom : (expanded ? logoLarge : logoSmall); + + const miqLogo = () => ( + + {alt} + + ); + + return ( + - -); + ); +}; MiqLogo.propTypes = { alt: PropTypes.string, customBrand: PropTypes.bool, + expanded: PropTypes.bool.isRequired, href: PropTypes.string, - imagePath: PropTypes.string.isRequired, + logoCustom: PropTypes.string, + logoLarge: PropTypes.string.isRequired, + logoSmall: PropTypes.string.isRequired, title: PropTypes.string, }; @@ -26,5 +50,6 @@ MiqLogo.defaultProps = { alt: 'ManageIQ', customBrand: false, href: '/dashboard/start_url', + logoCustom: '/upload/custom_brand.png', title: __('Go to my start page'), }; diff --git a/app/javascript/menu/second-level.jsx b/app/javascript/menu/second-level.jsx new file mode 100644 index 00000000000..960785bbc04 --- /dev/null +++ b/app/javascript/menu/second-level.jsx @@ -0,0 +1,55 @@ +import React, { useEffect, useState } from 'react'; +import PropTypes from 'prop-types'; +import { SideNavItems, SideNavMenu, SideNavMenuItem } from 'carbon-components-react/es/components/UIShell'; +import { itemId, linkProps } from './item-type'; + +const mapItems = (items) => items.map((item) => ( + item.items.length + ? + : +)); + + +const MenuItem = ({ active, href, id, title, type }) => ( + + {title} + +); + +MenuItem.props = { + active: PropTypes.bool, + href: PropTypes.string.isRequired, + id: PropTypes.string.isRequired, + title: PropTypes.string.isRequired, + type: PropTypes.string.isRequired, +}; + + +const MenuSection = ({ active, id, items, title }) => ( + + {mapItems(items)} + +); + +MenuSection.props = { + active: PropTypes.bool, + id: PropTypes.string.isRequired, + items: PropTypes.arrayOf(PropTypes.any).isRequired, + title: PropTypes.string.isRequired, +}; + + +export const SecondLevel = ({ menu }) => ( + + {mapItems(menu)} + +); diff --git a/app/javascript/menu/side-nav-menu-link.jsx b/app/javascript/menu/side-nav-menu-link.jsx new file mode 100644 index 00000000000..3f921d0091c --- /dev/null +++ b/app/javascript/menu/side-nav-menu-link.jsx @@ -0,0 +1,52 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { SideNavIcon, SideNavItem } from 'carbon-components-react/es/components/UIShell'; +import Link from 'carbon-components-react/es/components/UIShell/Link'; +import { ChevronRight20 } from '@carbon/icons-react'; +import cx from 'classnames'; + +// SideNavLink with a chevron from SideNavMenu instead of SideNavLinkText +// has an onClick, not items like SideNavMenu +export const SideNavMenuLink = ({ + id, + isActive, + onClick, + renderIcon: IconElement, + title, +}) => { + const className = cx({ + 'bx--side-nav__link': true, + 'bx--side-nav__link--current': isActive, + }); + + return ( + + + {IconElement && ( + + + + )} + + + {title} + + + + + + + + ); +}; + +SideNavMenuLink.propTypes = { + id: PropTypes.string.isRequired, + isActive: PropTypes.bool, + onClick: PropTypes.func.isRequired, + renderIcon: PropTypes.oneOfType([PropTypes.func, PropTypes.object]), + title: PropTypes.string.isRequired, +}; diff --git a/app/javascript/menu/username.jsx b/app/javascript/menu/username.jsx new file mode 100644 index 00000000000..cd263f2e122 --- /dev/null +++ b/app/javascript/menu/username.jsx @@ -0,0 +1,19 @@ +import React from 'react'; +import { SideNavItem } from 'carbon-components-react/es/components/UIShell'; + +export const Username = ({ applianceName, currentUser, expanded }) => { + const title = `${currentUser.name} | ${currentUser.userid} | ${applianceName}`; + const initials = Array.from(currentUser.name).filter((x) => x.match(/\p{Upper}/u)).join('').substr(0, 3) || currentUser.name[0]; + + return ( + +

    + { expanded ? currentUser.name : initials } +

    +
    + ); +}; From a98a701d2b58db47030d8071811f8f50b6a175fc Mon Sep 17 00:00:00 2001 From: Martin Hradil Date: Thu, 14 May 2020 14:57:43 +0000 Subject: [PATCH 37/57] menu username styling --- app/javascript/menu/group-switcher.jsx | 4 ++-- app/javascript/menu/username.jsx | 5 +++-- app/stylesheet/menu.scss | 12 ++++++++++++ 3 files changed, 17 insertions(+), 4 deletions(-) diff --git a/app/javascript/menu/group-switcher.jsx b/app/javascript/menu/group-switcher.jsx index 397ae7fd565..ea5db1f77b6 100644 --- a/app/javascript/menu/group-switcher.jsx +++ b/app/javascript/menu/group-switcher.jsx @@ -37,13 +37,13 @@ export const GroupSwitcher = ({ miqGroups, currentGroup, expanded }) => { onChange={groupChange} /> ) : ( - + {currentOption.label} )} ) : ( - + )} diff --git a/app/javascript/menu/username.jsx b/app/javascript/menu/username.jsx index cd263f2e122..f9c6a67a44d 100644 --- a/app/javascript/menu/username.jsx +++ b/app/javascript/menu/username.jsx @@ -7,13 +7,14 @@ export const Username = ({ applianceName, currentUser, expanded }) => { return ( -

    { expanded ? currentUser.name : initials } -

    +
    ); }; diff --git a/app/stylesheet/menu.scss b/app/stylesheet/menu.scss index 16b0009ed7a..544e7da2434 100644 --- a/app/stylesheet/menu.scss +++ b/app/stylesheet/menu.scss @@ -125,3 +125,15 @@ // compensate for in-image padding margin: -4px 0 0 -8px; } + +#username_display.collapsed { + display: block; + width: 32px; + height: 32px; + margin-left: -8px; + text-align: center; + border-radius: 24px; + line-height: 24px; + background-color: $ui-02; + margin-top: 8px; +} From b961aa24abc5548ec7f345e880d4a68f7e0c7295 Mon Sep 17 00:00:00 2001 From: Martin Hradil Date: Fri, 22 May 2020 11:57:28 +0000 Subject: [PATCH 38/57] Menu - ignore placement by default new menu has no concept of placement, but not removing yet --- app/helpers/application_helper/navbar.rb | 2 +- app/presenters/menu/manager.rb | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/helpers/application_helper/navbar.rb b/app/helpers/application_helper/navbar.rb index b99e9daf4fb..3f0625888fb 100644 --- a/app/helpers/application_helper/navbar.rb +++ b/app/helpers/application_helper/navbar.rb @@ -1,6 +1,6 @@ module ApplicationHelper module Navbar - def menu_to_json(placement = :default) + def menu_to_json(placement = :_all) Menu::Manager.menu(placement).map do |menu_section| item_to_hash(menu_section) if menu_section.visible? end.compact diff --git a/app/presenters/menu/manager.rb b/app/presenters/menu/manager.rb index 204886f6a0e..ef999659ecd 100644 --- a/app/presenters/menu/manager.rb +++ b/app/presenters/menu/manager.rb @@ -14,9 +14,9 @@ def each @menu.each { |section| yield section } end - def menu(placement = :default) + def menu(placement = :_all) @menu.select do |menu_section| - menu_section.placement == placement + placement == :_all || menu_section.placement == placement end.compact end From 83fc416e078f7dfa5fdb22f7290011ef7404dafe Mon Sep 17 00:00:00 2001 From: Martin Hradil Date: Fri, 22 May 2020 13:44:50 +0000 Subject: [PATCH 39/57] Carbon spacing & color fixes --- app/javascript/menu/README.md | 14 +++---- app/javascript/menu/first-level.jsx | 2 +- app/javascript/menu/group-switcher.jsx | 9 +++-- app/javascript/menu/miq-logo.jsx | 2 +- app/javascript/menu/search-results.jsx | 17 ++++++--- app/javascript/menu/search.jsx | 4 +- app/javascript/menu/username.jsx | 2 +- app/stylesheet/menu.scss | 51 ++++++++++++++++++++++++-- 8 files changed, 77 insertions(+), 24 deletions(-) diff --git a/app/javascript/menu/README.md b/app/javascript/menu/README.md index 4d068ef2693..243a6543ff0 100644 --- a/app/javascript/menu/README.md +++ b/app/javascript/menu/README.md @@ -20,13 +20,13 @@ Expanded state is persisted in `localStorage`, by setting `patternfly-navigation **Components** (in order): -* `MiqLogo` - manageiq logo, uses `logoLarge` and `logoSmall` for the URLs, and is hidden when `showLogo` is false -* `Username` - username, appliance name; collapses to initials; uses `applianceName`, `currentUser`, is hidden when `showUser` is false -* `GroupSwitcher` - display or switch current group; uses `currentGroup`, `miqGroups` -* `MenuSearch` - search in menu component, `onSearch` provides an array of results or null -* when searching: `SearchResults` - list of menu items -* when not searching: `FirstLevel` - the actual menu -* `MenuCollapse` - bottom expand/collapse button +* `MiqLogo` (`.menu-logo`) - manageiq logo, uses `logoLarge` and `logoSmall` for the URLs, and is hidden when `showLogo` is false +* `Username` (`.menu-user`) - username, appliance name; collapses to initials; uses `applianceName`, `currentUser`, is hidden when `showUser` is false +* `GroupSwitcher` (`.menu-group`) - display or switch current group; uses `currentGroup`, `miqGroups` +* `MenuSearch` (`.menu-search`) - search in menu component, `onSearch` provides an array of results or null +* when searching: `SearchResults` (`.menu-results`) - list of menu items +* when not searching: `FirstLevel` (`.menu-items`) - the actual menu +* `MenuCollapse` (`.menu-collapse`) - bottom expand/collapse button Second level SideNav only contains `SecondLevel` which is a simple `SideNavItems` with `SideNavMenu` and `SideNavMenuItem` items. diff --git a/app/javascript/menu/first-level.jsx b/app/javascript/menu/first-level.jsx index 91162cc5b1b..f9198f1a064 100644 --- a/app/javascript/menu/first-level.jsx +++ b/app/javascript/menu/first-level.jsx @@ -55,7 +55,7 @@ MenuSection.props = { export const FirstLevel = ({ menu, setSection }) => ( - + {mapItems(menu, setSection)} ); diff --git a/app/javascript/menu/group-switcher.jsx b/app/javascript/menu/group-switcher.jsx index ea5db1f77b6..69dc2ce7db8 100644 --- a/app/javascript/menu/group-switcher.jsx +++ b/app/javascript/menu/group-switcher.jsx @@ -24,7 +24,10 @@ export const GroupSwitcher = ({ miqGroups, currentGroup, expanded }) => { }; return ( -
    +
    { expanded ? ( <> { options.length > 1 ? ( @@ -37,13 +40,13 @@ export const GroupSwitcher = ({ miqGroups, currentGroup, expanded }) => { onChange={groupChange} /> ) : ( - + {currentOption.label} )} ) : ( - + )} diff --git a/app/javascript/menu/miq-logo.jsx b/app/javascript/menu/miq-logo.jsx index eb23d3d0bd7..4ce938c5411 100644 --- a/app/javascript/menu/miq-logo.jsx +++ b/app/javascript/menu/miq-logo.jsx @@ -29,7 +29,7 @@ export const MiqLogo = ({ return ( ); diff --git a/app/javascript/menu/search-results.jsx b/app/javascript/menu/search-results.jsx index da5ab5b5097..4dfb5f7d777 100644 --- a/app/javascript/menu/search-results.jsx +++ b/app/javascript/menu/search-results.jsx @@ -21,14 +21,19 @@ const ResultItem = ({ item, titles }) => ( ); +// can't use raw p as a descendant of items +const Count = ({ length }) => ( +

    + {__("Results")} +   + ({length}) +

    +); + // always expanded, or null export const SearchResults = ({ results }) => ( - -

    - {__("Results")} -   - ({results.length}) -

    + + {results.map(({ item, titles }) => ( { export const MenuSearch = ({ expanded, menu, onSearch }) => { if (! expanded) { return ( - + ); @@ -57,7 +57,7 @@ export const MenuSearch = ({ expanded, menu, onSearch }) => { }; return ( - + { const initials = Array.from(currentUser.name).filter((x) => x.match(/\p{Upper}/u)).join('').substr(0, 3) || currentUser.name[0]; return ( - + li { + height: 32px; + } + & .bx--dropdown__wrapper { + padding: 0 1rem; + } + } + + .menu-search { + height: 32px; } } @@ -130,10 +176,9 @@ display: block; width: 32px; height: 32px; - margin-left: -8px; + margin: 0 -8px; text-align: center; border-radius: 24px; - line-height: 24px; + line-height: 32px; background-color: $ui-02; - margin-top: 8px; } From d55a9c2653c8bb5f1ee3b6413948b0836753f654 Mon Sep 17 00:00:00 2001 From: Martin Hradil Date: Thu, 28 May 2020 23:06:35 +0000 Subject: [PATCH 40/57] menu - simulate hover effect on opened section be in Overview > Dashboard, click on Compute section, move mouse to secondary menu before: Compute is no longer highlighted, only Overview is after: Overview is active, Compute has a fake hover effect --- app/javascript/menu/first-level.jsx | 27 ++++++++++++++++------ app/javascript/menu/main-menu.jsx | 9 ++++---- app/javascript/menu/side-nav-menu-link.jsx | 4 ++++ app/stylesheet/menu.scss | 2 ++ 4 files changed, 31 insertions(+), 11 deletions(-) diff --git a/app/javascript/menu/first-level.jsx b/app/javascript/menu/first-level.jsx index f9198f1a064..3674e1469c1 100644 --- a/app/javascript/menu/first-level.jsx +++ b/app/javascript/menu/first-level.jsx @@ -5,10 +5,18 @@ import { SideNavMenuLink } from './side-nav-menu-link'; import { carbonizeIcon } from './icon'; import { itemId, linkProps } from './item-type'; -const mapItems = (items, setSection) => items.map((item) => ( +const mapItems = (items, { activeSection, setSection }) => items.map((item) => ( item.items.length - ? - : + ? + : )); @@ -34,11 +42,15 @@ MenuItem.props = { }; -const MenuSection = ({ active, icon, id, items, title, setSection }) => ( +const MenuSection = ({ active, hover, icon, id, items, title, setSection }) => ( {setSection(items); e.stopPropagation();}} + forceHover={hover} + onClick={(e) => { + setSection({ id, items }); + e.stopPropagation(); + }} renderIcon={carbonizeIcon(icon)} title={title} /> @@ -46,6 +58,7 @@ const MenuSection = ({ active, icon, id, items, title, setSection }) => ( MenuSection.props = { active: PropTypes.bool, + hover: PropTypes.bool, icon: PropTypes.string, id: PropTypes.string.isRequired, items: PropTypes.arrayOf(PropTypes.any).isRequired, @@ -54,8 +67,8 @@ MenuSection.props = { }; -export const FirstLevel = ({ menu, setSection }) => ( +export const FirstLevel = ({ activeSection, menu, setSection }) => ( - {mapItems(menu, setSection)} + {mapItems(menu, { setSection, activeSection })} ); diff --git a/app/javascript/menu/main-menu.jsx b/app/javascript/menu/main-menu.jsx index c8ab9be01a3..6360ab53deb 100644 --- a/app/javascript/menu/main-menu.jsx +++ b/app/javascript/menu/main-menu.jsx @@ -17,12 +17,12 @@ const initialExpanded = window.localStorage.getItem('patternfly-navigation-prima export const MainMenu = (props) => { const { applianceName, currentGroup, currentUser, customBrand, customLogo, logoLarge, logoSmall, menu, miqGroups, showLogo, showUser } = props; const [expanded, setExpanded] = useState(initialExpanded); - const [activeSectionItems, setSection] = useState(null); + const [activeSection, setSection] = useState(null); const [searchResults, setSearch] = useState(null); const hideSecondary = (e) => setSection(null); - let appearExpanded = expanded || !!activeSectionItems || !!searchResults; + let appearExpanded = expanded || !!activeSection || !!searchResults; useEffect(() => { window.localStorage.setItem('patternfly-navigation-primary', expanded ? 'expanded' : 'collapsed'); @@ -80,6 +80,7 @@ export const MainMenu = (props) => { {!searchResults && } { />
    - { activeSectionItems && ( + { activeSection && ( <> { isChildOfHeader={false} >
    .bx--side-nav__submenu, .bx--side-nav__link.bx--side-nav__link--current, .bx--side-nav__link:hover, + .bx--side-nav__link.force-hover, .bx--side-nav__submenu[aria-expanded=false]:hover { background-color: $active-ui; } @@ -23,6 +24,7 @@ } .bx--side-nav__link:hover, + .bx--side-nav__link.force-hover, .bx--side-nav__link.bx--side-nav__link--current { background-color: $active-secondary; } From ff55409b68417eba37db8a859e0eb74e3ea309b8 Mon Sep 17 00:00:00 2001 From: Martin Hradil Date: Wed, 27 May 2020 13:05:21 +0000 Subject: [PATCH 41/57] Split off menu-colors from rest of menu css --- app/stylesheet/menu-colors.scss | 64 +++++++++++++++++++++++++++++++++ app/stylesheet/menu.scss | 55 ++-------------------------- 2 files changed, 66 insertions(+), 53 deletions(-) create mode 100644 app/stylesheet/menu-colors.scss diff --git a/app/stylesheet/menu-colors.scss b/app/stylesheet/menu-colors.scss new file mode 100644 index 00000000000..247c7b6c90d --- /dev/null +++ b/app/stylesheet/menu-colors.scss @@ -0,0 +1,64 @@ +$rail-primary-bg: $ui-background; // carbon--gray-90 +$rail-secondary-bg: $ui-01; // carbon--gray-80 + +$row-active-primary-bg: $hover-row; // carbon--gray-80 +$row-active-secondary-bg: $hover-secondary; // carbon--gray-60 + +$row-fg-text: $text-01; // carbon--gray-10 +$row-fg-icon: $icon-01; // carbon--gray-10 + +$user-fg: $text-05; // $carbon--gray-50 + +$divider-fg: $disabled-02; // $carbon--gray-60; + +// unused, theme default +// $find-placeholder-fg: $carbon--gray-50; +// $find-bg: $carbon--gray-80; +// $row-selected-border: $carbon--blue-60; + + +.bx--side-nav { + background-color: $rail-primary-bg; + + .bx--side-nav__link.force-hover, + .bx--side-nav__link:hover, + .bx--side-nav__link.bx--side-nav__link--current { + background-color: $row-active-primary-bg; + } +} + +.bx--side-nav.second { + background-color: $rail-secondary-bg; + + .bx--side-nav__item--active > .bx--side-nav__submenu, + .bx--side-nav__link.bx--side-nav__link--current, + .bx--side-nav__link.force-hover, + .bx--side-nav__link:hover, + .bx--side-nav__submenu[aria-expanded=false]:hover { + background-color: $row-active-secondary-bg; + } +} + +.bx--side-nav__link-text, +.bx--side-nav__submenu-title, +li.menu-group { + color: $row-fg-text; +} + +.bx--side-nav__icon > svg, +.menu-collapse-button > svg, +.menu-group > svg, +.menu-search > svg { + fill: $row-fg-icon; +} +.bx--side-nav__icon > i { + color: $row-fg-icon; +} + +.bx--side-nav__hr { + color: $divider-fg; +} + +.menu-user { + color: $user-fg; +} diff --git a/app/stylesheet/menu.scss b/app/stylesheet/menu.scss index a266ffb0a70..ad2bbebbffe 100644 --- a/app/stylesheet/menu.scss +++ b/app/stylesheet/menu.scss @@ -1,4 +1,6 @@ #main-menu { + @import './menu-colors.scss'; + list-style: none; ul ul { @@ -6,59 +8,6 @@ padding: 0; } - // colors - .bx--side-nav { - background-color: $ui-background; - - &.second { - background-color: $ui-01; - - .bx--side-nav__item--active > .bx--side-nav__submenu, - .bx--side-nav__link.bx--side-nav__link--current, - .bx--side-nav__link:hover, - .bx--side-nav__link.force-hover, - .bx--side-nav__submenu[aria-expanded=false]:hover { - background-color: $active-ui; - } - } - } - - .bx--side-nav__link:hover, - .bx--side-nav__link.force-hover, - .bx--side-nav__link.bx--side-nav__link--current { - background-color: $active-secondary; - } - - .bx--side-nav__link-text, - .bx--side-nav__submenu-title { - color: $text-01; - } - - .bx--side-nav__icon > svg, - .menu-collapse-button > svg { - fill: $icon-01; - } - .bx--side-nav__icon > i { - color: $icon-01; - } - - .bx--side-nav__hr { - color: $text-03; - } - - .menu-user { - color: $carbon--gray-50; - } - - li.menu-group { - color: $carbon--gray-10; - } - - .menu-group svg, - .menu-search svg { - fill: $carbon--gray-10; - } - // sizing .bx--side-nav.second { left: 16rem; From 5a9ad77c7df5d0169db26ea03866b47646ade4ff Mon Sep 17 00:00:00 2001 From: Martin Hradil Date: Fri, 29 May 2020 00:29:03 +0000 Subject: [PATCH 42/57] menu - drop custom logo (not brand) --- app/javascript/menu/main-menu.jsx | 3 +-- app/views/layouts/_header.html.haml | 1 - 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/app/javascript/menu/main-menu.jsx b/app/javascript/menu/main-menu.jsx index 6360ab53deb..27dfda99af8 100644 --- a/app/javascript/menu/main-menu.jsx +++ b/app/javascript/menu/main-menu.jsx @@ -15,7 +15,7 @@ import { Username } from './username'; const initialExpanded = window.localStorage.getItem('patternfly-navigation-primary') !== 'collapsed'; export const MainMenu = (props) => { - const { applianceName, currentGroup, currentUser, customBrand, customLogo, logoLarge, logoSmall, menu, miqGroups, showLogo, showUser } = props; + const { applianceName, currentGroup, currentUser, customBrand, logoLarge, logoSmall, menu, miqGroups, showLogo, showUser } = props; const [expanded, setExpanded] = useState(initialExpanded); const [activeSection, setSection] = useState(null); const [searchResults, setSearch] = useState(null); @@ -126,7 +126,6 @@ MainMenu.propTypes = { currentGroup: propGroup.isRequired, currentUser: propUser.isRequired, customBrand: PropTypes.bool, - customLogo: PropTypes.bool.isRequired, logoLarge: PropTypes.string, logoSmall: PropTypes.string, menu: PropTypes.arrayOf(PropTypes.any).isRequired, diff --git a/app/views/layouts/_header.html.haml b/app/views/layouts/_header.html.haml index 3a70ba080a3..ffc037cd1b9 100644 --- a/app/views/layouts/_header.html.haml +++ b/app/views/layouts/_header.html.haml @@ -9,7 +9,6 @@ :currentGroup => group, :currentUser => user, :customBrand => ::Settings.server.custom_brand, - :customLogo => ::Settings.server.custom_logo, :logoLarge => image_path("layout/manageiq-logo-inverse.svg"), :logoSmall => image_path("layout/manageiq-logo-glyph-inverse.svg"), :menu => menu_to_json, From c6b75679ab2215cb100496c904e90cfef6153d5c Mon Sep 17 00:00:00 2001 From: Martin Hradil Date: Fri, 29 May 2020 01:08:42 +0000 Subject: [PATCH 43/57] cypress - fix menu helper; expose menu globally --- app/assets/javascripts/miq_global.js | 1 + app/javascript/menu/main-menu.jsx | 8 +++- app/stylesheet/menu-colors.scss | 2 +- app/stylesheet/menu.scss | 2 +- cypress/support/commands.js | 70 +++++++++++----------------- 5 files changed, 37 insertions(+), 46 deletions(-) diff --git a/app/assets/javascripts/miq_global.js b/app/assets/javascripts/miq_global.js index b529b2b31db..840d268a2b2 100644 --- a/app/assets/javascripts/miq_global.js +++ b/app/assets/javascripts/miq_global.js @@ -50,6 +50,7 @@ if (!window.ManageIQ) { mark_translated_strings: false, }, logoutInProgress: false, // prevent redirectLogin *during* logout and group change + menu: [], mouse: { x: null, // mouse X coordinate for popup menu y: null, // mouse Y coordinate for popup menu diff --git a/app/javascript/menu/main-menu.jsx b/app/javascript/menu/main-menu.jsx index 27dfda99af8..e6b93576c76 100644 --- a/app/javascript/menu/main-menu.jsx +++ b/app/javascript/menu/main-menu.jsx @@ -37,6 +37,11 @@ export const MainMenu = (props) => { document.body.classList.add(classNames[appearExpanded]); }, [appearExpanded]); + useEffect(() => { + // cypress, debugging + ManageIQ.menu = menu; + }, [menu]); + return ( <>
    { > @@ -93,7 +99,7 @@ export const MainMenu = (props) => { <> diff --git a/app/stylesheet/menu-colors.scss b/app/stylesheet/menu-colors.scss index 247c7b6c90d..cdc2c338702 100644 --- a/app/stylesheet/menu-colors.scss +++ b/app/stylesheet/menu-colors.scss @@ -27,7 +27,7 @@ $divider-fg: $disabled-02; // $carbon--gray-60; } } -.bx--side-nav.second { +.bx--side-nav.secondary { background-color: $rail-secondary-bg; .bx--side-nav__item--active > .bx--side-nav__submenu, diff --git a/app/stylesheet/menu.scss b/app/stylesheet/menu.scss index ad2bbebbffe..88c6e6b958a 100644 --- a/app/stylesheet/menu.scss +++ b/app/stylesheet/menu.scss @@ -9,7 +9,7 @@ } // sizing - .bx--side-nav.second { + .bx--side-nav.secondary { left: 16rem; max-width: 14rem; width: 14rem; diff --git a/cypress/support/commands.js b/cypress/support/commands.js index eb2ad772363..e5f6a7e1a9e 100644 --- a/cypress/support/commands.js +++ b/cypress/support/commands.js @@ -38,55 +38,39 @@ Cypress.Commands.add("login", (user = 'admin', password = 'smartvm') => { Cypress.Commands.add("menu", (...items) => { expect(items.length).to.be.within(1, 3); - const selectors = [ - '#main-menu', - '.nav-pf-secondary-nav', - '.nav-pf-tertiary-nav', - ]; - - let ret = cy.get(`${selectors[0]} > ul > li`) - .contains('a', items[0]) - .parent(); - - items.forEach((item, index) => { - if (index === 0) { - return; - } + const primary = '#main-menu nav.primary'; + const secondary = '#main-menu nav.secondary'; + + let ret = cy.get(`${primary} > ul > li`) + .contains('a > span', items[0]) + .parent().parent() + .click(); + + if (items.length === 2) { + ret = cy.get(`${secondary} > ul > li`) + .contains('a > span', items[1]) + .parent().parent() + .click(); + } - ret = ret.trigger('mouseover') - .find(`${selectors[index]} > ul > li`) - .contains('a', item) - .parent(); - }); + if (items.length === 3) { + ret = cy.get(`${secondary} > ul > li`) + .contains('button > span', items[1]) + .parent().parent() + .click() + .find(`ul > li`) + .contains('a > span', items[2]) + .parent().parent() + .click(); + } - return ret.click(); + return ret; // TODO support by id: cy.get('li[id=menu_item_provider_foreman]').click({ force: true }); }); -// cy.menuItems() - returns an array of top level menu items with {title, href, items (array of children), selector, click() method} +// cy.menuItems() - returns an array of top level menu items with {title, href, items (array of children)} Cypress.Commands.add("menuItems", () => { - const children = (parent, parentSelector, level, ...selectors) => { - if (! parent) - return []; - - const [selector, ...rest] = selectors; - const items = []; - parent.querySelectorAll(':scope > li > a').forEach((el, i) => { - const itemSelector = `${parentSelector} > li:nth-child(${i + 1}) > a`; - items.push({ - title: el.text.trim(), - href: el.href, - items: children(el.parentElement.querySelector(`${selector} > ul`), `${itemSelector.replace(/ > a$/, '')} > div > ul`, level + 1, ...rest), - selector: itemSelector, - click: () => cy.get(itemSelector).click({ force: true }), - }); - }); - - return items; - }; - - return cy.get('#maintab') - .then((maintab) => children(maintab[0], '#maintab', 0, '.nav-pf-secondary-nav', '.nav-pf-tertiary-nav')); + return cy.window().then((window) => window.ManageIQ.menu); }); // cy.accordion('Catalog Items') - click accordion, unless already expanded From 4f99c1887c3264c31ae3c02b15345758dc574149 Mon Sep 17 00:00:00 2001 From: Martin Hradil Date: Fri, 29 May 2020 01:11:33 +0000 Subject: [PATCH 44/57] item-type: add noreferrer noopener rel to external links (help) --- app/javascript/menu/item-type.js | 1 + 1 file changed, 1 insertion(+) diff --git a/app/javascript/menu/item-type.js b/app/javascript/menu/item-type.js index 25d0b8ee557..9347ff6ecd7 100644 --- a/app/javascript/menu/item-type.js +++ b/app/javascript/menu/item-type.js @@ -13,6 +13,7 @@ export const linkProps = ({type, href, id}) => ({ }[type || 'default'], target: (type === 'new_window' ? '_blank' : '_self'), + rel: (type === 'new_window' ? 'noreferrer noopener' : undefined), onClick: (event) => { if (type === 'modal') { From c4ee22c08c193892725c93664f4ea32ffd19d694 Mon Sep 17 00:00:00 2001 From: Martin Hradil Date: Fri, 29 May 2020 16:05:21 +0000 Subject: [PATCH 45/57] Fix breadcrumbs spec - needs redux for the notification button, update snapshot and made isDrawerVisible consistently a bool --- .../__snapshots__/breadcrumbs.spec.js.snap | 260 +++++++++++------- .../spec/breadcrumbs/breadcrumbs.spec.js | 27 +- .../actions/notifications-actions.spec.js | 2 +- .../miq-redux/notification-reducer.spec.js | 2 +- .../notification-drawer.spec.js | 2 +- .../spec/toast-list/toast-list.spec.js | 2 +- config/jest.setup.js | 1 + 7 files changed, 185 insertions(+), 111 deletions(-) diff --git a/app/javascript/spec/breadcrumbs/__snapshots__/breadcrumbs.spec.js.snap b/app/javascript/spec/breadcrumbs/__snapshots__/breadcrumbs.spec.js.snap index a6b87358f8a..6edcfb70c19 100644 --- a/app/javascript/spec/breadcrumbs/__snapshots__/breadcrumbs.spec.js.snap +++ b/app/javascript/spec/breadcrumbs/__snapshots__/breadcrumbs.spec.js.snap @@ -1,123 +1,177 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`Breadcrumbs component is correctly rendered 1`] = ` - - - + -
      - -
    1. - - - Providers - - -
    2. -
      - -
    3. - + + Providers + + +
    4. +
      + - - Google - - - - -
    5. - All -
    6. - + + Google + + + + +
    7. + All +
    8. + +
    9. + + + This + + +
    10. +
      +
    +
    +
    + + -
  • - - - This - - -
  • - - - - -
    + + + + + + + + + + + + + `; diff --git a/app/javascript/spec/breadcrumbs/breadcrumbs.spec.js b/app/javascript/spec/breadcrumbs/breadcrumbs.spec.js index 2eb11178107..95d90d22735 100644 --- a/app/javascript/spec/breadcrumbs/breadcrumbs.spec.js +++ b/app/javascript/spec/breadcrumbs/breadcrumbs.spec.js @@ -1,6 +1,8 @@ import React from 'react'; -import { mount } from 'enzyme'; +import configureStore from 'redux-mock-store'; import toJson from 'enzyme-to-json'; +import { Provider } from 'react-redux'; +import { mount } from 'enzyme'; import * as clickFunctions from '../../components/breadcrumbs/on-click-functions'; import Breadcrumbs from '../../components/breadcrumbs'; @@ -17,8 +19,25 @@ describe('Breadcrumbs component', () => { controllerName: 'provider', }; + const store = configureStore()({ + notificationReducer: { + unreadCount: 0, + isDrawerVisible: false, + }, + }); + + const reduxMount = (data) => { + const Component = () => data; + + return mount( + + + + ); + }; + it('is correctly rendered', () => { - const wrapper = mount(); + const wrapper = reduxMount(); expect(toJson(wrapper)).toMatchSnapshot(); }); @@ -32,7 +51,7 @@ describe('Breadcrumbs component', () => { { title: 'Header' }, ], }; - const wrapper = mount(); + const wrapper = reduxMount(); wrapper.find('a').first().simulate('click'); @@ -49,7 +68,7 @@ describe('Breadcrumbs component', () => { { title: 'Header' }, ], }; - const wrapper = mount(); + const wrapper = reduxMount(); wrapper.find('a').first().simulate('click'); diff --git a/app/javascript/spec/miq-redux/actions/notifications-actions.spec.js b/app/javascript/spec/miq-redux/actions/notifications-actions.spec.js index 994ea7ea21c..e81f37c3e0d 100644 --- a/app/javascript/spec/miq-redux/actions/notifications-actions.spec.js +++ b/app/javascript/spec/miq-redux/actions/notifications-actions.spec.js @@ -31,7 +31,7 @@ describe('Notifications actions tests', () => { const initialState = { notificationReducer: { unreadCount: 0, - isDrawerVisible: 'true', + isDrawerVisible: true, notifications, totalNotificationsCount: 0, toastNotifications: [], diff --git a/app/javascript/spec/miq-redux/notification-reducer.spec.js b/app/javascript/spec/miq-redux/notification-reducer.spec.js index 168fcceaea2..eb16eeaaa6b 100644 --- a/app/javascript/spec/miq-redux/notification-reducer.spec.js +++ b/app/javascript/spec/miq-redux/notification-reducer.spec.js @@ -15,7 +15,7 @@ import { describe('Notification reducer tests', () => { const initialState = { unreadCount: 0, - isDrawerVisible: 'true', + isDrawerVisible: true, notifications: [], totalNotificationsCount: 0, toastNotifications: [], diff --git a/app/javascript/spec/notification-drawer/notification-drawer.spec.js b/app/javascript/spec/notification-drawer/notification-drawer.spec.js index 579c8a45cfb..f9c1a22356f 100644 --- a/app/javascript/spec/notification-drawer/notification-drawer.spec.js +++ b/app/javascript/spec/notification-drawer/notification-drawer.spec.js @@ -33,7 +33,7 @@ describe('Notification drawer tests', () => { const initialState = { notificationReducer: { unreadCount: 2, - isDrawerVisible: 'true', + isDrawerVisible: true, notifications, totalNotificationsCount: 2, toastNotifications: [], diff --git a/app/javascript/spec/toast-list/toast-list.spec.js b/app/javascript/spec/toast-list/toast-list.spec.js index ebdbb08e28a..9cf79b34800 100644 --- a/app/javascript/spec/toast-list/toast-list.spec.js +++ b/app/javascript/spec/toast-list/toast-list.spec.js @@ -14,7 +14,7 @@ describe('Toast list tests', () => { const initialState = { notificationReducer: { unreadCount: 1, - isDrawerVisible: 'false', + isDrawerVisible: false, notifications, totalNotificationsCount: 0, toastNotifications: notifications, diff --git a/config/jest.setup.js b/config/jest.setup.js index 6baa8408572..b1683c6e163 100644 --- a/config/jest.setup.js +++ b/config/jest.setup.js @@ -9,6 +9,7 @@ window.$ = require('jquery'); window.__ = (x) => x; window.n__ = (x) => x; window._ = require('lodash'); +window.sprintf = require('sprintf-js').sprintf; require('whatwg-fetch'); From a7b37b3c3a6ea065ff11021b8d02a232ebaaf6a8 Mon Sep 17 00:00:00 2001 From: Martin Hradil Date: Tue, 2 Jun 2020 14:12:59 +0000 Subject: [PATCH 46/57] Breadcrumbs - split into original Breadcrumbs + BreadcrumbsBar with NotificationToggle --- .../components/breadcrumbs/breadcrumbs.jsx | 67 ++++++++++++++++++ .../components/breadcrumbs/index.jsx | 69 +------------------ .../packs/component-definitions-common.js | 4 +- app/views/layouts/_breadcrumbs.html.haml | 2 +- 4 files changed, 73 insertions(+), 69 deletions(-) create mode 100644 app/javascript/components/breadcrumbs/breadcrumbs.jsx diff --git a/app/javascript/components/breadcrumbs/breadcrumbs.jsx b/app/javascript/components/breadcrumbs/breadcrumbs.jsx new file mode 100644 index 00000000000..08a01bbed67 --- /dev/null +++ b/app/javascript/components/breadcrumbs/breadcrumbs.jsx @@ -0,0 +1,67 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { Breadcrumb } from 'patternfly-react'; +import { unescape } from 'lodash'; + +import { onClickTree, onClick, onClickToExplorer } from './on-click-functions'; + +// FIXME: don't parse html here +const parsedText = text => unescape(text).replace(/<[/]{0,1}strong>/g, ''); + +const renderItems = ({ items, controllerName }) => { + return items + .filter((_item, index) => index !== (items.length - 1)) + .map((item, index) => { + const text = parsedText(item.title); + if (item.action || (!item.url && !item.key && !item.to_explorer)) { + return
  • {text}
  • ; // eslint-disable-line react/no-array-index-key + } + + if (item.key || item.to_explorer) { + return ( + + (item.to_explorer + ? onClickToExplorer(e, controllerName, item.to_explorer) + : onClickTree(e, controllerName, item)) + } + > + {text} + + ); + } + + return ( + onClick(e, item.url)} + > + {text} + + ); + }); +}; + +export const Breadcrumbs = ({ items, title, controllerName }) => ( + + {items && renderItems({ items, controllerName })} + + + {items && items.length > 0 ? parsedText(items[items.length - 1].title) : parsedText(title)} + + + +); + +Breadcrumbs.propTypes = { + controllerName: PropTypes.string, + items: PropTypes.arrayOf(PropTypes.shape({ + action: PropTypes.string, + key: PropTypes.string, + title: PropTypes.string.isRequired, + url: PropTypes.string, + })), + title: PropTypes.string, +}; diff --git a/app/javascript/components/breadcrumbs/index.jsx b/app/javascript/components/breadcrumbs/index.jsx index 5fef9c4d387..71e4567c09d 100644 --- a/app/javascript/components/breadcrumbs/index.jsx +++ b/app/javascript/components/breadcrumbs/index.jsx @@ -1,73 +1,10 @@ import React from 'react'; -import PropTypes from 'prop-types'; -import { Breadcrumb } from 'patternfly-react'; -import { unescape } from 'lodash'; - +import { Breadcrumbs } from './breadcrumbs'; import { NotificationsToggle } from './notifications-toggle'; -import { onClickTree, onClick, onClickToExplorer } from './on-click-functions'; - -// FIXME: don't parse html here -const parsedText = text => unescape(text).replace(/<[/]{0,1}strong>/g, ''); - -const renderItems = ({ items, controllerName }) => { - return items - .filter((_item, index) => index !== (items.length - 1)) - .map((item, index) => { - const text = parsedText(item.title); - if (item.action || (!item.url && !item.key && !item.to_explorer)) { - return
  • {text}
  • ; // eslint-disable-line react/no-array-index-key - } - - if (item.key || item.to_explorer) { - return ( - - (item.to_explorer - ? onClickToExplorer(e, controllerName, item.to_explorer) - : onClickTree(e, controllerName, item)) - } - > - {text} - - ); - } - return ( - onClick(e, item.url)} - > - {text} - - ); - }); -}; - -const Breadcrumbs = ({ items, title, controllerName }) => ( +export const BreadcrumbsBar = (props) => ( <> - - {items && renderItems({ items, controllerName })} - - - {items && items.length > 0 ? parsedText(items[items.length - 1].title) : parsedText(title)} - - - + ); - -Breadcrumbs.propTypes = { - controllerName: PropTypes.string, - items: PropTypes.arrayOf(PropTypes.shape({ - action: PropTypes.string, - key: PropTypes.string, - title: PropTypes.string.isRequired, - url: PropTypes.string, - })), - title: PropTypes.string, -}; - -export default Breadcrumbs; diff --git a/app/javascript/packs/component-definitions-common.js b/app/javascript/packs/component-definitions-common.js index f36e3534f4b..b33b6344909 100644 --- a/app/javascript/packs/component-definitions-common.js +++ b/app/javascript/packs/component-definitions-common.js @@ -4,7 +4,7 @@ import { TagGroup, TableListView, GenericGroup } from '@manageiq/react-ui-compon import { Toolbar } from '@manageiq/react-ui-components/dist/toolbar'; import AggregateStatusCard from '../components/aggregate_status_card'; -import Breadcrumbs from '../components/breadcrumbs'; +import { BreadcrumbsBar } from '../components/breadcrumbs'; import CatalogForm from '../components/catalog-form/catalog-form'; import CloudNetworkForm from '../components/cloud-network-form/cloud-network-form'; import CloudTenantForm from '../components/cloud-tenant-form/cloud-tenant-form'; @@ -50,7 +50,7 @@ import WorkersForm from '../components/workers-form/workers-form'; */ ManageIQ.component.addReact('AggregateStatusCard', AggregateStatusCard); -ManageIQ.component.addReact('Breadcrumbs', Breadcrumbs); +ManageIQ.component.addReact('BreadcrumbsBar', BreadcrumbsBar); ManageIQ.component.addReact('CatalogForm', CatalogForm); ManageIQ.component.addReact('CloudNetworkForm', CloudNetworkForm); ManageIQ.component.addReact('CloudTenantForm', CloudTenantForm); diff --git a/app/views/layouts/_breadcrumbs.html.haml b/app/views/layouts/_breadcrumbs.html.haml index 39a5263efde..50eaab487c5 100644 --- a/app/views/layouts/_breadcrumbs.html.haml +++ b/app/views/layouts/_breadcrumbs.html.haml @@ -1,5 +1,5 @@ - right_cell_text ||= @right_cell_text -= react('Breadcrumbs', += react('BreadcrumbsBar', :items => (data_for_breadcrumbs({:right_cell_text => right_cell_text}) if respond_to?(:data_for_breadcrumbs)), :controllerName => controller_name, :title => @title) From 4441de95a7b6339b90c02c1a6364f98638ccef11 Mon Sep 17 00:00:00 2001 From: Martin Hradil Date: Tue, 2 Jun 2020 14:16:51 +0000 Subject: [PATCH 47/57] nottificationsToggle: use classnames --- .../components/breadcrumbs/notifications-toggle.jsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/app/javascript/components/breadcrumbs/notifications-toggle.jsx b/app/javascript/components/breadcrumbs/notifications-toggle.jsx index cb15ad4bd8e..e4881ab57c2 100644 --- a/app/javascript/components/breadcrumbs/notifications-toggle.jsx +++ b/app/javascript/components/breadcrumbs/notifications-toggle.jsx @@ -1,3 +1,4 @@ +import cx from 'classnames'; import React, { useEffect } from 'react'; import { useDispatch, useSelector } from 'react-redux'; import { TOGGLE_DRAWER_VISIBILITY } from '../../miq-redux/actions/notifications-actions'; @@ -22,7 +23,10 @@ export const NotificationsToggle = () => { return ( From 3f1607cd59dbcbca2d30f0655d9786957585ae45 Mon Sep 17 00:00:00 2001 From: Martin Hradil Date: Tue, 2 Jun 2020 14:17:20 +0000 Subject: [PATCH 48/57] nottificationsToggle, searchResults: prefer {" "} over   --- app/javascript/components/breadcrumbs/notifications-toggle.jsx | 2 +- app/javascript/menu/search-results.jsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/javascript/components/breadcrumbs/notifications-toggle.jsx b/app/javascript/components/breadcrumbs/notifications-toggle.jsx index e4881ab57c2..82fe795b861 100644 --- a/app/javascript/components/breadcrumbs/notifications-toggle.jsx +++ b/app/javascript/components/breadcrumbs/notifications-toggle.jsx @@ -31,7 +31,7 @@ export const NotificationsToggle = () => { onClick={toggle} > {__("Notifications")} -   + {' '} ); diff --git a/app/javascript/menu/search-results.jsx b/app/javascript/menu/search-results.jsx index 4dfb5f7d777..c35e5263d2c 100644 --- a/app/javascript/menu/search-results.jsx +++ b/app/javascript/menu/search-results.jsx @@ -25,7 +25,7 @@ const ResultItem = ({ item, titles }) => ( const Count = ({ length }) => (

    {__("Results")} -   + {' '} ({length})

    ); From 92fb0d2c2e5f1d14773061756ca3969a9f3f47c0 Mon Sep 17 00:00:00 2001 From: Martin Hradil Date: Tue, 2 Jun 2020 14:21:28 +0000 Subject: [PATCH 49/57] group-switcher: use destructuring --- app/javascript/menu/group-switcher.jsx | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/app/javascript/menu/group-switcher.jsx b/app/javascript/menu/group-switcher.jsx index 69dc2ce7db8..c7590896f85 100644 --- a/app/javascript/menu/group-switcher.jsx +++ b/app/javascript/menu/group-switcher.jsx @@ -6,9 +6,9 @@ import { Collaborate20 } from '@carbon/icons-react'; const { miqChangeGroup } = window; export const GroupSwitcher = ({ miqGroups, currentGroup, expanded }) => { - const options = miqGroups.map((g) => ({ - label: g.description, - value: g.id, + const options = miqGroups.map(({ id, description }) => ({ + label: description, + value: id, })); const currentOption = { @@ -16,8 +16,7 @@ export const GroupSwitcher = ({ miqGroups, currentGroup, expanded }) => { value: currentGroup.id, }; - const groupChange = ({ selectedItem }) => { - const group_id = selectedItem.value; + const groupChange = ({ selectedItem: { value: group_id } }) => { if (group_id && group_id !== currentGroup.id) { miqChangeGroup(group_id); } From fdc9057316becd069b0352ac531da01f859aa120 Mon Sep 17 00:00:00 2001 From: Martin Hradil Date: Tue, 2 Jun 2020 14:27:26 +0000 Subject: [PATCH 50/57] group-switcher: remove nested tertiary --- app/javascript/menu/group-switcher.jsx | 50 ++++++++++++++------------ 1 file changed, 27 insertions(+), 23 deletions(-) diff --git a/app/javascript/menu/group-switcher.jsx b/app/javascript/menu/group-switcher.jsx index c7590896f85..44a25387e6a 100644 --- a/app/javascript/menu/group-switcher.jsx +++ b/app/javascript/menu/group-switcher.jsx @@ -5,7 +5,7 @@ import { Collaborate20 } from '@carbon/icons-react'; const { miqChangeGroup } = window; -export const GroupSwitcher = ({ miqGroups, currentGroup, expanded }) => { +export const GroupSwitcher = ({ miqGroups, currentGroup, expanded: isExpanded }) => { const options = miqGroups.map(({ id, description }) => ({ label: description, value: id, @@ -22,33 +22,37 @@ export const GroupSwitcher = ({ miqGroups, currentGroup, expanded }) => { } }; + const collapsed = ( + + + + ); + + const singleGroup = ( + + {currentOption.label} + + ); + + const multiGroup = ( + + ); + + const expanded = options.length > 1 ? multiGroup : singleGroup; + return (
    - { expanded ? ( - <> - { options.length > 1 ? ( - - ) : ( - - {currentOption.label} - - )} - - ) : ( - - - - )} + { isExpanded ? expanded : collapsed }
    ); }; From 4c14fcf0c0a87025c1a88461e324513dac7f97ac Mon Sep 17 00:00:00 2001 From: Martin Hradil Date: Tue, 2 Jun 2020 14:28:35 +0000 Subject: [PATCH 51/57] main-menu: comment about useEffect having external effect --- app/javascript/menu/main-menu.jsx | 1 + 1 file changed, 1 insertion(+) diff --git a/app/javascript/menu/main-menu.jsx b/app/javascript/menu/main-menu.jsx index e6b93576c76..96579bab8ee 100644 --- a/app/javascript/menu/main-menu.jsx +++ b/app/javascript/menu/main-menu.jsx @@ -29,6 +29,7 @@ export const MainMenu = (props) => { }, [expanded]); useEffect(() => { + // set body class const classNames = { true: 'miq-main-menu-expanded', false: 'miq-main-menu-collapsed', From dae234f7af95d8e2069f25a8ec01ba8249e9af6a Mon Sep 17 00:00:00 2001 From: Martin Hradil Date: Tue, 2 Jun 2020 14:36:02 +0000 Subject: [PATCH 52/57] main-menu: change single prop components to oneliners and set the linter rule accordingly --- app/javascript/.eslintrc.json | 1 + app/javascript/menu/main-menu.jsx | 12 +++--------- 2 files changed, 4 insertions(+), 9 deletions(-) diff --git a/app/javascript/.eslintrc.json b/app/javascript/.eslintrc.json index 12296474dfe..54d2e17a367 100644 --- a/app/javascript/.eslintrc.json +++ b/app/javascript/.eslintrc.json @@ -53,6 +53,7 @@ "allowTemplateLiterals": true }], "react/jsx-filename-extension": "off", + "react/jsx-first-prop-new-line": [1, "multiline"], "react/destructuring-assignment": [1, "always"], "semi": [2, "always"], "space-before-function-paren": [2, "never"], diff --git a/app/javascript/menu/main-menu.jsx b/app/javascript/menu/main-menu.jsx index 96579bab8ee..dfc189e0255 100644 --- a/app/javascript/menu/main-menu.jsx +++ b/app/javascript/menu/main-menu.jsx @@ -45,9 +45,7 @@ export const MainMenu = (props) => { return ( <> -
    +
    {
    - {searchResults && } + {searchResults && } {!searchResults && { expanded={true} isChildOfHeader={false} > - +
    Date: Tue, 2 Jun 2020 14:37:10 +0000 Subject: [PATCH 53/57] item-type: linter spacing fix --- app/javascript/menu/item-type.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/javascript/menu/item-type.js b/app/javascript/menu/item-type.js index 9347ff6ecd7..f98a6c0f47b 100644 --- a/app/javascript/menu/item-type.js +++ b/app/javascript/menu/item-type.js @@ -4,7 +4,7 @@ // * modal () - open the About Modal (extend for any modals) // * new_window (href) - opens href in new window (for external links) -export const linkProps = ({type, href, id}) => ({ +export const linkProps = ({ type, href, id }) => ({ href: { big_iframe: `/dashboard/iframe?id=${id}`, default: href, From 50f6bb1ba395d2d5af7aae171cbaa923d250368d Mon Sep 17 00:00:00 2001 From: Martin Hradil Date: Tue, 2 Jun 2020 20:32:06 +0000 Subject: [PATCH 54/57] second level menu - hide after clicking on items needed for about modal & react router routes (because there's no full page reload to hide the secondary menu) --- app/javascript/menu/item-type.js | 7 ++++++- app/javascript/menu/main-menu.jsx | 5 ++++- app/javascript/menu/second-level.jsx | 28 +++++++++++++++++----------- 3 files changed, 27 insertions(+), 13 deletions(-) diff --git a/app/javascript/menu/item-type.js b/app/javascript/menu/item-type.js index f98a6c0f47b..29b023a504c 100644 --- a/app/javascript/menu/item-type.js +++ b/app/javascript/menu/item-type.js @@ -4,7 +4,7 @@ // * modal () - open the About Modal (extend for any modals) // * new_window (href) - opens href in new window (for external links) -export const linkProps = ({ type, href, id }) => ({ +export const linkProps = ({ type, href, id, hideSecondary = () => null }) => ({ href: { big_iframe: `/dashboard/iframe?id=${id}`, default: href, @@ -18,11 +18,14 @@ export const linkProps = ({ type, href, id }) => ({ onClick: (event) => { if (type === 'modal') { sendDataWithRx({ type: 'showAboutModal' }); + hideSecondary(); + event.preventDefault(); return; } if (['default', 'big_iframe'].includes(type) && miqCheckForChanges() === false) { + // cancelled event.preventDefault(); return; } @@ -30,6 +33,8 @@ export const linkProps = ({ type, href, id }) => ({ if (href === '/dashboard/logout') { ManageIQ.logoutInProgress = true; } + + hideSecondary(); }, }); diff --git a/app/javascript/menu/main-menu.jsx b/app/javascript/menu/main-menu.jsx index dfc189e0255..2d5243973eb 100644 --- a/app/javascript/menu/main-menu.jsx +++ b/app/javascript/menu/main-menu.jsx @@ -100,7 +100,10 @@ export const MainMenu = (props) => { expanded={true} isChildOfHeader={false} > - +
    items.map((item) => ( - item.items.length - ? - : -)); +const mapItems = (items, hideSecondary) => items.map((item) => { + let Component = item.items.length ? MenuSection : MenuItem; + return +}); -const MenuItem = ({ active, href, id, title, type }) => ( + +const MenuItem = ({ active, href, id, title, type, hideSecondary }) => ( {title} @@ -22,6 +26,7 @@ const MenuItem = ({ active, href, id, title, type }) => ( MenuItem.props = { active: PropTypes.bool, + hideSecondary: PropTypes.func, href: PropTypes.string.isRequired, id: PropTypes.string.isRequired, title: PropTypes.string.isRequired, @@ -29,27 +34,28 @@ MenuItem.props = { }; -const MenuSection = ({ active, id, items, title }) => ( +const MenuSection = ({ active, id, items, title, hideSecondary }) => ( - {mapItems(items)} + {mapItems(items, hideSecondary)} ); MenuSection.props = { active: PropTypes.bool, + hideSecondary: PropTypes.func, id: PropTypes.string.isRequired, items: PropTypes.arrayOf(PropTypes.any).isRequired, title: PropTypes.string.isRequired, }; -export const SecondLevel = ({ menu }) => ( +export const SecondLevel = ({ menu, hideSecondary }) => ( - {mapItems(menu)} + {mapItems(menu, hideSecondary)} ); From 858589f06357ce887c61fd32b5ea309f45c13491 Mon Sep 17 00:00:00 2001 From: Martin Hradil Date: Tue, 2 Jun 2020 20:45:57 +0000 Subject: [PATCH 55/57] menu: miqSparkleOn, miqSparkleOff for react routes --- app/javascript/menu/item-type.js | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/app/javascript/menu/item-type.js b/app/javascript/menu/item-type.js index 29b023a504c..c844f420953 100644 --- a/app/javascript/menu/item-type.js +++ b/app/javascript/menu/item-type.js @@ -4,6 +4,16 @@ // * modal () - open the About Modal (extend for any modals) // * new_window (href) - opens href in new window (for external links) +import { history } from '../miq-component/react-history.js'; +const { miqSparkleOn, miqSparkleOff } = window; + +const onNextRouteChange = (callback) => { + const unlisten = history.listen(() => { + unlisten(); + callback(); + }); +}; + export const linkProps = ({ type, href, id, hideSecondary = () => null }) => ({ href: { big_iframe: `/dashboard/iframe?id=${id}`, @@ -35,6 +45,10 @@ export const linkProps = ({ type, href, id, hideSecondary = () => null }) => ({ } hideSecondary(); + miqSparkleOn(); + + // react router support + onNextRouteChange(() => miqSparkleOff()); }, }); From a87532475cd89118191b543b0e3b78cf1b0dd675 Mon Sep 17 00:00:00 2001 From: Martin Hradil Date: Tue, 2 Jun 2020 23:21:41 +0000 Subject: [PATCH 56/57] breadcrumbs spec - fix import, update snapshot links --- .../__snapshots__/breadcrumbs.spec.js.snap | 156 ++++++++++-------- .../spec/breadcrumbs/breadcrumbs.spec.js | 2 +- 2 files changed, 91 insertions(+), 67 deletions(-) diff --git a/app/javascript/spec/breadcrumbs/__snapshots__/breadcrumbs.spec.js.snap b/app/javascript/spec/breadcrumbs/__snapshots__/breadcrumbs.spec.js.snap index 6edcfb70c19..4a6ebe72eb8 100644 --- a/app/javascript/spec/breadcrumbs/__snapshots__/breadcrumbs.spec.js.snap +++ b/app/javascript/spec/breadcrumbs/__snapshots__/breadcrumbs.spec.js.snap @@ -14,7 +14,7 @@ exports[`Breadcrumbs component is correctly rendered 1`] = ` } > - - -
      - -
    1. - - - Providers - - -
    2. -
      - -
    3. + Providers + + +
    4. +
      + - - - Google - - - - -
    5. - All -
    6. - + + Google + + + +
    7. - - - This - - + All
    8. - -
    + +
  • + + + This + + +
  • +
    + +
    - +
    Notifications -   + @@ -171,7 +195,7 @@ exports[`Breadcrumbs component is correctly rendered 1`] = ` - +
    `; diff --git a/app/javascript/spec/breadcrumbs/breadcrumbs.spec.js b/app/javascript/spec/breadcrumbs/breadcrumbs.spec.js index 95d90d22735..598fb06c38c 100644 --- a/app/javascript/spec/breadcrumbs/breadcrumbs.spec.js +++ b/app/javascript/spec/breadcrumbs/breadcrumbs.spec.js @@ -5,7 +5,7 @@ import { Provider } from 'react-redux'; import { mount } from 'enzyme'; import * as clickFunctions from '../../components/breadcrumbs/on-click-functions'; -import Breadcrumbs from '../../components/breadcrumbs'; +import { BreadcrumbsBar as Breadcrumbs } from '../../components/breadcrumbs'; describe('Breadcrumbs component', () => { const props = { From e2aab9644e41dc759d64fb706e7d8f7a2e3ea558 Mon Sep 17 00:00:00 2001 From: Martin Hradil Date: Thu, 4 Jun 2020 23:14:39 +0000 Subject: [PATCH 57/57] Menu - add history support for react router basically v2v support, but it will work for any plugin which uses `.menu` and `HashRouter` would need different, simpler `currentUrl` logic for `BrowserRouter` --- app/javascript/menu/history.js | 37 +++++++++++++++++++++++++++++++ app/javascript/menu/main-menu.jsx | 23 ++++++++++++------- app/javascript/menu/search.jsx | 21 ++++++++++++------ 3 files changed, 66 insertions(+), 15 deletions(-) create mode 100644 app/javascript/menu/history.js diff --git a/app/javascript/menu/history.js b/app/javascript/menu/history.js new file mode 100644 index 00000000000..96bdbf7668e --- /dev/null +++ b/app/javascript/menu/history.js @@ -0,0 +1,37 @@ +import { flatten } from './search'; + +const unsetActive = (menu) => menu.map((item) => ({ + ...item, + active: false, + items: item.items && unsetActive(item.items), +})); + +export const updateActiveItem = (_location) => { + const { menu } = window.ManageIQ; + const { setMenu } = updateActiveItem; + + const deactivated = unsetActive(menu); + + const flat = flatten(deactivated).map(({ item, parents }) => ({ + href: item.href, + item, + parents, + })); + + // FIXME: we should be using _location.pathname but that requires BrowserRouter, not HashRouter + const currentUrl = `${document.location.pathname}${document.location.hash}`; + + const current = _.find(flat, { href: currentUrl }); + + if (! current) { + return; + } + + current.item.active = true; + current.parents.forEach((p) => p.active = true); + + setMenu(deactivated); +}; + +// listen for history changes +ManageIQ.redux.history.listen(updateActiveItem); diff --git a/app/javascript/menu/main-menu.jsx b/app/javascript/menu/main-menu.jsx index 2d5243973eb..0f30d135dfa 100644 --- a/app/javascript/menu/main-menu.jsx +++ b/app/javascript/menu/main-menu.jsx @@ -10,26 +10,27 @@ import { MiqLogo } from './miq-logo'; import { SearchResults } from './search-results'; import { SecondLevel } from './second-level'; import { Username } from './username'; +import { updateActiveItem } from './history'; const initialExpanded = window.localStorage.getItem('patternfly-navigation-primary') !== 'collapsed'; -export const MainMenu = (props) => { - const { applianceName, currentGroup, currentUser, customBrand, logoLarge, logoSmall, menu, miqGroups, showLogo, showUser } = props; +export const MainMenu = ({ applianceName, currentGroup, currentUser, customBrand, logoLarge, logoSmall, menu: initialMenu, miqGroups, showLogo, showUser }) => { const [expanded, setExpanded] = useState(initialExpanded); - const [activeSection, setSection] = useState(null); + const [menu, setMenu] = useState(initialMenu); const [searchResults, setSearch] = useState(null); + const [activeSection, setSection] = useState(null); - const hideSecondary = (e) => setSection(null); - - let appearExpanded = expanded || !!activeSection || !!searchResults; + const appearExpanded = expanded || !!activeSection || !!searchResults; + const hideSecondary = (_e) => setSection(null); useEffect(() => { + // persist expanded state window.localStorage.setItem('patternfly-navigation-primary', expanded ? 'expanded' : 'collapsed'); }, [expanded]); useEffect(() => { - // set body class + // set body class - for content offset const classNames = { true: 'miq-main-menu-expanded', false: 'miq-main-menu-collapsed', @@ -40,9 +41,15 @@ export const MainMenu = (props) => { useEffect(() => { // cypress, debugging - ManageIQ.menu = menu; + window.ManageIQ.menu = menu; }, [menu]); + useEffect(() => { + // react router support - allow history changes to update the menu .. and try on load + updateActiveItem.setMenu = setMenu; + updateActiveItem(ManageIQ.redux.store.getState().router.location); + }, []); + return ( <>
    diff --git a/app/javascript/menu/search.jsx b/app/javascript/menu/search.jsx index 74c0e139421..c521432288a 100644 --- a/app/javascript/menu/search.jsx +++ b/app/javascript/menu/search.jsx @@ -5,7 +5,7 @@ import Search from 'carbon-components-react/es/components/Search'; import { Search20 } from '@carbon/icons-react'; import { SideNavItem } from 'carbon-components-react/es/components/UIShell'; -const flatten = (menuItems = []) => { +export const flatten = (menuItems = []) => { const flat = []; const process = (items, parents = []) => { @@ -17,13 +17,9 @@ const flatten = (menuItems = []) => { process(item.items, newParents); } else { // item - const titles = newParents.map((p) => p.title); - const haystack = titles.join(' ').toLocaleLowerCase(); - flat.push({ - haystack, item, - titles, + parents, }); } }); @@ -42,7 +38,18 @@ export const MenuSearch = ({ expanded, menu, onSearch }) => { ); } - const flatMenu = flatten(menu); + const flatMenu = flatten(menu) + .map(({item, parents}) => { + const titles = [...parents, item].map((p) => p.title); + const haystack = titles.join(' ').toLocaleLowerCase(); + + return { + haystack, + item, + parents, + titles, + }; + }); const searchResults = (string) => { if (!string || string.match(/^\s*$/)) {