mirror of
https://github.com/lensapp/lens.git
synced 2025-05-20 05:10:56 +00:00
Restructure to monorepo (#6907)
* wip: restructure to monorepo Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com> * refactor create-release-pr to a package Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com> * build fixes Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com> * github workflow fixes Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com> * fix typo Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com> * add webpack-env types to core Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com> * fix github workflows Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com> * refactor/fix integration tests Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com> * lint fix Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com> * yarn run dev Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com> * eslint settings for vscode Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com> * move templates to right package Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com> * open-lens build fixes Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com> * integration test fix Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com> * fix nx task dependencies Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com> * use bash shell for unit tests in test workflow Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com> * fix test:unit for windows Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com> * fix win-ca webpack error in open-lens Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com> * fix win-ca webpack error in open-lens Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com> * fix build:app on windows Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com> * remove ELECTRON_BUILDER_EXTRA_ARGS Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com> * sync src/ from master Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com> * remove Makefile from core Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com> Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com>
This commit is contained in:
1 parent
0066ca4ea2
commit
2657df2293
3321 files changed
+28699
-11447
No files matched your search
@@ -0,0 +1,174 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
import type { ExtensionLoader } from "../extension-loader";
|
||||
import extensionLoaderInjectable from "../extension-loader/extension-loader.injectable";
|
||||
import { runInAction } from "mobx";
|
||||
import updateExtensionsStateInjectable from "../extension-loader/update-extensions-state/update-extensions-state.injectable";
|
||||
import { delay } from "../../renderer/utils";
|
||||
import { getDiForUnitTesting } from "../../renderer/getDiForUnitTesting";
|
||||
import ipcRendererInjectable from "../../renderer/utils/channel/ipc-renderer.injectable";
|
||||
import type { IpcRenderer } from "electron";
|
||||
import directoryForUserDataInjectable from "../../common/app-paths/directory-for-user-data/directory-for-user-data.injectable";
|
||||
import currentlyInClusterFrameInjectable from "../../renderer/routes/currently-in-cluster-frame.injectable";
|
||||
|
||||
const manifestPath = "manifest/path";
|
||||
const manifestPath2 = "manifest/path2";
|
||||
const manifestPath3 = "manifest/path3";
|
||||
|
||||
describe("ExtensionLoader", () => {
|
||||
let extensionLoader: ExtensionLoader;
|
||||
let updateExtensionStateMock: jest.Mock;
|
||||
|
||||
beforeEach(() => {
|
||||
const di = getDiForUnitTesting({ doGeneralOverrides: true });
|
||||
|
||||
di.override(directoryForUserDataInjectable, () => "/some-directory-for-user-data");
|
||||
di.override(currentlyInClusterFrameInjectable, () => false);
|
||||
|
||||
di.override(ipcRendererInjectable, () => ({
|
||||
invoke: jest.fn(async (channel: string) => {
|
||||
if (channel === "extension-loader:main:state") {
|
||||
return [
|
||||
[
|
||||
manifestPath,
|
||||
{
|
||||
manifest: {
|
||||
name: "TestExtension",
|
||||
version: "1.0.0",
|
||||
},
|
||||
id: manifestPath,
|
||||
absolutePath: "/test/1",
|
||||
manifestPath,
|
||||
isBundled: false,
|
||||
isEnabled: true,
|
||||
},
|
||||
],
|
||||
[
|
||||
manifestPath2,
|
||||
{
|
||||
manifest: {
|
||||
name: "TestExtension2",
|
||||
version: "2.0.0",
|
||||
},
|
||||
id: manifestPath2,
|
||||
absolutePath: "/test/2",
|
||||
manifestPath: manifestPath2,
|
||||
isBundled: false,
|
||||
isEnabled: true,
|
||||
},
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
return [];
|
||||
}),
|
||||
|
||||
on: (channel: string, listener: (event: any, ...args: any[]) => void) => {
|
||||
if (channel === "extension-loader:main:state") {
|
||||
// First initialize with extensions 1 and 2
|
||||
// and then broadcast event to remove extension 2 and add extension number 3
|
||||
setTimeout(() => {
|
||||
listener({}, [
|
||||
[
|
||||
manifestPath,
|
||||
{
|
||||
manifest: {
|
||||
name: "TestExtension",
|
||||
version: "1.0.0",
|
||||
},
|
||||
id: manifestPath,
|
||||
absolutePath: "/test/1",
|
||||
manifestPath,
|
||||
isBundled: false,
|
||||
isEnabled: true,
|
||||
},
|
||||
],
|
||||
[
|
||||
manifestPath3,
|
||||
{
|
||||
manifest: {
|
||||
name: "TestExtension3",
|
||||
version: "3.0.0",
|
||||
},
|
||||
id: manifestPath3,
|
||||
absolutePath: "/test/3",
|
||||
manifestPath: manifestPath3,
|
||||
isBundled: false,
|
||||
isEnabled: true,
|
||||
},
|
||||
],
|
||||
]);
|
||||
}, 10);
|
||||
}
|
||||
},
|
||||
}) as unknown as IpcRenderer);
|
||||
|
||||
updateExtensionStateMock = jest.fn();
|
||||
|
||||
di.override(updateExtensionsStateInjectable, () => updateExtensionStateMock);
|
||||
|
||||
extensionLoader = di.inject(extensionLoaderInjectable);
|
||||
});
|
||||
|
||||
it("renderer updates extension after ipc broadcast", async () => {
|
||||
expect(extensionLoader.userExtensions).toMatchInlineSnapshot(`Map {}`);
|
||||
|
||||
await extensionLoader.init();
|
||||
await delay(10);
|
||||
|
||||
// Assert the extensions after the extension broadcast event
|
||||
expect(extensionLoader.userExtensions).toMatchInlineSnapshot(`
|
||||
Map {
|
||||
"manifest/path" => Object {
|
||||
"absolutePath": "/test/1",
|
||||
"id": "manifest/path",
|
||||
"isBundled": false,
|
||||
"isEnabled": true,
|
||||
"manifest": Object {
|
||||
"name": "TestExtension",
|
||||
"version": "1.0.0",
|
||||
},
|
||||
"manifestPath": "manifest/path",
|
||||
},
|
||||
"manifest/path3" => Object {
|
||||
"absolutePath": "/test/3",
|
||||
"id": "manifest/path3",
|
||||
"isBundled": false,
|
||||
"isEnabled": true,
|
||||
"manifest": Object {
|
||||
"name": "TestExtension3",
|
||||
"version": "3.0.0",
|
||||
},
|
||||
"manifestPath": "manifest/path3",
|
||||
},
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
it("updates ExtensionsStore after isEnabled is changed", async () => {
|
||||
await extensionLoader.init();
|
||||
|
||||
expect(updateExtensionStateMock).not.toHaveBeenCalled();
|
||||
|
||||
runInAction(() => {
|
||||
extensionLoader.setIsEnabled("manifest/path", false);
|
||||
});
|
||||
|
||||
expect(updateExtensionStateMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
"manifest/path": {
|
||||
enabled: false,
|
||||
name: "TestExtension",
|
||||
},
|
||||
|
||||
"manifest/path2": {
|
||||
enabled: true,
|
||||
name: "TestExtension2",
|
||||
},
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,63 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
import { isCompatibleExtension } from "../extension-discovery/is-compatible-extension/is-compatible-extension";
|
||||
import type { LensExtensionManifest } from "../lens-extension";
|
||||
|
||||
describe("Extension/App versions compatibility checks", () => {
|
||||
it("is compatible with exact version matching", () => {
|
||||
expect(isCompatible({ extLensEngineVersion: "5.5.0", extensionApiVersion: "5.5.0" })).toBeTruthy();
|
||||
});
|
||||
|
||||
it("is compatible with upper %PATCH versions of base app", () => {
|
||||
expect(isCompatible({ extLensEngineVersion: "5.5.0", extensionApiVersion: "5.5.5" })).toBeTruthy();
|
||||
});
|
||||
|
||||
it("is compatible with higher %MINOR version of base app", () => {
|
||||
expect(isCompatible({ extLensEngineVersion: "5.5.0", extensionApiVersion: "5.6.0" })).toBeTruthy();
|
||||
});
|
||||
|
||||
it("is not compatible with higher %MAJOR version of base app", () => {
|
||||
expect(isCompatible({ extLensEngineVersion: "5.6.0", extensionApiVersion: "6.0.0" })).toBeFalsy(); // extension for lens@5 not compatible with lens@6
|
||||
expect(isCompatible({ extLensEngineVersion: "6.0.0", extensionApiVersion: "5.6.0" })).toBeFalsy();
|
||||
});
|
||||
|
||||
it("supports short version format for manifest.engines.lens", () => {
|
||||
expect(isCompatible({ extLensEngineVersion: "5.5", extensionApiVersion: "5.5.1" })).toBeTruthy();
|
||||
});
|
||||
|
||||
it("throws for incorrect or not supported version format", () => {
|
||||
expect(() => isCompatible({
|
||||
extLensEngineVersion: ">=2.0",
|
||||
extensionApiVersion: "2.0",
|
||||
})).toThrow(/Invalid format/i);
|
||||
|
||||
expect(() => isCompatible({
|
||||
extLensEngineVersion: "~2.0",
|
||||
extensionApiVersion: "2.0",
|
||||
})).toThrow(/Invalid format/i);
|
||||
|
||||
expect(() => isCompatible({
|
||||
extLensEngineVersion: "*",
|
||||
extensionApiVersion: "1.0",
|
||||
})).toThrow(/Invalid format/i);
|
||||
});
|
||||
});
|
||||
|
||||
function isCompatible({ extLensEngineVersion = "^1.0", extensionApiVersion = "1.0" } = {}): boolean {
|
||||
const extensionManifestMock = getExtensionManifestMock(extLensEngineVersion);
|
||||
|
||||
return isCompatibleExtension({ extensionApiVersion })(extensionManifestMock);
|
||||
}
|
||||
|
||||
function getExtensionManifestMock(lensEngine = "1.0"): LensExtensionManifest {
|
||||
return {
|
||||
name: "some-extension",
|
||||
version: "1.0",
|
||||
engines: {
|
||||
lens: lensEngine,
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* 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;
|
||||
|
||||
describe("lens extension", () => {
|
||||
beforeEach(async () => {
|
||||
ext = new LensExtension({
|
||||
manifest: {
|
||||
name: "foo-bar",
|
||||
version: "0.1.1",
|
||||
engines: { lens: "^5.5.0" },
|
||||
},
|
||||
id: "/this/is/fake/package.json",
|
||||
absolutePath: "/absolute/fake/",
|
||||
manifestPath: "/this/is/fake/package.json",
|
||||
isBundled: false,
|
||||
isEnabled: true,
|
||||
isCompatible: true,
|
||||
});
|
||||
});
|
||||
|
||||
describe("name", () => {
|
||||
it("returns name", () => {
|
||||
expect(ext.name).toBe("foo-bar");
|
||||
});
|
||||
});
|
||||
});
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
import { getLegacyGlobalDiForExtensionApi } from "./legacy-global-di-for-extension-api";
|
||||
import type { Inject } from "@ogre-tools/injectable";
|
||||
|
||||
export const asLegacyGlobalFunctionForExtensionApi = ((
|
||||
injectableKey,
|
||||
instantiationParameter,
|
||||
) =>
|
||||
(...args: any[]) => {
|
||||
const injected = getLegacyGlobalDiForExtensionApi().inject(
|
||||
injectableKey,
|
||||
instantiationParameter,
|
||||
) as unknown as (...args: any[]) => any;
|
||||
|
||||
return injected(...args);
|
||||
}) as Inject;
|
||||
+81
@@ -0,0 +1,81 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import type {
|
||||
DiContainer,
|
||||
Injectable } from "@ogre-tools/injectable";
|
||||
import {
|
||||
createContainer,
|
||||
getInjectable,
|
||||
} from "@ogre-tools/injectable";
|
||||
import { Environments, setLegacyGlobalDiForExtensionApi } from "./legacy-global-di-for-extension-api";
|
||||
import { asLegacyGlobalObjectForExtensionApiWithModifications } from "./as-legacy-global-object-for-extension-api-with-modifications";
|
||||
|
||||
describe("asLegacyGlobalObjectForExtensionApiWithModifications", () => {
|
||||
describe("given legacy global object", () => {
|
||||
let di: DiContainer;
|
||||
let someInjectable: Injectable<{ someProperty: string }, unknown, void>;
|
||||
let actual: { someProperty: string } & {
|
||||
someModificationProperty: string;
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
di = createContainer("irrelevant");
|
||||
|
||||
jest.spyOn(di, "inject");
|
||||
|
||||
setLegacyGlobalDiForExtensionApi(di, Environments.renderer);
|
||||
|
||||
someInjectable = getInjectable({
|
||||
id: "some-injectable",
|
||||
instantiate: () => ({
|
||||
someProperty: "some-property-value",
|
||||
}),
|
||||
});
|
||||
|
||||
di.register(someInjectable);
|
||||
|
||||
actual = asLegacyGlobalObjectForExtensionApiWithModifications(
|
||||
someInjectable,
|
||||
{ someModificationProperty: "some-modification-value" },
|
||||
);
|
||||
});
|
||||
|
||||
it("when not accessed, does not inject yet", () => {
|
||||
expect(di.inject).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
describe("when a property of global is accessed, ", () => {
|
||||
let actualPropertyValue: string;
|
||||
|
||||
beforeEach(() => {
|
||||
actualPropertyValue = actual.someProperty;
|
||||
});
|
||||
|
||||
it("injects the injectable for global", () => {
|
||||
expect(di.inject).toHaveBeenCalledWith(someInjectable, undefined);
|
||||
});
|
||||
|
||||
it("global has property of injectable", () => {
|
||||
expect(actualPropertyValue).toBe("some-property-value");
|
||||
});
|
||||
});
|
||||
|
||||
describe("when a property of modification is accessed, ", () => {
|
||||
let actualModificationValue: string;
|
||||
|
||||
beforeEach(() => {
|
||||
actualModificationValue = actual.someModificationProperty;
|
||||
});
|
||||
|
||||
it("injects the injectable for global", () => {
|
||||
expect(di.inject).toHaveBeenCalledWith(someInjectable, undefined);
|
||||
});
|
||||
|
||||
it("global has property of modification", () => {
|
||||
expect(actualModificationValue).toBe("some-modification-value");
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import { asLegacyGlobalForExtensionApi } from "./as-legacy-global-object-for-extension-api";
|
||||
import type { Injectable } from "@ogre-tools/injectable";
|
||||
|
||||
/**
|
||||
* @deprecated use asLegacyGlobalForExtensionApi instead, and use proper implementations instead of "modifications".
|
||||
*/
|
||||
export const asLegacyGlobalObjectForExtensionApiWithModifications = <
|
||||
InjectableInstance extends InjectionTokenInstance & object,
|
||||
InjectionTokenInstance,
|
||||
ModificationObject extends object,
|
||||
>(
|
||||
injectable: Injectable<InjectableInstance, InjectionTokenInstance, void>,
|
||||
modificationObject: ModificationObject,
|
||||
) =>
|
||||
Object.assign(
|
||||
asLegacyGlobalForExtensionApi(injectable),
|
||||
modificationObject,
|
||||
);
|
||||
+46
@@ -0,0 +1,46 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import type { Inject } from "@ogre-tools/injectable";
|
||||
import { getLegacyGlobalDiForExtensionApi } from "./legacy-global-di-for-extension-api";
|
||||
|
||||
export const asLegacyGlobalForExtensionApi = ((
|
||||
injectable,
|
||||
instantiationParameter,
|
||||
) =>
|
||||
new Proxy(
|
||||
{},
|
||||
|
||||
{
|
||||
apply(target: any, thisArg, argArray) {
|
||||
const fn = getLegacyGlobalDiForExtensionApi().inject(
|
||||
injectable,
|
||||
instantiationParameter,
|
||||
) as unknown as (...args: any[]) => any;
|
||||
|
||||
return fn(...argArray);
|
||||
},
|
||||
|
||||
get(target, propertyName) {
|
||||
if (propertyName === "$$typeof") {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const instance: any = getLegacyGlobalDiForExtensionApi().inject(
|
||||
injectable,
|
||||
instantiationParameter,
|
||||
);
|
||||
|
||||
const propertyValue = instance[propertyName] ?? target[propertyName];
|
||||
|
||||
if (typeof propertyValue === "function") {
|
||||
return function (...args: any[]) {
|
||||
return propertyValue.apply(instance, args);
|
||||
};
|
||||
}
|
||||
|
||||
return propertyValue;
|
||||
},
|
||||
},
|
||||
)) as Inject;
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import type { Injectable } from "@ogre-tools/injectable";
|
||||
import { asLegacyGlobalForExtensionApi } from "./as-legacy-global-object-for-extension-api";
|
||||
import { getLegacyGlobalDiForExtensionApi } from "./legacy-global-di-for-extension-api";
|
||||
import loggerInjectable from "../../common/logger.injectable";
|
||||
|
||||
export interface LegacySingleton<T> {
|
||||
createInstance(): T;
|
||||
getInstance(): T;
|
||||
resetInstance(): void;
|
||||
}
|
||||
|
||||
export function asLegacyGlobalSingletonForExtensionApi<Instance>(injectable: Injectable<Instance, unknown, void>): LegacySingleton<Instance>;
|
||||
export function asLegacyGlobalSingletonForExtensionApi<Instance, InstantiationParameter>(injectable: Injectable<Instance, unknown, InstantiationParameter>, param: InstantiationParameter): LegacySingleton<Instance>;
|
||||
|
||||
export function asLegacyGlobalSingletonForExtensionApi<Instance, InstantiationParameter>(
|
||||
injectable: Injectable<Instance, unknown, InstantiationParameter>,
|
||||
instantiationParameter?: InstantiationParameter,
|
||||
): LegacySingleton<Instance> {
|
||||
const instance = asLegacyGlobalForExtensionApi(
|
||||
injectable as never,
|
||||
instantiationParameter,
|
||||
) as Instance;
|
||||
|
||||
return {
|
||||
createInstance: () => instance,
|
||||
|
||||
getInstance: () => instance,
|
||||
|
||||
resetInstance: () => {
|
||||
const di = getLegacyGlobalDiForExtensionApi();
|
||||
const logger = di.inject(loggerInjectable);
|
||||
|
||||
logger.warn(
|
||||
`resetInstance() for a legacy global singleton of "${injectable.id}" does nothing.`,
|
||||
);
|
||||
},
|
||||
};
|
||||
}
|
||||
+43
@@ -0,0 +1,43 @@
|
||||
/**
|
||||
* 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";
|
||||
|
||||
const legacyGlobalDis = new Map<Environments, DiContainer>();
|
||||
|
||||
export enum Environments {
|
||||
renderer,
|
||||
main,
|
||||
}
|
||||
|
||||
export const setLegacyGlobalDiForExtensionApi = (
|
||||
di: DiContainer,
|
||||
environment: Environments,
|
||||
) => {
|
||||
legacyGlobalDis.set(environment, di);
|
||||
};
|
||||
|
||||
export const getLegacyGlobalDiForExtensionApi = () => {
|
||||
if (legacyGlobalDis.size > 1) {
|
||||
throw new Error("Tried to get DI container using legacy globals where there is multiple containers available.");
|
||||
}
|
||||
|
||||
const [di] = [...legacyGlobalDis.values()];
|
||||
|
||||
if (!di) {
|
||||
throw new Error("Tried to get DI container using legacy globals where there is no containers available.");
|
||||
}
|
||||
|
||||
return di;
|
||||
};
|
||||
|
||||
export function getEnvironmentSpecificLegacyGlobalDiForExtensionApi(environment: Environments) {
|
||||
const di = legacyGlobalDis.get(environment);
|
||||
|
||||
if (!di) {
|
||||
throw new Error("Tried to get DI container using legacy globals in environment which doesn't exist");
|
||||
}
|
||||
|
||||
return di;
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
import appNameInjectable from "../../common/vars/app-name.injectable";
|
||||
import isLinuxInjectable from "../../common/vars/is-linux.injectable";
|
||||
import isMacInjectable from "../../common/vars/is-mac.injectable";
|
||||
import isSnapPackageInjectable from "../../common/vars/is-snap-package.injectable";
|
||||
import isWindowsInjectable from "../../common/vars/is-windows.injectable";
|
||||
import { asLegacyGlobalFunctionForExtensionApi } from "../as-legacy-globals-for-extension-api/as-legacy-global-function-for-extension-api";
|
||||
import { getLegacyGlobalDiForExtensionApi } from "../as-legacy-globals-for-extension-api/legacy-global-di-for-extension-api";
|
||||
import getEnabledExtensionsInjectable from "./get-enabled-extensions/get-enabled-extensions.injectable";
|
||||
import type { UserPreferenceExtensionItems } from "./user-preferences";
|
||||
import { Preferences } from "./user-preferences";
|
||||
import { slackUrl, issuesTrackerUrl } from "../../common/vars";
|
||||
import { buildVersionInjectionToken } from "../../common/vars/build-semantic-version.injectable";
|
||||
|
||||
export interface AppExtensionItems {
|
||||
readonly Preferences: UserPreferenceExtensionItems;
|
||||
readonly version: string;
|
||||
readonly appName: string;
|
||||
readonly slackUrl: string;
|
||||
readonly issuesTrackerUrl: string;
|
||||
readonly isSnap: boolean;
|
||||
readonly isWindows: boolean;
|
||||
readonly isMac: boolean;
|
||||
readonly isLinux: boolean;
|
||||
getEnabledExtensions: () => string[];
|
||||
}
|
||||
|
||||
export const App: AppExtensionItems = {
|
||||
Preferences,
|
||||
getEnabledExtensions: asLegacyGlobalFunctionForExtensionApi(getEnabledExtensionsInjectable),
|
||||
get version() {
|
||||
const di = getLegacyGlobalDiForExtensionApi();
|
||||
|
||||
return di.inject(buildVersionInjectionToken).get();
|
||||
},
|
||||
get appName() {
|
||||
const di = getLegacyGlobalDiForExtensionApi();
|
||||
|
||||
return di.inject(appNameInjectable);
|
||||
},
|
||||
get isSnap() {
|
||||
const di = getLegacyGlobalDiForExtensionApi();
|
||||
|
||||
return di.inject(isSnapPackageInjectable);
|
||||
},
|
||||
get isWindows() {
|
||||
const di = getLegacyGlobalDiForExtensionApi();
|
||||
|
||||
return di.inject(isWindowsInjectable);
|
||||
},
|
||||
get isMac() {
|
||||
const di = getLegacyGlobalDiForExtensionApi();
|
||||
|
||||
return di.inject(isMacInjectable);
|
||||
},
|
||||
get isLinux() {
|
||||
const di = getLegacyGlobalDiForExtensionApi();
|
||||
|
||||
return di.inject(isLinuxInjectable);
|
||||
},
|
||||
slackUrl,
|
||||
issuesTrackerUrl,
|
||||
};
|
||||
@@ -0,0 +1,28 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
import kubernetesClusterCategoryInjectable from "../../common/catalog/categories/kubernetes-cluster.injectable";
|
||||
import { asLegacyGlobalForExtensionApi } from "../as-legacy-globals-for-extension-api/as-legacy-global-object-for-extension-api";
|
||||
|
||||
export {
|
||||
KubernetesCluster,
|
||||
GeneralEntity,
|
||||
WebLink,
|
||||
} from "../../common/catalog-entities";
|
||||
|
||||
export const kubernetesClusterCategory = asLegacyGlobalForExtensionApi(kubernetesClusterCategoryInjectable);
|
||||
|
||||
export type {
|
||||
KubernetesClusterPrometheusMetrics,
|
||||
KubernetesClusterSpec,
|
||||
KubernetesClusterMetadata,
|
||||
WebLinkSpec,
|
||||
WebLinkStatus,
|
||||
WebLinkStatusPhase,
|
||||
KubernetesClusterStatusPhase,
|
||||
KubernetesClusterStatus,
|
||||
} from "../../common/catalog-entities";
|
||||
|
||||
export * from "../../common/catalog/catalog-entity";
|
||||
@@ -0,0 +1,11 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
import appEventBusInjectable from "../../common/app-event-bus/app-event-bus.injectable";
|
||||
import { asLegacyGlobalForExtensionApi } from "../as-legacy-globals-for-extension-api/as-legacy-global-object-for-extension-api";
|
||||
|
||||
export type { AppEvent } from "../../common/app-event-bus/event-bus";
|
||||
|
||||
export const appEventBus = asLegacyGlobalForExtensionApi(appEventBusInjectable);
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import { getInjectable } from "@ogre-tools/injectable";
|
||||
import extensionsStoreInjectable from "../../extensions-store/extensions-store.injectable";
|
||||
|
||||
const getEnabledExtensionsInjectable = getInjectable({
|
||||
id: "get-enabled-extensions",
|
||||
|
||||
instantiate: (di) => () =>
|
||||
di.inject(extensionsStoreInjectable).enabledExtensions,
|
||||
});
|
||||
|
||||
export default getEnabledExtensionsInjectable;
|
||||
@@ -0,0 +1,28 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
// APIs
|
||||
import { App } from "./app";
|
||||
import * as EventBus from "./event-bus";
|
||||
import * as Store from "./stores";
|
||||
import { Util } from "./utils";
|
||||
import * as Catalog from "./catalog";
|
||||
import * as Types from "./types";
|
||||
import * as Proxy from "./proxy";
|
||||
import loggerInjectable from "../../common/logger.injectable";
|
||||
import { asLegacyGlobalForExtensionApi } from "../as-legacy-globals-for-extension-api/as-legacy-global-object-for-extension-api";
|
||||
|
||||
const logger = asLegacyGlobalForExtensionApi(loggerInjectable);
|
||||
|
||||
export {
|
||||
App,
|
||||
EventBus,
|
||||
Catalog,
|
||||
Store,
|
||||
Types,
|
||||
Util,
|
||||
logger,
|
||||
Proxy,
|
||||
};
|
||||
@@ -0,0 +1,279 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
// NOTE: this file is not currently exported as part of `Common`, but should be.
|
||||
// It is here to consolidate the common parts which are exported to `Main`
|
||||
// and to `Renderer`
|
||||
|
||||
import apiManagerInjectable from "../../common/k8s-api/api-manager/manager.injectable";
|
||||
import createKubeApiForClusterInjectable from "../../common/k8s-api/create-kube-api-for-cluster.injectable";
|
||||
import createKubeApiForRemoteClusterInjectable from "../../common/k8s-api/create-kube-api-for-remote-cluster.injectable";
|
||||
import createResourceStackInjectable from "../../common/k8s/create-resource-stack.injectable";
|
||||
import type { ResourceApplyingStack } from "../../common/k8s/resource-stack";
|
||||
import { asLegacyGlobalFunctionForExtensionApi } from "../as-legacy-globals-for-extension-api/as-legacy-global-function-for-extension-api";
|
||||
import { asLegacyGlobalForExtensionApi } from "../as-legacy-globals-for-extension-api/as-legacy-global-object-for-extension-api";
|
||||
import type { KubernetesCluster } from "./catalog";
|
||||
import type { KubeApiDataFrom, KubeObjectStoreOptions } from "../../common/k8s-api/kube-object.store";
|
||||
import { KubeObjectStore as InternalKubeObjectStore } from "../../common/k8s-api/kube-object.store";
|
||||
import type { KubeJsonApiDataFor, KubeObject } from "../../common/k8s-api/kube-object";
|
||||
import type { DerivedKubeApiOptions, KubeApiDependencies, KubeApiOptions } from "../../common/k8s-api/kube-api";
|
||||
import { KubeApi as InternalKubeApi } from "../../common/k8s-api/kube-api";
|
||||
import clusterFrameContextForNamespacedResourcesInjectable from "../../renderer/cluster-frame-context/for-namespaced-resources.injectable";
|
||||
import type { ClusterContext } from "../../renderer/cluster-frame-context/cluster-frame-context";
|
||||
import loggerInjectable from "../../common/logger.injectable";
|
||||
import { getLegacyGlobalDiForExtensionApi } from "../as-legacy-globals-for-extension-api/legacy-global-di-for-extension-api";
|
||||
import maybeKubeApiInjectable from "../../common/k8s-api/maybe-kube-api.injectable";
|
||||
import { DeploymentApi as InternalDeploymentApi, IngressApi as InternalIngressApi, NodeApi, PersistentVolumeClaimApi, PodApi } from "../../common/k8s-api/endpoints";
|
||||
import { storesAndApisCanBeCreatedInjectionToken } from "../../common/k8s-api/stores-apis-can-be-created.token";
|
||||
import type { JsonApiConfig } from "../../common/k8s-api/json-api";
|
||||
import type { KubeJsonApi as InternalKubeJsonApi } from "../../common/k8s-api/kube-json-api";
|
||||
import createKubeJsonApiInjectable from "../../common/k8s-api/create-kube-json-api.injectable";
|
||||
import type { RequestInit } from "node-fetch";
|
||||
import createKubeJsonApiForClusterInjectable from "../../common/k8s-api/create-kube-json-api-for-cluster.injectable";
|
||||
|
||||
export const apiManager = asLegacyGlobalForExtensionApi(apiManagerInjectable);
|
||||
export const forCluster = asLegacyGlobalFunctionForExtensionApi(createKubeApiForClusterInjectable);
|
||||
export const forRemoteCluster = asLegacyGlobalFunctionForExtensionApi(createKubeApiForRemoteClusterInjectable);
|
||||
export const createResourceStack = asLegacyGlobalFunctionForExtensionApi(createResourceStackInjectable);
|
||||
|
||||
const getKubeApiDeps = (): KubeApiDependencies => {
|
||||
const di = getLegacyGlobalDiForExtensionApi();
|
||||
|
||||
return {
|
||||
logger: di.inject(loggerInjectable),
|
||||
maybeKubeApi: di.inject(maybeKubeApiInjectable),
|
||||
};
|
||||
};
|
||||
|
||||
// NOTE: this is done to preserve `instanceOf` behaviour
|
||||
function KubeApiCstr<
|
||||
Object extends KubeObject = KubeObject,
|
||||
Data extends KubeJsonApiDataFor<Object> = KubeJsonApiDataFor<Object>,
|
||||
>(opts: KubeApiOptions<Object, Data>) {
|
||||
const api = new InternalKubeApi(getKubeApiDeps(), opts);
|
||||
|
||||
const di = getLegacyGlobalDiForExtensionApi();
|
||||
const storesAndApisCanBeCreated = di.inject(storesAndApisCanBeCreatedInjectionToken);
|
||||
|
||||
if (storesAndApisCanBeCreated) {
|
||||
apiManager.registerApi(api);
|
||||
}
|
||||
|
||||
return api;
|
||||
}
|
||||
|
||||
export type KubeApi<
|
||||
Object extends KubeObject = KubeObject,
|
||||
Data extends KubeJsonApiDataFor<Object> = KubeJsonApiDataFor<Object>,
|
||||
> = InternalKubeApi<Object, Data>;
|
||||
|
||||
export const KubeApi = KubeApiCstr as unknown as new<
|
||||
Object extends KubeObject = KubeObject,
|
||||
Data extends KubeJsonApiDataFor<Object> = KubeJsonApiDataFor<Object>,
|
||||
>(opts: KubeApiOptions<Object, Data>) => InternalKubeApi<Object, Data>;
|
||||
|
||||
/**
|
||||
* @deprecated Switch to using `Common.createResourceStack` instead
|
||||
*/
|
||||
export class ResourceStack implements ResourceApplyingStack {
|
||||
private readonly impl: ResourceApplyingStack;
|
||||
|
||||
constructor(cluster: KubernetesCluster, name: string) {
|
||||
this.impl = createResourceStack(cluster, name);
|
||||
}
|
||||
|
||||
kubectlApplyFolder(folderPath: string, templateContext?: any, extraArgs?: string[] | undefined): Promise<string> {
|
||||
return this.impl.kubectlApplyFolder(folderPath, templateContext, extraArgs);
|
||||
}
|
||||
|
||||
kubectlDeleteFolder(folderPath: string, templateContext?: any, extraArgs?: string[] | undefined): Promise<string> {
|
||||
return this.impl.kubectlDeleteFolder(folderPath, templateContext, extraArgs);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated This type is unused
|
||||
*/
|
||||
export interface IKubeApiCluster {
|
||||
metadata: {
|
||||
uid: string;
|
||||
};
|
||||
}
|
||||
|
||||
export type { CreateKubeApiForRemoteClusterConfig as IRemoteKubeApiConfig } from "../../common/k8s-api/create-kube-api-for-remote-cluster.injectable";
|
||||
export type { CreateKubeApiForLocalClusterConfig as ILocalKubeApiConfig } from "../../common/k8s-api/create-kube-api-for-cluster.injectable";
|
||||
|
||||
export {
|
||||
KubeObject,
|
||||
KubeStatus,
|
||||
type OwnerReference,
|
||||
type KubeObjectMetadata,
|
||||
type NamespaceScopedMetadata,
|
||||
type ClusterScopedMetadata,
|
||||
type BaseKubeJsonApiObjectMetadata,
|
||||
type KubeJsonApiObjectMetadata,
|
||||
type KubeStatusData,
|
||||
} from "../../common/k8s-api/kube-object";
|
||||
|
||||
export type {
|
||||
KubeJsonApiData,
|
||||
} from "../../common/k8s-api/kube-json-api";
|
||||
|
||||
function KubeJsonApiCstr(config: JsonApiConfig, reqInit?: RequestInit) {
|
||||
const di = getLegacyGlobalDiForExtensionApi();
|
||||
const createKubeJsonApi = di.inject(createKubeJsonApiInjectable);
|
||||
|
||||
return createKubeJsonApi(config, reqInit);
|
||||
}
|
||||
|
||||
export type KubeJsonApi = InternalKubeJsonApi;
|
||||
|
||||
export const KubeJsonApi = Object.assign(
|
||||
KubeJsonApiCstr as unknown as new (config: JsonApiConfig, reqInit?: RequestInit) => InternalKubeJsonApi,
|
||||
{
|
||||
forCluster: asLegacyGlobalForExtensionApi(createKubeJsonApiForClusterInjectable),
|
||||
},
|
||||
);
|
||||
|
||||
export abstract class KubeObjectStore<
|
||||
K extends KubeObject = KubeObject,
|
||||
A extends InternalKubeApi<K, D> = InternalKubeApi<K, KubeJsonApiDataFor<K>>,
|
||||
D extends KubeJsonApiDataFor<K> = KubeApiDataFrom<K, A>,
|
||||
> extends InternalKubeObjectStore<K, A, D> {
|
||||
/**
|
||||
* @deprecated This is no longer used and shouldn't have been every really used
|
||||
*/
|
||||
static readonly context = {
|
||||
set: (ctx: ClusterContext) => {
|
||||
console.warn("Setting KubeObjectStore.context is no longer supported");
|
||||
void ctx;
|
||||
},
|
||||
get: () => asLegacyGlobalForExtensionApi(clusterFrameContextForNamespacedResourcesInjectable),
|
||||
};
|
||||
|
||||
get context() {
|
||||
return this.dependencies.context;
|
||||
}
|
||||
|
||||
constructor(api: A, opts?: KubeObjectStoreOptions);
|
||||
/**
|
||||
* @deprecated Supply API instance through constructor
|
||||
*/
|
||||
constructor();
|
||||
constructor(api?: A, opts?: KubeObjectStoreOptions) {
|
||||
const di = getLegacyGlobalDiForExtensionApi();
|
||||
|
||||
super(
|
||||
{
|
||||
context: di.inject(clusterFrameContextForNamespacedResourcesInjectable),
|
||||
logger: di.inject(loggerInjectable),
|
||||
},
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
api!,
|
||||
opts,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export {
|
||||
type JsonPatch,
|
||||
type KubeObjectStoreLoadAllParams,
|
||||
type KubeObjectStoreLoadingParams,
|
||||
type KubeObjectStoreSubscribeParams,
|
||||
} from "../../common/k8s-api/kube-object.store";
|
||||
|
||||
/**
|
||||
* @deprecated This type is only present for backwards compatable typescript support
|
||||
*/
|
||||
export interface IgnoredKubeApiOptions {
|
||||
/**
|
||||
* @deprecated this option is overridden and should not be used
|
||||
*/
|
||||
objectConstructor?: any;
|
||||
/**
|
||||
* @deprecated this option is overridden and should not be used
|
||||
*/
|
||||
kind?: any;
|
||||
/**
|
||||
* @deprecated this option is overridden and should not be used
|
||||
*/
|
||||
isNamespaces?: any;
|
||||
/**
|
||||
* @deprecated this option is overridden and should not be used
|
||||
*/
|
||||
apiBase?: any;
|
||||
}
|
||||
|
||||
// NOTE: these *Constructor functions MUST be `function` to work with `new X()`
|
||||
function PodsApiConstructor(opts?: DerivedKubeApiOptions & IgnoredKubeApiOptions) {
|
||||
return new PodApi(getKubeApiDeps(), opts);
|
||||
}
|
||||
|
||||
export type PodsApi = PodApi;
|
||||
export const PodsApi = PodsApiConstructor as unknown as new (opts?: DerivedKubeApiOptions & IgnoredKubeApiOptions) => PodApi;
|
||||
|
||||
function NodesApiConstructor(opts?: DerivedKubeApiOptions & IgnoredKubeApiOptions) {
|
||||
return new NodeApi(getKubeApiDeps(), opts);
|
||||
}
|
||||
|
||||
export type NodesApi = NodeApi;
|
||||
export const NodesApi = NodesApiConstructor as unknown as new (opts?: DerivedKubeApiOptions & IgnoredKubeApiOptions) => NodeApi;
|
||||
|
||||
function DeploymentApiConstructor(opts?: DerivedKubeApiOptions) {
|
||||
return new InternalDeploymentApi(getKubeApiDeps(), opts);
|
||||
}
|
||||
|
||||
export type DeploymentApi = InternalDeploymentApi;
|
||||
export const DeploymentApi = DeploymentApiConstructor as unknown as new (opts?: DerivedKubeApiOptions) => InternalDeploymentApi;
|
||||
|
||||
function IngressApiConstructor(opts?: DerivedKubeApiOptions & IgnoredKubeApiOptions) {
|
||||
return new InternalIngressApi(getKubeApiDeps(), opts);
|
||||
}
|
||||
|
||||
export type IngressApi = InternalIngressApi;
|
||||
export const IngressApi = IngressApiConstructor as unknown as new (opts?: DerivedKubeApiOptions & IgnoredKubeApiOptions) => InternalIngressApi;
|
||||
|
||||
function PersistentVolumeClaimsApiConstructor(opts?: DerivedKubeApiOptions & IgnoredKubeApiOptions) {
|
||||
return new PersistentVolumeClaimApi(getKubeApiDeps(), opts);
|
||||
}
|
||||
|
||||
export type PersistentVolumeClaimsApi = PersistentVolumeClaimApi;
|
||||
export const PersistentVolumeClaimsApi = PersistentVolumeClaimsApiConstructor as unknown as new (opts?: DerivedKubeApiOptions & IgnoredKubeApiOptions) => PersistentVolumeClaimApi;
|
||||
|
||||
export {
|
||||
type Container as IPodContainer,
|
||||
type PodContainerStatus as IPodContainerStatus,
|
||||
Pod,
|
||||
Node,
|
||||
Deployment,
|
||||
DaemonSet,
|
||||
StatefulSet,
|
||||
Job,
|
||||
CronJob,
|
||||
ConfigMap,
|
||||
type SecretReference as ISecretRef,
|
||||
Secret,
|
||||
ReplicaSet,
|
||||
ResourceQuota,
|
||||
LimitRange,
|
||||
HorizontalPodAutoscaler,
|
||||
PodDisruptionBudget,
|
||||
PriorityClass,
|
||||
Service,
|
||||
Endpoints as Endpoint,
|
||||
Ingress,
|
||||
NetworkPolicy,
|
||||
PersistentVolume,
|
||||
PersistentVolumeClaim,
|
||||
StorageClass,
|
||||
Namespace,
|
||||
KubeEvent,
|
||||
ServiceAccount,
|
||||
Role,
|
||||
RoleBinding,
|
||||
ClusterRole,
|
||||
ClusterRoleBinding,
|
||||
CustomResourceDefinition,
|
||||
} from "../../common/k8s-api/endpoints";
|
||||
@@ -0,0 +1,14 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
import { asLegacyGlobalFunctionForExtensionApi } from "../as-legacy-globals-for-extension-api/as-legacy-global-function-for-extension-api";
|
||||
import { resolveSystemProxyInjectionToken } from "../../common/utils/resolve-system-proxy/resolve-system-proxy-injection-token";
|
||||
|
||||
/**
|
||||
* Resolves URL-specific proxy information from system. See more here: https://www.electronjs.org/docs/latest/api/session#sesresolveproxyurl
|
||||
* @param url - The URL for proxy information
|
||||
* @returns Promise for proxy information as string
|
||||
*/
|
||||
export const resolveSystemProxy = asLegacyGlobalFunctionForExtensionApi(resolveSystemProxyInjectionToken);
|
||||
@@ -0,0 +1,17 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
export type { StatusBarRegistration } from "../../renderer/components/status-bar/status-bar-registration";
|
||||
export type { KubeObjectMenuRegistration, KubeObjectMenuComponents } from "../../renderer/components/kube-object-menu/kube-object-menu-registration";
|
||||
export type { AppPreferenceRegistration, AppPreferenceComponents } from "../../features/preferences/renderer/compliance-for-legacy-extension-api/app-preference-registration";
|
||||
export type { KubeObjectDetailRegistration, KubeObjectDetailComponents } from "../../renderer/components/kube-object-details/kube-object-detail-registration";
|
||||
export type { KubeObjectStatusRegistration } from "../../renderer/components/kube-object-status-icon/kube-object-status-registration";
|
||||
export type { PageRegistration, RegisteredPage, PageParams, PageComponentProps, PageComponents, PageTarget } from "../../renderer/routes/page-registration";
|
||||
export type { ClusterPageMenuRegistration, ClusterPageMenuComponents } from "../../renderer/components/layout/cluster-page-menu";
|
||||
export type { ProtocolHandlerRegistration, RouteParams as ProtocolRouteParams, RouteHandler as ProtocolRouteHandler } from "../../common/protocol-handler/registration";
|
||||
export type { CustomCategoryViewProps, CustomCategoryViewComponents, CustomCategoryViewRegistration } from "../../renderer/components/+catalog/custom-views";
|
||||
export type { ShellEnvModifier, ShellEnvContext } from "../../main/shell-session/shell-env-modifier/shell-env-modifier-registration";
|
||||
export type { KubeObjectContextMenuItem, KubeObjectOnContextMenuOpenContext, KubeObjectOnContextMenuOpen, KubeObjectHandlers, KubeObjectHandlerRegistration } from "../../renderer/kube-object/handler";
|
||||
export type { TrayMenuRegistration } from "../../main/tray/tray-menu-registration";
|
||||
export type { MenuRegistration } from "../../features/application-menu/main/menu-registration";
|
||||
@@ -0,0 +1,6 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
export { ExtensionStore } from "../extension-store";
|
||||
@@ -0,0 +1,10 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
export type IpcMainInvokeEvent = Electron.IpcMainInvokeEvent;
|
||||
export type IpcRendererEvent = Electron.IpcRendererEvent;
|
||||
export type IpcMainEvent = Electron.IpcMainEvent;
|
||||
|
||||
export * from "./registrations";
|
||||
@@ -0,0 +1,19 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
import userStoreInjectable from "../../common/user-store/user-store.injectable";
|
||||
import { asLegacyGlobalForExtensionApi } from "../as-legacy-globals-for-extension-api/as-legacy-global-object-for-extension-api";
|
||||
export interface UserPreferenceExtensionItems {
|
||||
/**
|
||||
* Get the configured kubectl binaries path.
|
||||
*/
|
||||
getKubectlPath: () => string | undefined;
|
||||
}
|
||||
|
||||
const userStore = asLegacyGlobalForExtensionApi(userStoreInjectable);
|
||||
|
||||
export const Preferences: UserPreferenceExtensionItems = {
|
||||
getKubectlPath: () => userStore.kubectlBinariesPath,
|
||||
};
|
||||
@@ -0,0 +1,37 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
import openLinkInBrowserInjectable from "../../common/utils/open-link-in-browser.injectable";
|
||||
import buildVersionInjectable from "../../main/vars/build-version/build-version.injectable";
|
||||
import { asLegacyGlobalFunctionForExtensionApi } from "../as-legacy-globals-for-extension-api/as-legacy-global-function-for-extension-api";
|
||||
import { getLegacyGlobalDiForExtensionApi } from "../as-legacy-globals-for-extension-api/legacy-global-di-for-extension-api";
|
||||
import { Singleton } from "../../common/utils";
|
||||
import { prevDefault, stopPropagation } from "../../renderer/utils/prevDefault";
|
||||
import type { IClassName } from "../../renderer/utils/cssNames";
|
||||
import { cssNames } from "../../renderer/utils/cssNames";
|
||||
|
||||
export interface UtilsExtensionItems {
|
||||
Singleton: typeof Singleton;
|
||||
prevDefault: <E extends React.SyntheticEvent | Event, R>(callback: (evt: E) => R) => (evt: E) => R;
|
||||
stopPropagation: (evt: Event | React.SyntheticEvent) => void;
|
||||
cssNames: (...classNames: IClassName[]) => string;
|
||||
openExternal: (url: string) => Promise<void>;
|
||||
openBrowser: (url: string) => Promise<void>;
|
||||
getAppVersion: () => string;
|
||||
}
|
||||
|
||||
export const Util: UtilsExtensionItems = {
|
||||
Singleton,
|
||||
prevDefault,
|
||||
stopPropagation,
|
||||
cssNames,
|
||||
openExternal: asLegacyGlobalFunctionForExtensionApi(openLinkInBrowserInjectable),
|
||||
openBrowser: asLegacyGlobalFunctionForExtensionApi(openLinkInBrowserInjectable),
|
||||
getAppVersion: () => {
|
||||
const di = getLegacyGlobalDiForExtensionApi();
|
||||
|
||||
return di.inject(buildVersionInjectable).get();
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,17 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
// Extensions-api types bundle (main + renderer)
|
||||
// Available for lens-extensions via NPM-package "@k8slens/extensions"
|
||||
|
||||
import * as Common from "./common-api";
|
||||
import * as Renderer from "./renderer-api";
|
||||
import * as Main from "./main-api";
|
||||
|
||||
export {
|
||||
Common,
|
||||
Renderer,
|
||||
Main,
|
||||
};
|
||||
@@ -0,0 +1,17 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
import { getInjectionToken } from "@ogre-tools/injectable";
|
||||
import type { LensExtensionConstructor, LensExtensionManifest } from "../lens-extension";
|
||||
|
||||
export interface BundledExtension {
|
||||
readonly manifest: LensExtensionManifest;
|
||||
main: () => LensExtensionConstructor | null;
|
||||
renderer: () => LensExtensionConstructor | null;
|
||||
}
|
||||
|
||||
export const bundledExtensionInjectionToken = getInjectionToken<BundledExtension>({
|
||||
id: "bundled-extension-path",
|
||||
});
|
||||
@@ -0,0 +1,63 @@
|
||||
/**
|
||||
* 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 { ExtensionDiscovery } from "./extension-discovery";
|
||||
import extensionLoaderInjectable from "../extension-loader/extension-loader.injectable";
|
||||
import isCompatibleExtensionInjectable from "./is-compatible-extension/is-compatible-extension.injectable";
|
||||
import extensionsStoreInjectable from "../extensions-store/extensions-store.injectable";
|
||||
import extensionInstallationStateStoreInjectable from "../extension-installation-state-store/extension-installation-state-store.injectable";
|
||||
import installExtensionInjectable from "../extension-installer/install-extension/install-extension.injectable";
|
||||
import extensionPackageRootDirectoryInjectable from "../extension-installer/extension-package-root-directory/extension-package-root-directory.injectable";
|
||||
import readJsonFileInjectable from "../../common/fs/read-json-file.injectable";
|
||||
import loggerInjectable from "../../common/logger.injectable";
|
||||
import pathExistsInjectable from "../../common/fs/path-exists.injectable";
|
||||
import watchInjectable from "../../common/fs/watch/watch.injectable";
|
||||
import accessPathInjectable from "../../common/fs/access-path.injectable";
|
||||
import copyInjectable from "../../common/fs/copy.injectable";
|
||||
import ensureDirInjectable from "../../common/fs/ensure-dir.injectable";
|
||||
import isProductionInjectable from "../../common/vars/is-production.injectable";
|
||||
import lstatInjectable from "../../common/fs/lstat.injectable";
|
||||
import readDirectoryInjectable from "../../common/fs/read-directory.injectable";
|
||||
import fileSystemSeparatorInjectable from "../../common/path/separator.injectable";
|
||||
import getBasenameOfPathInjectable from "../../common/path/get-basename.injectable";
|
||||
import getDirnameOfPathInjectable from "../../common/path/get-dirname.injectable";
|
||||
import getRelativePathInjectable from "../../common/path/get-relative-path.injectable";
|
||||
import joinPathsInjectable from "../../common/path/join-paths.injectable";
|
||||
import removePathInjectable from "../../common/fs/remove.injectable";
|
||||
import homeDirectoryPathInjectable from "../../common/os/home-directory-path.injectable";
|
||||
import lensResourcesDirInjectable from "../../common/vars/lens-resources-dir.injectable";
|
||||
|
||||
const extensionDiscoveryInjectable = getInjectable({
|
||||
id: "extension-discovery",
|
||||
|
||||
instantiate: (di) => new ExtensionDiscovery({
|
||||
extensionLoader: di.inject(extensionLoaderInjectable),
|
||||
extensionsStore: di.inject(extensionsStoreInjectable),
|
||||
extensionInstallationStateStore: di.inject(extensionInstallationStateStoreInjectable),
|
||||
isCompatibleExtension: di.inject(isCompatibleExtensionInjectable),
|
||||
installExtension: di.inject(installExtensionInjectable),
|
||||
extensionPackageRootDirectory: di.inject(extensionPackageRootDirectoryInjectable),
|
||||
resourcesDirectory: di.inject(lensResourcesDirInjectable),
|
||||
readJsonFile: di.inject(readJsonFileInjectable),
|
||||
pathExists: di.inject(pathExistsInjectable),
|
||||
watch: di.inject(watchInjectable),
|
||||
logger: di.inject(loggerInjectable),
|
||||
accessPath: di.inject(accessPathInjectable),
|
||||
copy: di.inject(copyInjectable),
|
||||
removePath: di.inject(removePathInjectable),
|
||||
ensureDirectory: di.inject(ensureDirInjectable),
|
||||
isProduction: di.inject(isProductionInjectable),
|
||||
lstat: di.inject(lstatInjectable),
|
||||
readDirectory: di.inject(readDirectoryInjectable),
|
||||
fileSystemSeparator: di.inject(fileSystemSeparatorInjectable),
|
||||
getBasenameOfPath: di.inject(getBasenameOfPathInjectable),
|
||||
getDirnameOfPath: di.inject(getDirnameOfPathInjectable),
|
||||
getRelativePath: di.inject(getRelativePathInjectable),
|
||||
joinPaths: di.inject(joinPathsInjectable),
|
||||
homeDirectoryPath: di.inject(homeDirectoryPathInjectable),
|
||||
}),
|
||||
});
|
||||
|
||||
export default extensionDiscoveryInjectable;
|
||||
@@ -0,0 +1,147 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
import type { FSWatcher } from "chokidar";
|
||||
import { getDiForUnitTesting } from "../../main/getDiForUnitTesting";
|
||||
import extensionDiscoveryInjectable from "../extension-discovery/extension-discovery.injectable";
|
||||
import type { ExtensionDiscovery } from "../extension-discovery/extension-discovery";
|
||||
import installExtensionInjectable from "../extension-installer/install-extension/install-extension.injectable";
|
||||
import directoryForUserDataInjectable from "../../common/app-paths/directory-for-user-data/directory-for-user-data.injectable";
|
||||
import { delay } from "../../renderer/utils";
|
||||
import { observable, runInAction, when } from "mobx";
|
||||
import readJsonFileInjectable from "../../common/fs/read-json-file.injectable";
|
||||
import pathExistsInjectable from "../../common/fs/path-exists.injectable";
|
||||
import watchInjectable from "../../common/fs/watch/watch.injectable";
|
||||
import extensionApiVersionInjectable from "../../common/vars/extension-api-version.injectable";
|
||||
import removePathInjectable from "../../common/fs/remove.injectable";
|
||||
import type { JoinPaths } from "../../common/path/join-paths.injectable";
|
||||
import joinPathsInjectable from "../../common/path/join-paths.injectable";
|
||||
import homeDirectoryPathInjectable from "../../common/os/home-directory-path.injectable";
|
||||
import pathExistsSyncInjectable from "../../common/fs/path-exists-sync.injectable";
|
||||
import readJsonSyncInjectable from "../../common/fs/read-json-sync.injectable";
|
||||
import writeJsonSyncInjectable from "../../common/fs/write-json-sync.injectable";
|
||||
|
||||
describe("ExtensionDiscovery", () => {
|
||||
let extensionDiscovery: ExtensionDiscovery;
|
||||
let readJsonFileMock: jest.Mock;
|
||||
let pathExistsMock: jest.Mock;
|
||||
let watchMock: jest.Mock;
|
||||
let joinPaths: JoinPaths;
|
||||
let homeDirectoryPath: string;
|
||||
|
||||
beforeEach(() => {
|
||||
const di = getDiForUnitTesting({ doGeneralOverrides: true });
|
||||
|
||||
di.override(directoryForUserDataInjectable, () => "/some-directory-for-user-data");
|
||||
di.override(installExtensionInjectable, () => () => Promise.resolve());
|
||||
di.override(extensionApiVersionInjectable, () => "5.0.0");
|
||||
di.override(pathExistsSyncInjectable, () => () => { throw new Error("tried call pathExistsSync without override"); });
|
||||
di.override(readJsonSyncInjectable, () => () => { throw new Error("tried call readJsonSync without override"); });
|
||||
di.override(writeJsonSyncInjectable, () => () => { throw new Error("tried call writeJsonSync without override"); });
|
||||
|
||||
joinPaths = di.inject(joinPathsInjectable);
|
||||
homeDirectoryPath = di.inject(homeDirectoryPathInjectable);
|
||||
|
||||
readJsonFileMock = jest.fn();
|
||||
di.override(readJsonFileInjectable, () => readJsonFileMock);
|
||||
|
||||
pathExistsMock = jest.fn(() => Promise.resolve(true));
|
||||
di.override(pathExistsInjectable, () => pathExistsMock);
|
||||
|
||||
watchMock = jest.fn();
|
||||
di.override(watchInjectable, () => watchMock);
|
||||
|
||||
di.override(removePathInjectable, () => async () => {}); // allow deleting files for now
|
||||
|
||||
extensionDiscovery = di.inject(extensionDiscoveryInjectable);
|
||||
});
|
||||
|
||||
it("emits add for added extension", async () => {
|
||||
const letTestFinish = observable.box(false);
|
||||
let addHandler!: (filePath: string) => void;
|
||||
|
||||
readJsonFileMock.mockImplementation((p) => {
|
||||
expect(p).toBe(joinPaths(homeDirectoryPath, ".k8slens/extensions/my-extension/package.json"));
|
||||
|
||||
return {
|
||||
name: "my-extension",
|
||||
version: "1.0.0",
|
||||
engines: {
|
||||
lens: "5.0.0",
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
const mockWatchInstance = {
|
||||
on: jest.fn((event: string, handler: typeof addHandler) => {
|
||||
if (event === "add") {
|
||||
addHandler = handler;
|
||||
}
|
||||
|
||||
return mockWatchInstance;
|
||||
}),
|
||||
} as unknown as FSWatcher;
|
||||
|
||||
watchMock.mockImplementationOnce(() => mockWatchInstance);
|
||||
|
||||
// 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: "/some-directory-for-user-data/node_modules/my-extension/package.json",
|
||||
isBundled: false,
|
||||
isEnabled: false,
|
||||
isCompatible: true,
|
||||
manifest: {
|
||||
name: "my-extension",
|
||||
version: "1.0.0",
|
||||
engines: {
|
||||
lens: "5.0.0",
|
||||
},
|
||||
},
|
||||
manifestPath: "/some-directory-for-user-data/node_modules/my-extension/package.json",
|
||||
});
|
||||
runInAction(() => letTestFinish.set(true));
|
||||
});
|
||||
|
||||
addHandler(joinPaths(extensionDiscovery.localFolderPath, "/my-extension/package.json"));
|
||||
await when(() => letTestFinish.get());
|
||||
});
|
||||
|
||||
it("doesn't emit add for added file under extension", async () => {
|
||||
let addHandler!: (filePath: string) => void;
|
||||
|
||||
const mockWatchInstance = {
|
||||
on: jest.fn((event: string, handler: typeof addHandler) => {
|
||||
if (event === "add") {
|
||||
addHandler = handler;
|
||||
}
|
||||
|
||||
return mockWatchInstance;
|
||||
}),
|
||||
} as unknown as FSWatcher;
|
||||
|
||||
watchMock.mockImplementationOnce(() => mockWatchInstance);
|
||||
|
||||
// Need to force isLoaded to be true so that the file watching is started
|
||||
extensionDiscovery.isLoaded = true;
|
||||
|
||||
await extensionDiscovery.watchExtensions();
|
||||
|
||||
const onAdd = jest.fn();
|
||||
|
||||
extensionDiscovery.events.on("add", onAdd);
|
||||
|
||||
addHandler(joinPaths(extensionDiscovery.localFolderPath, "/my-extension/node_modules/dep/package.json"));
|
||||
|
||||
await delay(10);
|
||||
|
||||
expect(onAdd).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,443 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
import { ipcRenderer } from "electron";
|
||||
import { EventEmitter } from "events";
|
||||
import { makeObservable, observable, reaction, when } from "mobx";
|
||||
import { broadcastMessage, ipcMainHandle, ipcRendererOn } from "../../common/ipc";
|
||||
import { isErrnoException, toJS } from "../../common/utils";
|
||||
import type { ExtensionsStore } from "../extensions-store/extensions-store";
|
||||
import type { ExtensionLoader } from "../extension-loader";
|
||||
import type { LensExtensionId, LensExtensionManifest } from "../lens-extension";
|
||||
import type { ExtensionInstallationStateStore } from "../extension-installation-state-store/extension-installation-state-store";
|
||||
import { extensionDiscoveryStateChannel } from "../../common/ipc/extension-handling";
|
||||
import { requestInitialExtensionDiscovery } from "../../renderer/ipc";
|
||||
import type { ReadJson } from "../../common/fs/read-json-file.injectable";
|
||||
import type { Logger } from "../../common/logger";
|
||||
import type { PathExists } from "../../common/fs/path-exists.injectable";
|
||||
import type { Watch } from "../../common/fs/watch/watch.injectable";
|
||||
import type { Stats } from "fs";
|
||||
import type { LStat } from "../../common/fs/lstat.injectable";
|
||||
import type { ReadDirectory } from "../../common/fs/read-directory.injectable";
|
||||
import type { EnsureDirectory } from "../../common/fs/ensure-dir.injectable";
|
||||
import type { AccessPath } from "../../common/fs/access-path.injectable";
|
||||
import type { Copy } from "../../common/fs/copy.injectable";
|
||||
import type { JoinPaths } from "../../common/path/join-paths.injectable";
|
||||
import type { GetBasenameOfPath } from "../../common/path/get-basename.injectable";
|
||||
import type { GetDirnameOfPath } from "../../common/path/get-dirname.injectable";
|
||||
import type { GetRelativePath } from "../../common/path/get-relative-path.injectable";
|
||||
import type { RemovePath } from "../../common/fs/remove.injectable";
|
||||
import type TypedEventEmitter from "typed-emitter";
|
||||
|
||||
interface Dependencies {
|
||||
readonly extensionLoader: ExtensionLoader;
|
||||
readonly extensionsStore: ExtensionsStore;
|
||||
readonly extensionInstallationStateStore: ExtensionInstallationStateStore;
|
||||
readonly extensionPackageRootDirectory: string;
|
||||
readonly resourcesDirectory: string;
|
||||
readonly logger: Logger;
|
||||
readonly isProduction: boolean;
|
||||
readonly fileSystemSeparator: string;
|
||||
readonly homeDirectoryPath: string;
|
||||
isCompatibleExtension: (manifest: LensExtensionManifest) => boolean;
|
||||
installExtension: (name: string) => Promise<void>;
|
||||
readJsonFile: ReadJson;
|
||||
pathExists: PathExists;
|
||||
removePath: RemovePath;
|
||||
lstat: LStat;
|
||||
watch: Watch;
|
||||
readDirectory: ReadDirectory;
|
||||
ensureDirectory: EnsureDirectory;
|
||||
accessPath: AccessPath;
|
||||
copy: Copy;
|
||||
joinPaths: JoinPaths;
|
||||
getBasenameOfPath: GetBasenameOfPath;
|
||||
getDirnameOfPath: GetDirnameOfPath;
|
||||
getRelativePath: GetRelativePath;
|
||||
}
|
||||
|
||||
export interface InstalledExtension {
|
||||
id: LensExtensionId;
|
||||
|
||||
readonly manifest: LensExtensionManifest;
|
||||
|
||||
// Absolute path to the non-symlinked source folder,
|
||||
// e.g. "/Users/user/.k8slens/extensions/helloworld"
|
||||
readonly absolutePath: string;
|
||||
|
||||
// Absolute to the symlinked package.json file
|
||||
readonly manifestPath: string;
|
||||
readonly isBundled: boolean; // defined in project root's package.json
|
||||
readonly isCompatible: boolean;
|
||||
isEnabled: boolean;
|
||||
}
|
||||
|
||||
const logModule = "[EXTENSION-DISCOVERY]";
|
||||
|
||||
export const manifestFilename = "package.json";
|
||||
|
||||
interface ExtensionDiscoveryChannelMessage {
|
||||
isLoaded: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the lstat is for a directory-like file (e.g. isDirectory or symbolic link)
|
||||
* @param lstat the stats to compare
|
||||
*/
|
||||
const isDirectoryLike = (lstat: Stats) => lstat.isDirectory() || lstat.isSymbolicLink();
|
||||
|
||||
interface LoadFromFolderOptions {
|
||||
isBundled?: boolean;
|
||||
}
|
||||
|
||||
interface ExtensionDiscoveryEvents {
|
||||
add: (ext: InstalledExtension) => void;
|
||||
remove: (extId: LensExtensionId) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Discovers installed bundled and local extensions from the filesystem.
|
||||
* Also watches for added and removed local extensions by watching the directory.
|
||||
* Uses ExtensionInstaller to install dependencies for all of the extensions.
|
||||
* This is also done when a new extension is copied to the local extensions directory.
|
||||
* .init() must be called to start the directory watching.
|
||||
* The class emits events for added and removed extensions:
|
||||
* - "add": When extension is added. The event is of type InstalledExtension
|
||||
* - "remove": When extension is removed. The event is of type LensExtensionId
|
||||
*/
|
||||
export class ExtensionDiscovery {
|
||||
protected bundledFolderPath!: string;
|
||||
|
||||
private loadStarted = false;
|
||||
private extensions: Map<string, InstalledExtension> = new Map();
|
||||
|
||||
// True if extensions have been loaded from the disk after app startup
|
||||
@observable isLoaded = false;
|
||||
|
||||
get whenLoaded() {
|
||||
return when(() => this.isLoaded);
|
||||
}
|
||||
|
||||
public readonly events: TypedEventEmitter<ExtensionDiscoveryEvents> = new EventEmitter();
|
||||
|
||||
constructor(protected readonly dependencies: Dependencies) {
|
||||
makeObservable(this);
|
||||
}
|
||||
|
||||
get localFolderPath(): string {
|
||||
return this.dependencies.joinPaths(this.dependencies.homeDirectoryPath, ".k8slens", "extensions");
|
||||
}
|
||||
|
||||
get packageJsonPath(): string {
|
||||
return this.dependencies.joinPaths(this.dependencies.extensionPackageRootDirectory, manifestFilename);
|
||||
}
|
||||
|
||||
get nodeModulesPath(): string {
|
||||
return this.dependencies.joinPaths(this.dependencies.extensionPackageRootDirectory, "node_modules");
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes the class and setups the file watcher for added/removed local extensions.
|
||||
*/
|
||||
async init(): Promise<void> {
|
||||
if (ipcRenderer) {
|
||||
await this.initRenderer();
|
||||
} else {
|
||||
await this.initMain();
|
||||
}
|
||||
}
|
||||
|
||||
async initRenderer(): Promise<void> {
|
||||
const onMessage = ({ isLoaded }: ExtensionDiscoveryChannelMessage) => {
|
||||
this.isLoaded = isLoaded;
|
||||
};
|
||||
|
||||
requestInitialExtensionDiscovery().then(onMessage);
|
||||
ipcRendererOn(extensionDiscoveryStateChannel, (_event, message: ExtensionDiscoveryChannelMessage) => {
|
||||
onMessage(message);
|
||||
});
|
||||
}
|
||||
|
||||
async initMain(): Promise<void> {
|
||||
ipcMainHandle(extensionDiscoveryStateChannel, () => this.toJSON());
|
||||
|
||||
reaction(() => this.toJSON(), () => {
|
||||
this.broadcast();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Watches for added/removed local extensions.
|
||||
* Dependencies are installed automatically after an extension folder is copied.
|
||||
*/
|
||||
async watchExtensions(): Promise<void> {
|
||||
this.dependencies.logger.info(`${logModule} watching extension add/remove in ${this.localFolderPath}`);
|
||||
|
||||
// Wait until .load() has been called and has been resolved
|
||||
await this.whenLoaded;
|
||||
|
||||
this.dependencies.watch(this.localFolderPath, {
|
||||
// For adding and removing symlinks to work, the depth has to be 1.
|
||||
depth: 1,
|
||||
ignoreInitial: true,
|
||||
// Try to wait until the file has been completely copied.
|
||||
// The OS might emit an event for added file even it's not completely written to the file-system.
|
||||
awaitWriteFinish: {
|
||||
// Wait 300ms until the file size doesn't change to consider the file written.
|
||||
// For a small file like package.json this should be plenty of time.
|
||||
stabilityThreshold: 300,
|
||||
},
|
||||
})
|
||||
// Extension add is detected by watching "<extensionDir>/package.json" add
|
||||
.on("add", this.handleWatchFileAdd)
|
||||
// Extension remove is detected by watching "<extensionDir>" unlink
|
||||
.on("unlinkDir", this.handleWatchUnlinkEvent)
|
||||
// Extension remove is detected by watching "<extensionSymLink>" unlink
|
||||
.on("unlink", this.handleWatchUnlinkEvent);
|
||||
}
|
||||
|
||||
handleWatchFileAdd = async (manifestPath: string): Promise<void> => {
|
||||
// e.g. "foo/package.json"
|
||||
const relativePath = this.dependencies.getRelativePath(this.localFolderPath, manifestPath);
|
||||
|
||||
// Converts "foo/package.json" to ["foo", "package.json"], where length of 2 implies
|
||||
// that the added file is in a folder under local folder path.
|
||||
// This safeguards against a file watch being triggered under a sub-directory which is not an extension.
|
||||
const isUnderLocalFolderPath = relativePath.split(this.dependencies.fileSystemSeparator).length === 2;
|
||||
|
||||
if (this.dependencies.getBasenameOfPath(manifestPath) === manifestFilename && isUnderLocalFolderPath) {
|
||||
try {
|
||||
this.dependencies.extensionInstallationStateStore.setInstallingFromMain(manifestPath);
|
||||
const absPath = this.dependencies.getDirnameOfPath(manifestPath);
|
||||
|
||||
// this.loadExtensionFromPath updates this.packagesJson
|
||||
const extension = await this.loadExtensionFromFolder(absPath);
|
||||
|
||||
if (extension) {
|
||||
// Remove a broken symlink left by a previous installation if it exists.
|
||||
await this.dependencies.removePath(extension.manifestPath);
|
||||
|
||||
// Install dependencies for the new extension
|
||||
await this.dependencies.installExtension(extension.absolutePath);
|
||||
|
||||
this.extensions.set(extension.id, extension);
|
||||
this.dependencies.logger.info(`${logModule} Added extension ${extension.manifest.name}`);
|
||||
this.events.emit("add", extension);
|
||||
}
|
||||
} catch (error) {
|
||||
this.dependencies.logger.error(`${logModule}: failed to add extension: ${error}`, { error });
|
||||
} finally {
|
||||
this.dependencies.extensionInstallationStateStore.clearInstallingFromMain(manifestPath);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Handle any unlink event, filtering out non-package.json links so the delete code
|
||||
* only happens once per extension.
|
||||
* @param filePath The absolute path to either a folder or file in the extensions folder
|
||||
*/
|
||||
handleWatchUnlinkEvent = async (filePath: string): Promise<void> => {
|
||||
// Check that the removed path is directly under this.localFolderPath
|
||||
// Note that the watcher can create unlink events for subdirectories of the extension
|
||||
const extensionFolderName = this.dependencies.getBasenameOfPath(filePath);
|
||||
const expectedPath = this.dependencies.getRelativePath(this.localFolderPath, filePath);
|
||||
|
||||
if (expectedPath !== extensionFolderName) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const extension of this.extensions.values()) {
|
||||
if (extension.absolutePath !== filePath) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const extensionName = extension.manifest.name;
|
||||
|
||||
// If the extension is deleted manually while the application is running, also remove the symlink
|
||||
await this.removeSymlinkByPackageName(extensionName);
|
||||
|
||||
// The path to the manifest file is the lens extension id
|
||||
// Note: that we need to use the symlinked path
|
||||
const lensExtensionId = extension.manifestPath;
|
||||
|
||||
this.extensions.delete(extension.id);
|
||||
this.dependencies.logger.info(`${logModule} removed extension ${extensionName}`);
|
||||
this.events.emit("remove", lensExtensionId);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
this.dependencies.logger.warn(`${logModule} extension ${extensionFolderName} not found, can't remove`);
|
||||
};
|
||||
|
||||
/**
|
||||
* Remove the symlink under node_modules if exists.
|
||||
* If we don't remove the symlink, the uninstall would leave a non-working symlink,
|
||||
* which wouldn't be fixed if the extension was reinstalled, causing the extension not to work.
|
||||
* @param name e.g. "@mirantis/lens-extension-cc"
|
||||
*/
|
||||
removeSymlinkByPackageName(name: string): Promise<void> {
|
||||
return this.dependencies.removePath(this.getInstalledPath(name));
|
||||
}
|
||||
|
||||
/**
|
||||
* Uninstalls extension.
|
||||
* The application will detect the folder unlink and remove the extension from the UI automatically.
|
||||
* @param extensionId The ID of the extension to uninstall.
|
||||
*/
|
||||
async uninstallExtension(extensionId: LensExtensionId): Promise<void> {
|
||||
const extension = this.extensions.get(extensionId) ?? this.dependencies.extensionLoader.getExtension(extensionId);
|
||||
|
||||
if (!extension) {
|
||||
return void this.dependencies.logger.warn(`${logModule} could not uninstall extension, not found`, { id: extensionId });
|
||||
}
|
||||
|
||||
const { manifest, absolutePath } = extension;
|
||||
|
||||
this.dependencies.logger.info(`${logModule} Uninstalling ${manifest.name}`);
|
||||
|
||||
await this.removeSymlinkByPackageName(manifest.name);
|
||||
|
||||
// fs.remove does nothing if the path doesn't exist anymore
|
||||
await this.dependencies.removePath(absolutePath);
|
||||
}
|
||||
|
||||
async load(): Promise<Map<LensExtensionId, InstalledExtension>> {
|
||||
if (this.loadStarted) {
|
||||
// The class is simplified by only supporting .load() to be called once
|
||||
throw new Error("ExtensionDiscovery.load() can be only be called once");
|
||||
}
|
||||
|
||||
this.loadStarted = true;
|
||||
|
||||
this.dependencies.logger.info(
|
||||
`${logModule} loading extensions from ${this.dependencies.extensionPackageRootDirectory}`,
|
||||
);
|
||||
|
||||
await this.dependencies.removePath(this.dependencies.joinPaths(this.dependencies.extensionPackageRootDirectory, "package-lock.json"));
|
||||
await this.dependencies.ensureDirectory(this.nodeModulesPath);
|
||||
await this.dependencies.ensureDirectory(this.localFolderPath);
|
||||
|
||||
const extensions = await this.ensureExtensions();
|
||||
|
||||
this.isLoaded = true;
|
||||
|
||||
return extensions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the symlinked path to the extension folder,
|
||||
* e.g. "/Users/<username>/Library/Application Support/Lens/node_modules/@publisher/extension"
|
||||
*/
|
||||
protected getInstalledPath(name: string): string {
|
||||
return this.dependencies.joinPaths(this.nodeModulesPath, name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the symlinked path to the package.json,
|
||||
* e.g. "/Users/<username>/Library/Application Support/Lens/node_modules/@publisher/extension/package.json"
|
||||
*/
|
||||
protected getInstalledManifestPath(name: string): string {
|
||||
return this.dependencies.joinPaths(this.getInstalledPath(name), manifestFilename);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns InstalledExtension from path to package.json file.
|
||||
* Also updates this.packagesJson.
|
||||
*/
|
||||
protected async getByManifest(manifestPath: string, { isBundled = false } = {}): Promise<InstalledExtension | null> {
|
||||
try {
|
||||
const manifest = await this.dependencies.readJsonFile(manifestPath) as unknown as LensExtensionManifest;
|
||||
const id = isBundled ? manifestPath : this.getInstalledManifestPath(manifest.name);
|
||||
const isEnabled = this.dependencies.extensionsStore.isEnabled({ id, isBundled });
|
||||
const extensionDir = this.dependencies.getDirnameOfPath(manifestPath);
|
||||
const npmPackage = this.dependencies.joinPaths(extensionDir, `${manifest.name}-${manifest.version}.tgz`);
|
||||
const absolutePath = this.dependencies.isProduction && await this.dependencies.pathExists(npmPackage)
|
||||
? npmPackage
|
||||
: extensionDir;
|
||||
const isCompatible = isBundled || this.dependencies.isCompatibleExtension(manifest);
|
||||
|
||||
return {
|
||||
id,
|
||||
absolutePath,
|
||||
manifestPath: id,
|
||||
manifest,
|
||||
isBundled,
|
||||
isEnabled,
|
||||
isCompatible,
|
||||
};
|
||||
} catch (error) {
|
||||
if (isErrnoException(error) && error.code === "ENOTDIR") {
|
||||
// ignore this error, probably from .DS_Store file
|
||||
this.dependencies.logger.debug(`${logModule}: failed to load extension manifest through a not-dir-like at ${manifestPath}`);
|
||||
} else {
|
||||
this.dependencies.logger.error(`${logModule}: can't load extension manifest at ${manifestPath}: ${error}`);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async ensureExtensions(): Promise<Map<LensExtensionId, InstalledExtension>> {
|
||||
const userExtensions = await this.loadFromFolder(this.localFolderPath);
|
||||
|
||||
return this.extensions = new Map(userExtensions.map(extension => [extension.id, extension]));
|
||||
}
|
||||
|
||||
async loadFromFolder(folderPath: string): Promise<InstalledExtension[]> {
|
||||
const extensions: InstalledExtension[] = [];
|
||||
const paths = await this.dependencies.readDirectory(folderPath);
|
||||
|
||||
for (const fileName of paths) {
|
||||
const absPath = this.dependencies.joinPaths(folderPath, fileName);
|
||||
|
||||
try {
|
||||
const lstat = await this.dependencies.lstat(absPath);
|
||||
|
||||
// skip non-directories
|
||||
if (!isDirectoryLike(lstat)) {
|
||||
continue;
|
||||
}
|
||||
} catch (error) {
|
||||
if (isErrnoException(error) && error.code === "ENOENT") {
|
||||
continue;
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
|
||||
const extension = await this.loadExtensionFromFolder(absPath);
|
||||
|
||||
if (extension) {
|
||||
extensions.push(extension);
|
||||
}
|
||||
}
|
||||
|
||||
this.dependencies.logger.debug(`${logModule}: ${extensions.length} extensions loaded`, { folderPath, extensions });
|
||||
|
||||
return extensions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads extension from absolute path, updates this.packagesJson to include it and returns the extension.
|
||||
* @param folderPath Folder path to extension
|
||||
*/
|
||||
async loadExtensionFromFolder(folderPath: string, { isBundled = false }: LoadFromFolderOptions = {}): Promise<InstalledExtension | null> {
|
||||
const manifestPath = this.dependencies.joinPaths(folderPath, manifestFilename);
|
||||
|
||||
return this.getByManifest(manifestPath, { isBundled });
|
||||
}
|
||||
|
||||
toJSON(): ExtensionDiscoveryChannelMessage {
|
||||
return toJS({
|
||||
isLoaded: this.isLoaded,
|
||||
});
|
||||
}
|
||||
|
||||
broadcast(): void {
|
||||
broadcastMessage(extensionDiscoveryStateChannel, this.toJSON());
|
||||
}
|
||||
}
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
/**
|
||||
* 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 extensionApiVersionInjectable from "../../../common/vars/extension-api-version.injectable";
|
||||
import { isCompatibleExtension } from "./is-compatible-extension";
|
||||
|
||||
const isCompatibleExtensionInjectable = getInjectable({
|
||||
id: "is-compatible-extension",
|
||||
instantiate: (di) => isCompatibleExtension({
|
||||
extensionApiVersion: di.inject(extensionApiVersionInjectable),
|
||||
}),
|
||||
});
|
||||
|
||||
export default isCompatibleExtensionInjectable;
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import semver from "semver";
|
||||
import type { LensExtensionManifest } from "../../lens-extension";
|
||||
|
||||
interface Dependencies {
|
||||
extensionApiVersion: string;
|
||||
}
|
||||
|
||||
export const isCompatibleExtension = ({ extensionApiVersion }: Dependencies): ((manifest: LensExtensionManifest) => boolean) => {
|
||||
return (manifest: LensExtensionManifest): boolean => {
|
||||
const manifestLensEngine = manifest.engines.lens;
|
||||
const validVersion = manifestLensEngine.match(/^[\^0-9]\d*\.\d+\b/); // must start from ^ or number
|
||||
|
||||
if (!validVersion) {
|
||||
const errorInfo = [
|
||||
`Invalid format for "manifest.engines.lens"="${manifestLensEngine}"`,
|
||||
`Range versions can only be specified starting with '^'.`,
|
||||
`Otherwise it's recommended to use plain %MAJOR.%MINOR to match with supported Lens version.`,
|
||||
].join("\n");
|
||||
|
||||
throw new Error(errorInfo);
|
||||
}
|
||||
|
||||
const { major: extMajor, minor: extMinor } = semver.coerce(manifestLensEngine, {
|
||||
loose: true,
|
||||
}) as semver.SemVer;
|
||||
const supportedVersionsByExtension = semver.validRange(`^${extMajor}.${extMinor}`) as string;
|
||||
|
||||
return semver.satisfies(extensionApiVersion, supportedVersionsByExtension, {
|
||||
loose: true,
|
||||
includePrerelease: false,
|
||||
});
|
||||
};
|
||||
};
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
/**
|
||||
* 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 loggerInjectable from "../../common/logger.injectable";
|
||||
import { ExtensionInstallationStateStore } from "./extension-installation-state-store";
|
||||
|
||||
const extensionInstallationStateStoreInjectable = getInjectable({
|
||||
id: "extension-installation-state-store",
|
||||
instantiate: (di) => new ExtensionInstallationStateStore({
|
||||
logger: di.inject(loggerInjectable),
|
||||
}),
|
||||
});
|
||||
|
||||
export default extensionInstallationStateStoreInjectable;
|
||||
+247
@@ -0,0 +1,247 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
import { action, computed, observable } from "mobx";
|
||||
import { disposer } from "../../renderer/utils";
|
||||
import type { ExtendableDisposer } from "../../renderer/utils";
|
||||
import * as uuid from "uuid";
|
||||
import { broadcastMessage } from "../../common/ipc";
|
||||
import { ipcRenderer } from "electron";
|
||||
import type { Logger } from "../../common/logger";
|
||||
|
||||
export enum ExtensionInstallationState {
|
||||
INSTALLING = "installing",
|
||||
UNINSTALLING = "uninstalling",
|
||||
IDLE = "idle",
|
||||
}
|
||||
|
||||
interface Dependencies {
|
||||
readonly logger: Logger;
|
||||
}
|
||||
|
||||
const Prefix = "[ExtensionInstallationStore]";
|
||||
|
||||
const installingFromMainChannel = "extension-installation-state-store:install";
|
||||
const clearInstallingFromMainChannel = "extension-installation-state-store:clear-install";
|
||||
|
||||
export class ExtensionInstallationStateStore {
|
||||
private readonly preInstallIds = observable.set<string>();
|
||||
private readonly uninstallingExtensions = observable.set<string>();
|
||||
private readonly installingExtensions = observable.set<string>();
|
||||
|
||||
constructor(private readonly dependencies: Dependencies) {}
|
||||
|
||||
bindIpcListeners = () => {
|
||||
ipcRenderer
|
||||
.on(installingFromMainChannel, (event, extId) => {
|
||||
this.setInstalling(extId);
|
||||
})
|
||||
|
||||
.on(clearInstallingFromMainChannel, (event, extId) => {
|
||||
this.clearInstalling(extId);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Strictly transitions an extension from not installing to installing
|
||||
* @param extId the ID of the extension
|
||||
* @throws if state is not IDLE
|
||||
*/
|
||||
@action setInstalling = (extId: string): void => {
|
||||
this.dependencies.logger.debug(`${Prefix}: trying to set ${extId} as installing`);
|
||||
|
||||
const curState = this.getInstallationState(extId);
|
||||
|
||||
if (curState !== ExtensionInstallationState.IDLE) {
|
||||
throw new Error(
|
||||
`${Prefix}: cannot set ${extId} as installing. Is currently ${curState}.`,
|
||||
);
|
||||
}
|
||||
|
||||
this.installingExtensions.add(extId);
|
||||
};
|
||||
|
||||
/**
|
||||
* Broadcasts that an extension is being installed by the main process
|
||||
* @param extId the ID of the extension
|
||||
*/
|
||||
setInstallingFromMain = (extId: string): void => {
|
||||
broadcastMessage(installingFromMainChannel, extId);
|
||||
};
|
||||
|
||||
/**
|
||||
* Broadcasts that an extension is no longer being installed by the main process
|
||||
* @param extId the ID of the extension
|
||||
*/
|
||||
clearInstallingFromMain = (extId: string): void => {
|
||||
broadcastMessage(clearInstallingFromMainChannel, extId);
|
||||
};
|
||||
|
||||
/**
|
||||
* Marks the start of a pre-install phase of an extension installation. The
|
||||
* part of the installation before the tarball has been unpacked and the ID
|
||||
* determined.
|
||||
* @returns a disposer which should be called to mark the end of the install phase
|
||||
*/
|
||||
@action startPreInstall = (): ExtendableDisposer => {
|
||||
const preInstallStepId = uuid.v4();
|
||||
|
||||
this.dependencies.logger.debug(
|
||||
`${Prefix}: starting a new preinstall phase: ${preInstallStepId}`,
|
||||
);
|
||||
this.preInstallIds.add(preInstallStepId);
|
||||
|
||||
return disposer(() => {
|
||||
this.preInstallIds.delete(preInstallStepId);
|
||||
this.dependencies.logger.debug(`${Prefix}: ending a preinstall phase: ${preInstallStepId}`);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Strictly transitions an extension from not uninstalling to uninstalling
|
||||
* @param extId the ID of the extension
|
||||
* @throws if state is not IDLE
|
||||
*/
|
||||
@action setUninstalling = (extId: string): void => {
|
||||
this.dependencies.logger.debug(`${Prefix}: trying to set ${extId} as uninstalling`);
|
||||
|
||||
const curState = this.getInstallationState(extId);
|
||||
|
||||
if (curState !== ExtensionInstallationState.IDLE) {
|
||||
throw new Error(
|
||||
`${Prefix}: cannot set ${extId} as uninstalling. Is currently ${curState}.`,
|
||||
);
|
||||
}
|
||||
|
||||
this.uninstallingExtensions.add(extId);
|
||||
};
|
||||
|
||||
/**
|
||||
* Strictly clears the INSTALLING state of an extension
|
||||
* @param extId The ID of the extension
|
||||
* @throws if state is not INSTALLING
|
||||
*/
|
||||
@action clearInstalling = (extId: string): void => {
|
||||
this.dependencies.logger.debug(`${Prefix}: trying to clear ${extId} as installing`);
|
||||
|
||||
const curState = this.getInstallationState(extId);
|
||||
|
||||
switch (curState) {
|
||||
case ExtensionInstallationState.INSTALLING:
|
||||
return void this.installingExtensions.delete(extId);
|
||||
default:
|
||||
throw new Error(
|
||||
`${Prefix}: cannot clear INSTALLING state for ${extId}, it is currently ${curState}`,
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Strictly clears the UNINSTALLING state of an extension
|
||||
* @param extId The ID of the extension
|
||||
* @throws if state is not UNINSTALLING
|
||||
*/
|
||||
@action clearUninstalling = (extId: string): void => {
|
||||
this.dependencies.logger.debug(`${Prefix}: trying to clear ${extId} as uninstalling`);
|
||||
|
||||
const curState = this.getInstallationState(extId);
|
||||
|
||||
switch (curState) {
|
||||
case ExtensionInstallationState.UNINSTALLING:
|
||||
return void this.uninstallingExtensions.delete(extId);
|
||||
default:
|
||||
throw new Error(
|
||||
`${Prefix}: cannot clear UNINSTALLING state for ${extId}, it is currently ${curState}`,
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns the current state of the extension. IDLE is default value.
|
||||
* @param extId The ID of the extension
|
||||
*/
|
||||
getInstallationState = (extId: string): ExtensionInstallationState => {
|
||||
if (this.installingExtensions.has(extId)) {
|
||||
return ExtensionInstallationState.INSTALLING;
|
||||
}
|
||||
|
||||
if (this.uninstallingExtensions.has(extId)) {
|
||||
return ExtensionInstallationState.UNINSTALLING;
|
||||
}
|
||||
|
||||
return ExtensionInstallationState.IDLE;
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns true if the extension is currently INSTALLING
|
||||
* @param extId The ID of the extension
|
||||
*/
|
||||
isExtensionInstalling = (extId: string): boolean =>
|
||||
this.getInstallationState(extId) === ExtensionInstallationState.INSTALLING;
|
||||
|
||||
/**
|
||||
* Returns true if the extension is currently UNINSTALLING
|
||||
* @param extId The ID of the extension
|
||||
*/
|
||||
isExtensionUninstalling = (extId: string): boolean =>
|
||||
this.getInstallationState(extId) ===
|
||||
ExtensionInstallationState.UNINSTALLING;
|
||||
|
||||
/**
|
||||
* Returns true if the extension is currently IDLE
|
||||
* @param extId The ID of the extension
|
||||
*/
|
||||
isExtensionIdle = (extId: string): boolean =>
|
||||
this.getInstallationState(extId) === ExtensionInstallationState.IDLE;
|
||||
|
||||
/**
|
||||
* The current number of extensions installing
|
||||
*/
|
||||
@computed get installing(): number {
|
||||
return this.installingExtensions.size;
|
||||
}
|
||||
|
||||
/**
|
||||
* The current number of extensions uninstalling
|
||||
*/
|
||||
get uninstalling(): number {
|
||||
return this.uninstallingExtensions.size;
|
||||
}
|
||||
|
||||
/**
|
||||
* If there is at least one extension currently installing
|
||||
*/
|
||||
get anyInstalling(): boolean {
|
||||
return this.installing > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* If there is at least one extension currently uninstalling
|
||||
*/
|
||||
get anyUninstalling(): boolean {
|
||||
return this.uninstalling > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* The current number of extensions preinstalling
|
||||
*/
|
||||
get preinstalling(): number {
|
||||
return this.preInstallIds.size;
|
||||
}
|
||||
|
||||
/**
|
||||
* If there is at least one extension currently downloading
|
||||
*/
|
||||
get anyPreinstalling(): boolean {
|
||||
return this.preinstalling > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* If there is at least one installing or preinstalling step taking place
|
||||
*/
|
||||
get anyPreInstallingOrInstalling(): boolean {
|
||||
return this.anyInstalling || this.anyPreinstalling;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import { getInjectable } from "@ogre-tools/injectable";
|
||||
import pathToNpmCliInjectable from "../../common/app-paths/path-to-npm-cli.injectable";
|
||||
import loggerInjectable from "../../common/logger.injectable";
|
||||
import { ExtensionInstaller } from "./extension-installer";
|
||||
import extensionPackageRootDirectoryInjectable from "./extension-package-root-directory/extension-package-root-directory.injectable";
|
||||
|
||||
const extensionInstallerInjectable = getInjectable({
|
||||
id: "extension-installer",
|
||||
|
||||
instantiate: (di) => new ExtensionInstaller({
|
||||
extensionPackageRootDirectory: di.inject(extensionPackageRootDirectoryInjectable),
|
||||
logger: di.inject(loggerInjectable),
|
||||
pathToNpmCli: di.inject(pathToNpmCliInjectable),
|
||||
}),
|
||||
});
|
||||
|
||||
export default extensionInstallerInjectable;
|
||||
@@ -0,0 +1,78 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
import AwaitLock from "await-lock";
|
||||
import child_process from "child_process";
|
||||
import type { Logger } from "../../common/logger";
|
||||
|
||||
const logModule = "[EXTENSION-INSTALLER]";
|
||||
|
||||
interface Dependencies {
|
||||
readonly extensionPackageRootDirectory: string;
|
||||
readonly logger: Logger;
|
||||
readonly pathToNpmCli: string;
|
||||
}
|
||||
|
||||
const baseNpmInstallArgs = [
|
||||
"install",
|
||||
"--audit=false",
|
||||
"--fund=false",
|
||||
// NOTE: we do not omit the `optional` dependencies because that is how we specify the non-bundled extensions
|
||||
"--omit=dev",
|
||||
"--omit=peer",
|
||||
"--prefer-offline",
|
||||
];
|
||||
|
||||
/**
|
||||
* Installs dependencies for extensions
|
||||
*/
|
||||
export class ExtensionInstaller {
|
||||
private readonly installLock = new AwaitLock();
|
||||
|
||||
constructor(private readonly dependencies: Dependencies) {}
|
||||
|
||||
/**
|
||||
* Install single package using npm
|
||||
*/
|
||||
installPackage = async (name: string): Promise<void> => {
|
||||
// Mutual exclusion to install packages in sequence
|
||||
await this.installLock.acquireAsync();
|
||||
|
||||
try {
|
||||
this.dependencies.logger.info(`${logModule} installing package from ${name} to ${this.dependencies.extensionPackageRootDirectory}`);
|
||||
await this.npm(...baseNpmInstallArgs, name);
|
||||
this.dependencies.logger.info(`${logModule} package ${name} installed to ${this.dependencies.extensionPackageRootDirectory}`);
|
||||
} finally {
|
||||
this.installLock.release();
|
||||
}
|
||||
};
|
||||
|
||||
private npm(...args: string[]): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const child = child_process.fork(this.dependencies.pathToNpmCli, args, {
|
||||
cwd: this.dependencies.extensionPackageRootDirectory,
|
||||
silent: true,
|
||||
env: {},
|
||||
});
|
||||
let stderr = "";
|
||||
|
||||
child.stderr?.on("data", data => {
|
||||
stderr += String(data);
|
||||
});
|
||||
|
||||
child.on("close", (code) => {
|
||||
if (code !== 0) {
|
||||
reject(new Error(stderr));
|
||||
} else {
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
|
||||
child.on("error", error => {
|
||||
reject(error);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
/**
|
||||
* 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 directoryForUserDataInjectable from "../../../common/app-paths/directory-for-user-data/directory-for-user-data.injectable";
|
||||
|
||||
const extensionPackageRootDirectoryInjectable = getInjectable({
|
||||
id: "extension-package-root-directory",
|
||||
|
||||
instantiate: (di) => di.inject(directoryForUserDataInjectable),
|
||||
});
|
||||
|
||||
export default extensionPackageRootDirectoryInjectable;
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
/**
|
||||
* 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 extensionInstallerInjectable from "../extension-installer.injectable";
|
||||
|
||||
const installExtensionInjectable = getInjectable({
|
||||
id: "install-extension",
|
||||
instantiate: (di) => di.inject(extensionInstallerInjectable).installPackage,
|
||||
});
|
||||
|
||||
export default installExtensionInjectable;
|
||||
@@ -0,0 +1,14 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
import { getInjectionToken } from "@ogre-tools/injectable";
|
||||
import type { InstalledExtension } from "../extension-discovery/extension-discovery";
|
||||
import type { LensExtension, LensExtensionConstructor } from "../lens-extension";
|
||||
|
||||
export type CreateExtensionInstance = (ExtensionClass: LensExtensionConstructor, extension: InstalledExtension) => LensExtension;
|
||||
|
||||
export const createExtensionInstanceInjectionToken = getInjectionToken<CreateExtensionInstance>({
|
||||
id: "create-extension-instance-token",
|
||||
});
|
||||
@@ -0,0 +1,10 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
import { getInjectionToken } from "@ogre-tools/injectable";
|
||||
|
||||
export const extensionEntryPointNameInjectionToken = getInjectionToken<"main" | "renderer">({
|
||||
id: "extension-entry-point-name-token",
|
||||
});
|
||||
@@ -0,0 +1,14 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import { getInjectable } from "@ogre-tools/injectable";
|
||||
import { observable } from "mobx";
|
||||
import type { LensExtensionId, LensExtension } from "../lens-extension";
|
||||
|
||||
const extensionInstancesInjectable = getInjectable({
|
||||
id: "extension-instances",
|
||||
instantiate: () => observable.map<LensExtensionId, LensExtension>(),
|
||||
});
|
||||
|
||||
export default extensionInstancesInjectable;
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
|
||||
import type { KubernetesCluster } from "../../common/catalog-entities";
|
||||
import type { LensRendererExtension } from "../lens-renderer-extension";
|
||||
|
||||
interface ExtensionIsEnabledForCluster {
|
||||
extension: LensRendererExtension;
|
||||
cluster: KubernetesCluster;
|
||||
}
|
||||
|
||||
const extensionIsEnabledForClusterInjectable = getInjectable({
|
||||
id: "extension-is-enabled-for-cluster",
|
||||
|
||||
instantiate: async (
|
||||
di,
|
||||
{ extension, cluster }: ExtensionIsEnabledForCluster,
|
||||
) => (await extension.isEnabledForCluster(cluster)) as boolean,
|
||||
|
||||
lifecycle: lifecycleEnum.keyedSingleton({
|
||||
getInstanceKey: (
|
||||
di,
|
||||
{ extension, cluster }: ExtensionIsEnabledForCluster,
|
||||
) => `${extension.sanitizedExtensionId}-${cluster.getId()}`,
|
||||
}),
|
||||
});
|
||||
|
||||
export default extensionIsEnabledForClusterInjectable;
|
||||
@@ -0,0 +1,34 @@
|
||||
/**
|
||||
* 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 { ExtensionLoader } from "./extension-loader";
|
||||
import updateExtensionsStateInjectable from "./update-extensions-state/update-extensions-state.injectable";
|
||||
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";
|
||||
import getDirnameOfPathInjectable from "../../common/path/get-dirname.injectable";
|
||||
import { bundledExtensionInjectionToken } from "../extension-discovery/bundled-extension-token";
|
||||
import { extensionEntryPointNameInjectionToken } from "./entry-point-name";
|
||||
|
||||
const extensionLoaderInjectable = getInjectable({
|
||||
id: "extension-loader",
|
||||
|
||||
instantiate: (di) => new ExtensionLoader({
|
||||
updateExtensionsState: di.inject(updateExtensionsStateInjectable),
|
||||
createExtensionInstance: di.inject(createExtensionInstanceInjectionToken),
|
||||
extensionInstances: di.inject(extensionInstancesInjectable),
|
||||
getExtension: (instance: LensExtension) => di.inject(extensionInjectable, instance),
|
||||
bundledExtensions: di.injectMany(bundledExtensionInjectionToken),
|
||||
extensionEntryPointName: di.inject(extensionEntryPointNameInjectionToken),
|
||||
logger: di.inject(loggerInjectable),
|
||||
joinPaths: di.inject(joinPathsInjectable),
|
||||
getDirnameOfPath: di.inject(getDirnameOfPathInjectable),
|
||||
}),
|
||||
});
|
||||
|
||||
export default extensionLoaderInjectable;
|
||||
@@ -0,0 +1,428 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
import { ipcMain, ipcRenderer } from "electron";
|
||||
import { isEqual } from "lodash";
|
||||
import type { ObservableMap } from "mobx";
|
||||
import { action, computed, makeObservable, observable, observe, reaction, when } from "mobx";
|
||||
import { broadcastMessage, ipcMainOn, ipcRendererOn, ipcMainHandle } from "../../common/ipc";
|
||||
import { isDefined, toJS } from "../../common/utils";
|
||||
import type { InstalledExtension } from "../extension-discovery/extension-discovery";
|
||||
import type { LensExtension, LensExtensionConstructor, LensExtensionId } from "../lens-extension";
|
||||
import type { LensExtensionState } from "../extensions-store/extensions-store";
|
||||
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";
|
||||
import type { GetDirnameOfPath } from "../../common/path/get-dirname.injectable";
|
||||
import type { BundledExtension } from "../extension-discovery/bundled-extension-token";
|
||||
|
||||
const logModule = "[EXTENSIONS-LOADER]";
|
||||
|
||||
interface Dependencies {
|
||||
readonly extensionInstances: ObservableMap<LensExtensionId, LensExtension>;
|
||||
readonly bundledExtensions: BundledExtension[];
|
||||
readonly logger: Logger;
|
||||
readonly extensionEntryPointName: "main" | "renderer";
|
||||
updateExtensionsState: (extensionsState: Record<LensExtensionId, LensExtensionState>) => void;
|
||||
createExtensionInstance: CreateExtensionInstance;
|
||||
getExtension: (instance: LensExtension) => Extension;
|
||||
joinPaths: JoinPaths;
|
||||
getDirnameOfPath: GetDirnameOfPath;
|
||||
}
|
||||
|
||||
interface ExtensionBeingActivated {
|
||||
instance: LensExtension;
|
||||
installedExtension: InstalledExtension;
|
||||
activated: Promise<void>;
|
||||
}
|
||||
|
||||
export interface ExtensionLoading {
|
||||
isBundled: boolean;
|
||||
loaded: Promise<void>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads installed extensions to the Lens application
|
||||
*/
|
||||
export class ExtensionLoader {
|
||||
protected readonly extensions = observable.map<LensExtensionId, InstalledExtension>();
|
||||
|
||||
/**
|
||||
* This is the set of extensions that don't come with either
|
||||
* - Main.LensExtension when running in the main process
|
||||
* - Renderer.LensExtension when running in the renderer process
|
||||
*/
|
||||
protected readonly nonInstancesByName = observable.set<string>();
|
||||
|
||||
/**
|
||||
* This is updated by the `observe` in the constructor. DO NOT write directly to it
|
||||
*/
|
||||
protected readonly instancesByName = observable.map<string, LensExtension>();
|
||||
|
||||
private readonly onRemoveExtensionId = new EventEmitter<[string]>();
|
||||
|
||||
@observable isLoaded = false;
|
||||
|
||||
get whenLoaded() {
|
||||
return when(() => this.isLoaded);
|
||||
}
|
||||
|
||||
constructor(protected readonly dependencies: Dependencies) {
|
||||
makeObservable(this);
|
||||
|
||||
observe(this.dependencies.extensionInstances, change => {
|
||||
switch (change.type) {
|
||||
case "add":
|
||||
if (this.instancesByName.has(change.newValue.name)) {
|
||||
throw new TypeError("Extension names must be unique");
|
||||
}
|
||||
|
||||
this.instancesByName.set(change.newValue.name, change.newValue);
|
||||
break;
|
||||
case "delete":
|
||||
this.instancesByName.delete(change.oldValue.name);
|
||||
break;
|
||||
case "update":
|
||||
throw new Error("Extension instances shouldn't be updated");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@computed get userExtensions(): Map<LensExtensionId, InstalledExtension> {
|
||||
const extensions = this.toJSON();
|
||||
|
||||
extensions.forEach((ext, extId) => {
|
||||
if (ext.isBundled) {
|
||||
extensions.delete(extId);
|
||||
}
|
||||
});
|
||||
|
||||
return extensions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the extension instance by its manifest name
|
||||
* @param name The name of the extension
|
||||
* @returns one of the following:
|
||||
* - the instance of `Main.LensExtension` on the main process if created
|
||||
* - the instance of `Renderer.LensExtension` on the renderer process if created
|
||||
* - `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 {
|
||||
if (this.nonInstancesByName.has(name)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return this.instancesByName.get(name);
|
||||
}
|
||||
|
||||
// Transform userExtensions to a state object for storing into ExtensionsStore
|
||||
@computed get storeState() {
|
||||
return Object.fromEntries(
|
||||
Array.from(this.userExtensions)
|
||||
.map(([extId, extension]) => [extId, {
|
||||
enabled: extension.isEnabled,
|
||||
name: extension.manifest.name,
|
||||
}]),
|
||||
);
|
||||
}
|
||||
|
||||
@action
|
||||
async init() {
|
||||
if (ipcMain) {
|
||||
await this.initMain();
|
||||
} else {
|
||||
await this.initRenderer();
|
||||
}
|
||||
|
||||
await Promise.all([this.whenLoaded]);
|
||||
|
||||
// broadcasting extensions between main/renderer processes
|
||||
reaction(() => this.toJSON(), () => this.broadcastExtensions(), {
|
||||
fireImmediately: true,
|
||||
});
|
||||
|
||||
reaction(
|
||||
() => this.storeState,
|
||||
|
||||
(state) => {
|
||||
this.dependencies.updateExtensionsState(state);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
initExtensions(extensions: Map<LensExtensionId, InstalledExtension>) {
|
||||
this.extensions.replace(extensions);
|
||||
}
|
||||
|
||||
addExtension(extension: InstalledExtension) {
|
||||
this.extensions.set(extension.id, extension);
|
||||
}
|
||||
|
||||
@action
|
||||
removeInstance(lensExtensionId: LensExtensionId) {
|
||||
this.dependencies.logger.info(`${logModule} deleting extension instance ${lensExtensionId}`);
|
||||
const instance = this.dependencies.extensionInstances.get(lensExtensionId);
|
||||
|
||||
if (!instance) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
instance.disable();
|
||||
|
||||
const extension = this.dependencies.getExtension(instance);
|
||||
|
||||
extension.deregister();
|
||||
|
||||
this.onRemoveExtensionId.emit(instance.id);
|
||||
this.dependencies.extensionInstances.delete(lensExtensionId);
|
||||
this.nonInstancesByName.delete(instance.name);
|
||||
} catch (error) {
|
||||
this.dependencies.logger.error(`${logModule}: deactivation extension error`, { lensExtensionId, error });
|
||||
}
|
||||
}
|
||||
|
||||
removeExtension(lensExtensionId: LensExtensionId) {
|
||||
this.removeInstance(lensExtensionId);
|
||||
|
||||
if (!this.extensions.delete(lensExtensionId)) {
|
||||
throw new Error(`Can't remove extension ${lensExtensionId}, doesn't exist.`);
|
||||
}
|
||||
}
|
||||
|
||||
setIsEnabled(lensExtensionId: LensExtensionId, isEnabled: boolean) {
|
||||
const extension = this.extensions.get(lensExtensionId);
|
||||
|
||||
assert(extension, `Must register extension ${lensExtensionId} with before enabling it`);
|
||||
|
||||
extension.isEnabled = isEnabled;
|
||||
}
|
||||
|
||||
protected async initMain() {
|
||||
this.isLoaded = true;
|
||||
await this.autoInitExtensions();
|
||||
|
||||
ipcMainHandle(extensionLoaderFromMainChannel, () => {
|
||||
return Array.from(this.toJSON());
|
||||
});
|
||||
|
||||
ipcMainOn(extensionLoaderFromRendererChannel, (event, extensions: [LensExtensionId, InstalledExtension][]) => {
|
||||
this.syncExtensions(extensions);
|
||||
});
|
||||
}
|
||||
|
||||
protected async initRenderer() {
|
||||
const extensionListHandler = (extensions: [LensExtensionId, InstalledExtension][]) => {
|
||||
this.isLoaded = true;
|
||||
this.syncExtensions(extensions);
|
||||
|
||||
const receivedExtensionIds = extensions.map(([lensExtensionId]) => lensExtensionId);
|
||||
|
||||
// Remove deleted extensions in renderer side only
|
||||
this.extensions.forEach((_, lensExtensionId) => {
|
||||
if (!receivedExtensionIds.includes(lensExtensionId)) {
|
||||
this.removeExtension(lensExtensionId);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
requestExtensionLoaderInitialState().then(extensionListHandler);
|
||||
ipcRendererOn(extensionLoaderFromMainChannel, (event, extensions: [LensExtensionId, InstalledExtension][]) => {
|
||||
extensionListHandler(extensions);
|
||||
});
|
||||
}
|
||||
|
||||
broadcastExtensions() {
|
||||
const channel = ipcRenderer
|
||||
? extensionLoaderFromRendererChannel
|
||||
: extensionLoaderFromMainChannel;
|
||||
|
||||
broadcastMessage(channel, Array.from(this.extensions));
|
||||
}
|
||||
|
||||
syncExtensions(extensions: [LensExtensionId, InstalledExtension][]) {
|
||||
extensions.forEach(([lensExtensionId, extension]) => {
|
||||
if (!isEqual(this.extensions.get(lensExtensionId), extension)) {
|
||||
this.extensions.set(lensExtensionId, extension);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
protected async loadBundledExtensions() {
|
||||
return this.dependencies.bundledExtensions
|
||||
.map(extension => {
|
||||
try {
|
||||
const LensExtensionClass = extension[this.dependencies.extensionEntryPointName]();
|
||||
|
||||
if (!LensExtensionClass) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const installedExtension: InstalledExtension = {
|
||||
absolutePath: "irrelevant",
|
||||
id: extension.manifest.name,
|
||||
isBundled: true,
|
||||
isCompatible: true,
|
||||
isEnabled: true,
|
||||
manifest: extension.manifest,
|
||||
manifestPath: "irrelevant",
|
||||
};
|
||||
const instance = this.dependencies.createExtensionInstance(
|
||||
LensExtensionClass,
|
||||
installedExtension,
|
||||
);
|
||||
|
||||
this.dependencies.extensionInstances.set(extension.manifest.name, instance);
|
||||
|
||||
return {
|
||||
instance,
|
||||
installedExtension,
|
||||
activated: instance.activate(),
|
||||
} as ExtensionBeingActivated;
|
||||
} catch (err) {
|
||||
this.dependencies.logger.error(`${logModule}: error loading extension`, { ext: extension, err });
|
||||
|
||||
return null;
|
||||
}
|
||||
})
|
||||
.filter(isDefined);
|
||||
}
|
||||
|
||||
protected async loadExtensions(extensions: ExtensionBeingActivated[]): Promise<ExtensionLoading[]> {
|
||||
// We first need to wait until each extension's `onActivate` is resolved or rejected,
|
||||
// as this might register new catalog categories. Afterwards we can safely .enable the extension.
|
||||
await Promise.all(
|
||||
extensions.map(extension =>
|
||||
// If extension activation fails, log error
|
||||
extension.activated.catch((error) => {
|
||||
this.dependencies.logger.error(`${logModule}: activation extension error`, { ext: extension.installedExtension, error });
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
||||
extensions.forEach(({ instance }) => {
|
||||
const extension = this.dependencies.getExtension(instance);
|
||||
|
||||
extension.register();
|
||||
});
|
||||
|
||||
return extensions.map(extension => {
|
||||
const loaded = extension.instance.enable().catch((err) => {
|
||||
this.dependencies.logger.error(`${logModule}: failed to enable`, { ext: extension, err });
|
||||
});
|
||||
|
||||
return {
|
||||
isBundled: extension.installedExtension.isBundled,
|
||||
loaded,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
protected async loadUserExtensions(installedExtensions: Map<string, InstalledExtension>) {
|
||||
// Steps of the function:
|
||||
// 1. require and call .activate for each Extension
|
||||
// 2. Wait until every extension's onActivate has been resolved
|
||||
// 3. Call .enable for each extension
|
||||
// 4. Return ExtensionLoading[]
|
||||
|
||||
return [...installedExtensions.entries()]
|
||||
.map(([extId, extension]) => {
|
||||
const alreadyInit = this.dependencies.extensionInstances.has(extId) || this.nonInstancesByName.has(extension.manifest.name);
|
||||
|
||||
if (extension.isCompatible && extension.isEnabled && !alreadyInit) {
|
||||
try {
|
||||
const LensExtensionClass = this.requireExtension(extension);
|
||||
|
||||
if (!LensExtensionClass) {
|
||||
this.nonInstancesByName.add(extension.manifest.name);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
const instance = this.dependencies.createExtensionInstance(
|
||||
LensExtensionClass,
|
||||
extension,
|
||||
);
|
||||
|
||||
this.dependencies.extensionInstances.set(extId, instance);
|
||||
|
||||
return {
|
||||
instance,
|
||||
installedExtension: extension,
|
||||
activated: instance.activate(),
|
||||
} as ExtensionBeingActivated;
|
||||
} catch (err) {
|
||||
this.dependencies.logger.error(`${logModule}: error loading extension`, { ext: extension, err });
|
||||
}
|
||||
} else if (!extension.isEnabled && alreadyInit) {
|
||||
this.removeInstance(extId);
|
||||
}
|
||||
|
||||
return null;
|
||||
})
|
||||
.filter(isDefined);
|
||||
}
|
||||
|
||||
async autoInitExtensions() {
|
||||
this.dependencies.logger.info(`${logModule}: auto initializing extensions`);
|
||||
|
||||
const bundledExtensions = await this.loadBundledExtensions();
|
||||
const userExtensions = await this.loadUserExtensions(this.toJSON());
|
||||
const loadedExtensions = await this.loadExtensions([
|
||||
...bundledExtensions,
|
||||
...userExtensions,
|
||||
]);
|
||||
|
||||
// Setup reaction to load extensions on JSON changes
|
||||
reaction(() => this.toJSON(), installedExtensions => {
|
||||
void (async () => {
|
||||
const userExtensions = await this.loadUserExtensions(installedExtensions);
|
||||
|
||||
await this.loadExtensions(userExtensions);
|
||||
})();
|
||||
});
|
||||
|
||||
return loadedExtensions;
|
||||
}
|
||||
|
||||
protected requireExtension(extension: InstalledExtension): LensExtensionConstructor | null {
|
||||
const extRelativePath = extension.manifest[this.dependencies.extensionEntryPointName];
|
||||
|
||||
if (!extRelativePath) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const extAbsolutePath = this.dependencies.joinPaths(this.dependencies.getDirnameOfPath(extension.manifestPath), extRelativePath);
|
||||
|
||||
try {
|
||||
return require(/* webpackIgnore: true */ extAbsolutePath).default;
|
||||
} catch (error) {
|
||||
const message = (error instanceof Error ? error.stack : undefined) || error;
|
||||
|
||||
this.dependencies.logger.error(`${logModule}: can't load ${this.dependencies.extensionEntryPointName} for "${extension.manifest.name}": ${message}`, { extension });
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
getExtension(extId: LensExtensionId) {
|
||||
return this.extensions.get(extId);
|
||||
}
|
||||
|
||||
getInstanceById(extId: LensExtensionId) {
|
||||
return this.dependencies.extensionInstances.get(extId);
|
||||
}
|
||||
|
||||
toJSON(): Map<LensExtensionId, InstalledExtension> {
|
||||
return toJS(this.extensions);
|
||||
}
|
||||
}
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
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 const extensionRegistratorInjectionToken = getInjectionToken<ExtensionRegistrator>({
|
||||
id: "extension-registrator-token",
|
||||
});
|
||||
@@ -0,0 +1,93 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import type { Injectable } from "@ogre-tools/injectable";
|
||||
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
|
||||
import { difference, find, map } from "lodash";
|
||||
import { reaction, runInAction } from "mobx";
|
||||
import { disposer } from "../../../common/utils/disposer";
|
||||
import type { LensExtension } from "../../lens-extension";
|
||||
import { extensionRegistratorInjectionToken } from "../extension-registrator-injection-token";
|
||||
|
||||
export interface Extension {
|
||||
register: () => void;
|
||||
deregister: () => void;
|
||||
}
|
||||
|
||||
const idsToInjectables = (ids: string[], injectables: Injectable<any, any, any>[]) => ids.map(id => {
|
||||
const injectable = find(injectables, { id });
|
||||
|
||||
if (!injectable) {
|
||||
throw new Error(`Injectable ${id} not found`);
|
||||
}
|
||||
|
||||
return injectable;
|
||||
});
|
||||
|
||||
const extensionInjectable = getInjectable({
|
||||
id: "extension",
|
||||
|
||||
instantiate: (parentDi, instance: LensExtension): Extension => {
|
||||
const extensionInjectable = getInjectable({
|
||||
id: `extension-${instance.sanitizedExtensionId}`,
|
||||
|
||||
instantiate: (childDi) => {
|
||||
const extensionRegistrators = childDi.injectMany(extensionRegistratorInjectionToken);
|
||||
const reactionDisposer = disposer();
|
||||
|
||||
return {
|
||||
register: () => {
|
||||
extensionRegistrators.forEach((getInjectablesOfExtension) => {
|
||||
const injectables = getInjectablesOfExtension(instance);
|
||||
|
||||
reactionDisposer.push(
|
||||
// injectables is either an array or a computed array, in which case
|
||||
// we need to update the registered injectables with a reaction every time they change
|
||||
reaction(
|
||||
() => Array.isArray(injectables) ? injectables : injectables.get(),
|
||||
(currentInjectables, previousInjectables = []) => {
|
||||
// Register new injectables and deregister removed injectables by id
|
||||
const currentIds = map(currentInjectables, "id");
|
||||
const previousIds = map(previousInjectables, "id");
|
||||
const idsToAdd = difference(currentIds, previousIds);
|
||||
const idsToRemove = previousIds.filter(previousId => !currentIds.includes(previousId));
|
||||
|
||||
if (idsToRemove.length > 0) {
|
||||
childDi.deregister(...idsToInjectables(idsToRemove, previousInjectables));
|
||||
}
|
||||
|
||||
if (idsToAdd.length > 0) {
|
||||
childDi.register(...idsToInjectables(idsToAdd, currentInjectables));
|
||||
}
|
||||
}, {
|
||||
fireImmediately: true,
|
||||
},
|
||||
));
|
||||
});
|
||||
},
|
||||
|
||||
deregister: () => {
|
||||
reactionDisposer();
|
||||
|
||||
runInAction(() => {
|
||||
parentDi.deregister(extensionInjectable);
|
||||
});
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
runInAction(() => {
|
||||
parentDi.register(extensionInjectable);
|
||||
});
|
||||
|
||||
return parentDi.inject(extensionInjectable);
|
||||
},
|
||||
|
||||
lifecycle: lifecycleEnum.keyedSingleton({
|
||||
getInstanceKey: (di, instance: LensExtension) => instance,
|
||||
}),
|
||||
});
|
||||
|
||||
export default extensionInjectable;
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
/**
|
||||
* 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 directoryForUserDataInjectable from "../../../common/app-paths/directory-for-user-data/directory-for-user-data.injectable";
|
||||
import joinPathsInjectable from "../../../common/path/join-paths.injectable";
|
||||
|
||||
const directoryForExtensionDataInjectable = getInjectable({
|
||||
id: "directory-for-extension-data",
|
||||
|
||||
instantiate: (di) => {
|
||||
const joinPaths = di.inject(joinPathsInjectable);
|
||||
const directoryForUserData = di.inject(directoryForUserDataInjectable);
|
||||
|
||||
return joinPaths(directoryForUserData, "extension_data");
|
||||
},
|
||||
});
|
||||
|
||||
export default directoryForExtensionDataInjectable;
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
/**
|
||||
* 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 { FileSystemProvisionerStore } from "./file-system-provisioner-store";
|
||||
import directoryForExtensionDataInjectable from "./directory-for-extension-data.injectable";
|
||||
import ensureDirectoryInjectable from "../../../common/fs/ensure-dir.injectable";
|
||||
import joinPathsInjectable from "../../../common/path/join-paths.injectable";
|
||||
import randomBytesInjectable from "../../../common/utils/random-bytes.injectable";
|
||||
import directoryForUserDataInjectable from "../../../common/app-paths/directory-for-user-data/directory-for-user-data.injectable";
|
||||
import getConfigurationFileModelInjectable from "../../../common/get-configuration-file-model/get-configuration-file-model.injectable";
|
||||
import loggerInjectable from "../../../common/logger.injectable";
|
||||
import storeMigrationVersionInjectable from "../../../common/vars/store-migration-version.injectable";
|
||||
import { baseStoreIpcChannelPrefixesInjectionToken } from "../../../common/base-store/channel-prefix";
|
||||
import { shouldBaseStoreDisableSyncInIpcListenerInjectionToken } from "../../../common/base-store/disable-sync";
|
||||
import { persistStateToConfigInjectionToken } from "../../../common/base-store/save-to-file";
|
||||
import getBasenameOfPathInjectable from "../../../common/path/get-basename.injectable";
|
||||
import { enlistMessageChannelListenerInjectionToken } from "../../../common/utils/channel/enlist-message-channel-listener-injection-token";
|
||||
|
||||
const fileSystemProvisionerStoreInjectable = getInjectable({
|
||||
id: "file-system-provisioner-store",
|
||||
|
||||
instantiate: (di) => new FileSystemProvisionerStore({
|
||||
directoryForExtensionData: di.inject(directoryForExtensionDataInjectable),
|
||||
ensureDirectory: di.inject(ensureDirectoryInjectable),
|
||||
joinPaths: di.inject(joinPathsInjectable),
|
||||
randomBytes: di.inject(randomBytesInjectable),
|
||||
directoryForUserData: di.inject(directoryForUserDataInjectable),
|
||||
getConfigurationFileModel: di.inject(getConfigurationFileModelInjectable),
|
||||
logger: di.inject(loggerInjectable),
|
||||
storeMigrationVersion: di.inject(storeMigrationVersionInjectable),
|
||||
migrations: {},
|
||||
getBasenameOfPath: di.inject(getBasenameOfPathInjectable),
|
||||
ipcChannelPrefixes: di.inject(baseStoreIpcChannelPrefixesInjectionToken),
|
||||
persistStateToConfig: di.inject(persistStateToConfigInjectionToken),
|
||||
enlistMessageChannelListener: di.inject(enlistMessageChannelListenerInjectionToken),
|
||||
shouldDisableSyncInListener: di.inject(shouldBaseStoreDisableSyncInIpcListenerInjectionToken),
|
||||
}),
|
||||
});
|
||||
|
||||
export default fileSystemProvisionerStoreInjectable;
|
||||
+68
@@ -0,0 +1,68 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
import { SHA256 } from "crypto-js";
|
||||
import { action, makeObservable, observable } from "mobx";
|
||||
import type { BaseStoreDependencies } from "../../../common/base-store/base-store";
|
||||
import { BaseStore } from "../../../common/base-store/base-store";
|
||||
import type { LensExtensionId } from "../../lens-extension";
|
||||
import { getOrInsertWithAsync, toJS } from "../../../common/utils";
|
||||
import type { EnsureDirectory } from "../../../common/fs/ensure-dir.injectable";
|
||||
import type { JoinPaths } from "../../../common/path/join-paths.injectable";
|
||||
import type { RandomBytes } from "../../../common/utils/random-bytes.injectable";
|
||||
|
||||
interface FSProvisionModel {
|
||||
extensions: Record<string, string>; // extension names to paths
|
||||
}
|
||||
|
||||
interface Dependencies extends BaseStoreDependencies {
|
||||
readonly directoryForExtensionData: string;
|
||||
ensureDirectory: EnsureDirectory;
|
||||
joinPaths: JoinPaths;
|
||||
randomBytes: RandomBytes;
|
||||
}
|
||||
|
||||
export class FileSystemProvisionerStore extends BaseStore<FSProvisionModel> {
|
||||
readonly registeredExtensions = observable.map<LensExtensionId, string>();
|
||||
|
||||
constructor(protected readonly dependencies: Dependencies) {
|
||||
super(dependencies, {
|
||||
configName: "lens-filesystem-provisioner-store",
|
||||
accessPropertiesByDotNotation: false, // To make dots safe in cluster context names
|
||||
});
|
||||
|
||||
makeObservable(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* This function retrieves the saved path to the folder which the extension
|
||||
* can saves files to. If the folder is not present then it is created.
|
||||
* @param extensionName the name of the extension requesting the path
|
||||
* @returns path to the folder that the extension can safely write files to.
|
||||
*/
|
||||
async requestDirectory(extensionName: string): Promise<string> {
|
||||
const dirPath = await getOrInsertWithAsync(this.registeredExtensions, extensionName, async () => {
|
||||
const salt = (await this.dependencies.randomBytes(32)).toString("hex");
|
||||
const hashedName = SHA256(`${extensionName}/${salt}`).toString();
|
||||
|
||||
return this.dependencies.joinPaths(this.dependencies.directoryForExtensionData, hashedName);
|
||||
});
|
||||
|
||||
await this.dependencies.ensureDirectory(dirPath);
|
||||
|
||||
return dirPath;
|
||||
}
|
||||
|
||||
@action
|
||||
protected fromStore({ extensions }: FSProvisionModel = { extensions: {}}): void {
|
||||
this.registeredExtensions.merge(extensions);
|
||||
}
|
||||
|
||||
toJSON(): FSProvisionModel {
|
||||
return toJS({
|
||||
extensions: Object.fromEntries(this.registeredExtensions),
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
export * from "./extension-loader";
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
/**
|
||||
* 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 extensionsStoreInjectable from "../../extensions-store/extensions-store.injectable";
|
||||
|
||||
const updateExtensionsStateInjectable = getInjectable({
|
||||
id: "update-extensions-state",
|
||||
instantiate: (di) => di.inject(extensionsStoreInjectable).mergeState,
|
||||
});
|
||||
|
||||
export default updateExtensionsStateInjectable;
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
/**
|
||||
* 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 directoryForUserDataInjectable
|
||||
from "../../common/app-paths/directory-for-user-data/directory-for-user-data.injectable";
|
||||
|
||||
const extensionPackagesRootInjectable = getInjectable({
|
||||
id: "extension-packages-root",
|
||||
instantiate: (di) => di.inject(directoryForUserDataInjectable),
|
||||
});
|
||||
|
||||
export default extensionPackagesRootInjectable;
|
||||
@@ -0,0 +1,97 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
import type { BaseStoreParams } from "../common/base-store/base-store";
|
||||
import { BaseStore } from "../common/base-store/base-store";
|
||||
import * as path from "path";
|
||||
import type { LensExtension } from "./lens-extension";
|
||||
import assert from "assert";
|
||||
import type { StaticThis } from "../common/utils";
|
||||
import { getOrInsertWith } from "../common/utils";
|
||||
import { getLegacyGlobalDiForExtensionApi } from "./as-legacy-globals-for-extension-api/legacy-global-di-for-extension-api";
|
||||
import directoryForUserDataInjectable from "../common/app-paths/directory-for-user-data/directory-for-user-data.injectable";
|
||||
import getConfigurationFileModelInjectable from "../common/get-configuration-file-model/get-configuration-file-model.injectable";
|
||||
import loggerInjectable from "../common/logger.injectable";
|
||||
import storeMigrationVersionInjectable from "../common/vars/store-migration-version.injectable";
|
||||
import type { Migrations } from "conf/dist/source/types";
|
||||
import { baseStoreIpcChannelPrefixesInjectionToken } from "../common/base-store/channel-prefix";
|
||||
import { shouldBaseStoreDisableSyncInIpcListenerInjectionToken } from "../common/base-store/disable-sync";
|
||||
import { persistStateToConfigInjectionToken } from "../common/base-store/save-to-file";
|
||||
import getBasenameOfPathInjectable from "../common/path/get-basename.injectable";
|
||||
import { enlistMessageChannelListenerInjectionToken } from "../common/utils/channel/enlist-message-channel-listener-injection-token";
|
||||
|
||||
export interface ExtensionStoreParams<T extends object> extends BaseStoreParams<T> {
|
||||
migrations?: Migrations<T>;
|
||||
}
|
||||
|
||||
export abstract class ExtensionStore<T extends object> extends BaseStore<T> {
|
||||
private static readonly instances = new WeakMap<object, any>();
|
||||
|
||||
/**
|
||||
* @deprecated This is a form of global shared state. Just call `new Store(...)`
|
||||
*/
|
||||
static createInstance<T, R extends any[]>(this: StaticThis<T, R>, ...args: R): T {
|
||||
return getOrInsertWith(ExtensionStore.instances, this, () => new this(...args)) as T;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated This is a form of global shared state. Just call `new Store(...)`
|
||||
*/
|
||||
static getInstance<T, R extends any[]>(this: StaticThis<T, R>, strict?: true): T;
|
||||
static getInstance<T, R extends any[]>(this: StaticThis<T, R>, strict: false): T | undefined;
|
||||
static getInstance<T, R extends any[]>(this: StaticThis<T, R>, strict = true): T | undefined {
|
||||
if (!ExtensionStore.instances.has(this) && strict) {
|
||||
throw new TypeError(`instance of ${this.name} is not created`);
|
||||
}
|
||||
|
||||
return ExtensionStore.instances.get(this) as (T | undefined);
|
||||
}
|
||||
|
||||
constructor({ migrations, ...params }: ExtensionStoreParams<T>) {
|
||||
const di = getLegacyGlobalDiForExtensionApi();
|
||||
|
||||
super({
|
||||
directoryForUserData: di.inject(directoryForUserDataInjectable),
|
||||
getConfigurationFileModel: di.inject(getConfigurationFileModelInjectable),
|
||||
logger: di.inject(loggerInjectable),
|
||||
storeMigrationVersion: di.inject(storeMigrationVersionInjectable),
|
||||
migrations: migrations as Migrations<Record<string, unknown>>,
|
||||
getBasenameOfPath: di.inject(getBasenameOfPathInjectable),
|
||||
ipcChannelPrefixes: di.inject(baseStoreIpcChannelPrefixesInjectionToken),
|
||||
persistStateToConfig: di.inject(persistStateToConfigInjectionToken),
|
||||
enlistMessageChannelListener: di.inject(enlistMessageChannelListenerInjectionToken),
|
||||
shouldDisableSyncInListener: di.inject(shouldBaseStoreDisableSyncInIpcListenerInjectionToken),
|
||||
}, params);
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated This is a form of global shared state. Just call `new Store(...)`
|
||||
*/
|
||||
static resetInstance() {
|
||||
ExtensionStore.instances.delete(this);
|
||||
}
|
||||
|
||||
protected extension?: LensExtension;
|
||||
|
||||
loadExtension(extension: LensExtension) {
|
||||
this.extension = extension;
|
||||
|
||||
this.params.projectVersion ??= this.extension.version;
|
||||
|
||||
return super.load();
|
||||
}
|
||||
|
||||
load() {
|
||||
if (!this.extension) { return; }
|
||||
|
||||
return super.load();
|
||||
}
|
||||
|
||||
protected cwd() {
|
||||
assert(this.extension, "must call this.load() first");
|
||||
|
||||
return path.join(super.cwd(), "extension-store", this.extension.name);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
/**
|
||||
* 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 directoryForUserDataInjectable from "../../common/app-paths/directory-for-user-data/directory-for-user-data.injectable";
|
||||
import { baseStoreIpcChannelPrefixesInjectionToken } from "../../common/base-store/channel-prefix";
|
||||
import { shouldBaseStoreDisableSyncInIpcListenerInjectionToken } from "../../common/base-store/disable-sync";
|
||||
import { persistStateToConfigInjectionToken } from "../../common/base-store/save-to-file";
|
||||
import getConfigurationFileModelInjectable from "../../common/get-configuration-file-model/get-configuration-file-model.injectable";
|
||||
import loggerInjectable from "../../common/logger.injectable";
|
||||
import getBasenameOfPathInjectable from "../../common/path/get-basename.injectable";
|
||||
import { enlistMessageChannelListenerInjectionToken } from "../../common/utils/channel/enlist-message-channel-listener-injection-token";
|
||||
import storeMigrationVersionInjectable from "../../common/vars/store-migration-version.injectable";
|
||||
import { ExtensionsStore } from "./extensions-store";
|
||||
|
||||
const extensionsStoreInjectable = getInjectable({
|
||||
id: "extensions-store",
|
||||
instantiate: (di) => new ExtensionsStore({
|
||||
directoryForUserData: di.inject(directoryForUserDataInjectable),
|
||||
getConfigurationFileModel: di.inject(getConfigurationFileModelInjectable),
|
||||
logger: di.inject(loggerInjectable),
|
||||
storeMigrationVersion: di.inject(storeMigrationVersionInjectable),
|
||||
migrations: {},
|
||||
getBasenameOfPath: di.inject(getBasenameOfPathInjectable),
|
||||
ipcChannelPrefixes: di.inject(baseStoreIpcChannelPrefixesInjectionToken),
|
||||
persistStateToConfig: di.inject(persistStateToConfigInjectionToken),
|
||||
enlistMessageChannelListener: di.inject(enlistMessageChannelListenerInjectionToken),
|
||||
shouldDisableSyncInListener: di.inject(shouldBaseStoreDisableSyncInIpcListenerInjectionToken),
|
||||
}),
|
||||
});
|
||||
|
||||
export default extensionsStoreInjectable;
|
||||
@@ -0,0 +1,64 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
import type { LensExtensionId } from "../lens-extension";
|
||||
import { action, computed, makeObservable, observable } from "mobx";
|
||||
import { toJS } from "../../common/utils";
|
||||
import type { BaseStoreDependencies } from "../../common/base-store/base-store";
|
||||
import { BaseStore } from "../../common/base-store/base-store";
|
||||
|
||||
export interface LensExtensionsStoreModel {
|
||||
extensions: Record<LensExtensionId, LensExtensionState>;
|
||||
}
|
||||
|
||||
export interface LensExtensionState {
|
||||
enabled?: boolean;
|
||||
name: string;
|
||||
}
|
||||
|
||||
export interface IsEnabledExtensionDescriptor {
|
||||
id: string;
|
||||
isBundled: boolean;
|
||||
}
|
||||
|
||||
export class ExtensionsStore extends BaseStore<LensExtensionsStoreModel> {
|
||||
constructor(deps: BaseStoreDependencies) {
|
||||
super(deps, {
|
||||
configName: "lens-extensions",
|
||||
});
|
||||
makeObservable(this);
|
||||
this.load();
|
||||
}
|
||||
|
||||
@computed
|
||||
get enabledExtensions() {
|
||||
return Array.from(this.state.values())
|
||||
.filter(({ enabled }) => enabled)
|
||||
.map(({ name }) => name);
|
||||
}
|
||||
|
||||
protected state = observable.map<LensExtensionId, LensExtensionState>();
|
||||
|
||||
isEnabled({ id, isBundled }: IsEnabledExtensionDescriptor): boolean {
|
||||
// By default false, so that copied extensions are disabled by default.
|
||||
// If user installs the extension from the UI, the Extensions component will specifically enable it.
|
||||
return isBundled || Boolean(this.state.get(id)?.enabled);
|
||||
}
|
||||
|
||||
mergeState = action((extensionsState: Record<LensExtensionId, LensExtensionState> | [LensExtensionId, LensExtensionState][]) => {
|
||||
this.state.merge(extensionsState);
|
||||
});
|
||||
|
||||
@action
|
||||
protected fromStore({ extensions }: LensExtensionsStoreModel) {
|
||||
this.state.merge(extensions);
|
||||
}
|
||||
|
||||
toJSON(): LensExtensionsStoreModel {
|
||||
return toJS({
|
||||
extensions: Object.fromEntries(this.state),
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
/**
|
||||
* 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 { computed } from "mobx";
|
||||
import extensionInstancesInjectable from "./extension-loader/extension-instances.injectable";
|
||||
|
||||
const extensionsInjectable = getInjectable({
|
||||
id: "extensions",
|
||||
instantiate: (di) => {
|
||||
const extensionInstances = di.inject(extensionInstancesInjectable);
|
||||
|
||||
return computed(() => [...extensionInstances.values()].filter(extension => extension.isEnabled));
|
||||
},
|
||||
});
|
||||
|
||||
export default extensionsInjectable;
|
||||
@@ -0,0 +1,58 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import { ipcMain } from "electron";
|
||||
import { IpcPrefix, IpcRegistrar } from "./ipc-registrar";
|
||||
import { Disposers, lensExtensionDependencies } from "../lens-extension";
|
||||
import type { LensMainExtension } from "../lens-main-extension";
|
||||
import type { Disposer } from "../../common/utils";
|
||||
import { once } from "lodash";
|
||||
import { ipcMainHandle } from "../../common/ipc";
|
||||
|
||||
export abstract class IpcMain extends IpcRegistrar {
|
||||
constructor(extension: LensMainExtension) {
|
||||
super(extension);
|
||||
|
||||
// Call the static method on the bottom child class.
|
||||
extension[Disposers].push(() => (this.constructor as typeof IpcMain).resetInstance());
|
||||
}
|
||||
|
||||
/**
|
||||
* Listen for broadcasts within your extension
|
||||
* @param channel The channel to listen for broadcasts on
|
||||
* @param listener The function that will be called with the arguments of the broadcast
|
||||
* @returns An optional disposer, Lens will cleanup when the extension is disabled or uninstalled even if this is not called
|
||||
*/
|
||||
listen(channel: string, listener: (event: Electron.IpcRendererEvent, ...args: any[]) => any): Disposer {
|
||||
const prefixedChannel = `extensions@${this[IpcPrefix]}:${channel}`;
|
||||
const cleanup = once(() => {
|
||||
this.extension[lensExtensionDependencies].logger.info(`[IPC-RENDERER]: removing extension listener`, { channel, extension: { name: this.extension.name, version: this.extension.version }});
|
||||
|
||||
return ipcMain.removeListener(prefixedChannel, listener);
|
||||
});
|
||||
|
||||
this.extension[lensExtensionDependencies].logger.info(`[IPC-RENDERER]: adding extension listener`, { channel, extension: { name: this.extension.name, version: this.extension.version }});
|
||||
ipcMain.addListener(prefixedChannel, listener);
|
||||
this.extension[Disposers].push(cleanup);
|
||||
|
||||
return cleanup;
|
||||
}
|
||||
|
||||
/**
|
||||
* Declare a RPC over `channel`. Lens will cleanup when the extension is disabled or uninstalled
|
||||
* @param channel The name of the RPC
|
||||
* @param handler The remote procedure that is called
|
||||
*/
|
||||
handle(channel: string, handler: (event: Electron.IpcMainInvokeEvent, ...args: any[]) => any): void {
|
||||
const prefixedChannel = `extensions@${this[IpcPrefix]}:${channel}`;
|
||||
|
||||
this.extension[lensExtensionDependencies].logger.info(`[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.info(`[IPC-RENDERER]: removing extension handler`, { channel, extension: { name: this.extension.name, version: this.extension.version }});
|
||||
|
||||
return ipcMain.removeHandler(prefixedChannel);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import { Singleton } from "../../common/utils";
|
||||
import type { LensExtension } from "../lens-extension";
|
||||
import { createHash } from "crypto";
|
||||
import { broadcastMessage } from "../../common/ipc";
|
||||
|
||||
export const IpcPrefix = Symbol();
|
||||
|
||||
export abstract class IpcRegistrar extends Singleton {
|
||||
readonly [IpcPrefix]: string;
|
||||
|
||||
constructor(protected readonly extension: LensExtension) {
|
||||
super();
|
||||
this[IpcPrefix] = createHash("sha256").update(extension.id).digest("hex");
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param channel The channel to broadcast to your whole extension, both `main` and `renderer`
|
||||
* @param args The arguments passed to all listeners
|
||||
*/
|
||||
broadcast(channel: string, ...args: any[]): void {
|
||||
broadcastMessage(`extensions@${this[IpcPrefix]}:${channel}`, ...args);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import { ipcRenderer } from "electron";
|
||||
import { IpcPrefix, IpcRegistrar } from "./ipc-registrar";
|
||||
import { Disposers } from "../lens-extension";
|
||||
import type { LensRendererExtension } from "../lens-renderer-extension";
|
||||
import type { Disposer } from "../../common/utils";
|
||||
import { once } from "lodash";
|
||||
|
||||
export abstract class IpcRenderer extends IpcRegistrar {
|
||||
constructor(extension: LensRendererExtension) {
|
||||
super(extension);
|
||||
|
||||
// Call the static method on the bottom child class.
|
||||
extension[Disposers].push(() => (this.constructor as typeof IpcRenderer).resetInstance());
|
||||
}
|
||||
|
||||
/**
|
||||
* Listen for broadcasts within your extension.
|
||||
* If the lifetime of the listener should be tied to the mounted lifetime of
|
||||
* a component then putting the returned value in a `disposeOnUnmount` call will suffice.
|
||||
* @param channel The channel to listen for broadcasts on
|
||||
* @param listener The function that will be called with the arguments of the broadcast
|
||||
* @returns An optional disposer, Lens will cleanup even if this is not called
|
||||
*/
|
||||
listen(channel: string, listener: (event: Electron.IpcRendererEvent, ...args: any[]) => any): Disposer {
|
||||
const prefixedChannel = `extensions@${this[IpcPrefix]}:${channel}`;
|
||||
const cleanup = once(() => {
|
||||
console.info(`[IPC-RENDERER]: removing extension listener`, { channel, extension: { name: this.extension.name, version: this.extension.version }});
|
||||
|
||||
return ipcRenderer.removeListener(prefixedChannel, listener);
|
||||
});
|
||||
|
||||
console.info(`[IPC-RENDERER]: adding extension listener`, { channel, extension: { name: this.extension.name, version: this.extension.version }});
|
||||
ipcRenderer.addListener(prefixedChannel, listener);
|
||||
this.extension[Disposers].push(cleanup);
|
||||
|
||||
return cleanup;
|
||||
}
|
||||
|
||||
/**
|
||||
* Request main to execute its function over the `channel` channel.
|
||||
* This function only interacts with functions registered via `Ipc.IpcMain.handleRpc`
|
||||
* An error will be thrown if no function has been registered on `main` with this channel ID.
|
||||
* @param channel The channel to invoke a RPC on
|
||||
* @param args The arguments to pass to the RPC
|
||||
* @returns A promise of the resulting value
|
||||
*/
|
||||
invoke(channel: string, ...args: any[]): Promise<any> {
|
||||
const prefixedChannel = `extensions@${this[IpcPrefix]}:${channel}`;
|
||||
|
||||
return ipcRenderer.invoke(prefixedChannel, ...args);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
/**
|
||||
* 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 { FileSystemProvisionerStore } from "./extension-loader/file-system-provisioner-store/file-system-provisioner-store";
|
||||
import type { NavigateForExtension } from "../main/start-main-application/lens-window/navigate-for-extension.injectable";
|
||||
import type { Logger } from "../common/logger";
|
||||
|
||||
export interface LensExtensionDependencies {
|
||||
readonly fileSystemProvisionerStore: FileSystemProvisionerStore;
|
||||
readonly logger: Logger;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
import type { InstalledExtension } from "./extension-discovery/extension-discovery";
|
||||
import { action, computed, makeObservable, observable } from "mobx";
|
||||
import type { PackageJson } from "type-fest";
|
||||
import { disposer } from "../common/utils";
|
||||
import type { LensExtensionDependencies } from "./lens-extension-set-dependencies";
|
||||
import type { ProtocolHandlerRegistration } from "./common-api/registrations";
|
||||
|
||||
export type LensExtensionId = string; // path to manifest (package.json)
|
||||
export type LensExtensionConstructor = new (...args: ConstructorParameters<typeof LensExtension>) => LensExtension;
|
||||
|
||||
export interface LensExtensionManifest extends PackageJson {
|
||||
name: string;
|
||||
version: string;
|
||||
main?: string; // path to %ext/dist/main.js
|
||||
renderer?: string; // path to %ext/dist/renderer.js
|
||||
/**
|
||||
* Supported Lens version engine by extension could be defined in `manifest.engines.lens`
|
||||
* Only MAJOR.MINOR version is taken in consideration.
|
||||
*/
|
||||
engines: {
|
||||
lens: string; // "semver"-package format
|
||||
npm?: string;
|
||||
node?: string;
|
||||
};
|
||||
}
|
||||
|
||||
export const lensExtensionDependencies = Symbol("lens-extension-dependencies");
|
||||
export const Disposers = Symbol("disposers");
|
||||
|
||||
export class LensExtension<Dependencies extends LensExtensionDependencies = LensExtensionDependencies> {
|
||||
readonly id: LensExtensionId;
|
||||
readonly manifest: LensExtensionManifest;
|
||||
readonly manifestPath: string;
|
||||
readonly isBundled: boolean;
|
||||
|
||||
get sanitizedExtensionId() {
|
||||
return sanitizeExtensionName(this.name);
|
||||
}
|
||||
|
||||
protocolHandlers: ProtocolHandlerRegistration[] = [];
|
||||
|
||||
@observable private _isEnabled = false;
|
||||
|
||||
@computed get isEnabled() {
|
||||
return this._isEnabled;
|
||||
}
|
||||
|
||||
[Disposers] = disposer();
|
||||
|
||||
constructor({ id, manifest, manifestPath, isBundled }: InstalledExtension) {
|
||||
makeObservable(this);
|
||||
this.id = id;
|
||||
this.manifest = manifest;
|
||||
this.manifestPath = manifestPath;
|
||||
this.isBundled = !!isBundled;
|
||||
}
|
||||
|
||||
get name() {
|
||||
return this.manifest.name;
|
||||
}
|
||||
|
||||
get version() {
|
||||
return this.manifest.version;
|
||||
}
|
||||
|
||||
get description() {
|
||||
return this.manifest.description;
|
||||
}
|
||||
|
||||
readonly [lensExtensionDependencies]!: Dependencies;
|
||||
|
||||
/**
|
||||
* getExtensionFileFolder returns the path to an already created folder. This
|
||||
* folder is for the sole use of this extension.
|
||||
*
|
||||
* Note: there is no security done on this folder, only obfuscation of the
|
||||
* folder name.
|
||||
*/
|
||||
async getExtensionFileFolder(): Promise<string> {
|
||||
return this[lensExtensionDependencies].fileSystemProvisionerStore.requestDirectory(this.id);
|
||||
}
|
||||
|
||||
@action
|
||||
async enable() {
|
||||
if (this._isEnabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._isEnabled = true;
|
||||
this[lensExtensionDependencies].logger.info(`[EXTENSION]: enabled ${this.name}@${this.version}`);
|
||||
}
|
||||
|
||||
@action
|
||||
async disable() {
|
||||
if (!this._isEnabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._isEnabled = false;
|
||||
|
||||
try {
|
||||
await this.onDeactivate();
|
||||
this[Disposers]();
|
||||
this[lensExtensionDependencies].logger.info(`[EXTENSION]: disabled ${this.name}@${this.version}`);
|
||||
} catch (error) {
|
||||
this[lensExtensionDependencies].logger.error(`[EXTENSION]: disabling ${this.name}@${this.version} threw an error: ${error}`);
|
||||
}
|
||||
}
|
||||
|
||||
async activate(): Promise<void> {
|
||||
return this.onActivate();
|
||||
}
|
||||
|
||||
protected onActivate(): Promise<void> | void {
|
||||
return;
|
||||
}
|
||||
|
||||
protected onDeactivate(): Promise<void> | void {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
export function sanitizeExtensionName(name: string) {
|
||||
return name.replace("@", "").replace("/", "--");
|
||||
}
|
||||
|
||||
export function getSanitizedPath(...parts: string[]) {
|
||||
return parts
|
||||
.filter(Boolean)
|
||||
.join("/")
|
||||
.replace(/\/+/g, "/")
|
||||
.replace(/\/$/, "");
|
||||
} // normalize multi-slashes (e.g. coming from page.id)
|
||||
|
||||
export function extensionDisplayName(name: string, version: string) {
|
||||
return `${name}@${version}`;
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
import { LensExtension, lensExtensionDependencies } from "./lens-extension";
|
||||
import type { CatalogEntity } from "../common/catalog";
|
||||
import type { IComputedValue, IObservableArray } 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";
|
||||
|
||||
export class LensMainExtension extends LensExtension<LensMainExtensionDependencies> {
|
||||
appMenus: MenuRegistration[] | IComputedValue<MenuRegistration[]> = [];
|
||||
trayMenus: TrayMenuRegistration[] | IComputedValue<TrayMenuRegistration[]> = [];
|
||||
|
||||
/**
|
||||
* implement this to modify the shell environment that Lens terminals are opened with. The ShellEnvModifier type has the signature
|
||||
*
|
||||
* (ctx: ShellEnvContext, env: Record<string, string | undefined>) => Record<string, string | undefined>
|
||||
*
|
||||
* @param ctx the shell environment context, specifically the relevant catalog entity for the terminal. This can be used, for example, to get
|
||||
* cluster-specific information that can be made available in the shell environment by the implementation of terminalShellEnvModifier
|
||||
*
|
||||
* @param env the current shell environment that the terminal will be opened with. The implementation should modify this as desired.
|
||||
*
|
||||
* @returns the modified shell environment that the terminal will be opened with. The implementation must return env as passed in, if it
|
||||
* does not modify the shell environment
|
||||
*/
|
||||
terminalShellEnvModifier?: ShellEnvModifier;
|
||||
|
||||
async navigate(pageId?: string, params?: Record<string, any>, frameId?: number) {
|
||||
await this[lensExtensionDependencies].navigate(this.id, pageId, params, frameId);
|
||||
}
|
||||
|
||||
addCatalogSource(id: string, source: IObservableArray<CatalogEntity>) {
|
||||
this[lensExtensionDependencies].entityRegistry.addObservableSource(`${this.name}:${id}`, source);
|
||||
}
|
||||
|
||||
removeCatalogSource(id: string) {
|
||||
this[lensExtensionDependencies].entityRegistry.removeSource(`${this.name}:${id}`);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* 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 type { Disposer } from "../common/utils";
|
||||
import type { EntityFilter } 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";
|
||||
import type { WelcomeBannerRegistration } from "../renderer/components/+welcome/welcome-banner-items/welcome-banner-registration";
|
||||
import type { CommandRegistration } from "../renderer/components/command-palette/registered-commands/commands";
|
||||
import type { AppPreferenceRegistration } from "../features/preferences/renderer/compliance-for-legacy-extension-api/app-preference-registration";
|
||||
import type { AdditionalCategoryColumnRegistration } from "../renderer/components/+catalog/custom-category-columns";
|
||||
import type { CustomCategoryViewRegistration } from "../renderer/components/+catalog/custom-views";
|
||||
import type { StatusBarRegistration } from "../renderer/components/status-bar/status-bar-registration";
|
||||
import type { KubeObjectMenuRegistration } from "../renderer/components/kube-object-menu/kube-object-menu-registration";
|
||||
import type { WorkloadsOverviewDetailRegistration } from "../renderer/components/+workloads-overview/workloads-overview-detail-registration";
|
||||
import type { KubeObjectStatusRegistration } from "../renderer/components/kube-object-status-icon/kube-object-status-registration";
|
||||
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";
|
||||
import type { ClusterFrameChildComponent } from "../renderer/frames/cluster-frame/cluster-frame-child-component-injection-token";
|
||||
import type { EntitySettingRegistration } from "../renderer/components/+entity-settings/extension-registrator.injectable";
|
||||
import type { CatalogEntityDetailRegistration } from "../renderer/components/+catalog/entity-details/token";
|
||||
import type { ClusterPageMenuRegistration, PageRegistration } from "./common-api/registrations";
|
||||
|
||||
export class LensRendererExtension extends LensExtension<LensRendererExtensionDependencies> {
|
||||
globalPages: PageRegistration[] = [];
|
||||
clusterPages: PageRegistration[] = [];
|
||||
clusterPageMenus: ClusterPageMenuRegistration[] = [];
|
||||
clusterFrameComponents: ClusterFrameChildComponent[] = [];
|
||||
kubeObjectStatusTexts: KubeObjectStatusRegistration[] = [];
|
||||
appPreferences: AppPreferenceRegistration[] = [];
|
||||
appPreferenceTabs: AppPreferenceTabRegistration[] = [];
|
||||
entitySettings: EntitySettingRegistration[] = [];
|
||||
statusBarItems: StatusBarRegistration[] = [];
|
||||
kubeObjectDetailItems: KubeObjectDetailRegistration[] = [];
|
||||
kubeObjectMenuItems: KubeObjectMenuRegistration[] = [];
|
||||
kubeWorkloadsOverviewItems: WorkloadsOverviewDetailRegistration[] = [];
|
||||
commands: CommandRegistration[] = [];
|
||||
welcomeMenus: WelcomeMenuRegistration[] = [];
|
||||
welcomeBanners: WelcomeBannerRegistration[] = [];
|
||||
catalogEntityDetailItems: CatalogEntityDetailRegistration<CatalogEntity>[] = [];
|
||||
topBarItems: TopBarRegistration[] = [];
|
||||
additionalCategoryColumns: AdditionalCategoryColumnRegistration[] = [];
|
||||
customCategoryViews: CustomCategoryViewRegistration[] = [];
|
||||
kubeObjectHandlers: KubeObjectHandlerRegistration[] = [];
|
||||
|
||||
async navigate(pageId?: string, params: object = {}) {
|
||||
const routes = this[lensExtensionDependencies].routes.get();
|
||||
const targetRegistration = [...this.globalPages, ...this.clusterPages]
|
||||
.find(registration => registration.id === (pageId || undefined));
|
||||
|
||||
if (!targetRegistration) {
|
||||
return;
|
||||
}
|
||||
|
||||
const targetRoutePath = getExtensionRoutePath(this, targetRegistration.id);
|
||||
const targetRoute = routes.find(matches({ path: targetRoutePath }));
|
||||
|
||||
if (!targetRoute) {
|
||||
return;
|
||||
}
|
||||
|
||||
const normalizedParams = this[lensExtensionDependencies].getExtensionPageParameters({
|
||||
extension: this,
|
||||
registration: targetRegistration,
|
||||
});
|
||||
const query = pipeline(
|
||||
params,
|
||||
toPairs,
|
||||
map(([key, value]) => [
|
||||
key,
|
||||
normalizedParams[key].stringify(value),
|
||||
]),
|
||||
fromPairs,
|
||||
);
|
||||
|
||||
this[lensExtensionDependencies].navigateToRoute(targetRoute, {
|
||||
query,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Defines if extension is enabled for a given cluster. This method is only
|
||||
* called when the extension is created within a cluster frame.
|
||||
*
|
||||
* The default implementation is to return `true`
|
||||
*
|
||||
* @deprecated Switch to using "enabled" or "visible" properties in each registration together with `activeCluster`
|
||||
*/
|
||||
async isEnabledForCluster(cluster: KubernetesCluster): Promise<Boolean> {
|
||||
return (void cluster) || true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a filtering function for the catalog entities. This will be removed if the extension is disabled.
|
||||
* @param fn The function which should return a truthy value for those entities which should be kept.
|
||||
* @returns A function to clean up the filter
|
||||
*/
|
||||
addCatalogFilter(fn: EntityFilter): Disposer {
|
||||
const dispose = this[lensExtensionDependencies].entityRegistry.addCatalogFilter(fn);
|
||||
|
||||
this[Disposers].push(dispose);
|
||||
|
||||
return dispose;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a filtering function for the catalog categories. This will be removed if the extension is disabled.
|
||||
* @param fn The function which should return a truthy value for those categories which should be kept.
|
||||
* @returns A function to clean up the filter
|
||||
*/
|
||||
addCatalogCategoryFilter(fn: CategoryFilter): Disposer {
|
||||
const dispose = this[lensExtensionDependencies].categoryRegistry.addCatalogCategoryFilter(fn);
|
||||
|
||||
this[Disposers].push(dispose);
|
||||
|
||||
return dispose;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
import type { CatalogEntity } from "../../common/catalog";
|
||||
import { asLegacyGlobalForExtensionApi } from "../as-legacy-globals-for-extension-api/as-legacy-global-object-for-extension-api";
|
||||
import catalogCategoryRegistryInjectable from "../../common/catalog/category-registry.injectable";
|
||||
import catalogEntityRegistryInjectable from "../../main/catalog/entity-registry.injectable";
|
||||
|
||||
export const catalogCategories = asLegacyGlobalForExtensionApi(catalogCategoryRegistryInjectable);
|
||||
const catalogEntityRegistry = asLegacyGlobalForExtensionApi(catalogEntityRegistryInjectable);
|
||||
|
||||
export interface CatalogEntityRegistry {
|
||||
getItemsForApiKind(apiVersion: string, kind: string): CatalogEntity[];
|
||||
/**
|
||||
* @deprecated use a cast instead of a unbounded type parameter
|
||||
*/
|
||||
getItemsForApiKind<T extends CatalogEntity>(apiVersion: string, kind: string): T[];
|
||||
}
|
||||
|
||||
export const catalogEntities: CatalogEntityRegistry = {
|
||||
getItemsForApiKind(apiVersion: string, kind: string) {
|
||||
return catalogEntityRegistry.filterItemsForApiKind(apiVersion, kind);
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,20 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
import * as Catalog from "./catalog";
|
||||
import * as Navigation from "./navigation";
|
||||
import * as K8sApi from "./k8s-api";
|
||||
import * as Power from "./power";
|
||||
import { IpcMain as Ipc } from "../ipc/ipc-main";
|
||||
import { LensMainExtension as LensExtension } from "../lens-main-extension";
|
||||
|
||||
export {
|
||||
Catalog,
|
||||
Navigation,
|
||||
K8sApi,
|
||||
Ipc,
|
||||
LensExtension,
|
||||
Power,
|
||||
};
|
||||
@@ -0,0 +1,14 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @deprecated This function never works
|
||||
* @returns false
|
||||
*/
|
||||
export function isAllowedResource(...args: any[]) {
|
||||
return Boolean(void args);
|
||||
}
|
||||
|
||||
export * from "../common-api/k8s-api";
|
||||
@@ -0,0 +1,19 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
import {
|
||||
Environments,
|
||||
getEnvironmentSpecificLegacyGlobalDiForExtensionApi,
|
||||
} from "../as-legacy-globals-for-extension-api/legacy-global-di-for-extension-api";
|
||||
|
||||
import navigateInjectable from "../../main/start-main-application/lens-window/navigate.injectable";
|
||||
|
||||
export function navigate(url: string) {
|
||||
const di = getEnvironmentSpecificLegacyGlobalDiForExtensionApi(Environments.main);
|
||||
|
||||
const navigate = di.inject(navigateInjectable);
|
||||
|
||||
return navigate(url);
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import { powerMonitor } from "electron";
|
||||
import type { Disposer } from "../../common/utils/disposer";
|
||||
|
||||
/**
|
||||
* Event listener for system power events
|
||||
*/
|
||||
export type PowerEventListener = () => void;
|
||||
|
||||
/**
|
||||
* Adds event listener to system suspend events
|
||||
* @param listener function which will be called on system suspend
|
||||
* @returns function to remove event listener
|
||||
*/
|
||||
export const onSuspend = (listener: PowerEventListener): Disposer => {
|
||||
powerMonitor.on("suspend", listener);
|
||||
|
||||
return () => {
|
||||
powerMonitor.off("suspend", listener);
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Adds event listener to system resume event
|
||||
* @param listener function which will be called on system resume
|
||||
* @returns function to remove event listener
|
||||
*/
|
||||
export const onResume = (listener: PowerEventListener): Disposer => {
|
||||
powerMonitor.on("resume", listener);
|
||||
|
||||
return () => {
|
||||
powerMonitor.off("resume", listener);
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Adds event listener to the event which is emitted when
|
||||
* the system is about to reboot or shut down
|
||||
* @param listener function which will be called on system shutdown
|
||||
* @returns function to remove event listener
|
||||
*/
|
||||
export const onShutdown = (listener: PowerEventListener): Disposer => {
|
||||
powerMonitor.on("shutdown", listener);
|
||||
|
||||
return () => {
|
||||
powerMonitor.off("shutdown", listener);
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,17 @@
|
||||
/**
|
||||
* 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 { IComputedValue } from "mobx";
|
||||
import extensionsInjectable from "./extensions.injectable";
|
||||
import type { LensMainExtension } from "./lens-main-extension";
|
||||
|
||||
const mainExtensionsInjectable = getInjectable({
|
||||
id: "main-extensions",
|
||||
|
||||
instantiate: (di) =>
|
||||
di.inject(extensionsInjectable) as IComputedValue<LensMainExtension[]>,
|
||||
});
|
||||
|
||||
export default mainExtensionsInjectable;
|
||||
@@ -0,0 +1,60 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
|
||||
import type { CatalogCategory, CatalogEntity } from "../../common/catalog";
|
||||
import type { CatalogEntityOnBeforeRun } from "../../renderer/api/catalog/entity/registry";
|
||||
import type { Disposer } from "../../common/utils";
|
||||
import catalogCategoryRegistryInjectable from "../../common/catalog/category-registry.injectable";
|
||||
import { asLegacyGlobalForExtensionApi } from "../as-legacy-globals-for-extension-api/as-legacy-global-object-for-extension-api";
|
||||
import catalogEntityRegistryInjectable from "../../renderer/api/catalog/entity/registry.injectable";
|
||||
import activeKubernetesClusterInjectable from "../../renderer/cluster-frame-context/active-kubernetes-cluster.injectable";
|
||||
|
||||
export const catalogCategories = asLegacyGlobalForExtensionApi(catalogCategoryRegistryInjectable);
|
||||
|
||||
const internalEntityRegistry = asLegacyGlobalForExtensionApi(catalogEntityRegistryInjectable);
|
||||
|
||||
export class CatalogEntityRegistry {
|
||||
/**
|
||||
* Currently active/visible entity
|
||||
*/
|
||||
get activeEntity() {
|
||||
return internalEntityRegistry.activeEntity;
|
||||
}
|
||||
|
||||
get entities(): Map<string, CatalogEntity> {
|
||||
return internalEntityRegistry.entities;
|
||||
}
|
||||
|
||||
getById(id: string) {
|
||||
return this.entities.get(id);
|
||||
}
|
||||
|
||||
getItemsForApiKind<T extends CatalogEntity>(apiVersion: string, kind: string): T[] {
|
||||
return internalEntityRegistry.getItemsForApiKind<T>(apiVersion, kind);
|
||||
}
|
||||
|
||||
getItemsForCategory<T extends CatalogEntity>(category: CatalogCategory): T[] {
|
||||
return internalEntityRegistry.getItemsForCategory(category);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a onBeforeRun hook to a catalog entities. If `onBeforeRun` was previously
|
||||
* added then it will not be added again.
|
||||
* @param onBeforeRun The function to be called with a `CatalogRunEvent`
|
||||
* event target will be the catalog entity. onBeforeRun hook can call event.preventDefault()
|
||||
* to stop run sequence
|
||||
* @returns A function to remove that hook
|
||||
*/
|
||||
addOnBeforeRun(onBeforeRun: CatalogEntityOnBeforeRun): Disposer {
|
||||
return internalEntityRegistry.addOnBeforeRun(onBeforeRun);
|
||||
}
|
||||
}
|
||||
|
||||
export const catalogEntities = new CatalogEntityRegistry();
|
||||
|
||||
export const activeCluster = asLegacyGlobalForExtensionApi(
|
||||
activeKubernetesClusterInjectable,
|
||||
);
|
||||
@@ -0,0 +1,173 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import { asLegacyGlobalFunctionForExtensionApi } from "../as-legacy-globals-for-extension-api/as-legacy-global-function-for-extension-api";
|
||||
import createTerminalTabInjectable from "../../renderer/components/dock/terminal/create-terminal-tab.injectable";
|
||||
import terminalStoreInjectable from "../../renderer/components/dock/terminal/store.injectable";
|
||||
import { asLegacyGlobalForExtensionApi } from "../as-legacy-globals-for-extension-api/as-legacy-global-object-for-extension-api";
|
||||
import logTabStoreInjectable from "../../renderer/components/dock/logs/tab-store.injectable";
|
||||
|
||||
import commandOverlayInjectable from "../../renderer/components/command-palette/command-overlay.injectable";
|
||||
import createPodLogsTabInjectable from "../../renderer/components/dock/logs/create-pod-logs-tab.injectable";
|
||||
import createWorkloadLogsTabInjectable from "../../renderer/components/dock/logs/create-workload-logs-tab.injectable";
|
||||
import sendCommandInjectable from "../../renderer/components/dock/terminal/send-command.injectable";
|
||||
import renameTabInjectable from "../../renderer/components/dock/dock/rename-tab.injectable";
|
||||
import { asLegacyGlobalObjectForExtensionApiWithModifications } from "../as-legacy-globals-for-extension-api/as-legacy-global-object-for-extension-api-with-modifications";
|
||||
import { ConfirmDialog as _ConfirmDialog } from "../../renderer/components/confirm-dialog";
|
||||
import type { ConfirmDialogBooleanParams, ConfirmDialogParams, ConfirmDialogProps } from "../../renderer/components/confirm-dialog";
|
||||
import openConfirmDialogInjectable from "../../renderer/components/confirm-dialog/open.injectable";
|
||||
import confirmInjectable from "../../renderer/components/confirm-dialog/confirm.injectable";
|
||||
import notificationsStoreInjectable from "../../renderer/components/notifications/notifications-store.injectable";
|
||||
import podStoreInjectable from "../../renderer/components/+workloads-pods/store.injectable";
|
||||
import getDetailsUrlInjectable from "../../renderer/components/kube-detail-params/get-details-url.injectable";
|
||||
import showDetailsInjectable from "../../renderer/components/kube-detail-params/show-details.injectable";
|
||||
import showCheckedErrorNotificationInjectable from "../../renderer/components/notifications/show-checked-error.injectable";
|
||||
import showErrorNotificationInjectable from "../../renderer/components/notifications/show-error-notification.injectable";
|
||||
import showInfoNotificationInjectable from "../../renderer/components/notifications/show-info-notification.injectable";
|
||||
import showShortInfoNotificationInjectable from "../../renderer/components/notifications/show-short-info.injectable";
|
||||
import showSuccessNotificationInjectable from "../../renderer/components/notifications/show-success-notification.injectable";
|
||||
|
||||
// layouts
|
||||
export * from "../../renderer/components/layout/main-layout";
|
||||
export * from "../../renderer/components/layout/setting-layout";
|
||||
export * from "../../renderer/components/layout/page-layout";
|
||||
export * from "../../renderer/components/layout/wizard-layout";
|
||||
export * from "../../renderer/components/layout/tab-layout";
|
||||
|
||||
// form-controls
|
||||
export * from "../../renderer/components/button";
|
||||
export * from "../../renderer/components/checkbox";
|
||||
export * from "../../renderer/components/radio";
|
||||
export * from "../../renderer/components/select";
|
||||
export * from "../../renderer/components/slider";
|
||||
export * from "../../renderer/components/switch";
|
||||
export * from "../../renderer/components/input/input";
|
||||
|
||||
// command-overlay
|
||||
export const CommandOverlay = asLegacyGlobalForExtensionApi(commandOverlayInjectable);
|
||||
|
||||
export type {
|
||||
CategoryColumnRegistration,
|
||||
AdditionalCategoryColumnRegistration,
|
||||
} from "../../renderer/components/+catalog/custom-category-columns";
|
||||
|
||||
// other components
|
||||
export type {
|
||||
ConfirmDialogBooleanParams,
|
||||
ConfirmDialogParams,
|
||||
ConfirmDialogProps,
|
||||
};
|
||||
export const ConfirmDialog = Object.assign(_ConfirmDialog, {
|
||||
open: asLegacyGlobalFunctionForExtensionApi(openConfirmDialogInjectable),
|
||||
confirm: asLegacyGlobalFunctionForExtensionApi(confirmInjectable),
|
||||
});
|
||||
|
||||
export * from "../../renderer/components/icon";
|
||||
export * from "../../renderer/components/tooltip";
|
||||
export * from "../../renderer/components/tabs";
|
||||
export * from "../../renderer/components/table";
|
||||
export * from "../../renderer/components/badge";
|
||||
export * from "../../renderer/components/drawer";
|
||||
export * from "../../renderer/components/dialog";
|
||||
export * from "../../renderer/components/line-progress";
|
||||
export * from "../../renderer/components/menu";
|
||||
|
||||
export {
|
||||
NotificationStatus,
|
||||
type CreateNotificationOptions,
|
||||
type Notification,
|
||||
type NotificationId,
|
||||
type NotificationMessage,
|
||||
type ShowNotification,
|
||||
type NotificationsStore,
|
||||
} from "../../renderer/components/notifications";
|
||||
|
||||
export const Notifications = {
|
||||
ok: asLegacyGlobalFunctionForExtensionApi(showSuccessNotificationInjectable),
|
||||
error: asLegacyGlobalFunctionForExtensionApi(showErrorNotificationInjectable),
|
||||
checkedError: asLegacyGlobalFunctionForExtensionApi(showCheckedErrorNotificationInjectable),
|
||||
info: asLegacyGlobalFunctionForExtensionApi(showInfoNotificationInjectable),
|
||||
shortInfo: asLegacyGlobalFunctionForExtensionApi(showShortInfoNotificationInjectable),
|
||||
};
|
||||
|
||||
export * from "../../renderer/components/spinner";
|
||||
export * from "../../renderer/components/stepper";
|
||||
export * from "../../renderer/components/wizard";
|
||||
export * from "../../renderer/components/+workloads-pods/pod-details-list";
|
||||
export * from "../../renderer/components/+namespaces/namespace-select";
|
||||
export * from "../../renderer/components/+namespaces/namespace-select-filter";
|
||||
export * from "../../renderer/components/layout/sub-title";
|
||||
export * from "../../renderer/components/input/search-input";
|
||||
export * from "../../renderer/components/chart/bar-chart";
|
||||
export * from "../../renderer/components/chart/pie-chart";
|
||||
export {
|
||||
MonacoEditor,
|
||||
type MonacoEditorProps, type MonacoEditorId,
|
||||
type MonacoTheme, type MonacoCustomTheme,
|
||||
} from "../../renderer/components/monaco-editor";
|
||||
|
||||
/**
|
||||
* @deprecated Use `Renderer.Navigation.getDetailsUrl`
|
||||
*/
|
||||
export const getDetailsUrl = asLegacyGlobalFunctionForExtensionApi(getDetailsUrlInjectable);
|
||||
|
||||
/**
|
||||
* @deprecated Use `Renderer.Navigation.showDetails`
|
||||
*/
|
||||
export const showDetails = asLegacyGlobalFunctionForExtensionApi(showDetailsInjectable);
|
||||
|
||||
// kube helpers
|
||||
export * from "../../renderer/components/kube-object-details";
|
||||
export * from "../../renderer/components/kube-object-list-layout";
|
||||
export * from "../../renderer/components/kube-object-menu";
|
||||
export * from "../../renderer/components/kube-object-meta";
|
||||
export * from "../../renderer/components/+events/kube-event-details";
|
||||
|
||||
// specific exports
|
||||
export * from "../../renderer/components/status-brick";
|
||||
|
||||
export const createTerminalTab = asLegacyGlobalFunctionForExtensionApi(createTerminalTabInjectable);
|
||||
|
||||
export const terminalStore = asLegacyGlobalObjectForExtensionApiWithModifications(
|
||||
terminalStoreInjectable,
|
||||
{
|
||||
sendCommand: asLegacyGlobalFunctionForExtensionApi(sendCommandInjectable),
|
||||
},
|
||||
);
|
||||
|
||||
const renameTab = asLegacyGlobalFunctionForExtensionApi(renameTabInjectable);
|
||||
const podStore = asLegacyGlobalForExtensionApi(podStoreInjectable);
|
||||
|
||||
export const logTabStore = asLegacyGlobalObjectForExtensionApiWithModifications(
|
||||
logTabStoreInjectable,
|
||||
{
|
||||
createPodTab: asLegacyGlobalFunctionForExtensionApi(createPodLogsTabInjectable),
|
||||
createWorkloadTab: asLegacyGlobalFunctionForExtensionApi(createWorkloadLogsTabInjectable),
|
||||
renameTab: (tabId: string): void => {
|
||||
const { selectedPodId } = logTabStore.getData(tabId) ?? {};
|
||||
const pod = selectedPodId && podStore.getById(selectedPodId);
|
||||
|
||||
if (pod) {
|
||||
renameTab(tabId, `Pod ${pod.getName()}`);
|
||||
}
|
||||
},
|
||||
tabs: undefined,
|
||||
},
|
||||
);
|
||||
|
||||
export class TerminalStore {
|
||||
static getInstance() {
|
||||
return terminalStore;
|
||||
}
|
||||
|
||||
static createInstance() {
|
||||
return terminalStore;
|
||||
}
|
||||
|
||||
static resetInstance() {
|
||||
console.warn("TerminalStore.resetInstance() does nothing");
|
||||
}
|
||||
}
|
||||
|
||||
export const notificationsStore = asLegacyGlobalForExtensionApi(notificationsStoreInjectable);
|
||||
@@ -0,0 +1,25 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
// Lens-extensions apis, required in renderer process runtime
|
||||
|
||||
// APIs
|
||||
import * as Catalog from "./catalog";
|
||||
import * as Component from "./components";
|
||||
import * as K8sApi from "./k8s-api";
|
||||
import * as Navigation from "./navigation";
|
||||
import * as Theme from "./theming";
|
||||
import { IpcRenderer as Ipc } from "../ipc/ipc-renderer";
|
||||
import { LensRendererExtension as LensExtension } from "../lens-renderer-extension";
|
||||
|
||||
export {
|
||||
Catalog,
|
||||
Component,
|
||||
K8sApi,
|
||||
Navigation,
|
||||
Theme,
|
||||
Ipc,
|
||||
LensExtension,
|
||||
};
|
||||
@@ -0,0 +1,129 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import type { KubeResource } from "../../common/rbac";
|
||||
import { apiResourceRecord } from "../../common/rbac";
|
||||
import { getLegacyGlobalDiForExtensionApi } from "../as-legacy-globals-for-extension-api/legacy-global-di-for-extension-api";
|
||||
import clusterRoleBindingApiInjectable from "../../common/k8s-api/endpoints/cluster-role-binding.api.injectable";
|
||||
import clusterRoleApiInjectable from "../../common/k8s-api/endpoints/cluster-role.api.injectable";
|
||||
import serviceAccountApiInjectable from "../../common/k8s-api/endpoints/service-account.api.injectable";
|
||||
import { asLegacyGlobalForExtensionApi } from "../as-legacy-globals-for-extension-api/as-legacy-global-object-for-extension-api";
|
||||
import roleApiInjectable from "../../common/k8s-api/endpoints/role.api.injectable";
|
||||
import podApiInjectable from "../../common/k8s-api/endpoints/pod.api.injectable";
|
||||
import daemonSetApiInjectable from "../../common/k8s-api/endpoints/daemon-set.api.injectable";
|
||||
import replicaSetApiInjectable from "../../common/k8s-api/endpoints/replica-set.api.injectable";
|
||||
import statefulSetApiInjectable from "../../common/k8s-api/endpoints/stateful-set.api.injectable";
|
||||
import deploymentApiInjectable from "../../common/k8s-api/endpoints/deployment.api.injectable";
|
||||
import jobApiInjectable from "../../common/k8s-api/endpoints/job.api.injectable";
|
||||
import cronJobApiInjectable from "../../common/k8s-api/endpoints/cron-job.api.injectable";
|
||||
import nodeApiInjectable from "../../common/k8s-api/endpoints/node.api.injectable";
|
||||
import configMapApiInjectable from "../../common/k8s-api/endpoints/config-map.api.injectable";
|
||||
import secretApiInjectable from "../../common/k8s-api/endpoints/secret.api.injectable";
|
||||
import resourceQuotaApiInjectable from "../../common/k8s-api/endpoints/resource-quota.api.injectable";
|
||||
import limitRangeApiInjectable from "../../common/k8s-api/endpoints/limit-range.api.injectable";
|
||||
import horizontalPodAutoscalerApiInjectable from "../../common/k8s-api/endpoints/horizontal-pod-autoscaler.api.injectable";
|
||||
import podDisruptionBudgetApiInjectable from "../../common/k8s-api/endpoints/pod-disruption-budget.api.injectable";
|
||||
import priorityClassStoreApiInjectable from "../../common/k8s-api/endpoints/priority-class.api.injectable";
|
||||
import serviceApiInjectable from "../../common/k8s-api/endpoints/service.api.injectable";
|
||||
import endpointsApiInjectable from "../../common/k8s-api/endpoints/endpoint.api.injectable";
|
||||
import ingressApiInjectable from "../../common/k8s-api/endpoints/ingress.api.injectable";
|
||||
import networkPolicyApiInjectable from "../../common/k8s-api/endpoints/network-policy.api.injectable";
|
||||
import persistentVolumeApiInjectable from "../../common/k8s-api/endpoints/persistent-volume.api.injectable";
|
||||
import persistentVolumeClaimApiInjectable from "../../common/k8s-api/endpoints/persistent-volume-claim.api.injectable";
|
||||
import storageClassApiInjectable from "../../common/k8s-api/endpoints/storage-class.api.injectable";
|
||||
import namespaceApiInjectable from "../../common/k8s-api/endpoints/namespace.api.injectable";
|
||||
import kubeEventApiInjectable from "../../common/k8s-api/endpoints/events.api.injectable";
|
||||
import roleBindingApiInjectable from "../../common/k8s-api/endpoints/role-binding.api.injectable";
|
||||
import customResourceDefinitionApiInjectable from "../../common/k8s-api/endpoints/custom-resource-definition.api.injectable";
|
||||
import { shouldShowResourceInjectionToken } from "../../common/cluster-store/allowed-resources-injection-token";
|
||||
|
||||
export function isAllowedResource(resources: KubeResource | KubeResource[]) {
|
||||
const di = getLegacyGlobalDiForExtensionApi();
|
||||
|
||||
return [resources].flat().every((resourceName) => {
|
||||
const resource = apiResourceRecord[resourceName];
|
||||
|
||||
if (!resource) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const _isAllowedResource = di.inject(shouldShowResourceInjectionToken, {
|
||||
apiName: resourceName,
|
||||
group: resource.group,
|
||||
});
|
||||
|
||||
// Note: Legacy isAllowedResource does not advertise reactivity
|
||||
return _isAllowedResource.get();
|
||||
});
|
||||
}
|
||||
|
||||
export const serviceAccountsApi = asLegacyGlobalForExtensionApi(serviceAccountApiInjectable);
|
||||
export const clusterRoleApi = asLegacyGlobalForExtensionApi(clusterRoleApiInjectable);
|
||||
export const clusterRoleBindingApi = asLegacyGlobalForExtensionApi(clusterRoleBindingApiInjectable);
|
||||
export const roleApi = asLegacyGlobalForExtensionApi(roleApiInjectable);
|
||||
export const podsApi = asLegacyGlobalForExtensionApi(podApiInjectable);
|
||||
export const daemonSetApi = asLegacyGlobalForExtensionApi(daemonSetApiInjectable);
|
||||
export const replicaSetApi = asLegacyGlobalForExtensionApi(replicaSetApiInjectable);
|
||||
export const statefulSetApi = asLegacyGlobalForExtensionApi(statefulSetApiInjectable);
|
||||
export const deploymentApi = asLegacyGlobalForExtensionApi(deploymentApiInjectable);
|
||||
export const jobApi = asLegacyGlobalForExtensionApi(jobApiInjectable);
|
||||
export const cronJobApi = asLegacyGlobalForExtensionApi(cronJobApiInjectable);
|
||||
export const nodesApi = asLegacyGlobalForExtensionApi(nodeApiInjectable);
|
||||
export const secretsApi = asLegacyGlobalForExtensionApi(secretApiInjectable);
|
||||
export const configMapApi = asLegacyGlobalForExtensionApi(configMapApiInjectable);
|
||||
export const resourceQuotaApi = asLegacyGlobalForExtensionApi(resourceQuotaApiInjectable);
|
||||
export const limitRangeApi = asLegacyGlobalForExtensionApi(limitRangeApiInjectable);
|
||||
export const serviceApi = asLegacyGlobalForExtensionApi(serviceApiInjectable);
|
||||
export const hpaApi = asLegacyGlobalForExtensionApi(horizontalPodAutoscalerApiInjectable);
|
||||
export const pdbApi = asLegacyGlobalForExtensionApi(podDisruptionBudgetApiInjectable);
|
||||
export const pcApi = asLegacyGlobalForExtensionApi(priorityClassStoreApiInjectable);
|
||||
export const endpointApi = asLegacyGlobalForExtensionApi(endpointsApiInjectable);
|
||||
export const ingressApi = asLegacyGlobalForExtensionApi(ingressApiInjectable);
|
||||
export const networkPolicyApi = asLegacyGlobalForExtensionApi(networkPolicyApiInjectable);
|
||||
export const persistentVolumeApi = asLegacyGlobalForExtensionApi(persistentVolumeApiInjectable);
|
||||
export const pvcApi = asLegacyGlobalForExtensionApi(persistentVolumeClaimApiInjectable);
|
||||
export const storageClassApi = asLegacyGlobalForExtensionApi(storageClassApiInjectable);
|
||||
export const namespacesApi = asLegacyGlobalForExtensionApi(namespaceApiInjectable);
|
||||
export const eventApi = asLegacyGlobalForExtensionApi(kubeEventApiInjectable);
|
||||
export const roleBindingApi = asLegacyGlobalForExtensionApi(roleBindingApiInjectable);
|
||||
export const crdApi = asLegacyGlobalForExtensionApi(customResourceDefinitionApiInjectable);
|
||||
|
||||
export * from "../common-api/k8s-api";
|
||||
|
||||
export {
|
||||
KubeObjectStatusLevel,
|
||||
type KubeObjectStatus,
|
||||
} from "../../common/k8s-api/kube-object-status";
|
||||
|
||||
// stores
|
||||
export type { EventStore } from "../../renderer/components/+events/store";
|
||||
export type { PodStore as PodsStore } from "../../renderer/components/+workloads-pods/store";
|
||||
export type { NodeStore as NodesStore } from "../../renderer/components/+nodes/store";
|
||||
export type { DeploymentStore } from "../../renderer/components/+workloads-deployments/store";
|
||||
export type { DaemonSetStore } from "../../renderer/components/+workloads-daemonsets/store";
|
||||
export type { StatefulSetStore } from "../../renderer/components/+workloads-statefulsets/store";
|
||||
export type { JobStore } from "../../renderer/components/+workloads-jobs/store";
|
||||
export type { CronJobStore } from "../../renderer/components/+workloads-cronjobs/store";
|
||||
export type { ConfigMapStore as ConfigMapsStore } from "../../renderer/components/+config-maps/store";
|
||||
export type { SecretStore as SecretsStore } from "../../renderer/components/+config-secrets/store";
|
||||
export type { ReplicaSetStore } from "../../renderer/components/+workloads-replicasets/store";
|
||||
export type { ResourceQuotaStore as ResourceQuotasStore } from "../../renderer/components/+config-resource-quotas/store";
|
||||
export type { LimitRangeStore as LimitRangesStore } from "../../renderer/components/+config-limit-ranges/store";
|
||||
export type { HorizontalPodAutoscalerStore as HPAStore } from "../../renderer/components/+config-autoscalers/store";
|
||||
export type { PodDisruptionBudgetStore as PodDisruptionBudgetsStore } from "../../renderer/components/+config-pod-disruption-budgets/store";
|
||||
export type { PriorityClassStore as PriorityClassStoreStore } from "../../renderer/components/+config-priority-classes/store";
|
||||
export type { ServiceStore } from "../../renderer/components/+network-services/store";
|
||||
export type { EndpointsStore as EndpointStore } from "../../renderer/components/+network-endpoints/store";
|
||||
export type { IngressStore } from "../../renderer/components/+network-ingresses/ingress-store";
|
||||
export type { IngressClassStore } from "../../renderer/components/+network-ingresses/ingress-class-store";
|
||||
export type { NetworkPolicyStore } from "../../renderer/components/+network-policies/store";
|
||||
export type { PersistentVolumeStore as PersistentVolumesStore } from "../../renderer/components/+storage-volumes/store";
|
||||
export type { PersistentVolumeClaimStore as VolumeClaimStore } from "../../renderer/components/+storage-volume-claims/store";
|
||||
export type { StorageClassStore } from "../../renderer/components/+storage-classes/store";
|
||||
export type { NamespaceStore } from "../../renderer/components/+namespaces/store";
|
||||
export type { ServiceAccountStore as ServiceAccountsStore } from "../../renderer/components/+user-management/+service-accounts/store";
|
||||
export type { RoleStore as RolesStore } from "../../renderer/components/+user-management/+roles/store";
|
||||
export type { RoleBindingStore as RoleBindingsStore } from "../../renderer/components/+user-management/+role-bindings/store";
|
||||
export type { CustomResourceDefinitionStore as CRDStore } from "../../renderer/components/+custom-resources/definition.store";
|
||||
export type { CustomResourceStore as CRDResourceStore } from "../../common/k8s-api/api-manager/resource.store";
|
||||
@@ -0,0 +1,22 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
import getDetailsUrlInjectable from "../../renderer/components/kube-detail-params/get-details-url.injectable";
|
||||
import hideDetailsInjectable from "../../renderer/components/kube-detail-params/hide-details.injectable";
|
||||
import showDetailsInjectable from "../../renderer/components/kube-detail-params/show-details.injectable";
|
||||
import createPageParamInjectable from "../../renderer/navigation/create-page-param.injectable";
|
||||
import isActiveRouteInjectable from "../../renderer/navigation/is-route-active.injectable";
|
||||
import navigateInjectable from "../../renderer/navigation/navigate.injectable";
|
||||
import { asLegacyGlobalFunctionForExtensionApi } from "../as-legacy-globals-for-extension-api/as-legacy-global-function-for-extension-api";
|
||||
|
||||
export type { PageParamInit, PageParam } from "../../renderer/navigation/page-param";
|
||||
export type { URLParams } from "../../common/utils/buildUrl";
|
||||
|
||||
export const getDetailsUrl = asLegacyGlobalFunctionForExtensionApi(getDetailsUrlInjectable);
|
||||
export const showDetails = asLegacyGlobalFunctionForExtensionApi(showDetailsInjectable);
|
||||
export const hideDetails = asLegacyGlobalFunctionForExtensionApi(hideDetailsInjectable);
|
||||
export const createPageParam = asLegacyGlobalFunctionForExtensionApi(createPageParamInjectable);
|
||||
export const isActiveRoute = asLegacyGlobalFunctionForExtensionApi(isActiveRouteInjectable);
|
||||
export const navigate = asLegacyGlobalFunctionForExtensionApi(navigateInjectable);
|
||||
@@ -0,0 +1,19 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
import activeThemeInjectable from "../../renderer/themes/active.injectable";
|
||||
import type { LensTheme } from "../../renderer/themes/lens-theme";
|
||||
import { asLegacyGlobalForExtensionApi } from "../as-legacy-globals-for-extension-api/as-legacy-global-object-for-extension-api";
|
||||
|
||||
export const activeTheme = asLegacyGlobalForExtensionApi(activeThemeInjectable);
|
||||
|
||||
/**
|
||||
* @deprecated This hides the reactivity of active theme, use {@link activeTheme} instead
|
||||
*/
|
||||
export function getActiveTheme() {
|
||||
return activeTheme.get();
|
||||
}
|
||||
|
||||
export type { LensTheme };
|
||||
@@ -0,0 +1,15 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import { getInjectable } from "@ogre-tools/injectable";
|
||||
import type { IComputedValue } from "mobx";
|
||||
import extensionsInjectable from "./extensions.injectable";
|
||||
import type { LensRendererExtension } from "./lens-renderer-extension";
|
||||
|
||||
const rendererExtensionsInjectable = getInjectable({
|
||||
id: "renderer-extensions",
|
||||
instantiate: (di) => di.inject(extensionsInjectable) as IComputedValue<LensRendererExtension[]>,
|
||||
});
|
||||
|
||||
export default rendererExtensionsInjectable;
|
||||
Reference in new issue
Block a user