1
0
mirror of https://github.com/lensapp/lens.git synced 2025-05-20 05:10:56 +00:00

Fix: highlight sidebar's active section (#2366)

* - fix: highlight active sidebar section / link
- fix: caching crd definitions to prevent sidebar's scroll jumps
- refactored & simplified `Sidebar` and `SidebarItem`

Signed-off-by: Roman <ixrock@gmail.com>

* responding to comments

Signed-off-by: Roman <ixrock@gmail.com>

* adding @jsdoc comments to SidebarItem

Signed-off-by: Roman <ixrock@gmail.com>
This commit is contained in:
Roman 2021-03-24 15:17:56 +02:00 committed by GitHub
parent ee4d434d35
commit 4303700bb2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 80 additions and 54 deletions

View File

@ -2,7 +2,7 @@ import React from "react";
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import { Redirect, Route, Switch } from "react-router"; import { Redirect, Route, Switch } from "react-router";
import { TabLayout, TabLayoutRoute } from "../layout/tab-layout"; import { TabLayout, TabLayoutRoute } from "../layout/tab-layout";
import { crdResourcesRoute, crdRoute, crdURL, crdDefinitionsRoute } from "./crd.route"; import { crdDefinitionsRoute, crdResourcesRoute, crdURL } from "./crd.route";
import { CrdList } from "./crd-list"; import { CrdList } from "./crd-list";
import { CrdResources } from "./crd-resources"; import { CrdResources } from "./crd-resources";
@ -14,7 +14,7 @@ export class CustomResources extends React.Component {
title: "Definitions", title: "Definitions",
component: CustomResources, component: CustomResources,
url: crdURL(), url: crdURL(),
routePath: crdRoute.path.toString(), routePath: String(crdDefinitionsRoute.path),
} }
]; ];
} }

View File

