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

Show active hotbar on bottom (#4476)

- Extends extension API to support choosing sides for bottom bar entries
This commit is contained in:
Alex Andreev 2021-12-03 17:41:08 +03:00 committed by GitHub
parent 39955b623f
commit d73df7fe0d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 231 additions and 28 deletions

View File

@ -26,6 +26,12 @@ import { BaseRegistry } from "./base-registry";
interface StatusBarComponents {
Item?: React.ComponentType;
/**
* The side of the bottom bar to place this component.
*
* @default "right"
*/
position?: "left" | "right";
}
interface StatusBarRegistrationV2 {

View File

@ -104,13 +104,13 @@ export async function bootstrap(comp: () => Promise<AppComponent>, di: Dependenc
logger.info(`${logPrefix} initializing WelcomeMenuRegistry`);
initializers.initWelcomeMenuRegistry();
logger.info(`${logPrefix} initializing WorkloadsOverviewDetailRegist`);
logger.info(`${logPrefix} initializing WorkloadsOverviewDetailRegistry`);
initializers.initWorkloadsOverviewDetailRegistry();
logger.info(`${logPrefix} initializing CatalogEntityDetailRegistry`);
initializers.initCatalogEntityDetailRegistry();
logger.info(`${logPrefix} initializing CatalogCategoryRegistryEntrie`);
logger.info(`${logPrefix} initializing CatalogCategoryRegistryEntries`);
initializers.initCatalogCategoryRegistryEntries();
logger.info(`${logPrefix} initializing Catalog`);
@ -119,6 +119,9 @@ export async function bootstrap(comp: () => Promise<AppComponent>, di: Dependenc
logger.info(`${logPrefix} initializing IpcRendererListeners`);
initializers.initIpcRendererListeners();
logger.info(`${logPrefix} initializing StatusBarRegistry`);
initializers.initStatusBarRegistry();
ExtensionLoader.createInstance().init();
ExtensionDiscovery.createInstance().init();

View File

@ -0,0 +1,40 @@
/**
* 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 { observer } from "mobx-react";
import { Icon } from "../icon";
import { HotbarStore } from "../../../common/hotbar-store";
import { CommandOverlay } from "../command-palette";
import { HotbarSwitchCommand } from "../hotbar/hotbar-switch-command";
export const ActiveHotbarName = observer(() => {
return (
<div
className="flex items-center"
data-testid="current-hotbar-name"
onClick={() => CommandOverlay.open(<HotbarSwitchCommand />)}
>
<Icon material="bookmarks" className="mr-2" size={14}/>
{HotbarStore.getInstance().getActive()?.name}
</div>
);
});

View File

@ -20,22 +20,26 @@
*/
.BottomBar {
$spacing: $padding * 0.5;
--flex-gap: #{$spacing};
@apply flex px-2 text-white;
grid-area: bottom-bar;
background-color: var(--blue);
padding: 0 2px;
height: var(--bottom-bar-height);
.extensions {
font-size: var(--font-size-small);
color: white;
}
.item {
padding: $padding * 0.25 $padding * 0.5;
@apply flex items-center mr-2 h-full px-2 last:mr-0;
&:hover {
@apply cursor-pointer;
background-color: #ffffff33;
cursor: pointer;
}
}
}
.onLeft + .onRight {
@apply ml-auto;
}
.onRight {}

View File

@ -20,25 +20,58 @@
*/
import React from "react";
import { render } from "@testing-library/react";
import mockFs from "mock-fs";
import { render, fireEvent } from "@testing-library/react";
import "@testing-library/jest-dom/extend-expect";
import { BottomBar } from "./bottom-bar";
import { StatusBarRegistry } from "../../../extensions/registries";
import { HotbarStore } from "../../../common/hotbar-store";
import { AppPaths } from "../../../common/app-paths";
import { CommandOverlay } from "../command-palette";
import { HotbarSwitchCommand } from "../hotbar/hotbar-switch-command";
import { ActiveHotbarName } from "./active-hotbar-name";
jest.mock("electron", () => ({
app: {
getPath: () => "/foo",
jest.mock("../command-palette", () => ({
CommandOverlay: {
open: jest.fn(),
},
}));
import { BottomBar } from "./bottom-bar";
import { StatusBarRegistry } from "../../../extensions/registries";
AppPaths.init();
jest.mock("electron", () => ({
app: {
getName: () => "lens",
setName: jest.fn(),
setPath: jest.fn(),
getPath: () => "tmp",
},
ipcMain: {
handle: jest.fn(),
on: jest.fn(),
removeAllListeners: jest.fn(),
off: jest.fn(),
send: jest.fn(),
},
}));
describe("<BottomBar />", () => {
beforeEach(() => {
const mockOpts = {
"tmp": {
"test-store.json": JSON.stringify({}),
},
};
mockFs(mockOpts);
StatusBarRegistry.createInstance();
HotbarStore.createInstance();
});
afterEach(() => {
StatusBarRegistry.resetInstance();
HotbarStore.resetInstance();
mockFs.restore();
});
it("renders w/o errors", () => {
@ -87,4 +120,73 @@ describe("<BottomBar />", () => {
expect(await getByTestId(testId)).toHaveTextContent(text);
});
it("show default hotbar name", () => {
StatusBarRegistry.getInstance().getItems = jest.fn().mockImplementationOnce(() => [
{ item: () => <ActiveHotbarName/> },
]);
const { getByTestId } = render(<BottomBar />);
expect(getByTestId("current-hotbar-name")).toHaveTextContent("default");
});
it("show active hotbar name", () => {
StatusBarRegistry.getInstance().getItems = jest.fn().mockImplementationOnce(() => [
{ item: () => <ActiveHotbarName/> },
]);
const { getByTestId } = render(<BottomBar />);
HotbarStore.getInstance().add({
id: "new",
name: "new",
}, { setActive: true });
expect(getByTestId("current-hotbar-name")).toHaveTextContent("new");
});
it("opens command palette on click", () => {
StatusBarRegistry.getInstance().getItems = jest.fn().mockImplementationOnce(() => [
{ item: () => <ActiveHotbarName/> },
]);
const { getByTestId } = render(<BottomBar />);
const activeHotbar = getByTestId("current-hotbar-name");
fireEvent.click(activeHotbar);
expect(CommandOverlay.open).toHaveBeenCalledWith(<HotbarSwitchCommand />);
});
it("sort positioned items properly", () => {
StatusBarRegistry.getInstance().getItems = jest.fn().mockImplementationOnce(() => [
{
components: {
Item: () => <div data-testid="sortedElem">right</div>,
},
},
{
components: {
Item: () => <div data-testid="sortedElem">right</div>,
position: "right",
},
},
{
components: {
Item: () => <div data-testid="sortedElem">left</div>,
position: "left",
},
},
{
components: {
Item: () => <div data-testid="sortedElem">left</div>,
position: "left",
},
},
]);
const { getAllByTestId } = render(<BottomBar />);
const elems = getAllByTestId("sortedElem");
const positions = elems.map(elem => elem.textContent);
expect(positions).toEqual(["left", "left", "right", "right"]);
});
});

View File

@ -19,11 +19,12 @@
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
import "./bottom-bar.scss";
import styles from "./bottom-bar.module.css";
import React from "react";
import { observer } from "mobx-react";
import { StatusBarRegistration, StatusBarRegistry } from "../../../extensions/registries";
import { cssNames } from "../../utils";
@observer
export class BottomBar extends React.Component {
@ -44,26 +45,36 @@ export class BottomBar extends React.Component {
return null;
}
items.sort(function sortLeftPositionFirst(a, b) {
return a.components?.position?.localeCompare(b.components?.position);
});
return (
<div className="extensions box grow flex gaps justify-flex-end">
<>
{items.map((registration, index) => {
if (!registration?.item && !registration?.components?.Item) {
return null;
}
return (
<div className="flex align-center gaps item" key={index}>
<div
className={cssNames(styles.item, {
[styles.onLeft]: registration.components?.position == "left",
[styles.onRight]: registration.components?.position != "left",
})}
key={index}
>
{this.renderRegisteredItem(registration)}
</div>
);
})}
</div>
</>
);
}
render() {
return (
<div className="BottomBar flex gaps">
<div className={styles.BottomBar}>
{this.renderRegisteredItems()}
</div>
);

View File

@ -20,7 +20,7 @@
*/
.ClusterManager {
--bottom-bar-height: 22px;
--bottom-bar-height: 21px;
--hotbar-width: 75px;
display: grid;

View File

@ -43,7 +43,8 @@
.Icon {
--size: 16px;
padding: 0 4px;
padding: 0 4px 0 0px;
margin: 0px 4px;
&:hover {
box-shadow: none;

View File

@ -30,3 +30,4 @@ export * from "./registries";
export * from "./welcome-menu-registry";
export * from "./workloads-overview-detail-registry";
export * from "./catalog-category-registry";
export * from "./status-bar-registry";

View File

@ -0,0 +1,35 @@
/**
* 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 { StatusBarRegistry } from "../../extensions/registries";
import { ActiveHotbarName } from "../components/cluster-manager/active-hotbar-name";
export function initStatusBarRegistry() {
StatusBarRegistry.getInstance().add([
{
components: {
Item: () => <ActiveHotbarName/>,
position: "left",
},
},
]);
}