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

feat: Introduce API for changing the status bar colour

Signed-off-by: Sebastian Malton <sebastian@malton.name>
This commit is contained in:
Sebastian Malton 2023-04-14 15:38:46 -04:00
parent f6977428da
commit 06a0dce612
9 changed files with 3420 additions and 127 deletions

View File

@ -3,8 +3,12 @@
* Licensed under MIT License. See LICENSE in root directory for more information. * Licensed under MIT License. See LICENSE in root directory for more information.
*/ */
import type { StatusBarStatus } from "../../renderer/components/status-bar/current-status.injectable";
import type { SetStatusBarStatus } from "../../renderer/components/status-bar/set-status-bar-status.injectable";
import setStatusBarStatusInjectable from "../../renderer/components/status-bar/set-status-bar-status.injectable";
import activeThemeInjectable from "../../renderer/themes/active.injectable"; import activeThemeInjectable from "../../renderer/themes/active.injectable";
import type { LensTheme } from "../../renderer/themes/lens-theme"; import type { LensTheme } from "../../renderer/themes/lens-theme";
import { asLegacyGlobalFunctionForExtensionApi } from "../as-legacy-globals-for-extension-api/as-legacy-global-function-for-extension-api";
import { asLegacyGlobalForExtensionApi } from "../as-legacy-globals-for-extension-api/as-legacy-global-object-for-extension-api"; import { asLegacyGlobalForExtensionApi } from "../as-legacy-globals-for-extension-api/as-legacy-global-object-for-extension-api";
export const activeTheme = asLegacyGlobalForExtensionApi(activeThemeInjectable); export const activeTheme = asLegacyGlobalForExtensionApi(activeThemeInjectable);
@ -16,4 +20,10 @@ export function getActiveTheme() {
return activeTheme.get(); return activeTheme.get();
} }
export type { LensTheme }; export const setStatusBarStatus = asLegacyGlobalFunctionForExtensionApi(setStatusBarStatusInjectable);
export type {
LensTheme,
StatusBarStatus,
SetStatusBarStatus,
};

View File

@ -0,0 +1,15 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import { observable } from "mobx";
export type StatusBarStatus = "default" | "warning" | "error";
const statusBarCurrentStatusInjectable = getInjectable({
id: "status-bar-current-status",
instantiate: () => observable.box<StatusBarStatus>("default"),
});
export default statusBarCurrentStatusInjectable;

View File

@ -0,0 +1,21 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import { action } from "mobx";
import type { StatusBarStatus } from "./current-status.injectable";
import statusBarCurrentStatusInjectable from "./current-status.injectable";
export type SetStatusBarStatus = (newStatus: StatusBarStatus) => void;
const setStatusBarStatusInjectable = getInjectable({
id: "set-status-bar-status",
instantiate: (di): SetStatusBarStatus => {
const status = di.inject(statusBarCurrentStatusInjectable);
return action((newStatus) => status.set(newStatus));
},
});
export default setStatusBarStatusInjectable;

View File

@ -4,7 +4,6 @@
*/ */
import type { Injectable } from "@ogre-tools/injectable"; import type { Injectable } from "@ogre-tools/injectable";
import { getInjectable } from "@ogre-tools/injectable"; import { getInjectable } from "@ogre-tools/injectable";
import type { IComputedValue } from "mobx";
import { computed } from "mobx"; import { computed } from "mobx";
import { extensionRegistratorInjectionToken } from "../../../extensions/extension-loader/extension-registrator-injection-token"; import { extensionRegistratorInjectionToken } from "../../../extensions/extension-loader/extension-registrator-injection-token";
import type { LensRendererExtension } from "../../../extensions/lens-renderer-extension"; import type { LensRendererExtension } from "../../../extensions/lens-renderer-extension";
@ -39,7 +38,6 @@ const toItemInjectableFor = (extension: LensRendererExtension, getRandomId: () =
const id = `${getRandomId()}-status-bar-item-for-extension-${extension.sanitizedExtensionId}`; const id = `${getRandomId()}-status-bar-item-for-extension-${extension.sanitizedExtensionId}`;
let component: React.ComponentType; let component: React.ComponentType;
let position: "left" | "right"; let position: "left" | "right";
const visible: IComputedValue<boolean> | undefined = registration?.visible;
if (registration?.item) { if (registration?.item) {
const { item } = registration; const { item } = registration;
@ -77,7 +75,7 @@ const toItemInjectableFor = (extension: LensRendererExtension, getRandomId: () =
origin: extension.sanitizedExtensionId, origin: extension.sanitizedExtensionId,
component, component,
position, position,
visible: visible ?? computed(() => true), visible: registration?.visible ?? computed(() => true),
}), }),
injectionToken: statusBarItemInjectionToken, injectionToken: statusBarItemInjectionToken,

