1
0
mirror of https://github.com/lensapp/lens.git synced 2024-11-10 18:55:52 +03:00

fix side bar scrolls after clicking on lower item (#928)

* fix bug: side bar scrolls after clicking on lower item

Signed-off-by: Yangjun Wang <yangjun.wang@wartsila.com>

* fix issue main area missing issue, add mobx observer to TabLayout

Signed-off-by: Yangjun Wang <yangjun.wang@wartsila.com>

Co-authored-by: Yangjun Wang <yangjun.wang@wartsila.com>
This commit is contained in:
Yangjun Wang 2020-09-21 22:18:41 +03:00 committed by GitHub
parent 9a10db837e
commit cb3ab09b42
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 185 additions and 154 deletions

View File

@ -1,15 +1,15 @@
import React from "react";
import { Trans } from "@lingui/macro";
import { MainLayout } from "../layout/main-layout";
import { TabLayout } from "../layout/tab-layout";
export class NotFound extends React.Component {
render() {
return (
<MainLayout className="NotFound" contentClass="flex" footer={null}>
<TabLayout className="NotFound" contentClass="flex">
<p className="box center">
<Trans>Page not found</Trans>
</p>
</MainLayout>
)
</TabLayout>
);
}
}
}

View File

@ -2,7 +2,7 @@ import React from "react";
import { observer } from "mobx-react";
import { Redirect, Route, Switch } from "react-router";
import { Trans } from "@lingui/macro";
import { MainLayout, TabRoute } from "../layout/main-layout";
import { TabLayout, TabRoute } from "../layout/tab-layout";
import { HelmCharts, helmChartsRoute, helmChartsURL } from "../+apps-helm-charts";
import { HelmReleases, releaseRoute, releaseURL } from "../+apps-releases";
import { namespaceStore } from "../+namespaces/namespace.store";
@ -30,12 +30,12 @@ export class Apps extends React.Component {
render() {
const tabRoutes = Apps.tabRoutes;
return (
<MainLayout className="Apps" tabs={tabRoutes}>
<TabLayout className="Apps" tabs={tabRoutes}>
<Switch>
{tabRoutes.map((route, index) => <Route key={index} {...route}/>)}
<Redirect to={tabRoutes[0].url}/>
</Switch>
</MainLayout>
</TabLayout>
)
}
}

View File

@ -3,7 +3,7 @@ import "./cluster.scss"
import React from "react";
import { computed, reaction } from "mobx";
import { disposeOnUnmount, observer } from "mobx-react";
import { MainLayout } from "../layout/main-layout";
import { TabLayout } from "../layout/tab-layout";
import { ClusterIssues } from "./cluster-issues";
import { Spinner } from "../spinner";
import { cssNames, interval, isElectron } from "../../utils";
@ -54,7 +54,7 @@ export class Cluster extends React.Component {
render() {
const { isLoaded } = this;
return (
<MainLayout>
<TabLayout>
<div className="Cluster">
{!isLoaded && <Spinner center/>}
{isLoaded && (
@ -65,7 +65,7 @@ export class Cluster extends React.Component {
</>
)}
</div>
</MainLayout>
</TabLayout>
)
}
}

View File

@ -2,7 +2,7 @@ import React from "react";
import { observer } from "mobx-react";
import { Redirect, Route, Switch } from "react-router";
import { Trans } from "@lingui/macro";
import { MainLayout, TabRoute } from "../layout/main-layout";
import { TabLayout, TabRoute } from "../layout/tab-layout";
import { ConfigMaps, configMapsRoute, configMapsURL } from "../+config-maps";
import { Secrets, secretsRoute, secretsURL } from "../+config-secrets";
import { namespaceStore } from "../+namespaces/namespace.store";
@ -68,12 +68,12 @@ export class Config extends React.Component {
render() {
const tabRoutes = Config.tabRoutes;
return (
<MainLayout className="Config" tabs={tabRoutes}>
<TabLayout className="Config" tabs={tabRoutes}>
<Switch>
{tabRoutes.map((route, index) => <Route key={index} {...route}/>)}
<Redirect to={configURL({ query: namespaceStore.getContextParams() })}/>
</Switch>
</MainLayout>
</TabLayout>
)
}
}

View File

@ -2,7 +2,7 @@ import React from "react";
import { observer } from "mobx-react";
import { Redirect, Route, Switch } from "react-router";
import { Trans } from "@lingui/macro";
import { MainLayout, TabRoute } from "../layout/main-layout";
import { TabLayout, TabRoute } from "../layout/tab-layout";
import { crdResourcesRoute, crdRoute, crdURL, crdDefinitionsRoute } from "./crd.route";
import { CrdList } from "./crd-list";
import { CrdResources } from "./crd-resources";
@ -25,13 +25,13 @@ export class CustomResources extends React.Component {
render() {
return (
<MainLayout>
<TabLayout>
<Switch>
<Route component={CrdList} {...crdDefinitionsRoute} exact/>
<Route component={CrdResources} {...crdResourcesRoute}/>
<Redirect to={crdURL()}/>
</Switch>
</MainLayout>
</TabLayout>
);
}
}

View File

@ -2,7 +2,7 @@ import "./events.scss";
import React, { Fragment } from "react";
import { observer } from "mobx-react";
import { MainLayout } from "../layout/main-layout";
import { TabLayout } from "../layout/tab-layout";
import { eventStore } from "./event.store";
import { KubeObjectListLayout, KubeObjectListLayoutProps } from "../kube-object";
import { Trans } from "@lingui/macro";
@ -118,9 +118,9 @@ export class Events extends React.Component<Props> {
return events;
}
return (
<MainLayout>
<TabLayout>
{events}
</MainLayout>
</TabLayout>
)
}
}

