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

chore: Simplify extension dependency injection

- Has better typing
- Removes use of unnusual unique symbol
- Fix welcome banner tests
    - Update associated snapshots
- Start converting custom column tests to use ApplicationBuilder
- Remove old and unnused RecursiveTreeView
- Introduce new TreeView for use in CatalogMenu to fix tests

Signed-off-by: Sebastian Malton <sebastian@malton.name>
This commit is contained in:
Sebastian Malton 2023-02-15 09:04:11 -05:00
parent 5db8fc1342
commit 82bf67cc9e
70 changed files with 7110 additions and 2945 deletions

View File

@ -201,7 +201,7 @@ export abstract class LensProtocolRouter {
return name;
}
const extension = extensionLoader.getInstanceByName(name);
const extension = extensionLoader.getInstanceByName(name) as LensExtension | undefined;
if (!extension) {
this.dependencies.logger.info(`${LensProtocolRouter.LoggingPrefix}: Extension ${name} matched, but does not have a class for ${ipcRenderer ? "renderer" : "main"}`);

View File

@ -3,17 +3,23 @@
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { LensExtension } from "../lens-extension";
import { Console } from "console";
import { stdout, stderr } from "process";
console = new Console(stdout, stderr);
let ext: LensExtension;
import { getApplicationBuilder } from "../../renderer/components/test-utils/get-application-builder";
import { LensMainExtension } from "../lens-main-extension";
describe("lens extension", () => {
let ext: LensMainExtension;
beforeEach(async () => {
ext = new LensExtension({
const builder = getApplicationBuilder();
/**
* This is required because it sets up `AppPaths` which are required by LensMainExtension.
*
* That type isn't used internally so it needs to use "legacy global DI" to get its dependencies.
*/
await builder.render();
ext = new LensMainExtension({
manifest: {
name: "foo-bar",
version: "0.1.1",

View File

@ -1,17 +0,0 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import type { LensExtensionConstructor, BundledInstalledExtension, ExternalInstalledExtension, BundledLensExtensionConstructor } from "@k8slens/legacy-extensions";
import { getInjectionToken } from "@ogre-tools/injectable";
import type { LensExtension } from "../lens-extension";
export interface CreateExtensionInstance {
(ExtensionClass: LensExtensionConstructor, extension: ExternalInstalledExtension): LensExtension;
(ExtensionClass: BundledLensExtensionConstructor, extension: BundledInstalledExtension): LensExtension;
}
export const createExtensionInstanceInjectionToken = getInjectionToken<CreateExtensionInstance>({
id: "create-extension-instance-token",
});

View File

@ -2,14 +2,13 @@
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import type { LensExtensionId } from "@k8slens/legacy-extensions";
import type { LegacyLensExtension, LensExtensionId } from "@k8slens/legacy-extensions";
import { getInjectable } from "@ogre-tools/injectable";
import { observable } from "mobx";
import type { LensExtension } from "../lens-extension";
const extensionInstancesInjectable = getInjectable({
id: "extension-instances",
instantiate: () => observable.map<LensExtensionId, LensExtension>(),
instantiate: () => observable.map<LensExtensionId, LegacyLensExtension>(),
});
export default extensionInstancesInjectable;

View File

@ -4,9 +4,7 @@
*/
import { getInjectable } from "@ogre-tools/injectable";
import { ExtensionLoader } from "./extension-loader";
import { createExtensionInstanceInjectionToken } from "./create-extension-instance.token";
import extensionInstancesInjectable from "./extension-instances.injectable";
import type { LensExtension } from "../lens-extension";
import extensionInjectable from "./extension/extension.injectable";
import loggerInjectable from "../../common/logger.injectable";
import joinPathsInjectable from "../../common/path/join-paths.injectable";
@ -20,9 +18,8 @@ const extensionLoaderInjectable = getInjectable({
instantiate: (di) => new ExtensionLoader({
updateExtensionsState: di.inject(updateExtensionsStateInjectable),
createExtensionInstance: di.inject(createExtensionInstanceInjectionToken),
extensionInstances: di.inject(extensionInstancesInjectable),
getExtension: (instance: LensExtension) => di.inject(extensionInjectable, instance),
getExtension: (instance) => di.inject(extensionInjectable, instance),
bundledExtensions: di.injectMany(bundledExtensionInjectionToken),
extensionEntryPointName: di.inject(extensionEntryPointNameInjectionToken),
logger: di.inject(loggerInjectable),

View File

@ -9,13 +9,12 @@ import type { ObservableMap } from "mobx";
import { runInAction, action, computed, toJS, observable, reaction, when } from "mobx";
import { broadcastMessage, ipcMainOn, ipcRendererOn, ipcMainHandle } from "../../common/ipc";
import { isDefined, iter } from "@k8slens/utilities";
import type { ExternalInstalledExtension, InstalledExtension, LensExtensionConstructor, LensExtensionId, BundledExtension } from "@k8slens/legacy-extensions";
import type { ExternalInstalledExtension, InstalledExtension, LensExtensionConstructor, LensExtensionId, BundledExtension, BundledInstalledExtension, LegacyLensExtension } from "@k8slens/legacy-extensions";
import type { LensExtension } from "../lens-extension";
import { extensionLoaderFromMainChannel, extensionLoaderFromRendererChannel } from "../../common/ipc/extension-handling";
import { requestExtensionLoaderInitialState } from "../../renderer/ipc";
import assert from "assert";
import { EventEmitter } from "../../common/event-emitter";
import type { CreateExtensionInstance } from "./create-extension-instance.token";
import type { Extension } from "./extension/extension.injectable";
import type { Logger } from "../../common/logger";
import type { JoinPaths } from "../../common/path/join-paths.injectable";
@ -25,13 +24,12 @@ import type { UpdateExtensionsState } from "../../features/extensions/enabled/co
const logModule = "[EXTENSIONS-LOADER]";
interface Dependencies {
readonly extensionInstances: ObservableMap<LensExtensionId, LensExtension>;
readonly extensionInstances: ObservableMap<LensExtensionId, LegacyLensExtension>;
readonly bundledExtensions: BundledExtension[];
readonly logger: Logger;
readonly extensionEntryPointName: "main" | "renderer";
updateExtensionsState: UpdateExtensionsState;
createExtensionInstance: CreateExtensionInstance;
getExtension: (instance: LensExtension) => Extension;
getExtension: (instance: LegacyLensExtension) => Extension;
joinPaths: JoinPaths;
getDirnameOfPath: GetDirnameOfPath;
}
@ -85,7 +83,7 @@ export class ExtensionLoader {
* - `null` if no class definition is provided for the current process
* - `undefined` if the name is not known about
*/
getInstanceByName(name: string): LensExtension | null | undefined {
getInstanceByName(name: string): LegacyLensExtension | null | undefined {
if (this.nonInstancesByName.has(name)) {
return null;
}
@ -236,7 +234,7 @@ export class ExtensionLoader {
return null;
}
const installedExtension: InstalledExtension = {
const installedExtension: BundledInstalledExtension = {
absolutePath: "irrelevant",
id: extension.manifest.name,
isBundled: true,
@ -245,10 +243,7 @@ export class ExtensionLoader {
manifest: extension.manifest,
manifestPath: "irrelevant",
};
const instance = this.dependencies.createExtensionInstance(
LensExtensionClass,
installedExtension,
);
const instance = new LensExtensionClass(installedExtension);
this.dependencies.extensionInstances.set(extension.manifest.name, instance);
@ -307,35 +302,32 @@ export class ExtensionLoader {
return [...installedExtensions.entries()]
.filter((entry): entry is [string, ExternalInstalledExtension] => !entry[1].isBundled)
.map(([extId, extension]) => {
const alreadyInit = this.dependencies.extensionInstances.has(extId) || this.nonInstancesByName.has(extension.manifest.name);
.map(([extId, installedExtension]) => {
const alreadyInit = this.dependencies.extensionInstances.has(extId) || this.nonInstancesByName.has(installedExtension.manifest.name);
if (extension.isCompatible && extension.isEnabled && !alreadyInit) {
if (installedExtension.isCompatible && installedExtension.isEnabled && !alreadyInit) {
try {
const LensExtensionClass = this.requireExtension(extension);
const LensExtensionClass = this.requireExtension(installedExtension);
if (!LensExtensionClass) {
this.nonInstancesByName.add(extension.manifest.name);
this.nonInstancesByName.add(installedExtension.manifest.name);
return null;
}
const instance = this.dependencies.createExtensionInstance(
LensExtensionClass,
extension,
);
const instance = new LensExtensionClass(installedExtension);
this.dependencies.extensionInstances.set(extId, instance);
return {
instance,
installedExtension: extension,
installedExtension,
activated: instance.activate(),
} as ExtensionBeingActivated;
} catch (err) {
this.dependencies.logger.error(`${logModule}: error loading extension`, { ext: extension, err });
this.dependencies.logger.error(`${logModule}: error loading extension`, { ext: installedExtension, err });
}
} else if (!extension.isEnabled && alreadyInit) {
} else if (!installedExtension.isEnabled && alreadyInit) {
this.removeInstance(extId);
}

View File

@ -2,13 +2,14 @@
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import type { LegacyLensExtension } from "@k8slens/legacy-extensions";
import type { Injectable } from "@ogre-tools/injectable";
import { getInjectionToken } from "@ogre-tools/injectable";
import type { IComputedValue } from "mobx";
import type { LensExtension } from "../lens-extension";
export type ExtensionRegistrator = (extension: LensExtension) =>
Injectable<any, any, any>[] | IComputedValue<Injectable<any, any, any>[]>;
export type Injectables = Injectable<any, any, any>[];
export type Registration = Injectables | IComputedValue<Injectables>;
export type ExtensionRegistrator = (extension: LegacyLensExtension) => Registration;
export const extensionRegistratorInjectionToken = getInjectionToken<ExtensionRegistrator>({
id: "extension-registrator-token",

View File

@ -5,9 +5,9 @@
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
import { reaction, runInAction } from "mobx";
import { disposer } from "@k8slens/utilities";
import type { LensExtension } from "../../lens-extension";
import { extensionRegistratorInjectionToken } from "../extension-registrator-injection-token";
import { injectableDifferencingRegistratorWith } from "../../../common/utils/registrator-helper";
import type { LegacyLensExtension } from "@k8slens/legacy-extensions";
export interface Extension {
register: () => void;
@ -17,7 +17,7 @@ export interface Extension {
const extensionInjectable = getInjectable({
id: "extension",
instantiate: (parentDi, instance: LensExtension): Extension => {
instantiate: (parentDi, instance): Extension => {
const extensionInjectable = getInjectable({
id: `extension-${instance.sanitizedExtensionId}`,
@ -66,7 +66,7 @@ const extensionInjectable = getInjectable({
},
lifecycle: lifecycleEnum.keyedSingleton({
getInstanceKey: (di, instance: LensExtension) => instance,
getInstanceKey: (di, instance: LegacyLensExtension) => instance,
}),
});

View File

@ -2,16 +2,23 @@
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { iter } from "@k8slens/utilities";
import { getInjectable } from "@ogre-tools/injectable";
import { computed } from "mobx";
import isExtensionEnabledInjectable from "../features/extensions/enabled/common/is-enabled.injectable";
import extensionInstancesInjectable from "./extension-loader/extension-instances.injectable";
const extensionsInjectable = getInjectable({
id: "extensions",
instantiate: (di) => {
const extensionInstances = di.inject(extensionInstancesInjectable);
const isExtensionEnabled = di.inject(isExtensionEnabledInjectable);
return computed(() => [...extensionInstances.values()].filter(extension => extension.isEnabled));
return computed(() => (
iter.chain(extensionInstances.values())
.filter(extension => extension.isBundled || isExtensionEnabled(extension.id))
.toArray()
));
},
});

View File

@ -4,16 +4,31 @@
*/
import { ipcMain } from "electron";
import { IpcPrefix, IpcRegistrar } from "./ipc-registrar";
import { Disposers, lensExtensionDependencies } from "../lens-extension";
import { Disposers } from "../lens-extension";
import type { LensMainExtension } from "../lens-main-extension";
import type { Disposer } from "@k8slens/utilities";
import { once } from "lodash";
import { ipcMainHandle } from "../../common/ipc";
import type { Logger } from "../common-api";
import { getEnvironmentSpecificLegacyGlobalDiForExtensionApi } from "../as-legacy-globals-for-extension-api/legacy-global-di-for-extension-api";
import loggerInjectable from "../../common/logger.injectable";
interface Dependencies {
readonly logger: Logger;
}
export abstract class IpcMain extends IpcRegistrar {
private readonly dependencies: Dependencies;
constructor(extension: LensMainExtension) {
super(extension);
const di = getEnvironmentSpecificLegacyGlobalDiForExtensionApi("main");
this.dependencies = {
logger: di.inject(loggerInjectable),
};
// Call the static method on the bottom child class.
extension[Disposers].push(() => (this.constructor as typeof IpcMain).resetInstance());
}
@ -27,12 +42,12 @@ export abstract class IpcMain extends IpcRegistrar {
listen(channel: string, listener: (event: Electron.IpcRendererEvent, ...args: any[]) => any): Disposer {
const prefixedChannel = `extensions@${this[IpcPrefix]}:${channel}`;
const cleanup = once(() => {
this.extension[lensExtensionDependencies].logger.debug(`[IPC-RENDERER]: removing extension listener`, { channel, extension: { name: this.extension.name, version: this.extension.version }});
this.dependencies.logger.debug(`[IPC-RENDERER]: removing extension listener`, { channel, extension: { name: this.extension.name, version: this.extension.version }});
return ipcMain.removeListener(prefixedChannel, listener);
});
this.extension[lensExtensionDependencies].logger.debug(`[IPC-RENDERER]: adding extension listener`, { channel, extension: { name: this.extension.name, version: this.extension.version }});
this.dependencies.logger.debug(`[IPC-RENDERER]: adding extension listener`, { channel, extension: { name: this.extension.name, version: this.extension.version }});
ipcMain.addListener(prefixedChannel, listener);
this.extension[Disposers].push(cleanup);
@ -47,10 +62,10 @@ export abstract class IpcMain extends IpcRegistrar {
handle(channel: string, handler: (event: Electron.IpcMainInvokeEvent, ...args: any[]) => any): void {
const prefixedChannel = `extensions@${this[IpcPrefix]}:${channel}`;
this.extension[lensExtensionDependencies].logger.debug(`[IPC-RENDERER]: adding extension handler`, { channel, extension: { name: this.extension.name, version: this.extension.version }});
this.dependencies.logger.debug(`[IPC-RENDERER]: adding extension handler`, { channel, extension: { name: this.extension.name, version: this.extension.version }});
ipcMainHandle(prefixedChannel, handler);
this.extension[Disposers].push(() => {
this.extension[lensExtensionDependencies].logger.debug(`[IPC-RENDERER]: removing extension handler`, { channel, extension: { name: this.extension.name, version: this.extension.version }});
this.dependencies.logger.debug(`[IPC-RENDERER]: removing extension handler`, { channel, extension: { name: this.extension.name, version: this.extension.version }});
return ipcMain.removeHandler(prefixedChannel);
});

View File

@ -1,33 +0,0 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import type { IComputedValue } from "mobx";
import type { CatalogCategoryRegistry } from "../common/catalog";
import type { NavigateToRoute } from "../common/front-end-routing/navigate-to-route-injection-token";
import type { Route } from "../common/front-end-routing/front-end-route-injection-token";
import type { CatalogEntityRegistry as MainCatalogEntityRegistry } from "../main/catalog";
import type { CatalogEntityRegistry as RendererCatalogEntityRegistry } from "../renderer/api/catalog/entity/registry";
import type { GetExtensionPageParameters } from "../renderer/routes/get-extension-page-parameters.injectable";
import type { NavigateForExtension } from "../main/start-main-application/lens-window/navigate-for-extension.injectable";
import type { Logger } from "../common/logger";
import type { EnsureHashedDirectoryForExtension } from "./extension-loader/file-system-provisioner-store/ensure-hashed-directory-for-extension.injectable";
export interface LensExtensionDependencies {
readonly logger: Logger;
ensureHashedDirectoryForExtension: EnsureHashedDirectoryForExtension;
}
export interface LensMainExtensionDependencies extends LensExtensionDependencies {
readonly entityRegistry: MainCatalogEntityRegistry;
readonly navigate: NavigateForExtension;
}
export interface LensRendererExtensionDependencies extends LensExtensionDependencies {
navigateToRoute: NavigateToRoute;
getExtensionPageParameters: GetExtensionPageParameters;
readonly routes: IComputedValue<Route<unknown>[]>;
readonly entityRegistry: RendererCatalogEntityRegistry;
readonly categoryRegistry: CatalogCategoryRegistry;
}

View File

@ -5,19 +5,19 @@
import { action, computed, makeObservable, observable } from "mobx";
import { disposer } from "@k8slens/utilities";
import type { LensExtensionDependencies } from "./lens-extension-set-dependencies";
import type { ProtocolHandlerRegistration } from "../common/protocol-handler/registration";
import type { InstalledExtension, LegacyLensExtension, LensExtensionId, LensExtensionManifest } from "@k8slens/legacy-extensions";
import type { InstalledExtension, LensExtensionId, LensExtensionManifest } from "@k8slens/legacy-extensions";
import type { Logger } from "./common-api";
import type { EnsureHashedDirectoryForExtension } from "./extension-loader/file-system-provisioner-store/ensure-hashed-directory-for-extension.injectable";
export const lensExtensionDependencies = Symbol("lens-extension-dependencies");
export const Disposers = Symbol("disposers");
export class LensExtension<
/**
* @ignore
*/
Dependencies extends LensExtensionDependencies = LensExtensionDependencies,
> implements LegacyLensExtension {
export interface LensExtensionDependencies {
readonly logger: Logger;
ensureHashedDirectoryForExtension: EnsureHashedDirectoryForExtension;
}
export class LensExtension {
readonly id: LensExtensionId;
readonly manifest: LensExtensionManifest;
readonly manifestPath: string;
@ -27,6 +27,11 @@ export class LensExtension<
return sanitizeExtensionName(this.name);
}
/**
* @ignore
*/
protected readonly dependencies: LensExtensionDependencies;
protocolHandlers: ProtocolHandlerRegistration[] = [];
@observable private _isEnabled = false;
@ -40,12 +45,12 @@ export class LensExtension<
*/
[Disposers] = disposer();
constructor({ id, manifest, manifestPath, isBundled }: InstalledExtension) {
// id is the name of the manifest
constructor(deps: LensExtensionDependencies, { id, manifest, manifestPath, isBundled }: InstalledExtension) {
this.dependencies = deps;
this.id = id;
this.manifest = manifest as LensExtensionManifest;
this.manifestPath = manifestPath;
this.isBundled = !!isBundled;
this.isBundled = isBundled;
makeObservable(this);
}
@ -66,11 +71,6 @@ export class LensExtension<
return this.manifest.storeName || this.name;
}
/**
* @ignore
*/
readonly [lensExtensionDependencies]!: Dependencies;
/**
* getExtensionFileFolder returns the path to an already created folder. This
* folder is for the sole use of this extension.
@ -80,7 +80,7 @@ export class LensExtension<
*/
async getExtensionFileFolder(): Promise<string> {
// storeName is read from the manifest and has a fallback to the manifest name, which equals id
return this[lensExtensionDependencies].ensureHashedDirectoryForExtension(this.storeName);
return this.dependencies.ensureHashedDirectoryForExtension(this.storeName);
}
@action
@ -90,7 +90,7 @@ export class LensExtension<
}
this._isEnabled = true;
this[lensExtensionDependencies].logger.info(`[EXTENSION]: enabled ${this.name}@${this.version}`);
this.dependencies.logger.info(`[EXTENSION]: enabled ${this.name}@${this.version}`);
}
@action
@ -104,9 +104,9 @@ export class LensExtension<
try {
await this.onDeactivate();
this[Disposers]();
this[lensExtensionDependencies].logger.info(`[EXTENSION]: disabled ${this.name}@${this.version}`);
this.dependencies.logger.info(`[EXTENSION]: disabled ${this.name}@${this.version}`);
} catch (error) {
this[lensExtensionDependencies].logger.error(`[EXTENSION]: disabling ${this.name}@${this.version} threw an error: ${error}`);
this.dependencies.logger.error(`[EXTENSION]: disabling ${this.name}@${this.version} threw an error: ${error}`);
}
}

View File

@ -3,19 +3,49 @@
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { LensExtension, lensExtensionDependencies } from "./lens-extension";
import type { LensExtensionDependencies } from "./lens-extension";
import { LensExtension } from "./lens-extension";
import type { CatalogEntity } from "../common/catalog";
import type { IComputedValue, IObservableArray } from "mobx";
import { isObservableArray } from "mobx";
import type { MenuRegistration } from "../features/application-menu/main/menu-registration";
import type { TrayMenuRegistration } from "../main/tray/tray-menu-registration";
import type { ShellEnvModifier } from "../main/shell-session/shell-env-modifier/shell-env-modifier-registration";
import type { LensMainExtensionDependencies } from "./lens-extension-set-dependencies";
import { getEnvironmentSpecificLegacyGlobalDiForExtensionApi } from "./as-legacy-globals-for-extension-api/legacy-global-di-for-extension-api";
import type { InstalledExtension } from "./common-api";
import type { CatalogEntityRegistry } from "../main/catalog";
import type { NavigateForExtension } from "../main/start-main-application/lens-window/navigate-for-extension.injectable";
import catalogEntityRegistryInjectable from "../main/catalog/entity-registry.injectable";
import loggerInjectable from "../common/logger.injectable";
import navigateForExtensionInjectable from "../main/start-main-application/lens-window/navigate-for-extension.injectable";
import ensureHashedDirectoryForExtensionInjectable from "./extension-loader/file-system-provisioner-store/ensure-hashed-directory-for-extension.injectable";
export class LensMainExtension extends LensExtension<LensMainExtensionDependencies> {
interface LensMainExtensionDependencies extends LensExtensionDependencies {
readonly entityRegistry: CatalogEntityRegistry;
readonly navigate: NavigateForExtension;
}
export class LensMainExtension extends LensExtension {
appMenus: MenuRegistration[] | IComputedValue<MenuRegistration[]> = [];
trayMenus: TrayMenuRegistration[] | IComputedValue<TrayMenuRegistration[]> = [];
/**
* @ignore
*/
declare readonly dependencies: LensMainExtensionDependencies;
constructor(extension: InstalledExtension) {
const di = getEnvironmentSpecificLegacyGlobalDiForExtensionApi("main");
const deps: LensMainExtensionDependencies = {
ensureHashedDirectoryForExtension: di.inject(ensureHashedDirectoryForExtensionInjectable),
navigate: di.inject(navigateForExtensionInjectable),
entityRegistry: di.inject(catalogEntityRegistryInjectable),
logger: di.inject(loggerInjectable),
};
super(deps, extension);
}
/**
* implement this to modify the shell environment that Lens terminals are opened with. The ShellEnvModifier type has the signature
*
@ -32,18 +62,18 @@ export class LensMainExtension extends LensExtension<LensMainExtensionDependenci
terminalShellEnvModifier?: ShellEnvModifier;
async navigate(pageId?: string, params?: Record<string, any>, frameId?: number) {
await this[lensExtensionDependencies].navigate(this.id, pageId, params, frameId);
await this.dependencies.navigate(this.id, pageId, params, frameId);
}
addCatalogSource(id: string, source: IObservableArray<CatalogEntity> | IComputedValue<CatalogEntity[]>) {
if (isObservableArray(source)) {
this[lensExtensionDependencies].entityRegistry.addObservableSource(`${this.name}:${id}`, source);
this.dependencies.entityRegistry.addObservableSource(`${this.name}:${id}`, source);
} else {
this[lensExtensionDependencies].entityRegistry.addComputedSource(`${this.name}:${id}`, source);
this.dependencies.entityRegistry.addComputedSource(`${this.name}:${id}`, source);
}
}
removeCatalogSource(id: string) {
this[lensExtensionDependencies].entityRegistry.removeSource(`${this.name}:${id}`);
this.dependencies.entityRegistry.removeSource(`${this.name}:${id}`);
}
}

View File

@ -3,10 +3,11 @@
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { Disposers, LensExtension, lensExtensionDependencies } from "./lens-extension";
import type { CatalogEntity, CategoryFilter } from "../common/catalog";
import { Disposers, LensExtension } from "./lens-extension";
import type { Disposer } from "@k8slens/utilities";
import type { EntityFilter } from "../renderer/api/catalog/entity/registry";
import type { LensExtensionDependencies } from "./lens-extension";
import type { CatalogEntity, CategoryFilter, CatalogCategoryRegistry } from "../common/catalog";
import type { EntityFilter, CatalogEntityRegistry } from "../renderer/api/catalog/entity/registry";
import type { TopBarRegistration } from "../renderer/components/layout/top-bar/top-bar-registration";
import type { KubernetesCluster } from "../common/catalog-entities";
import type { WelcomeMenuRegistration } from "../renderer/components/+welcome/welcome-menu-items/welcome-menu-registration";
@ -22,7 +23,6 @@ import type { KubeObjectStatusRegistration } from "../renderer/components/kube-o
import { fromPairs, map, matches, toPairs } from "lodash/fp";
import { pipeline } from "@ogre-tools/fp";
import { getExtensionRoutePath } from "../renderer/routes/for-extension";
import type { LensRendererExtensionDependencies } from "./lens-extension-set-dependencies";
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 { KubeObjectDetailRegistration } from "../renderer/components/kube-object-details/kube-object-detail-registration";
@ -31,8 +31,29 @@ import type { EntitySettingRegistration } from "../renderer/components/+entity-s
import type { CatalogEntityDetailRegistration } from "../renderer/components/+catalog/entity-details/token";
import type { PageRegistration } from "../renderer/routes/page-registration";
import type { ClusterPageMenuRegistration } from "../renderer/components/layout/cluster-page-menu";
import type { IComputedValue } from "mobx";
import type { NavigateToRoute } from "../common/front-end-routing/navigate-to-route-injection-token";
import type { Route } from "../common/front-end-routing/front-end-route-injection-token";
import type { GetExtensionPageParameters } from "../renderer/routes/get-extension-page-parameters.injectable";
import type { InstalledExtension } from "./common-api";
import { getEnvironmentSpecificLegacyGlobalDiForExtensionApi } from "./as-legacy-globals-for-extension-api/legacy-global-di-for-extension-api";
import catalogCategoryRegistryInjectable from "../common/catalog/category-registry.injectable";
import catalogEntityRegistryInjectable from "../renderer/api/catalog/entity/registry.injectable";
import loggerInjectable from "../common/logger.injectable";
import getExtensionPageParametersInjectable from "../renderer/routes/get-extension-page-parameters.injectable";
import navigateToRouteInjectable from "../renderer/routes/navigate-to-route.injectable";
import routesInjectable from "../renderer/routes/routes.injectable";
import ensureHashedDirectoryForExtensionInjectable from "./extension-loader/file-system-provisioner-store/ensure-hashed-directory-for-extension.injectable";
export class LensRendererExtension extends LensExtension<LensRendererExtensionDependencies> {
interface LensRendererExtensionDependencies extends LensExtensionDependencies {
navigateToRoute: NavigateToRoute;
getExtensionPageParameters: GetExtensionPageParameters;
readonly routes: IComputedValue<Route<unknown>[]>;
readonly entityRegistry: CatalogEntityRegistry;
readonly categoryRegistry: CatalogCategoryRegistry;
}
export class LensRendererExtension extends LensExtension {
globalPages: PageRegistration[] = [];
clusterPages: PageRegistration[] = [];
clusterPageMenus: ClusterPageMenuRegistration[] = [];
@ -54,8 +75,28 @@ export class LensRendererExtension extends LensExtension<LensRendererExtensionDe
customCategoryViews: CustomCategoryViewRegistration[] = [];
kubeObjectHandlers: KubeObjectHandlerRegistration[] = [];
/**
* @ignore
*/
declare protected readonly dependencies: LensRendererExtensionDependencies;
constructor(extension: InstalledExtension) {
const di = getEnvironmentSpecificLegacyGlobalDiForExtensionApi("renderer");
const deps: LensRendererExtensionDependencies = {
getExtensionPageParameters: di.inject(getExtensionPageParametersInjectable),
navigateToRoute: di.inject(navigateToRouteInjectable),
ensureHashedDirectoryForExtension: di.inject(ensureHashedDirectoryForExtensionInjectable),
categoryRegistry: di.inject(catalogCategoryRegistryInjectable),
entityRegistry: di.inject(catalogEntityRegistryInjectable),
routes: di.inject(routesInjectable),
logger: di.inject(loggerInjectable),
};
super(deps, extension);
}
async navigate(pageId?: string, params: object = {}) {
const routes = this[lensExtensionDependencies].routes.get();
const routes = this.dependencies.routes.get();
const targetRegistration = [...this.globalPages, ...this.clusterPages]
.find(registration => registration.id === (pageId || undefined));
@ -70,7 +111,7 @@ export class LensRendererExtension extends LensExtension<LensRendererExtensionDe
return;
}
const normalizedParams = this[lensExtensionDependencies].getExtensionPageParameters({
const normalizedParams = this.dependencies.getExtensionPageParameters({
extension: this,
registration: targetRegistration,
});
@ -84,7 +125,7 @@ export class LensRendererExtension extends LensExtension<LensRendererExtensionDe
fromPairs,
);
this[lensExtensionDependencies].navigateToRoute(targetRoute, {
this.dependencies.navigateToRoute(targetRoute, {
query,
});
}
@ -107,7 +148,7 @@ export class LensRendererExtension extends LensExtension<LensRendererExtensionDe
* @returns A function to clean up the filter
*/
addCatalogFilter(fn: EntityFilter): Disposer {
const dispose = this[lensExtensionDependencies].entityRegistry.addCatalogFilter(fn);
const dispose = this.dependencies.entityRegistry.addCatalogFilter(fn);
this[Disposers].push(dispose);
@ -120,7 +161,7 @@ export class LensRendererExtension extends LensExtension<LensRendererExtensionDe
* @returns A function to clean up the filter
*/
addCatalogCategoryFilter(fn: CategoryFilter): Disposer {
const dispose = this[lensExtensionDependencies].categoryRegistry.addCatalogCategoryFilter(fn);
const dispose = this.dependencies.categoryRegistry.addCatalogCategoryFilter(fn);
this[Disposers].push(dispose);

View File

@ -81,6 +81,7 @@ exports[`extension special characters in page registrations renders 1`] = `
>
<i
class="Icon logo svg focusable"
data-testid="no-welcome-banners-icon"
>
<span
class="icon"

View File

@ -81,6 +81,7 @@ exports[`navigate to extension page renders 1`] = `
>
<i
class="Icon logo svg focusable"
data-testid="no-welcome-banners-icon"
>
<span
class="icon"

View File

@ -81,6 +81,7 @@ exports[`add-cluster - navigation using application menu renders 1`] = `
>
<i
class="Icon logo svg focusable"
data-testid="no-welcome-banners-icon"
>
<span
class="icon"

View File

@ -6,7 +6,6 @@ import { computed } from "mobx";
import type { Injectable } from "@ogre-tools/injectable";
import { getInjectable } from "@ogre-tools/injectable";
import { extensionRegistratorInjectionToken } from "../../../extensions/extension-loader/extension-registrator-injection-token";
import type { LensExtension } from "../../../extensions/lens-extension";
import type { LensMainExtension } from "../../../extensions/lens-main-extension";
import type {
ApplicationMenuItemTypes,
@ -25,7 +24,7 @@ const applicationMenuItemRegistratorInjectable = getInjectable({
const logError = di.inject(logErrorInjectable);
const toRecursedInjectables = toRecursedInjectablesFor(logError);
return (ext: LensExtension) => {
return (ext) => {
const mainExtension = ext as LensMainExtension;
return computed(() => {

View File

@ -82,6 +82,7 @@ exports[`installing update when started renders 1`] = `
>
<i
class="Icon logo svg focusable"
data-testid="no-welcome-banners-icon"
>
<span
class="icon"
@ -367,6 +368,7 @@ exports[`installing update when started when user checks for updates renders 1`]
>
<i
class="Icon logo svg focusable"
data-testid="no-welcome-banners-icon"
>
<span
class="icon"
@ -652,6 +654,7 @@ exports[`installing update when started when user checks for updates when new up
>
<i
class="Icon logo svg focusable"
data-testid="no-welcome-banners-icon"
>
<span
class="icon"
@ -937,6 +940,7 @@ exports[`installing update when started when user checks for updates when new up
>
<i
class="Icon logo svg focusable"
data-testid="no-welcome-banners-icon"
>
<span
class="icon"
@ -1247,6 +1251,7 @@ exports[`installing update when started when user checks for updates when new up
>
<i
class="Icon logo svg focusable"
data-testid="no-welcome-banners-icon"
>
<span
class="icon"
@ -1557,6 +1562,7 @@ exports[`installing update when started when user checks for updates when new up
>
<i
class="Icon logo svg focusable"
data-testid="no-welcome-banners-icon"
>
<span
class="icon"
@ -1867,6 +1873,7 @@ exports[`installing update when started when user checks for updates when new up
>
<i
class="Icon logo svg focusable"
data-testid="no-welcome-banners-icon"
>
<span
class="icon"
@ -2152,6 +2159,7 @@ exports[`installing update when started when user checks for updates when no new
>
<i
class="Icon logo svg focusable"
data-testid="no-welcome-banners-icon"
>
<span
class="icon"

View File

@ -107,6 +107,7 @@ exports[`encourage user to update when sufficient time passed since update was d
>
<i
class="Icon logo svg focusable"
data-testid="no-welcome-banners-icon"
>
<span
class="icon"
@ -392,6 +393,7 @@ exports[`encourage user to update when sufficient time passed since update was d
>
<i
class="Icon logo svg focusable"
data-testid="no-welcome-banners-icon"
>
<span
class="icon"

View File

@ -82,6 +82,7 @@ exports[`installing update using tray when started renders 1`] = `
>
<i
class="Icon logo svg focusable"
data-testid="no-welcome-banners-icon"
>
<span
class="icon"
@ -367,6 +368,7 @@ exports[`installing update using tray when started when user checks for updates
>
<i
class="Icon logo svg focusable"
data-testid="no-welcome-banners-icon"
>
<span
class="icon"
@ -652,6 +654,7 @@ exports[`installing update using tray when started when user checks for updates
>
<i
class="Icon logo svg focusable"
data-testid="no-welcome-banners-icon"
>
<span
class="icon"
@ -937,6 +940,7 @@ exports[`installing update using tray when started when user checks for updates
>
<i
class="Icon logo svg focusable"
data-testid="no-welcome-banners-icon"
>
<span
class="icon"
@ -1247,6 +1251,7 @@ exports[`installing update using tray when started when user checks for updates
>
<i
class="Icon logo svg focusable"
data-testid="no-welcome-banners-icon"
>
<span
class="icon"
@ -1532,6 +1537,7 @@ exports[`installing update using tray when started when user checks for updates
>
<i
class="Icon logo svg focusable"
data-testid="no-welcome-banners-icon"
>
<span
class="icon"

View File

@ -107,6 +107,7 @@ exports[`force user to update when too long since update was downloaded when app
>
<i
class="Icon logo svg focusable"
data-testid="no-welcome-banners-icon"
>
<span
class="icon"
@ -417,6 +418,7 @@ exports[`force user to update when too long since update was downloaded when app
>
<i
class="Icon logo svg focusable"
data-testid="no-welcome-banners-icon"
>
<span
class="icon"
@ -774,6 +776,7 @@ exports[`force user to update when too long since update was downloaded when app
>
<i
class="Icon logo svg focusable"
data-testid="no-welcome-banners-icon"
>
<span
class="icon"

View File

@ -82,6 +82,7 @@ exports[`periodical checking of updates given updater is enabled and configurati
>
<i
class="Icon logo svg focusable"
data-testid="no-welcome-banners-icon"
>
<span
class="icon"

View File

@ -82,6 +82,7 @@ exports[`selection of update stability when started renders 1`] = `
>
<i
class="Icon logo svg focusable"
data-testid="no-welcome-banners-icon"
>
<span
class="icon"

File diff suppressed because it is too large Load Diff

View File

@ -89,41 +89,35 @@ exports[`entity running technical tests when navigated to catalog renders 1`] =
Catalog
</div>
<ul
aria-multiselectable="false"
class="MuiTreeView-root"
class="treeView"
role="tree"
>
<li
aria-selected="true"
class="MuiTreeItem-root Mui-selected"
class="root selected treeItem"
data-testid="*-tab"
role="treeitem"
tabindex="0"
>
<div
class="MuiTreeItem-content"
class="iconContainer"
/>
<div
class="label"
>
<div
class="MuiTreeItem-iconContainer"
/>
<div
class="MuiTypography-root MuiTreeItem-label MuiTypography-body1"
>
Browse
</div>
Browse
</div>
</li>
<div
class="HorizontalLine size-xxs"
/>
<li
aria-expanded="true"
class="MuiTreeItem-root bordered Mui-expanded"
role="treeitem"
tabindex="-1"
class="root treeGroup"
role="group"
>
<div
class="MuiTreeItem-content"
class="group"
>
<div
class="MuiTreeItem-iconContainer"
class="iconContainer"
>
<i
class="Icon material focusable"
@ -137,7 +131,7 @@ exports[`entity running technical tests when navigated to catalog renders 1`] =
</i>
</div>
<div
class="MuiTypography-root MuiTreeItem-label MuiTypography-body1"
class="label"
>
<div
class="parent"
@ -147,160 +141,130 @@ exports[`entity running technical tests when navigated to catalog renders 1`] =
</div>
</div>
<ul
class="MuiCollapse-root MuiTreeItem-group MuiCollapse-entered"
role="group"
style="min-height: 0px;"
class="contents expanded"
>
<div
class="MuiCollapse-wrapper"
<li
class="root treeItem"
data-testid="entity.k8slens.dev/General-tab"
role="treeitem"
>
<div
class="MuiCollapse-wrapperInner"
class="iconContainer"
>
<li
class="MuiTreeItem-root"
data-testid="entity.k8slens.dev/General-tab"
role="treeitem"
tabindex="-1"
<i
class="Icon material focusable small"
>
<div
class="MuiTreeItem-content"
<span
class="icon"
data-icon-name="settings"
>
<div
class="MuiTreeItem-iconContainer"
>
<i
class="Icon material focusable small"
>
<span
class="icon"
data-icon-name="settings"
>
settings
</span>
</i>
</div>
<div
class="MuiTypography-root MuiTreeItem-label MuiTypography-body1"
>
<div
class="flex"
>
<div>
General
</div>
</div>
</div>
</div>
</li>
<li
class="MuiTreeItem-root"
data-testid="entity.k8slens.dev/KubernetesCluster-tab"
role="treeitem"
tabindex="-1"
>
<div
class="MuiTreeItem-content"
>
<div
class="MuiTreeItem-iconContainer"
>
<i
class="Icon focusable small"
>
<span
class="icon"
data-icon-name=""
/>
</i>
</div>
<div
class="MuiTypography-root MuiTreeItem-label MuiTypography-body1"
>
<div
class="flex"
>
<div>
Clusters
</div>
</div>
</div>
</div>
</li>
<li
class="MuiTreeItem-root"
data-testid="entity.k8slens.dev/WebLink-tab"
role="treeitem"
tabindex="-1"
>
<div
class="MuiTreeItem-content"
>
<div
class="MuiTreeItem-iconContainer"
>
<i
class="Icon material focusable small"
>
<span
class="icon"
data-icon-name="public"
>
public
</span>
</i>
</div>
<div
class="MuiTypography-root MuiTreeItem-label MuiTypography-body1"
>
<div
class="flex"
>
<div>
Web Links
</div>
</div>
</div>
</div>
</li>
<li
class="MuiTreeItem-root"
data-testid="entity.k8slens.dev/Mock-tab"
role="treeitem"
tabindex="-1"
>
<div
class="MuiTreeItem-content"
>
<div
class="MuiTreeItem-iconContainer"
>
<i
class="Icon material focusable small"
>
<span
class="icon"
data-icon-name="gear"
>
gear
</span>
</i>
</div>
<div
class="MuiTypography-root MuiTreeItem-label MuiTypography-body1"
>
<div
class="flex"
>
<div>
mock
</div>
</div>
</div>
</div>
</li>
settings
</span>
</i>
</div>
</div>
<div
class="label"
>
<div
class="flex"
>
<div>
General
</div>
</div>
</div>
</li>
<li
class="root treeItem"
data-testid="entity.k8slens.dev/KubernetesCluster-tab"
role="treeitem"
>
<div
class="iconContainer"
>
<i
class="Icon focusable small"
>
<span
class="icon"
data-icon-name=""
/>
</i>
</div>
<div
class="label"
>
<div
class="flex"
>
<div>
Clusters
</div>
</div>
</div>
</li>
<li
class="root treeItem"
data-testid="entity.k8slens.dev/WebLink-tab"
role="treeitem"
>
<div
class="iconContainer"
>
<i
class="Icon material focusable small"
>
<span
class="icon"
data-icon-name="public"
>
public
</span>
</i>
</div>
<div
class="label"
>
<div
class="flex"
>
<div>
Web Links
</div>
</div>
</div>
</li>
<li
class="root treeItem"
data-testid="entity.k8slens.dev/Mock-tab"
role="treeitem"
>
<div
class="iconContainer"
>
<i
class="Icon material focusable small"
>
<span
class="icon"
data-icon-name="gear"
>
gear
</span>
</i>
</div>
<div
class="label"
>
<div
class="flex"
>
<div>
mock
</div>
</div>
</div>
</li>
</ul>
</li>
</ul>
@ -392,6 +356,7 @@ exports[`entity running technical tests when navigated to catalog renders 1`] =
</div>
<div
class="TableCell nowrap sorting"
data-testid="catalog-kind-column"
id="kind"
>
<div
@ -412,6 +377,7 @@ exports[`entity running technical tests when navigated to catalog renders 1`] =
</div>
<div
class="TableCell sourceCell nowrap sorting"
data-testid="catalog-source-column"
id="source"
>
<div
@ -432,6 +398,7 @@ exports[`entity running technical tests when navigated to catalog renders 1`] =
</div>
<div
class="TableCell labelsCell scrollable nowrap"
data-testid="catalog-labels-column"
id="labels"
>
<div
@ -442,6 +409,7 @@ exports[`entity running technical tests when navigated to catalog renders 1`] =
</div>
<div
class="TableCell statusCell nowrap sorting"
data-testid="catalog-status-column"
id="status"
>
<div
@ -805,41 +773,35 @@ exports[`entity running technical tests when navigated to catalog when details p
Catalog
</div>
<ul
aria-multiselectable="false"
class="MuiTreeView-root"
class="treeView"
role="tree"
>
<li
aria-selected="true"
class="MuiTreeItem-root Mui-selected"
class="root selected treeItem"
data-testid="*-tab"
role="treeitem"
tabindex="0"
>
<div
class="MuiTreeItem-content"
class="iconContainer"
/>
<div
class="label"
>
<div
class="MuiTreeItem-iconContainer"
/>
<div
class="MuiTypography-root MuiTreeItem-label MuiTypography-body1"
>
Browse
</div>
Browse
</div>
</li>
<div
class="HorizontalLine size-xxs"
/>
<li
aria-expanded="true"
class="MuiTreeItem-root bordered Mui-expanded"
role="treeitem"
tabindex="-1"
class="root treeGroup"
role="group"
>
<div
class="MuiTreeItem-content"
class="group"
>
<div
class="MuiTreeItem-iconContainer"
class="iconContainer"
>
<i
class="Icon material focusable"
@ -853,7 +815,7 @@ exports[`entity running technical tests when navigated to catalog when details p
</i>
</div>
<div
class="MuiTypography-root MuiTreeItem-label MuiTypography-body1"
class="label"
>
<div
class="parent"
@ -863,160 +825,130 @@ exports[`entity running technical tests when navigated to catalog when details p
</div>
</div>
<ul
class="MuiCollapse-root MuiTreeItem-group MuiCollapse-entered"
role="group"
style="min-height: 0px;"
class="contents expanded"
>
<div
class="MuiCollapse-wrapper"
<li
class="root treeItem"
data-testid="entity.k8slens.dev/General-tab"
role="treeitem"
>
<div
class="MuiCollapse-wrapperInner"
class="iconContainer"
>
<li
class="MuiTreeItem-root"
data-testid="entity.k8slens.dev/General-tab"
role="treeitem"
tabindex="-1"
<i
class="Icon material focusable small"
>
<div
class="MuiTreeItem-content"
<span
class="icon"
data-icon-name="settings"
>
<div
class="MuiTreeItem-iconContainer"
>
<i
class="Icon material focusable small"
>
<span
class="icon"
data-icon-name="settings"
>
settings
</span>
</i>
</div>
<div
class="MuiTypography-root MuiTreeItem-label MuiTypography-body1"
>
<div
class="flex"
>
<div>
General
</div>
</div>
</div>
</div>
</li>
<li
class="MuiTreeItem-root"
data-testid="entity.k8slens.dev/KubernetesCluster-tab"
role="treeitem"
tabindex="-1"
>
<div
class="MuiTreeItem-content"
>
<div
class="MuiTreeItem-iconContainer"
>
<i
class="Icon focusable small"
>
<span
class="icon"
data-icon-name=""
/>
</i>
</div>
<div
class="MuiTypography-root MuiTreeItem-label MuiTypography-body1"
>
<div
class="flex"
>
<div>
Clusters
</div>
</div>
</div>
</div>
</li>
<li
class="MuiTreeItem-root"
data-testid="entity.k8slens.dev/WebLink-tab"
role="treeitem"
tabindex="-1"
>
<div
class="MuiTreeItem-content"
>
<div
class="MuiTreeItem-iconContainer"
>
<i
class="Icon material focusable small"
>
<span
class="icon"
data-icon-name="public"
>
public
</span>
</i>
</div>
<div
class="MuiTypography-root MuiTreeItem-label MuiTypography-body1"
>
<div
class="flex"
>
<div>
Web Links
</div>
</div>
</div>
</div>
</li>
<li
class="MuiTreeItem-root"
data-testid="entity.k8slens.dev/Mock-tab"
role="treeitem"
tabindex="-1"
>
<div
class="MuiTreeItem-content"
>
<div
class="MuiTreeItem-iconContainer"
>
<i
class="Icon material focusable small"
>
<span
class="icon"
data-icon-name="gear"
>
gear
</span>
</i>
</div>
<div
class="MuiTypography-root MuiTreeItem-label MuiTypography-body1"
>
<div
class="flex"
>
<div>
mock
</div>
</div>
</div>
</div>
</li>
settings
</span>
</i>
</div>
</div>
<div
class="label"
>
<div
class="flex"
>
<div>
General
</div>
</div>
</div>
</li>
<li
class="root treeItem"
data-testid="entity.k8slens.dev/KubernetesCluster-tab"
role="treeitem"
>
<div
class="iconContainer"
>
<i
class="Icon focusable small"
>
<span
class="icon"
data-icon-name=""
/>
</i>
</div>
<div
class="label"
>
<div
class="flex"
>
<div>
Clusters
</div>
</div>
</div>
</li>
<li
class="root treeItem"
data-testid="entity.k8slens.dev/WebLink-tab"
role="treeitem"
>
<div
class="iconContainer"
>
<i
class="Icon material focusable small"
>
<span
class="icon"
data-icon-name="public"
>
public
</span>
</i>
</div>
<div
class="label"
>
<div
class="flex"
>
<div>
Web Links
</div>
</div>
</div>
</li>
<li
class="root treeItem"
data-testid="entity.k8slens.dev/Mock-tab"
role="treeitem"
>
<div
class="iconContainer"
>
<i
class="Icon material focusable small"
>
<span
class="icon"
data-icon-name="gear"
>
gear
</span>
</i>
</div>
<div
class="label"
>
<div
class="flex"
>
<div>
mock
</div>
</div>
</div>
</li>
</ul>
</li>
</ul>
@ -1108,6 +1040,7 @@ exports[`entity running technical tests when navigated to catalog when details p
</div>
<div
class="TableCell nowrap sorting"
data-testid="catalog-kind-column"
id="kind"
>
<div
@ -1128,6 +1061,7 @@ exports[`entity running technical tests when navigated to catalog when details p
</div>
<div
class="TableCell sourceCell nowrap sorting"
data-testid="catalog-source-column"
id="source"
>
<div
@ -1148,6 +1082,7 @@ exports[`entity running technical tests when navigated to catalog when details p
</div>
<div
class="TableCell labelsCell scrollable nowrap"
data-testid="catalog-labels-column"
id="labels"
>
<div
@ -1158,6 +1093,7 @@ exports[`entity running technical tests when navigated to catalog when details p
</div>
<div
class="TableCell statusCell nowrap sorting"
data-testid="catalog-status-column"
id="status"
>
<div

View File

@ -0,0 +1,314 @@
/**
* 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 { CatalogCategory, type CatalogCategorySpec, type CategoryColumnRegistration } from "../../common/catalog";
import catalogCategoryRegistryInjectable from "../../common/catalog/category-registry.injectable";
import navigateToCatalogInjectable from "../../common/front-end-routing/routes/catalog/navigate-to-catalog.injectable";
import type { ApplicationBuilder } from "../../renderer/components/test-utils/get-application-builder";
import { getApplicationBuilder } from "../../renderer/components/test-utils/get-application-builder";
describe("custom category columns for catalog", () => {
let builder: ApplicationBuilder;
let renderResult: RenderResult;
beforeEach(async () => {
builder = getApplicationBuilder();
renderResult = await builder.render();
const navigateToCatalog = builder.applicationWindow.only.di.inject(navigateToCatalogInjectable);
navigateToCatalog();
});
it("renders", () => {
expect(renderResult.baseElement).toMatchSnapshot();
});
it("shows 'Browse All' view", () => {
expect(renderResult.queryByTestId("catalog-list-for-browse-all")).toBeInTheDocument();
});
it("should show the 'Kind' column", () => {
expect(renderResult.queryByTestId("catalog-kind-column")).toBeInTheDocument();
});
it("should show the 'Status' column", () => {
expect(renderResult.queryByTestId("catalog-status-column")).toBeInTheDocument();
});
it("should show the 'Labels' column", () => {
expect(renderResult.queryByTestId("catalog-labels-column")).toBeInTheDocument();
});
it("should show the 'Source' column", () => {
expect(renderResult.queryByTestId("catalog-source-column")).toBeInTheDocument();
});
describe("when category is added using default colemns", () => {
beforeEach(() => {
const catalogCategoryRegistry = builder.applicationWindow.only.di.inject(catalogCategoryRegistryInjectable);
catalogCategoryRegistry.add(new TestCategory());
});
it("renders", () => {
expect(renderResult.baseElement).toMatchSnapshot();
});
it("shows category in sidebar", () => {
expect(renderResult.queryByTestId("foo.bar.bat/Test-tab")).toBeInTheDocument();
});
it("still shows 'Browse All' view", () => {
expect(renderResult.queryByTestId("catalog-list-for-browse-all")).toBeInTheDocument();
});
describe("when the Test category tab is clicked", () => {
beforeEach(async () => {
const testCategory = renderResult.getByTestId("foo.bar.bat/Test-tab");
testCategory.click();
});
it("renders", () => {
expect(renderResult.baseElement).toMatchSnapshot();
});
it("shows view for category", () => {
expect(renderResult.queryByTestId("catalog-list-for-Test")).toBeInTheDocument();
});
it("does not show the 'Kind' column", () => {
expect(renderResult.queryByTestId("catalog-kind-column")).not.toBeInTheDocument();
});
it("should show the 'Status' column", () => {
expect(renderResult.queryByTestId("catalog-status-column")).toBeInTheDocument();
});
it("should show the 'Labels' column", () => {
expect(renderResult.queryByTestId("catalog-labels-column")).toBeInTheDocument();
});
it("should show the 'Source' column", () => {
expect(renderResult.queryByTestId("catalog-source-column")).toBeInTheDocument();
});
});
describe("when an extension is registered with additional custom columns", () => {
beforeEach(() => {
builder.extensions.enable({
id: "some-id",
name: "some-name",
rendererOptions: {
additionalCategoryColumns: [
{
group: "foo.bar.bat",
id: "high",
kind: "Test",
renderCell: () => "",
titleProps: {
title: "High",
"data-testid": "my-high-column",
},
},
{
group: "foo.bar",
id: "high",
kind: "Test",
renderCell: () => "",
titleProps: {
title: "High2",
"data-testid": "my-high2-column",
},
},
],
},
});
});
describe("when the Test category tab is clicked", () => {
beforeEach(async () => {
const testCategory = renderResult.getByTestId("foo.bar.bat/Test-tab");
testCategory.click();
});
it("renders", () => {
expect(renderResult.baseElement).toMatchSnapshot();
});
it("shows view for category", () => {
expect(renderResult.queryByTestId("catalog-list-for-Test")).toBeInTheDocument();
});
it("does not show the 'Kind' column", () => {
expect(renderResult.queryByTestId("catalog-kind-column")).not.toBeInTheDocument();
});
it("should show the 'Status' column", () => {
expect(renderResult.queryByTestId("catalog-status-column")).toBeInTheDocument();
});
it("should show the 'Labels' column", () => {
expect(renderResult.queryByTestId("catalog-labels-column")).toBeInTheDocument();
});
it("should show the 'Source' column", () => {
expect(renderResult.queryByTestId("catalog-source-column")).toBeInTheDocument();
});
it("should show the additional column that matches", () => {
expect(renderResult.queryByTestId("my-high-column")).toBeInTheDocument();
});
it("should not show the additional column that does not match", () => {
expect(renderResult.queryByTestId("my-high2-column")).not.toBeInTheDocument();
});
});
});
});
describe("when category is added with custom columns", () => {
beforeEach(() => {
const catalogCategoryRegistry = builder.applicationWindow.only.di.inject(catalogCategoryRegistryInjectable);
catalogCategoryRegistry.add(new TestCategory([{
id: "foo",
renderCell: () => null,
titleProps: {
title: "Foo",
"data-testid": "my-custom-column",
},
}]));
});
it("renders", () => {
expect(renderResult.baseElement).toMatchSnapshot();
});
it("shows category in sidebar", () => {
expect(renderResult.queryByTestId("foo.bar.bat/Test-tab")).toBeInTheDocument();
});
it("still shows 'Browse All' view", () => {
expect(renderResult.queryByTestId("catalog-list-for-browse-all")).toBeInTheDocument();
});
describe("when the Test category tab is clicked", () => {
beforeEach(async () => {
const testCategory = renderResult.getByTestId("foo.bar.bat/Test-tab");
testCategory.click();
});
it("renders", () => {
expect(renderResult.baseElement).toMatchSnapshot();
});
it("shows view for category", () => {
expect(renderResult.queryByTestId("catalog-list-for-Test")).toBeInTheDocument();
});
it("does not show the 'Kind' column", () => {
expect(renderResult.queryByTestId("catalog-kind-column")).not.toBeInTheDocument();
});
it("does not the 'Status' column", () => {
expect(renderResult.queryByTestId("catalog-status-column")).not.toBeInTheDocument();
});
it("does not the 'Labels' column", () => {
expect(renderResult.queryByTestId("catalog-labels-column")).not.toBeInTheDocument();
});
it("does not the 'Source' column", () => {
expect(renderResult.queryByTestId("catalog-source-column")).not.toBeInTheDocument();
});
it("should show the custom column", () => {
expect(renderResult.queryByTestId("my-custom-column")).toBeInTheDocument();
});
});
});
describe("when category is added without default columns", () => {
beforeEach(() => {
const catalogCategoryRegistry = builder.applicationWindow.only.di.inject(catalogCategoryRegistryInjectable);
catalogCategoryRegistry.add(new TestCategory([]));
});
it("renders", () => {
expect(renderResult.baseElement).toMatchSnapshot();
});
it("shows category in sidebar", () => {
expect(renderResult.queryByTestId("foo.bar.bat/Test-tab")).toBeInTheDocument();
});
it("still shows 'Browse All' view", () => {
expect(renderResult.queryByTestId("catalog-list-for-browse-all")).toBeInTheDocument();
});
describe("when the Test category tab is clicked", () => {
beforeEach(async () => {
const testCategory = renderResult.getByTestId("foo.bar.bat/Test-tab");
testCategory.click();
});
it("renders", () => {
expect(renderResult.baseElement).toMatchSnapshot();
});
it("shows view for category", () => {
expect(renderResult.queryByTestId("catalog-list-for-Test")).toBeInTheDocument();
});
it("does not show the 'Kind' column", () => {
expect(renderResult.queryByTestId("catalog-kind-column")).not.toBeInTheDocument();
});
it("does not the 'Status' column", () => {
expect(renderResult.queryByTestId("catalog-status-column")).not.toBeInTheDocument();
});
it("does not the 'Labels' column", () => {
expect(renderResult.queryByTestId("catalog-labels-column")).not.toBeInTheDocument();
});
it("does not the 'Source' column", () => {
expect(renderResult.queryByTestId("catalog-source-column")).not.toBeInTheDocument();
});
});
});
});
class TestCategory extends CatalogCategory {
apiVersion = "catalog.k8slens.dev/v1alpha1";
kind = "CatalogCategory";
metadata = {
name: "Test",
icon: "question_mark",
};
spec: CatalogCategorySpec = {
group: "foo.bar.bat",
names: {
kind: "Test",
},
versions: [],
};
constructor(columns?: CategoryColumnRegistration[]) {
super();
this.spec = {
displayColumns: columns,
...this.spec,
};
}
}

View File

@ -173,6 +173,7 @@ exports[`Command Pallet: keyboard shortcut tests when on linux renders 1`] = `
>
<i
class="Icon logo svg focusable"
data-testid="no-welcome-banners-icon"
>
<span
class="icon"
@ -549,6 +550,7 @@ exports[`Command Pallet: keyboard shortcut tests when on linux when pressing ESC
>
<i
class="Icon logo svg focusable"
data-testid="no-welcome-banners-icon"
>
<span
class="icon"
@ -925,6 +927,7 @@ exports[`Command Pallet: keyboard shortcut tests when on linux when pressing SHI
>
<i
class="Icon logo svg focusable"
data-testid="no-welcome-banners-icon"
>
<span
class="icon"
@ -1313,6 +1316,7 @@ exports[`Command Pallet: keyboard shortcut tests when on linux when pressing SHI
>
<i
class="Icon logo svg focusable"
data-testid="no-welcome-banners-icon"
>
<span
class="icon"
@ -1598,6 +1602,7 @@ exports[`Command Pallet: keyboard shortcut tests when on macOS renders 1`] = `
>
<i
class="Icon logo svg focusable"
data-testid="no-welcome-banners-icon"
>
<span
class="icon"
@ -1883,6 +1888,7 @@ exports[`Command Pallet: keyboard shortcut tests when on macOS when pressing ESC
>
<i
class="Icon logo svg focusable"
data-testid="no-welcome-banners-icon"
>
<span
class="icon"
@ -2168,6 +2174,7 @@ exports[`Command Pallet: keyboard shortcut tests when on macOS when pressing SHI
>
<i
class="Icon logo svg focusable"
data-testid="no-welcome-banners-icon"
>
<span
class="icon"
@ -2465,6 +2472,7 @@ exports[`Command Pallet: keyboard shortcut tests when on macOS when pressing SHI
>
<i
class="Icon logo svg focusable"
data-testid="no-welcome-banners-icon"
>
<span
class="icon"

View File

@ -82,6 +82,7 @@ exports[`Showing correct entity settings renders 1`] = `
>
<i
class="Icon logo svg focusable"
data-testid="no-welcome-banners-icon"
>
<span
class="icon"

View File

@ -81,6 +81,7 @@ exports[`extensions - navigation using application menu renders 1`] = `
>
<i
class="Icon logo svg focusable"
data-testid="no-welcome-banners-icon"
>
<span
class="icon"

View File

@ -81,6 +81,7 @@ exports[`preferences - navigation using application menu renders 1`] = `
>
<i
class="Icon logo svg focusable"
data-testid="no-welcome-banners-icon"
>
<span
class="icon"

View File

@ -82,6 +82,7 @@ exports[`show-about-using-tray renders 1`] = `
>
<i
class="Icon logo svg focusable"
data-testid="no-welcome-banners-icon"
>
<span
class="icon"

View File

@ -82,6 +82,7 @@ exports[`status-bar-items-originating-from-extensions when application starts wh
>
<i
class="Icon logo svg focusable"
data-testid="no-welcome-banners-icon"
>
<span
class="icon"

View File

@ -82,6 +82,7 @@ exports[`extendability-using-extension-api given an extension with a weakly type
>
<i
class="Icon logo svg focusable"
data-testid="no-welcome-banners-icon"
>
<span
class="icon"
@ -376,6 +377,7 @@ exports[`extendability-using-extension-api given an extension with top-bar items
>
<i
class="Icon logo svg focusable"
data-testid="no-welcome-banners-icon"
>
<span
class="icon"
@ -661,6 +663,7 @@ exports[`extendability-using-extension-api given an extension with top-bar items
>
<i
class="Icon logo svg focusable"
data-testid="no-welcome-banners-icon"
>
<span
class="icon"
@ -946,6 +949,7 @@ exports[`extendability-using-extension-api renders 1`] = `
>
<i
class="Icon logo svg focusable"
data-testid="no-welcome-banners-icon"
>
<span
class="icon"

View File

@ -81,6 +81,7 @@ exports[`welcome - navigation using application menu renders 1`] = `
>
<i
class="Icon logo svg focusable"
data-testid="no-welcome-banners-icon"
>
<span
class="icon"
@ -1116,6 +1117,7 @@ exports[`welcome - navigation using application menu when navigated somewhere el
>
<i
class="Icon logo svg focusable"
data-testid="no-welcome-banners-icon"
>
<span
class="icon"

View File

@ -0,0 +1,91 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import React from "react";
import type { RenderResult } from "@testing-library/react";
import { screen } from "@testing-library/react";
import { defaultWidth } from "../../renderer/components/+welcome/welcome";
import type { ApplicationBuilder } from "../../renderer/components/test-utils/get-application-builder";
import { getApplicationBuilder } from "../../renderer/components/test-utils/get-application-builder";
describe("Banners from extensions", () => {
let builder: ApplicationBuilder;
let renderResult: RenderResult;
beforeEach(async () => {
builder = getApplicationBuilder();
renderResult = await builder.render();
});
it("initially renderes welcome page", () => {
expect(renderResult.queryByTestId("welcome-page")).toBeInTheDocument();
});
it("shows the empty welcome banner icon", () => {
expect(renderResult.queryByTestId("no-welcome-banners-icon")).toBeInTheDocument();
});
describe("when an extension is enabled with a single welcome banner", () => {
beforeEach(() => {
builder.extensions.enable({
id: "some-id",
name: "some-name",
rendererOptions: {
welcomeBanners: [
{
Banner: () => <div data-testid="some-test-id" />,
},
],
},
});
});
it("renders the banner from the extension", () => {
expect(renderResult.queryByTestId("some-test-id")).toBeInTheDocument();
});
it("no longer shows the empty welcome banner icon", () => {
expect(renderResult.queryByTestId("no-welcome-banners-icon")).not.toBeInTheDocument();
});
});
describe("when an extension is enabled with multiple banners with custom widths", () => {
beforeEach(() => {
builder.extensions.enable({
id: "some-id",
name: "some-name",
rendererOptions: {
welcomeBanners: [
{
width: 100,
Banner: () => <div />,
},
{
width: 800,
Banner: () => <div />,
},
],
},
});
});
it("no longer shows the empty welcome banner icon", () => {
expect(renderResult.queryByTestId("no-welcome-banners-icon")).not.toBeInTheDocument();
});
it("computes an opropriate width for the carosel", () => {
expect(screen.queryByTestId("welcome-banner-container")).toHaveStyle({
// should take the max width of the banners (if > defaultWidth)
width: `800px`,
});
expect(screen.queryByTestId("welcome-text-container")).toHaveStyle({
width: `${defaultWidth}px`,
});
expect(screen.queryByTestId("welcome-menu-container")).toHaveStyle({
width: `${defaultWidth}px`,
});
});
});
});

View File

@ -1,37 +0,0 @@
/**
* 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 type { Writable } from "type-fest";
import loggerInjectable from "../../common/logger.injectable";
import { createExtensionInstanceInjectionToken } from "../../extensions/extension-loader/create-extension-instance.token";
import ensureHashedDirectoryForExtensionInjectable from "../../extensions/extension-loader/file-system-provisioner-store/ensure-hashed-directory-for-extension.injectable";
import { lensExtensionDependencies } from "../../extensions/lens-extension";
import type { LensMainExtensionDependencies } from "../../extensions/lens-extension-set-dependencies";
import type { LensMainExtension } from "../../extensions/lens-main-extension";
import catalogEntityRegistryInjectable from "../catalog/entity-registry.injectable";
import navigateForExtensionInjectable from "../start-main-application/lens-window/navigate-for-extension.injectable";
const createExtensionInstanceInjectable = getInjectable({
id: "create-extension-instance",
instantiate: (di) => {
const deps: LensMainExtensionDependencies = {
ensureHashedDirectoryForExtension: di.inject(ensureHashedDirectoryForExtensionInjectable),
entityRegistry: di.inject(catalogEntityRegistryInjectable),
navigate: di.inject(navigateForExtensionInjectable),
logger: di.inject(loggerInjectable),
};
return (ExtensionClass, extension) => {
const instance = new ExtensionClass(extension as any) as LensMainExtension;
(instance as Writable<LensMainExtension>)[lensExtensionDependencies] = deps;
return instance;
};
},
injectionToken: createExtensionInstanceInjectionToken,
});
export default createExtensionInstanceInjectable;

View File

@ -10,15 +10,15 @@ import { noop } from "@k8slens/utilities";
import type { LensProtocolRouterMain } from "../lens-protocol-router-main/lens-protocol-router-main";
import { getDiForUnitTesting } from "../../getDiForUnitTesting";
import lensProtocolRouterMainInjectable from "../lens-protocol-router-main/lens-protocol-router-main.injectable";
import { LensExtension } from "../../../extensions/lens-extension";
import type { ObservableMap } from "mobx";
import { runInAction } from "mobx";
import extensionInstancesInjectable from "../../../extensions/extension-loader/extension-instances.injectable";
import directoryForUserDataInjectable from "../../../common/app-paths/directory-for-user-data/directory-for-user-data.injectable";
import broadcastMessageInjectable from "../../../common/ipc/broadcast-message.injectable";
import type { LensExtensionId } from "@k8slens/legacy-extensions";
import type { LensExtensionState } from "../../../features/extensions/enabled/common/state.injectable";
import enabledExtensionsStateInjectable from "../../../features/extensions/enabled/common/state.injectable";
import type { LegacyLensExtension, LensExtensionId } from "@k8slens/legacy-extensions";
import { LensMainExtension } from "../../../extensions/lens-main-extension";
function throwIfDefined(val: any): void {
if (val != null) {
@ -27,7 +27,7 @@ function throwIfDefined(val: any): void {
}
describe("protocol router tests", () => {
let extensionInstances: ObservableMap<LensExtensionId, LensExtension>;
let extensionInstances: ObservableMap<LensExtensionId, LegacyLensExtension>;
let lpr: LensProtocolRouterMain;
let enabledExtensions: ObservableMap<LensExtensionId, LensExtensionState>;
let broadcastMessageMock: jest.Mock;
@ -73,7 +73,7 @@ describe("protocol router tests", () => {
it("should broadcast external route when called with valid host", async () => {
const extId = uuid.v4();
const ext = new LensExtension({
const ext = new LensMainExtension({
id: extId,
manifestPath: "/foo/bar",
manifest: {
@ -149,7 +149,7 @@ describe("protocol router tests", () => {
let called: any = 0;
const extId = uuid.v4();
const ext = new LensExtension({
const ext = new LensMainExtension({
id: extId,
manifestPath: "/foo/bar",
manifest: {
@ -190,7 +190,7 @@ describe("protocol router tests", () => {
{
const extId = uuid.v4();
const ext = new LensExtension({
const ext = new LensMainExtension({
id: extId,
manifestPath: "/foo/bar",
manifest: {
@ -216,7 +216,7 @@ describe("protocol router tests", () => {
{
const extId = uuid.v4();
const ext = new LensExtension({
const ext = new LensMainExtension({
id: extId,
manifestPath: "/foo/bar",
manifest: {

View File

@ -13,6 +13,7 @@ import { getDiForUnitTesting } from "../../../getDiForUnitTesting";
import type { CatalogEntityStore } from "../catalog-entity-store.injectable";
import catalogEntityStoreInjectable from "../catalog-entity-store.injectable";
import { noop } from "@k8slens/utilities";
import type { CatalogEntityRegistry } from "../../../api/catalog/entity/registry";
class TestEntityOne extends CatalogEntity {
public static readonly apiVersion: string = "entity.k8slens.dev/v1alpha1";
@ -152,7 +153,7 @@ describe("CatalogEntityStore", () => {
getItemsForCategory: <T extends CatalogEntity>(category: CatalogCategory): T[] => {
return entityItems.filter(item => category.spec.versions.some(version => item instanceof version.entityClass)) as T[];
},
} as any));
} as CatalogEntityRegistry));
store = di.inject(catalogEntityStoreInjectable);
});

View File

@ -1,135 +0,0 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import type { DiContainer } from "@ogre-tools/injectable";
import type { CatalogCategorySpec } from "../../../../common/catalog";
import { LensRendererExtension } from "../../../../extensions/lens-renderer-extension";
import { CatalogCategory } from "../../../api/catalog-entity";
import { getDiForUnitTesting } from "../../../getDiForUnitTesting";
import type { AdditionalCategoryColumnRegistration, CategoryColumnRegistration } from "../custom-category-columns";
import type { CategoryColumns, GetCategoryColumnsParams } from "../columns/get.injectable";
import getCategoryColumnsInjectable from "../columns/get.injectable";
import extensionInjectable from "../../../../extensions/extension-loader/extension/extension.injectable";
import currentlyInClusterFrameInjectable from "../../../routes/currently-in-cluster-frame.injectable";
class TestCategory extends CatalogCategory {
apiVersion = "catalog.k8slens.dev/v1alpha1";
kind = "CatalogCategory";
metadata = {
name: "Test",
icon: "question_mark",
};
spec: CatalogCategorySpec = {
group: "foo.bar.bat",
names: {
kind: "Test",
},
versions: [],
};
constructor(columns?: CategoryColumnRegistration[]) {
super();
this.spec = {
displayColumns: columns,
...this.spec,
};
}
}
describe("Custom Category Columns", () => {
let di: DiContainer;
let getCategoryColumns: (params: GetCategoryColumnsParams) => CategoryColumns;
beforeEach(() => {
di = getDiForUnitTesting();
di.override(currentlyInClusterFrameInjectable, () => false);
getCategoryColumns = di.inject(getCategoryColumnsInjectable);
});
describe("without extensions", () => {
it("should contain a kind column if activeCategory is falsy", () => {
expect(getCategoryColumns({ activeCategory: null }).renderTableHeader.find(elem => elem?.title === "Kind")).toBeTruthy();
});
it("should not contain a kind column if activeCategory is truthy", () => {
expect(getCategoryColumns({ activeCategory: new TestCategory() }).renderTableHeader.find(elem => elem?.title === "Kind")).toBeFalsy();
});
it("should include the default columns if the provided category doesn't provide any", () => {
expect(getCategoryColumns({ activeCategory: new TestCategory() }).renderTableHeader.find(elem => elem?.title === "Source")).toBeTruthy();
});
it("should not include the default columns if the provided category provides any", () => {
expect(getCategoryColumns({ activeCategory: new TestCategory([]) }).renderTableHeader.find(elem => elem?.title === "Source")).toBeFalsy();
});
it("should include the displayColumns from the provided category", () => {
const columns: CategoryColumnRegistration[] = [
{
id: "foo",
renderCell: () => null,
titleProps: {
title: "Foo",
},
},
];
expect(getCategoryColumns({ activeCategory: new TestCategory(columns) }).renderTableHeader.find(elem => elem?.title === "Foo")).toBeTruthy();
});
});
describe("with extensions", () => {
beforeEach(() => {
const ext = di.inject(extensionInjectable, new (class extends LensRendererExtension {
additionalCategoryColumns = [
{
group: "foo.bar.bat",
id: "high",
kind: "Test",
renderCell: () => "",
titleProps: {
title: "High",
},
} as AdditionalCategoryColumnRegistration,
{
group: "foo.bar",
id: "high",
kind: "Test",
renderCell: () => "",
titleProps: {
title: "High2",
},
} as AdditionalCategoryColumnRegistration,
];
})({
absolutePath: "/some-absolute-path",
id: "some-id",
isBundled: false,
isCompatible: true,
isEnabled: true,
manifest: {
engines: {
lens: "",
},
name: "some-extension-name",
version: "1.0.0",
},
manifestPath: "/some-manifest-path",
}));
ext.register();
});
it("should include columns from extensions that match", () => {
expect(getCategoryColumns({ activeCategory: new TestCategory() }).renderTableHeader.find(elem => elem?.title === "High")).toBeTruthy();
});
it("should not include columns from extensions that don't match", () => {
expect(getCategoryColumns({ activeCategory: new TestCategory() }).renderTableHeader.find(elem => elem?.title === "High2")).toBeFalsy();
});
});
});

View File

@ -30,7 +30,7 @@ const catalogEntityStoreInjectable = getInjectable({
const catalogCategoryRegistry = di.inject(catalogCategoryRegistryInjectable);
const selectedCatalogEntityParam = di.inject(selectedCatalogEntityParamInjectable);
const activeCategory = observable.box<CatalogCategory | undefined>(undefined);
const activeCategory = observable.box<CatalogCategory>();
const entities = computed(() => {
const category = activeCategory.get();

View File

@ -7,25 +7,24 @@ import treeStyles from "./catalog-tree.module.scss";
import styles from "./catalog-menu.module.scss";
import React from "react";
import type { TreeItemProps } from "@material-ui/lab";
import { TreeItem, TreeView } from "@material-ui/lab";
import { Icon } from "../icon";
import { StylesProvider } from "@material-ui/core";
import { cssNames } from "@k8slens/utilities";
import type { CatalogCategory } from "../../api/catalog-entity";
import { observer } from "mobx-react";
import { CatalogCategoryLabel } from "./catalog-category-label";
import type { IComputedValue } from "mobx";
import { withInjectables } from "@ogre-tools/injectable-react";
import filteredCategoriesInjectable from "../../../common/catalog/filtered-categories.injectable";
import { TreeGroup, TreeItem, TreeView } from "../tree-view/tree-view";
import { browseCatalogTab } from "./catalog-browse-tab";
import { HorizontalLine } from "../horizontal-line/horizontal-line";
export interface CatalogMenuProps {
activeTab: string | undefined;
onItemClick: (id: string) => void;
}
function getCategoryIcon(category: CatalogCategory) {
const { icon } = category.metadata ?? {};
function CategoryIcon(props: { category: CatalogCategory }) {
const { icon } = props.category.metadata ?? {};
if (typeof icon === "string") {
return Icon.isSvg(icon)
@ -36,12 +35,6 @@ function getCategoryIcon(category: CatalogCategory) {
return null;
}
function Item(props: TreeItemProps) {
return (
<TreeItem classes={treeStyles} {...props}/>
);
}
interface Dependencies {
filteredCategories: IComputedValue<CatalogCategory[]>;
}
@ -51,42 +44,36 @@ const NonInjectedCatalogMenu = observer(({
filteredCategories,
onItemClick,
}: CatalogMenuProps & Dependencies) => (
// Overwrite Material UI styles with injectFirst https://material-ui.com/guides/interoperability/#controlling-priority-4
<StylesProvider injectFirst>
<div className="flex flex-col w-full">
<div className={styles.catalog}>Catalog</div>
<TreeView
defaultExpanded={["catalog"]}
defaultCollapseIcon={<Icon material="expand_more" />}
defaultExpandIcon={<Icon material="chevron_right" />}
selected={activeTab || "browse"}
<div className="flex flex-col w-full">
<div className={styles.catalog}>Catalog</div>
<TreeView>
<TreeItem
classes={treeStyles}
label="Browse"
data-testid="*-tab"
onClick={() => onItemClick("*")}
selected={activeTab === browseCatalogTab}
/>
<HorizontalLine size="xxs" />
<TreeGroup
classes={treeStyles}
label={<div className={styles.parent}>Categories</div>}
>
<Item
nodeId="browse"
label="Browse"
data-testid="*-tab"
onClick={() => onItemClick("*")} />
<Item
nodeId="catalog"
label={<div className={styles.parent}>Categories</div>}
className={cssNames(styles.bordered)}
>
{
filteredCategories.get()
.map(category => (
<Item
icon={getCategoryIcon(category)}
key={category.getId()}
nodeId={category.getId()}
label={<CatalogCategoryLabel category={category} />}
data-testid={`${category.getId()}-tab`}
onClick={() => onItemClick(category.getId())} />
))
}
</Item>
</TreeView>
</div>
</StylesProvider>
{filteredCategories.get()
.map(category => (
<TreeItem
classes={treeStyles}
key={category.getId()}
icon={<CategoryIcon category={category} />}
label={<CatalogCategoryLabel category={category} />}
selected={activeTab === category.getId()}
data-testid={`${category.getId()}-tab`}
onClick={() => onItemClick(category.getId())}
/>
))}
</TreeGroup>
</TreeView>
</div>
));
export const CatalogMenu = withInjectables<Dependencies, CatalogMenuProps>(NonInjectedCatalogMenu, {

View File

@ -23,39 +23,8 @@
.content {
min-height: 26px;
line-height: 1.3;
padding: 2px var(--padding) 2px 0;
&:hover {
background-color: var(--sidebarItemHoverBackground);
border-radius: 2px;
}
&:active {
color: white;
background-color: var(--blue);
}
}
.group {
margin-left: 0px;
.iconContainer {
margin-left: 28px;
margin-top: 2px;
align-self: flex-start;
}
}
.selected {
& > *:first-child {
background-color: var(--blue);
color: white;
border-radius: 2px;
}
}
.iconContainer {
width: 21px;
margin-left: 5px;
margin-right: 0;
}

View File

@ -135,16 +135,19 @@ class NonInjectedCatalog extends React.Component<Dependencies> {
}
}, { fireImmediately: true }),
// If active category is filtered out, automatically switch to the first category
reaction(() => catalogCategoryRegistry.filteredItems, () => {
if (!catalogCategoryRegistry.filteredItems.find(item => item.getId() === catalogEntityStore.activeCategory.get()?.getId())) {
const item = catalogCategoryRegistry.filteredItems[0];
reaction(() => [...catalogCategoryRegistry.filteredItems], (categories) => {
const currentCategory = catalogEntityStore.activeCategory.get();
const someCategory = categories[0];
runInAction(() => {
if (item) {
this.activeTab = item.getId();
this.props.catalogEntityStore.activeCategory.set(item);
}
});
if (this.routeActiveTab === browseCatalogTab || !someCategory) {
return;
}
const currentCategoryShouldBeShown = Boolean(categories.find(item => item.getId() === someCategory.getId()));
if (!currentCategory || !currentCategoryShouldBeShown) {
this.activeTab = someCategory.getId();
this.props.catalogEntityStore.activeCategory.set(someCategory);
}
}),
]);

View File

@ -17,6 +17,7 @@ const defaultBrowseAllColumns: RegisteredAdditionalCategoryColumn[] = [
id: "kind",
sortBy: "kind",
title: "Kind",
"data-testid": "catalog-kind-column",
},
sortCallback: entity => entity.kind,
},

View File

@ -25,6 +25,7 @@ const defaultCategoryColumnsInjectable = getInjectable({
className: styles.sourceCell,
id: "source",
sortBy: "source",
"data-testid": "catalog-source-column",
},
sortCallback: entity => entity.getSource(),
searchFilter: entity => `source=${entity.getSource()}`,
@ -37,6 +38,7 @@ const defaultCategoryColumnsInjectable = getInjectable({
id: "labels",
title: "Labels",
className: `${styles.labelsCell} scrollable`,
"data-testid": "catalog-labels-column",
},
searchFilter: entity => KubeObject.stringifyLabels(entity.metadata.labels),
},
@ -53,6 +55,7 @@ const defaultCategoryColumnsInjectable = getInjectable({
className: styles.statusCell,
id: "status",
sortBy: "status",
"data-testid": "catalog-status-column",
},
searchFilter: entity => entity.status.phase,
sortCallback: entity => entity.status.phase,

View File

@ -13,6 +13,7 @@ import type { TableCellProps } from "../table";
export interface TitleCellProps {
className?: string;
title: string;
"data-testid"?: string;
}
export interface CategoryColumnRegistration {

View File

@ -1,102 +0,0 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import React from "react";
import { screen } from "@testing-library/react";
import "@testing-library/jest-dom/extend-expect";
import { defaultWidth, Welcome } from "../welcome";
import { computed } from "mobx";
import { getDiForUnitTesting } from "../../../getDiForUnitTesting";
import type { DiRender } from "../../test-utils/renderFor";
import { renderFor } from "../../test-utils/renderFor";
import type { DiContainer } from "@ogre-tools/injectable";
import rendererExtensionsInjectable from "../../../../extensions/renderer-extensions.injectable";
import { LensRendererExtension } from "../../../../extensions/lens-renderer-extension";
import type { WelcomeBannerRegistration } from "../welcome-banner-items/welcome-banner-registration";
import currentlyInClusterFrameInjectable from "../../../routes/currently-in-cluster-frame.injectable";
describe("<Welcome/>", () => {
let render: DiRender;
let di: DiContainer;
let welcomeBannersStub: WelcomeBannerRegistration[];
beforeEach(() => {
di = getDiForUnitTesting();
di.override(currentlyInClusterFrameInjectable, () => false);
render = renderFor(di);
welcomeBannersStub = [];
di.override(rendererExtensionsInjectable, () =>
computed(() => [
new TestExtension({
id: "some-id",
welcomeBanners: welcomeBannersStub,
}),
]),
);
});
it("renders <Banner /> registered in WelcomeBannerRegistry and hide logo", async () => {
const testId = "testId";
welcomeBannersStub.push({
Banner: () => <div data-testid={testId} />,
});
const { container } = render(<Welcome />);
expect(screen.queryByTestId(testId)).toBeInTheDocument();
expect(container.getElementsByClassName("logo").length).toBe(0);
});
it("calculates max width from WelcomeBanner.width registered in WelcomeBannerRegistry", async () => {
welcomeBannersStub.push({
width: 100,
Banner: () => <div />,
});
welcomeBannersStub.push({
width: 800,
Banner: () => <div />,
});
render(<Welcome />);
expect(screen.queryByTestId("welcome-banner-container")).toHaveStyle({
// should take the max width of the banners (if > defaultWidth)
width: `800px`,
});
expect(screen.queryByTestId("welcome-text-container")).toHaveStyle({
width: `${defaultWidth}px`,
});
expect(screen.queryByTestId("welcome-menu-container")).toHaveStyle({
width: `${defaultWidth}px`,
});
});
});
class TestExtension extends LensRendererExtension {
constructor({
id,
welcomeBanners,
}: {
id: string;
welcomeBanners: WelcomeBannerRegistration[];
}) {
super({
id,
absolutePath: "irrelevant",
isBundled: false,
isCompatible: false,
isEnabled: false,
manifest: { name: id, version: "some-version", engines: { lens: "^5.5.0" }},
manifestPath: "irrelevant",
});
this.welcomeBanners = welcomeBanners;
}
}

View File

@ -71,7 +71,11 @@ const NonInjectedWelcome = observer(({
))}
</Carousel>
) : (
<Icon svg="logo-lens" className="logo" />
<Icon
svg="logo-lens"
className="logo"
data-testid="no-welcome-banners-icon"
/>
)}
<div className="flex justify-center">

View File

@ -6,7 +6,7 @@ import { getInjectionToken } from "@ogre-tools/injectable";
import type { IComputedValue } from "mobx";
import type React from "react";
interface WorkloadOverviewDetail {
export interface WorkloadOverviewDetail {
orderNumber: number;
Component: React.ElementType<{}>;
enabled: IComputedValue<boolean>;

View File

@ -16,43 +16,29 @@ const workloadOverviewDetailRegistratorInjectable = getInjectable({
instantiate: (di) => {
const getRandomId = di.inject(getRandomIdInjectable);
const getExtensionShouldBeEnabledForClusterFrame = (
extension: LensRendererExtension,
) =>
di.inject(extensionShouldBeEnabledForClusterFrameInjectable, extension);
return (ext) => {
const extension = ext as LensRendererExtension;
const extensionShouldBeEnabledForClusterFrame = di.inject(extensionShouldBeEnabledForClusterFrameInjectable, extension);
const extensionShouldBeEnabledForClusterFrame =
getExtensionShouldBeEnabledForClusterFrame(extension);
return extension.kubeWorkloadsOverviewItems.map((registration) => getInjectable({
id: `workload-overview-detail-from-${extension.sanitizedExtensionId}-${getRandomId()}`,
return extension.kubeWorkloadsOverviewItems.map((registration) => {
const id = `workload-overview-detail-from-${
extension.sanitizedExtensionId
}-${getRandomId()}`;
instantiate: () => ({
Component: registration.components.Details,
return getInjectable({
id,
enabled: computed(() => {
if (!extensionShouldBeEnabledForClusterFrame.value.get()) {
return false;
}
instantiate: () => ({
Component: registration.components.Details,
enabled: computed(() => {
if (!extensionShouldBeEnabledForClusterFrame.value.get()) {
return false;
}
return registration.visible ? registration.visible.get() : true;
}),
orderNumber:
0.5 + (registration.priority ? 100 - registration.priority : 50),
return registration.visible ? registration.visible.get() : true;
}),
injectionToken: workloadOverviewDetailInjectionToken,
});
});
orderNumber: 0.5 + (registration.priority ? 100 - registration.priority : 50),
}),
injectionToken: workloadOverviewDetailInjectionToken,
}));
};
},

View File

@ -13,7 +13,6 @@ import { HotbarMenu } from "../hotbar/hotbar-menu";
import { DeleteClusterDialog } from "../delete-cluster-dialog";
import { withInjectables } from "@ogre-tools/injectable-react";
import { TopBar } from "../layout/top-bar/top-bar";
import catalogPreviousActiveTabStorageInjectable from "../+catalog/catalog-previous-active-tab-storage/catalog-previous-active-tab-storage.injectable";
import type { IComputedValue } from "mobx";
import currentRouteComponentInjectable from "../../routes/current-route-component.injectable";
import welcomeRouteInjectable from "../../../common/front-end-routing/routes/welcome/welcome-route.injectable";
@ -21,10 +20,8 @@ import { buildURL } from "@k8slens/utilities";
import type { WatchForGeneralEntityNavigation } from "../../api/helpers/watch-for-general-entity-navigation.injectable";
import watchForGeneralEntityNavigationInjectable from "../../api/helpers/watch-for-general-entity-navigation.injectable";
import currentPathInjectable from "../../routes/current-path.injectable";
import type { StorageLayer } from "../../utils/storage-helper";
interface Dependencies {
catalogPreviousActiveTabStorage: StorageLayer<string | null>;
currentRouteComponent: IComputedValue<React.ElementType | undefined>;
welcomeUrl: string;
watchForGeneralEntityNavigation: WatchForGeneralEntityNavigation;
@ -84,7 +81,6 @@ class NonInjectedClusterManager extends React.Component<Dependencies> {
export const ClusterManager = withInjectables<Dependencies>(NonInjectedClusterManager, {
getProps: (di) => ({
catalogPreviousActiveTabStorage: di.inject(catalogPreviousActiveTabStorageInjectable),
currentRouteComponent: di.inject(currentRouteComponentInjectable),
welcomeUrl: buildURL(di.inject(welcomeRouteInjectable).path),
watchForGeneralEntityNavigation: di.inject(watchForGeneralEntityNavigationInjectable),

View File

@ -20,6 +20,9 @@
$baseline: 8px;
@include horizontalLineSize('xxs', 0.5 * $baseline);
@include horizontalLineSize('xs', 1 * $baseline);
@include horizontalLineSize('sm', 2 * $baseline);
@include horizontalLineSize('md', 3 * $baseline);
@include horizontalLineSize('lg', 4 * $baseline);
@include horizontalLineSize('xl', 5 * $baseline);

View File

@ -7,7 +7,7 @@ import styles from "./horizontal-line.module.scss";
import { cssNames } from "@k8slens/utilities";
interface HorizontalLineProps {
size?: "sm" | "md" | "xl";
size?: "xxs" | "xs" | "sm" | "md" | "lg" | "xl";
}
export const HorizontalLine = ({ size = "xl" }: HorizontalLineProps = { size: "xl" }) => {

View File

@ -25,6 +25,8 @@ import createEditResourceTabInjectable from "../dock/edit-resource/edit-resource
import hideDetailsInjectable from "../kube-detail-params/hide-details.injectable";
import { kubeObjectMenuItemInjectionToken } from "./kube-object-menu-item-injection-token";
import activeEntityInternalClusterInjectable from "../../api/catalog/entity/get-active-cluster-entity.injectable";
import directoryForTempInjectable from "../../../common/app-paths/directory-for-temp/directory-for-temp.injectable";
import directoryForUserDataInjectable from "../../../common/app-paths/directory-for-user-data/directory-for-user-data.injectable";
// TODO: make `animated={false}` not required to make tests deterministic
describe("kube-object-menu", () => {
@ -34,6 +36,9 @@ describe("kube-object-menu", () => {
beforeEach(() => {
di = getDiForUnitTesting();
di.override(directoryForUserDataInjectable, () => "/some-directory-for-user-data");
di.override(directoryForTempInjectable, () => "/some-directory-for-temp");
runInAction(() => {
di.register(
someMenuItemInjectable,

View File

@ -1,235 +0,0 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import React from "react";
import "@testing-library/jest-dom/extend-expect";
import { screen, waitFor } from "@testing-library/react";
import { ScrollSpy } from "../scroll-spy";
import { RecursiveTreeView } from "../../tree-view";
import { getDiForUnitTesting } from "../../../getDiForUnitTesting";
import { type DiRender, renderFor } from "../../test-utils/renderFor";
const observe = jest.fn();
Object.defineProperty(window, "IntersectionObserver", {
writable: true,
value: jest.fn().mockImplementation(() => ({
observe,
unobserve: jest.fn(),
})),
});
describe("<ScrollSpy/>", () => {
let render: DiRender;
beforeEach(() => {
const di = getDiForUnitTesting();
render = renderFor(di);
});
it("renders w/o errors", () => {
const { container } = render((
<ScrollSpy
render={() => (
<div>
<section id="application">
<h1>Application</h1>
</section>
</div>
)}
/>
));
expect(container).toBeInstanceOf(HTMLElement);
});
it("calls intersection observer", () => {
render((
<ScrollSpy
render={() => (
<div>
<section id="application">
<h1>Application</h1>
</section>
</div>
)}
/>
));
expect(observe).toHaveBeenCalled();
});
it("renders dataTree component", async () => {
render((
<ScrollSpy
render={dataTree => (
<div>
<nav>
<RecursiveTreeView data={dataTree}/>
</nav>
<section id="application">
<h1>Application</h1>
</section>
</div>
)}
/>
));
expect(await screen.findByTestId("TreeView")).toBeInTheDocument();
});
it("throws if no sections founded", () => {
// Prevent writing to stderr during this render.
const err = console.error;
console.error = jest.fn();
expect(() => render((
<ScrollSpy
render={() => (
<div>
Content
</div>
)}
/>
))).toThrow();
// Restore writing to stderr.
console.error = err;
});
});
describe("<TreeView/> dataTree inside <ScrollSpy/>", () => {
let render: DiRender;
beforeEach(() => {
const di = getDiForUnitTesting();
render = renderFor(di);
});
it("contains links to all sections", async () => {
render((
<ScrollSpy
render={dataTree => (
<div>
<nav>
<RecursiveTreeView data={dataTree}/>
</nav>
<section id="application">
<h1>Application</h1>
<section id="appearance">
<h2>Appearance</h2>
</section>
<section id="theme">
<h2>Theme</h2>
<div>description</div>
</section>
</section>
</div>
)}
/>
));
expect(await screen.findByTitle("Application")).toBeInTheDocument();
expect(await screen.findByTitle("Appearance")).toBeInTheDocument();
expect(await screen.findByTitle("Theme")).toBeInTheDocument();
});
it("not showing links to sections without id", async () => {
const { queryByTitle } = render((
<ScrollSpy
render={dataTree => (
<div>
<nav>
<RecursiveTreeView data={dataTree}/>
</nav>
<section id="application">
<h1>Application</h1>
<section>
<h2>Kubectl</h2>
</section>
<section id="appearance">
<h2>Appearance</h2>
</section>
</section>
</div>
)}
/>
));
expect(await screen.findByTitle("Application")).toBeInTheDocument();
expect(await screen.findByTitle("Appearance")).toBeInTheDocument();
await waitFor(() => {
expect(queryByTitle("Kubectl")).not.toBeInTheDocument();
});
});
it("expands parent sections", async () => {
render((
<ScrollSpy
render={dataTree => (
<div>
<nav>
<RecursiveTreeView data={dataTree}/>
</nav>
<section id="application">
<h1>Application</h1>
<section id="appearance">
<h2>Appearance</h2>
</section>
<section id="theme">
<h2>Theme</h2>
<div>description</div>
</section>
</section>
<section id="Kubernetes">
<h1>Kubernetes</h1>
<section id="kubectl">
<h2>Kubectl</h2>
</section>
</section>
</div>
)}
/>
));
expect(await screen.findByTitle("Application")).toHaveAttribute("aria-expanded");
expect(await screen.findByTitle("Kubernetes")).toHaveAttribute("aria-expanded");
});
it("skips sections without headings", async () => {
render((
<ScrollSpy
render={dataTree => (
<div>
<nav>
<RecursiveTreeView data={dataTree}/>
</nav>
<section id="application">
<h1>Application</h1>
<section id="appearance">
<p>Appearance</p>
</section>
<section id="theme">
<h2>Theme</h2>
</section>
</section>
</div>
)}
/>
));
expect(await screen.findByTitle("Application")).toBeInTheDocument();
await waitFor(() => {
expect(screen.queryByTitle("appearance")).not.toBeInTheDocument();
expect(screen.queryByTitle("Appearance")).not.toBeInTheDocument();
});
});
});

View File

@ -6,7 +6,14 @@
import { observer } from "mobx-react";
import React, { useEffect, useRef, useState } from "react";
import { useMutationObserver } from "../../hooks";
import type { NavigationTree } from "../tree-view";
export interface NavigationTree {
id: string;
parentId?: string;
name: string;
selected?: boolean;
children?: NavigationTree[];
}
export interface ScrollSpyProps extends React.DOMAttributes<HTMLElement> {
render: (data: NavigationTree[]) => JSX.Element;

View File

@ -71,6 +71,11 @@ export interface TableCellProps extends React.DOMAttributes<HTMLDivElement> {
* indicator, might come from parent <TableHead>, don't use this prop outside (!)
*/
_nowrap?: boolean;
/**
* For passing in the testid
*/
"data-testid"?: string;
}
export class TableCell extends React.Component<TableCellProps> {

View File

@ -55,7 +55,7 @@ export interface TabProps<D> extends DOMAttributes<HTMLElement> {
className?: string;
active?: boolean;
disabled?: boolean;
icon?: React.ReactNode | string; // material-ui name or custom icon
icon?: React.ReactNode | string; // material-io name or custom icon
label?: React.ReactNode;
value: D;
}

View File

@ -53,7 +53,7 @@ import { applicationWindowInjectionToken } from "../../../main/start-main-applic
import closeAllWindowsInjectable from "../../../main/start-main-application/lens-window/hide-all-windows/close-all-windows.injectable";
import type { LensWindow } from "../../../main/start-main-application/lens-window/application-window/create-lens-window.injectable";
import type { FakeExtensionOptions } from "./get-extension-fake";
import { getMainExtensionFakeWith, getRendererExtensionFakeWith } from "./get-extension-fake";
import { getExtensionFakeForMain, getExtensionFakeForRenderer } from "./get-extension-fake";
import namespaceApiInjectable from "../../../common/k8s-api/endpoints/namespace.api.injectable";
import { Namespace } from "../../../common/k8s-api/endpoints";
import { getOverrideFsWithFakes } from "../../../test-utils/override-fs-with-fakes";
@ -594,13 +594,13 @@ export const getApplicationBuilder = () => {
enable: (...extensions) => {
builder.afterWindowStart(action(({ windowDi }) => {
extensions
.map(getRendererExtensionFakeWith(windowDi))
.map(getExtensionFakeForRenderer)
.forEach(enableExtensionFor(windowDi, rendererExtensionsStateInjectable));
}));
builder.afterApplicationStart(action(({ mainDi }) => {
extensions
.map(getMainExtensionFakeWith(mainDi))
.map(getExtensionFakeForMain)
.forEach(enableExtensionFor(mainDi, mainExtensionsStateInjectable));
}));
},

View File

@ -2,20 +2,8 @@
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import type { Writable } from "type-fest";
import { lensExtensionDependencies } from "../../../extensions/lens-extension";
import { LensMainExtension } from "../../../extensions/lens-main-extension";
import navigateForExtensionInjectable from "../../../main/start-main-application/lens-window/navigate-for-extension.injectable";
import { LensRendererExtension } from "../../../extensions/lens-renderer-extension";
import catalogCategoryRegistryInjectable from "../../../common/catalog/category-registry.injectable";
import getExtensionPageParametersInjectable from "../../routes/get-extension-page-parameters.injectable";
import navigateToRouteInjectable from "../../routes/navigate-to-route.injectable";
import routesInjectable from "../../routes/routes.injectable";
import catalogEntityRegistryForMainInjectable from "../../../main/catalog/entity-registry.injectable";
import catalogEntityRegistryForRendererInjectable from "../../api/catalog/entity/registry.injectable";
import type { DiContainer } from "@ogre-tools/injectable";
import loggerInjectable from "../../../common/logger.injectable";
import ensureHashedDirectoryForExtensionInjectable from "../../../extensions/extension-loader/file-system-provisioner-store/ensure-hashed-directory-for-extension.injectable";
export class TestExtensionMain extends LensMainExtension {}
export class TestExtensionRenderer extends LensRendererExtension {}
@ -27,63 +15,44 @@ export interface FakeExtensionOptions {
mainOptions?: Partial<LensMainExtension>;
}
export const getMainExtensionFakeWith = (di: DiContainer) => ({ id, name, mainOptions = {}}: FakeExtensionOptions) => {
const instance = new TestExtensionMain({
id,
absolutePath: "irrelevant",
isBundled: false,
isCompatible: false,
isEnabled: false,
manifest: {
name,
version: "1.0.0",
engines: {
lens: "^5.5.0",
export const getExtensionFakeForMain = ({ id, name, mainOptions = {}}: FakeExtensionOptions) => (
Object.assign(
new TestExtensionMain({
id,
absolutePath: "irrelevant",
isBundled: false,
isCompatible: false,
isEnabled: false,
manifest: {
name,
version: "1.0.0",
engines: {
lens: "^5.5.0",
},
},
},
manifestPath: "irrelevant",
});
manifestPath: "irrelevant",
}),
mainOptions,
)
);
Object.assign(instance, mainOptions);
(instance as Writable<LensMainExtension>)[lensExtensionDependencies] = {
ensureHashedDirectoryForExtension: di.inject(ensureHashedDirectoryForExtensionInjectable),
entityRegistry: di.inject(catalogEntityRegistryForMainInjectable),
navigate: di.inject(navigateForExtensionInjectable),
logger: di.inject(loggerInjectable),
};
return instance;
};
export const getRendererExtensionFakeWith = (di: DiContainer) => ({ id, name, rendererOptions = {}}: FakeExtensionOptions) => {
const instance = new TestExtensionRenderer({
id,
absolutePath: "irrelevant",
isBundled: false,
isCompatible: false,
isEnabled: false,
manifest: {
name,
version: "1.0.0",
engines: {
lens: "^5.5.0",
export const getExtensionFakeForRenderer = ({ id, name, rendererOptions = {}}: FakeExtensionOptions) => (
Object.assign(
new TestExtensionRenderer({
id,
absolutePath: "irrelevant",
isBundled: false,
isCompatible: false,
isEnabled: false,
manifest: {
name,
version: "1.0.0",
engines: {
lens: "^5.5.0",
},
},
},
manifestPath: "irrelevant",
});
Object.assign(instance, rendererOptions);
(instance as Writable<LensRendererExtension>)[lensExtensionDependencies] = {
categoryRegistry: di.inject(catalogCategoryRegistryInjectable),
entityRegistry: di.inject(catalogEntityRegistryForRendererInjectable),
ensureHashedDirectoryForExtension: di.inject(ensureHashedDirectoryForExtensionInjectable),
getExtensionPageParameters: di.inject(getExtensionPageParametersInjectable),
navigateToRoute: di.inject(navigateToRouteInjectable),
routes: di.inject(routesInjectable),
logger: di.inject(loggerInjectable),
};
return instance;
};
manifestPath: "irrelevant",
}),
rendererOptions,
)
);

View File

@ -1,6 +0,0 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
export * from "./tree-view";

View File

@ -0,0 +1,63 @@
.treeItem {
display: flex;
flex-direction: row;
padding: 2px var(--padding) 2px 0;
cursor: pointer;
&:hover {
background-color: var(--sidebarItemHoverBackground);
}
&.selected:hover {
background-color: var(--blue);
}
}
.selected {
background-color: var(--blue);
color: white;
border-radius: 2px;
width: 100%;
}
.treeGroup {
display: flex;
flex-direction: column;
cursor: pointer;
}
.contents {
padding-left: 25px;
transition: all 300ms ease;
overflow: hidden;
&.expanded {
max-height: 100%;
}
&:not(.expanded) {
max-height: 0;
}
}
.selected {
color: white;
background-color: var(--blue);
}
.group {
display: flex;
flex-direction: row;
}
.iconContainer {
align-self: flex-start;
width: 21px;
margin-left: 5px;
margin-right: 5px;
}
.treeView {
display: flex;
flex-direction: column;
}

View File

@ -1,32 +0,0 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
.TreeView {
.MuiTypography-body1 {
font-size: var(--font-size);
color: var(--textColorAccent);
}
.MuiTreeItem-root {
> .MuiTreeItem-content .MuiTreeItem-label {
border-radius: 4px;
border: 1px solid transparent;
}
&.selected {
> .MuiTreeItem-content .MuiTreeItem-label {
border-color: var(--blue);
font-weight: bold;
}
}
// Make inner component selected state invisible
&.Mui-selected, &.Mui-selected:focus {
> .MuiTreeItem-content .MuiTreeItem-label {
background-color: transparent;
}
}
}
}

View File

@ -3,103 +3,153 @@
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import "./tree-view.scss";
import React, { useEffect, useRef } from "react";
import { Icon } from "../icon";
import TreeView from "@material-ui/lab/TreeView";
import TreeItem from "@material-ui/lab/TreeItem";
import styles from "./tree-view.module.scss";
import type { MouseEventHandler } from "react";
import React, { useState } from "react";
import { cssNames } from "@k8slens/utilities";
import { Icon } from "../icon";
import _ from "lodash";
import getDeepDash from "deepdash";
export interface TreeViewClasses {
root?: string;
}
const deepDash = getDeepDash(_);
export interface TreeViewProps {
classes?: TreeViewClasses;
children: React.ReactNode;
}
export interface NavigationTree {
id: string;
parentId?: string;
name: string;
export function TreeView(props: TreeViewProps) {
const {
children,
classes = {},
} = props;
return (
<ul
className={cssNames(classes.root, styles.treeView)}
role="tree"
>
{children}
</ul>
);
}
export interface TreeItemClasses {
root?: string;
label?: string;
selected?: string;
hover?: string;
iconContainer?: string;
}
export interface TreeItemProps {
classes?: TreeItemClasses;
icon?: JSX.Element;
label: JSX.Element | string;
"data-testid"?: string;
selected?: boolean;
children?: NavigationTree[];
onClick?: MouseEventHandler;
}
export interface RecursiveTreeViewProps {
data: NavigationTree[];
}
export function TreeItem(props: TreeItemProps) {
const {
label,
"data-testid": dataTestId,
classes = {},
icon,
onClick,
selected = false,
} = props;
const [hovering, setHovering] = useState(false);
const optionalCssNames: Partial<Record<string, any>> = {};
function scrollToItem(id: string) {
document.getElementById(id)?.scrollIntoView();
}
if (classes.selected) {
optionalCssNames[classes.selected] = selected;
}
function getSelectedNode(data: NavigationTree[]) {
return deepDash.findDeep(data, (value, key) => key === "selected" && value === true)?.parent;
}
export function RecursiveTreeView({ data }: RecursiveTreeViewProps) {
const [expanded, setExpanded] = React.useState<string[]>([]);
const prevData = useRef<NavigationTree[]>(data);
const handleToggle = (event: React.ChangeEvent<{}>, nodeIds: string[]) => {
setExpanded(nodeIds);
};
const expandTopLevelNodes = () => {
setExpanded(data.map(node => node.id));
};
const expandParentNode = () => {
const node = getSelectedNode(data) as any as NavigationTree;
const id = node?.parentId;
if (id && !expanded.includes(id)) {
setExpanded([...expanded, id]);
}
};
const onLabelClick = (event: React.MouseEvent, nodeId: string) => {
event.preventDefault();
scrollToItem(nodeId);
};
const renderTree = (nodes: NavigationTree[]) => {
return nodes.map(node => (
<TreeItem
key={node.id}
nodeId={node.id}
label={node.name}
onLabelClick={(event) => onLabelClick(event, node.id)}
className={cssNames({ selected: node.selected })}
title={node.name}
>
{Array.isArray(node.children) ? node.children.map((node) => renderTree([node])) : null}
</TreeItem>
));
};
useEffect(() => {
if (!prevData.current.length) {
expandTopLevelNodes();
} else {
expandParentNode();
}
prevData.current = data;
}, [data]);
if (!data.length) {
return null;
if (classes.hover) {
optionalCssNames[classes.hover] = hovering;
}
return (
<TreeView
data-testid="TreeView"
className="TreeView"
expanded={expanded}
onNodeToggle={handleToggle}
defaultCollapseIcon={<Icon material="expand_more"/>}
defaultExpandIcon={<Icon material="chevron_right" />}
<li
className={cssNames(classes.root, optionalCssNames, styles.treeItem, {
[styles.selected]: selected,
})}
role="treeitem"
data-testid={dataTestId}
onClick={onClick}
onMouseOver={() => setHovering(true)}
onMouseLeave={() => setHovering(false)}
>
{renderTree(data)}
</TreeView>
<div className={cssNames(classes.iconContainer, styles.iconContainer)}>
{icon}
</div>
<div className={classes.label}>
{label}
</div>
</li>
);
}
export interface TreeGroupClasses {
root?: string;
group?: string;
iconContainer?: string;
label?: string;
contents?: string;
}
export interface TreeGroupProps {
classes?: TreeGroupClasses;
children?: JSX.Element[] | JSX.Element;
defaultExpanded?: boolean;
label: JSX.Element | string;
"data-testid"?: string;
collapseIcon?: JSX.Element;
expandIcon?: JSX.Element;
}
export function TreeGroup(props: TreeGroupProps) {
const {
label,
"data-testid": dataTestId,
children,
classes = {},
collapseIcon,
defaultExpanded = true,
expandIcon,
} = props;
const [expanded, setExpanded] = useState(defaultExpanded);
return (
<li
className={cssNames(classes.root, styles.treeGroup)}
role="group"
data-testid={dataTestId}
>
<div
className={cssNames(classes.group, styles.group)}
onClick={() => setExpanded(!expanded)}
>
<div className={cssNames(classes.iconContainer, styles.iconContainer)}>
{
expanded
? collapseIcon ?? <Icon material="expand_more" />
: expandIcon ?? <Icon material="chevron_right" />
}
</div>
<div className={classes.label}>
{label}
</div>
</div>
<ul
className={cssNames(classes.contents, styles.contents, {
[styles.expanded]: expanded,
})}
>
{children}
</ul>
</li>
);
}

View File

@ -1,43 +0,0 @@
/**
* 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 type { Writable } from "type-fest";
import catalogCategoryRegistryInjectable from "../../common/catalog/category-registry.injectable";
import loggerInjectable from "../../common/logger.injectable";
import { createExtensionInstanceInjectionToken } from "../../extensions/extension-loader/create-extension-instance.token";
import ensureHashedDirectoryForExtensionInjectable from "../../extensions/extension-loader/file-system-provisioner-store/ensure-hashed-directory-for-extension.injectable";
import { lensExtensionDependencies } from "../../extensions/lens-extension";
import type { LensRendererExtensionDependencies } from "../../extensions/lens-extension-set-dependencies";
import type { LensRendererExtension } from "../../extensions/lens-renderer-extension";
import catalogEntityRegistryInjectable from "../api/catalog/entity/registry.injectable";
import getExtensionPageParametersInjectable from "../routes/get-extension-page-parameters.injectable";
import navigateToRouteInjectable from "../routes/navigate-to-route.injectable";
import routesInjectable from "../routes/routes.injectable";
const createExtensionInstanceInjectable = getInjectable({
id: "create-extension-instance",
instantiate: (di) => {
const deps: LensRendererExtensionDependencies = {
categoryRegistry: di.inject(catalogCategoryRegistryInjectable),
entityRegistry: di.inject(catalogEntityRegistryInjectable),
ensureHashedDirectoryForExtension: di.inject(ensureHashedDirectoryForExtensionInjectable),
getExtensionPageParameters: di.inject(getExtensionPageParametersInjectable),
navigateToRoute: di.inject(navigateToRouteInjectable),
routes: di.inject(routesInjectable),
logger: di.inject(loggerInjectable),
};
return (ExtensionClass, extension) => {
const instance = new ExtensionClass(extension as any) as LensRendererExtension;
(instance as Writable<LensRendererExtension>)[lensExtensionDependencies] = deps;
return instance;
};
},
injectionToken: createExtensionInstanceInjectionToken,
});
export default createExtensionInstanceInjectable;

View File

@ -4,10 +4,12 @@ import type {
BundledLensExtensionManifest,
} from "./lens-extension";
export type BundledExtensionResult = BundledLensExtensionConstructor | null;
export interface BundledExtension {
readonly manifest: BundledLensExtensionManifest;
main: () => Promise<BundledLensExtensionConstructor | null>;
renderer: () => Promise<BundledLensExtensionConstructor | null>;
main: () => BundledExtensionResult | Promise<BundledExtensionResult>;
renderer: () => BundledExtensionResult | Promise<BundledExtensionResult>;
}
export const bundledExtensionInjectionToken =

View File

@ -21,7 +21,6 @@
"skipLibCheck": true,
"allowJs": false,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"importsNotUsedAsValues": "error",
"traceResolution": false,
"resolveJsonModule": true,