diff --git a/src/renderer/components/layout/sidebar-context.ts b/src/renderer/components/layout/sidebar-context.ts new file mode 100644 index 0000000000..fff192cba3 --- /dev/null +++ b/src/renderer/components/layout/sidebar-context.ts @@ -0,0 +1,7 @@ +import React from "react"; + +export const SidebarContext = React.createContext({ pinned: false }); + +export type SidebarContextValue = { + pinned: boolean; +}; \ No newline at end of file diff --git a/src/renderer/components/layout/sidebar-nav-item.scss b/src/renderer/components/layout/sidebar-nav-item.scss new file mode 100644 index 0000000000..24469dbd0f --- /dev/null +++ b/src/renderer/components/layout/sidebar-nav-item.scss @@ -0,0 +1,76 @@ +.SidebarNavItem { + $itemSpacing: floor($unit / 2.6) floor($unit / 1.6); + + width: 100%; + user-select: none; + flex-shrink: 0; + + .nav-item { + cursor: pointer; + width: inherit; + display: flex; + align-items: center; + text-decoration: none; + border: none; + padding: $itemSpacing; + + &.active, &:hover { + background: $lensBlue; + color: $sidebarActiveColor; + } + } + + .expand-icon { + --size: 20px; + } + + .sub-menu { + border-left: 4px solid transparent; + + &.active { + border-left-color: $lensBlue; + } + + a, .SidebarNavItem { + display: block; + border: none; + text-decoration: none; + color: $textColorPrimary; + font-weight: normal; + padding-left: 40px; // parent icon width + overflow: hidden; + text-overflow: ellipsis; + line-height: 0px; // hidden by default + max-height: 0px; + opacity: 0; + transition: 125ms line-height ease-out, 200ms 100ms opacity; + + &.visible { + line-height: 28px; + max-height: 1000px; + opacity: 1; + } + + &.active, &:hover { + color: $sidebarSubmenuActiveColor; + } + } + + .sub-menu-parent { + padding-left: 27px; + font-weight: 500; + + .nav-item { + &:hover { + background: transparent; + } + } + + .sub-menu { + a { + padding-left: $padding * 3; + } + } + } + } +} \ No newline at end of file diff --git a/src/renderer/components/layout/sidebar-nav-item.tsx b/src/renderer/components/layout/sidebar-nav-item.tsx new file mode 100644 index 0000000000..7dfcbb50e6 --- /dev/null +++ b/src/renderer/components/layout/sidebar-nav-item.tsx @@ -0,0 +1,83 @@ +import "./sidebar-nav-item.scss"; + +import React from "react"; +import { computed, observable, reaction } from "mobx"; +import { observer } from "mobx-react"; +import { NavLink } from "react-router-dom"; + +import { createStorage, cssNames } from "../../utils"; +import { Icon } from "../icon"; +import { SidebarContext } from "./sidebar-context"; + +import type { TabLayoutRoute } from "./tab-layout"; +import type { SidebarContextValue } from "./sidebar-context"; + +interface SidebarNavItemProps { + id: string; // Used to save nav item collapse/expand state in local storage + url: string; + text: React.ReactNode | string; + className?: string; + icon?: React.ReactNode; + isHidden?: boolean; + isActive?: boolean; + subMenus?: TabLayoutRoute[]; +} + +const navItemStorage = createStorage<[string, boolean][]>("sidebar_menu_item", []); +const navItemState = observable.map(navItemStorage.get()); + +reaction(() => [...navItemState], (value) => navItemStorage.set(value)); + +@observer +export class SidebarNavItem extends React.Component { + static contextType = SidebarContext; + public context: SidebarContextValue; + + @computed get isExpanded() { + return navItemState.get(this.props.id); + } + + toggleSubMenu = () => { + navItemState.set(this.props.id, !this.isExpanded); + }; + + render() { + const { isHidden, isActive, subMenus = [], icon, text, url, children, className, id } = this.props; + + if (isHidden) { + return null; + } + const extendedView = (subMenus.length > 0 || children) && this.context.pinned; + + if (extendedView) { + return ( +
+
+ {icon} + {text} + +
+
    + {subMenus.map(({ title, url }) => ( + + {title} + + ))} + {React.Children.toArray(children).map((child: React.ReactElement) => { + return React.cloneElement(child, { + className: cssNames(child.props.className, { visible: this.isExpanded }), + }); + })} +
+
+ ); + } + + return ( + isActive}> + {icon} + {text} + + ); + } +} diff --git a/src/renderer/components/layout/sidebar.scss b/src/renderer/components/layout/sidebar.scss index 1c5932b8d9..1379c87d9e 100644 --- a/src/renderer/components/layout/sidebar.scss +++ b/src/renderer/components/layout/sidebar.scss @@ -1,22 +1,7 @@ .Sidebar { $iconSize: 24px; - $activeBgc: $lensBlue; - $activeTextColor: $sidebarActiveColor; $itemSpacing: floor($unit / 2.6) floor($unit / 1.6); - @mixin activeLinkState { - &.active { - background: $activeBgc; - color: $activeTextColor; - } - @media (hover: hover) { // only for devices supported "true" hover (with mouse or similar) - &:hover { - background: $activeBgc; - color: $activeTextColor; - } - } - } - &.pinned { .sidebar-nav { overflow: auto; @@ -77,13 +62,16 @@ } > a { - @include activeLinkState; - display: flex; align-items: center; text-decoration: none; border: none; padding: $itemSpacing; + + &.active, &:hover { + background: $lensBlue; + color: $sidebarActiveColor; + } } hr { @@ -91,78 +79,6 @@ } } - .SidebarNavItem { - width: 100%; - user-select: none; - flex-shrink: 0; - - .nav-item { - @include activeLinkState; - - cursor: pointer; - width: inherit; - display: flex; - align-items: center; - text-decoration: none; - border: none; - padding: $itemSpacing; - } - - .expand-icon { - --size: 20px; - } - - .sub-menu { - border-left: 4px solid transparent; - - &.active { - border-left-color: $activeBgc; - } - - a, .SidebarNavItem { - display: block; - border: none; - text-decoration: none; - color: $textColorPrimary; - font-weight: normal; - padding-left: 40px; // parent icon width - overflow: hidden; - text-overflow: ellipsis; - line-height: 0px; // hidden by default - max-height: 0px; - opacity: 0; - transition: 125ms line-height ease-out, 200ms 100ms opacity; - - &.visible { - line-height: 28px; - max-height: 1000px; - opacity: 1; - } - - &.active, &:hover { - color: $sidebarSubmenuActiveColor; - } - } - - .sub-menu-parent { - padding-left: 27px; - font-weight: 500; - - .nav-item { - &:hover { - background: transparent; - } - } - - .sub-menu { - a { - padding-left: $padding * 3; - } - } - } - } - } - .loading { padding: $padding; text-align: center; diff --git a/src/renderer/components/layout/sidebar.tsx b/src/renderer/components/layout/sidebar.tsx index f9f8494318..5386d9fb52 100644 --- a/src/renderer/components/layout/sidebar.tsx +++ b/src/renderer/components/layout/sidebar.tsx @@ -1,12 +1,11 @@ -import type { TabLayoutRoute } from "./tab-layout"; import "./sidebar.scss"; import React from "react"; -import { computed, observable, reaction } from "mobx"; +import type { TabLayoutRoute } from "./tab-layout"; import { observer } from "mobx-react"; import { NavLink } from "react-router-dom"; import { Trans } from "@lingui/macro"; -import { createStorage, cssNames } from "../../utils"; +import { cssNames } from "../../utils"; import { Icon } from "../icon"; import { workloadsRoute, workloadsURL } from "../+workloads/workloads.route"; import { namespacesRoute, namespacesURL } from "../+namespaces/namespaces.route"; @@ -30,12 +29,8 @@ import { isActiveRoute } from "../../navigation"; import { isAllowedResource } from "../../../common/rbac"; import { Spinner } from "../spinner"; import { ClusterPageMenuRegistration, clusterPageMenuRegistry, clusterPageRegistry, getExtensionPageUrl } from "../../../extensions/registries"; - -const SidebarContext = React.createContext({ pinned: false }); - -type SidebarContextValue = { - pinned: boolean; -}; +import { SidebarNavItem } from "./sidebar-nav-item"; +import { SidebarContext } from "./sidebar-context"; interface Props { className?: string; @@ -69,6 +64,7 @@ export class Sidebar extends React.Component { return ( { return clusterPageMenuRegistry.getRootItems().map((menuItem, index) => { const registeredPage = clusterPageRegistry.getByPageMenuTarget(menuItem.target); const tabRoutes = this.getTabLayoutRoutes(menuItem); + const id = `registered-item-${index}`; let pageUrl: string; let isActive = false; @@ -122,7 +119,8 @@ export class Sidebar extends React.Component { return ( } @@ -155,7 +153,7 @@ export class Sidebar extends React.Component {
{ icon={} /> { icon={} /> { icon={} /> { icon={} /> { icon={} /> { text={Storage} /> { text={Namespaces} /> { text={Events} /> { text={Apps} /> { text={Access Control} /> { ); } } - -interface SidebarNavItemProps { - url: string; - text: React.ReactNode | string; - className?: string; - icon?: React.ReactNode; - isHidden?: boolean; - isActive?: boolean; - subMenus?: TabLayoutRoute[]; - testId?: string; // data-test-id="" property for integration tests -} - -const navItemStorage = createStorage<[string, boolean][]>("sidebar_menu_item", []); -const navItemState = observable.map(navItemStorage.get()); - -reaction(() => [...navItemState], (value) => navItemStorage.set(value)); - -@observer -class SidebarNavItem extends React.Component { - static contextType = SidebarContext; - public context: SidebarContextValue; - - get itemId() { - const url = new URL(this.props.url, `${window.location.protocol}//${window.location.host}`); - - return url.pathname; // pathname without get params - } - - @computed get isExpanded() { - return navItemState.get(this.itemId); - } - - toggleSubMenu = () => { - navItemState.set(this.itemId, !this.isExpanded); - }; - - render() { - const { isHidden, isActive, subMenus = [], icon, text, url, children, className, testId } = this.props; - - if (isHidden) { - return null; - } - const extendedView = (subMenus.length > 0 || children) && this.context.pinned; - - if (extendedView) { - return ( -
-
- {icon} - {text} - -
-
    - {subMenus.map(({ title, url }) => ( - - {title} - - ))} - {React.Children.toArray(children).map((child: React.ReactElement) => { - return React.cloneElement(child, { - className: cssNames(child.props.className, { visible: this.isExpanded }), - }); - })} -
-
- ); - } - - return ( - isActive}> - {icon} - {text} - - ); - } -}