1
0
mirror of https://github.com/lensapp/lens.git synced 2025-05-20 05:10:56 +00: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 React from "react";
import { Trans } from "@lingui/macro"; import { Trans } from "@lingui/macro";
import { MainLayout } from "../layout/main-layout"; import { TabLayout } from "../layout/tab-layout";
export class NotFound extends React.Component { export class NotFound extends React.Component {
render() { render() {
return ( return (
<MainLayout className="NotFound" contentClass="flex" footer={null}> <TabLayout className="NotFound" contentClass="flex">
<p className="box center"> <p className="box center">
<Trans>Page not found</Trans> <Trans>Page not found</Trans>
</p> </p>
</MainLayout> </TabLayout>
) );
} }
} }

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

View File

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

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

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 { Trans } from "@lingui/macro"; 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 { crdResourcesRoute, crdRoute, crdURL, crdDefinitionsRoute } from "./crd.route";
import { CrdList } from "./crd-list"; import { CrdList } from "./crd-list";
import { CrdResources } from "./crd-resources"; import { CrdResources } from "./crd-resources";
@ -25,13 +25,13 @@ export class CustomResources extends React.Component {
render() { render() {
return ( return (
<MainLayout> <TabLayout>
<Switch> <Switch>
<Route component={CrdList} {...crdDefinitionsRoute} exact/> <Route component={CrdList} {...crdDefinitionsRoute} exact/>
<Route component={CrdResources} {...crdResourcesRoute}/> <Route component={CrdResources} {...crdResourcesRoute}/>
<Redirect to={crdURL()}/> <Redirect to={crdURL()}/>
</Switch> </Switch>
</MainLayout> </TabLayout>
); );
} }
} }

View File

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

View File

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

View File

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

View File

