From 026cbbac0997d04fb6c414941d88a42530705bde Mon Sep 17 00:00:00 2001 From: Alex Andreev Date: Mon, 7 Jun 2021 11:46:40 +0300 Subject: [PATCH] Generic TopBar component for Catalog/Cluster views (#2882) * Rendering close button in cluster view Signed-off-by: Alex Andreev * Changing Icon hover style Signed-off-by: Alex Andreev * Moving onClick handler away Signed-off-by: Alex Andreev * Removing sidebar compact view Signed-off-by: Alex Andreev * Removing 'compact' refs in SidebarItem Signed-off-by: Alex Andreev * Wrapping Catalog with MainLayout Signed-off-by: Alex Andreev * Making sidebar resizing indicator visible on hover Signed-off-by: Alex Andreev * Adding TopBar to catalog view Signed-off-by: Alex Andreev * Using TopBar in cluster views Signed-off-by: Alex Andreev * Cleaning up Sidebar styles Signed-off-by: Alex Andreev * Using getActiveClusterEntity() for searching Signed-off-by: Alex Andreev * Fix resizing anchor position Signed-off-by: Alex Andreev * Align cluster name on left Signed-off-by: Alex Andreev * Removing unused files Signed-off-by: Alex Andreev * Removing unused css var Signed-off-by: Alex Andreev * Showing Topbar in ClusterStatus page Signed-off-by: Alex Andreev * Removing TopBar from ClusterManager Signed-off-by: Alex Andreev --- .../components/+catalog/catalog.module.css | 91 +++++++++++++++ src/renderer/components/+catalog/catalog.tsx | 58 ++++++---- src/renderer/components/+welcome/welcome.scss | 1 + src/renderer/components/app.scss | 2 +- src/renderer/components/app.tsx | 4 +- .../cluster-manager/cluster-manager.scss | 3 +- .../cluster-manager/cluster-topbar.tsx | 44 ++++++++ .../cluster-manager/cluster-view.tsx | 5 +- src/renderer/components/icon/icon.scss | 2 +- .../item-object-list/item-list-layout.scss | 2 +- .../__test__/main-layout-header.test.tsx | 105 ------------------ .../components/layout/main-layout.module.css | 50 +++++++++ .../components/layout/main-layout.scss | 76 ------------- .../components/layout/main-layout.tsx | 50 +++------ .../components/layout/sidebar-item.tsx | 12 +- .../components/layout/sidebar-storage.ts | 4 +- src/renderer/components/layout/sidebar.scss | 44 +------- src/renderer/components/layout/sidebar.tsx | 22 +--- .../components/layout/tab-layout.scss | 9 +- .../components/layout/topbar.module.css | 29 ++++- .../{main-layout-header.tsx => topbar.tsx} | 17 ++- .../resizing-anchor/resizing-anchor.scss | 28 +++++ src/renderer/themes/lens-dark.json | 3 +- src/renderer/themes/lens-light.json | 1 + 24 files changed, 325 insertions(+), 337 deletions(-) create mode 100644 src/renderer/components/+catalog/catalog.module.css create mode 100644 src/renderer/components/cluster-manager/cluster-topbar.tsx delete mode 100644 src/renderer/components/layout/__test__/main-layout-header.test.tsx create mode 100644 src/renderer/components/layout/main-layout.module.css delete mode 100755 src/renderer/components/layout/main-layout.scss rename __mocks__/@linguiMacro.ts => src/renderer/components/layout/topbar.module.css (70%) rename src/renderer/components/layout/{main-layout-header.tsx => topbar.tsx} (75%) diff --git a/src/renderer/components/+catalog/catalog.module.css b/src/renderer/components/+catalog/catalog.module.css new file mode 100644 index 0000000000..937195aa8f --- /dev/null +++ b/src/renderer/components/+catalog/catalog.module.css @@ -0,0 +1,91 @@ +/** + * Copyright (c) 2021 OpenLens Authors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +.iconCell { + max-width: 40px; + display: flex; + align-items: center; +} + +.nameCell { +} + +.sourceCell { + max-width: 100px; +} + +.statusCell { + max-width: 100px; +} + +.connected { + color: var(--colorSuccess); +} + +.disconnected { + color: var(--halfGray); +} + +.labelsCell { + overflow-x: scroll; + text-overflow: unset; +} + +.labelsCell::-webkit-scrollbar { + display: none; +} + +.badge { + overflow: unset; + text-overflow: unset; + max-width: unset; +} + +.badge:not(:first-child) { + margin-left: 0.5em; +} + +.catalogIcon { + font-size: 10px; + -webkit-font-smoothing: auto; +} + +.tabs { + @apply flex flex-grow flex-col; +} + +.tab { + @apply px-8 py-4; +} + +.tab:hover { + background-color: var(--sidebarItemHoverBackground); + --color-active: var(--textColorTertiary); +} + +.tab::after { + display: none; +} + +.activeTab, .activeTab:hover { + background-color: var(--blue); + --color-active: white; +} \ No newline at end of file diff --git a/src/renderer/components/+catalog/catalog.tsx b/src/renderer/components/+catalog/catalog.tsx index 90adf27a65..2059658d1f 100644 --- a/src/renderer/components/+catalog/catalog.tsx +++ b/src/renderer/components/+catalog/catalog.tsx @@ -19,7 +19,8 @@ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -import "./catalog.scss"; +import styles from "./catalog.module.css"; + import React from "react"; import { disposeOnUnmount, observer } from "mobx-react"; import { ItemListLayout } from "../item-object-list"; @@ -27,7 +28,6 @@ import { action, makeObservable, observable, reaction, when } from "mobx"; import { CatalogEntityItem, CatalogEntityStore } from "./catalog-entity.store"; import { navigate } from "../../navigation"; import { kebabCase } from "lodash"; -import { PageLayout } from "../layout/page-layout"; import { MenuItem, MenuActions } from "../menu"; import type { CatalogEntityContextMenu, CatalogEntityContextMenuContext } from "../../api/catalog-entity"; import { Badge } from "../badge"; @@ -40,6 +40,12 @@ import type { RouteComponentProps } from "react-router"; import type { ICatalogViewRouteParam } from "./catalog.route"; import { Notifications } from "../notifications"; import { Avatar } from "../avatar/avatar"; +import { MainLayout } from "../layout/main-layout"; +import { cssNames } from "../../utils"; +import { TopBar } from "../layout/topbar"; +import { welcomeURL } from "../+welcome"; +import { Icon } from "../icon"; +import { MaterialTooltip } from "../material-tooltip/material-tooltip"; import { CatalogEntityDetails } from "./catalog-entity-details"; enum sortBy { @@ -139,14 +145,14 @@ export class Catalog extends React.Component { renderNavigation() { return ( - -
Catalog
-
+ +
{ this.categories.map(category => ( @@ -155,6 +161,7 @@ export class Catalog extends React.Component { key={category.getId()} label={category.metadata.name} data-testid={`${category.getId()}-tab`} + className={cssNames(styles.tab, { [styles.activeTab]: this.activeTab == category.getId() })} /> )) } @@ -189,7 +196,7 @@ export class Catalog extends React.Component { colorHash={`${item.name}-${item.source}`} width={24} height={24} - className="catalogIcon" + className={styles.catalogIcon} /> ); } @@ -278,22 +285,29 @@ export class Catalog extends React.Component { } return ( - - { this.catalogEntityStore.activeCategory ? this.renderSingleCategoryList() : this.renderAllCategoriesList() } - { !this.selectedItem && ( - - )} - { this.selectedItem && ( - this.selectedItem = null} - /> - )} - + <> + +
+ + navigate(welcomeURL())}/> + +
+
+ +
+ { this.catalogEntityStore.activeCategory ? this.renderSingleCategoryList() : this.renderAllCategoriesList() } +
+ { !this.selectedItem && ( + + )} + { this.selectedItem && ( + this.selectedItem = null} + /> + )} +
+ ); } } diff --git a/src/renderer/components/+welcome/welcome.scss b/src/renderer/components/+welcome/welcome.scss index f74e135f70..6141f9f517 100644 --- a/src/renderer/components/+welcome/welcome.scss +++ b/src/renderer/components/+welcome/welcome.scss @@ -22,6 +22,7 @@ .Welcome { text-align: center; width: 100%; + height: 100%; z-index: 1; .box { diff --git a/src/renderer/components/app.scss b/src/renderer/components/app.scss index b4c15b89fd..e1d002c8f7 100755 --- a/src/renderer/components/app.scss +++ b/src/renderer/components/app.scss @@ -39,7 +39,7 @@ --font-weight-normal: 400; --font-weight-bold: 500; --main-layout-header: 40px; - --drag-region-height: 22px + --drag-region-height: 22px; } *, *:before, *:after { diff --git a/src/renderer/components/app.tsx b/src/renderer/components/app.tsx index 89a04a58d3..948fec03ce 100755 --- a/src/renderer/components/app.tsx +++ b/src/renderer/components/app.tsx @@ -71,6 +71,8 @@ import { CommandContainer } from "./command-palette/command-container"; import { KubeObjectStore } from "../kube-object.store"; import { clusterContext } from "./context"; import { namespaceStore } from "./+namespaces/namespace.store"; +import { Sidebar } from "./layout/sidebar"; +import { Dock } from "./dock"; @observer export class App extends React.Component { @@ -176,7 +178,7 @@ export class App extends React.Component { return ( - + } footer={}> diff --git a/src/renderer/components/cluster-manager/cluster-manager.scss b/src/renderer/components/cluster-manager/cluster-manager.scss index 0c7c450f1b..16f77051c9 100644 --- a/src/renderer/components/cluster-manager/cluster-manager.scss +++ b/src/renderer/components/cluster-manager/cluster-manager.scss @@ -32,6 +32,7 @@ grid-area: main; position: relative; display: flex; + flex-direction: column; } .HotbarMenu { @@ -45,7 +46,7 @@ #lens-views { position: absolute; left: 0; - top: 0; + top: var(--main-layout-header); // Move below the TopBar right: 0; bottom: 0; display: flex; diff --git a/src/renderer/components/cluster-manager/cluster-topbar.tsx b/src/renderer/components/cluster-manager/cluster-topbar.tsx new file mode 100644 index 0000000000..af49194056 --- /dev/null +++ b/src/renderer/components/cluster-manager/cluster-topbar.tsx @@ -0,0 +1,44 @@ +/** + * Copyright (c) 2021 OpenLens Authors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +import React from "react"; +import { catalogURL } from "../+catalog"; +import type { Cluster } from "../../../main/cluster"; +import { navigate } from "../../navigation"; +import { Icon } from "../icon"; +import { TopBar } from "../layout/topbar"; +import { MaterialTooltip } from "../material-tooltip/material-tooltip"; + +interface Props { + cluster: Cluster +} + +export function ClusterTopbar({ cluster }: Props) { + return ( + +
+ + navigate(catalogURL())}/> + +
+
+ ); +} diff --git a/src/renderer/components/cluster-manager/cluster-view.tsx b/src/renderer/components/cluster-manager/cluster-view.tsx index fc9ba90807..f1530032e3 100644 --- a/src/renderer/components/cluster-manager/cluster-view.tsx +++ b/src/renderer/components/cluster-manager/cluster-view.tsx @@ -32,13 +32,13 @@ import { clusterActivateHandler } from "../../../common/cluster-ipc"; import { catalogEntityRegistry } from "../../api/catalog-entity-registry"; import { navigate } from "../../navigation"; import { catalogURL } from "../+catalog/catalog.route"; +import { ClusterTopbar } from "./cluster-topbar"; import type { RouteComponentProps } from "react-router-dom"; import type { IClusterViewRouteParams } from "./cluster-view.route"; interface Props extends RouteComponentProps { } - @observer export class ClusterView extends React.Component { private store = ClusterStore.getInstance(); @@ -103,7 +103,8 @@ export class ClusterView extends React.Component { render() { return ( -
+
+ {this.cluster && } {this.renderStatus()}
); diff --git a/src/renderer/components/icon/icon.scss b/src/renderer/components/icon/icon.scss index 124e2c1cb9..b10eb3708b 100644 --- a/src/renderer/components/icon/icon.scss +++ b/src/renderer/components/icon/icon.scss @@ -135,7 +135,7 @@ &.interactive { cursor: pointer; transition: 250ms color, 250ms opacity, 150ms background-color, 150ms box-shadow; - border-radius: 50%; + border-radius: var(--border-radius); &.focusable:focus:not(:hover) { box-shadow: 0 0 0 2px var(--focus-color); diff --git a/src/renderer/components/item-object-list/item-list-layout.scss b/src/renderer/components/item-object-list/item-list-layout.scss index a91e2a2a18..2609d0b323 100644 --- a/src/renderer/components/item-object-list/item-list-layout.scss +++ b/src/renderer/components/item-object-list/item-list-layout.scss @@ -28,7 +28,7 @@ padding: var(--flex-gap); .title { - color: $textColorPrimary; + color: var(--textColorTertiary); } .info-panel { diff --git a/src/renderer/components/layout/__test__/main-layout-header.test.tsx b/src/renderer/components/layout/__test__/main-layout-header.test.tsx deleted file mode 100644 index ad50d77fe8..0000000000 --- a/src/renderer/components/layout/__test__/main-layout-header.test.tsx +++ /dev/null @@ -1,105 +0,0 @@ -/** - * Copyright (c) 2021 OpenLens Authors - * - * Permission is hereby granted, free of charge, to any person obtaining a copy of - * this software and associated documentation files (the "Software"), to deal in - * the Software without restriction, including without limitation the rights to - * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of - * the Software, and to permit persons to whom the Software is furnished to do so, - * subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS - * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR - * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER - * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN - * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - */ - -jest.mock("../../../../common/ipc"); - -import React from "react"; -import { render } from "@testing-library/react"; -import "@testing-library/jest-dom/extend-expect"; - -import { MainLayoutHeader } from "../main-layout-header"; -import { Cluster } from "../../../../main/cluster"; -import { ClusterStore } from "../../../../common/cluster-store"; -import mockFs from "mock-fs"; -import { ThemeStore } from "../../../theme.store"; -import { UserStore } from "../../../../common/user-store"; - -jest.mock("electron", () => { - return { - app: { - getVersion: () => "99.99.99", - getPath: () => "tmp", - getLocale: () => "en", - setLoginItemSettings: jest.fn(), - }, - }; -}); - -describe("", () => { - let cluster: Cluster; - - beforeEach(() => { - const mockOpts = { - "minikube-config.yml": JSON.stringify({ - apiVersion: "v1", - clusters: [{ - name: "minikube", - cluster: { - server: "https://192.168.64.3:8443", - }, - }], - contexts: [{ - context: { - cluster: "minikube", - user: "minikube", - }, - name: "minikube", - }], - users: [{ - name: "minikube", - }], - kind: "Config", - preferences: {}, - }) - }; - - mockFs(mockOpts); - - UserStore.createInstance(); - ThemeStore.createInstance(); - ClusterStore.createInstance(); - - cluster = new Cluster({ - id: "foo", - contextName: "minikube", - kubeConfigPath: "minikube-config.yml", - }); - }); - - afterEach(() => { - ClusterStore.resetInstance(); - ThemeStore.resetInstance(); - UserStore.resetInstance(); - mockFs.restore(); - }); - - it("renders w/o errors", () => { - const { container } = render(); - - expect(container).toBeInstanceOf(HTMLElement); - }); - - it("renders cluster name", () => { - const { getByText } = render(); - - expect(getByText("minikube")).toBeInTheDocument(); - }); -}); diff --git a/src/renderer/components/layout/main-layout.module.css b/src/renderer/components/layout/main-layout.module.css new file mode 100644 index 0000000000..abee3c0258 --- /dev/null +++ b/src/renderer/components/layout/main-layout.module.css @@ -0,0 +1,50 @@ +/** + * Copyright (c) 2021 OpenLens Authors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +.mainLayout { + display: grid; + grid-template-areas: + "sidebar contents" + "sidebar footer"; + grid-template-rows: [contents] 1fr [footer] auto; + grid-template-columns: [sidebar] var(--sidebar-width) [contents] 1fr; + width: 100%; + z-index: 1; + height: calc(100% - var(--main-layout-header)); +} + +.sidebar { + grid-area: sidebar; + display: flex; + position: relative; + background: var(--sidebarBackground); +} + +.contents { + grid-area: contents; + overflow: auto; +} + +.footer { + position: relative; + grid-area: footer; + min-width: 0; /* restrict size when overflow content (e.g. tabs scrolling) */ +} \ No newline at end of file diff --git a/src/renderer/components/layout/main-layout.scss b/src/renderer/components/layout/main-layout.scss deleted file mode 100755 index a2deeea392..0000000000 --- a/src/renderer/components/layout/main-layout.scss +++ /dev/null @@ -1,76 +0,0 @@ -/** - * Copyright (c) 2021 OpenLens Authors - * - * Permission is hereby granted, free of charge, to any person obtaining a copy of - * this software and associated documentation files (the "Software"), to deal in - * the Software without restriction, including without limitation the rights to - * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of - * the Software, and to permit persons to whom the Software is furnished to do so, - * subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS - * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR - * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER - * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN - * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - */ - -.MainLayout { - display: grid; - 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%; - - > header { - grid-area: header; - background: $layoutBackground; - padding: $padding $padding * 2; - } - - > aside { - grid-area: aside; - position: relative; - background: $sidebarBackground; - white-space: nowrap; - transition: width 150ms cubic-bezier(0.4, 0, 0.2, 1); - width: var(--sidebar-width); - - &.compact { - position: absolute; - width: var(--main-layout-header); - height: 100%; - overflow: hidden; - - &:hover { - width: var(--sidebar-width); - transition-delay: 750ms; - box-shadow: 3px 3px 16px rgba(0, 0, 0, 0.35); - z-index: $zIndex-sidebar-hover; - } - } - } - - > main { - display: contents; - - > * { - grid-area: main; - overflow: auto; - } - } - - footer { - position: relative; - grid-area: footer; - min-width: 0; // restrict size when overflow content (e.g. tabs scrolling) - } -} diff --git a/src/renderer/components/layout/main-layout.tsx b/src/renderer/components/layout/main-layout.tsx index 455f0dfdfc..a85e90b158 100755 --- a/src/renderer/components/layout/main-layout.tsx +++ b/src/renderer/components/layout/main-layout.tsx @@ -19,73 +19,53 @@ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -import "./main-layout.scss"; +import styles from "./main-layout.module.css"; import React from "react"; import { observer } from "mobx-react"; -import { getHostedCluster } from "../../../common/cluster-store"; import { cssNames } from "../../utils"; -import { Dock } from "../dock"; import { ErrorBoundary } from "../error-boundary"; import { ResizeDirection, ResizeGrowthDirection, ResizeSide, ResizingAnchor } from "../resizing-anchor"; -import { MainLayoutHeader } from "./main-layout-header"; -import { Sidebar } from "./sidebar"; import { sidebarStorage } from "./sidebar-storage"; -export interface MainLayoutProps { - className?: any; +interface Props { + sidebar: React.ReactNode; + className?: string; footer?: React.ReactNode; - headerClass?: string; - footerClass?: string; } @observer -export class MainLayout extends React.Component { - onSidebarCompactModeChange = () => { - sidebarStorage.merge(draft => { - draft.compact = !draft.compact; - }); - }; - +export class MainLayout extends React.Component { onSidebarResize = (width: number) => { sidebarStorage.merge({ width }); }; render() { - const cluster = getHostedCluster(); - const { onSidebarCompactModeChange, onSidebarResize } = this; - const { className, headerClass, footer, footerClass, children } = this.props; - const { compact, width: sidebarWidth } = sidebarStorage.get(); + const { onSidebarResize } = this; + const { className, footer, children, sidebar } = this.props; + const { width: sidebarWidth } = sidebarStorage.get(); const style = { "--sidebar-width": `${sidebarWidth}px` } as React.CSSProperties; - if (!cluster) { - return null; // fix: skip render when removing active (visible) cluster - } - return ( -
- - - +
-
+
{children} -
+
-
{footer ?? }
+
{footer}
); } diff --git a/src/renderer/components/layout/sidebar-item.tsx b/src/renderer/components/layout/sidebar-item.tsx index 61957a61a3..113cef6634 100644 --- a/src/renderer/components/layout/sidebar-item.tsx +++ b/src/renderer/components/layout/sidebar-item.tsx @@ -62,10 +62,6 @@ export class SidebarItem extends React.Component { return this.props.id; } - @computed get compact(): boolean { - return Boolean(sidebarStorage.get().compact); - } - @computed get expanded(): boolean { return Boolean(sidebarStorage.get().expanded[this.id]); } @@ -78,8 +74,6 @@ export class SidebarItem extends React.Component { } @computed get isExpandable(): boolean { - if (this.compact) return false; // not available in compact-mode currently - return Boolean(this.props.children); } @@ -108,10 +102,8 @@ export class SidebarItem extends React.Component { if (isHidden) return null; - const { isActive, id, compact, expanded, isExpandable, toggleExpand } = this; - const classNames = cssNames(SidebarItem.displayName, className, { - compact, - }); + const { isActive, id, expanded, isExpandable, toggleExpand } = this; + const classNames = cssNames(SidebarItem.displayName, className); return (
diff --git a/src/renderer/components/layout/sidebar-storage.ts b/src/renderer/components/layout/sidebar-storage.ts index 976f40b31c..663cb7c177 100644 --- a/src/renderer/components/layout/sidebar-storage.ts +++ b/src/renderer/components/layout/sidebar-storage.ts @@ -23,14 +23,12 @@ import { createStorage } from "../../utils"; export interface SidebarStorageState { width: number; - compact: boolean; expanded: { [itemId: string]: boolean; } } export const sidebarStorage = createStorage("sidebar", { - width: 200, // sidebar size in non-compact mode - compact: false, // compact-mode (icons only) + width: 200, expanded: {}, }); diff --git a/src/renderer/components/layout/sidebar.scss b/src/renderer/components/layout/sidebar.scss index 6212758905..f0d777fadd 100644 --- a/src/renderer/components/layout/sidebar.scss +++ b/src/renderer/components/layout/sidebar.scss @@ -23,49 +23,9 @@ $iconSize: 24px; $itemSpacing: floor($unit / 2.6) floor($unit / 1.6); - &.compact { - .sidebar-nav { - @include hidden-scrollbar; // fix: scrollbar overlaps icons - } - } - - .header { - background: $sidebarLogoBackground; - padding: $padding / 2; - height: var(--main-layout-header); - - a { - font-size: 18.5px; - text-decoration: none; - } - - div.logo-text { - position: absolute; - left: 42px; - top: 11px; - } - - .logo-icon { - width: 28px; - height: 28px; - margin-left: 2px; - margin-top: 2px; - margin-right: 10px; - - svg { - --size: 28px; - padding: 2px; - } - } - - .pin-icon { - margin: auto; - margin-right: $padding / 2; - } - } - .sidebar-nav { - padding: $padding / 1.5 0; + width: var(--sidebar-width); + padding-bottom: calc(var(--padding) * 3); overflow: auto; .Icon { diff --git a/src/renderer/components/layout/sidebar.tsx b/src/renderer/components/layout/sidebar.tsx index 30b14919e9..6ecc9b8df9 100644 --- a/src/renderer/components/layout/sidebar.tsx +++ b/src/renderer/components/layout/sidebar.tsx @@ -24,7 +24,6 @@ import type { TabLayoutRoute } from "./tab-layout"; import React from "react"; import { observer } from "mobx-react"; -import { NavLink } from "react-router-dom"; import { cssNames } from "../../utils"; import { Icon } from "../icon"; import { workloadsRoute, workloadsURL } from "../+workloads/workloads.route"; @@ -52,8 +51,6 @@ import { SidebarItem } from "./sidebar-item"; interface Props { className?: string; - compact?: boolean; // compact-mode view: show only icons and expand on :hover - toggle(): void; // compact-mode updater } @observer @@ -173,24 +170,11 @@ export class Sidebar extends React.Component { } render() { - const { toggle, compact, className } = this.props; + const { className } = this.props; return ( -
-
- - -
Lens
-
- -
-
+
+
.Tabs { - grid-area: tabs; background: $layoutTabsBackground; + min-height: 32px; } - main { $spacing: $margin * 2; - grid-area: main; + flex-grow: 1; overflow-y: scroll; // always reserve space for scrollbar (17px) overflow-x: auto; margin: $spacing; diff --git a/__mocks__/@linguiMacro.ts b/src/renderer/components/layout/topbar.module.css similarity index 70% rename from __mocks__/@linguiMacro.ts rename to src/renderer/components/layout/topbar.module.css index 35fca119d9..08378bb1d4 100644 --- a/__mocks__/@linguiMacro.ts +++ b/src/renderer/components/layout/topbar.module.css @@ -18,7 +18,28 @@ * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -export default { - Trans: ({ children }: { children: React.ReactNode }) => children, - t: (message: string) => message -}; + +.topBar { + display: grid; + grid-template-columns: [title] 1fr [controls] auto; + grid-template-rows: var(--main-layout-header); + grid-template-areas: "title controls"; + background-color: var(--layoutBackground); + z-index: 1; + width: 100%; +} + +.title { + @apply font-bold px-6; + color: var(--textColorAccent); + align-items: center; + display: flex; +} + +.controls { + align-self: flex-end; + padding-right: 1.5rem; + align-items: center; + display: flex; + height: 100%; +} \ No newline at end of file diff --git a/src/renderer/components/layout/main-layout-header.tsx b/src/renderer/components/layout/topbar.tsx similarity index 75% rename from src/renderer/components/layout/main-layout-header.tsx rename to src/renderer/components/layout/topbar.tsx index 354794e363..2fe302ce55 100644 --- a/src/renderer/components/layout/main-layout-header.tsx +++ b/src/renderer/components/layout/topbar.tsx @@ -19,20 +19,19 @@ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +import styles from "./topbar.module.css"; import React from "react"; import { observer } from "mobx-react"; -import type { Cluster } from "../../../main/cluster"; -import { cssNames } from "../../utils"; -interface Props { - cluster: Cluster - className?: string +interface Props extends React.HTMLAttributes { + label: React.ReactNode; } -export const MainLayoutHeader = observer(({ cluster, className }: Props) => { +export const TopBar = observer(({ label, children, ...rest }: Props) => { return ( -
- {cluster.name} -
+
+
{label}
+
{children}
+
); }); diff --git a/src/renderer/components/resizing-anchor/resizing-anchor.scss b/src/renderer/components/resizing-anchor/resizing-anchor.scss index 0896a9ee62..024a83105b 100644 --- a/src/renderer/components/resizing-anchor/resizing-anchor.scss +++ b/src/renderer/components/resizing-anchor/resizing-anchor.scss @@ -31,6 +31,23 @@ body.resizing { position: absolute; z-index: 10; + &::after { + content: " "; + display: block; + width: 3px; + height: 100%; + margin-left: 50%; + background: transparent; + transition: all 0.2s 0s; + } + + &:hover { + &::after { + background: var(--blue); + transition: all 0.2s 0.5s; + } + } + &.disabled { display: none; } @@ -56,6 +73,17 @@ body.resizing { cursor: col-resize; width: $dimension; + // Expand hoverable area while resizing to keep highlighting resizer. + // Otherwise, cursor can move far away dropping hover indicator. + .resizing & { + $expandedWidth: 200px; + width: $expandedWidth; + + &.trailing { + right: -$expandedWidth / 2; + } + } + &.leading { left: -$dimension / 2; } diff --git a/src/renderer/themes/lens-dark.json b/src/renderer/themes/lens-dark.json index f96eb81453..822a5f1c36 100644 --- a/src/renderer/themes/lens-dark.json +++ b/src/renderer/themes/lens-dark.json @@ -26,6 +26,7 @@ "sidebarLogoBackground": "#414448", "sidebarActiveColor": "#ffffff", "sidebarSubmenuActiveColor": "#ffffff", + "sidebarItemHoverBackground": "#3a3e44", "buttonPrimaryBackground": "#3d90ce", "buttonDefaultBackground": "#414448", "buttonLightBackground": "#f1f1f1", @@ -109,7 +110,7 @@ "addClusterIconColor": "#252729", "boxShadow": "#0000003a", "iconActiveColor": "#ffffff", - "iconActiveBackground": "#ffffff22", + "iconActiveBackground": "#ffffff18", "filterAreaBackground": "#23272b", "chartLiveBarBackgound": "#00000033", "chartStripesColor": "#ffffff08", diff --git a/src/renderer/themes/lens-light.json b/src/renderer/themes/lens-light.json index d3b5938de3..8d9339d71a 100644 --- a/src/renderer/themes/lens-light.json +++ b/src/renderer/themes/lens-light.json @@ -27,6 +27,7 @@ "sidebarActiveColor": "#ffffff", "sidebarSubmenuActiveColor": "#3d90ce", "sidebarBackground": "#e8e8e8", + "sidebarItemHoverBackground": "#f0f2f5", "buttonPrimaryBackground": "#3d90ce", "buttonDefaultBackground": "#414448", "buttonLightBackground": "#f1f1f1",