View File

@ -3,14 +3,12 @@
* Licensed under MIT License. See LICENSE in root directory for more information. * Licensed under MIT License. See LICENSE in root directory for more information.
*/ */
import { getInjectable } from "@ogre-tools/injectable"; import { getInjectable } from "@ogre-tools/injectable";
import type { IComputedValue } from "mobx";
import { computed } from "mobx"; import { computed } from "mobx";
import type { StatusBarItemProps } from "./status-bar-registration"; import type { StatusBarItemProps } from "./status-bar-registration";
import type { StatusBarItem } from "./status-bar-item-injection-token";
import { statusBarItemInjectionToken } from "./status-bar-item-injection-token"; import { statusBarItemInjectionToken } from "./status-bar-item-injection-token";
import { computedInjectManyInjectable } from "@ogre-tools/injectable-extension-for-mobx"; import { computedInjectManyInjectable } from "@ogre-tools/injectable-extension-for-mobx";
interface StatusItem { export interface StatusItem {
origin?: string; origin?: string;
component: React.ComponentType<StatusBarItemProps>; component: React.ComponentType<StatusBarItemProps>;
} }
@ -20,50 +18,38 @@ export interface StatusBarItems {
left: StatusItem[]; left: StatusItem[];
} }
interface Dependencies {
registrations: IComputedValue<StatusBarItem[]>;
}
function getStatusBarItems({ registrations }: Dependencies): IComputedValue<StatusBarItems> {
return computed(() => {
const res: StatusBarItems = {
left: [],
right: [],
};
for (const registration of registrations.get()) {
const { position = "right", component, visible, origin } = registration;
if (!visible.get()) {
continue;
}
res[position].push({
origin,
component,
});
}
// This is done so that the first ones registered are closest to the corner
res.right.reverse();
return res;
});
}
const statusBarItemsInjectable = getInjectable({ const statusBarItemsInjectable = getInjectable({
id: "status-bar-items", id: "status-bar-items",
instantiate: (di) => { instantiate: (di) => {
const computedInjectMany = di.inject( const computedInjectMany = di.inject(computedInjectManyInjectable);
computedInjectManyInjectable, const registrations = computedInjectMany(statusBarItemInjectionToken);
);
return getStatusBarItems({ return computed(() => {
registrations: computedInjectMany(statusBarItemInjectionToken), const res: StatusBarItems = {
left: [],
right: [],
};
for (const registration of registrations.get()) {
const { position = "right", component, visible, origin } = registration;
if (!visible.get()) {
continue;
}
res[position].push({
origin,
component,
});
}
// This is done so that the first ones registered are closest to the corner
res.right.reverse();
return res;
}); });
}, },
}); });
export default statusBarItemsInjectable; export default statusBarItemsInjectable;

View File

