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

Adding Topbar extension registries (#2997)

* Move topbars to cluster manager

Signed-off-by: Alex Andreev <alex.andreev.email@gmail.com>

* Adding topBar extension registries

Signed-off-by: Alex Andreev <alex.andreev.email@gmail.com>

* topbar test clean up

Signed-off-by: Alex Andreev <alex.andreev.email@gmail.com>

* Removing unused class

Signed-off-by: Alex Andreev <alex.andreev.email@gmail.com>

* Expanding CSS Module typings

Signed-off-by: Alex Andreev <alex.andreev.email@gmail.com>

* Fixing welcomeURL path

Signed-off-by: Alex Andreev <alex.andreev.email@gmail.com>

* Remove unused import

Signed-off-by: Alex Andreev <alex.andreev.email@gmail.com>

* Fix TopBarRegistry references

Signed-off-by: Alex Andreev <alex.andreev.email@gmail.com>

* Fixing topbar test

Signed-off-by: Alex Andreev <alex.andreev.email@gmail.com>

* Init missing registry

Signed-off-by: Alex Andreev <alex.andreev.email@gmail.com>
This commit is contained in:
Alex Andreev 2021-06-14 07:40:14 +03:00 committed by GitHub
parent 6f6ad9cc12
commit d14a3e4a6d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 263 additions and 60 deletions

View File

@ -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<T>(styles: T) {
return styles as typeof styles & { [key: string]: string };
}

View File

@ -253,6 +253,7 @@ export class ExtensionLoader extends Singleton {
registries.CommandRegistry.getInstance().add(extension.commands), registries.CommandRegistry.getInstance().add(extension.commands),
registries.WelcomeMenuRegistry.getInstance().add(extension.welcomeMenus), registries.WelcomeMenuRegistry.getInstance().add(extension.welcomeMenus),
registries.CatalogEntityDetailRegistry.getInstance().add(extension.catalogEntityDetailItems), registries.CatalogEntityDetailRegistry.getInstance().add(extension.catalogEntityDetailItems),
registries.TopBarRegistry.getInstance().add(extension.topBarItems),
]; ];
this.events.on("remove", (removedExtension: LensRendererExtension) => { this.events.on("remove", (removedExtension: LensRendererExtension) => {

View File

@ -28,6 +28,7 @@ import { LensExtension } from "./lens-extension";
import { getExtensionPageUrl } from "./registries/page-registry"; import { getExtensionPageUrl } from "./registries/page-registry";
import type { CommandRegistration } from "./registries/command-registry"; import type { CommandRegistration } from "./registries/command-registry";
import type { EntitySettingRegistration } from "./registries/entity-setting-registry"; import type { EntitySettingRegistration } from "./registries/entity-setting-registry";
import type { TopBarRegistration } from "./registries/topbar-registry";
export class LensRendererExtension extends LensExtension { export class LensRendererExtension extends LensExtension {
globalPages: PageRegistration[] = []; globalPages: PageRegistration[] = [];
@ -44,6 +45,7 @@ export class LensRendererExtension extends LensExtension {
commands: CommandRegistration[] = []; commands: CommandRegistration[] = [];
welcomeMenus: WelcomeMenuRegistration[] = []; welcomeMenus: WelcomeMenuRegistration[] = [];
catalogEntityDetailItems: CatalogEntityDetailRegistration[] = []; catalogEntityDetailItems: CatalogEntityDetailRegistration[] = [];
topBarItems: TopBarRegistration[] = [];
async navigate<P extends object>(pageId?: string, params?: P) { async navigate<P extends object>(pageId?: string, params?: P) {
const { navigate } = await import("../renderer/navigation"); const { navigate } = await import("../renderer/navigation");

View File

@ -34,4 +34,5 @@ export * from "./entity-setting-registry";
export * from "./welcome-menu-registry"; export * from "./welcome-menu-registry";
export * from "./catalog-entity-detail-registry"; export * from "./catalog-entity-detail-registry";
export * from "./workloads-overview-detail-registry"; export * from "./workloads-overview-detail-registry";
export * from "./topbar-registry";
export * from "./protocol-handler"; export * from "./protocol-handler";

View File

@ -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<TopBarRegistration> {
}

View File

@ -27,7 +27,6 @@ import { ItemListLayout } from "../item-object-list";
import { action, makeObservable, observable, reaction, when } from "mobx"; import { action, makeObservable, observable, reaction, when } from "mobx";
import { CatalogEntityItem, CatalogEntityStore } from "./catalog-entity.store"; import { CatalogEntityItem, CatalogEntityStore } from "./catalog-entity.store";
import { navigate } from "../../navigation"; import { navigate } from "../../navigation";
import { kebabCase } from "lodash";
import { MenuItem, MenuActions } from "../menu"; import { MenuItem, MenuActions } from "../menu";
import type { CatalogEntityContextMenu, CatalogEntityContextMenuContext } from "../../api/catalog-entity"; import type { CatalogEntityContextMenu, CatalogEntityContextMenuContext } from "../../api/catalog-entity";
import { Badge } from "../badge"; import { Badge } from "../badge";
@ -41,11 +40,9 @@ import { Notifications } from "../notifications";
import { Avatar } from "../avatar/avatar"; import { Avatar } from "../avatar/avatar";
import { MainLayout } from "../layout/main-layout"; import { MainLayout } from "../layout/main-layout";
import { cssNames } from "../../utils"; import { cssNames } from "../../utils";
import { TopBar } from "../layout/topbar"; import { makeCss } from "../../../common/utils/makeCss";
import { Icon } from "../icon";
import { MaterialTooltip } from "../material-tooltip/material-tooltip";
import { CatalogEntityDetails } from "./catalog-entity-details"; import { CatalogEntityDetails } from "./catalog-entity-details";
import { CatalogViewRouteParam, welcomeURL } from "../../../common/routes"; import type { CatalogViewRouteParam } from "../../../common/routes";
enum sortBy { enum sortBy {
name = "name", name = "name",
@ -54,6 +51,8 @@ enum sortBy {
status = "status" status = "status"
} }
const css = makeCss(styles);
interface Props extends RouteComponentProps<CatalogViewRouteParam> {} interface Props extends RouteComponentProps<CatalogViewRouteParam> {}
@observer @observer
export class Catalog extends React.Component<Props> { export class Catalog extends React.Component<Props> {
@ -143,14 +142,14 @@ export class Catalog extends React.Component<Props> {
renderNavigation() { renderNavigation() {
return ( return (
<Tabs className={cssNames(styles.tabs, "flex column")} scrollable={false} onChange={this.onTabChange} value={this.activeTab}> <Tabs className={cssNames(css.tabs, "flex column")} scrollable={false} onChange={this.onTabChange} value={this.activeTab}>
<div> <div>
<Tab <Tab
value={undefined} value={undefined}
key="*" key="*"
label="Browse" label="Browse"
data-testid="*-tab" data-testid="*-tab"
className={cssNames(styles.tab, { [styles.activeTab]: this.activeTab == null })} className={cssNames(css.tab, { [css.activeTab]: this.activeTab == null })}
/> />
{ {
this.categories.map(category => ( this.categories.map(category => (
@ -159,7 +158,7 @@ export class Catalog extends React.Component<Props> {
key={category.getId()} key={category.getId()}
label={category.metadata.name} label={category.metadata.name}
data-testid={`${category.getId()}-tab`} 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<Props> {
colorHash={`${item.name}-${item.source}`} colorHash={`${item.name}-${item.source}`}
width={24} width={24}
height={24} height={24}
className={styles.catalogIcon} className={css.catalogIcon}
/> />
); );
} }
@ -220,18 +219,18 @@ export class Catalog extends React.Component<Props> {
(entity: CatalogEntityItem) => entity.searchFields, (entity: CatalogEntityItem) => entity.searchFields,
]} ]}
renderTableHeader={[ renderTableHeader={[
{ title: "", className: styles.iconCell }, { title: "", className: css.iconCell },
{ title: "Name", className: styles.nameCell, sortBy: sortBy.name }, { title: "Name", className: css.nameCell, sortBy: sortBy.name },
{ title: "Source", className: styles.sourceCell, sortBy: sortBy.source }, { title: "Source", className: css.sourceCell, sortBy: sortBy.source },
{ title: "Labels", className: styles.labelsCell }, { title: "Labels", className: css.labelsCell },
{ title: "Status", className: styles.statusCell, sortBy: sortBy.status }, { title: "Status", className: css.statusCell, sortBy: sortBy.status },
]} ]}
renderTableContents={(item: CatalogEntityItem) => [ renderTableContents={(item: CatalogEntityItem) => [
this.renderIcon(item), this.renderIcon(item),
item.name, item.name,
item.source, item.source,
item.labels.map((label) => <Badge key={label} label={label} title={label} />), item.labels.map((label) => <Badge key={label} label={label} title={label} />),
{ title: item.phase, className: kebabCase(item.phase) } { title: item.phase, className: cssNames(css[item.phase]) }
]} ]}
onDetails={(item: CatalogEntityItem) => this.onDetails(item) } onDetails={(item: CatalogEntityItem) => this.onDetails(item) }
renderItemMenu={this.renderItemMenu} renderItemMenu={this.renderItemMenu}
@ -257,12 +256,12 @@ export class Catalog extends React.Component<Props> {
(entity: CatalogEntityItem) => entity.searchFields, (entity: CatalogEntityItem) => entity.searchFields,
]} ]}
renderTableHeader={[ renderTableHeader={[
{ title: "", className: styles.iconCell }, { title: "", className: css.iconCell },
{ title: "Name", className: styles.nameCell, sortBy: sortBy.name }, { title: "Name", className: css.nameCell, sortBy: sortBy.name },
{ title: "Kind", className: styles.kindCell, sortBy: sortBy.kind }, { title: "Kind", className: css.kindCell, sortBy: sortBy.kind },
{ title: "Source", className: styles.sourceCell, sortBy: sortBy.source }, { title: "Source", className: css.sourceCell, sortBy: sortBy.source },
{ title: "Labels", className: styles.labelsCell }, { title: "Labels", className: css.labelsCell },
{ title: "Status", className: styles.statusCell, sortBy: sortBy.status }, { title: "Status", className: css.statusCell, sortBy: sortBy.status },
]} ]}
renderTableContents={(item: CatalogEntityItem) => [ renderTableContents={(item: CatalogEntityItem) => [
this.renderIcon(item), this.renderIcon(item),
@ -270,7 +269,7 @@ export class Catalog extends React.Component<Props> {
item.kind, item.kind,
item.source, item.source,
item.labels.map((label) => <Badge key={label} label={label} title={label} />), item.labels.map((label) => <Badge key={label} label={label} title={label} />),
{ title: item.phase, className: kebabCase(item.phase) } { title: item.phase, className: cssNames(css[item.phase]) }
]} ]}
detailsItem={this.selectedItem} detailsItem={this.selectedItem}
onDetails={(item: CatalogEntityItem) => this.onDetails(item) } onDetails={(item: CatalogEntityItem) => this.onDetails(item) }
@ -285,29 +284,20 @@ export class Catalog extends React.Component<Props> {
} }
return ( return (
<> <MainLayout sidebar={this.renderNavigation()}>
<TopBar label="Catalog"> <div className="p-6 h-full">
<div> { this.catalogEntityStore.activeCategory ? this.renderSingleCategoryList() : this.renderAllCategoriesList() }
<MaterialTooltip title="Close Catalog" placement="left"> </div>
<Icon style={{ cursor: "default" }} material="close" onClick={() => navigate(welcomeURL())}/> { !this.selectedItem && (
</MaterialTooltip> <CatalogAddButton category={this.catalogEntityStore.activeCategory} />
</div> )}
</TopBar> { this.selectedItem && (
<MainLayout sidebar={this.renderNavigation()}> <CatalogEntityDetails
<div className="p-6 h-full"> entity={this.selectedItem.entity}
{ this.catalogEntityStore.activeCategory ? this.renderSingleCategoryList() : this.renderAllCategoriesList() } hideDetails={() => this.selectedItem = null}
</div> />
{ !this.selectedItem && ( )}
<CatalogAddButton category={this.catalogEntityStore.activeCategory} /> </MainLayout>
)}
{ this.selectedItem && (
<CatalogEntityDetails
entity={this.selectedItem.entity}
hideDetails={() => this.selectedItem = null}
/>
)}
</MainLayout>
</>
); );
} }
} }

View File

@ -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 (
<TopBar label="Catalog">
<div>
<MaterialTooltip title="Close Catalog" placement="left">
<Icon style={{ cursor: "default" }} material="close" onClick={() => navigate(welcomeURL())}/>
</MaterialTooltip>
</div>
</TopBar>
);
}

View File

@ -23,10 +23,12 @@
--bottom-bar-height: 22px; --bottom-bar-height: 22px;
display: grid; 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-rows: auto 1fr min-content;
grid-template-columns: min-content 1fr; grid-template-columns: min-content 1fr;
height: 100%;
main { main {
grid-area: main; grid-area: main;
@ -46,7 +48,7 @@
#lens-views { #lens-views {
position: absolute; position: absolute;
left: 0; left: 0;
top: var(--main-layout-header); // Move below the TopBar top: 0;
right: 0; right: 0;
bottom: 0; bottom: 0;
display: flex; display: flex;

View File

@ -34,6 +34,8 @@ import { Extensions } from "../+extensions";
import { HotbarMenu } from "../hotbar/hotbar-menu"; import { HotbarMenu } from "../hotbar/hotbar-menu";
import { EntitySettings } from "../+entity-settings"; import { EntitySettings } from "../+entity-settings";
import { Welcome } from "../+welcome"; import { Welcome } from "../+welcome";
import { ClusterTopbar } from "./cluster-topbar";
import { CatalogTopbar } from "./catalog-topbar";
import * as routes from "../../../common/routes"; import * as routes from "../../../common/routes";
@observer @observer
@ -41,6 +43,8 @@ export class ClusterManager extends React.Component {
render() { render() {
return ( return (
<div className="ClusterManager"> <div className="ClusterManager">
<Route component={CatalogTopbar} {...routes.catalogRoute} />
<Route component={ClusterTopbar} {...routes.clusterViewRoute} />
<main> <main>
<div id="lens-views"/> <div id="lens-views"/>
<Switch> <Switch>

View File

@ -19,21 +19,28 @@
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/ */
import { observer } from "mobx-react";
import React from "react"; import React from "react";
import type { RouteComponentProps } from "react-router";
import { catalogURL } from "../../../common/routes"; import { catalogURL } from "../../../common/routes";
import type { Cluster } from "../../../main/cluster";
import { navigate } from "../../navigation"; import { navigate } from "../../navigation";
import { Icon } from "../icon"; import { Icon } from "../icon";
import { TopBar } from "../layout/topbar"; import { TopBar } from "../layout/topbar";
import { MaterialTooltip } from "../material-tooltip/material-tooltip"; 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 { interface Props extends RouteComponentProps<ClusterViewRouteParams> {
cluster: Cluster
} }
export function ClusterTopbar({ cluster }: Props) { export const ClusterTopbar = observer((props: Props) => {
const getCluster = (): Cluster | undefined => {
return ClusterStore.getInstance().getById(props.match.params.clusterId);
};
return ( return (
<TopBar label={cluster.name}> <TopBar label={getCluster()?.name}>
<div> <div>
<MaterialTooltip title="Back to Catalog" placement="left"> <MaterialTooltip title="Back to Catalog" placement="left">
<Icon style={{ cursor: "default" }} material="close" onClick={() => navigate(catalogURL())}/> <Icon style={{ cursor: "default" }} material="close" onClick={() => navigate(catalogURL())}/>
@ -41,4 +48,4 @@ export function ClusterTopbar({ cluster }: Props) {
</div> </div>
</TopBar> </TopBar>
); );
} });

View File

@ -32,7 +32,6 @@ import { requestMain } from "../../../common/ipc";
import { clusterActivateHandler } from "../../../common/cluster-ipc"; import { clusterActivateHandler } from "../../../common/cluster-ipc";
import { catalogEntityRegistry } from "../../api/catalog-entity-registry"; import { catalogEntityRegistry } from "../../api/catalog-entity-registry";
import { navigate } from "../../navigation"; import { navigate } from "../../navigation";
import { ClusterTopbar } from "./cluster-topbar";
import { catalogURL, ClusterViewRouteParams } from "../../../common/routes"; import { catalogURL, ClusterViewRouteParams } from "../../../common/routes";
interface Props extends RouteComponentProps<ClusterViewRouteParams> { interface Props extends RouteComponentProps<ClusterViewRouteParams> {
@ -104,7 +103,6 @@ export class ClusterView extends React.Component<Props> {
render() { render() {
return ( return (
<div className="ClusterView flex column align-center"> <div className="ClusterView flex column align-center">
{this.cluster && <ClusterTopbar cluster={this.cluster}/>}
{this.renderStatus()} {this.renderStatus()}
</div> </div>
); );

View File

@ -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("<TopBar/>", () => {
beforeEach(() => {
TopBarRegistry.createInstance();
});
afterEach(() => {
TopBarRegistry.resetInstance();
});
it("renders w/o errors", () => {
const { container } = render(<TopBar label="test bar" />);
expect(container).toBeInstanceOf(HTMLElement);
});
it("renders title", async () => {
const { getByTestId } = render(<TopBar label="topbar" />);
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: <span data-testid={testId}>{text}</span>
}
}
]);
const { getByTestId } = render(<TopBar label="topbar" />);
expect(await getByTestId(testId)).toHaveTextContent(text);
});
});

View File

@ -28,7 +28,7 @@
grid-template-columns: [sidebar] var(--sidebar-width) [contents] 1fr; grid-template-columns: [sidebar] var(--sidebar-width) [contents] 1fr;
width: 100%; width: 100%;
z-index: 1; z-index: 1;
height: calc(100% - var(--main-layout-header)); height: 100%;
} }
.sidebar { .sidebar {

View File

@ -27,6 +27,7 @@
background-color: var(--layoutBackground); background-color: var(--layoutBackground);
z-index: 1; z-index: 1;
width: 100%; width: 100%;
grid-area: topbar;
} }
.title { .title {

View File

@ -22,16 +22,44 @@
import styles from "./topbar.module.css"; import styles from "./topbar.module.css";
import React from "react"; import React from "react";
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import { TopBarRegistry } from "../../../extensions/registries";
interface Props extends React.HTMLAttributes<any> { interface Props extends React.HTMLAttributes<any> {
label: React.ReactNode; label: React.ReactNode;
} }
export const TopBar = observer(({ label, children, ...rest }: Props) => { export const TopBar = observer(({ label, children, ...rest }: Props) => {
const renderRegisteredItems = () => {
const items = TopBarRegistry.getInstance().getItems();
if (!Array.isArray(items)) {
return null;
}
return (
<div className="px-6">
{items.map((registration, index) => {
if (!registration?.components?.Item) {
return null;
}
return (
<div key={index}>
{registration.components.Item}
</div>
);
})}
</div>
);
};
return ( return (
<div className={styles.topBar} {...rest}> <div className={styles.topBar} {...rest}>
<div className={styles.title}>{label}</div> <div className={styles.title} data-testid="topbarLabel">{label}</div>
<div className={styles.controls}>{children}</div> <div className={styles.controls}>
{renderRegisteredItems()}
{children}
</div>
</div> </div>
); );
}); });

View File

@ -36,4 +36,5 @@ export function initRegistries() {
registries.StatusBarRegistry.createInstance(); registries.StatusBarRegistry.createInstance();
registries.WelcomeMenuRegistry.createInstance(); registries.WelcomeMenuRegistry.createInstance();
registries.WorkloadsOverviewDetailRegistry.createInstance(); registries.WorkloadsOverviewDetailRegistry.createInstance();
registries.TopBarRegistry.createInstance();
} }