@ -6,19 +6,27 @@ import { cssNames, prevDefault } from "../../utils";
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import { NavLink } from "react-router-dom"; import { NavLink } from "react-router-dom";
import { Icon } from "../icon"; import { Icon } from "../icon";
import { TabLayoutRoute } from "./tab-layout";
import { sidebarStorage } from "./sidebar-storage"; import { sidebarStorage } from "./sidebar-storage";
import { isActiveRoute } from "../../navigation"; import { isActiveRoute } from "../../navigation";
interface SidebarItemProps { interface SidebarItemProps {
id: string; // Used to save nav item collapse/expand state in local storage /**
* Unique id, used in storage and integration tests
*/
id: string;
url: string; url: string;
text: React.ReactNode | string;
className?: string; className?: string;
text: React.ReactNode;
icon?: React.ReactNode; icon?: React.ReactNode;
isHidden?: boolean; isHidden?: boolean;
/**
* Forces this item to be also show as active or not.
*
* Default: dynamically checks the location against the `url` props to determine if
* this item should be shown as active
*/
isActive?: boolean; isActive?: boolean;
subMenus?: TabLayoutRoute[]; subMenus?: React.ReactNode | React.ComponentType<SidebarItemProps>[];
} }
@observer @observer
@ -26,22 +34,30 @@ export class SidebarItem extends React.Component<SidebarItemProps> {
static displayName = "SidebarItem"; static displayName = "SidebarItem";
get id(): string { get id(): string {
return this.props.id; // unique id, used in storage and integration tests return this.props.id;
} }
get compact(): boolean { @computed get compact(): boolean {
return Boolean(sidebarStorage.get().compact); return Boolean(sidebarStorage.get().compact);
} }
get expanded(): boolean { @computed get expanded(): boolean {
return Boolean(sidebarStorage.get().expanded[this.id]); return Boolean(sidebarStorage.get().expanded[this.id]);
} }
@computed get isExpandable(): boolean { @computed get isActive(): boolean {
const { subMenus, children } = this.props; return this.props.isActive ?? isActiveRoute({
const hasContent = subMenus?.length > 0 || children; path: this.props.url,
exact: true,
});
}
return Boolean(hasContent && !this.compact) /*not available in compact-mode*/; @computed get isExpandable(): boolean {
if (this.compact) {
return false; // not available currently
}
return Boolean(this.props.subMenus || this.props.children);
} }
toggleExpand = () => { toggleExpand = () => {
@ -51,11 +67,11 @@ export class SidebarItem extends React.Component<SidebarItemProps> {
}; };
render() { render() {
const { isHidden, isActive, subMenus = [], icon, text, children, url, className } = this.props; const { isHidden, icon, text, children, url, className, subMenus } = this.props;
if (isHidden) return null; if (isHidden) return null;
const { id, compact, expanded, isExpandable, toggleExpand } = this; const { isActive, id, compact, expanded, isExpandable, toggleExpand } = this;
const classNames = cssNames(SidebarItem.displayName, className, { const classNames = cssNames(SidebarItem.displayName, className, {
compact, compact,
}); });
@ -76,19 +92,7 @@ export class SidebarItem extends React.Component<SidebarItemProps> {
</NavLink> </NavLink>
{isExpandable && expanded && ( {isExpandable && expanded && (
<ul className={cssNames("sub-menu", { active: isActive })}> <ul className={cssNames("sub-menu", { active: isActive })}>
{subMenus.map(({ title, routePath, url = routePath }) => { {subMenus}
const subItemId = `${id}${routePath}`;
return (
<SidebarItem
key={subItemId}
id={subItemId}
url={url}
text={title}
isActive={isActiveRoute({ path: url, exact: true })}
/>
);
})}
{children} {children}
</ul> </ul>
)} )}

View File

@ -1,7 +1,8 @@
import "./sidebar.scss"; import "./sidebar.scss";
import type { TabLayoutRoute } from "./tab-layout";
import React from "react"; import React from "react";
import type { TabLayoutRoute } from "./tab-layout"; import { computed } from "mobx";
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import { NavLink } from "react-router-dom"; import { NavLink } from "react-router-dom";
import { cssNames } from "../../utils"; import { cssNames } from "../../utils";
@ -22,7 +23,7 @@ import { UserManagement } from "../+user-management";
import { Storage } from "../+storage"; import { Storage } from "../+storage";
import { Network } from "../+network"; import { Network } from "../+network";
import { crdStore } from "../+custom-resources/crd.store"; import { crdStore } from "../+custom-resources/crd.store";
import { CrdList, crdResourcesRoute, crdRoute, crdURL } from "../+custom-resources"; import { crdRoute, crdURL } from "../+custom-resources";
import { CustomResources } from "../+custom-resources/custom-resources"; import { CustomResources } from "../+custom-resources/custom-resources";
import { isActiveRoute } from "../../navigation"; import { isActiveRoute } from "../../navigation";
import { isAllowedResource } from "../../../common/rbac"; import { isAllowedResource } from "../../../common/rbac";
@ -44,29 +45,50 @@ export class Sidebar extends React.Component<Props> {
crdStore.reloadAll(); crdStore.reloadAll();
} }
renderCustomResources() { @computed get crdSubMenus(): React.ReactNode {
if (crdStore.isLoading) { if (!crdStore.isLoaded && crdStore.isLoading) {
return <Spinner centerHorizontal/>; return <Spinner centerHorizontal/>;
} }
return Object.entries(crdStore.groups).map(([group, crds]) => { return Object.entries(crdStore.groups).map(([group, crds]) => {
const submenus: TabLayoutRoute[] = crds.map((crd) => { const crdGroupSubMenu: React.ReactNode = crds.map((crd) => {
return { return (
title: crd.getResourceKind(), <SidebarItem
component: CrdList, key={crd.getResourceApiBase()}
url: crd.getResourceUrl(), id={`crd-resource:${crd.getResourceApiBase()}`}
routePath: String(crdResourcesRoute.path), url={crd.getResourceUrl()}
}; text={crd.getResourceTitle()}
/>
);
}); });
return ( return (
<SidebarItem <SidebarItem
key={group} key={group}
text={group}
id={`crd-group:${group}`} id={`crd-group:${group}`}
url={crdURL({ query: { groups: group } })} url={crdURL({ query: { groups: group } })}
subMenus={submenus} subMenus={crdGroupSubMenu}
text={group} />
isActive={false} );
});
}
renderTreeFromTabRoutes(tabRoutes: TabLayoutRoute[] = []): React.ReactNode {
if (!tabRoutes.length) {
return null;
}
return tabRoutes.map(({ title, routePath, url = routePath, exact = true }) => {
const subMenuItemId = `tab-route-item-${url}`;
return (
<SidebarItem
key={subMenuItemId}
id={subMenuItemId}
url={url}
text={title}
isActive={isActiveRoute({ path: routePath, exact })}
/> />
); );
}); });
@ -122,10 +144,10 @@ export class Sidebar extends React.Component<Props> {
key={id} key={id}
id={id} id={id}
url={pageUrl} url={pageUrl}
isActive={isActive}
text={menuItem.title} text={menuItem.title}
icon={<menuItem.components.Icon/>} icon={<menuItem.components.Icon/>}
isActive={isActive} subMenus={this.renderTreeFromTabRoutes(tabRoutes)}
subMenus={tabRoutes}
/> />
); );
}); });
@ -172,7 +194,7 @@ export class Sidebar extends React.Component<Props> {
isActive={isActiveRoute(workloadsRoute)} isActive={isActiveRoute(workloadsRoute)}
isHidden={Workloads.tabRoutes.length == 0} isHidden={Workloads.tabRoutes.length == 0}
url={workloadsURL({ query })} url={workloadsURL({ query })}
subMenus={Workloads.tabRoutes} subMenus={this.renderTreeFromTabRoutes(Workloads.tabRoutes)}
text="Workloads" text="Workloads"
icon={<Icon svg="workloads"/>} icon={<Icon svg="workloads"/>}
/> />
@ -181,7 +203,7 @@ export class Sidebar extends React.Component<Props> {
isActive={isActiveRoute(configRoute)} isActive={isActiveRoute(configRoute)}
isHidden={Config.tabRoutes.length == 0} isHidden={Config.tabRoutes.length == 0}
url={configURL({ query })} url={configURL({ query })}
subMenus={Config.tabRoutes} subMenus={this.renderTreeFromTabRoutes(Config.tabRoutes)}
text="Configuration" text="Configuration"
icon={<Icon material="list"/>} icon={<Icon material="list"/>}
/> />
@ -190,7 +212,7 @@ export class Sidebar extends React.Component<Props> {
isActive={isActiveRoute(networkRoute)} isActive={isActiveRoute(networkRoute)}
isHidden={Network.tabRoutes.length == 0} isHidden={Network.tabRoutes.length == 0}
url={networkURL({ query })} url={networkURL({ query })}
subMenus={Network.tabRoutes} subMenus={this.renderTreeFromTabRoutes(Network.tabRoutes)}
text="Network" text="Network"
icon={<Icon material="device_hub"/>} icon={<Icon material="device_hub"/>}
/> />
@ -199,7 +221,7 @@ export class Sidebar extends React.Component<Props> {
isActive={isActiveRoute(storageRoute)} isActive={isActiveRoute(storageRoute)}
isHidden={Storage.tabRoutes.length == 0} isHidden={Storage.tabRoutes.length == 0}
url={storageURL({ query })} url={storageURL({ query })}
subMenus={Storage.tabRoutes} subMenus={this.renderTreeFromTabRoutes(Storage.tabRoutes)}
icon={<Icon svg="storage"/>} icon={<Icon svg="storage"/>}
text="Storage" text="Storage"
/> />
@ -223,7 +245,7 @@ export class Sidebar extends React.Component<Props> {
id="apps" id="apps"
isActive={isActiveRoute(appsRoute)} isActive={isActiveRoute(appsRoute)}
url={appsURL({ query })} url={appsURL({ query })}
subMenus={Apps.tabRoutes} subMenus={this.renderTreeFromTabRoutes(Apps.tabRoutes)}
icon={<Icon material="apps"/>} icon={<Icon material="apps"/>}
text="Apps" text="Apps"
/> />
@ -231,20 +253,20 @@ export class Sidebar extends React.Component<Props> {
id="users" id="users"
isActive={isActiveRoute(usersManagementRoute)} isActive={isActiveRoute(usersManagementRoute)}
url={usersManagementURL({ query })} url={usersManagementURL({ query })}
subMenus={UserManagement.tabRoutes} subMenus={this.renderTreeFromTabRoutes(UserManagement.tabRoutes)}
icon={<Icon material="security"/>} icon={<Icon material="security"/>}
text="Access Control" text="Access Control"
/> />
<SidebarItem <SidebarItem
id="custom-resources" id="custom-resources"
text="Custom Resources"
url={crdURL()}
isActive={isActiveRoute(crdRoute)} isActive={isActiveRoute(crdRoute)}
isHidden={!isAllowedResource("customresourcedefinitions")} isHidden={!isAllowedResource("customresourcedefinitions")}
url={crdURL()}
subMenus={CustomResources.tabRoutes}
icon={<Icon material="extension"/>} icon={<Icon material="extension"/>}
text="Custom Resources"
> >
{this.renderCustomResources()} {this.renderTreeFromTabRoutes(CustomResources.tabRoutes)}
{this.crdSubMenus}
</SidebarItem> </SidebarItem>
{this.renderRegisteredMenus()} {this.renderRegisteredMenus()}
</div> </div>