From 54b87efd8967ee39a9059cb96b51a5c1b319179d Mon Sep 17 00:00:00 2001 From: Janne Savolainen Date: Thu, 3 Mar 2022 14:55:57 +0200 Subject: [PATCH] Replace KubeObjectStatusRegistry with reactive solution (#4815) --- src/extensions/common-api/registrations.ts | 2 +- .../extension-loader/extension-loader.ts | 1 - src/extensions/lens-renderer-extension.ts | 3 +- src/extensions/registries/index.ts | 1 - .../registries/kube-object-status-registry.ts | 29 -- .../kube-object-status-icon.test.tsx.snap | 332 ++++++++++++++++++ .../kube-object-status-icon.test.tsx | 263 ++++++++++++++ .../kube-object-status-icon.tsx | 22 +- .../kube-object-status-registration.ts | 12 + .../status-registrations.injectable.ts | 23 ++ .../statuses-for-kube-object.injectable.ts | 35 ++ src/renderer/initializers/registries.ts | 1 - 12 files changed, 687 insertions(+), 37 deletions(-) delete mode 100644 src/extensions/registries/kube-object-status-registry.ts create mode 100644 src/renderer/components/kube-object-status-icon/__snapshots__/kube-object-status-icon.test.tsx.snap create mode 100644 src/renderer/components/kube-object-status-icon/kube-object-status-icon.test.tsx create mode 100644 src/renderer/components/kube-object-status-icon/kube-object-status-registration.ts create mode 100644 src/renderer/components/kube-object-status-icon/status-registrations.injectable.ts create mode 100644 src/renderer/components/kube-object-status-icon/statuses-for-kube-object.injectable.ts diff --git a/src/extensions/common-api/registrations.ts b/src/extensions/common-api/registrations.ts index 76d7538e12..01b4407a6b 100644 --- a/src/extensions/common-api/registrations.ts +++ b/src/extensions/common-api/registrations.ts @@ -6,7 +6,7 @@ export type { StatusBarRegistration } from "../../renderer/components/status-bar export type { KubeObjectMenuRegistration, KubeObjectMenuComponents } from "../../renderer/components/kube-object-menu/dependencies/kube-object-menu-items/kube-object-menu-registration"; export type { AppPreferenceRegistration, AppPreferenceComponents } from "../../renderer/components/+preferences/app-preferences/app-preference-registration"; export type { KubeObjectDetailRegistration, KubeObjectDetailComponents } from "../registries/kube-object-detail-registry"; -export type { KubeObjectStatusRegistration } from "../registries/kube-object-status-registry"; +export type { KubeObjectStatusRegistration } from "../../renderer/components/kube-object-status-icon/kube-object-status-registration"; export type { PageRegistration, RegisteredPage, PageParams, PageComponentProps, PageComponents, PageTarget } from "../registries/page-registry"; export type { ClusterPageMenuRegistration, ClusterPageMenuComponents } from "../registries/page-menu-registry"; export type { ProtocolHandlerRegistration, RouteParams as ProtocolRouteParams, RouteHandler as ProtocolRouteHandler } from "../registries/protocol-handler"; diff --git a/src/extensions/extension-loader/extension-loader.ts b/src/extensions/extension-loader/extension-loader.ts index 40121ae37e..dee22e9407 100644 --- a/src/extensions/extension-loader/extension-loader.ts +++ b/src/extensions/extension-loader/extension-loader.ts @@ -277,7 +277,6 @@ export class ExtensionLoader { registries.ClusterPageRegistry.getInstance().add(extension.clusterPages, extension), registries.ClusterPageMenuRegistry.getInstance().add(extension.clusterPageMenus, extension), registries.KubeObjectDetailRegistry.getInstance().add(extension.kubeObjectDetailItems), - registries.KubeObjectStatusRegistry.getInstance().add(extension.kubeObjectStatusTexts), registries.WorkloadsOverviewDetailRegistry.getInstance().add(extension.kubeWorkloadsOverviewItems), ]; diff --git a/src/extensions/lens-renderer-extension.ts b/src/extensions/lens-renderer-extension.ts index b61cd9a2c8..ba68306886 100644 --- a/src/extensions/lens-renderer-extension.ts +++ b/src/extensions/lens-renderer-extension.ts @@ -20,12 +20,13 @@ import type { AdditionalCategoryColumnRegistration } from "../renderer/component import type { CustomCategoryViewRegistration } from "../renderer/components/+catalog/custom-views"; import type { StatusBarRegistration } from "../renderer/components/status-bar/status-bar-registration"; import type { KubeObjectMenuRegistration } from "../renderer/components/kube-object-menu/dependencies/kube-object-menu-items/kube-object-menu-registration"; +import type { KubeObjectStatusRegistration } from "../renderer/components/kube-object-status-icon/kube-object-status-registration"; export class LensRendererExtension extends LensExtension { globalPages: registries.PageRegistration[] = []; clusterPages: registries.PageRegistration[] = []; clusterPageMenus: registries.ClusterPageMenuRegistration[] = []; - kubeObjectStatusTexts: registries.KubeObjectStatusRegistration[] = []; + kubeObjectStatusTexts: KubeObjectStatusRegistration[] = []; appPreferences: AppPreferenceRegistration[] = []; entitySettings: registries.EntitySettingRegistration[] = []; statusBarItems: StatusBarRegistration[] = []; diff --git a/src/extensions/registries/index.ts b/src/extensions/registries/index.ts index 0713e3eede..6a4a821aaa 100644 --- a/src/extensions/registries/index.ts +++ b/src/extensions/registries/index.ts @@ -8,7 +8,6 @@ export * from "./page-registry"; export * from "./page-menu-registry"; export * from "./kube-object-detail-registry"; -export * from "./kube-object-status-registry"; export * from "./entity-setting-registry"; export * from "./catalog-entity-detail-registry"; export * from "./workloads-overview-detail-registry"; diff --git a/src/extensions/registries/kube-object-status-registry.ts b/src/extensions/registries/kube-object-status-registry.ts deleted file mode 100644 index 0121e9a39e..0000000000 --- a/src/extensions/registries/kube-object-status-registry.ts +++ /dev/null @@ -1,29 +0,0 @@ -/** - * Copyright (c) OpenLens Authors. All rights reserved. - * Licensed under MIT License. See LICENSE in root directory for more information. - */ - -import type { KubeObject, KubeObjectStatus } from "../renderer-api/k8s-api"; -import { BaseRegistry } from "./base-registry"; - -export interface KubeObjectStatusRegistration { - kind: string; - apiVersions: string[]; - resolve: (object: KubeObject) => KubeObjectStatus; -} - -export class KubeObjectStatusRegistry extends BaseRegistry { - getItemsForKind(kind: string, apiVersion: string) { - return this.getItems() - .filter((item) => ( - item.kind === kind - && item.apiVersions.includes(apiVersion) - )); - } - - getItemsForObject(src: KubeObject) { - return this.getItemsForKind(src.kind, src.apiVersion) - .map(item => item.resolve(src)) - .filter(Boolean); - } -} diff --git a/src/renderer/components/kube-object-status-icon/__snapshots__/kube-object-status-icon.test.tsx.snap b/src/renderer/components/kube-object-status-icon/__snapshots__/kube-object-status-icon.test.tsx.snap new file mode 100644 index 0000000000..86f5688d82 --- /dev/null +++ b/src/renderer/components/kube-object-status-icon/__snapshots__/kube-object-status-icon.test.tsx.snap @@ -0,0 +1,332 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`kube-object-status-icon given info and warning statuses are present, when rendered, renders with statuses 1`] = ` + +
+ + + warning + +
+ +
+
+
+
+ + Warning + +
+ - + Some warning status for some-name + + + · + 2d + +
+
+
+ + Info + +
+ - + Some info status for some-name + + + · + 2d + +
+
+
+
+ +`; + +exports[`kube-object-status-icon given level "critical" status, when rendered, renders with status 1`] = ` + +
+ + + error + +
+ +
+
+
+
+ + Critical + +
+ - + Some critical status for some-name + + + · + 2d + +
+
+
+
+ +`; + +exports[`kube-object-status-icon given level "info" status, when rendered, renders with status 1`] = ` + +
+ + + info + +
+ +
+
+
+
+ + Info + +
+ - + Some info status for some-name + + + · + 2d + +
+
+
+
+ +`; + +exports[`kube-object-status-icon given level "warning" status, when rendered, renders with status 1`] = ` + +
+ + + warning + +
+ +
+
+
+
+ + Warning + +
+ - + Some warning status for some-name + + + · + 2d + +
+
+
+
+ +`; + +exports[`kube-object-status-icon given no statuses, when rendered, renders as empty 1`] = `
`; + +exports[`kube-object-status-icon given registration for wrong api version, when rendered, renders as empty 1`] = ` + +
+ +`; + +exports[`kube-object-status-icon given registration for wrong kind, when rendered, renders as empty 1`] = ` + +
+ +`; + +exports[`kube-object-status-icon given registration without status for exact kube object, when rendered, renders as empty 1`] = ` + +
+ +`; + +exports[`kube-object-status-icon given status for all levels is present, when rendered, renders with statuses 1`] = ` + +
+ + + error + +
+ +
+
+
+
+ + Critical + +
+ - + Some critical status for some-name + + + · + 2d + +
+
+
+ + Warning + +
+ - + Some warning status for some-name + + + · + 2d + +
+
+
+ + Info + +
+ - + Some info status for some-name + + + · + 2d + +
+
+
+
+ +`; diff --git a/src/renderer/components/kube-object-status-icon/kube-object-status-icon.test.tsx b/src/renderer/components/kube-object-status-icon/kube-object-status-icon.test.tsx new file mode 100644 index 0000000000..7f181af8f8 --- /dev/null +++ b/src/renderer/components/kube-object-status-icon/kube-object-status-icon.test.tsx @@ -0,0 +1,263 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getDiForUnitTesting } from "../../getDiForUnitTesting"; +import rendererExtensionsInjectable from "../../../extensions/renderer-extensions.injectable"; +import { DiRender, renderFor } from "../test-utils/renderFor"; +import { computed } from "mobx"; +import { LensRendererExtension } from "../../../extensions/lens-renderer-extension"; +import { KubeObjectStatusLevel } from "../../../extensions/renderer-api/kube-object-status"; +import { KubeObject } from "../../../common/k8s-api/kube-object"; +import { KubeObjectStatusIcon } from "./kube-object-status-icon"; +import React from "react"; +import type { KubeObjectStatusRegistration } from "./kube-object-status-registration"; + +describe("kube-object-status-icon", () => { + let render: DiRender; + let kubeObjectStatusRegistrations: KubeObjectStatusRegistration[]; + + beforeEach(async () => { + // TODO: Make mocking of date in unit tests global + global.Date.now = () => new Date("2015-10-21T07:28:00Z").getTime(); + + const di = getDiForUnitTesting({ doGeneralOverrides: true }); + + render = renderFor(di); + + kubeObjectStatusRegistrations = []; + + const someTestExtension = new SomeTestExtension( + kubeObjectStatusRegistrations, + ); + + di.override(rendererExtensionsInjectable, () => + computed(() => [someTestExtension]), + ); + + await di.runSetups(); + }); + + it("given no statuses, when rendered, renders as empty", () => { + const kubeObject = getKubeObjectStub("irrelevant", "irrelevant"); + + const { container } = render(); + + expect(container).toMatchSnapshot(); + }); + + it('given level "critical" status, when rendered, renders with status', () => { + const kubeObject = getKubeObjectStub("some-kind", "some-api-version"); + + const statusRegistration = getStatusRegistration( + KubeObjectStatusLevel.CRITICAL, + "critical", + "some-kind", + ["some-api-version"], + ); + + kubeObjectStatusRegistrations.push(statusRegistration); + + const { baseElement } = render( + , + ); + + expect(baseElement).toMatchSnapshot(); + }); + + it('given level "info" status, when rendered, renders with status', () => { + const kubeObject = getKubeObjectStub("some-kind", "some-api-version"); + + const statusRegistration = getStatusRegistration( + KubeObjectStatusLevel.INFO, + "info", + "some-kind", + ["some-api-version"], + ); + + kubeObjectStatusRegistrations.push(statusRegistration); + + const { baseElement } = render( + , + ); + + expect(baseElement).toMatchSnapshot(); + }); + + it('given level "warning" status, when rendered, renders with status', () => { + const kubeObject = getKubeObjectStub("some-kind", "some-api-version"); + + const statusRegistration = getStatusRegistration( + KubeObjectStatusLevel.WARNING, + "warning", + "some-kind", + ["some-api-version"], + ); + + kubeObjectStatusRegistrations.push(statusRegistration); + + const { baseElement } = render( + , + ); + + expect(baseElement).toMatchSnapshot(); + }); + + it("given status for all levels is present, when rendered, renders with statuses", () => { + const kubeObject = getKubeObjectStub("some-kind", "some-api-version"); + + const critical = getStatusRegistration( + KubeObjectStatusLevel.CRITICAL, + "critical", + "some-kind", + ["some-api-version"], + ); + + const warning = getStatusRegistration( + KubeObjectStatusLevel.WARNING, + "warning", + "some-kind", + ["some-api-version"], + ); + + const info = getStatusRegistration( + KubeObjectStatusLevel.INFO, + "info", + "some-kind", + ["some-api-version"], + ); + + kubeObjectStatusRegistrations.push(critical); + kubeObjectStatusRegistrations.push(warning); + kubeObjectStatusRegistrations.push(info); + + const { baseElement } = render( + , + ); + + expect(baseElement).toMatchSnapshot(); + }); + + it("given info and warning statuses are present, when rendered, renders with statuses", () => { + const kubeObject = getKubeObjectStub("some-kind", "some-api-version"); + + const warning = getStatusRegistration( + KubeObjectStatusLevel.WARNING, + "warning", + "some-kind", + ["some-api-version"], + ); + + const info = getStatusRegistration( + KubeObjectStatusLevel.INFO, + "info", + "some-kind", + ["some-api-version"], + ); + + kubeObjectStatusRegistrations.push(warning); + kubeObjectStatusRegistrations.push(info); + + const { baseElement } = render( + , + ); + + expect(baseElement).toMatchSnapshot(); + }); + + + it("given registration for wrong api version, when rendered, renders as empty", () => { + const kubeObject = getKubeObjectStub("some-kind", "some-api-version"); + + const statusRegistration = getStatusRegistration( + KubeObjectStatusLevel.CRITICAL, + "irrelevant", + "some-kind", + ["some-other-api-version"], + ); + + kubeObjectStatusRegistrations.push(statusRegistration); + + const { baseElement } = render( + , + ); + + expect(baseElement).toMatchSnapshot(); + }); + + it("given registration for wrong kind, when rendered, renders as empty", () => { + const kubeObject = getKubeObjectStub("some-kind", "some-api-version"); + + const statusRegistration = getStatusRegistration( + KubeObjectStatusLevel.CRITICAL, + "irrelevant", + "some-other-kind", + ["some-api-version"], + ); + + kubeObjectStatusRegistrations.push(statusRegistration); + + const { baseElement } = render( + , + ); + + expect(baseElement).toMatchSnapshot(); + }); + + it("given registration without status for exact kube object, when rendered, renders as empty", () => { + const kubeObject = getKubeObjectStub("some-kind", "some-api-version"); + + const statusRegistration = { + apiVersions: ["some-api-version"], + kind: "some-kind", + resolve: (): void => {}, + }; + + // @ts-ignore + kubeObjectStatusRegistrations.push(statusRegistration); + + const { baseElement } = render( + , + ); + + expect(baseElement).toMatchSnapshot(); + + }); +}); + +const getKubeObjectStub = (kind: string, apiVersion: string) => KubeObject.create({ + apiVersion, + kind, + metadata: { + uid: "some-uid", + name: "some-name", + resourceVersion: "some-resource-version", + namespace: "some-namespace", + }, +}); + +const getStatusRegistration = (level: KubeObjectStatusLevel, title: string, kind: string, apiVersions: string[]) => ({ + apiVersions, + kind, + resolve: (kubeObject: KubeObject) => ({ + level, + text: `Some ${title} status for ${kubeObject.getName()}`, + timestamp: "2015-10-19T07:28:00Z", + }), +}); + +class SomeTestExtension extends LensRendererExtension { + constructor(kubeObjectStatusTexts: KubeObjectStatusRegistration[]) { + super({ + id: "some-id", + absolutePath: "irrelevant", + isBundled: false, + isCompatible: false, + isEnabled: false, + manifest: { name: "some-id", version: "some-version" }, + manifestPath: "irrelevant", + }); + + this.kubeObjectStatusTexts = kubeObjectStatusTexts; + } +} diff --git a/src/renderer/components/kube-object-status-icon/kube-object-status-icon.tsx b/src/renderer/components/kube-object-status-icon/kube-object-status-icon.tsx index 7ca6098e3f..db5429998e 100644 --- a/src/renderer/components/kube-object-status-icon/kube-object-status-icon.tsx +++ b/src/renderer/components/kube-object-status-icon/kube-object-status-icon.tsx @@ -9,7 +9,8 @@ import React from "react"; import { Icon } from "../icon"; import { cssNames, formatDuration } from "../../utils"; import { KubeObject, KubeObjectStatus, KubeObjectStatusLevel } from "../../..//extensions/renderer-api/k8s-api"; -import { KubeObjectStatusRegistry } from "../../../extensions/registries"; +import { withInjectables } from "@ogre-tools/injectable-react"; +import statusesForKubeObjectInjectable from "./statuses-for-kube-object.injectable"; function statusClassName(level: KubeObjectStatusLevel): string { switch (level) { @@ -68,7 +69,11 @@ export interface KubeObjectStatusIconProps { object: KubeObject; } -export class KubeObjectStatusIcon extends React.Component { +interface Dependencies { + statuses: KubeObjectStatus[]; +} + +class NonInjectedKubeObjectStatusIcon extends React.Component { renderStatuses(statuses: KubeObjectStatus[], level: number) { const filteredStatuses = statuses.filter((item) => item.level == level); @@ -89,7 +94,7 @@ export class KubeObjectStatusIcon extends React.Component( + NonInjectedKubeObjectStatusIcon, + + { + getProps: (di, props) => ({ + statuses: di.inject(statusesForKubeObjectInjectable, props.object), + ...props, + }), + }, +); diff --git a/src/renderer/components/kube-object-status-icon/kube-object-status-registration.ts b/src/renderer/components/kube-object-status-icon/kube-object-status-registration.ts new file mode 100644 index 0000000000..17b8f86196 --- /dev/null +++ b/src/renderer/components/kube-object-status-icon/kube-object-status-registration.ts @@ -0,0 +1,12 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import type { KubeObject } from "../../../common/k8s-api/kube-object"; +import type { KubeObjectStatus } from "../../../extensions/renderer-api/kube-object-status"; + +export interface KubeObjectStatusRegistration { + kind: string; + apiVersions: string[]; + resolve: (object: KubeObject) => KubeObjectStatus; +} diff --git a/src/renderer/components/kube-object-status-icon/status-registrations.injectable.ts b/src/renderer/components/kube-object-status-icon/status-registrations.injectable.ts new file mode 100644 index 0000000000..c75aa85236 --- /dev/null +++ b/src/renderer/components/kube-object-status-icon/status-registrations.injectable.ts @@ -0,0 +1,23 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable"; +import { computed } from "mobx"; +import rendererExtensionsInjectable from "../../../extensions/renderer-extensions.injectable"; + +const statusRegistrationsInjectable = getInjectable({ + id: "status-registrations", + + instantiate: (di) => { + const extensions = di.inject(rendererExtensionsInjectable); + + return computed(() => + extensions.get().flatMap((extension) => extension.kubeObjectStatusTexts), + ); + }, + + lifecycle: lifecycleEnum.singleton, +}); + +export default statusRegistrationsInjectable; diff --git a/src/renderer/components/kube-object-status-icon/statuses-for-kube-object.injectable.ts b/src/renderer/components/kube-object-status-icon/statuses-for-kube-object.injectable.ts new file mode 100644 index 0000000000..d277dd04a6 --- /dev/null +++ b/src/renderer/components/kube-object-status-icon/statuses-for-kube-object.injectable.ts @@ -0,0 +1,35 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable"; +import statusRegistrationsInjectable from "./status-registrations.injectable"; +import type { KubeObject } from "../../../common/k8s-api/kube-object"; +import { conforms, eq, includes } from "lodash/fp"; +import type { KubeObjectStatusRegistration } from "./kube-object-status-registration"; + +const statusesForKubeObjectInjectable = getInjectable({ + id: "statuses-for-kube-object", + + instantiate: (di, kubeObject: KubeObject) => + di + .inject(statusRegistrationsInjectable) + .get() + .filter(toKubeObjectRelated(kubeObject)) + .map(toStatus(kubeObject)) + .filter(Boolean), + + lifecycle: lifecycleEnum.transient, +}); + +const toKubeObjectRelated = (kubeObject: KubeObject) => + conforms({ + kind: eq(kubeObject.kind), + apiVersions: includes(kubeObject.apiVersion), + }); + +const toStatus = + (kubeObject: KubeObject) => (item: KubeObjectStatusRegistration) => + item.resolve(kubeObject); + +export default statusesForKubeObjectInjectable; diff --git a/src/renderer/initializers/registries.ts b/src/renderer/initializers/registries.ts index 374e114b7c..2f19e758b5 100644 --- a/src/renderer/initializers/registries.ts +++ b/src/renderer/initializers/registries.ts @@ -12,6 +12,5 @@ export function initRegistries() { registries.EntitySettingRegistry.createInstance(); registries.GlobalPageRegistry.createInstance(); registries.KubeObjectDetailRegistry.createInstance(); - registries.KubeObjectStatusRegistry.createInstance(); registries.WorkloadsOverviewDetailRegistry.createInstance(); }