View File

@ -4,7 +4,7 @@ import React from "react";
import { Trans } from "@lingui/macro";
import { Namespace, namespacesApi, NamespaceStatus } from "../../api/endpoints";
import { AddNamespaceDialog } from "./add-namespace-dialog";
import { MainLayout } from "../layout/main-layout";
import { TabLayout } from "../layout/tab-layout";
import { Badge } from "../badge";
import { RouteComponentProps } from "react-router";
import { KubeObjectMenu, KubeObjectMenuProps } from "../kube-object/kube-object-menu";
@ -26,7 +26,7 @@ interface Props extends RouteComponentProps<INamespacesRouteParams> {
export class Namespaces extends React.Component<Props> {
render() {
return (
<MainLayout>
<TabLayout>
<KubeObjectListLayout
isClusterScoped
className="Namespaces" store={namespaceStore}
@ -65,7 +65,7 @@ export class Namespaces extends React.Component<Props> {
})}
/>
<AddNamespaceDialog/>
</MainLayout>
</TabLayout>
)
}
}

View File

@ -5,7 +5,7 @@ import { observer } from "mobx-react";
import { Redirect, Route, Switch } from "react-router";
import { RouteComponentProps } from "react-router-dom";
import { Trans } from "@lingui/macro";
import { MainLayout, TabRoute } from "../layout/main-layout";
import { TabLayout, TabRoute } from "../layout/tab-layout";
import { Services, servicesRoute, servicesURL } from "../+network-services";
import { Endpoints, endpointRoute, endpointURL } from "../+network-endpoints";
import { Ingresses, ingressRoute, ingressURL } from "../+network-ingresses";
@ -60,12 +60,12 @@ export class Network extends React.Component<Props> {
render() {
const tabRoutes = Network.tabRoutes;
return (
<MainLayout className="Network" tabs={tabRoutes}>
<TabLayout className="Network" tabs={tabRoutes}>
<Switch>
{tabRoutes.map((route, index) => <Route key={index} {...route}/>)}
<Redirect to={networkURL({ query: namespaceStore.getContextParams() })}/>
</Switch>
</MainLayout>
</TabLayout>
)
}
}

View File

@ -4,7 +4,7 @@ import { observer } from "mobx-react";
import { RouteComponentProps } from "react-router";
import { t, Trans } from "@lingui/macro";
import { cssNames, interval } from "../../utils";
import { MainLayout } from "../layout/main-layout";
import { TabLayout } from "../layout/tab-layout";
import { nodesStore } from "./nodes.store";
import { podsStore } from "../+workloads-pods/pods.store";
import { KubeObjectListLayout } from "../kube-object";
@ -123,7 +123,7 @@ export class Nodes extends React.Component<Props> {
render() {
return (
<MainLayout>
<TabLayout>
<KubeObjectListLayout
className="Nodes"
store={nodesStore} isClusterScoped
@ -182,7 +182,7 @@ export class Nodes extends React.Component<Props> {
return <NodeMenu object={item}/>
}}
/>
</MainLayout>
</TabLayout>
)
}
}

