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

Expose a way to add cluster frame components in Extension API (#6385)

* Add cluster modals registrator

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

* Add ClusterModal components and injection token

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

* Add clusterModals tests

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

* Update snapshots and use css modules

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

* Linter fixes

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

* Setting 0 height as an inline style

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

* Update snapshots

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

* Do not export clusterModalsInjectionToken to extensions

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

* Testing changing visibility flag

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

* Linter fix

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

* Refactor cluster modals registrator and injectable

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

* Linter fixes

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

* Harder linter fix

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

* Fix linter again

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

* Using clusterFrameChildComponentsInjectionToken

for specific extension elements

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

* Removing unused files

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

* Removing unused modal registration

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

* Improving tests

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

* Fix linting

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Update snapshots

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Rename test suite for consistency

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

Signed-off-by: Alex Andreev <alex.andreev.email@gmail.com>
Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>
Co-authored-by: Janne Savolainen <janne.savolainen@live.fi>
This commit is contained in:
Alex Andreev 2022-11-04 09:15:16 +03:00 committed by GitHub
parent 36ee4d8289
commit c527014011
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 650 additions and 8 deletions

View File

@ -28,11 +28,13 @@ import type { LensRendererExtensionDependencies } from "./lens-extension-set-dep
import type { KubeObjectHandlerRegistration } from "../renderer/kube-object/handler"; import type { KubeObjectHandlerRegistration } from "../renderer/kube-object/handler";
import type { AppPreferenceTabRegistration } from "../features/preferences/renderer/compliance-for-legacy-extension-api/app-preference-tab-registration"; import type { AppPreferenceTabRegistration } from "../features/preferences/renderer/compliance-for-legacy-extension-api/app-preference-tab-registration";
import type { KubeObjectDetailRegistration } from "../renderer/components/kube-object-details/kube-object-detail-registration"; import type { KubeObjectDetailRegistration } from "../renderer/components/kube-object-details/kube-object-detail-registration";
import type { ClusterFrameChildComponent } from "../renderer/frames/cluster-frame/cluster-frame-child-component-injection-token";
export class LensRendererExtension extends LensExtension<LensRendererExtensionDependencies> { export class LensRendererExtension extends LensExtension<LensRendererExtensionDependencies> {
globalPages: registries.PageRegistration[] = []; globalPages: registries.PageRegistration[] = [];
clusterPages: registries.PageRegistration[] = []; clusterPages: registries.PageRegistration[] = [];
clusterPageMenus: registries.ClusterPageMenuRegistration[] = []; clusterPageMenus: registries.ClusterPageMenuRegistration[] = [];
clusterFrameComponents: ClusterFrameChildComponent[] = [];
kubeObjectStatusTexts: KubeObjectStatusRegistration[] = []; kubeObjectStatusTexts: KubeObjectStatusRegistration[] = [];
appPreferences: AppPreferenceRegistration[] = []; appPreferences: AppPreferenceRegistration[] = [];
appPreferenceTabs: AppPreferenceTabRegistration[] = []; appPreferenceTabs: AppPreferenceTabRegistration[] = [];

View File

@ -0,0 +1,514 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`legacy extension adding cluster frame components given custom components for cluster view available renders 1`] = `
<div>
<div
class="Notifications flex column align-flex-end"
/>
<div
class="mainLayout"
style="--sidebar-width: 200px;"
>
<div
class="sidebar"
>
<div
class="flex flex-col"
data-testid="cluster-sidebar"
>
<div
class="SidebarCluster"
>
<div
class="Avatar rounded loadingAvatar"
style="width: 40px; height: 40px;"
>
??
</div>
<div
class="loadingClusterName"
/>
</div>
<div
class="sidebarNav sidebar-active-status"
>
<div
class="SidebarItem"
data-is-active-test="true"
data-testid="sidebar-item-workloads"
>
<a
aria-current="page"
class="navItem active"
data-testid="sidebar-item-link-for-workloads"
href="/"
>
<i
class="Icon svg focusable"
>
<span
class="icon"
/>
</i>
<span>
Workloads
</span>
<i
class="Icon expandIcon material focusable"
>
<span
class="icon"
data-icon-name="keyboard_arrow_down"
>
keyboard_arrow_down
</span>
</i>
</a>
</div>
<div
class="SidebarItem"
data-is-active-test="false"
data-testid="sidebar-item-config"
>
<a
class="navItem"
data-testid="sidebar-item-link-for-config"
href="/"
>
<i
class="Icon material focusable"
>
<span
class="icon"
data-icon-name="list"
>
list
</span>
</i>
<span>
Config
</span>
</a>
</div>
<div
class="SidebarItem"
data-is-active-test="false"
data-testid="sidebar-item-network"
>
<a
class="navItem"
data-testid="sidebar-item-link-for-network"
href="/"
>
<i
class="Icon material focusable"
>
<span
class="icon"
data-icon-name="device_hub"
>
device_hub
</span>
</i>
<span>
Network
</span>
<i
class="Icon expandIcon material focusable"
>
<span
class="icon"
data-icon-name="keyboard_arrow_down"
>
keyboard_arrow_down
</span>
</i>
</a>
</div>
<div
class="SidebarItem"
data-is-active-test="false"
data-testid="sidebar-item-storage"
>
<a
class="navItem"
data-testid="sidebar-item-link-for-storage"
href="/"
>
<i
class="Icon material focusable"
>
<span
class="icon"
data-icon-name="storage"
>
storage
</span>
</i>
<span>
Storage
</span>
</a>
</div>
<div
class="SidebarItem"
data-is-active-test="false"
data-testid="sidebar-item-helm"
>
<a
class="navItem"
data-testid="sidebar-item-link-for-helm"
href="/"
>
<i
class="Icon svg focusable"
>
<span
class="icon"
/>
</i>
<span>
Helm
</span>
<i
class="Icon expandIcon material focusable"
>
<span
class="icon"
data-icon-name="keyboard_arrow_down"
>
keyboard_arrow_down
</span>
</i>
</a>
</div>
<div
class="SidebarItem"
data-is-active-test="false"
data-testid="sidebar-item-user-management"
>
<a
class="navItem"
data-testid="sidebar-item-link-for-user-management"
href="/"
>
<i
class="Icon material focusable"
>
<span
class="icon"
data-icon-name="security"
>
security
</span>
</i>
<span>
Access Control
</span>
</a>
</div>
<div
class="SidebarItem"
data-is-active-test="false"
data-testid="sidebar-item-custom-resources"
>
<a
class="navItem"
data-testid="sidebar-item-link-for-custom-resources"
href="/"
>
<i
class="Icon material focusable"
>
<span
class="icon"
data-icon-name="extension"
>
extension
</span>
</i>
<span>
Custom Resources
</span>
<i
class="Icon expandIcon material focusable"
>
<span
class="icon"
data-icon-name="keyboard_arrow_down"
>
keyboard_arrow_down
</span>
</i>
</a>
</div>
</div>
</div>
<div
class="ResizingAnchor horizontal trailing"
/>
</div>
<div
class="contents"
>
<div
class="TabLayout"
data-testid="tab-layout"
>
<div
class="Tabs center scrollable"
>
<div
class="Tab flex gaps align-center active"
data-is-active-test="true"
data-testid="tab-link-for-overview"
role="tab"
tabindex="0"
>
<div
class="label"
>
Overview
</div>
</div>
</div>
<main>
<div
class="WorkloadsOverview flex column gaps"
data-testid="page-for-workloads-overview"
>
<div
class="header flex gaps align-center"
>
<h5
class="box grow"
>
Overview
</h5>
<div
class="NamespaceSelectFilterParent"
data-testid="namespace-select-filter"
>
<div
class="Select theme-dark NamespaceSelect NamespaceSelectFilter css-b62m3t-container"
>
<span
class="css-1f43avz-a11yText-A11yText"
id="react-select-overview-namespace-select-filter-input-live-region"
/>
<span
aria-atomic="false"
aria-live="polite"
aria-relevant="additions text"
class="css-1f43avz-a11yText-A11yText"
/>
<div
class="Select__control css-1s2u09g-control"
>
<div
class="Select__value-container Select__value-container--is-multi css-319lph-ValueContainer"
>
<div
class="Select__placeholder css-14el2xx-placeholder"
id="react-select-overview-namespace-select-filter-input-placeholder"
>
All namespaces
</div>
<div
class="Select__input-container css-6j8wv5-Input"
data-value=""
>
<input
aria-autocomplete="list"
aria-describedby="react-select-overview-namespace-select-filter-input-placeholder"
aria-expanded="false"
aria-haspopup="true"
autocapitalize="none"
autocomplete="off"
autocorrect="off"
class="Select__input"
id="overview-namespace-select-filter-input"
role="combobox"
spellcheck="false"
style="opacity: 1; width: 100%; grid-area: 1 / 2; min-width: 2px; border: 0px; margin: 0px; outline: 0; padding: 0px;"
tabindex="0"
type="text"
value=""
/>
</div>
</div>
<div
class="Select__indicators css-1hb7zxy-IndicatorsContainer"
>
<span
class="Select__indicator-separator css-1okebmr-indicatorSeparator"
/>
<div
aria-hidden="true"
class="Select__indicator Select__dropdown-indicator css-tlfecz-indicatorContainer"
>
<svg
aria-hidden="true"
class="css-tj5bde-Svg"
focusable="false"
height="20"
viewBox="0 0 20 20"
width="20"
>
<path
d="M4.516 7.548c0.436-0.446 1.043-0.481 1.576 0l3.908 3.747 3.908-3.747c0.533-0.481 1.141-0.446 1.574 0 0.436 0.445 0.408 1.197 0 1.615-0.406 0.418-4.695 4.502-4.695 4.502-0.217 0.223-0.502 0.335-0.787 0.335s-0.57-0.112-0.789-0.335c0 0-4.287-4.084-4.695-4.502s-0.436-1.17 0-1.615z"
/>
</svg>
</div>
</div>
</div>
</div>
</div>
</div>
<div
class="OverviewStatuses"
>
<div
class="workloads"
/>
</div>
</div>
</main>
</div>
</div>
<div
class="footer"
>
<div
class="Dock"
tabindex="-1"
>
<div
class="ResizingAnchor vertical leading"
/>
<div
class="tabs-container flex align-center"
>
<div
class="dockTabs"
role="tablist"
>
<div
class="Tabs tabs"
>
<div
class="Tab flex gaps align-center DockTab TerminalTab active"
data-testid="dock-tab-for-terminal"
id="tab-terminal"
role="tab"
tabindex="0"
>
<i
class="Icon material focusable"
>
<span
class="icon"
data-icon-name="terminal"
>
terminal
</span>
</i>
<div
class="label"
>
<div
class="flex align-center"
>
<span
class="title"
>
Terminal
</span>
<div
class="close"
>
<i
class="Icon material interactive focusable small"
data-testid="dock-tab-close-for-terminal"
tabindex="0"
>
<span
class="icon"
data-icon-name="close"
>
close
</span>
</i>
<div
data-testid="tooltip-content-for-dock-tab-close-for-terminal"
>
Close ⌘+W
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div
class="toolbar flex gaps align-center box grow"
>
<div
class="dock-menu box grow"
>
<i
class="Icon new-dock-tab material interactive focusable"
id="menu-actions-for-dock"
tabindex="0"
>
<span
class="icon"
data-icon-name="add"
>
add
</span>
</i>
<div>
New tab
</div>
</div>
<i
class="Icon material interactive focusable"
tabindex="0"
>
<span
class="icon"
data-icon-name="fullscreen"
>
fullscreen
</span>
</i>
<div>
Fit to window
</div>
<i
class="Icon material interactive focusable"
tabindex="0"
>
<span
class="icon"
data-icon-name="keyboard_arrow_up"
>
keyboard_arrow_up
</span>
</i>
<div>
Open
</div>
</div>
</div>
</div>
</div>
</div>
<div
data-testid="test-modal"
>
test modal
</div>
</div>
`;

View File

@ -0,0 +1,78 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import type { RenderResult } from "@testing-library/react";
import { act } from "@testing-library/react";
import type { IObservableValue } from "mobx";
import { computed, observable, runInAction } from "mobx";
import React from "react";
import type { ApplicationBuilder } from "../../renderer/components/test-utils/get-application-builder";
import { getApplicationBuilder } from "../../renderer/components/test-utils/get-application-builder";
describe("legacy extension adding cluster frame components", () => {
let builder: ApplicationBuilder;
let rendered: RenderResult;
beforeEach(() => {
builder = getApplicationBuilder();
builder.setEnvironmentToClusterFrame();
});
describe("given custom components for cluster view available", () => {
let someObservable: IObservableValue<boolean>;
beforeEach(async () => {
someObservable = observable.box(false);
const testExtension = {
id: "some-extension-id",
name: "some-extension-name",
rendererOptions: {
clusterFrameComponents: [
{
id: "test-modal-id",
Component: () => <div data-testid="test-modal">test modal</div>,
shouldRender: computed(() => true),
},
{
id: "dialog-with-observable-visibility-id",
Component: () => <div data-testid="dialog-with-observable-visibility">dialog contents</div>,
shouldRender: computed(() => someObservable.get()),
},
],
},
};
rendered = await builder.render();
builder.extensions.enable(testExtension);
});
it("renders", () => {
expect(rendered.container).toMatchSnapshot();
});
it("renders provided component html", () => {
const modal = rendered.getByTestId("test-modal");
expect(modal).toBeInTheDocument();
});
it("doesn't render component which should be invisible", () => {
const dialog = rendered.queryByTestId("dialog-with-observable-visibility");
expect(dialog).not.toBeInTheDocument();
});
it("when injectable component becomes visible, shows it", () => {
runInAction(() => {
act(() => someObservable.set(true));
});
const dialog = rendered.getByTestId("dialog-with-observable-visibility");
expect(dialog).toBeInTheDocument();
});
});
});

View File

@ -0,0 +1,42 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { pipeline } from "@ogre-tools/fp";
import { getInjectable } from "@ogre-tools/injectable";
import { map } from "lodash/fp";
import { extensionRegistratorInjectionToken } from "../../../extensions/extension-loader/extension-registrator-injection-token";
import type { ExtensionRegistrator } from "../../../extensions/extension-loader/extension-registrator-injection-token";
import type { LensRendererExtension } from "../../../extensions/lens-renderer-extension";
import { clusterFrameChildComponentInjectionToken } from "./cluster-frame-child-component-injection-token";
const clusterFrameComponentRegistratorInjectable = getInjectable({
id: "cluster-frame-component-registrator",
instantiate: (): ExtensionRegistrator => {
return (ext) => {
const extension = ext as LensRendererExtension;
return pipeline(
extension.clusterFrameComponents,
map((clusterFrameComponentRegistration) => {
const id = `${extension.sanitizedExtensionId}-${clusterFrameComponentRegistration.id}`;
return getInjectable({
id,
injectionToken: clusterFrameChildComponentInjectionToken,
instantiate: () => ({
id,
shouldRender: clusterFrameComponentRegistration.shouldRender,
Component: clusterFrameComponentRegistration.Component,
}),
});
}),
);
};
},
injectionToken: extensionRegistratorInjectionToken,
});
export default clusterFrameComponentRegistratorInjectable;

View File

@ -14,11 +14,13 @@ import subscribeStoresInjectable from "../../kube-watch-api/subscribe-stores.inj
import type { ClusterFrameChildComponent } from "./cluster-frame-child-component-injection-token"; import type { ClusterFrameChildComponent } from "./cluster-frame-child-component-injection-token";
import { clusterFrameChildComponentInjectionToken } from "./cluster-frame-child-component-injection-token"; import { clusterFrameChildComponentInjectionToken } from "./cluster-frame-child-component-injection-token";
import watchHistoryStateInjectable from "../../remote-helpers/watch-history-state.injectable"; import watchHistoryStateInjectable from "../../remote-helpers/watch-history-state.injectable";
import { computedInjectManyInjectable } from "@ogre-tools/injectable-extension-for-mobx";
import type { IComputedValue } from "mobx";
interface Dependencies { interface Dependencies {
namespaceStore: NamespaceStore; namespaceStore: NamespaceStore;
subscribeStores: SubscribeStores; subscribeStores: SubscribeStores;
childComponents: ClusterFrameChildComponent[]; childComponents: IComputedValue<ClusterFrameChildComponent[]>;
watchHistoryState: () => () => void; watchHistoryState: () => () => void;
} }
@ -37,7 +39,7 @@ export const NonInjectedClusterFrame = observer(({
return ( return (
<ErrorBoundary> <ErrorBoundary>
{childComponents {childComponents.get()
.map((child) => ( .map((child) => (
<Observer key={child.id}> <Observer key={child.id}>
{() => (child.shouldRender.get() ? <child.Component /> : null) } {() => (child.shouldRender.get() ? <child.Component /> : null) }
@ -48,12 +50,16 @@ export const NonInjectedClusterFrame = observer(({
}); });
export const ClusterFrame = withInjectables<Dependencies>(NonInjectedClusterFrame, { export const ClusterFrame = withInjectables<Dependencies>(NonInjectedClusterFrame, {
getProps: di => ({ getProps: di => {
namespaceStore: di.inject(namespaceStoreInjectable), const computedInjectMany = di.inject(computedInjectManyInjectable);
subscribeStores: di.inject(subscribeStoresInjectable),
childComponents: di.injectMany(clusterFrameChildComponentInjectionToken), return {
watchHistoryState: di.inject(watchHistoryStateInjectable), namespaceStore: di.inject(namespaceStoreInjectable),
}), subscribeStores: di.inject(subscribeStoresInjectable),
childComponents: computedInjectMany(clusterFrameChildComponentInjectionToken),
watchHistoryState: di.inject(watchHistoryStateInjectable),
};
},
}); });
ClusterFrame.displayName = "ClusterFrame"; ClusterFrame.displayName = "ClusterFrame";