diff --git a/src/common/utils/makeCss.ts b/src/common/utils/makeCss.ts new file mode 100644 index 0000000000..0187f0196b --- /dev/null +++ b/src/common/utils/makeCss.ts @@ -0,0 +1,30 @@ +/** + * 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. + */ + +/** + * Function expands generic CSS Modules literal types and adds dictionary with arbitrary + * indexes. + * @param styles Styles imported from CSS Module having only literal types + * @returns Passed style list with expanded typescript types + */ +export function makeCss(styles: T) { + return styles as typeof styles & { [key: string]: string }; +} diff --git a/src/extensions/extension-loader.ts b/src/extensions/extension-loader.ts index a01fed787c..4073ad1e79 100644 --- a/src/extensions/extension-loader.ts +++ b/src/extensions/extension-loader.ts @@ -253,6 +253,7 @@ export class ExtensionLoader extends Singleton { registries.CommandRegistry.getInstance().add(extension.commands), registries.WelcomeMenuRegistry.getInstance().add(extension.welcomeMenus), registries.CatalogEntityDetailRegistry.getInstance().add(extension.catalogEntityDetailItems), + registries.TopBarRegistry.getInstance().add(extension.topBarItems), ]; this.events.on("remove", (removedExtension: LensRendererExtension) => { diff --git a/src/extensions/lens-renderer-extension.ts b/src/extensions/lens-renderer-extension.ts index fb26d383b0..7119b0f7ac 100644 --- a/src/extensions/lens-renderer-extension.ts +++ b/src/extensions/lens-renderer-extension.ts @@ -28,6 +28,7 @@ import { LensExtension } from "./lens-extension"; import { getExtensionPageUrl } from "./registries/page-registry"; import type { CommandRegistration } from "./registries/command-registry"; import type { EntitySettingRegistration } from "./registries/entity-setting-registry"; +import type { TopBarRegistration } from "./registries/topbar-registry"; export class LensRendererExtension extends LensExtension { globalPages: PageRegistration[] = []; @@ -44,6 +45,7 @@ export class LensRendererExtension extends LensExtension { commands: CommandRegistration[] = []; welcomeMenus: WelcomeMenuRegistration[] = []; catalogEntityDetailItems: CatalogEntityDetailRegistration[] = []; + topBarItems: TopBarRegistration[] = []; async navigate

(pageId?: string, params?: P) { const { navigate } = await import("../renderer/navigation"); diff --git a/src/extensions/registries/index.ts b/src/extensions/registries/index.ts index 1f65d8f280..febc0ba7ca 100644 --- a/src/extensions/registries/index.ts +++ b/src/extensions/registries/index.ts @@ -34,4 +34,5 @@ export * from "./entity-setting-registry"; export * from "./welcome-menu-registry"; export * from "./catalog-entity-detail-registry"; export * from "./workloads-overview-detail-registry"; +export * from "./topbar-registry"; export * from "./protocol-handler"; diff --git a/src/extensions/registries/topbar-registry.ts b/src/extensions/registries/topbar-registry.ts new file mode 100644 index 0000000000..a75aeab8c6 --- /dev/null +++ b/src/extensions/registries/topbar-registry.ts @@ -0,0 +1,34 @@ +/** + * 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 type React from "react"; +import { BaseRegistry } from "./base-registry"; + +interface TopBarComponents { + Item?: React.ComponentType; +} + +export interface TopBarRegistration { + components: TopBarComponents; +} + +export class TopBarRegistry extends BaseRegistry { +} diff --git a/src/renderer/components/+catalog/catalog.tsx b/src/renderer/components/+catalog/catalog.tsx index 929e19c27a..9d9ac6fd2e 100644 --- a/src/renderer/components/+catalog/catalog.tsx +++ b/src/renderer/components/+catalog/catalog.tsx @@ -27,7 +27,6 @@ import { ItemListLayout } from "../item-object-list"; import { action, makeObservable, observable, reaction, when } from "mobx"; import { CatalogEntityItem, CatalogEntityStore } from "./catalog-entity.store"; import { navigate } from "../../navigation"; -import { kebabCase } from "lodash"; import { MenuItem, MenuActions } from "../menu"; import type { CatalogEntityContextMenu, CatalogEntityContextMenuContext } from "../../api/catalog-entity"; import { Badge } from "../badge"; @@ -41,11 +40,9 @@ 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 { Icon } from "../icon"; -import { MaterialTooltip } from "../material-tooltip/material-tooltip"; +import { makeCss } from "../../../common/utils/makeCss"; import { CatalogEntityDetails } from "./catalog-entity-details"; -import { CatalogViewRouteParam, welcomeURL } from "../../../common/routes"; +import type { CatalogViewRouteParam } from "../../../common/routes"; enum sortBy { name = "name", @@ -54,6 +51,8 @@ enum sortBy { status = "status" } +const css = makeCss(styles); + interface Props extends RouteComponentProps {} @observer export class Catalog extends React.Component { @@ -143,14 +142,14 @@ export class Catalog extends React.Component { renderNavigation() { return ( - +

{ this.categories.map(category => ( @@ -159,7 +158,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() })} + className={cssNames(css.tab, { [css.activeTab]: this.activeTab == category.getId() })} /> )) } @@ -198,7 +197,7 @@ export class Catalog extends React.Component { colorHash={`${item.name}-${item.source}`} width={24} height={24} - className={styles.catalogIcon} + className={css.catalogIcon} /> ); } @@ -220,18 +219,18 @@ export class Catalog extends React.Component { (entity: CatalogEntityItem) => entity.searchFields, ]} renderTableHeader={[ - { title: "", className: styles.iconCell }, - { title: "Name", className: styles.nameCell, sortBy: sortBy.name }, - { title: "Source", className: styles.sourceCell, sortBy: sortBy.source }, - { title: "Labels", className: styles.labelsCell }, - { title: "Status", className: styles.statusCell, sortBy: sortBy.status }, + { title: "", className: css.iconCell }, + { title: "Name", className: css.nameCell, sortBy: sortBy.name }, + { title: "Source", className: css.sourceCell, sortBy: sortBy.source }, + { title: "Labels", className: css.labelsCell }, + { title: "Status", className: css.statusCell, sortBy: sortBy.status }, ]} renderTableContents={(item: CatalogEntityItem) => [ this.renderIcon(item), item.name, item.source, item.labels.map((label) => ), - { title: item.phase, className: kebabCase(item.phase) } + { title: item.phase, className: cssNames(css[item.phase]) } ]} onDetails={(item: CatalogEntityItem) => this.onDetails(item) } renderItemMenu={this.renderItemMenu} @@ -257,12 +256,12 @@ export class Catalog extends React.Component { (entity: CatalogEntityItem) => entity.searchFields, ]} renderTableHeader={[ - { title: "", className: styles.iconCell }, - { title: "Name", className: styles.nameCell, sortBy: sortBy.name }, - { title: "Kind", className: styles.kindCell, sortBy: sortBy.kind }, - { title: "Source", className: styles.sourceCell, sortBy: sortBy.source }, - { title: "Labels", className: styles.labelsCell }, - { title: "Status", className: styles.statusCell, sortBy: sortBy.status }, + { title: "", className: css.iconCell }, + { title: "Name", className: css.nameCell, sortBy: sortBy.name }, + { title: "Kind", className: css.kindCell, sortBy: sortBy.kind }, + { title: "Source", className: css.sourceCell, sortBy: sortBy.source }, + { title: "Labels", className: css.labelsCell }, + { title: "Status", className: css.statusCell, sortBy: sortBy.status }, ]} renderTableContents={(item: CatalogEntityItem) => [ this.renderIcon(item), @@ -270,7 +269,7 @@ export class Catalog extends React.Component { item.kind, item.source, item.labels.map((label) => ), - { title: item.phase, className: kebabCase(item.phase) } + { title: item.phase, className: cssNames(css[item.phase]) } ]} detailsItem={this.selectedItem} onDetails={(item: CatalogEntityItem) => this.onDetails(item) } @@ -285,29 +284,20 @@ export class Catalog extends React.Component { } return ( - <> - -
- - navigate(welcomeURL())}/> - -
-
- -
- { this.catalogEntityStore.activeCategory ? this.renderSingleCategoryList() : this.renderAllCategoriesList() } -
- { !this.selectedItem && ( - - )} - { this.selectedItem && ( - this.selectedItem = null} - /> - )} -
- + +
+ { this.catalogEntityStore.activeCategory ? this.renderSingleCategoryList() : this.renderAllCategoriesList() } +
+ { !this.selectedItem && ( + + )} + { this.selectedItem && ( + this.selectedItem = null} + /> + )} +
); } } diff --git a/src/renderer/components/cluster-manager/catalog-topbar.tsx b/src/renderer/components/cluster-manager/catalog-topbar.tsx new file mode 100644 index 0000000000..3027765fc9 --- /dev/null +++ b/src/renderer/components/cluster-manager/catalog-topbar.tsx @@ -0,0 +1,39 @@ +/** + * 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 { welcomeURL } from "../../../common/routes"; +import { navigate } from "../../navigation"; +import { Icon } from "../icon"; +import { TopBar } from "../layout/topbar"; +import { MaterialTooltip } from "../material-tooltip/material-tooltip"; + +export function CatalogTopbar() { + return ( + +
+ + navigate(welcomeURL())}/> + +
+
+ ); +} diff --git a/src/renderer/components/cluster-manager/cluster-manager.scss b/src/renderer/components/cluster-manager/cluster-manager.scss index 16f77051c9..da31c2a07b 100644 --- a/src/renderer/components/cluster-manager/cluster-manager.scss +++ b/src/renderer/components/cluster-manager/cluster-manager.scss @@ -23,10 +23,12 @@ --bottom-bar-height: 22px; display: grid; - grid-template-areas: "menu main" "menu main" "bottom-bar bottom-bar"; + grid-template-areas: + "menu topbar" + "menu main" + "bottom-bar bottom-bar"; grid-template-rows: auto 1fr min-content; grid-template-columns: min-content 1fr; - height: 100%; main { grid-area: main; @@ -46,7 +48,7 @@ #lens-views { position: absolute; left: 0; - top: var(--main-layout-header); // Move below the TopBar + top: 0; right: 0; bottom: 0; display: flex; diff --git a/src/renderer/components/cluster-manager/cluster-manager.tsx b/src/renderer/components/cluster-manager/cluster-manager.tsx index 4bd14e7805..df5ae5d486 100644 --- a/src/renderer/components/cluster-manager/cluster-manager.tsx +++ b/src/renderer/components/cluster-manager/cluster-manager.tsx @@ -34,6 +34,8 @@ import { Extensions } from "../+extensions"; import { HotbarMenu } from "../hotbar/hotbar-menu"; import { EntitySettings } from "../+entity-settings"; import { Welcome } from "../+welcome"; +import { ClusterTopbar } from "./cluster-topbar"; +import { CatalogTopbar } from "./catalog-topbar"; import * as routes from "../../../common/routes"; @observer @@ -41,6 +43,8 @@ export class ClusterManager extends React.Component { render() { return (
+ +
diff --git a/src/renderer/components/cluster-manager/cluster-topbar.tsx b/src/renderer/components/cluster-manager/cluster-topbar.tsx index df2e3e8945..7dcc3c7d5a 100644 --- a/src/renderer/components/cluster-manager/cluster-topbar.tsx +++ b/src/renderer/components/cluster-manager/cluster-topbar.tsx @@ -19,21 +19,28 @@ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +import { observer } from "mobx-react"; import React from "react"; +import type { RouteComponentProps } from "react-router"; import { catalogURL } from "../../../common/routes"; -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"; +import type { Cluster } from "../../../main/cluster"; +import { ClusterStore } from "../../../common/cluster-store"; +import type { ClusterViewRouteParams } from "../../../common/routes"; -interface Props { - cluster: Cluster +interface Props extends RouteComponentProps { } -export function ClusterTopbar({ cluster }: Props) { +export const ClusterTopbar = observer((props: Props) => { + const getCluster = (): Cluster | undefined => { + return ClusterStore.getInstance().getById(props.match.params.clusterId); + }; + return ( - +
navigate(catalogURL())}/> @@ -41,4 +48,4 @@ export function ClusterTopbar({ cluster }: Props) {
); -} +}); diff --git a/src/renderer/components/cluster-manager/cluster-view.tsx b/src/renderer/components/cluster-manager/cluster-view.tsx index 6c40e22ee9..78626cccb1 100644 --- a/src/renderer/components/cluster-manager/cluster-view.tsx +++ b/src/renderer/components/cluster-manager/cluster-view.tsx @@ -32,7 +32,6 @@ import { requestMain } from "../../../common/ipc"; import { clusterActivateHandler } from "../../../common/cluster-ipc"; import { catalogEntityRegistry } from "../../api/catalog-entity-registry"; import { navigate } from "../../navigation"; -import { ClusterTopbar } from "./cluster-topbar"; import { catalogURL, ClusterViewRouteParams } from "../../../common/routes"; interface Props extends RouteComponentProps { @@ -104,7 +103,6 @@ export class ClusterView extends React.Component { render() { return (
- {this.cluster && } {this.renderStatus()}
); diff --git a/src/renderer/components/cluster-manager/topbar.test.tsx b/src/renderer/components/cluster-manager/topbar.test.tsx new file mode 100644 index 0000000000..3471b977db --- /dev/null +++ b/src/renderer/components/cluster-manager/topbar.test.tsx @@ -0,0 +1,65 @@ +/** + * 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 { render } from "@testing-library/react"; +import "@testing-library/jest-dom/extend-expect"; +import { TopBar } from "../layout/topbar"; +import { TopBarRegistry } from "../../../extensions/registries"; + +describe("", () => { + beforeEach(() => { + TopBarRegistry.createInstance(); + }); + + afterEach(() => { + TopBarRegistry.resetInstance(); + }); + + it("renders w/o errors", () => { + const { container } = render(); + + expect(container).toBeInstanceOf(HTMLElement); + }); + + it("renders title", async () => { + const { getByTestId } = render(); + + expect(await getByTestId("topbarLabel")).toHaveTextContent("topbar"); + }); + + it("renders items", async () => { + const testId = "testId"; + const text = "an item"; + + TopBarRegistry.getInstance().getItems = jest.fn().mockImplementationOnce(() => [ + { + components: { + Item: {text} + } + } + ]); + + const { getByTestId } = render(); + + expect(await getByTestId(testId)).toHaveTextContent(text); + }); +}); diff --git a/src/renderer/components/layout/main-layout.module.css b/src/renderer/components/layout/main-layout.module.css index abee3c0258..f79a6d4fc7 100644 --- a/src/renderer/components/layout/main-layout.module.css +++ b/src/renderer/components/layout/main-layout.module.css @@ -28,7 +28,7 @@ grid-template-columns: [sidebar] var(--sidebar-width) [contents] 1fr; width: 100%; z-index: 1; - height: calc(100% - var(--main-layout-header)); + height: 100%; } .sidebar { diff --git a/src/renderer/components/layout/topbar.module.css b/src/renderer/components/layout/topbar.module.css index 08378bb1d4..da4bdcda99 100644 --- a/src/renderer/components/layout/topbar.module.css +++ b/src/renderer/components/layout/topbar.module.css @@ -27,6 +27,7 @@ background-color: var(--layoutBackground); z-index: 1; width: 100%; + grid-area: topbar; } .title { @@ -42,4 +43,4 @@ align-items: center; display: flex; height: 100%; -} \ No newline at end of file +} diff --git a/src/renderer/components/layout/topbar.tsx b/src/renderer/components/layout/topbar.tsx index 2fe302ce55..74bc73de36 100644 --- a/src/renderer/components/layout/topbar.tsx +++ b/src/renderer/components/layout/topbar.tsx @@ -22,16 +22,44 @@ import styles from "./topbar.module.css"; import React from "react"; import { observer } from "mobx-react"; +import { TopBarRegistry } from "../../../extensions/registries"; interface Props extends React.HTMLAttributes { label: React.ReactNode; } export const TopBar = observer(({ label, children, ...rest }: Props) => { + const renderRegisteredItems = () => { + const items = TopBarRegistry.getInstance().getItems(); + + if (!Array.isArray(items)) { + return null; + } + + return ( +
+ {items.map((registration, index) => { + if (!registration?.components?.Item) { + return null; + } + + return ( +
+ {registration.components.Item} +
+ ); + })} +
+ ); + }; + return (
-
{label}
-
{children}
+
{label}
+
+ {renderRegisteredItems()} + {children} +
); }); diff --git a/src/renderer/initializers/registries.ts b/src/renderer/initializers/registries.ts index 80513a2461..08594d41c5 100644 --- a/src/renderer/initializers/registries.ts +++ b/src/renderer/initializers/registries.ts @@ -36,4 +36,5 @@ export function initRegistries() { registries.StatusBarRegistry.createInstance(); registries.WelcomeMenuRegistry.createInstance(); registries.WorkloadsOverviewDetailRegistry.createInstance(); + registries.TopBarRegistry.createInstance(); }