View File

@ -5,7 +5,7 @@ import { observer } from "mobx-react";
import { Redirect, Route, Switch } from "react-router";
import { RouteComponentProps } from "react-router-dom";
import { Trans } from "@lingui/macro";
import { MainLayout, TabRoute } from "../layout/main-layout";
import { TabLayout, TabRoute } from "../layout/tab-layout";
import { PersistentVolumes, volumesRoute, volumesURL } from "../+storage-volumes";
import { StorageClasses, storageClassesRoute, storageClassesURL } from "../+storage-classes";
import { PersistentVolumeClaims, volumeClaimsRoute, volumeClaimsURL } from "../+storage-volume-claims";
@ -52,12 +52,12 @@ export class Storage extends React.Component<Props> {
render() {
const tabRoutes = Storage.tabRoutes;
return (
<MainLayout className="Storage" tabs={tabRoutes}>
<TabLayout className="Storage" tabs={tabRoutes}>
<Switch>
{tabRoutes.map((route, index) => <Route key={index} {...route}/>)}
<Redirect to={storageURL({ query: namespaceStore.getContextParams() })}/>
</Switch>
</MainLayout>
</TabLayout>
)
}
}

View File

@ -4,7 +4,7 @@ import { observer } from "mobx-react";
import { Redirect, Route, Switch } from "react-router";
import { RouteComponentProps } from "react-router-dom";
import { Trans } from "@lingui/macro";
import { MainLayout, TabRoute } from "../layout/main-layout";
import { TabLayout, TabRoute } from "../layout/tab-layout";
import { Roles } from "../+user-management-roles";
import { RoleBindings } from "../+user-management-roles-bindings";
import { ServiceAccounts } from "../+user-management-service-accounts";
@ -55,12 +55,12 @@ export class UserManagement extends React.Component<Props> {
render() {
const tabRoutes = UserManagement.tabRoutes;
return (
<MainLayout className="UserManagement" tabs={tabRoutes}>
<TabLayout className="UserManagement" tabs={tabRoutes}>
<Switch>
{tabRoutes.map((route, index) => <Route key={index} {...route}/>)}
<Redirect to={usersManagementURL({ query: namespaceStore.getContextParams() })}/>
</Switch>
</MainLayout>
</TabLayout>
)
}
}

View File

@ -5,7 +5,7 @@ import { observer } from "mobx-react";
import { Redirect, Route, Switch } from "react-router";
import { RouteComponentProps } from "react-router-dom";
import { Trans } from "@lingui/macro";
import { MainLayout, TabRoute } from "../layout/main-layout";
import { TabLayout, TabRoute } from "../layout/tab-layout";
import { WorkloadsOverview } from "../+workloads-overview/overview";
import { cronJobsRoute, cronJobsURL, daemonSetsRoute, daemonSetsURL, deploymentsRoute, deploymentsURL, jobsRoute, jobsURL, overviewRoute, overviewURL, podsRoute, podsURL, statefulSetsRoute, statefulSetsURL, workloadsURL } from "./workloads.route";
import { namespaceStore } from "../+namespaces/namespace.store";
@ -86,12 +86,12 @@ export class Workloads extends React.Component<Props> {
render() {
const tabRoutes = Workloads.tabRoutes;
return (
<MainLayout className="Workloads" tabs={tabRoutes}>
<TabLayout className="Workloads" tabs={tabRoutes}>
<Switch>
{tabRoutes.map((route, index) => <Route key={index} {...route}/>)}
<Redirect to={workloadsURL({ query: namespaceStore.getContextParams() })}/>
</Switch>
</MainLayout>
</TabLayout>
)
}
}

View File