@ -4,7 +4,7 @@ import { observer } from "mobx-react";
import { RouteComponentProps } from "react-router"; import { RouteComponentProps } from "react-router";
import { t, Trans } from "@lingui/macro"; import { t, Trans } from "@lingui/macro";
import { cssNames, interval } from "../../utils"; import { cssNames, interval } from "../../utils";
import { MainLayout } from "../layout/main-layout"; import { TabLayout } from "../layout/tab-layout";
import { nodesStore } from "./nodes.store"; import { nodesStore } from "./nodes.store";
import { podsStore } from "../+workloads-pods/pods.store"; import { podsStore } from "../+workloads-pods/pods.store";
import { KubeObjectListLayout } from "../kube-object"; import { KubeObjectListLayout } from "../kube-object";
@ -123,7 +123,7 @@ export class Nodes extends React.Component<Props> {
render() { render() {
return ( return (
<MainLayout> <TabLayout>
<KubeObjectListLayout <KubeObjectListLayout
className="Nodes" className="Nodes"
store={nodesStore} isClusterScoped store={nodesStore} isClusterScoped
@ -182,7 +182,7 @@ export class Nodes extends React.Component<Props> {
return <NodeMenu object={item}/> 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 { Redirect, Route, Switch } from "react-router";
import { RouteComponentProps } from "react-router-dom"; import { RouteComponentProps } from "react-router-dom";
import { Trans } from "@lingui/macro"; 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 { PersistentVolumes, volumesRoute, volumesURL } from "../+storage-volumes";
import { StorageClasses, storageClassesRoute, storageClassesURL } from "../+storage-classes"; import { StorageClasses, storageClassesRoute, storageClassesURL } from "../+storage-classes";
import { PersistentVolumeClaims, volumeClaimsRoute, volumeClaimsURL } from "../+storage-volume-claims"; import { PersistentVolumeClaims, volumeClaimsRoute, volumeClaimsURL } from "../+storage-volume-claims";
@ -52,12 +52,12 @@ export class Storage extends React.Component<Props> {
render() { render() {
const tabRoutes = Storage.tabRoutes; const tabRoutes = Storage.tabRoutes;
return ( return (
<MainLayout className="Storage" tabs={tabRoutes}> <TabLayout className="Storage" tabs={tabRoutes}>
<Switch> <Switch>
{tabRoutes.map((route, index) => <Route key={index} {...route}/>)} {tabRoutes.map((route, index) => <Route key={index} {...route}/>)}
<Redirect to={storageURL({ query: namespaceStore.getContextParams() })}/> <Redirect to={storageURL({ query: namespaceStore.getContextParams() })}/>
</Switch> </Switch>
</MainLayout> </TabLayout>
) )
} }
} }

View File

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

View File

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

View File

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

View File

@ -1,19 +1,18 @@
.MainLayout { .MainLayout {
--sidebar-max-size: 200px; --sidebar-max-size: 200px;
display: grid; 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-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; grid-template-columns: [sidebar] minmax(var(--main-layout-header), min-content) [main] 1fr;
height: 100%; height: 100%;
> .Tabs { > header {
grid-area: tabs;
background: $layoutTabsBackground;
}
header {
grid-area: header; grid-area: header;
background: $layoutBackground; background: $layoutBackground;
padding: $padding $padding * 2; padding: $padding $padding * 2;
@ -28,7 +27,7 @@
} }
} }
aside { > aside {
grid-area: aside; grid-area: aside;
position: relative; position: relative;
background: $sidebarBackground; background: $sidebarBackground;
@ -48,25 +47,14 @@
&.accessible:hover { &.accessible:hover {
width: var(--sidebar-max-size); width: var(--sidebar-max-size);
transition-delay: 750ms; 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; z-index: $zIndex-sidebar-hover;
} }
} }
} }
main { > main {
@include custom-scrollbar; display: contents;
$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;
} }
footer { footer {
@ -74,4 +62,4 @@
grid-area: footer; grid-area: footer;
min-width: 0; // restrict size when overflow content (e.g. <Dock> tabs scrolling) 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 React from "react";
import { observable, reaction } from "mobx"; import { observable, reaction } from "mobx";
import { disposeOnUnmount, observer } from "mobx-react"; import { disposeOnUnmount, observer } from "mobx-react";
import { matchPath, RouteProps } from "react-router-dom";
import { createStorage, cssNames } from "../../utils"; import { createStorage, cssNames } from "../../utils";
import { Tab, Tabs } from "../tabs";
import { Sidebar } from "./sidebar"; import { Sidebar } from "./sidebar";
import { ErrorBoundary } from "../error-boundary"; import { ErrorBoundary } from "../error-boundary";
import { Dock } from "../dock"; import { Dock } from "../dock";
import { navigate, navigation } from "../../navigation";
import { getHostedCluster } from "../../../common/cluster-store"; import { getHostedCluster } from "../../../common/cluster-store";
export interface TabRoute extends RouteProps {
title: React.ReactNode;
url: string;
}
interface Props { interface Props {
className?: any; className?: any;
tabs?: TabRoute[];
footer?: React.ReactNode; footer?: React.ReactNode;
headerClass?: string; headerClass?: string;
contentClass?: string;
footerClass?: string; footerClass?: string;
} }
@ -35,18 +25,17 @@ export class MainLayout extends React.Component<Props> {
@disposeOnUnmount syncPinnedStateWithStorage = reaction( @disposeOnUnmount syncPinnedStateWithStorage = reaction(
() => this.isPinned, () => this.isPinned,
isPinned => this.storage.merge({ pinnedSidebar: isPinned }) (isPinned) => this.storage.merge({ pinnedSidebar: isPinned })
); );
toggleSidebar = () => { toggleSidebar = () => {
this.isPinned = !this.isPinned; this.isPinned = !this.isPinned;
this.isAccessible = false; this.isAccessible = false;
setTimeout(() => this.isAccessible = true, 250); setTimeout(() => (this.isAccessible = true), 250);
} };
render() { render() {
const { className, contentClass, headerClass, tabs, footer, footerClass, children } = this.props; const { className, headerClass, footer, footerClass, children } = this.props;
const routePath = navigation.location.pathname;
const cluster = getHostedCluster(); const cluster = getHostedCluster();
if (!cluster) { if (!cluster) {
return null; // fix: skip render when removing active (visible) cluster return null; // fix: skip render when removing active (visible) cluster
@ -54,37 +43,18 @@ export class MainLayout extends React.Component<Props> {
return ( return (
<div className={cssNames("MainLayout", className)}> <div className={cssNames("MainLayout", className)}>
<header className={cssNames("flex gaps align-center", headerClass)}> <header className={cssNames("flex gaps align-center", headerClass)}>
<span className="cluster"> <span className="cluster">{cluster.preferences.clusterName || cluster.contextName}</span>
{cluster.preferences.clusterName || cluster.contextName}
</span>
</header> </header>
<aside className={cssNames("flex column", { pinned: this.isPinned, accessible: this.isAccessible })}> <aside className={cssNames("flex column", { pinned: this.isPinned, accessible: this.isAccessible })}>
<Sidebar <Sidebar className="box grow" isPinned={this.isPinned} toggle={this.toggleSidebar} />
className="box grow"
isPinned={this.isPinned}
toggle={this.toggleSidebar}
/>
</aside> </aside>
{tabs && ( <main>
<Tabs center onChange={url => navigate(url)}> <ErrorBoundary>{children}</ErrorBoundary>
{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> </main>
<footer className={footerClass}> <footer className={footerClass}>{footer === undefined ? <Dock /> : footer}</footer>
{footer === undefined ? <Dock/> : footer}
</footer>
</div> </div>
); );
} }

View File

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