Merge branch 'master' into extension-auto-update

This commit is contained in:
Lauri Nevala authored and GitHub committed 2022-01-17 11:11:02 +02:00
commit ee265a9db6
163 files changed
+3307 -1656

No files matched your search

@@ -19,7 +19,6 @@
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
import mockFs from "mock-fs";
import { watch } from "chokidar";
import { ExtensionsStore } from "../extensions-store";
import path from "path";
@@ -30,6 +29,7 @@ import { AppPaths } from "../../common/app-paths";
import type { ExtensionLoader } from "../extension-loader";
import extensionLoaderInjectable from "../extension-loader/extension-loader.injectable";
import { getDiForUnitTesting } from "../getDiForUnitTesting";
import * as fse from "fs-extra";
jest.setTimeout(60_000);
@@ -43,6 +43,7 @@ jest.mock("../extension-installer", () => ({
installPackage: jest.fn(),
},
}));
jest.mock("fs-extra");
jest.mock("electron", () => ({
app: {
getVersion: () => "99.99.99",
@@ -63,6 +64,7 @@ AppPaths.init();
console = new Console(process.stdout, process.stderr); // fix mockFS
const mockedWatch = watch as jest.MockedFunction<typeof watch>;
const mockedFse = fse as jest.Mocked<typeof fse>;
describe("ExtensionDiscovery", () => {
let extensionLoader: ExtensionLoader;
@@ -77,63 +79,60 @@ describe("ExtensionDiscovery", () => {
extensionLoader = di.inject(extensionLoaderInjectable);
});
describe("with mockFs", () => {
beforeEach(() => {
mockFs({
[`${os.homedir()}/.k8slens/extensions/my-extension/package.json`]: JSON.stringify({
name: "my-extension",
}),
});
});
it("emits add for added extension", async (done) => {
let addHandler: (filePath: string) => void;
afterEach(() => {
mockFs.restore();
});
mockedFse.readJson.mockImplementation((p) => {
expect(p).toBe(path.join(os.homedir(), ".k8slens/extensions/my-extension/package.json"));
it("emits add for added extension", async (done) => {
let addHandler: (filePath: string) => void;
const mockWatchInstance: any = {
on: jest.fn((event: string, handler: typeof addHandler) => {
if (event === "add") {
addHandler = handler;
}
return mockWatchInstance;
}),
return {
name: "my-extension",
version: "1.0.0",
};
mockedWatch.mockImplementationOnce(() =>
(mockWatchInstance) as any,
);
const extensionDiscovery = ExtensionDiscovery.createInstance(
extensionLoader,
);
// Need to force isLoaded to be true so that the file watching is started
extensionDiscovery.isLoaded = true;
await extensionDiscovery.watchExtensions();
extensionDiscovery.events.on("add", extension => {
expect(extension).toEqual({
absolutePath: expect.any(String),
id: path.normalize("node_modules/my-extension/package.json"),
isBundled: false,
isEnabled: false,
isCompatible: false,
manifest: {
name: "my-extension",
},
manifestPath: path.normalize("node_modules/my-extension/package.json"),
availableUpdate: null,
});
done();
});
addHandler(path.join(extensionDiscovery.localFolderPath, "/my-extension/package.json"));
});
mockedFse.pathExists.mockImplementation(() => true);
const mockWatchInstance: any = {
on: jest.fn((event: string, handler: typeof addHandler) => {
if (event === "add") {
addHandler = handler;
}
return mockWatchInstance;
}),
};
mockedWatch.mockImplementationOnce(() =>
(mockWatchInstance) as any,
);
const extensionDiscovery = ExtensionDiscovery.createInstance(
extensionLoader,
);
// Need to force isLoaded to be true so that the file watching is started
extensionDiscovery.isLoaded = true;
await extensionDiscovery.watchExtensions();
extensionDiscovery.events.on("add", extension => {
expect(extension).toEqual({
absolutePath: expect.any(String),
id: path.normalize("node_modules/my-extension/package.json"),
isBundled: false,
isEnabled: false,
isCompatible: false,
manifest: {
name: "my-extension",
version: "1.0.0",
},
manifestPath: path.normalize("node_modules/my-extension/package.json"),
availableUpdate: null,
});
done();
});
addHandler(path.join(extensionDiscovery.localFolderPath, "/my-extension/package.json"));
});
it("doesn't emit add for added file under extension", async done => {
@@ -150,7 +149,7 @@ describe("ExtensionDiscovery", () => {
};
mockedWatch.mockImplementationOnce(() =>
(mockWatchInstance) as any,
(mockWatchInstance) as any,
);
const extensionDiscovery = ExtensionDiscovery.createInstance(
extensionLoader,
@@ -173,3 +172,4 @@ describe("ExtensionDiscovery", () => {
}, 10);
});
});
@@ -18,13 +18,31 @@
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
import type { Injectable } from "@ogre-tools/injectable";
import { BaseRegistry } from "./base-registry";
import { getLegacyGlobalDiForExtensionApi } from "./legacy-global-di-for-extension-api";
export interface WelcomeMenuRegistration {
title: string | (() => string);
icon: string;
click: () => void | Promise<void>;
}
type TentativeTuple<T> = T extends object ? [T] : [undefined?];
export class WelcomeMenuRegistry extends BaseRegistry<WelcomeMenuRegistration> {}
type FactoryType = <
TInjectable extends Injectable<unknown, TInstance, TInstantiationParameter>,
TInstantiationParameter,
TInstance extends (...args: unknown[]) => any,
TFunction extends (...args: unknown[]) => any = Awaited<
ReturnType<TInjectable["instantiate"]>
>,
>(
injectableKey: TInjectable,
...instantiationParameter: TentativeTuple<TInstantiationParameter>
) => (...args: Parameters<TFunction>) => ReturnType<TFunction>;
export const asLegacyGlobalFunctionForExtensionApi: FactoryType =
(injectableKey, ...instantiationParameter) =>
(...args) => {
const injected = getLegacyGlobalDiForExtensionApi().inject(
injectableKey,
...instantiationParameter,
);
return injected(...args);
};
@@ -18,35 +18,40 @@
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
import type { Injectable } from "@ogre-tools/injectable";
import { getLegacyGlobalDiForExtensionApi } from "./legacy-global-di-for-extension-api";
// Extensions API -> Commands
type TentativeTuple<T> = T extends object ? [T] : [undefined?];
import { BaseRegistry } from "./base-registry";
import type { LensExtension } from "../lens-extension";
import type { CatalogEntity } from "../../common/catalog";
export const asLegacyGlobalObjectForExtensionApi = <
TInjectable extends Injectable<unknown, unknown, TInstantiationParameter>,
TInstantiationParameter,
>(
injectableKey: TInjectable,
...instantiationParameter: TentativeTuple<TInstantiationParameter>
) =>
new Proxy(
{},
{
get(target, propertyName) {
if (propertyName === "$$typeof") {
return undefined;
}
export interface CommandContext {
entity?: CatalogEntity;
}
const instance: any = getLegacyGlobalDiForExtensionApi().inject(
injectableKey,
...instantiationParameter,
);
export interface CommandRegistration {
id: string;
title: string;
scope: "entity" | "global";
action: (context: CommandContext) => void;
isActive?: (context: CommandContext) => boolean;
}
const propertyValue = instance[propertyName];
export class CommandRegistry extends BaseRegistry<CommandRegistration> {
add(items: CommandRegistration | CommandRegistration[], extension?: LensExtension) {
const itemArray = [items].flat();
if (typeof propertyValue === "function") {
return function (...args: any[]) {
return propertyValue.apply(instance, args);
};
}
const newIds = itemArray.map((item) => item.id);
const currentIds = this.getItems().map((item) => item.id);
const filteredIds = newIds.filter((id) => !currentIds.includes(id));
const filteredItems = itemArray.filter((item) => filteredIds.includes(item.id));
return super.add(filteredItems, extension);
}
}
return propertyValue;
},
},
) as ReturnType<TInjectable["instantiate"]>;
@@ -0,0 +1,59 @@
/**
* Copyright (c) 2021 OpenLens Authors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
* the Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
import type { Injectable } from "@ogre-tools/injectable";
import { getLegacyGlobalDiForExtensionApi } from "./legacy-global-di-for-extension-api";
type TentativeTuple<T> = T extends object ? [T] : [undefined?];
export const asLegacyGlobalSingletonForExtensionApi = <
TClass extends abstract new (...args: any[]) => any,
TInjectable extends Injectable<unknown, unknown, TInstantiationParameter>,
TInstantiationParameter,
>(
Class: TClass,
injectableKey: TInjectable,
...instantiationParameter: TentativeTuple<TInstantiationParameter>
) =>
new Proxy(Class, {
construct: () => {
throw new Error("A legacy singleton class must be created by createInstance()");
},
get: (target: any, propertyName) => {
if (propertyName === "getInstance" || propertyName === "createInstance") {
return () =>
getLegacyGlobalDiForExtensionApi().inject(
injectableKey,
...instantiationParameter,
);
}
if (propertyName === "resetInstance") {
return () => getLegacyGlobalDiForExtensionApi().purge(injectableKey);
}
return target[propertyName];
},
}) as InstanceType<TClass> & {
getInstance: () => InstanceType<TClass>;
createInstance: () => InstanceType<TClass>;
resetInstance: () => void;
};
@@ -18,17 +18,12 @@
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
import type { DependencyInjectionContainer } from "@ogre-tools/injectable";
import type React from "react";
import { BaseRegistry } from "./base-registry";
let legacyGlobalDi: DependencyInjectionContainer;
interface TopBarComponents {
Item: React.ComponentType;
}
export const setLegacyGlobalDiForExtensionApi = (di: DependencyInjectionContainer) => {
legacyGlobalDi = di;
};
export interface TopBarRegistration {
components: TopBarComponents;
}
export class TopBarRegistry extends BaseRegistry<TopBarRegistration> {
}
export const getLegacyGlobalDiForExtensionApi = () => legacyGlobalDi;
@@ -18,8 +18,7 @@
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
import { getInjectable } from "@ogre-tools/injectable";
import { lifecycleEnum } from "@ogre-tools/injectable";
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
import { ExtensionLoader } from "./extension-loader";
const extensionLoaderInjectable = getInjectable({
@@ -279,11 +279,7 @@ export class ExtensionLoader {
registries.AppPreferenceRegistry.getInstance().add(extension.appPreferences),
registries.EntitySettingRegistry.getInstance().add(extension.entitySettings),
registries.StatusBarRegistry.getInstance().add(extension.statusBarItems),
registries.CommandRegistry.getInstance().add(extension.commands),
registries.WelcomeMenuRegistry.getInstance().add(extension.welcomeMenus),
registries.WelcomeBannerRegistry.getInstance().add(extension.welcomeBanners),
registries.CatalogEntityDetailRegistry.getInstance().add(extension.catalogEntityDetailItems),
registries.TopBarRegistry.getInstance().add(extension.topBarItems),
];
this.events.on("remove", (removedExtension: LensRendererExtension) => {
@@ -315,7 +311,6 @@ export class ExtensionLoader {
registries.KubeObjectDetailRegistry.getInstance().add(extension.kubeObjectDetailItems),
registries.KubeObjectStatusRegistry.getInstance().add(extension.kubeObjectStatusTexts),
registries.WorkloadsOverviewDetailRegistry.getInstance().add(extension.kubeWorkloadsOverviewItems),
registries.CommandRegistry.getInstance().add(extension.commands),
];
this.events.on("remove", (removedExtension: LensRendererExtension) => {
-1
View File
@@ -41,7 +41,6 @@ export const getDiForUnitTesting = () => {
aliases: [injectable, ...(injectable.aliases || [])],
};
})
.forEach(injectable => di.register(injectable));
di.preventSideEffects();
+2 -1
View File
@@ -25,9 +25,10 @@ import { catalogEntityRegistry } from "../main/catalog";
import type { CatalogEntity } from "../common/catalog";
import type { IObservableArray } from "mobx";
import type { MenuRegistration } from "../main/menu/menu-registration";
import type { TrayMenuRegistration } from "../main/tray/tray-menu-registration";
export class LensMainExtension extends LensExtension {
appMenus: MenuRegistration[] = [];
trayMenus: TrayMenuRegistration[] = [];
async navigate(pageId?: string, params?: Record<string, any>, frameId?: number) {
return WindowManager.getInstance().navigateExtension(this.id, pageId, params, frameId);
+8 -4
View File
@@ -26,7 +26,11 @@ import type { CatalogEntity } from "../common/catalog";
import type { Disposer } from "../common/utils";
import { catalogEntityRegistry, EntityFilter } from "../renderer/api/catalog-entity-registry";
import { catalogCategoryRegistry, CategoryFilter } from "../renderer/api/catalog-category-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";
import type { WelcomeBannerRegistration } from "../renderer/components/+welcome/welcome-banner-items/welcome-banner-registration";
import type { CommandRegistration } from "../renderer/components/command-palette/registered-commands/commands";
export class LensRendererExtension extends LensExtension {
globalPages: registries.PageRegistration[] = [];
@@ -39,11 +43,11 @@ export class LensRendererExtension extends LensExtension {
kubeObjectDetailItems: registries.KubeObjectDetailRegistration[] = [];
kubeObjectMenuItems: registries.KubeObjectMenuRegistration[] = [];
kubeWorkloadsOverviewItems: registries.WorkloadsOverviewDetailRegistration[] = [];
commands: registries.CommandRegistration[] = [];
welcomeMenus: registries.WelcomeMenuRegistration[] = [];
welcomeBanners: registries.WelcomeBannerRegistration[] = [];
commands: CommandRegistration[] = [];
welcomeMenus: WelcomeMenuRegistration[] = [];
welcomeBanners: WelcomeBannerRegistration[] = [];
catalogEntityDetailItems: registries.CatalogEntityDetailRegistration<CatalogEntity>[] = [];
topBarItems: registries.TopBarRegistration[] = [];
topBarItems: TopBarRegistration[] = [];
async navigate<P extends object>(pageId?: string, params?: P) {
const { navigate } = await import("../renderer/navigation");
+1 -1
View File
@@ -22,7 +22,7 @@
// Base class for extensions-api registries
import { action, observable, makeObservable } from "mobx";
import { Singleton } from "../../common/utils";
import { LensExtension } from "../lens-extension";
import type { LensExtension } from "../lens-extension";
export class BaseRegistry<T, I = T> extends Singleton {
private items = observable.map<T, I>([], { deep: false });
@@ -54,7 +54,7 @@ export class EntitySettingRegistry extends BaseRegistry<EntitySettingRegistratio
};
}
getItemsForKind(kind: string, apiVersion: string, source?: string) {
getItemsForKind = (kind: string, apiVersion: string, source?: string) => {
let items = this.getItems().filter((item) => {
return item.kind === kind && item.apiVersions.includes(apiVersion);
});
@@ -66,5 +66,5 @@ export class EntitySettingRegistry extends BaseRegistry<EntitySettingRegistratio
}
return items.sort((a, b) => (b.priority ?? 50) - (a.priority ?? 50));
}
};
}
-4
View File
@@ -28,11 +28,7 @@ export * from "./status-bar-registry";
export * from "./kube-object-detail-registry";
export * from "./kube-object-menu-registry";
export * from "./kube-object-status-registry";
export * from "./command-registry";
export * from "./entity-setting-registry";
export * from "./welcome-menu-registry";
export * from "./welcome-banner-registry";
export * from "./catalog-entity-detail-registry";
export * from "./workloads-overview-detail-registry";
export * from "./topbar-registry";
export * from "./protocol-handler";
+1 -4
View File
@@ -135,10 +135,7 @@ class PageRegistry extends BaseRegistry<PageRegistration, RegisteredPage> {
);
if (notAStringValue && !(parse || stringify)) {
throw new Error(
`PageRegistry: param's "${paramName}" initialization has failed:
paramInit.parse() and paramInit.stringify() are required for non string | string[] "defaultValue"`,
);
throw new Error(`PageRegistry: param's "${paramName}" initialization has failed: paramInit.parse() and paramInit.stringify() are required for non string | string[] "defaultValue"`);
}
paramInit.defaultValue = value;
+4 -1
View File
@@ -19,6 +19,9 @@
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
import commandOverlayInjectable from "../../renderer/components/command-palette/command-overlay.injectable";
import { asLegacyGlobalObjectForExtensionApi } from "../as-legacy-globals-for-extension-api/as-legacy-global-object-for-extension-api";
// layouts
export * from "../../renderer/components/layout/main-layout";
export * from "../../renderer/components/layout/setting-layout";
@@ -36,7 +39,7 @@ export * from "../../renderer/components/switch";
export * from "../../renderer/components/input/input";
// command-overlay
export { CommandOverlay } from "../../renderer/components/command-palette";
export const CommandOverlay = asLegacyGlobalObjectForExtensionApi(commandOverlayInjectable);
// other components
export * from "../../renderer/components/icon";
@@ -18,22 +18,16 @@
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
import type { IComputedValue } from "mobx";
import extensionsInjectable from "./extensions.injectable";
import type { LensRendererExtension } from "./lens-renderer-extension";
import { BaseRegistry } from "./base-registry";
const rendererExtensionsInjectable = getInjectable({
lifecycle: lifecycleEnum.singleton,
/**
* WelcomeBannerRegistration is for an extension to register
* Provide a Banner component to be renderered in the welcome screen.
*/
export interface WelcomeBannerRegistration {
/**
* The banner component to be shown on the welcome screen.
*/
Banner?: React.ComponentType
/**
* The banner width in px.
*/
width?: number
}
instantiate: (di) =>
di.inject(extensionsInjectable) as IComputedValue<LensRendererExtension[]>,
});
export class WelcomeBannerRegistry extends BaseRegistry<WelcomeBannerRegistration> { }
export default rendererExtensionsInjectable;