@ -35,6 +35,7 @@ import { getHostedCluster, getHostedClusterId } from "../../common/cluster-store
import logger from "../../main/logger";
import { clusterIpc } from "../../common/cluster-ipc";
import { webFrame } from "electron";
import { MainLayout } from "./layout/main-layout";
@observer
export class App extends React.Component {
@ -59,21 +60,23 @@ export class App extends React.Component {
<I18nProvider i18n={_i18n}>
<Router history={history}>
<ErrorBoundary>
<Switch>
<Route component={Cluster} {...clusterRoute}/>
<Route component={Nodes} {...nodesRoute}/>
<Route component={Workloads} {...workloadsRoute}/>
<Route component={Config} {...configRoute}/>
<Route component={Network} {...networkRoute}/>
<Route component={Storage} {...storageRoute}/>
<Route component={Namespaces} {...namespacesRoute}/>
<Route component={Events} {...eventRoute}/>
<Route component={CustomResources} {...crdRoute}/>
<Route component={UserManagement} {...usersManagementRoute}/>
<Route component={Apps} {...appsRoute}/>
<Redirect exact from="/" to={this.startURL}/>
<Route component={NotFound}/>
</Switch>
<MainLayout>
<Switch>
<Route component={Cluster} {...clusterRoute}/>
<Route component={Nodes} {...nodesRoute}/>
<Route component={Workloads} {...workloadsRoute}/>
<Route component={Config} {...configRoute}/>
<Route component={Network} {...networkRoute}/>
<Route component={Storage} {...storageRoute}/>
<Route component={Namespaces} {...namespacesRoute}/>
<Route component={Events} {...eventRoute}/>
<Route component={CustomResources} {...crdRoute}/>
<Route component={UserManagement} {...usersManagementRoute}/>
<Route component={Apps} {...appsRoute}/>
<Redirect exact from="/" to={this.startURL}/>
<Route component={NotFound}/>
</Switch>
</MainLayout>
<Notifications/>
<ConfirmDialog/>
<KubeObjectDetails/>

View File

@ -1,19 +1,18 @@
.MainLayout {
--sidebar-max-size: 200px;
display: grid;
grid-template-areas: "aside header" "aside tabs" "aside main" "aside footer";
grid-template-areas:
"aside header"
"aside tabs"
"aside main"
"aside footer";
grid-template-rows: [header] var(--main-layout-header) [tabs] min-content [main] 1fr [footer] auto;
grid-template-columns: [sidebar] minmax(var(--main-layout-header), min-content) [main] 1fr;
height: 100%;
> .Tabs {
grid-area: tabs;
background: $layoutTabsBackground;
}
header {
> header {
grid-area: header;
background: $layoutBackground;
padding: $padding $padding * 2;
@ -28,7 +27,7 @@
}
}
aside {
> aside {
grid-area: aside;
position: relative;
background: $sidebarBackground;
@ -48,25 +47,14 @@
&.accessible:hover {
width: var(--sidebar-max-size);
transition-delay: 750ms;
box-shadow: 3px 3px 16px rgba(0, 0, 0, .35);
box-shadow: 3px 3px 16px rgba(0, 0, 0, 0.35);
z-index: $zIndex-sidebar-hover;
}
}
}
main {
@include custom-scrollbar;
$spacing: $margin * 2;
.theme-light & {
@include custom-scrollbar(dark);
}
grid-area: main;
overflow-y: scroll; // always reserve space for scrollbar (17px)
overflow-x: auto;
margin: $spacing;
margin-right: 0;
> main {
display: contents;
}
footer {
@ -74,4 +62,4 @@
grid-area: footer;
min-width: 0; // restrict size when overflow content (e.g. <Dock> tabs scrolling)
}
}
}

View File

@ -3,26 +3,16 @@ import "./main-layout.scss";
import React from "react";
import { observable, reaction } from "mobx";
import { disposeOnUnmount, observer } from "mobx-react";
import { matchPath, RouteProps } from "react-router-dom";
import { createStorage, cssNames } from "../../utils";
import { Tab, Tabs } from "../tabs";
import { Sidebar } from "./sidebar";
import { ErrorBoundary } from "../error-boundary";
import { Dock } from "../dock";
import { navigate, navigation } from "../../navigation";
import { getHostedCluster } from "../../../common/cluster-store";
export interface TabRoute extends RouteProps {
title: React.ReactNode;
url: string;
}
interface Props {
className?: any;
tabs?: TabRoute[];
footer?: React.ReactNode;
headerClass?: string;
contentClass?: string;
footerClass?: string;
}
@ -35,18 +25,17 @@ export class MainLayout extends React.Component<Props> {
@disposeOnUnmount syncPinnedStateWithStorage = reaction(
() => this.isPinned,
isPinned => this.storage.merge({ pinnedSidebar: isPinned })
(isPinned) => this.storage.merge({ pinnedSidebar: isPinned })
);
toggleSidebar = () => {
this.isPinned = !this.isPinned;
this.isAccessible = false;
setTimeout(() => this.isAccessible = true, 250);
}
setTimeout(() => (this.isAccessible = true), 250);
};
render() {
const { className, contentClass, headerClass, tabs, footer, footerClass, children } = this.props;
const routePath = navigation.location.pathname;
const { className, headerClass, footer, footerClass, children } = this.props;
const cluster = getHostedCluster();
if (!cluster) {
return null; // fix: skip render when removing active (visible) cluster
@ -54,37 +43,18 @@ export class MainLayout extends React.Component<Props> {
return (
<div className={cssNames("MainLayout", className)}>
<header className={cssNames("flex gaps align-center", headerClass)}>
<span className="cluster">
{cluster.preferences.clusterName || cluster.contextName}
</span>
<span className="cluster">{cluster.preferences.clusterName || cluster.contextName}</span>
</header>
<aside className={cssNames("flex column", { pinned: this.isPinned, accessible: this.isAccessible })}>
<Sidebar
className="box grow"
isPinned={this.isPinned}
toggle={this.toggleSidebar}
/>
<Sidebar className="box grow" isPinned={this.isPinned} toggle={this.toggleSidebar} />
</aside>
{tabs && (
<Tabs center onChange={url => navigate(url)}>
{tabs.map(({ title, path, url, ...routeProps }) => {
const isActive = !!matchPath(routePath, { path, ...routeProps });
return <Tab key={url} label={title} value={url} active={isActive}/>
})}
</Tabs>
)}
<main className={contentClass}>
<ErrorBoundary>
{children}
</ErrorBoundary>
<main>
<ErrorBoundary>{children}</ErrorBoundary>
</main>
<footer className={footerClass}>
{footer === undefined ? <Dock/> : footer}
</footer>
<footer className={footerClass}>{footer === undefined ? <Dock /> : footer}</footer>
</div>
);
}

View File

@ -1,4 +1,4 @@
import type { TabRoute } from "./main-layout";
import type { TabRoute } from "./tab-layout";
import "./sidebar.scss";
import React from "react";
@ -27,7 +27,7 @@ import { crdStore } from "../+custom-resources/crd.store";
import { CrdList, crdResourcesRoute, crdRoute, crdURL } from "../+custom-resources";
import { CustomResources } from "../+custom-resources/custom-resources";
import { navigation } from "../../navigation";
import { isAllowedResource } from "../../../common/rbac"
import { isAllowedResource } from "../../../common/rbac";
const SidebarContext = React.createContext<SidebarContextValue>({ pinned: false });
type SidebarContextValue = {
@ -43,21 +43,21 @@ interface Props {
@observer
export class Sidebar extends React.Component<Props> {
async componentDidMount() {
if (!crdStore.isLoaded && isAllowedResource('customresourcedefinitions')) {
crdStore.loadAll()
if (!crdStore.isLoaded && isAllowedResource("customresourcedefinitions")) {
crdStore.loadAll();
}
}
renderCustomResources() {
return Object.entries(crdStore.groups).map(([group, crds]) => {
const submenus = crds.map(crd => {
const submenus = crds.map((crd) => {
return {
title: crd.getResourceKind(),
component: CrdList,
url: crd.getResourceUrl(),
path: crdResourcesRoute.path,
}
})
};
});
return (
<SidebarNavItem
key={group}
@ -67,8 +67,8 @@ export class Sidebar extends React.Component<Props> {
subMenus={submenus}
text={group}
/>
)
})
);
});
}
render() {
@ -79,7 +79,7 @@ export class Sidebar extends React.Component<Props> {
<div className={cssNames("Sidebar flex column", className, { pinned: isPinned })}>
<div className="header flex align-center">
<NavLink exact to="/" className="box grow">
<Icon svg="logo-full" className="logo-icon"/>
<Icon svg="logo-full" className="logo-icon" />
<div className="logo-text">Lens</div>
</NavLink>
<Icon
@ -93,17 +93,17 @@ export class Sidebar extends React.Component<Props> {
<div className="sidebar-nav flex column box grow-fixed">
<SidebarNavItem
id="cluster"
isHidden={!isAllowedResource('nodes')}
isHidden={!isAllowedResource("nodes")}
url={clusterURL()}
text={<Trans>Cluster</Trans>}
icon={<Icon svg="kube"/>}
icon={<Icon svg="kube" />}
/>
<SidebarNavItem
id="nodes"
isHidden={!isAllowedResource('nodes')}
isHidden={!isAllowedResource("nodes")}
url={nodesURL()}
text={<Trans>Nodes</Trans>}
icon={<Icon svg="nodes"/>}
icon={<Icon svg="nodes" />}
/>
<SidebarNavItem
id="workloads"
@ -112,7 +112,7 @@ export class Sidebar extends React.Component<Props> {
routePath={workloadsRoute.path}
subMenus={Workloads.tabRoutes}
text={<Trans>Workloads</Trans>}
icon={<Icon svg="workloads"/>}
icon={<Icon svg="workloads" />}
/>
<SidebarNavItem
id="config"
@ -121,7 +121,7 @@ export class Sidebar extends React.Component<Props> {
routePath={configRoute.path}
subMenus={Config.tabRoutes}
text={<Trans>Configuration</Trans>}
icon={<Icon material="list"/>}
icon={<Icon material="list" />}
/>
<SidebarNavItem
id="networks"
@ -130,7 +130,7 @@ export class Sidebar extends React.Component<Props> {
routePath={networkRoute.path}
subMenus={Network.tabRoutes}
text={<Trans>Network</Trans>}
icon={<Icon material="device_hub"/>}
icon={<Icon material="device_hub" />}
/>
<SidebarNavItem
id="storage"
@ -138,22 +138,22 @@ export class Sidebar extends React.Component<Props> {
url={storageURL({ query })}
routePath={storageRoute.path}
subMenus={Storage.tabRoutes}
icon={<Icon svg="storage"/>}
icon={<Icon svg="storage" />}
text={<Trans>Storage</Trans>}
/>
<SidebarNavItem
id="namespaces"
isHidden={!isAllowedResource('namespaces')}
isHidden={!isAllowedResource("namespaces")}
url={namespacesURL()}
icon={<Icon material="layers"/>}
icon={<Icon material="layers" />}
text={<Trans>Namespaces</Trans>}
/>
<SidebarNavItem
id="events"
isHidden={!isAllowedResource('events')}
isHidden={!isAllowedResource("events")}
url={eventsURL({ query })}
routePath={eventRoute.path}
icon={<Icon material="access_time"/>}
icon={<Icon material="access_time" />}
text={<Trans>Events</Trans>}
/>
<SidebarNavItem
@ -161,7 +161,7 @@ export class Sidebar extends React.Component<Props> {
url={appsURL({ query })}
subMenus={Apps.tabRoutes}
routePath={appsRoute.path}
icon={<Icon material="apps"/>}
icon={<Icon material="apps" />}
text={<Trans>Apps</Trans>}
/>
<SidebarNavItem
@ -169,16 +169,16 @@ export class Sidebar extends React.Component<Props> {
url={usersManagementURL({ query })}
routePath={usersManagementRoute.path}
subMenus={UserManagement.tabRoutes}
icon={<Icon material="security"/>}
icon={<Icon material="security" />}
text={<Trans>Access Control</Trans>}
/>
<SidebarNavItem
id="custom-resources"
isHidden={!isAllowedResource('customresourcedefinitions')}
isHidden={!isAllowedResource("customresourcedefinitions")}
url={crdURL()}
subMenus={CustomResources.tabRoutes}
routePath={crdRoute.path}
icon={<Icon material="extension"/>}
icon={<Icon material="extension" />}
text={<Trans>Custom Resources</Trans>}
>
{this.renderCustomResources()}
@ -186,7 +186,7 @@ export class Sidebar extends React.Component<Props> {
</div>
</div>
</SidebarContext.Provider>
)
);
}
}
@ -203,7 +203,10 @@ interface SidebarNavItemProps {
const navItemStorage = createStorage<[string, boolean][]>("sidebar_menu_item", []);
const navItemState = observable.map<string, boolean>(navItemStorage.get());
reaction(() => [...navItemState], value => navItemStorage.set(value));
reaction(
() => [...navItemState],
(value) => navItemStorage.set(value)
);
@observer
class SidebarNavItem extends React.Component<SidebarNavItemProps> {
@ -216,15 +219,15 @@ class SidebarNavItem extends React.Component<SidebarNavItemProps> {
toggleSubMenu = () => {
navItemState.set(this.props.id, !this.isExpanded);
}
};
isActive = () => {
const { routePath, url } = this.props;
const { pathname } = navigation.location;
return !!matchPath(pathname, {
path: routePath || url
path: routePath || url,
});
}
};
render() {
const { id, isHidden, subMenus = [], icon, text, url, children, className } = this.props;
@ -239,10 +242,7 @@ class SidebarNavItem extends React.Component<SidebarNavItemProps> {
<div className={cssNames("nav-item", { active: isActive })} onClick={this.toggleSubMenu}>
{icon}
<span className="link-text">{text}</span>
<Icon
className="expand-icon"
material={this.isExpanded ? "keyboard_arrow_up" : "keyboard_arrow_down"}
/>
<Icon className="expand-icon" material={this.isExpanded ? "keyboard_arrow_up" : "keyboard_arrow_down"} />
</div>
<ul className={cssNames("sub-menu", { active: isActive })}>
{subMenus.map(({ title, url }) => (
@ -252,18 +252,18 @@ class SidebarNavItem extends React.Component<SidebarNavItemProps> {
))}
{React.Children.toArray(children).map((child: React.ReactElement<any>) => {
return React.cloneElement(child, {
className: cssNames(child.props.className, { visible: this.isExpanded })
className: cssNames(child.props.className, { visible: this.isExpanded }),
});
})}
</ul>
</div>
)
);
}
return (
<NavLink className={cssNames("SidebarNavItem", className)} to={url} isActive={this.isActive}>
{icon}
<span className="link-text">{text}</span>
</NavLink>
)
);
}
}

View File

@ -0,0 +1,25 @@
.TabLayout {
display: contents;
> .Tabs {
grid-area: tabs;
background: $layoutTabsBackground;
}
main {
@include custom-scrollbar;
$spacing: $margin * 2;
.theme-light & {
@include custom-scrollbar(dark);
}
grid-area: main;
overflow-y: scroll; // always reserve space for scrollbar (17px)
overflow-x: auto;
margin: $spacing;
margin-right: 0;
}
}

View File

@ -0,0 +1,45 @@
import "./tab-layout.scss";
import React, { ReactNode } from "react";
import { matchPath, RouteProps } from "react-router-dom";
import { observer } from "mobx-react";
import { cssNames } from "../../utils";
import { Tab, Tabs } from "../tabs";
import { ErrorBoundary } from "../error-boundary";
import { navigate, navigation } from "../../navigation";
import { getHostedCluster } from "../../../common/cluster-store";
export interface TabRoute extends RouteProps {
title: React.ReactNode;
url: string;
}
interface Props {
children: ReactNode;
className?: any;
tabs?: TabRoute[];
contentClass?: string;
}
export const TabLayout = observer(({ className, contentClass, tabs, children }: Props) => {
const routePath = navigation.location.pathname;
const cluster = getHostedCluster();
if (!cluster) {
return null; // fix: skip render when removing active (visible) cluster
}
return (
<div className={cssNames("TabLayout", className)}>
{tabs && (
<Tabs center onChange={(url) => navigate(url)}>
{tabs.map(({ title, path, url, ...routeProps }) => {
const isActive = !!matchPath(routePath, { path, ...routeProps });
return <Tab key={url} label={title} value={url} active={isActive} />;
})}
</Tabs>
)}
<main className={contentClass}>
<ErrorBoundary>{children}</ErrorBoundary>
</main>
</div>
);
});