@ -8,12 +8,23 @@
color: white; color: white;
grid-area: status-bar; grid-area: status-bar;
background-color: var(--blue);
height: var(--status-bar-height); height: var(--status-bar-height);
font-size: var(--font-size-small); font-size: var(--font-size-small);
display: inline-grid; display: inline-grid;
grid-template-columns: 1fr 1fr; grid-template-columns: 1fr 1fr;
&.status-default {
background-color: var(--colorInfo);
}
&.status-warning {
background-color: var(--colorWarning);
}
&.status-error {
background-color: var(--colorError);
}
} }
.leftSide { .leftSide {

View File

@ -5,47 +5,45 @@
import React from "react"; import React from "react";
import "@testing-library/jest-dom/extend-expect"; import "@testing-library/jest-dom/extend-expect";
import type { IObservableArray } from "mobx";
import { computed, observable } from "mobx";
import type { StatusBarItems } from "./status-bar-items.injectable";
import statusBarItemsInjectable from "./status-bar-items.injectable";
import directoryForUserDataInjectable from "../../../common/app-paths/directory-for-user-data/directory-for-user-data.injectable";
import type { ApplicationBuilder } from "../test-utils/get-application-builder"; import type { ApplicationBuilder } from "../test-utils/get-application-builder";
import { getApplicationBuilder } from "../test-utils/get-application-builder"; import { getApplicationBuilder } from "../test-utils/get-application-builder";
import setStatusBarStatusInjectable from "./set-status-bar-status.injectable";
import type { RenderResult } from "@testing-library/react";
import getRandomIdInjectable from "../../../common/utils/get-random-id.injectable"; import getRandomIdInjectable from "../../../common/utils/get-random-id.injectable";
describe("<StatusBar />", () => { describe("<StatusBar />", () => {
let statusBarItems: IObservableArray<any>;
let builder: ApplicationBuilder; let builder: ApplicationBuilder;
let result: RenderResult;
beforeEach(async () => { beforeEach(async () => {
statusBarItems = observable.array([]);
builder = getApplicationBuilder(); builder = getApplicationBuilder();
builder.beforeWindowStart(({ windowDi }) => { builder.beforeWindowStart(({ windowDi }) => {
windowDi.unoverride(getRandomIdInjectable);
windowDi.permitSideEffects(getRandomIdInjectable); windowDi.permitSideEffects(getRandomIdInjectable);
windowDi.override(directoryForUserDataInjectable, () => "some-directory-for-user-data"); windowDi.unoverride(getRandomIdInjectable);
}); });
builder.extensions.enable({ result = await builder.render();
id: "some-id", });
name: "some-name",
rendererOptions: { describe("when an extension is enabled with no status items", () => {
statusBarItems, beforeEach(() => {
}, builder.extensions.enable({
id: "some-id",
name: "some-name",
rendererOptions: {
statusBarItems: [],
},
});
});
it("renders", () => {
expect(result.baseElement).toMatchSnapshot();
}); });
}); });
it("renders w/o errors", async () => { describe.each([
const { container } = await builder.render();
expect(container).toBeInstanceOf(HTMLElement);
});
it.each([
undefined, undefined,
"hello", "hello",
6, 6,
@ -53,73 +51,130 @@ describe("<StatusBar />", () => {
[], [],
[{}], [{}],
{}, {},
])("renders w/o errors when registrations are not type compliant (%p)", async val => { ])("when an extension is enabled with an invalid data type, (%p)", (value) => {
statusBarItems.replace([val]); beforeEach(() => {
builder.extensions.enable({
id: "some-id",
name: "some-name",
await expect(builder.render()).resolves.toBeTruthy(); rendererOptions: {
}); statusBarItems: [value as any],
},
it("renders items [{item: React.ReactNode}] (4.0.0-rc.1)", async () => { });
const testId = "testId";
const text = "heee";
builder.beforeWindowStart(({ windowDi }) => {
windowDi.override(statusBarItemsInjectable, () => computed(() => ({
right: [ { origin: testId, component: () => <span data-testid={testId} >{text}</span> }],
left: [],
}) as StatusBarItems));
}); });
const { getByTestId } = await builder.render(); it("renders", () => {
expect(result.baseElement).toMatchSnapshot();
expect(getByTestId(testId)).toHaveTextContent(text); });
}); });
it("renders items [{item: () => React.ReactNode}] (4.0.0-rc.1+)", async () => { describe("when an extension is enabled using a deprecated registration of a plain ReactNode", () => {
const testId = "testId"; beforeEach(() => {
const text = "heee"; builder.extensions.enable({
id: "some-id",
name: "some-name",
statusBarItems.replace([{ rendererOptions: {
item: () => <span data-testid={testId} >{text}</span>, statusBarItems: [{
}]); item: "heeeeeeee",
}],
},
});
});
const { getByTestId } = await builder.render(); it("renders the provided ReactNode", () => {
expect(result.baseElement).toHaveTextContent("heeeeeeee");
expect(getByTestId(testId)).toHaveTextContent(text); });
}); });
describe("when an extension is enabled using a deprecated registration of a function returning a ReactNode", () => {
beforeEach(() => {
builder.extensions.enable({
id: "some-id",
name: "some-name",
it("sort positioned items properly", async () => { rendererOptions: {
statusBarItems.replace([ statusBarItems: [{
{ item: () => "heeeeeeee",
components: { }],
Item: () => <div data-testid="sortedElem">right1</div>,
}, },
}, });
{ });
components: {
Item: () => <div data-testid="sortedElem">right2</div>,
position: "right",
},
},
{
components: {
Item: () => <div data-testid="sortedElem">left1</div>,
position: "left",
},
},
{
components: {
Item: () => <div data-testid="sortedElem">left2</div>,
position: "left",
},
},
]);
const { getAllByTestId } = await builder.render(); it("renders the provided ReactNode", () => {
const elems = getAllByTestId("sortedElem"); expect(result.baseElement).toHaveTextContent("heeeeeeee");
const positions = elems.map(elem => elem.textContent); });
});
expect(positions).toEqual(["left1", "left2", "right2", "right1"]); describe("when an extension is enabled specifying the side the elements should be on", () => {
beforeEach(() => {
builder.extensions.enable({
id: "some-id",
name: "some-name",
rendererOptions: {
statusBarItems: [
{
components: {
Item: () => <div data-testid="sortedElem">right1</div>,
},
},
{
components: {
Item: () => <div data-testid="sortedElem">right2</div>,
position: "right",
},
},
{
components: {
Item: () => <div data-testid="sortedElem">left1</div>,
position: "left",
},
},
{
components: {
Item: () => <div data-testid="sortedElem">left2</div>,
position: "left",
},
},
],
},
});
});
it("renders", () => {
expect(result.baseElement).toMatchSnapshot();
});
it("sort positioned items properly", async () => {
const elems = result.getAllByTestId("sortedElem");
const positions = elems.map(elem => elem.textContent);
expect(positions).toEqual(["left1", "left2", "right2", "right1"]);
});
});
it("has the default status by default", () => {
expect([...result.getByTestId("status-bar").classList]).toContain("status-default");
});
describe.each([
"warning" as const,
"error" as const,
])("when StatusBar's status is set to %p", (value) => {
beforeEach(() => {
const di = builder.applicationWindow.only.di;
const setStatusBarStatus = di.inject(setStatusBarStatusInjectable);
setStatusBarStatus(value);
});
it("renders", () => {
expect(result.baseElement).toMatchSnapshot();
});
it(`has the ${value} status by default`, () => {
expect([...result.getByTestId("status-bar").classList]).toContain(`status-${value}`);
});
}); });
}); });

View File

@ -10,19 +10,26 @@ import { observer } from "mobx-react";
import { withInjectables } from "@ogre-tools/injectable-react"; import { withInjectables } from "@ogre-tools/injectable-react";
import type { StatusBarItems } from "./status-bar-items.injectable"; import type { StatusBarItems } from "./status-bar-items.injectable";
import statusBarItemsInjectable from "./status-bar-items.injectable"; import statusBarItemsInjectable from "./status-bar-items.injectable";
import type { IComputedValue } from "mobx"; import type { IComputedValue, IObservableValue } from "mobx";
import type { StatusBarStatus } from "./current-status.injectable";
import statusBarCurrentStatusInjectable from "./current-status.injectable";
import { cssNames } from "@k8slens/utilities";
export interface StatusBarProps {} export interface StatusBarProps {}
interface Dependencies { interface Dependencies {
items: IComputedValue<StatusBarItems>; items: IComputedValue<StatusBarItems>;
status: IObservableValue<StatusBarStatus>;
} }
const NonInjectedStatusBar = observer(({ items }: Dependencies & StatusBarProps) => { const NonInjectedStatusBar = observer(({
items,
status,
}: Dependencies & StatusBarProps) => {
const { left, right } = items.get(); const { left, right } = items.get();
return ( return (
<div className={styles.StatusBar} data-testid="status-bar"> <div className={cssNames(styles.StatusBar, styles[`status-${status.get()}`])} data-testid="status-bar">
<div className={styles.leftSide} data-testid="status-bar-left"> <div className={styles.leftSide} data-testid="status-bar-left">
{left.map((Item, index) => ( {left.map((Item, index) => (
<div <div
@ -50,7 +57,8 @@ const NonInjectedStatusBar = observer(({ items }: Dependencies & StatusBarProps)
export const StatusBar = withInjectables<Dependencies, StatusBarProps>(NonInjectedStatusBar, { export const StatusBar = withInjectables<Dependencies, StatusBarProps>(NonInjectedStatusBar, {
getProps: (di, props) => ({ getProps: (di, props) => ({
items: di.inject(statusBarItemsInjectable),
...props, ...props,
items: di.inject(statusBarItemsInjectable),
status: di.inject(statusBarCurrentStatusInjectable),
}), }),
}); });