mirror of
https://github.com/lensapp/lens.git
synced 2025-05-20 05:10:56 +00:00
Merge branch 'master' into metrics-extraction
This commit is contained in:
commit
e0040e1c1c
8
package-lock.json
generated
8
package-lock.json
generated
@ -23326,9 +23326,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/joi": {
|
"node_modules/joi": {
|
||||||
"version": "17.8.4",
|
"version": "17.9.1",
|
||||||
"resolved": "https://registry.npmjs.org/joi/-/joi-17.8.4.tgz",
|
"resolved": "https://registry.npmjs.org/joi/-/joi-17.9.1.tgz",
|
||||||
"integrity": "sha512-jjdRHb5WtL+KgSHvOULQEPPv4kcl+ixd1ybOFQq3rWLgEEqc03QMmilodL0GVJE14U/SQDXkUhQUSZANGDH/AA==",
|
"integrity": "sha512-FariIi9j6QODKATGBrEX7HZcja8Bsh3rfdGYy/Sb65sGlZWK/QWesU1ghk7aJWDj95knjXlQfSmzFSPPkLVsfw==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@hapi/hoek": "^9.0.0",
|
"@hapi/hoek": "^9.0.0",
|
||||||
"@hapi/topo": "^5.0.0",
|
"@hapi/topo": "^5.0.0",
|
||||||
@ -38180,7 +38180,7 @@
|
|||||||
"hpagent": "^1.2.0",
|
"hpagent": "^1.2.0",
|
||||||
"http-proxy": "^1.18.1",
|
"http-proxy": "^1.18.1",
|
||||||
"immer": "^9.0.19",
|
"immer": "^9.0.19",
|
||||||
"joi": "^17.7.1",
|
"joi": "^17.9.1",
|
||||||
"js-yaml": "^4.1.0",
|
"js-yaml": "^4.1.0",
|
||||||
"lodash": "^4.17.15",
|
"lodash": "^4.17.15",
|
||||||
"marked": "^4.2.12",
|
"marked": "^4.2.12",
|
||||||
|
|||||||
@ -21,13 +21,13 @@
|
|||||||
"postdev": "lerna watch -- lerna run build --stream --scope \\$LERNA_PACKAGE_NAME",
|
"postdev": "lerna watch -- lerna run build --stream --scope \\$LERNA_PACKAGE_NAME",
|
||||||
"prestart-dev": "cd packages/open-lens && rimraf static/build/ && npm run build:tray-icons && npm run download:binaries",
|
"prestart-dev": "cd packages/open-lens && rimraf static/build/ && npm run build:tray-icons && npm run download:binaries",
|
||||||
"start-dev": "lerna run start",
|
"start-dev": "lerna run start",
|
||||||
"lint": "lerna run lint --stream",
|
"lint": "lerna run lint --stream --no-bail",
|
||||||
"lint:fix": "lerna run lint:fix --stream",
|
"lint:fix": "lerna run lint:fix --stream",
|
||||||
"mkdocs:serve-local": "docker build -t mkdocs-serve-local:latest mkdocs/ && docker run --rm -it -p 8000:8000 -v ${PWD}:/docs mkdocs-serve-local:latest",
|
"mkdocs:serve-local": "docker build -t mkdocs-serve-local:latest mkdocs/ && docker run --rm -it -p 8000:8000 -v ${PWD}:/docs mkdocs-serve-local:latest",
|
||||||
"mkdocs:verify": "docker build -t mkdocs-serve-local:latest mkdocs/ && docker run --rm -v ${PWD}:/docs mkdocs-serve-local:latest build --strict",
|
"mkdocs:verify": "docker build -t mkdocs-serve-local:latest mkdocs/ && docker run --rm -v ${PWD}:/docs mkdocs-serve-local:latest build --strict",
|
||||||
"test:unit": "lerna run --stream test:unit",
|
"test:unit": "lerna run --stream test:unit --no-bail",
|
||||||
"test:unit:watch": "jest --watch",
|
"test:unit:watch": "jest --watch",
|
||||||
"test:integration": "lerna run --stream test:integration",
|
"test:integration": "lerna run --stream test:integration --no-bail",
|
||||||
"bump-version": "lerna version --no-git-tag-version --no-push",
|
"bump-version": "lerna version --no-git-tag-version --no-push",
|
||||||
"precreate-release-pr": "cd packages/release-tool && npm run build",
|
"precreate-release-pr": "cd packages/release-tool && npm run build",
|
||||||
"create-release-pr": "node packages/release-tool/dist/index.js"
|
"create-release-pr": "node packages/release-tool/dist/index.js"
|
||||||
|
|||||||
@ -1,11 +1,7 @@
|
|||||||
import { pipeline } from "@ogre-tools/fp";
|
import { pipeline } from "@ogre-tools/fp";
|
||||||
import { filter, isString } from "lodash/fp";
|
import { filter, isString } from "lodash/fp";
|
||||||
import { getInjectable } from "@ogre-tools/injectable";
|
import { getInjectable } from "@ogre-tools/injectable";
|
||||||
import {
|
import { Binding, KeyboardShortcut, keyboardShortcutInjectionToken } from "./keyboard-shortcut-injection-token";
|
||||||
Binding,
|
|
||||||
KeyboardShortcut,
|
|
||||||
keyboardShortcutInjectionToken,
|
|
||||||
} from "./keyboard-shortcut-injection-token";
|
|
||||||
import platformInjectable from "./platform.injectable";
|
import platformInjectable from "./platform.injectable";
|
||||||
|
|
||||||
export type InvokeShortcut = (event: KeyboardEvent) => void;
|
export type InvokeShortcut = (event: KeyboardEvent) => void;
|
||||||
@ -46,29 +42,26 @@ const toBindingWithDefaults = (binding: Binding) =>
|
|||||||
...binding,
|
...binding,
|
||||||
};
|
};
|
||||||
|
|
||||||
const toShortcutsWithMatchingBinding =
|
const toShortcutsWithMatchingBinding = (event: KeyboardEvent, platform: string) => (shortcut: KeyboardShortcut) => {
|
||||||
(event: KeyboardEvent, platform: string) => (shortcut: KeyboardShortcut) => {
|
const binding = toBindingWithDefaults(shortcut.binding);
|
||||||
const binding = toBindingWithDefaults(shortcut.binding);
|
|
||||||
|
|
||||||
const shiftModifierMatches = binding.shift === event.shiftKey;
|
const shiftModifierMatches = binding.shift === event.shiftKey;
|
||||||
const altModifierMatches = binding.altOrOption === event.altKey;
|
const altModifierMatches = binding.altOrOption === event.altKey;
|
||||||
|
|
||||||
const isMac = platform === "darwin";
|
const isMac = platform === "darwin";
|
||||||
|
|
||||||
const ctrlModifierMatches =
|
const ctrlModifierMatches = binding.ctrl === event.ctrlKey || (!isMac && binding.ctrlOrCommand === event.ctrlKey);
|
||||||
binding.ctrl === event.ctrlKey || (!isMac && binding.ctrlOrCommand === event.ctrlKey);
|
|
||||||
|
|
||||||
const metaModifierMatches =
|
const metaModifierMatches = binding.meta === event.metaKey || (isMac && binding.ctrlOrCommand === event.metaKey);
|
||||||
binding.meta === event.metaKey || (isMac && binding.ctrlOrCommand === event.metaKey);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
event.code === binding.code &&
|
event.code === binding.code &&
|
||||||
shiftModifierMatches &&
|
shiftModifierMatches &&
|
||||||
ctrlModifierMatches &&
|
ctrlModifierMatches &&
|
||||||
altModifierMatches &&
|
altModifierMatches &&
|
||||||
metaModifierMatches
|
metaModifierMatches
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const invokeShortcutInjectable = getInjectable({
|
const invokeShortcutInjectable = getInjectable({
|
||||||
id: "invoke-shortcut",
|
id: "invoke-shortcut",
|
||||||
|
|||||||
@ -26,10 +26,7 @@ const NonInjectedKeyboardShortcutListener = ({
|
|||||||
return <>{children}</>;
|
return <>{children}</>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const KeyboardShortcutListener = withInjectables<
|
export const KeyboardShortcutListener = withInjectables<Dependencies, KeyboardShortcutListenerProps>(
|
||||||
Dependencies,
|
|
||||||
KeyboardShortcutListenerProps
|
|
||||||
>(
|
|
||||||
NonInjectedKeyboardShortcutListener,
|
NonInjectedKeyboardShortcutListener,
|
||||||
|
|
||||||
{
|
{
|
||||||
|
|||||||
@ -175,8 +175,7 @@ describe("keyboard-shortcuts", () => {
|
|||||||
shouldCallCallback: true,
|
shouldCallCallback: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
scenario:
|
scenario: "given shortcut with shift modifier, when shortcut is pressed, calls the callback",
|
||||||
"given shortcut with shift modifier, when shortcut is pressed, calls the callback",
|
|
||||||
|
|
||||||
binding: { shift: true, code: "F1" },
|
binding: { shift: true, code: "F1" },
|
||||||
keyboard: "{Shift>}[F1]",
|
keyboard: "{Shift>}[F1]",
|
||||||
|
|||||||
@ -143,7 +143,7 @@
|
|||||||
"hpagent": "^1.2.0",
|
"hpagent": "^1.2.0",
|
||||||
"http-proxy": "^1.18.1",
|
"http-proxy": "^1.18.1",
|
||||||
"immer": "^9.0.19",
|
"immer": "^9.0.19",
|
||||||
"joi": "^17.7.1",
|
"joi": "^17.9.1",
|
||||||
"js-yaml": "^4.1.0",
|
"js-yaml": "^4.1.0",
|
||||||
"lodash": "^4.17.15",
|
"lodash": "^4.17.15",
|
||||||
"marked": "^4.2.12",
|
"marked": "^4.2.12",
|
||||||
|
|||||||
@ -206,3 +206,17 @@ describe("ApiManager", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("ApiManger without storesAndApisCanBeCreated", () => {
|
||||||
|
let di: DiContainer;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
di = getDiForUnitTesting();
|
||||||
|
|
||||||
|
di.override(storesAndApisCanBeCreatedInjectable, () => false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should not throw when creating apiManager", () => {
|
||||||
|
di.inject(apiManagerInjectable);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@ -25,7 +25,7 @@ export type KubeObjectStoreFrom<Api> = Api extends KubeApi<infer KubeObj, infer
|
|||||||
|
|
||||||
export type FindApiCallback = (api: KubeApi<KubeObject>) => boolean;
|
export type FindApiCallback = (api: KubeApi<KubeObject>) => boolean;
|
||||||
|
|
||||||
interface Dependencies {
|
export interface ApiManagerDependencies {
|
||||||
readonly apis: IComputedValue<KubeApi[]>;
|
readonly apis: IComputedValue<KubeApi[]>;
|
||||||
readonly crdApis: IComputedValue<KubeApi[]>;
|
readonly crdApis: IComputedValue<KubeApi[]>;
|
||||||
readonly stores: IComputedValue<KubeObjectStore[]>;
|
readonly stores: IComputedValue<KubeObjectStore[]>;
|
||||||
@ -38,7 +38,7 @@ export class ApiManager {
|
|||||||
private readonly defaultCrdStores = observable.map<string, KubeObjectStore>();
|
private readonly defaultCrdStores = observable.map<string, KubeObjectStore>();
|
||||||
private readonly apis = observable.map<string, KubeApi>();
|
private readonly apis = observable.map<string, KubeApi>();
|
||||||
|
|
||||||
constructor(private readonly dependencies: Dependencies) {
|
constructor(private readonly dependencies: ApiManagerDependencies) {
|
||||||
// NOTE: this is done to preserve the old behaviour of an API being discoverable using all previous apiBases
|
// NOTE: this is done to preserve the old behaviour of an API being discoverable using all previous apiBases
|
||||||
autorun(() => {
|
autorun(() => {
|
||||||
const apis = iter.chain(this.dependencies.apis.get().values())
|
const apis = iter.chain(this.dependencies.apis.get().values())
|
||||||
|
|||||||
@ -18,18 +18,23 @@ const apiManagerInjectable = getInjectable({
|
|||||||
const computedInjectMany = di.inject(computedInjectManyInjectable);
|
const computedInjectMany = di.inject(computedInjectManyInjectable);
|
||||||
const storesAndApisCanBeCreated = di.inject(storesAndApisCanBeCreatedInjectionToken);
|
const storesAndApisCanBeCreated = di.inject(storesAndApisCanBeCreatedInjectionToken);
|
||||||
|
|
||||||
return new ApiManager({
|
return new ApiManager((
|
||||||
apis: storesAndApisCanBeCreated
|
storesAndApisCanBeCreated
|
||||||
? computedInjectMany(kubeApiInjectionToken)
|
? {
|
||||||
: computed(() => []),
|
apis: computedInjectMany(kubeApiInjectionToken),
|
||||||
stores: storesAndApisCanBeCreated
|
stores: computedInjectMany(kubeObjectStoreInjectionToken),
|
||||||
? computedInjectMany(kubeObjectStoreInjectionToken)
|
crdApis: computedInjectMany(customResourceDefinitionApiInjectionToken),
|
||||||
: computed(() => []),
|
createCustomResourceStore: di.inject(createCustomResourceStoreInjectable),
|
||||||
crdApis: storesAndApisCanBeCreated
|
}
|
||||||
? computedInjectMany(customResourceDefinitionApiInjectionToken)
|
: {
|
||||||
: computed(() => []),
|
apis: computed(() => []),
|
||||||
createCustomResourceStore: di.inject(createCustomResourceStoreInjectable),
|
stores: computed(() => []),
|
||||||
});
|
crdApis: computed(() => []),
|
||||||
|
createCustomResourceStore: () => {
|
||||||
|
throw new Error("Tried to create a KubeObjectStore for a CustomResource in a disallowed environment");
|
||||||
|
},
|
||||||
|
}
|
||||||
|
));
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -109,7 +109,25 @@ export function parseKubeApi(path: string): IKubeApiParsed {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createKubeApiURL({ apiPrefix = "/apis", resource, apiVersion, name, namespace }: IKubeApiLinkRef): string {
|
function isIKubeApiParsed(refOrParsed: IKubeApiLinkRef | IKubeApiParsed): refOrParsed is IKubeApiParsed {
|
||||||
|
return "apiGroup" in refOrParsed;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createKubeApiURL(linkRef: IKubeApiLinkRef): string;
|
||||||
|
export function createKubeApiURL(linkParsed: IKubeApiParsed): string;
|
||||||
|
|
||||||
|
export function createKubeApiURL(ref: IKubeApiLinkRef | IKubeApiParsed): string {
|
||||||
|
if (isIKubeApiParsed(ref)) {
|
||||||
|
return createKubeApiURL({
|
||||||
|
apiPrefix: ref.apiPrefix,
|
||||||
|
resource: ref.resource,
|
||||||
|
name: ref.name,
|
||||||
|
namespace: ref.namespace,
|
||||||
|
apiVersion: `${ref.apiGroup}/${ref.apiVersion}`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const { apiPrefix = "/apis", resource, apiVersion, name, namespace } = ref;
|
||||||
const parts = [apiPrefix, apiVersion];
|
const parts = [apiPrefix, apiVersion];
|
||||||
|
|
||||||
if (namespace) {
|
if (namespace) {
|
||||||
|
|||||||
@ -193,7 +193,7 @@ export interface KubeApiWatchOptions<Object extends KubeObject, Data extends Kub
|
|||||||
|
|
||||||
export type KubeApiPatchType = "merge" | "json" | "strategic";
|
export type KubeApiPatchType = "merge" | "json" | "strategic";
|
||||||
|
|
||||||
const patchTypeHeaders: Record<KubeApiPatchType, string> = {
|
export const patchTypeHeaders: Record<KubeApiPatchType, string> = {
|
||||||
"merge": "application/merge-patch+json",
|
"merge": "application/merge-patch+json",
|
||||||
"json": "application/json-patch+json",
|
"json": "application/json-patch+json",
|
||||||
"strategic": "application/strategic-merge-patch+json",
|
"strategic": "application/strategic-merge-patch+json",
|
||||||
|
|||||||
@ -437,6 +437,19 @@ const resourceApplierAnnotationsForFiltering = [
|
|||||||
|
|
||||||
const filterOutResourceApplierAnnotations = (label: string) => !resourceApplierAnnotationsForFiltering.some(key => label.startsWith(key));
|
const filterOutResourceApplierAnnotations = (label: string) => !resourceApplierAnnotationsForFiltering.some(key => label.startsWith(key));
|
||||||
|
|
||||||
|
export interface RawKubeObject<
|
||||||
|
Metadata extends KubeObjectMetadata = KubeObjectMetadata,
|
||||||
|
Status = Record<string, unknown>,
|
||||||
|
Spec = Record<string, unknown>,
|
||||||
|
> {
|
||||||
|
apiVersion: string;
|
||||||
|
kind: string;
|
||||||
|
metadata: Metadata;
|
||||||
|
status?: Status;
|
||||||
|
spec?: Spec;
|
||||||
|
managedFields?: any;
|
||||||
|
}
|
||||||
|
|
||||||
export class KubeObject<
|
export class KubeObject<
|
||||||
Metadata extends KubeObjectMetadata<KubeObjectScope> = KubeObjectMetadata<KubeObjectScope>,
|
Metadata extends KubeObjectMetadata<KubeObjectScope> = KubeObjectMetadata<KubeObjectScope>,
|
||||||
Status = unknown,
|
Status = unknown,
|
||||||
@ -538,23 +551,6 @@ export class KubeObject<
|
|||||||
return Object.entries(labels).map(([name, value]) => `${name}=${value}`);
|
return Object.entries(labels).map(([name, value]) => `${name}=${value}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* These must be RFC6902 compliant paths
|
|
||||||
*/
|
|
||||||
private static readonly nonEditablePathPrefixes = [
|
|
||||||
"/metadata/managedFields",
|
|
||||||
"/status",
|
|
||||||
];
|
|
||||||
private static readonly nonEditablePaths = new Set([
|
|
||||||
"/apiVersion",
|
|
||||||
"/kind",
|
|
||||||
"/metadata/name",
|
|
||||||
"/metadata/selfLink",
|
|
||||||
"/metadata/resourceVersion",
|
|
||||||
"/metadata/uid",
|
|
||||||
...KubeObject.nonEditablePathPrefixes,
|
|
||||||
]);
|
|
||||||
|
|
||||||
constructor(data: KubeJsonApiData<Metadata, Status, Spec>) {
|
constructor(data: KubeJsonApiData<Metadata, Status, Spec>) {
|
||||||
if (typeof data !== "object") {
|
if (typeof data !== "object") {
|
||||||
throw new TypeError(`Cannot create a KubeObject from ${typeof data}`);
|
throw new TypeError(`Cannot create a KubeObject from ${typeof data}`);
|
||||||
@ -684,18 +680,6 @@ export class KubeObject<
|
|||||||
* @deprecated use KubeApi.patch instead
|
* @deprecated use KubeApi.patch instead
|
||||||
*/
|
*/
|
||||||
async patch(patch: Patch): Promise<KubeJsonApiData | null> {
|
async patch(patch: Patch): Promise<KubeJsonApiData | null> {
|
||||||
for (const op of patch) {
|
|
||||||
if (KubeObject.nonEditablePaths.has(op.path)) {
|
|
||||||
throw new Error(`Failed to update ${this.kind}: JSON pointer ${op.path} has been modified`);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const pathPrefix of KubeObject.nonEditablePathPrefixes) {
|
|
||||||
if (op.path.startsWith(`${pathPrefix}/`)) {
|
|
||||||
throw new Error(`Failed to update ${this.kind}: Child JSON pointer of ${op.path} has been modified`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const di = getLegacyGlobalDiForExtensionApi();
|
const di = getLegacyGlobalDiForExtensionApi();
|
||||||
const requestKubeObjectPatch = di.inject(requestKubeObjectPatchInjectable);
|
const requestKubeObjectPatch = di.inject(requestKubeObjectPatchInjectable);
|
||||||
const result = await requestKubeObjectPatch(this.getName(), this.kind, this.getNs(), patch);
|
const result = await requestKubeObjectPatch(this.getName(), this.kind, this.getNs(), patch);
|
||||||
|
|||||||
@ -209,7 +209,7 @@ export abstract class LensProtocolRouter {
|
|||||||
return name;
|
return name;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!this.dependencies.isExtensionEnabled(extension)) {
|
if (!extension.isBundled && !this.dependencies.isExtensionEnabled(extension.id)) {
|
||||||
this.dependencies.logger.info(`${LensProtocolRouter.LoggingPrefix}: Extension ${name} matched, but not enabled`);
|
this.dependencies.logger.info(`${LensProtocolRouter.LoggingPrefix}: Extension ${name} matched, but not enabled`);
|
||||||
|
|
||||||
return name;
|
return name;
|
||||||
|
|||||||
@ -0,0 +1,29 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||||
|
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type { DiContainerForInjection, InjectionToken } from "@ogre-tools/injectable";
|
||||||
|
import { getInjectable } from "@ogre-tools/injectable";
|
||||||
|
import platformInjectable from "../vars/platform.injectable";
|
||||||
|
|
||||||
|
export interface PlatformSpecific<T> {
|
||||||
|
instantiate: () => T;
|
||||||
|
readonly platform: NodeJS.Platform;
|
||||||
|
}
|
||||||
|
|
||||||
|
const platformSpecificVersionInjectable = getInjectable({
|
||||||
|
id: "platform-specific-version",
|
||||||
|
instantiate: (di: DiContainerForInjection) => {
|
||||||
|
const targetPlatform = di.inject(platformInjectable);
|
||||||
|
|
||||||
|
return <T>(token: InjectionToken<PlatformSpecific<T>, void>) => (
|
||||||
|
di.injectMany(token)
|
||||||
|
.find(impl => impl.platform === targetPlatform)
|
||||||
|
?.instantiate()
|
||||||
|
);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export default platformSpecificVersionInjectable;
|
||||||
|
|
||||||
@ -113,13 +113,13 @@ describe("ExtensionLoader", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("renderer updates extension after ipc broadcast", async () => {
|
it("renderer updates extension after ipc broadcast", async () => {
|
||||||
expect(extensionLoader.userExtensions).toEqual(new Map());
|
expect(extensionLoader.userExtensions.get()).toEqual(new Map());
|
||||||
|
|
||||||
await extensionLoader.init();
|
await extensionLoader.init();
|
||||||
await delay(10);
|
await delay(10);
|
||||||
|
|
||||||
// Assert the extensions after the extension broadcast event
|
// Assert the extensions after the extension broadcast event
|
||||||
expect(extensionLoader.userExtensions).toEqual(
|
expect(extensionLoader.userExtensions.get()).toEqual(
|
||||||
new Map([
|
new Map([
|
||||||
["manifest/path", {
|
["manifest/path", {
|
||||||
absolutePath: "/test/1",
|
absolutePath: "/test/1",
|
||||||
|
|||||||
@ -14,6 +14,7 @@ import { buildVersionInjectionToken } from "../../common/vars/build-semantic-ver
|
|||||||
import { asLegacyGlobalForExtensionApi } from "../as-legacy-globals-for-extension-api/as-legacy-global-object-for-extension-api";
|
import { asLegacyGlobalForExtensionApi } from "../as-legacy-globals-for-extension-api/as-legacy-global-object-for-extension-api";
|
||||||
import enabledExtensionsInjectable from "../../features/extensions/enabled/common/enabled-extensions.injectable";
|
import enabledExtensionsInjectable from "../../features/extensions/enabled/common/enabled-extensions.injectable";
|
||||||
import userPreferencesStateInjectable from "../../features/user-preferences/common/state.injectable";
|
import userPreferencesStateInjectable from "../../features/user-preferences/common/state.injectable";
|
||||||
|
import { lensBuildEnvironmentInjectionToken } from "@k8slens/application";
|
||||||
|
|
||||||
const userStore = asLegacyGlobalForExtensionApi(userPreferencesStateInjectable);
|
const userStore = asLegacyGlobalForExtensionApi(userPreferencesStateInjectable);
|
||||||
const enabledExtensions = asLegacyGlobalForExtensionApi(enabledExtensionsInjectable);
|
const enabledExtensions = asLegacyGlobalForExtensionApi(enabledExtensionsInjectable);
|
||||||
@ -53,6 +54,11 @@ export const App = {
|
|||||||
|
|
||||||
return di.inject(isLinuxInjectable);
|
return di.inject(isLinuxInjectable);
|
||||||
},
|
},
|
||||||
|
get lensBuildEnvironment() {
|
||||||
|
const di = getLegacyGlobalDiForExtensionApi();
|
||||||
|
|
||||||
|
return di.inject(lensBuildEnvironmentInjectionToken);
|
||||||
|
},
|
||||||
/**
|
/**
|
||||||
* @deprecated This value is now `""` and is left here for backwards compatibility.
|
* @deprecated This value is now `""` and is left here for backwards compatibility.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@ -10,14 +10,14 @@ import { broadcastMessage, ipcMainHandle, ipcRendererOn } from "../../common/ipc
|
|||||||
import { toJS } from "../../common/utils";
|
import { toJS } from "../../common/utils";
|
||||||
import { isErrnoException } from "@k8slens/utilities";
|
import { isErrnoException } from "@k8slens/utilities";
|
||||||
import type { ExtensionLoader } from "../extension-loader";
|
import type { ExtensionLoader } from "../extension-loader";
|
||||||
import type { InstalledExtension, LensExtensionId, LensExtensionManifest } from "@k8slens/legacy-extensions";
|
import type { InstalledExtension, LensExtensionId, LensExtensionManifest, ExternalInstalledExtension } from "@k8slens/legacy-extensions";
|
||||||
import type { ExtensionInstallationStateStore } from "../extension-installation-state-store/extension-installation-state-store";
|
import type { ExtensionInstallationStateStore } from "../extension-installation-state-store/extension-installation-state-store";
|
||||||
import { extensionDiscoveryStateChannel } from "../../common/ipc/extension-handling";
|
import { extensionDiscoveryStateChannel } from "../../common/ipc/extension-handling";
|
||||||
import { requestInitialExtensionDiscovery } from "../../renderer/ipc";
|
import { requestInitialExtensionDiscovery } from "../../renderer/ipc";
|
||||||
import type { ReadJson } from "../../common/fs/read-json-file.injectable";
|
import type { ReadJson } from "../../common/fs/read-json-file.injectable";
|
||||||
import type { Logger } from "../../common/logger";
|
import type { Logger } from "../../common/logger";
|
||||||
import type { PathExists } from "../../common/fs/path-exists.injectable";
|
import type { PathExists } from "../../common/fs/path-exists.injectable";
|
||||||
import type { Watch } from "../../common/fs/watch/watch.injectable";
|
import type { Watch, Watcher } from "../../common/fs/watch/watch.injectable";
|
||||||
import type { Stats } from "fs";
|
import type { Stats } from "fs";
|
||||||
import type { LStat } from "../../common/fs/lstat.injectable";
|
import type { LStat } from "../../common/fs/lstat.injectable";
|
||||||
import type { ReadDirectory } from "../../common/fs/read-directory.injectable";
|
import type { ReadDirectory } from "../../common/fs/read-directory.injectable";
|
||||||
@ -73,10 +73,6 @@ interface ExtensionDiscoveryChannelMessage {
|
|||||||
*/
|
*/
|
||||||
const isDirectoryLike = (lstat: Stats) => lstat.isDirectory() || lstat.isSymbolicLink();
|
const isDirectoryLike = (lstat: Stats) => lstat.isDirectory() || lstat.isSymbolicLink();
|
||||||
|
|
||||||
interface LoadFromFolderOptions {
|
|
||||||
isBundled?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ExtensionDiscoveryEvents {
|
interface ExtensionDiscoveryEvents {
|
||||||
add: (ext: InstalledExtension) => void;
|
add: (ext: InstalledExtension) => void;
|
||||||
remove: (extId: LensExtensionId) => void;
|
remove: (extId: LensExtensionId) => void;
|
||||||
@ -153,6 +149,8 @@ export class ExtensionDiscovery {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private _watch: Watcher<false>|undefined;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Watches for added/removed local extensions.
|
* Watches for added/removed local extensions.
|
||||||
* Dependencies are installed automatically after an extension folder is copied.
|
* Dependencies are installed automatically after an extension folder is copied.
|
||||||
@ -163,7 +161,7 @@ export class ExtensionDiscovery {
|
|||||||
// Wait until .load() has been called and has been resolved
|
// Wait until .load() has been called and has been resolved
|
||||||
await this.whenLoaded;
|
await this.whenLoaded;
|
||||||
|
|
||||||
this.dependencies.watch(this.localFolderPath, {
|
this._watch = this.dependencies.watch(this.localFolderPath, {
|
||||||
// For adding and removing symlinks to work, the depth has to be 1.
|
// For adding and removing symlinks to work, the depth has to be 1.
|
||||||
depth: 1,
|
depth: 1,
|
||||||
ignoreInitial: true,
|
ignoreInitial: true,
|
||||||
@ -183,6 +181,12 @@ export class ExtensionDiscovery {
|
|||||||
.on("unlink", this.handleWatchUnlinkEvent);
|
.on("unlink", this.handleWatchUnlinkEvent);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async stopWatchingExtensions() {
|
||||||
|
this.dependencies.logger.info(`${logModule} stopping the watch for extensions`);
|
||||||
|
|
||||||
|
await this._watch?.close();
|
||||||
|
}
|
||||||
|
|
||||||
handleWatchFileAdd = async (manifestPath: string): Promise<void> => {
|
handleWatchFileAdd = async (manifestPath: string): Promise<void> => {
|
||||||
// e.g. "foo/package.json"
|
// e.g. "foo/package.json"
|
||||||
const relativePath = this.dependencies.getRelativePath(this.localFolderPath, manifestPath);
|
const relativePath = this.dependencies.getRelativePath(this.localFolderPath, manifestPath);
|
||||||
@ -271,7 +275,7 @@ export class ExtensionDiscovery {
|
|||||||
* @param extensionId The ID of the extension to uninstall.
|
* @param extensionId The ID of the extension to uninstall.
|
||||||
*/
|
*/
|
||||||
async uninstallExtension(extensionId: LensExtensionId): Promise<void> {
|
async uninstallExtension(extensionId: LensExtensionId): Promise<void> {
|
||||||
const extension = this.extensions.get(extensionId) ?? this.dependencies.extensionLoader.getExtension(extensionId);
|
const extension = this.extensions.get(extensionId) ?? this.dependencies.extensionLoader.getExtensionById(extensionId);
|
||||||
|
|
||||||
if (!extension) {
|
if (!extension) {
|
||||||
return void this.dependencies.logger.warn(`${logModule} could not uninstall extension, not found`, { id: extensionId });
|
return void this.dependencies.logger.warn(`${logModule} could not uninstall extension, not found`, { id: extensionId });
|
||||||
@ -330,24 +334,26 @@ export class ExtensionDiscovery {
|
|||||||
* Returns InstalledExtension from path to package.json file.
|
* Returns InstalledExtension from path to package.json file.
|
||||||
* Also updates this.packagesJson.
|
* Also updates this.packagesJson.
|
||||||
*/
|
*/
|
||||||
protected async getByManifest(manifestPath: string, { isBundled = false } = {}): Promise<InstalledExtension | null> {
|
protected async loadExtensionFromFolder(folderPath: string): Promise<ExternalInstalledExtension | null> {
|
||||||
|
const manifestPath = this.dependencies.joinPaths(folderPath, manifestFilename);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const manifest = await this.dependencies.readJsonFile(manifestPath) as unknown as LensExtensionManifest;
|
const manifest = await this.dependencies.readJsonFile(manifestPath) as unknown as LensExtensionManifest;
|
||||||
const id = isBundled ? manifestPath : this.getInstalledManifestPath(manifest.name);
|
const id = this.getInstalledManifestPath(manifest.name);
|
||||||
const isEnabled = this.dependencies.isExtensionEnabled({ id, isBundled });
|
const isEnabled = this.dependencies.isExtensionEnabled(id);
|
||||||
const extensionDir = this.dependencies.getDirnameOfPath(manifestPath);
|
const extensionDir = this.dependencies.getDirnameOfPath(manifestPath);
|
||||||
const npmPackage = this.dependencies.joinPaths(extensionDir, `${manifest.name}-${manifest.version}.tgz`);
|
const npmPackage = this.dependencies.joinPaths(extensionDir, `${manifest.name}-${manifest.version}.tgz`);
|
||||||
const absolutePath = this.dependencies.isProduction && await this.dependencies.pathExists(npmPackage)
|
const absolutePath = this.dependencies.isProduction && await this.dependencies.pathExists(npmPackage)
|
||||||
? npmPackage
|
? npmPackage
|
||||||
: extensionDir;
|
: extensionDir;
|
||||||
const isCompatible = isBundled || this.dependencies.isCompatibleExtension(manifest);
|
const isCompatible = this.dependencies.isCompatibleExtension(manifest);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id,
|
id,
|
||||||
absolutePath,
|
absolutePath,
|
||||||
manifestPath: id,
|
manifestPath: id,
|
||||||
manifest,
|
manifest,
|
||||||
isBundled,
|
isBundled: false,
|
||||||
isEnabled,
|
isEnabled,
|
||||||
isCompatible,
|
isCompatible,
|
||||||
};
|
};
|
||||||
@ -363,14 +369,14 @@ export class ExtensionDiscovery {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async ensureExtensions(): Promise<Map<LensExtensionId, InstalledExtension>> {
|
async ensureExtensions(): Promise<Map<LensExtensionId, ExternalInstalledExtension>> {
|
||||||
const userExtensions = await this.loadFromFolder(this.localFolderPath);
|
const userExtensions = await this.loadFromFolder(this.localFolderPath);
|
||||||
|
|
||||||
return this.extensions = new Map(userExtensions.map(extension => [extension.id, extension]));
|
return this.extensions = new Map(userExtensions.map(extension => [extension.id, extension]));
|
||||||
}
|
}
|
||||||
|
|
||||||
async loadFromFolder(folderPath: string): Promise<InstalledExtension[]> {
|
async loadFromFolder(folderPath: string): Promise<ExternalInstalledExtension[]> {
|
||||||
const extensions: InstalledExtension[] = [];
|
const extensions: ExternalInstalledExtension[] = [];
|
||||||
const paths = await this.dependencies.readDirectory(folderPath);
|
const paths = await this.dependencies.readDirectory(folderPath);
|
||||||
|
|
||||||
for (const fileName of paths) {
|
for (const fileName of paths) {
|
||||||
@ -403,16 +409,6 @@ export class ExtensionDiscovery {
|
|||||||
return 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 {
|
toJSON(): ExtensionDiscoveryChannelMessage {
|
||||||
return toJS({
|
return toJS({
|
||||||
isLoaded: this.isLoaded,
|
isLoaded: this.isLoaded,
|
||||||
|
|||||||
@ -0,0 +1,23 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||||
|
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||||
|
*/
|
||||||
|
import { getInjectable } from "@ogre-tools/injectable";
|
||||||
|
import { beforeQuitOfBackEndInjectionToken } from "../../main/start-main-application/runnable-tokens/phases";
|
||||||
|
import extensionDiscoveryInjectable from "./extension-discovery.injectable";
|
||||||
|
|
||||||
|
const stopWatchingExtensionsOnQuitInjectable = getInjectable({
|
||||||
|
id: "stop-watching-extensions-on-quit",
|
||||||
|
|
||||||
|
instantiate: (di) => ({
|
||||||
|
run: async () => {
|
||||||
|
const extensionDiscovery = di.inject(extensionDiscoveryInjectable);
|
||||||
|
|
||||||
|
await extensionDiscovery.stopWatchingExtensions();
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
|
||||||
|
injectionToken: beforeQuitOfBackEndInjectionToken,
|
||||||
|
});
|
||||||
|
|
||||||
|
export default stopWatchingExtensionsOnQuitInjectable;
|
||||||
@ -3,11 +3,14 @@
|
|||||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { LensExtensionConstructor, InstalledExtension } from "@k8slens/legacy-extensions";
|
import type { LensExtensionConstructor, BundledInstalledExtension, ExternalInstalledExtension, BundledLensExtensionConstructor } from "@k8slens/legacy-extensions";
|
||||||
import { getInjectionToken } from "@ogre-tools/injectable";
|
import { getInjectionToken } from "@ogre-tools/injectable";
|
||||||
import type { LensExtension } from "../lens-extension";
|
import type { LensExtension } from "../lens-extension";
|
||||||
|
|
||||||
export type CreateExtensionInstance = (ExtensionClass: LensExtensionConstructor, extension: InstalledExtension) => LensExtension;
|
export interface CreateExtensionInstance {
|
||||||
|
(ExtensionClass: LensExtensionConstructor, extension: ExternalInstalledExtension): LensExtension;
|
||||||
|
(ExtensionClass: BundledLensExtensionConstructor, extension: BundledInstalledExtension): LensExtension;
|
||||||
|
}
|
||||||
|
|
||||||
export const createExtensionInstanceInjectionToken = getInjectionToken<CreateExtensionInstance>({
|
export const createExtensionInstanceInjectionToken = getInjectionToken<CreateExtensionInstance>({
|
||||||
id: "create-extension-instance-token",
|
id: "create-extension-instance-token",
|
||||||
|
|||||||
@ -6,9 +6,10 @@
|
|||||||
import { ipcMain, ipcRenderer } from "electron";
|
import { ipcMain, ipcRenderer } from "electron";
|
||||||
import { isEqual } from "lodash";
|
import { isEqual } from "lodash";
|
||||||
import type { ObservableMap } from "mobx";
|
import type { ObservableMap } from "mobx";
|
||||||
import { action, computed, makeObservable, toJS, observable, observe, reaction, when } from "mobx";
|
import { runInAction, action, computed, toJS, observable, reaction, when } from "mobx";
|
||||||
import { broadcastMessage, ipcMainOn, ipcRendererOn, ipcMainHandle } from "../../common/ipc";
|
import { broadcastMessage, ipcMainOn, ipcRendererOn, ipcMainHandle } from "../../common/ipc";
|
||||||
import { isDefined } from "@k8slens/utilities";
|
import { isDefined, iter } from "@k8slens/utilities";
|
||||||
|
import type { ExternalInstalledExtension, InstalledExtension, LensExtensionConstructor, LensExtensionId, BundledExtension } from "@k8slens/legacy-extensions";
|
||||||
import type { LensExtension } from "../lens-extension";
|
import type { LensExtension } from "../lens-extension";
|
||||||
import { extensionLoaderFromMainChannel, extensionLoaderFromRendererChannel } from "../../common/ipc/extension-handling";
|
import { extensionLoaderFromMainChannel, extensionLoaderFromRendererChannel } from "../../common/ipc/extension-handling";
|
||||||
import { requestExtensionLoaderInitialState } from "../../renderer/ipc";
|
import { requestExtensionLoaderInitialState } from "../../renderer/ipc";
|
||||||
@ -19,7 +20,6 @@ import type { Extension } from "./extension/extension.injectable";
|
|||||||
import type { Logger } from "../../common/logger";
|
import type { Logger } from "../../common/logger";
|
||||||
import type { JoinPaths } from "../../common/path/join-paths.injectable";
|
import type { JoinPaths } from "../../common/path/join-paths.injectable";
|
||||||
import type { GetDirnameOfPath } from "../../common/path/get-dirname.injectable";
|
import type { GetDirnameOfPath } from "../../common/path/get-dirname.injectable";
|
||||||
import type { LensExtensionId, BundledExtension, InstalledExtension, LensExtensionConstructor } from "@k8slens/legacy-extensions";
|
|
||||||
import type { UpdateExtensionsState } from "../../features/extensions/enabled/common/update-state.injectable";
|
import type { UpdateExtensionsState } from "../../features/extensions/enabled/common/update-state.injectable";
|
||||||
|
|
||||||
const logModule = "[EXTENSIONS-LOADER]";
|
const logModule = "[EXTENSIONS-LOADER]";
|
||||||
@ -60,51 +60,21 @@ export class ExtensionLoader {
|
|||||||
*/
|
*/
|
||||||
protected readonly nonInstancesByName = observable.set<string>();
|
protected readonly nonInstancesByName = observable.set<string>();
|
||||||
|
|
||||||
/**
|
protected readonly instancesByName = computed(() => new Map((
|
||||||
* This is updated by the `observe` in the constructor. DO NOT write directly to it
|
iter.chain(this.dependencies.extensionInstances.entries())
|
||||||
*/
|
.map(([, instance]) => [instance.name, instance])
|
||||||
protected readonly instancesByName = observable.map<string, LensExtension>();
|
)));
|
||||||
|
|
||||||
private readonly onRemoveExtensionId = new EventEmitter<[string]>();
|
private readonly onRemoveExtensionId = new EventEmitter<[string]>();
|
||||||
|
|
||||||
@observable isLoaded = false;
|
readonly isLoaded = observable.box(false);
|
||||||
|
|
||||||
get whenLoaded() {
|
constructor(protected readonly dependencies: Dependencies) {}
|
||||||
return when(() => this.isLoaded);
|
|
||||||
}
|
|
||||||
|
|
||||||
constructor(protected readonly dependencies: Dependencies) {
|
readonly userExtensions = computed(() => new Map((
|
||||||
makeObservable(this);
|
this.extensions.toJSON()
|
||||||
|
.filter(([, extension]) => !extension.isBundled)
|
||||||
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
|
* Get the extension instance by its manifest name
|
||||||
@ -120,19 +90,18 @@ export class ExtensionLoader {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.instancesByName.get(name);
|
return this.instancesByName.get().get(name);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Transform userExtensions to a state object for storing into ExtensionsStore
|
// Transform userExtensions to a state object for storing into ExtensionsStore
|
||||||
@computed get storeState() {
|
readonly storeState = computed(() => Array.from(
|
||||||
return Array.from(this.userExtensions)
|
this.userExtensions.get(),
|
||||||
.map(([extId, extension]) => [extId, {
|
([extId, extension]) => [extId, {
|
||||||
enabled: extension.isEnabled,
|
enabled: extension.isEnabled,
|
||||||
name: extension.manifest.name,
|
name: extension.manifest.name,
|
||||||
}] as const);
|
}] as const,
|
||||||
}
|
));
|
||||||
|
|
||||||
@action
|
|
||||||
async init() {
|
async init() {
|
||||||
if (ipcMain) {
|
if (ipcMain) {
|
||||||
await this.initMain();
|
await this.initMain();
|
||||||
@ -140,7 +109,7 @@ export class ExtensionLoader {
|
|||||||
await this.initRenderer();
|
await this.initRenderer();
|
||||||
}
|
}
|
||||||
|
|
||||||
await Promise.all([this.whenLoaded]);
|
await when(() => this.isLoaded.get());
|
||||||
|
|
||||||
// broadcasting extensions between main/renderer processes
|
// broadcasting extensions between main/renderer processes
|
||||||
reaction(() => this.toJSON(), () => this.broadcastExtensions(), {
|
reaction(() => this.toJSON(), () => this.broadcastExtensions(), {
|
||||||
@ -148,8 +117,7 @@ export class ExtensionLoader {
|
|||||||
});
|
});
|
||||||
|
|
||||||
reaction(
|
reaction(
|
||||||
() => this.storeState,
|
() => this.storeState.get(),
|
||||||
|
|
||||||
(state) => {
|
(state) => {
|
||||||
this.dependencies.updateExtensionsState(state);
|
this.dependencies.updateExtensionsState(state);
|
||||||
},
|
},
|
||||||
@ -199,18 +167,20 @@ export class ExtensionLoader {
|
|||||||
setIsEnabled(lensExtensionId: LensExtensionId, isEnabled: boolean) {
|
setIsEnabled(lensExtensionId: LensExtensionId, isEnabled: boolean) {
|
||||||
const extension = this.extensions.get(lensExtensionId);
|
const extension = this.extensions.get(lensExtensionId);
|
||||||
|
|
||||||
assert(extension, `Must register extension ${lensExtensionId} with before enabling it`);
|
assert(extension, `Extension "${lensExtensionId}" must be registered before it can be enabled.`);
|
||||||
|
assert(!extension.isBundled, `Cannot change the enabled state of a bundled extension`);
|
||||||
|
|
||||||
extension.isEnabled = isEnabled;
|
extension.isEnabled = isEnabled;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async initMain() {
|
protected async initMain() {
|
||||||
this.isLoaded = true;
|
runInAction(() => {
|
||||||
|
this.isLoaded.set(true);
|
||||||
|
});
|
||||||
|
|
||||||
await this.autoInitExtensions();
|
await this.autoInitExtensions();
|
||||||
|
|
||||||
ipcMainHandle(extensionLoaderFromMainChannel, () => {
|
ipcMainHandle(extensionLoaderFromMainChannel, () => [...this.toJSON()]);
|
||||||
return Array.from(this.toJSON());
|
|
||||||
});
|
|
||||||
|
|
||||||
ipcMainOn(extensionLoaderFromRendererChannel, (event, extensions: [LensExtensionId, InstalledExtension][]) => {
|
ipcMainOn(extensionLoaderFromRendererChannel, (event, extensions: [LensExtensionId, InstalledExtension][]) => {
|
||||||
this.syncExtensions(extensions);
|
this.syncExtensions(extensions);
|
||||||
@ -219,7 +189,9 @@ export class ExtensionLoader {
|
|||||||
|
|
||||||
protected async initRenderer() {
|
protected async initRenderer() {
|
||||||
const extensionListHandler = (extensions: [LensExtensionId, InstalledExtension][]) => {
|
const extensionListHandler = (extensions: [LensExtensionId, InstalledExtension][]) => {
|
||||||
this.isLoaded = true;
|
runInAction(() => {
|
||||||
|
this.isLoaded.set(true);
|
||||||
|
});
|
||||||
this.syncExtensions(extensions);
|
this.syncExtensions(extensions);
|
||||||
|
|
||||||
const receivedExtensionIds = extensions.map(([lensExtensionId]) => lensExtensionId);
|
const receivedExtensionIds = extensions.map(([lensExtensionId]) => lensExtensionId);
|
||||||
@ -255,10 +227,10 @@ export class ExtensionLoader {
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected async loadBundledExtensions() {
|
protected async loadBundledExtensions() {
|
||||||
return this.dependencies.bundledExtensions
|
const bundledExtensions = await Promise.all((this.dependencies.bundledExtensions
|
||||||
.map(extension => {
|
.map(async extension => {
|
||||||
try {
|
try {
|
||||||
const LensExtensionClass = extension[this.dependencies.extensionEntryPointName]();
|
const LensExtensionClass = await extension[this.dependencies.extensionEntryPointName]();
|
||||||
|
|
||||||
if (!LensExtensionClass) {
|
if (!LensExtensionClass) {
|
||||||
return null;
|
return null;
|
||||||
@ -291,7 +263,9 @@ export class ExtensionLoader {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.filter(isDefined);
|
));
|
||||||
|
|
||||||
|
return bundledExtensions.filter(isDefined);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async loadExtensions(extensions: ExtensionBeingActivated[]): Promise<ExtensionLoading[]> {
|
protected async loadExtensions(extensions: ExtensionBeingActivated[]): Promise<ExtensionLoading[]> {
|
||||||
@ -332,6 +306,7 @@ export class ExtensionLoader {
|
|||||||
// 4. Return ExtensionLoading[]
|
// 4. Return ExtensionLoading[]
|
||||||
|
|
||||||
return [...installedExtensions.entries()]
|
return [...installedExtensions.entries()]
|
||||||
|
.filter((entry): entry is [string, ExternalInstalledExtension] => !entry[1].isBundled)
|
||||||
.map(([extId, extension]) => {
|
.map(([extId, extension]) => {
|
||||||
const alreadyInit = this.dependencies.extensionInstances.has(extId) || this.nonInstancesByName.has(extension.manifest.name);
|
const alreadyInit = this.dependencies.extensionInstances.has(extId) || this.nonInstancesByName.has(extension.manifest.name);
|
||||||
|
|
||||||
@ -391,7 +366,7 @@ export class ExtensionLoader {
|
|||||||
return loadedExtensions;
|
return loadedExtensions;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected requireExtension(extension: InstalledExtension): LensExtensionConstructor | null {
|
protected requireExtension(extension: ExternalInstalledExtension): LensExtensionConstructor | null {
|
||||||
const extRelativePath = extension.manifest[this.dependencies.extensionEntryPointName];
|
const extRelativePath = extension.manifest[this.dependencies.extensionEntryPointName];
|
||||||
|
|
||||||
if (!extRelativePath) {
|
if (!extRelativePath) {
|
||||||
@ -411,7 +386,7 @@ export class ExtensionLoader {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
getExtension(extId: LensExtensionId) {
|
getExtensionById(extId: LensExtensionId) {
|
||||||
return this.extensions.get(extId);
|
return this.extensions.get(extId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -9,7 +9,6 @@ import type { LensExtensionDependencies } from "./lens-extension-set-dependencie
|
|||||||
import type { ProtocolHandlerRegistration } from "../common/protocol-handler/registration";
|
import type { ProtocolHandlerRegistration } from "../common/protocol-handler/registration";
|
||||||
import type { InstalledExtension, LegacyLensExtension, LensExtensionId, LensExtensionManifest } from "@k8slens/legacy-extensions";
|
import type { InstalledExtension, LegacyLensExtension, LensExtensionId, LensExtensionManifest } from "@k8slens/legacy-extensions";
|
||||||
|
|
||||||
|
|
||||||
export const lensExtensionDependencies = Symbol("lens-extension-dependencies");
|
export const lensExtensionDependencies = Symbol("lens-extension-dependencies");
|
||||||
export const Disposers = Symbol("disposers");
|
export const Disposers = Symbol("disposers");
|
||||||
|
|
||||||
@ -42,14 +41,12 @@ export class LensExtension<
|
|||||||
[Disposers] = disposer();
|
[Disposers] = disposer();
|
||||||
|
|
||||||
constructor({ id, manifest, manifestPath, isBundled }: InstalledExtension) {
|
constructor({ id, manifest, manifestPath, isBundled }: InstalledExtension) {
|
||||||
makeObservable(this);
|
|
||||||
|
|
||||||
// id is the name of the manifest
|
// id is the name of the manifest
|
||||||
this.id = id;
|
this.id = id;
|
||||||
|
this.manifest = manifest as LensExtensionManifest;
|
||||||
this.manifest = manifest;
|
|
||||||
this.manifestPath = manifestPath;
|
this.manifestPath = manifestPath;
|
||||||
this.isBundled = !!isBundled;
|
this.isBundled = !!isBundled;
|
||||||
|
makeObservable(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
get name() {
|
get name() {
|
||||||
|
|||||||
@ -4,7 +4,14 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { getInjectionToken } from "@ogre-tools/injectable";
|
import { getInjectionToken } from "@ogre-tools/injectable";
|
||||||
|
import type { PlatformSpecific } from "../../../common/utils/platform-specific-version.injectable";
|
||||||
|
|
||||||
export const requestSystemCAsInjectionToken = getInjectionToken<() => Promise<string[]>>({
|
export type RequestSystemCAs = () => Promise<string[]>;
|
||||||
|
|
||||||
|
export const platformSpecificRequestSystemCAsInjectionToken = getInjectionToken<PlatformSpecific<RequestSystemCAs>>({
|
||||||
|
id: "platform-specific-request-system-cas-token",
|
||||||
|
});
|
||||||
|
|
||||||
|
export const requestSystemCAsInjectionToken = getInjectionToken<RequestSystemCAs>({
|
||||||
id: "request-system-cas-token",
|
id: "request-system-cas-token",
|
||||||
});
|
});
|
||||||
|
|||||||
@ -0,0 +1,60 @@
|
|||||||
|
/**
|
||||||
|
* 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 execFileInjectable from "../../../common/fs/exec-file.injectable";
|
||||||
|
import loggerInjectable from "../../../common/logger.injectable";
|
||||||
|
import type { AsyncResult } from "@k8slens/utilities";
|
||||||
|
import { platformSpecificRequestSystemCAsInjectionToken } from "../common/request-system-cas-token";
|
||||||
|
|
||||||
|
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions/Cheatsheet#other_assertions
|
||||||
|
const certSplitPattern = /(?=-----BEGIN\sCERTIFICATE-----)/g;
|
||||||
|
|
||||||
|
const darwinRequestSystemCAsInjectable = getInjectable({
|
||||||
|
id: "darwin-request-system-cas",
|
||||||
|
instantiate: (di) => ({
|
||||||
|
platform: "darwin" as const,
|
||||||
|
instantiate: () => {
|
||||||
|
const execFile = di.inject(execFileInjectable);
|
||||||
|
const logger = di.inject(loggerInjectable);
|
||||||
|
|
||||||
|
const execSecurity = async (...args: string[]): AsyncResult<string[]> => {
|
||||||
|
const result = await execFile("/usr/bin/security", args);
|
||||||
|
|
||||||
|
if (!result.callWasSuccessful) {
|
||||||
|
return {
|
||||||
|
callWasSuccessful: false,
|
||||||
|
error: result.error.stderr || result.error.message,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
callWasSuccessful: true,
|
||||||
|
response: result.response.split(certSplitPattern),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
return async () => {
|
||||||
|
const [trustedResult, rootCAResult] = await Promise.all([
|
||||||
|
execSecurity("find-certificate", "-a", "-p"),
|
||||||
|
execSecurity("find-certificate", "-a", "-p", "/System/Library/Keychains/SystemRootCertificates.keychain"),
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (!trustedResult.callWasSuccessful) {
|
||||||
|
logger.warn(`[INJECT-CAS]: Error retrieving trusted CAs: ${trustedResult.error}`);
|
||||||
|
} else if (!rootCAResult.callWasSuccessful) {
|
||||||
|
logger.warn(`[INJECT-CAS]: Error retrieving root CAs: ${rootCAResult.error}`);
|
||||||
|
} else {
|
||||||
|
return [...new Set([...trustedResult.response, ...rootCAResult.response])];
|
||||||
|
}
|
||||||
|
|
||||||
|
return [];
|
||||||
|
};
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
causesSideEffects: true,
|
||||||
|
injectionToken: platformSpecificRequestSystemCAsInjectionToken,
|
||||||
|
});
|
||||||
|
|
||||||
|
export default darwinRequestSystemCAsInjectable;
|
||||||
@ -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 { platformSpecificRequestSystemCAsInjectionToken } from "../common/request-system-cas-token";
|
||||||
|
|
||||||
|
const linuxRequestSystemCAsInjectable = getInjectable({
|
||||||
|
id: "linux-request-system-cas",
|
||||||
|
instantiate: () => ({
|
||||||
|
platform: "linux" as const,
|
||||||
|
instantiate: () => async () => [],
|
||||||
|
}),
|
||||||
|
injectionToken: platformSpecificRequestSystemCAsInjectionToken,
|
||||||
|
});
|
||||||
|
|
||||||
|
export default linuxRequestSystemCAsInjectable;
|
||||||
@ -0,0 +1,9 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||||
|
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { getGlobalOverride } from "@k8slens/test-utils";
|
||||||
|
import requestSystemCAsInjectable from "./request-system-cas.injectable";
|
||||||
|
|
||||||
|
export default getGlobalOverride(requestSystemCAsInjectable, () => async () => []);
|
||||||
@ -1,57 +0,0 @@
|
|||||||
/**
|
|
||||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
|
||||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
|
||||||
*/
|
|
||||||
import { getInjectable } from "@ogre-tools/injectable";
|
|
||||||
import execFileInjectable from "../../../common/fs/exec-file.injectable";
|
|
||||||
import loggerInjectable from "../../../common/logger.injectable";
|
|
||||||
import type { AsyncResult } from "@k8slens/utilities";
|
|
||||||
import { requestSystemCAsInjectionToken } from "../common/request-system-cas-token";
|
|
||||||
|
|
||||||
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions/Cheatsheet#other_assertions
|
|
||||||
const certSplitPattern = /(?=-----BEGIN\sCERTIFICATE-----)/g;
|
|
||||||
|
|
||||||
const requestSystemCAsInjectable = getInjectable({
|
|
||||||
id: "request-system-cas",
|
|
||||||
instantiate: (di) => {
|
|
||||||
const execFile = di.inject(execFileInjectable);
|
|
||||||
const logger = di.inject(loggerInjectable);
|
|
||||||
|
|
||||||
const execSecurity = async (...args: string[]): AsyncResult<string[]> => {
|
|
||||||
const result = await execFile("/usr/bin/security", args);
|
|
||||||
|
|
||||||
if (!result.callWasSuccessful) {
|
|
||||||
return {
|
|
||||||
callWasSuccessful: false,
|
|
||||||
error: result.error.stderr || result.error.message,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
callWasSuccessful: true,
|
|
||||||
response: result.response.split(certSplitPattern),
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
return async () => {
|
|
||||||
const [trustedResult, rootCAResult] = await Promise.all([
|
|
||||||
execSecurity("find-certificate", "-a", "-p"),
|
|
||||||
execSecurity("find-certificate", "-a", "-p", "/System/Library/Keychains/SystemRootCertificates.keychain"),
|
|
||||||
]);
|
|
||||||
|
|
||||||
if (!trustedResult.callWasSuccessful) {
|
|
||||||
logger.warn(`[INJECT-CAS]: Error retreiving trusted CAs: ${trustedResult.error}`);
|
|
||||||
} else if (!rootCAResult.callWasSuccessful) {
|
|
||||||
logger.warn(`[INJECT-CAS]: Error retreiving root CAs: ${rootCAResult.error}`);
|
|
||||||
} else {
|
|
||||||
return [...new Set([...trustedResult.response, ...rootCAResult.response])];
|
|
||||||
}
|
|
||||||
|
|
||||||
return [];
|
|
||||||
};
|
|
||||||
},
|
|
||||||
causesSideEffects: true,
|
|
||||||
injectionToken: requestSystemCAsInjectionToken,
|
|
||||||
});
|
|
||||||
|
|
||||||
export default requestSystemCAsInjectable;
|
|
||||||
@ -1,14 +0,0 @@
|
|||||||
/**
|
|
||||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
|
||||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
|
||||||
*/
|
|
||||||
import { getInjectable } from "@ogre-tools/injectable";
|
|
||||||
import { requestSystemCAsInjectionToken } from "../common/request-system-cas-token";
|
|
||||||
|
|
||||||
const requestSystemCAsInjectable = getInjectable({
|
|
||||||
id: "request-system-cas",
|
|
||||||
instantiate: () => async () => [],
|
|
||||||
injectionToken: requestSystemCAsInjectionToken,
|
|
||||||
});
|
|
||||||
|
|
||||||
export default requestSystemCAsInjectable;
|
|
||||||
@ -1,14 +0,0 @@
|
|||||||
/**
|
|
||||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
|
||||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
|
||||||
*/
|
|
||||||
import { getInjectable } from "@ogre-tools/injectable";
|
|
||||||
import { requestSystemCAsInjectionToken } from "../common/request-system-cas-token";
|
|
||||||
|
|
||||||
const requestSystemCAsInjectable = getInjectable({
|
|
||||||
id: "request-system-cas",
|
|
||||||
instantiate: () => async () => [],
|
|
||||||
injectionToken: requestSystemCAsInjectionToken,
|
|
||||||
});
|
|
||||||
|
|
||||||
export default requestSystemCAsInjectable;
|
|
||||||
@ -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 platformSpecificVersionInjectable from "../../../common/utils/platform-specific-version.injectable";
|
||||||
|
import { platformSpecificRequestSystemCAsInjectionToken, requestSystemCAsInjectionToken } from "../common/request-system-cas-token";
|
||||||
|
|
||||||
|
const requestSystemCAsInjectable = getInjectable({
|
||||||
|
id: "request-system-cas",
|
||||||
|
instantiate: (di) => {
|
||||||
|
const platformSpecificVersion = di.inject(platformSpecificVersionInjectable);
|
||||||
|
const platformSpecificRequestSystemCAs = platformSpecificVersion(platformSpecificRequestSystemCAsInjectionToken);
|
||||||
|
|
||||||
|
return platformSpecificRequestSystemCAs ?? (async () => []);
|
||||||
|
},
|
||||||
|
injectionToken: requestSystemCAsInjectionToken,
|
||||||
|
});
|
||||||
|
|
||||||
|
export default requestSystemCAsInjectable;
|
||||||
@ -1,56 +0,0 @@
|
|||||||
/**
|
|
||||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
|
||||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
|
||||||
*/
|
|
||||||
import { getInjectable } from "@ogre-tools/injectable";
|
|
||||||
import execFileInjectable from "../../../common/fs/exec-file.injectable";
|
|
||||||
import loggerInjectable from "../../../common/logger.injectable";
|
|
||||||
import { requestSystemCAsInjectionToken } from "../common/request-system-cas-token";
|
|
||||||
|
|
||||||
const pemEncoding = (hexEncodedCert: String) => {
|
|
||||||
const certData = Buffer.from(hexEncodedCert, "hex").toString("base64");
|
|
||||||
const lines = ["-----BEGIN CERTIFICATE-----"];
|
|
||||||
|
|
||||||
for (let i = 0; i < certData.length; i += 64) {
|
|
||||||
lines.push(certData.substring(i, i + 64));
|
|
||||||
}
|
|
||||||
|
|
||||||
lines.push("-----END CERTIFICATE-----", "");
|
|
||||||
|
|
||||||
return lines.join("\r\n");
|
|
||||||
};
|
|
||||||
|
|
||||||
const requestSystemCAsInjectable = getInjectable({
|
|
||||||
id: "request-system-cas",
|
|
||||||
instantiate: (di) => {
|
|
||||||
const wincaRootsExePath: string = __non_webpack_require__.resolve("win-ca/lib/roots.exe");
|
|
||||||
const execFile = di.inject(execFileInjectable);
|
|
||||||
const logger = di.inject(loggerInjectable);
|
|
||||||
|
|
||||||
return async () => {
|
|
||||||
/**
|
|
||||||
* This needs to be done manually because for some reason calling the api from "win-ca"
|
|
||||||
* directly fails to load "child_process" correctly on renderer
|
|
||||||
*/
|
|
||||||
const result = await execFile(wincaRootsExePath, {
|
|
||||||
maxBuffer: 128 * 1024 * 1024, // 128 MiB
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!result.callWasSuccessful) {
|
|
||||||
logger.warn(`[INJECT-CAS]: Error retreiving CAs`, result.error);
|
|
||||||
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
return result
|
|
||||||
.response
|
|
||||||
.split("\r\n")
|
|
||||||
.filter(Boolean)
|
|
||||||
.map(pemEncoding);
|
|
||||||
};
|
|
||||||
},
|
|
||||||
causesSideEffects: true,
|
|
||||||
injectionToken: requestSystemCAsInjectionToken,
|
|
||||||
});
|
|
||||||
|
|
||||||
export default requestSystemCAsInjectable;
|
|
||||||
@ -0,0 +1,59 @@
|
|||||||
|
/**
|
||||||
|
* 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 execFileInjectable from "../../../common/fs/exec-file.injectable";
|
||||||
|
import loggerInjectable from "../../../common/logger.injectable";
|
||||||
|
import { platformSpecificRequestSystemCAsInjectionToken } from "../common/request-system-cas-token";
|
||||||
|
|
||||||
|
const pemEncoding = (hexEncodedCert: String) => {
|
||||||
|
const certData = Buffer.from(hexEncodedCert, "hex").toString("base64");
|
||||||
|
const lines = ["-----BEGIN CERTIFICATE-----"];
|
||||||
|
|
||||||
|
for (let i = 0; i < certData.length; i += 64) {
|
||||||
|
lines.push(certData.substring(i, i + 64));
|
||||||
|
}
|
||||||
|
|
||||||
|
lines.push("-----END CERTIFICATE-----", "");
|
||||||
|
|
||||||
|
return lines.join("\r\n");
|
||||||
|
};
|
||||||
|
|
||||||
|
const win32RequestSystemCAsInjectable = getInjectable({
|
||||||
|
id: "win32-request-system-cas",
|
||||||
|
instantiate: (di) => ({
|
||||||
|
platform: "win32" as const,
|
||||||
|
instantiate: () => {
|
||||||
|
const winCARootsExePath: string = __non_webpack_require__.resolve("win-ca/lib/roots.exe");
|
||||||
|
const execFile = di.inject(execFileInjectable);
|
||||||
|
const logger = di.inject(loggerInjectable);
|
||||||
|
|
||||||
|
return async () => {
|
||||||
|
/**
|
||||||
|
* This needs to be done manually because for some reason calling the api from "win-ca"
|
||||||
|
* directly fails to load "child_process" correctly on renderer
|
||||||
|
*/
|
||||||
|
const result = await execFile(winCARootsExePath, {
|
||||||
|
maxBuffer: 128 * 1024 * 1024, // 128 MiB
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!result.callWasSuccessful) {
|
||||||
|
logger.warn(`[INJECT-CAS]: Error retrieving CAs`, result.error);
|
||||||
|
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
.response
|
||||||
|
.split("\r\n")
|
||||||
|
.filter(Boolean)
|
||||||
|
.map(pemEncoding);
|
||||||
|
};
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
causesSideEffects: true,
|
||||||
|
injectionToken: platformSpecificRequestSystemCAsInjectionToken,
|
||||||
|
});
|
||||||
|
|
||||||
|
export default win32RequestSystemCAsInjectable;
|
||||||
@ -7469,6 +7469,8 @@ metadata:
|
|||||||
selfLink: /apis/some-api-version/namespaces/some-uid
|
selfLink: /apis/some-api-version/namespaces/some-uid
|
||||||
somePropertyToBeRemoved: some-value
|
somePropertyToBeRemoved: some-value
|
||||||
somePropertyToBeChanged: some-old-value
|
somePropertyToBeChanged: some-old-value
|
||||||
|
labels:
|
||||||
|
k8slens-edit-resource-version: some-api-version
|
||||||
|
|
||||||
</textarea>
|
</textarea>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -12,8 +12,6 @@ import createEditResourceTabInjectable from "../../../renderer/components/dock/e
|
|||||||
import getRandomIdForEditResourceTabInjectable from "../../../renderer/components/dock/edit-resource/get-random-id-for-edit-resource-tab.injectable";
|
import getRandomIdForEditResourceTabInjectable from "../../../renderer/components/dock/edit-resource/get-random-id-for-edit-resource-tab.injectable";
|
||||||
import type { AsyncFnMock } from "@async-fn/jest";
|
import type { AsyncFnMock } from "@async-fn/jest";
|
||||||
import asyncFn from "@async-fn/jest";
|
import asyncFn from "@async-fn/jest";
|
||||||
import type { CallForResource } from "../../../renderer/components/dock/edit-resource/edit-resource-model/call-for-resource/call-for-resource.injectable";
|
|
||||||
import callForResourceInjectable from "../../../renderer/components/dock/edit-resource/edit-resource-model/call-for-resource/call-for-resource.injectable";
|
|
||||||
import type { CallForPatchResource } from "../../../renderer/components/dock/edit-resource/edit-resource-model/call-for-patch-resource/call-for-patch-resource.injectable";
|
import type { CallForPatchResource } from "../../../renderer/components/dock/edit-resource/edit-resource-model/call-for-patch-resource/call-for-patch-resource.injectable";
|
||||||
import callForPatchResourceInjectable from "../../../renderer/components/dock/edit-resource/edit-resource-model/call-for-patch-resource/call-for-patch-resource.injectable";
|
import callForPatchResourceInjectable from "../../../renderer/components/dock/edit-resource/edit-resource-model/call-for-patch-resource/call-for-patch-resource.injectable";
|
||||||
import dockStoreInjectable from "../../../renderer/components/dock/dock/store.injectable";
|
import dockStoreInjectable from "../../../renderer/components/dock/dock/store.injectable";
|
||||||
@ -23,6 +21,8 @@ import showErrorNotificationInjectable from "../../../renderer/components/notifi
|
|||||||
import readJsonFileInjectable from "../../../common/fs/read-json-file.injectable";
|
import readJsonFileInjectable from "../../../common/fs/read-json-file.injectable";
|
||||||
import directoryForLensLocalStorageInjectable from "../../../common/directory-for-lens-local-storage/directory-for-lens-local-storage.injectable";
|
import directoryForLensLocalStorageInjectable from "../../../common/directory-for-lens-local-storage/directory-for-lens-local-storage.injectable";
|
||||||
import hostedClusterIdInjectable from "../../../renderer/cluster-frame-context/hosted-cluster-id.injectable";
|
import hostedClusterIdInjectable from "../../../renderer/cluster-frame-context/hosted-cluster-id.injectable";
|
||||||
|
import type { CallForResource } from "../../../renderer/components/dock/edit-resource/edit-resource-model/call-for-resource/call-for-resource.injectable";
|
||||||
|
import callForResourceInjectable from "../../../renderer/components/dock/edit-resource/edit-resource-model/call-for-resource/call-for-resource.injectable";
|
||||||
|
|
||||||
describe("cluster/namespaces - edit namespace from new tab", () => {
|
describe("cluster/namespaces - edit namespace from new tab", () => {
|
||||||
let builder: ApplicationBuilder;
|
let builder: ApplicationBuilder;
|
||||||
@ -225,10 +225,16 @@ metadata:
|
|||||||
expect(rendered.baseElement).toMatchSnapshot();
|
expect(rendered.baseElement).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("calls for save with empty values", () => {
|
it("calls for save with just the adding version label", () => {
|
||||||
expect(callForPatchResourceMock).toHaveBeenCalledWith(
|
expect(callForPatchResourceMock).toHaveBeenCalledWith(
|
||||||
someNamespace,
|
someNamespace,
|
||||||
[],
|
[{
|
||||||
|
op: "add",
|
||||||
|
path: "/metadata/labels",
|
||||||
|
value: {
|
||||||
|
"k8slens-edit-resource-version": "some-api-version",
|
||||||
|
},
|
||||||
|
}],
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -532,6 +538,13 @@ metadata:
|
|||||||
path: "/metadata/someAddedProperty",
|
path: "/metadata/someAddedProperty",
|
||||||
value: "some-new-value",
|
value: "some-new-value",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
op: "add",
|
||||||
|
path: "/metadata/labels",
|
||||||
|
value: {
|
||||||
|
"k8slens-edit-resource-version": "some-api-version",
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
op: "replace",
|
op: "replace",
|
||||||
path: "/metadata/somePropertyToBeChanged",
|
path: "/metadata/somePropertyToBeChanged",
|
||||||
@ -759,7 +772,7 @@ metadata:
|
|||||||
`);
|
`);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("when selecting to save, calls for save of second namespace", () => {
|
it("when selecting to save, calls for save of second namespace with just the add edit version label", () => {
|
||||||
callForPatchResourceMock.mockClear();
|
callForPatchResourceMock.mockClear();
|
||||||
|
|
||||||
const saveButton = rendered.getByTestId(
|
const saveButton = rendered.getByTestId(
|
||||||
@ -770,7 +783,13 @@ metadata:
|
|||||||
|
|
||||||
expect(callForPatchResourceMock).toHaveBeenCalledWith(
|
expect(callForPatchResourceMock).toHaveBeenCalledWith(
|
||||||
someOtherNamespace,
|
someOtherNamespace,
|
||||||
[],
|
[{
|
||||||
|
op: "add",
|
||||||
|
path: "/metadata/labels",
|
||||||
|
value: {
|
||||||
|
"k8slens-edit-resource-version": "some-api-version",
|
||||||
|
},
|
||||||
|
}],
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -826,7 +845,7 @@ metadata:
|
|||||||
`);
|
`);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("when selecting to save, calls for save of first namespace", () => {
|
it("when selecting to save, calls for save of first namespace with just the new edit version label", () => {
|
||||||
callForPatchResourceMock.mockClear();
|
callForPatchResourceMock.mockClear();
|
||||||
|
|
||||||
const saveButton = rendered.getByTestId(
|
const saveButton = rendered.getByTestId(
|
||||||
@ -837,7 +856,13 @@ metadata:
|
|||||||
|
|
||||||
expect(callForPatchResourceMock).toHaveBeenCalledWith(
|
expect(callForPatchResourceMock).toHaveBeenCalledWith(
|
||||||
someNamespace,
|
someNamespace,
|
||||||
[],
|
[ {
|
||||||
|
op: "add",
|
||||||
|
path: "/metadata/labels",
|
||||||
|
value: {
|
||||||
|
"k8slens-edit-resource-version": "some-api-version",
|
||||||
|
},
|
||||||
|
}],
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -376,7 +376,7 @@ exports[`extensions - navigation using application menu when navigating to exten
|
|||||||
<p>
|
<p>
|
||||||
Add new features via Lens Extensions. Check out the
|
Add new features via Lens Extensions. Check out the
|
||||||
<a
|
<a
|
||||||
href="https://docs.k8slens.dev/extensions/"
|
href="https://docs.k8slens.dev/extensions/lens-extensions"
|
||||||
rel="noreferrer"
|
rel="noreferrer"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
>
|
>
|
||||||
|
|||||||
@ -5,19 +5,14 @@
|
|||||||
import { getInjectable } from "@ogre-tools/injectable";
|
import { getInjectable } from "@ogre-tools/injectable";
|
||||||
import enabledExtensionsStateInjectable from "./state.injectable";
|
import enabledExtensionsStateInjectable from "./state.injectable";
|
||||||
|
|
||||||
export interface IsEnabledExtensionDescriptor {
|
export type IsExtensionEnabled = (id: string) => boolean;
|
||||||
readonly id: string;
|
|
||||||
readonly isBundled: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type IsExtensionEnabled = (desc: IsEnabledExtensionDescriptor) => boolean;
|
|
||||||
|
|
||||||
const isExtensionEnabledInjectable = getInjectable({
|
const isExtensionEnabledInjectable = getInjectable({
|
||||||
id: "is-extension-enabled",
|
id: "is-extension-enabled",
|
||||||
instantiate: (di): IsExtensionEnabled => {
|
instantiate: (di): IsExtensionEnabled => {
|
||||||
const state = di.inject(enabledExtensionsStateInjectable);
|
const state = di.inject(enabledExtensionsStateInjectable);
|
||||||
|
|
||||||
return ({ id, isBundled }) => isBundled || (state.get(id)?.enabled ?? false);
|
return (id) => (state.get(id)?.enabled ?? false);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -58,7 +58,7 @@ jest.mock("./renderer/components/tooltip/withTooltip");
|
|||||||
jest.mock("monaco-editor");
|
jest.mock("monaco-editor");
|
||||||
|
|
||||||
const getInjectables = (environment: "renderer" | "main", filePathGlob: string) => [
|
const getInjectables = (environment: "renderer" | "main", filePathGlob: string) => [
|
||||||
...glob.sync(`./{common,extensions,${environment}}/**/${filePathGlob}`, {
|
...glob.sync(`./{common,extensions,${environment},test-env}/**/${filePathGlob}`, {
|
||||||
cwd: __dirname,
|
cwd: __dirname,
|
||||||
}),
|
}),
|
||||||
|
|
||||||
@ -70,10 +70,10 @@ const getInjectables = (environment: "renderer" | "main", filePathGlob: string)
|
|||||||
global.injectablePaths = {
|
global.injectablePaths = {
|
||||||
renderer: {
|
renderer: {
|
||||||
globalOverridePaths: getInjectables("renderer", "*.global-override-for-injectable.{ts,tsx}"),
|
globalOverridePaths: getInjectables("renderer", "*.global-override-for-injectable.{ts,tsx}"),
|
||||||
paths: getInjectables("renderer", "*.{injectable,injectable.testing-env}.{ts,tsx}"),
|
paths: getInjectables("renderer", "*.injectable.{ts,tsx}"),
|
||||||
},
|
},
|
||||||
main: {
|
main: {
|
||||||
globalOverridePaths: getInjectables("main", "*.global-override-for-injectable.{ts,tsx}"),
|
globalOverridePaths: getInjectables("main", "*.global-override-for-injectable.{ts,tsx}"),
|
||||||
paths: getInjectables("main", "*.{injectable,injectable.testing-env}.{ts,tsx}"),
|
paths: getInjectables("main", "*.injectable.{ts,tsx}"),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@ -98,11 +98,19 @@ export class KubeconfigSyncManager {
|
|||||||
|
|
||||||
@action
|
@action
|
||||||
protected stopOldSync(filePath: string): void {
|
protected stopOldSync(filePath: string): void {
|
||||||
if (!this.sources.delete(filePath)) {
|
const source = this.sources.get(filePath);
|
||||||
// already stopped
|
|
||||||
|
// already stopped
|
||||||
|
if (!source) {
|
||||||
return this.dependencies.logger.debug(`no syncing file/folder to stop`, { filePath });
|
return this.dependencies.logger.debug(`no syncing file/folder to stop`, { filePath });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const [, disposer] = source;
|
||||||
|
|
||||||
|
disposer();
|
||||||
|
|
||||||
|
this.sources.delete(filePath);
|
||||||
|
|
||||||
this.dependencies.logger.info(`stopping sync of file/folder`, { filePath });
|
this.dependencies.logger.info(`stopping sync of file/folder`, { filePath });
|
||||||
this.dependencies.logger.debug(`${this.sources.size} files/folders watched`, { files: Array.from(this.sources.keys()) });
|
this.dependencies.logger.debug(`${this.sources.size} files/folders watched`, { files: Array.from(this.sources.keys()) });
|
||||||
}
|
}
|
||||||
|
|||||||
@ -24,7 +24,7 @@ const createExtensionInstanceInjectable = getInjectable({
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (ExtensionClass, extension) => {
|
return (ExtensionClass, extension) => {
|
||||||
const instance = new ExtensionClass(extension) as LensMainExtension;
|
const instance = new ExtensionClass(extension as any) as LensMainExtension;
|
||||||
|
|
||||||
(instance as Writable<LensMainExtension>)[lensExtensionDependencies] = deps;
|
(instance as Writable<LensMainExtension>)[lensExtensionDependencies] = deps;
|
||||||
|
|
||||||
|
|||||||
@ -10,16 +10,12 @@ import { noop } from "@k8slens/utilities";
|
|||||||
import type { LensProtocolRouterMain } from "../lens-protocol-router-main/lens-protocol-router-main";
|
import type { LensProtocolRouterMain } from "../lens-protocol-router-main/lens-protocol-router-main";
|
||||||
import { getDiForUnitTesting } from "../../getDiForUnitTesting";
|
import { getDiForUnitTesting } from "../../getDiForUnitTesting";
|
||||||
import lensProtocolRouterMainInjectable from "../lens-protocol-router-main/lens-protocol-router-main.injectable";
|
import lensProtocolRouterMainInjectable from "../lens-protocol-router-main/lens-protocol-router-main.injectable";
|
||||||
import getConfigurationFileModelInjectable from "../../../common/get-configuration-file-model/get-configuration-file-model.injectable";
|
|
||||||
import { LensExtension } from "../../../extensions/lens-extension";
|
import { LensExtension } from "../../../extensions/lens-extension";
|
||||||
import type { ObservableMap } from "mobx";
|
import type { ObservableMap } from "mobx";
|
||||||
|
import { runInAction } from "mobx";
|
||||||
import extensionInstancesInjectable from "../../../extensions/extension-loader/extension-instances.injectable";
|
import extensionInstancesInjectable from "../../../extensions/extension-loader/extension-instances.injectable";
|
||||||
import directoryForUserDataInjectable from "../../../common/app-paths/directory-for-user-data/directory-for-user-data.injectable";
|
import directoryForUserDataInjectable from "../../../common/app-paths/directory-for-user-data/directory-for-user-data.injectable";
|
||||||
import broadcastMessageInjectable from "../../../common/ipc/broadcast-message.injectable";
|
import broadcastMessageInjectable from "../../../common/ipc/broadcast-message.injectable";
|
||||||
import pathExistsSyncInjectable from "../../../common/fs/path-exists-sync.injectable";
|
|
||||||
import pathExistsInjectable from "../../../common/fs/path-exists.injectable";
|
|
||||||
import readJsonSyncInjectable from "../../../common/fs/read-json-sync.injectable";
|
|
||||||
import writeJsonSyncInjectable from "../../../common/fs/write-json-sync.injectable";
|
|
||||||
import type { LensExtensionId } from "@k8slens/legacy-extensions";
|
import type { LensExtensionId } from "@k8slens/legacy-extensions";
|
||||||
import type { LensExtensionState } from "../../../features/extensions/enabled/common/state.injectable";
|
import type { LensExtensionState } from "../../../features/extensions/enabled/common/state.injectable";
|
||||||
import enabledExtensionsStateInjectable from "../../../features/extensions/enabled/common/state.injectable";
|
import enabledExtensionsStateInjectable from "../../../features/extensions/enabled/common/state.injectable";
|
||||||
@ -39,16 +35,8 @@ describe("protocol router tests", () => {
|
|||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
const di = getDiForUnitTesting();
|
const di = getDiForUnitTesting();
|
||||||
|
|
||||||
di.override(pathExistsInjectable, () => () => { throw new Error("tried call pathExists without override"); });
|
|
||||||
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"); });
|
|
||||||
|
|
||||||
enabledExtensions = di.inject(enabledExtensionsStateInjectable);
|
enabledExtensions = di.inject(enabledExtensionsStateInjectable);
|
||||||
|
di.override(directoryForUserDataInjectable, () => "/some-directory-for-user-data");
|
||||||
di.permitSideEffects(getConfigurationFileModelInjectable);
|
|
||||||
|
|
||||||
di.override(directoryForUserDataInjectable, () => "some-directory-for-user-data");
|
|
||||||
|
|
||||||
broadcastMessageMock = jest.fn();
|
broadcastMessageMock = jest.fn();
|
||||||
di.override(broadcastMessageInjectable, () => broadcastMessageMock);
|
di.override(broadcastMessageInjectable, () => broadcastMessageMock);
|
||||||
@ -56,7 +44,9 @@ describe("protocol router tests", () => {
|
|||||||
extensionInstances = di.inject(extensionInstancesInjectable);
|
extensionInstances = di.inject(extensionInstancesInjectable);
|
||||||
lpr = di.inject(lensProtocolRouterMainInjectable);
|
lpr = di.inject(lensProtocolRouterMainInjectable);
|
||||||
|
|
||||||
lpr.rendererLoaded = true;
|
runInAction(() => {
|
||||||
|
lpr.rendererLoaded.set(true);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should broadcast invalid protocol on non-lens URLs", async () => {
|
it("should broadcast invalid protocol on non-lens URLs", async () => {
|
||||||
@ -69,7 +59,19 @@ describe("protocol router tests", () => {
|
|||||||
expect(broadcastMessageMock).toBeCalledWith(ProtocolHandlerInvalid, "invalid host", "lens://foobar");
|
expect(broadcastMessageMock).toBeCalledWith(ProtocolHandlerInvalid, "invalid host", "lens://foobar");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should not throw when has valid host", async () => {
|
it("should broadcast internal route when called with valid host", async () => {
|
||||||
|
lpr.addInternalHandler("/", noop);
|
||||||
|
|
||||||
|
try {
|
||||||
|
expect(await lpr.route("lens://app")).toBeUndefined();
|
||||||
|
} catch (error) {
|
||||||
|
expect(throwIfDefined(error)).not.toThrow();
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(broadcastMessageMock).toHaveBeenCalledWith(ProtocolHandlerInternal, "lens://app", "matched");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should broadcast external route when called with valid host", async () => {
|
||||||
const extId = uuid.v4();
|
const extId = uuid.v4();
|
||||||
const ext = new LensExtension({
|
const ext = new LensExtension({
|
||||||
id: extId,
|
id: extId,
|
||||||
|
|||||||
@ -6,7 +6,7 @@
|
|||||||
import * as proto from "../../../common/protocol-handler";
|
import * as proto from "../../../common/protocol-handler";
|
||||||
import URLParse from "url-parse";
|
import URLParse from "url-parse";
|
||||||
import type { LensExtension } from "../../../extensions/lens-extension";
|
import type { LensExtension } from "../../../extensions/lens-extension";
|
||||||
import { observable, when, makeObservable } from "mobx";
|
import { observable, when } from "mobx";
|
||||||
import type { LensProtocolRouterDependencies, RouteAttempt } from "../../../common/protocol-handler";
|
import type { LensProtocolRouterDependencies, RouteAttempt } from "../../../common/protocol-handler";
|
||||||
import { ProtocolHandlerInvalid } from "../../../common/protocol-handler";
|
import { ProtocolHandlerInvalid } from "../../../common/protocol-handler";
|
||||||
import { disposer, noop } from "@k8slens/utilities";
|
import { disposer, noop } from "@k8slens/utilities";
|
||||||
@ -39,17 +39,15 @@ export interface LensProtocolRouterMainDependencies extends LensProtocolRouterDe
|
|||||||
}
|
}
|
||||||
|
|
||||||
export class LensProtocolRouterMain extends proto.LensProtocolRouter {
|
export class LensProtocolRouterMain extends proto.LensProtocolRouter {
|
||||||
private missingExtensionHandlers: FallbackHandler[] = [];
|
private readonly missingExtensionHandlers: FallbackHandler[] = [];
|
||||||
|
|
||||||
// TODO: This is used to solve out-of-place temporal dependency. Remove, and solve otherwise.
|
// TODO: This is used to solve out-of-place temporal dependency. Remove, and solve otherwise.
|
||||||
@observable rendererLoaded = false;
|
readonly rendererLoaded = observable.box(false);
|
||||||
|
|
||||||
protected disposers = disposer();
|
protected readonly disposers = disposer();
|
||||||
|
|
||||||
constructor(protected readonly dependencies: LensProtocolRouterMainDependencies) {
|
constructor(protected readonly dependencies: LensProtocolRouterMainDependencies) {
|
||||||
super(dependencies);
|
super(dependencies);
|
||||||
|
|
||||||
makeObservable(this);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public cleanup() {
|
public cleanup() {
|
||||||
@ -118,13 +116,12 @@ export class LensProtocolRouterMain extends proto.LensProtocolRouter {
|
|||||||
protected _routeToInternal(url: URLParse<Record<string, string | undefined>>): RouteAttempt {
|
protected _routeToInternal(url: URLParse<Record<string, string | undefined>>): RouteAttempt {
|
||||||
const rawUrl = url.toString(); // for sending to renderer
|
const rawUrl = url.toString(); // for sending to renderer
|
||||||
const attempt = super._routeToInternal(url);
|
const attempt = super._routeToInternal(url);
|
||||||
|
const broadcastToRenderer = () => this.dependencies.broadcastMessage(proto.ProtocolHandlerInternal, rawUrl, attempt);
|
||||||
|
|
||||||
const sendRoutingToRenderer = () => this.dependencies.broadcastMessage(proto.ProtocolHandlerInternal, rawUrl, attempt);
|
if (this.rendererLoaded.get()) {
|
||||||
|
broadcastToRenderer();
|
||||||
if (this.rendererLoaded) {
|
|
||||||
sendRoutingToRenderer();
|
|
||||||
} else {
|
} else {
|
||||||
this.disposers.push(when(() => this.rendererLoaded, sendRoutingToRenderer));
|
this.disposers.push(when(() => this.rendererLoaded.get(), broadcastToRenderer));
|
||||||
}
|
}
|
||||||
|
|
||||||
return attempt;
|
return attempt;
|
||||||
@ -141,13 +138,12 @@ export class LensProtocolRouterMain extends proto.LensProtocolRouter {
|
|||||||
* argument.
|
* argument.
|
||||||
*/
|
*/
|
||||||
const attempt = await super._routeToExtension(new URLParse(url.toString(), true));
|
const attempt = await super._routeToExtension(new URLParse(url.toString(), true));
|
||||||
|
const broadcastToRenderer = () => this.dependencies.broadcastMessage(proto.ProtocolHandlerExtension, rawUrl, attempt);
|
||||||
|
|
||||||
const sendRoutingToRenderer = () => this.dependencies.broadcastMessage(proto.ProtocolHandlerExtension, rawUrl, attempt);
|
if (this.rendererLoaded.get()) {
|
||||||
|
broadcastToRenderer();
|
||||||
if (this.rendererLoaded) {
|
|
||||||
sendRoutingToRenderer();
|
|
||||||
} else {
|
} else {
|
||||||
this.disposers.push(when(() => this.rendererLoaded, sendRoutingToRenderer));
|
this.disposers.push(when(() => this.rendererLoaded.get(), broadcastToRenderer));
|
||||||
}
|
}
|
||||||
|
|
||||||
return attempt;
|
return attempt;
|
||||||
|
|||||||
@ -16,7 +16,7 @@ const flagRendererAsLoadedInjectable = getInjectable({
|
|||||||
|
|
||||||
runInAction(() => {
|
runInAction(() => {
|
||||||
// Todo: remove this kludge which enables out-of-place temporal dependency.
|
// Todo: remove this kludge which enables out-of-place temporal dependency.
|
||||||
lensProtocolRouterMain.rendererLoaded = true;
|
lensProtocolRouterMain.rendererLoaded.set(true);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
|
|||||||
@ -16,7 +16,7 @@ const flagRendererAsNotLoadedInjectable = getInjectable({
|
|||||||
|
|
||||||
runInAction(() => {
|
runInAction(() => {
|
||||||
// Todo: remove this kludge which enables out-of-place temporal dependency.
|
// Todo: remove this kludge which enables out-of-place temporal dependency.
|
||||||
lensProtocolRouterMain.rendererLoaded = false;
|
lensProtocolRouterMain.rendererLoaded.set(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
return undefined;
|
return undefined;
|
||||||
|
|||||||
@ -16,6 +16,7 @@
|
|||||||
.TableCell {
|
.TableCell {
|
||||||
word-break: break-word;
|
word-break: break-word;
|
||||||
|
|
||||||
|
|
||||||
&:first-child {
|
&:first-child {
|
||||||
margin-left: $margin * 2;
|
margin-left: $margin * 2;
|
||||||
}
|
}
|
||||||
@ -13,7 +13,7 @@ import { getDiForUnitTesting } from "../../getDiForUnitTesting";
|
|||||||
import storesAndApisCanBeCreatedInjectable from "../../stores-apis-can-be-created.injectable";
|
import storesAndApisCanBeCreatedInjectable from "../../stores-apis-can-be-created.injectable";
|
||||||
import type { DiRender } from "../test-utils/renderFor";
|
import type { DiRender } from "../test-utils/renderFor";
|
||||||
import { renderFor } from "../test-utils/renderFor";
|
import { renderFor } from "../test-utils/renderFor";
|
||||||
import { HpaDetails } from "./hpa-details";
|
import { HorizontalPodAutoscalerDetails } from "./details";
|
||||||
|
|
||||||
jest.mock("react-router-dom", () => ({
|
jest.mock("react-router-dom", () => ({
|
||||||
Link: ({ children }: { children: React.ReactNode }) => children,
|
Link: ({ children }: { children: React.ReactNode }) => children,
|
||||||
@ -62,7 +62,7 @@ describe("<HpaDetails/>", () => {
|
|||||||
const hpa = new HorizontalPodAutoscaler(hpaV2);
|
const hpa = new HorizontalPodAutoscaler(hpaV2);
|
||||||
|
|
||||||
result = render(
|
result = render(
|
||||||
<HpaDetails object={hpa} />,
|
<HorizontalPodAutoscalerDetails object={hpa} />,
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(result.baseElement).toMatchSnapshot();
|
expect(result.baseElement).toMatchSnapshot();
|
||||||
@ -72,7 +72,7 @@ describe("<HpaDetails/>", () => {
|
|||||||
const hpa = new HorizontalPodAutoscaler(hpaV2);
|
const hpa = new HorizontalPodAutoscaler(hpaV2);
|
||||||
|
|
||||||
result = render(
|
result = render(
|
||||||
<HpaDetails object={hpa} />,
|
<HorizontalPodAutoscalerDetails object={hpa} />,
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(result.queryByTestId("hpa-metrics")).toBeNull();
|
expect(result.queryByTestId("hpa-metrics")).toBeNull();
|
||||||
@ -101,7 +101,7 @@ describe("<HpaDetails/>", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
result = render(
|
result = render(
|
||||||
<HpaDetails object={hpa} />,
|
<HorizontalPodAutoscalerDetails object={hpa} />,
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(result.getByText("CPU Utilization percentage")).toBeInTheDocument();
|
expect(result.getByText("CPU Utilization percentage")).toBeInTheDocument();
|
||||||
@ -131,7 +131,7 @@ describe("<HpaDetails/>", () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
result = render(
|
result = render(
|
||||||
<HpaDetails object={hpa} />,
|
<HorizontalPodAutoscalerDetails object={hpa} />,
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(result.getByText("Resource cpu on Pods")).toBeInTheDocument();
|
expect(result.getByText("Resource cpu on Pods")).toBeInTheDocument();
|
||||||
@ -160,7 +160,7 @@ describe("<HpaDetails/>", () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
result = render(
|
result = render(
|
||||||
<HpaDetails object={hpa} />,
|
<HorizontalPodAutoscalerDetails object={hpa} />,
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(result.getByText("Resource cpu on Pods")).toBeInTheDocument();
|
expect(result.getByText("Resource cpu on Pods")).toBeInTheDocument();
|
||||||
@ -191,7 +191,7 @@ describe("<HpaDetails/>", () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
result = render(
|
result = render(
|
||||||
<HpaDetails object={hpa} />,
|
<HorizontalPodAutoscalerDetails object={hpa} />,
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(result.getByText("packets-per-second on Pods")).toBeInTheDocument();
|
expect(result.getByText("packets-per-second on Pods")).toBeInTheDocument();
|
||||||
@ -216,7 +216,7 @@ describe("<HpaDetails/>", () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
result = render(
|
result = render(
|
||||||
<HpaDetails object={hpa} />,
|
<HorizontalPodAutoscalerDetails object={hpa} />,
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(result.getByText("packets-per-second on Pods")).toBeInTheDocument();
|
expect(result.getByText("packets-per-second on Pods")).toBeInTheDocument();
|
||||||
@ -252,7 +252,7 @@ describe("<HpaDetails/>", () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
result = render(
|
result = render(
|
||||||
<HpaDetails object={hpa} />,
|
<HorizontalPodAutoscalerDetails object={hpa} />,
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(result.getByText(/requests-per-second/)).toHaveTextContent("requests-per-second onService/nginx");
|
expect(result.getByText(/requests-per-second/)).toHaveTextContent("requests-per-second onService/nginx");
|
||||||
@ -277,7 +277,7 @@ describe("<HpaDetails/>", () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
result = render(
|
result = render(
|
||||||
<HpaDetails object={hpa} />,
|
<HorizontalPodAutoscalerDetails object={hpa} />,
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(result.getByText("requests-per-second")).toBeInTheDocument();
|
expect(result.getByText("requests-per-second")).toBeInTheDocument();
|
||||||
@ -311,7 +311,7 @@ describe("<HpaDetails/>", () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
result = render(
|
result = render(
|
||||||
<HpaDetails object={hpa} />,
|
<HorizontalPodAutoscalerDetails object={hpa} />,
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(result.getByText("queue_messages_ready on {\"matchLabels\":{\"queue\":\"worker_tasks\"}}")).toBeInTheDocument();
|
expect(result.getByText("queue_messages_ready on {\"matchLabels\":{\"queue\":\"worker_tasks\"}}")).toBeInTheDocument();
|
||||||
@ -339,7 +339,7 @@ describe("<HpaDetails/>", () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
result = render(
|
result = render(
|
||||||
<HpaDetails object={hpa} />,
|
<HorizontalPodAutoscalerDetails object={hpa} />,
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(result.getByText("queue_messages_ready on {\"matchLabels\":{\"queue\":\"worker_tasks\"}}")).toBeInTheDocument();
|
expect(result.getByText("queue_messages_ready on {\"matchLabels\":{\"queue\":\"worker_tasks\"}}")).toBeInTheDocument();
|
||||||
@ -368,7 +368,7 @@ describe("<HpaDetails/>", () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
result = render(
|
result = render(
|
||||||
<HpaDetails object={hpa} />,
|
<HorizontalPodAutoscalerDetails object={hpa} />,
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(result.baseElement).toMatchSnapshot();
|
expect(result.baseElement).toMatchSnapshot();
|
||||||
@ -398,7 +398,7 @@ describe("<HpaDetails/>", () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
result = render(
|
result = render(
|
||||||
<HpaDetails object={hpa} />,
|
<HorizontalPodAutoscalerDetails object={hpa} />,
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(result.baseElement).toMatchSnapshot();
|
expect(result.baseElement).toMatchSnapshot();
|
||||||
@ -3,7 +3,7 @@
|
|||||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import "./hpa-details.scss";
|
import "./details.scss";
|
||||||
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
@ -22,8 +22,8 @@ import { withInjectables } from "@ogre-tools/injectable-react";
|
|||||||
import apiManagerInjectable from "../../../common/k8s-api/api-manager/manager.injectable";
|
import apiManagerInjectable from "../../../common/k8s-api/api-manager/manager.injectable";
|
||||||
import getDetailsUrlInjectable from "../kube-detail-params/get-details-url.injectable";
|
import getDetailsUrlInjectable from "../kube-detail-params/get-details-url.injectable";
|
||||||
import loggerInjectable from "../../../common/logger.injectable";
|
import loggerInjectable from "../../../common/logger.injectable";
|
||||||
import getHorizontalPodAutoscalerMetrics from "./get-hpa-metrics.injectable";
|
import getHorizontalPodAutoscalerMetrics from "./get-metrics.injectable";
|
||||||
import { getMetricName } from "./get-hpa-metric-name";
|
import { getMetricName } from "./get-metric-name";
|
||||||
|
|
||||||
export interface HpaDetailsProps extends KubeObjectDetailsProps<HorizontalPodAutoscaler> {
|
export interface HpaDetailsProps extends KubeObjectDetailsProps<HorizontalPodAutoscaler> {
|
||||||
}
|
}
|
||||||
@ -36,7 +36,7 @@ interface Dependencies {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@observer
|
@observer
|
||||||
class NonInjectedHpaDetails extends React.Component<HpaDetailsProps & Dependencies> {
|
class NonInjectedHorizontalPodAutoscalerDetails extends React.Component<HpaDetailsProps & Dependencies> {
|
||||||
private renderTargetLink(target: HorizontalPodAutoscalerMetricTarget | undefined) {
|
private renderTargetLink(target: HorizontalPodAutoscalerMetricTarget | undefined) {
|
||||||
if (!target) {
|
if (!target) {
|
||||||
return null;
|
return null;
|
||||||
@ -177,7 +177,7 @@ class NonInjectedHpaDetails extends React.Component<HpaDetailsProps & Dependenci
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const HpaDetails = withInjectables<Dependencies, HpaDetailsProps>(NonInjectedHpaDetails, {
|
export const HorizontalPodAutoscalerDetails = withInjectables<Dependencies, HpaDetailsProps>(NonInjectedHorizontalPodAutoscalerDetails, {
|
||||||
getProps: (di, props) => ({
|
getProps: (di, props) => ({
|
||||||
...props,
|
...props,
|
||||||
apiManager: di.inject(apiManagerInjectable),
|
apiManager: di.inject(apiManagerInjectable),
|
||||||
@ -5,9 +5,9 @@
|
|||||||
import { getInjectable } from "@ogre-tools/injectable";
|
import { getInjectable } from "@ogre-tools/injectable";
|
||||||
import type { HorizontalPodAutoscaler, HorizontalPodAutoscalerMetricSpec, HorizontalPodAutoscalerMetricStatus } from "../../../common/k8s-api/endpoints";
|
import type { HorizontalPodAutoscaler, HorizontalPodAutoscalerMetricSpec, HorizontalPodAutoscalerMetricStatus } from "../../../common/k8s-api/endpoints";
|
||||||
import { HpaMetricType } from "../../../common/k8s-api/endpoints";
|
import { HpaMetricType } from "../../../common/k8s-api/endpoints";
|
||||||
import { getMetricName } from "./get-hpa-metric-name";
|
import { getMetricName } from "./get-metric-name";
|
||||||
import { HorizontalPodAutoscalerV1MetricParser } from "./hpa-v1-metric-parser";
|
import { HorizontalPodAutoscalerV1MetricParser } from "./metric-parser-v1";
|
||||||
import { HorizontalPodAutoscalerV2MetricParser } from "./hpa-v2-metric-parser";
|
import { HorizontalPodAutoscalerV2MetricParser } from "./metric-parser-v2";
|
||||||
|
|
||||||
type Parser = HorizontalPodAutoscalerV1MetricParser | HorizontalPodAutoscalerV2MetricParser;
|
type Parser = HorizontalPodAutoscalerV1MetricParser | HorizontalPodAutoscalerV2MetricParser;
|
||||||
|
|
||||||
@ -3,5 +3,5 @@
|
|||||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export * from "./hpa";
|
export * from "./list-view";
|
||||||
export * from "./hpa-details";
|
export * from "./details";
|
||||||
|
|||||||
@ -3,7 +3,7 @@
|
|||||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import "./hpa.scss";
|
import "./list-view.scss";
|
||||||
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
@ -17,7 +17,7 @@ import { KubeObjectAge } from "../kube-object/age";
|
|||||||
import type { HorizontalPodAutoscalerStore } from "./store";
|
import type { HorizontalPodAutoscalerStore } from "./store";
|
||||||
import { withInjectables } from "@ogre-tools/injectable-react";
|
import { withInjectables } from "@ogre-tools/injectable-react";
|
||||||
import horizontalPodAutoscalerStoreInjectable from "./store.injectable";
|
import horizontalPodAutoscalerStoreInjectable from "./store.injectable";
|
||||||
import getHorizontalPodAutoscalerMetrics from "./get-hpa-metrics.injectable";
|
import getHorizontalPodAutoscalerMetrics from "./get-metrics.injectable";
|
||||||
import { NamespaceSelectBadge } from "../+namespaces/namespace-select-badge";
|
import { NamespaceSelectBadge } from "../+namespaces/namespace-select-badge";
|
||||||
|
|
||||||
enum columnId {
|
enum columnId {
|
||||||
@ -3,7 +3,7 @@
|
|||||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||||
*/
|
*/
|
||||||
import type { DiContainer } from "@ogre-tools/injectable";
|
import type { DiContainer } from "@ogre-tools/injectable";
|
||||||
import getHorizontalPodAutoscalerMetrics from "./get-hpa-metrics.injectable";
|
import getHorizontalPodAutoscalerMetrics from "./get-metrics.injectable";
|
||||||
import { getDiForUnitTesting } from "../../getDiForUnitTesting";
|
import { getDiForUnitTesting } from "../../getDiForUnitTesting";
|
||||||
import { HorizontalPodAutoscaler, HpaMetricType } from "../../../common/k8s-api/endpoints";
|
import { HorizontalPodAutoscaler, HpaMetricType } from "../../../common/k8s-api/endpoints";
|
||||||
|
|
||||||
@ -658,10 +658,10 @@ describe("getHorizontalPodAutoscalerMetrics", () => {
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(getMetrics(hpa)[0]).toEqual("10% / 50%");
|
expect(getMetrics(hpa)[0]).toEqual("10% / 50%");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should return correct resource metrics with current value", () => {
|
it("should return correct resource metrics with current value", () => {
|
||||||
const hpa = new HorizontalPodAutoscaler({
|
const hpa = new HorizontalPodAutoscaler({
|
||||||
...hpaV2Beta1,
|
...hpaV2Beta1,
|
||||||
@ -691,7 +691,7 @@ describe("getHorizontalPodAutoscalerMetrics", () => {
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(getMetrics(hpa)[0]).toEqual("500m / 100m");
|
expect(getMetrics(hpa)[0]).toEqual("500m / 100m");
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -787,7 +787,7 @@ describe("getHorizontalPodAutoscalerMetrics", () => {
|
|||||||
type: HpaMetricType.Pods,
|
type: HpaMetricType.Pods,
|
||||||
pods: {
|
pods: {
|
||||||
metricName: "packets-per-second",
|
metricName: "packets-per-second",
|
||||||
|
|
||||||
targetAverageValue: "1k",
|
targetAverageValue: "1k",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -1038,7 +1038,7 @@ describe("getHorizontalPodAutoscalerMetrics", () => {
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(getMetrics(hpa)[0]).toEqual("unknown / 50%");
|
expect(getMetrics(hpa)[0]).toEqual("unknown / 50%");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@ -5,7 +5,7 @@
|
|||||||
import { getInjectable } from "@ogre-tools/injectable";
|
import { getInjectable } from "@ogre-tools/injectable";
|
||||||
import { routeSpecificComponentInjectionToken } from "../../routes/route-specific-component-injection-token";
|
import { routeSpecificComponentInjectionToken } from "../../routes/route-specific-component-injection-token";
|
||||||
import horizontalPodAutoscalersRouteInjectable from "../../../common/front-end-routing/routes/cluster/config/horizontal-pod-autoscalers/horizontal-pod-autoscalers-route.injectable";
|
import horizontalPodAutoscalersRouteInjectable from "../../../common/front-end-routing/routes/cluster/config/horizontal-pod-autoscalers/horizontal-pod-autoscalers-route.injectable";
|
||||||
import { HorizontalPodAutoscalers } from "./hpa";
|
import { HorizontalPodAutoscalers } from "./list-view";
|
||||||
|
|
||||||
const horizontalPodAutoscalersRouteComponentInjectable = getInjectable({
|
const horizontalPodAutoscalersRouteComponentInjectable = getInjectable({
|
||||||
id: "horizontal-pod-autoscalers-route-component",
|
id: "horizontal-pod-autoscalers-route-component",
|
||||||
@ -86,7 +86,7 @@ const attemptInstall = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const extensionFolder = getExtensionDestFolder(name);
|
const extensionFolder = getExtensionDestFolder(name);
|
||||||
const installedExtension = extensionLoader.getExtension(validatedRequest.id);
|
const installedExtension = extensionLoader.getExtensionById(validatedRequest.id);
|
||||||
|
|
||||||
if (installedExtension) {
|
if (installedExtension) {
|
||||||
const { version: oldVersion } = installedExtension.manifest;
|
const { version: oldVersion } = installedExtension.manifest;
|
||||||
|
|||||||
@ -73,7 +73,7 @@ const unpackExtensionInjectable = getInjectable({
|
|||||||
await fse.move(unpackedRootFolder, extensionFolder, { overwrite: true });
|
await fse.move(unpackedRootFolder, extensionFolder, { overwrite: true });
|
||||||
|
|
||||||
// wait for the loader has actually install it
|
// wait for the loader has actually install it
|
||||||
await when(() => extensionLoader.userExtensions.has(id));
|
await when(() => extensionLoader.userExtensions.get().has(id));
|
||||||
|
|
||||||
// Enable installed extensions by default.
|
// Enable installed extensions by default.
|
||||||
extensionLoader.setIsEnabled(id, true);
|
extensionLoader.setIsEnabled(id, true);
|
||||||
|
|||||||
@ -0,0 +1,27 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||||
|
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||||
|
*/
|
||||||
|
import type { LensExtensionId } from "@k8slens/legacy-extensions";
|
||||||
|
import { getInjectable } from "@ogre-tools/injectable";
|
||||||
|
import extensionLoaderInjectable from "../../../extensions/extension-loader/extension-loader.injectable";
|
||||||
|
|
||||||
|
export type DisableExtension = (extId: LensExtensionId) => void;
|
||||||
|
|
||||||
|
const disableExtensionInjectable = getInjectable({
|
||||||
|
id: "disable-extension",
|
||||||
|
|
||||||
|
instantiate: (di): DisableExtension => {
|
||||||
|
const extensionLoader = di.inject(extensionLoaderInjectable);
|
||||||
|
|
||||||
|
return (extId) => {
|
||||||
|
const ext = extensionLoader.getExtensionById(extId);
|
||||||
|
|
||||||
|
if (ext && !ext.isBundled) {
|
||||||
|
ext.isEnabled = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export default disableExtensionInjectable;
|
||||||
@ -1,18 +0,0 @@
|
|||||||
/**
|
|
||||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
|
||||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
|
||||||
*/
|
|
||||||
import { getInjectable } from "@ogre-tools/injectable";
|
|
||||||
import extensionLoaderInjectable from "../../../../extensions/extension-loader/extension-loader.injectable";
|
|
||||||
import { disableExtension } from "./disable-extension";
|
|
||||||
|
|
||||||
const disableExtensionInjectable = getInjectable({
|
|
||||||
id: "disable-extension",
|
|
||||||
|
|
||||||
instantiate: (di) =>
|
|
||||||
disableExtension({
|
|
||||||
extensionLoader: di.inject(extensionLoaderInjectable),
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
|
|
||||||
export default disableExtensionInjectable;
|
|
||||||
@ -1,20 +0,0 @@
|
|||||||
/**
|
|
||||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
|
||||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
|
||||||
*/
|
|
||||||
import type { LensExtensionId } from "@k8slens/legacy-extensions";
|
|
||||||
import type { ExtensionLoader } from "../../../../extensions/extension-loader";
|
|
||||||
|
|
||||||
interface Dependencies {
|
|
||||||
extensionLoader: ExtensionLoader;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const disableExtension =
|
|
||||||
({ extensionLoader }: Dependencies) =>
|
|
||||||
(id: LensExtensionId) => {
|
|
||||||
const extension = extensionLoader.getExtension(id);
|
|
||||||
|
|
||||||
if (extension) {
|
|
||||||
extension.isEnabled = false;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@ -0,0 +1,27 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||||
|
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||||
|
*/
|
||||||
|
import type { LensExtensionId } from "@k8slens/legacy-extensions";
|
||||||
|
import { getInjectable } from "@ogre-tools/injectable";
|
||||||
|
import extensionLoaderInjectable from "../../../extensions/extension-loader/extension-loader.injectable";
|
||||||
|
|
||||||
|
export type EnableExtension = (extId: LensExtensionId) => void;
|
||||||
|
|
||||||
|
const enableExtensionInjectable = getInjectable({
|
||||||
|
id: "enable-extension",
|
||||||
|
|
||||||
|
instantiate: (di): EnableExtension => {
|
||||||
|
const extensionLoader = di.inject(extensionLoaderInjectable);
|
||||||
|
|
||||||
|
return (extId) => {
|
||||||
|
const ext = extensionLoader.getExtensionById(extId);
|
||||||
|
|
||||||
|
if (ext && !ext.isBundled) {
|
||||||
|
ext.isEnabled = true;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export default enableExtensionInjectable;
|
||||||
@ -1,18 +0,0 @@
|
|||||||
/**
|
|
||||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
|
||||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
|
||||||
*/
|
|
||||||
import { getInjectable } from "@ogre-tools/injectable";
|
|
||||||
import extensionLoaderInjectable from "../../../../extensions/extension-loader/extension-loader.injectable";
|
|
||||||
import { enableExtension } from "./enable-extension";
|
|
||||||
|
|
||||||
const enableExtensionInjectable = getInjectable({
|
|
||||||
id: "enable-extension",
|
|
||||||
|
|
||||||
instantiate: (di) =>
|
|
||||||
enableExtension({
|
|
||||||
extensionLoader: di.inject(extensionLoaderInjectable),
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
|
|
||||||
export default enableExtensionInjectable;
|
|
||||||
@ -1,20 +0,0 @@
|
|||||||
/**
|
|
||||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
|
||||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
|
||||||
*/
|
|
||||||
import type { LensExtensionId } from "@k8slens/legacy-extensions";
|
|
||||||
import type { ExtensionLoader } from "../../../../extensions/extension-loader";
|
|
||||||
|
|
||||||
interface Dependencies {
|
|
||||||
extensionLoader: ExtensionLoader;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const enableExtension =
|
|
||||||
({ extensionLoader }: Dependencies) =>
|
|
||||||
(id: LensExtensionId) => {
|
|
||||||
const extension = extensionLoader.getExtension(id);
|
|
||||||
|
|
||||||
if (extension) {
|
|
||||||
extension.isEnabled = true;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@ -4,138 +4,66 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import styles from "./extensions.module.scss";
|
import styles from "./extensions.module.scss";
|
||||||
import type { IComputedValue } from "mobx";
|
|
||||||
import {
|
|
||||||
makeObservable,
|
|
||||||
observable,
|
|
||||||
reaction,
|
|
||||||
when,
|
|
||||||
} from "mobx";
|
|
||||||
import { disposeOnUnmount, observer } from "mobx-react";
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { DropFileInput } from "../input";
|
import { DropFileInput } from "../input";
|
||||||
import { Install } from "./install";
|
import { ExtensionInstall } from "./install";
|
||||||
import { InstalledExtensions } from "./installed-extensions";
|
import { InstalledExtensions } from "./installed-extensions";
|
||||||
import { Notice } from "./notice";
|
import { Notice } from "./notice";
|
||||||
import { SettingLayout } from "../layout/setting-layout";
|
import { SettingLayout } from "../layout/setting-layout";
|
||||||
import { docsUrl } from "../../../common/vars";
|
import { docsUrl } from "../../../common/vars";
|
||||||
import { withInjectables } from "@ogre-tools/injectable-react";
|
import { withInjectables } from "@ogre-tools/injectable-react";
|
||||||
|
|
||||||
import userExtensionsInjectable from "./user-extensions/user-extensions.injectable";
|
|
||||||
import enableExtensionInjectable from "./enable-extension/enable-extension.injectable";
|
|
||||||
import disableExtensionInjectable from "./disable-extension/disable-extension.injectable";
|
|
||||||
import type { ConfirmUninstallExtension } from "./confirm-uninstall-extension.injectable";
|
|
||||||
import confirmUninstallExtensionInjectable from "./confirm-uninstall-extension.injectable";
|
|
||||||
import type { InstallExtensionFromInput } from "./install-extension-from-input.injectable";
|
|
||||||
import installExtensionFromInputInjectable from "./install-extension-from-input.injectable";
|
|
||||||
import installFromSelectFileDialogInjectable from "./install-from-select-file-dialog.injectable";
|
|
||||||
import type { InstallOnDrop } from "./install-on-drop.injectable";
|
import type { InstallOnDrop } from "./install-on-drop.injectable";
|
||||||
import installOnDropInjectable from "./install-on-drop.injectable";
|
import installOnDropInjectable from "./install-on-drop.injectable";
|
||||||
import { supportedExtensionFormats } from "./supported-extension-formats";
|
|
||||||
import extensionInstallationStateStoreInjectable from "../../../extensions/extension-installation-state-store/extension-installation-state-store.injectable";
|
|
||||||
import type { ExtensionInstallationStateStore } from "../../../extensions/extension-installation-state-store/extension-installation-state-store";
|
|
||||||
import Gutter from "../gutter/gutter";
|
import Gutter from "../gutter/gutter";
|
||||||
import type { InstalledExtension, LensExtensionId } from "@k8slens/legacy-extensions";
|
|
||||||
|
const ExtensionsNotice = () => (
|
||||||
|
<Notice className={styles.notice}>
|
||||||
|
<p>
|
||||||
|
{"Add new features via Lens Extensions. Check out the "}
|
||||||
|
<a
|
||||||
|
href={`${docsUrl}/extensions/lens-extensions`}
|
||||||
|
target="_blank"
|
||||||
|
rel="noreferrer"
|
||||||
|
>
|
||||||
|
docs
|
||||||
|
</a>
|
||||||
|
{" and list of "}
|
||||||
|
<a
|
||||||
|
href="https://github.com/lensapp/lens-extensions/blob/main/README.md"
|
||||||
|
target="_blank"
|
||||||
|
rel="noreferrer"
|
||||||
|
>
|
||||||
|
available extensions
|
||||||
|
</a>
|
||||||
|
.
|
||||||
|
</p>
|
||||||
|
</Notice>
|
||||||
|
);
|
||||||
|
|
||||||
interface Dependencies {
|
interface Dependencies {
|
||||||
userExtensions: IComputedValue<InstalledExtension[]>;
|
|
||||||
enableExtension: (id: LensExtensionId) => void;
|
|
||||||
disableExtension: (id: LensExtensionId) => void;
|
|
||||||
confirmUninstallExtension: ConfirmUninstallExtension;
|
|
||||||
installExtensionFromInput: InstallExtensionFromInput;
|
|
||||||
installFromSelectFileDialog: () => Promise<void>;
|
|
||||||
installOnDrop: InstallOnDrop;
|
installOnDrop: InstallOnDrop;
|
||||||
extensionInstallationStateStore: ExtensionInstallationStateStore;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@observer
|
const NonInjectedExtensions = ({ installOnDrop }: Dependencies) => (
|
||||||
class NonInjectedExtensions extends React.Component<Dependencies> {
|
<DropFileInput onDropFiles={installOnDrop}>
|
||||||
@observable installPath = "";
|
<SettingLayout
|
||||||
|
className="Extensions"
|
||||||
constructor(props: Dependencies) {
|
contentGaps={false}
|
||||||
super(props);
|
data-testid="extensions-page"
|
||||||
makeObservable(this);
|
>
|
||||||
}
|
<section>
|
||||||
|
<h1>Extensions</h1>
|
||||||
componentDidMount() {
|
<ExtensionsNotice />
|
||||||
disposeOnUnmount(this, [
|
<ExtensionInstall />
|
||||||
reaction(() => this.props.userExtensions.get().length, (curSize, prevSize) => {
|
<Gutter size="md" />
|
||||||
if (curSize > prevSize) {
|
<InstalledExtensions />
|
||||||
disposeOnUnmount(this, [
|
</section>
|
||||||
when(() => !this.props.extensionInstallationStateStore.anyInstalling, () => this.installPath = ""),
|
</SettingLayout>
|
||||||
]);
|
</DropFileInput>
|
||||||
}
|
);
|
||||||
}),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const userExtensions = this.props.userExtensions.get();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<DropFileInput onDropFiles={this.props.installOnDrop}>
|
|
||||||
<SettingLayout
|
|
||||||
className="Extensions"
|
|
||||||
contentGaps={false}
|
|
||||||
data-testid="extensions-page"
|
|
||||||
>
|
|
||||||
<section>
|
|
||||||
<h1>Extensions</h1>
|
|
||||||
|
|
||||||
<Notice className={styles.notice}>
|
|
||||||
<p>
|
|
||||||
{"Add new features via Lens Extensions. Check out the "}
|
|
||||||
<a
|
|
||||||
href={`${docsUrl}/extensions/`}
|
|
||||||
target="_blank"
|
|
||||||
rel="noreferrer"
|
|
||||||
>
|
|
||||||
docs
|
|
||||||
</a>
|
|
||||||
{" and list of "}
|
|
||||||
<a
|
|
||||||
href="https://github.com/lensapp/lens-extensions/blob/main/README.md"
|
|
||||||
target="_blank"
|
|
||||||
rel="noreferrer"
|
|
||||||
>
|
|
||||||
available extensions
|
|
||||||
</a>
|
|
||||||
.
|
|
||||||
</p>
|
|
||||||
</Notice>
|
|
||||||
|
|
||||||
<Install
|
|
||||||
supportedFormats={supportedExtensionFormats}
|
|
||||||
onChange={value => (this.installPath = value)}
|
|
||||||
installFromInput={() => this.props.installExtensionFromInput(this.installPath)}
|
|
||||||
installFromSelectFileDialog={this.props.installFromSelectFileDialog}
|
|
||||||
installPath={this.installPath}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Gutter size="md" />
|
|
||||||
|
|
||||||
<InstalledExtensions
|
|
||||||
extensions={userExtensions}
|
|
||||||
enable={this.props.enableExtension}
|
|
||||||
disable={this.props.disableExtension}
|
|
||||||
uninstall={this.props.confirmUninstallExtension}
|
|
||||||
/>
|
|
||||||
</section>
|
|
||||||
</SettingLayout>
|
|
||||||
</DropFileInput>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const Extensions = withInjectables<Dependencies>(NonInjectedExtensions, {
|
export const Extensions = withInjectables<Dependencies>(NonInjectedExtensions, {
|
||||||
getProps: (di) => ({
|
getProps: (di) => ({
|
||||||
userExtensions: di.inject(userExtensionsInjectable),
|
|
||||||
enableExtension: di.inject(enableExtensionInjectable),
|
|
||||||
disableExtension: di.inject(disableExtensionInjectable),
|
|
||||||
confirmUninstallExtension: di.inject(confirmUninstallExtensionInjectable),
|
|
||||||
installExtensionFromInput: di.inject(installExtensionFromInputInjectable),
|
|
||||||
installOnDrop: di.inject(installOnDropInjectable),
|
installOnDrop: di.inject(installOnDropInjectable),
|
||||||
installFromSelectFileDialog: di.inject(installFromSelectFileDialogInjectable),
|
|
||||||
extensionInstallationStateStore: di.inject(extensionInstallationStateStoreInjectable),
|
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|||||||
@ -4,7 +4,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import styles from "./install.module.scss";
|
import styles from "./install.module.scss";
|
||||||
import React from "react";
|
import React, { useEffect, useRef, useState } from "react";
|
||||||
import { prevDefault } from "@k8slens/utilities";
|
import { prevDefault } from "@k8slens/utilities";
|
||||||
import { Button } from "../button";
|
import { Button } from "../button";
|
||||||
import { Icon } from "../icon";
|
import { Icon } from "../icon";
|
||||||
@ -16,17 +16,16 @@ import type { ExtensionInstallationStateStore } from "../../../extensions/extens
|
|||||||
import extensionInstallationStateStoreInjectable from "../../../extensions/extension-installation-state-store/extension-installation-state-store.injectable";
|
import extensionInstallationStateStoreInjectable from "../../../extensions/extension-installation-state-store/extension-installation-state-store.injectable";
|
||||||
import { withInjectables } from "@ogre-tools/injectable-react";
|
import { withInjectables } from "@ogre-tools/injectable-react";
|
||||||
import { unionInputValidatorsAsync } from "../input/input_validators";
|
import { unionInputValidatorsAsync } from "../input/input_validators";
|
||||||
|
import { supportedExtensionFormats } from "./supported-extension-formats";
|
||||||
export interface InstallProps {
|
import type { InstallExtensionFromInput } from "./install-extension-from-input.injectable";
|
||||||
installPath: string;
|
import type { InstallFromSelectFileDialog } from "./install-from-select-file-dialog.injectable";
|
||||||
supportedFormats: string[];
|
import installExtensionFromInputInjectable from "./install-extension-from-input.injectable";
|
||||||
onChange: (path: string) => void;
|
import installFromSelectFileDialogInjectable from "./install-from-select-file-dialog.injectable";
|
||||||
installFromInput: () => void;
|
|
||||||
installFromSelectFileDialog: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Dependencies {
|
interface Dependencies {
|
||||||
extensionInstallationStateStore: ExtensionInstallationStateStore;
|
installState: ExtensionInstallationStateStore;
|
||||||
|
installExtensionFromInput: InstallExtensionFromInput;
|
||||||
|
installFromSelectFileDialog: InstallFromSelectFileDialog;
|
||||||
}
|
}
|
||||||
|
|
||||||
const installInputValidator = unionInputValidatorsAsync(
|
const installInputValidator = unionInputValidatorsAsync(
|
||||||
@ -38,71 +37,75 @@ const installInputValidator = unionInputValidatorsAsync(
|
|||||||
InputValidators.isPath,
|
InputValidators.isPath,
|
||||||
);
|
);
|
||||||
|
|
||||||
const NonInjectedInstall: React.FC<Dependencies & InstallProps> = ({
|
const installTitle = `Name or file path or URL to an extension package (${supportedExtensionFormats.join(", ")})`;
|
||||||
installPath,
|
|
||||||
supportedFormats,
|
const NonInjectedInstall = observer(({
|
||||||
onChange,
|
installExtensionFromInput,
|
||||||
installFromInput,
|
|
||||||
installFromSelectFileDialog,
|
installFromSelectFileDialog,
|
||||||
extensionInstallationStateStore,
|
installState,
|
||||||
}) => (
|
}: Dependencies) => {
|
||||||
<section>
|
const [installPath, setInstallPath] = useState("");
|
||||||
<SubTitle
|
const prevAnyInstalling = useRef(installState.anyInstalling);
|
||||||
title={`Name or file path or URL to an extension package (${supportedFormats.join(
|
|
||||||
", ",
|
|
||||||
)})`}
|
|
||||||
/>
|
|
||||||
<div className={styles.inputs}>
|
|
||||||
<div>
|
|
||||||
<Input
|
|
||||||
theme="round-black"
|
|
||||||
disabled={extensionInstallationStateStore.anyPreInstallingOrInstalling}
|
|
||||||
placeholder={"Name or file path or URL"}
|
|
||||||
showErrorsAsTooltip={{ preferredPositions: TooltipPosition.BOTTOM }}
|
|
||||||
validators={installPath ? installInputValidator : undefined}
|
|
||||||
value={installPath}
|
|
||||||
onChange={onChange}
|
|
||||||
onSubmit={installFromInput}
|
|
||||||
iconRight={(
|
|
||||||
<Icon
|
|
||||||
className={styles.icon}
|
|
||||||
smallest
|
|
||||||
material="folder_open"
|
|
||||||
onClick={prevDefault(installFromSelectFileDialog)}
|
|
||||||
tooltip="Browse"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<Button
|
|
||||||
className={styles.button}
|
|
||||||
primary
|
|
||||||
label="Install"
|
|
||||||
disabled={
|
|
||||||
extensionInstallationStateStore.anyPreInstallingOrInstalling
|
|
||||||
}
|
|
||||||
waiting={extensionInstallationStateStore.anyPreInstallingOrInstalling}
|
|
||||||
onClick={installFromInput}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<small className={styles.proTip}>
|
|
||||||
<b>Pro-Tip</b>
|
|
||||||
: you can drag and drop a tarball file to this area
|
|
||||||
</small>
|
|
||||||
</section>
|
|
||||||
);
|
|
||||||
|
|
||||||
export const Install = withInjectables<Dependencies, InstallProps>(
|
useEffect(() => {
|
||||||
observer(NonInjectedInstall),
|
const currentlyInstalling = installState.anyInstalling;
|
||||||
{
|
const previouslyInstalling = prevAnyInstalling.current;
|
||||||
getProps: (di, props) => ({
|
|
||||||
extensionInstallationStateStore: di.inject(
|
|
||||||
extensionInstallationStateStoreInjectable,
|
|
||||||
),
|
|
||||||
|
|
||||||
...props,
|
if (!currentlyInstalling && previouslyInstalling) {
|
||||||
}),
|
prevAnyInstalling.current = false;
|
||||||
},
|
setInstallPath("");
|
||||||
);
|
}
|
||||||
|
}, [installState.anyInstalling]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section>
|
||||||
|
<SubTitle title={installTitle} />
|
||||||
|
<div className={styles.inputs}>
|
||||||
|
<div>
|
||||||
|
<Input
|
||||||
|
theme="round-black"
|
||||||
|
disabled={installState.anyPreInstallingOrInstalling}
|
||||||
|
placeholder="Name or file path or URL"
|
||||||
|
showErrorsAsTooltip={{ preferredPositions: TooltipPosition.BOTTOM }}
|
||||||
|
validators={installPath ? installInputValidator : undefined}
|
||||||
|
value={installPath}
|
||||||
|
onChange={setInstallPath}
|
||||||
|
onSubmit={() => installExtensionFromInput(installPath)}
|
||||||
|
iconRight={(
|
||||||
|
<Icon
|
||||||
|
className={styles.icon}
|
||||||
|
smallest
|
||||||
|
material="folder_open"
|
||||||
|
onClick={prevDefault(installFromSelectFileDialog)}
|
||||||
|
tooltip="Browse"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Button
|
||||||
|
className={styles.button}
|
||||||
|
primary
|
||||||
|
label="Install"
|
||||||
|
disabled={installState.anyPreInstallingOrInstalling}
|
||||||
|
waiting={installState.anyPreInstallingOrInstalling}
|
||||||
|
onClick={() => installExtensionFromInput(installPath)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<small className={styles.proTip}>
|
||||||
|
<b>Pro-Tip</b>
|
||||||
|
: you can drag and drop a tarball file to this area
|
||||||
|
</small>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
export const ExtensionInstall = withInjectables<Dependencies>(NonInjectedInstall, {
|
||||||
|
getProps: (di, props) => ({
|
||||||
|
...props,
|
||||||
|
installState: di.inject(extensionInstallationStateStoreInjectable),
|
||||||
|
installExtensionFromInput: di.inject(installExtensionFromInputInjectable),
|
||||||
|
installFromSelectFileDialog: di.inject(installFromSelectFileDialogInjectable),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|||||||
@ -4,8 +4,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import styles from "./installed-extensions.module.scss";
|
import styles from "./installed-extensions.module.scss";
|
||||||
import React, { useMemo } from "react";
|
import React from "react";
|
||||||
import type { ExtensionDiscovery } from "../../../extensions/extension-discovery/extension-discovery";
|
|
||||||
import { Icon } from "../icon";
|
import { Icon } from "../icon";
|
||||||
import { List } from "../list/list";
|
import { List } from "../list/list";
|
||||||
import { MenuActions, MenuItem } from "../menu";
|
import { MenuActions, MenuItem } from "../menu";
|
||||||
@ -17,18 +16,27 @@ import extensionDiscoveryInjectable from "../../../extensions/extension-discover
|
|||||||
import { withInjectables } from "@ogre-tools/injectable-react";
|
import { withInjectables } from "@ogre-tools/injectable-react";
|
||||||
import extensionInstallationStateStoreInjectable from "../../../extensions/extension-installation-state-store/extension-installation-state-store.injectable";
|
import extensionInstallationStateStoreInjectable from "../../../extensions/extension-installation-state-store/extension-installation-state-store.injectable";
|
||||||
import type { ExtensionInstallationStateStore } from "../../../extensions/extension-installation-state-store/extension-installation-state-store";
|
import type { ExtensionInstallationStateStore } from "../../../extensions/extension-installation-state-store/extension-installation-state-store";
|
||||||
import type { InstalledExtension, LensExtensionId } from "@k8slens/legacy-extensions";
|
import type { InstalledExtension } from "@k8slens/legacy-extensions";
|
||||||
|
import type { IComputedValue } from "mobx";
|
||||||
|
import type { ConfirmUninstallExtension } from "./confirm-uninstall-extension.injectable";
|
||||||
|
import confirmUninstallExtensionInjectable from "./confirm-uninstall-extension.injectable";
|
||||||
|
import type { DisableExtension } from "./disable-extension.injectable";
|
||||||
|
import disableExtensionInjectable from "./disable-extension.injectable";
|
||||||
|
import type { EnableExtension } from "./enable-extension.injectable";
|
||||||
|
import enableExtensionInjectable from "./enable-extension.injectable";
|
||||||
|
import userExtensionsInjectable from "./user-extensions/user-extensions.injectable";
|
||||||
|
import type { ExtensionDiscovery } from "../../../extensions/extension-discovery/extension-discovery";
|
||||||
|
|
||||||
export interface InstalledExtensionsProps {
|
export interface InstalledExtensionsProps {
|
||||||
extensions: InstalledExtension[];
|
|
||||||
enable: (id: LensExtensionId) => void;
|
|
||||||
disable: (id: LensExtensionId) => void;
|
|
||||||
uninstall: (extension: InstalledExtension) => void;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Dependencies {
|
interface Dependencies {
|
||||||
extensionDiscovery: ExtensionDiscovery;
|
extensionDiscovery: ExtensionDiscovery;
|
||||||
extensionInstallationStateStore: ExtensionInstallationStateStore;
|
extensionInstallationStateStore: ExtensionInstallationStateStore;
|
||||||
|
userExtensions: IComputedValue<InstalledExtension[]>;
|
||||||
|
enableExtension: EnableExtension;
|
||||||
|
disableExtension: DisableExtension;
|
||||||
|
confirmUninstallExtension: ConfirmUninstallExtension;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getStatus(extension: InstalledExtension) {
|
function getStatus(extension: InstalledExtension) {
|
||||||
@ -39,106 +47,20 @@ function getStatus(extension: InstalledExtension) {
|
|||||||
return extension.isEnabled ? "Enabled" : "Disabled";
|
return extension.isEnabled ? "Enabled" : "Disabled";
|
||||||
}
|
}
|
||||||
|
|
||||||
const NonInjectedInstalledExtensions = observer(({ extensionDiscovery, extensionInstallationStateStore, extensions, uninstall, enable, disable }: Dependencies & InstalledExtensionsProps) => {
|
const NonInjectedInstalledExtensions = observer(({
|
||||||
const columns = useMemo(
|
extensionDiscovery,
|
||||||
() => [
|
extensionInstallationStateStore,
|
||||||
{
|
userExtensions,
|
||||||
Header: "Name",
|
confirmUninstallExtension,
|
||||||
accessor: "extension",
|
enableExtension,
|
||||||
width: 200,
|
disableExtension,
|
||||||
sortType: (rowA: Row, rowB: Row) => { // Custom sorting for extension name
|
}: Dependencies & InstalledExtensionsProps) => {
|
||||||
const nameA = extensions[rowA.index].manifest.name;
|
|
||||||
const nameB = extensions[rowB.index].manifest.name;
|
|
||||||
|
|
||||||
if (nameA > nameB) return -1;
|
|
||||||
if (nameB > nameA) return 1;
|
|
||||||
|
|
||||||
return 0;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Header: "Version",
|
|
||||||
accessor: "version",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Header: "Status",
|
|
||||||
accessor: "status",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Header: "",
|
|
||||||
accessor: "actions",
|
|
||||||
disableSortBy: true,
|
|
||||||
width: 20,
|
|
||||||
className: "actions",
|
|
||||||
},
|
|
||||||
], [],
|
|
||||||
);
|
|
||||||
|
|
||||||
const data = useMemo(
|
|
||||||
() => extensions.map(extension => {
|
|
||||||
const { id, isEnabled, isCompatible, manifest } = extension;
|
|
||||||
const { name, description, version } = manifest;
|
|
||||||
const isUninstalling = extensionInstallationStateStore.isExtensionUninstalling(id);
|
|
||||||
|
|
||||||
return {
|
|
||||||
extension: (
|
|
||||||
<div className={"flex items-start"}>
|
|
||||||
<div>
|
|
||||||
<div className={styles.extensionName}>{name}</div>
|
|
||||||
<div className={styles.extensionDescription}>{description}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
),
|
|
||||||
version,
|
|
||||||
status: (
|
|
||||||
<div className={cssNames({ [styles.enabled]: isEnabled, [styles.invalid]: !isCompatible })}>
|
|
||||||
{getStatus(extension)}
|
|
||||||
</div>
|
|
||||||
),
|
|
||||||
actions: (
|
|
||||||
<MenuActions
|
|
||||||
id={`menu-actions-for-installed-extensions-for-${id}`}
|
|
||||||
usePortal
|
|
||||||
toolbar={false}>
|
|
||||||
{isCompatible && (
|
|
||||||
<>
|
|
||||||
{isEnabled ? (
|
|
||||||
<MenuItem
|
|
||||||
disabled={isUninstalling}
|
|
||||||
onClick={() => disable(id)}
|
|
||||||
>
|
|
||||||
<Icon material="unpublished" />
|
|
||||||
<span className="title" aria-disabled={isUninstalling}>Disable</span>
|
|
||||||
</MenuItem>
|
|
||||||
) : (
|
|
||||||
<MenuItem
|
|
||||||
disabled={isUninstalling}
|
|
||||||
onClick={() => enable(id)}
|
|
||||||
>
|
|
||||||
<Icon material="check_circle" />
|
|
||||||
<span className="title" aria-disabled={isUninstalling}>Enable</span>
|
|
||||||
</MenuItem>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<MenuItem
|
|
||||||
disabled={isUninstalling}
|
|
||||||
onClick={() => uninstall(extension)}
|
|
||||||
>
|
|
||||||
<Icon material="delete" />
|
|
||||||
<span className="title" aria-disabled={isUninstalling}>Uninstall</span>
|
|
||||||
</MenuItem>
|
|
||||||
</MenuActions>
|
|
||||||
),
|
|
||||||
};
|
|
||||||
}), [extensions, extensionInstallationStateStore.anyUninstalling],
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!extensionDiscovery.isLoaded) {
|
if (!extensionDiscovery.isLoaded) {
|
||||||
return <div><Spinner center /></div>;
|
return <div><Spinner center /></div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const extensions = userExtensions.get();
|
||||||
|
|
||||||
if (extensions.length == 0) {
|
if (extensions.length == 0) {
|
||||||
return (
|
return (
|
||||||
<div className="flex column h-full items-center justify-center">
|
<div className="flex column h-full items-center justify-center">
|
||||||
@ -151,13 +73,96 @@ const NonInjectedInstalledExtensions = observer(({ extensionDiscovery, extension
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const toggleExtensionWith = (enabled: boolean) => (
|
||||||
|
enabled
|
||||||
|
? disableExtension
|
||||||
|
: enableExtension
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section data-testid="extensions-table">
|
<section data-testid="extensions-table">
|
||||||
<List
|
<List
|
||||||
title={<h2 className={styles.title}>Installed extensions</h2>}
|
title={<h2 className={styles.title}>Installed extensions</h2>}
|
||||||
columns={columns}
|
columns={[
|
||||||
data={data}
|
{
|
||||||
items={extensions}
|
Header: "Name",
|
||||||
|
accessor: "extension",
|
||||||
|
width: 200,
|
||||||
|
sortType: (rowA: Row, rowB: Row) => { // Custom sorting for extension name
|
||||||
|
const nameA = extensions[rowA.index].manifest.name;
|
||||||
|
const nameB = extensions[rowB.index].manifest.name;
|
||||||
|
|
||||||
|
if (nameA > nameB) return -1;
|
||||||
|
if (nameB > nameA) return 1;
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Header: "Version",
|
||||||
|
accessor: "version",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Header: "Status",
|
||||||
|
accessor: "status",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Header: "",
|
||||||
|
accessor: "actions",
|
||||||
|
disableSortBy: true,
|
||||||
|
width: 20,
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
data={extensions.map(extension => {
|
||||||
|
const { id, isEnabled, isCompatible, manifest } = extension;
|
||||||
|
const { name, description, version } = manifest;
|
||||||
|
const isUninstalling = extensionInstallationStateStore.isExtensionUninstalling(id);
|
||||||
|
const toggleExtension = toggleExtensionWith(isEnabled);
|
||||||
|
|
||||||
|
return {
|
||||||
|
extension: (
|
||||||
|
<div className={"flex items-start"}>
|
||||||
|
<div>
|
||||||
|
<div className={styles.extensionName}>{name}</div>
|
||||||
|
<div className={styles.extensionDescription}>{description}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
version,
|
||||||
|
status: (
|
||||||
|
<div className={cssNames({ [styles.enabled]: isEnabled, [styles.invalid]: !isCompatible })}>
|
||||||
|
{getStatus(extension)}
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
actions: (
|
||||||
|
<MenuActions
|
||||||
|
id={`menu-actions-for-installed-extensions-for-${id}`}
|
||||||
|
usePortal
|
||||||
|
toolbar={false}>
|
||||||
|
{isCompatible && (
|
||||||
|
<MenuItem
|
||||||
|
disabled={isUninstalling}
|
||||||
|
onClick={() => toggleExtension(id)}
|
||||||
|
>
|
||||||
|
<Icon material={isEnabled ? "unpublished" : "check_circle"} />
|
||||||
|
<span className="title" aria-disabled={isUninstalling}>
|
||||||
|
{isEnabled ? "Disable" : "Enabled"}
|
||||||
|
</span>
|
||||||
|
</MenuItem>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<MenuItem
|
||||||
|
disabled={isUninstalling}
|
||||||
|
onClick={() => confirmUninstallExtension(extension)}
|
||||||
|
>
|
||||||
|
<Icon material="delete" />
|
||||||
|
<span className="title" aria-disabled={isUninstalling}>Uninstall</span>
|
||||||
|
</MenuItem>
|
||||||
|
</MenuActions>
|
||||||
|
),
|
||||||
|
};
|
||||||
|
})}
|
||||||
|
items={userExtensions.get()}
|
||||||
filters={[
|
filters={[
|
||||||
(extension) => extension.manifest.name,
|
(extension) => extension.manifest.name,
|
||||||
(extension) => getStatus(extension),
|
(extension) => getStatus(extension),
|
||||||
@ -170,8 +175,12 @@ const NonInjectedInstalledExtensions = observer(({ extensionDiscovery, extension
|
|||||||
|
|
||||||
export const InstalledExtensions = withInjectables<Dependencies, InstalledExtensionsProps>(NonInjectedInstalledExtensions, {
|
export const InstalledExtensions = withInjectables<Dependencies, InstalledExtensionsProps>(NonInjectedInstalledExtensions, {
|
||||||
getProps: (di, props) => ({
|
getProps: (di, props) => ({
|
||||||
|
...props,
|
||||||
extensionDiscovery: di.inject(extensionDiscoveryInjectable),
|
extensionDiscovery: di.inject(extensionDiscoveryInjectable),
|
||||||
extensionInstallationStateStore: di.inject(extensionInstallationStateStoreInjectable),
|
extensionInstallationStateStore: di.inject(extensionInstallationStateStoreInjectable),
|
||||||
...props,
|
userExtensions: di.inject(userExtensionsInjectable),
|
||||||
|
enableExtension: di.inject(enableExtensionInjectable),
|
||||||
|
disableExtension: di.inject(disableExtensionInjectable),
|
||||||
|
confirmUninstallExtension: di.inject(confirmUninstallExtensionInjectable),
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|||||||
@ -27,7 +27,7 @@ const uninstallExtensionInjectable = getInjectable({
|
|||||||
const showErrorNotification = di.inject(showErrorNotificationInjectable);
|
const showErrorNotification = di.inject(showErrorNotificationInjectable);
|
||||||
|
|
||||||
return async (extensionId: LensExtensionId): Promise<boolean> => {
|
return async (extensionId: LensExtensionId): Promise<boolean> => {
|
||||||
const ext = extensionLoader.getExtension(extensionId);
|
const ext = extensionLoader.getExtensionById(extensionId);
|
||||||
|
|
||||||
if (!ext) {
|
if (!ext) {
|
||||||
logger.debug(`[EXTENSIONS]: cannot uninstall ${extensionId}, was not installed`);
|
logger.debug(`[EXTENSIONS]: cannot uninstall ${extensionId}, was not installed`);
|
||||||
@ -45,7 +45,7 @@ const uninstallExtensionInjectable = getInjectable({
|
|||||||
await extensionDiscovery.uninstallExtension(extensionId);
|
await extensionDiscovery.uninstallExtension(extensionId);
|
||||||
|
|
||||||
// wait for the ExtensionLoader to actually uninstall the extension
|
// wait for the ExtensionLoader to actually uninstall the extension
|
||||||
await when(() => !extensionLoader.userExtensions.has(extensionId));
|
await when(() => !extensionLoader.userExtensions.get().has(extensionId));
|
||||||
|
|
||||||
showSuccessNotification(
|
showSuccessNotification(
|
||||||
<p>
|
<p>
|
||||||
|
|||||||
@ -12,7 +12,7 @@ const userExtensionsInjectable = getInjectable({
|
|||||||
instantiate: (di) => {
|
instantiate: (di) => {
|
||||||
const extensionLoader = di.inject(extensionLoaderInjectable);
|
const extensionLoader = di.inject(extensionLoaderInjectable);
|
||||||
|
|
||||||
return computed(() => [...extensionLoader.userExtensions.values()]);
|
return computed(() => [...extensionLoader.userExtensions.get().values()]);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -3,44 +3,35 @@
|
|||||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||||
*/
|
*/
|
||||||
import { getInjectable } from "@ogre-tools/injectable";
|
import { getInjectable } from "@ogre-tools/injectable";
|
||||||
import type { KubeObject } from "../../../../../../common/k8s-api/kube-object";
|
import { KubeObject } from "../../../../../../common/k8s-api/kube-object";
|
||||||
import { parseKubeApi } from "../../../../../../common/k8s-api/kube-api-parse";
|
import { parseKubeApi } from "../../../../../../common/k8s-api/kube-api-parse";
|
||||||
import type { AsyncResult } from "@k8slens/utilities";
|
import type { AsyncResult } from "@k8slens/utilities";
|
||||||
import { getErrorMessage } from "../../../../../../common/utils/get-error-message";
|
import { getErrorMessage } from "../../../../../../common/utils/get-error-message";
|
||||||
import apiManagerInjectable from "../../../../../../common/k8s-api/api-manager/manager.injectable";
|
import apiKubeInjectable from "../../../../../k8s/api-kube.injectable";
|
||||||
import { waitUntilDefined } from "@k8slens/utilities";
|
|
||||||
|
|
||||||
export type CallForResource = (
|
export type CallForResource = (selfLink: string) => AsyncResult<KubeObject | undefined>;
|
||||||
selfLink: string
|
|
||||||
) => AsyncResult<KubeObject | undefined>;
|
|
||||||
|
|
||||||
const callForResourceInjectable = getInjectable({
|
const callForResourceInjectable = getInjectable({
|
||||||
id: "call-for-resource",
|
id: "call-for-resource",
|
||||||
|
|
||||||
instantiate: (di): CallForResource => {
|
instantiate: (di): CallForResource => {
|
||||||
const apiManager = di.inject(apiManagerInjectable);
|
const apiKube = di.inject(apiKubeInjectable);
|
||||||
|
|
||||||
return async (apiPath: string) => {
|
return async (apiPath: string) => {
|
||||||
const api = await waitUntilDefined(() => apiManager.getApi(apiPath));
|
|
||||||
|
|
||||||
const parsed = parseKubeApi(apiPath);
|
const parsed = parseKubeApi(apiPath);
|
||||||
|
|
||||||
if (!api || !parsed.name) {
|
if (!parsed.name) {
|
||||||
return { callWasSuccessful: false, error: "Invalid API path" };
|
return { callWasSuccessful: false, error: "Invalid API path" };
|
||||||
}
|
}
|
||||||
|
|
||||||
let resource: KubeObject | null;
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
resource = await api.get({
|
return {
|
||||||
name: parsed.name,
|
callWasSuccessful: true,
|
||||||
namespace: parsed.namespace,
|
response: new KubeObject(await apiKube.get(apiPath)),
|
||||||
});
|
};
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return { callWasSuccessful: false, error: getErrorMessage(e) };
|
return { callWasSuccessful: false, error: getErrorMessage(e) };
|
||||||
}
|
}
|
||||||
|
|
||||||
return { callWasSuccessful: true, response: resource || undefined };
|
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@ -7,9 +7,9 @@ import type { CallForResource } from "./call-for-resource/call-for-resource.inje
|
|||||||
import callForResourceInjectable from "./call-for-resource/call-for-resource.injectable";
|
import callForResourceInjectable from "./call-for-resource/call-for-resource.injectable";
|
||||||
import { waitUntilDefined } from "@k8slens/utilities";
|
import { waitUntilDefined } from "@k8slens/utilities";
|
||||||
import editResourceTabStoreInjectable from "../store.injectable";
|
import editResourceTabStoreInjectable from "../store.injectable";
|
||||||
import type { EditResourceTabStore } from "../store";
|
import type { EditingResource, EditResourceTabStore } from "../store";
|
||||||
import { action, computed, makeObservable, observable, runInAction } from "mobx";
|
import { action, computed, observable, runInAction } from "mobx";
|
||||||
import type { KubeObject } from "../../../../../common/k8s-api/kube-object";
|
import type { KubeObject, RawKubeObject } from "../../../../../common/k8s-api/kube-object";
|
||||||
import yaml from "js-yaml";
|
import yaml from "js-yaml";
|
||||||
import assert from "assert";
|
import assert from "assert";
|
||||||
import type { CallForPatchResource } from "./call-for-patch-resource/call-for-patch-resource.injectable";
|
import type { CallForPatchResource } from "./call-for-patch-resource/call-for-patch-resource.injectable";
|
||||||
@ -19,18 +19,22 @@ import type { ShowNotification } from "../../../notifications";
|
|||||||
import showSuccessNotificationInjectable from "../../../notifications/show-success-notification.injectable";
|
import showSuccessNotificationInjectable from "../../../notifications/show-success-notification.injectable";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import showErrorNotificationInjectable from "../../../notifications/show-error-notification.injectable";
|
import showErrorNotificationInjectable from "../../../notifications/show-error-notification.injectable";
|
||||||
|
import { createKubeApiURL, parseKubeApi } from "../../../../../common/k8s-api/kube-api-parse";
|
||||||
|
|
||||||
const editResourceModelInjectable = getInjectable({
|
const editResourceModelInjectable = getInjectable({
|
||||||
id: "edit-resource-model",
|
id: "edit-resource-model",
|
||||||
|
|
||||||
instantiate: async (di, tabId: string) => {
|
instantiate: async (di, tabId: string) => {
|
||||||
|
const store = di.inject(editResourceTabStoreInjectable);
|
||||||
|
|
||||||
const model = new EditResourceModel({
|
const model = new EditResourceModel({
|
||||||
callForResource: di.inject(callForResourceInjectable),
|
callForResource: di.inject(callForResourceInjectable),
|
||||||
callForPatchResource: di.inject(callForPatchResourceInjectable),
|
callForPatchResource: di.inject(callForPatchResourceInjectable),
|
||||||
showSuccessNotification: di.inject(showSuccessNotificationInjectable),
|
showSuccessNotification: di.inject(showSuccessNotificationInjectable),
|
||||||
showErrorNotification: di.inject(showErrorNotificationInjectable),
|
showErrorNotification: di.inject(showErrorNotificationInjectable),
|
||||||
store: di.inject(editResourceTabStoreInjectable),
|
store,
|
||||||
tabId,
|
tabId,
|
||||||
|
waitForEditingResource: () => waitUntilDefined(() => store.getData(tabId)),
|
||||||
});
|
});
|
||||||
|
|
||||||
await model.load();
|
await model.load();
|
||||||
@ -48,19 +52,42 @@ export default editResourceModelInjectable;
|
|||||||
interface Dependencies {
|
interface Dependencies {
|
||||||
callForResource: CallForResource;
|
callForResource: CallForResource;
|
||||||
callForPatchResource: CallForPatchResource;
|
callForPatchResource: CallForPatchResource;
|
||||||
|
waitForEditingResource: () => Promise<EditingResource>;
|
||||||
showSuccessNotification: ShowNotification;
|
showSuccessNotification: ShowNotification;
|
||||||
showErrorNotification: ShowNotification;
|
showErrorNotification: ShowNotification;
|
||||||
readonly store: EditResourceTabStore;
|
readonly store: EditResourceTabStore;
|
||||||
readonly tabId: string;
|
readonly tabId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class EditResourceModel {
|
function getEditSelfLinkFor(object: RawKubeObject): string {
|
||||||
constructor(private readonly dependencies: Dependencies) {
|
const lensVersionLabel = object.metadata.labels?.[EditResourceLabelName];
|
||||||
makeObservable(this);
|
|
||||||
|
if (lensVersionLabel) {
|
||||||
|
const { apiVersionWithGroup, ...parsedApi } = parseKubeApi(object.metadata.selfLink);
|
||||||
|
|
||||||
|
parsedApi.apiVersion = lensVersionLabel;
|
||||||
|
|
||||||
|
return createKubeApiURL({
|
||||||
|
...parsedApi,
|
||||||
|
apiVersion: `${parsedApi.apiGroup}/${parsedApi.apiVersion}`,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return object.metadata.selfLink;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The label name that Lens uses to receive the desired api version
|
||||||
|
*/
|
||||||
|
export const EditResourceLabelName = "k8slens-edit-resource-version";
|
||||||
|
|
||||||
|
export class EditResourceModel {
|
||||||
|
constructor(protected readonly dependencies: Dependencies) {}
|
||||||
|
|
||||||
readonly configuration = {
|
readonly configuration = {
|
||||||
value: computed(() => this.editingResource.draft || this.editingResource.firstDraft || ""),
|
value: computed(
|
||||||
|
() => this.editingResource.draft || this.editingResource.firstDraft || "",
|
||||||
|
),
|
||||||
|
|
||||||
onChange: action((value: string) => {
|
onChange: action((value: string) => {
|
||||||
this.editingResource.draft = value;
|
this.editingResource.draft = value;
|
||||||
@ -100,27 +127,39 @@ export class EditResourceModel {
|
|||||||
return this.editingResource.resource;
|
return this.editingResource.resource;
|
||||||
}
|
}
|
||||||
|
|
||||||
load = async () => {
|
load = async (): Promise<void> => {
|
||||||
await waitUntilDefined(() => this.dependencies.store.getData(this.dependencies.tabId));
|
await this.dependencies.waitForEditingResource();
|
||||||
|
|
||||||
const result = await this.dependencies.callForResource(this.selfLink);
|
let result = await this.dependencies.callForResource(this.selfLink);
|
||||||
|
|
||||||
if (!result.callWasSuccessful) {
|
if (!result.callWasSuccessful) {
|
||||||
this.dependencies.showErrorNotification(
|
return void this.dependencies.showErrorNotification(`Loading resource failed: ${result.error}`);
|
||||||
`Loading resource failed: ${result.error}`,
|
}
|
||||||
);
|
|
||||||
|
|
||||||
|
if (result?.response?.metadata.labels?.[EditResourceLabelName]) {
|
||||||
|
const parsed = parseKubeApi(this.selfLink);
|
||||||
|
|
||||||
|
parsed.apiVersion = result.response.metadata.labels[EditResourceLabelName];
|
||||||
|
|
||||||
|
result = await this.dependencies.callForResource(createKubeApiURL(parsed));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!result.callWasSuccessful) {
|
||||||
|
return void this.dependencies.showErrorNotification(`Loading resource failed: ${result.error}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const resource = result.response;
|
||||||
|
|
||||||
|
runInAction(() => {
|
||||||
|
this._resource = resource;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!resource) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
runInAction(() => {
|
runInAction(() => {
|
||||||
this._resource = result.response;
|
this.editingResource.firstDraft = yaml.dump(resource.toPlainObject());
|
||||||
|
|
||||||
if (this._resource) {
|
|
||||||
this.editingResource.firstDraft = yaml.dump(
|
|
||||||
this._resource.toPlainObject(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -138,16 +177,16 @@ export class EditResourceModel {
|
|||||||
|
|
||||||
save = async () => {
|
save = async () => {
|
||||||
const currentValue = this.configuration.value.get();
|
const currentValue = this.configuration.value.get();
|
||||||
const currentVersion = yaml.load(currentValue);
|
const currentVersion = yaml.load(currentValue) as RawKubeObject;
|
||||||
const firstVersion = yaml.load(
|
const firstVersion = yaml.load(this.editingResource.firstDraft ?? currentValue);
|
||||||
this.editingResource.firstDraft ?? currentValue,
|
|
||||||
);
|
|
||||||
const patches = createPatch(firstVersion, currentVersion);
|
|
||||||
|
|
||||||
const result = await this.dependencies.callForPatchResource(
|
// Make sure we save this label so that we can use it in the future
|
||||||
this.resource,
|
currentVersion.metadata.labels ??= {};
|
||||||
patches,
|
currentVersion.metadata.labels[EditResourceLabelName] = currentVersion.apiVersion.split("/").pop();
|
||||||
);
|
|
||||||
|
const patches = createPatch(firstVersion, currentVersion);
|
||||||
|
const selfLink = getEditSelfLinkFor(currentVersion);
|
||||||
|
const result = await this.dependencies.callForPatchResource(this.resource, patches);
|
||||||
|
|
||||||
if (!result.callWasSuccessful) {
|
if (!result.callWasSuccessful) {
|
||||||
this.dependencies.showErrorNotification((
|
this.dependencies.showErrorNotification((
|
||||||
@ -158,23 +197,26 @@ export class EditResourceModel {
|
|||||||
</p>
|
</p>
|
||||||
));
|
));
|
||||||
|
|
||||||
return;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { kind, name } = result.response;
|
const { kind, name } = result.response;
|
||||||
|
|
||||||
this.dependencies.showSuccessNotification((
|
this.dependencies.showSuccessNotification(
|
||||||
<p>
|
<p>
|
||||||
{`${kind} `}
|
{kind}
|
||||||
|
{" "}
|
||||||
<b>{name}</b>
|
<b>{name}</b>
|
||||||
{" updated."}
|
{" updated."}
|
||||||
</p>
|
</p>,
|
||||||
));
|
);
|
||||||
|
|
||||||
runInAction(() => {
|
runInAction(() => {
|
||||||
this.editingResource.firstDraft = currentValue;
|
this.editingResource.firstDraft = yaml.dump(currentVersion);
|
||||||
|
this.editingResource.resource = selfLink;
|
||||||
});
|
});
|
||||||
|
|
||||||
return result.response.toString();
|
// NOTE: This is required for `saveAndClose` to work correctly
|
||||||
|
return [];
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@ -56,12 +56,14 @@ const NonInjectedEditResource = observer(({
|
|||||||
<span>Namespace:</span>
|
<span>Namespace:</span>
|
||||||
<Badge label={model.namespace} />
|
<Badge label={model.namespace} />
|
||||||
</div>
|
</div>
|
||||||
)} />
|
)}
|
||||||
|
/>
|
||||||
<EditorPanel
|
<EditorPanel
|
||||||
tabId={tabId}
|
tabId={tabId}
|
||||||
value={model.configuration.value.get()}
|
value={model.configuration.value.get()}
|
||||||
onChange={model.configuration.onChange}
|
onChange={model.configuration.onChange}
|
||||||
onError={model.configuration.error.onChange} />
|
onError={model.configuration.error.onChange}
|
||||||
|
/>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,7 +4,7 @@
|
|||||||
*/
|
*/
|
||||||
import { getInjectable } from "@ogre-tools/injectable";
|
import { getInjectable } from "@ogre-tools/injectable";
|
||||||
import { kubeObjectDetailItemInjectionToken } from "../kube-object-detail-item-injection-token";
|
import { kubeObjectDetailItemInjectionToken } from "../kube-object-detail-item-injection-token";
|
||||||
import { HpaDetails } from "../../../+config-horizontal-pod-autoscalers";
|
import { HorizontalPodAutoscalerDetails } from "../../../+config-horizontal-pod-autoscalers";
|
||||||
import { computed } from "mobx";
|
import { computed } from "mobx";
|
||||||
import { kubeObjectMatchesToKindAndApiVersion } from "../kube-object-matches-to-kind-and-api-version";
|
import { kubeObjectMatchesToKindAndApiVersion } from "../kube-object-matches-to-kind-and-api-version";
|
||||||
import currentKubeObjectInDetailsInjectable from "../../current-kube-object-in-details.injectable";
|
import currentKubeObjectInDetailsInjectable from "../../current-kube-object-in-details.injectable";
|
||||||
@ -16,7 +16,7 @@ const horizontalPodAutoscalerDetailItemInjectable = getInjectable({
|
|||||||
const kubeObject = di.inject(currentKubeObjectInDetailsInjectable);
|
const kubeObject = di.inject(currentKubeObjectInDetailsInjectable);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
Component: HpaDetails,
|
Component: HorizontalPodAutoscalerDetails,
|
||||||
enabled: computed(() => isHorizontalPodAutoscaler(kubeObject.value.get()?.object)),
|
enabled: computed(() => isHorizontalPodAutoscaler(kubeObject.value.get()?.object)),
|
||||||
orderNumber: 10,
|
orderNumber: 10,
|
||||||
};
|
};
|
||||||
|
|||||||
@ -246,12 +246,10 @@ class NonInjectedMonacoEditor extends React.Component<MonacoEditorProps & Depend
|
|||||||
|
|
||||||
this.dispose.push(
|
this.dispose.push(
|
||||||
reaction(() => this.model, this.onModelChange),
|
reaction(() => this.model, this.onModelChange),
|
||||||
reaction(() => this.theme, theme => {
|
reaction(() => this.theme, editor.setTheme),
|
||||||
if (theme) {
|
reaction(() => this.props.value, value => this.setValue(value), {
|
||||||
editor.setTheme(theme);
|
fireImmediately: true,
|
||||||
}
|
|
||||||
}),
|
}),
|
||||||
reaction(() => this.props.value, value => this.setValue(value)),
|
|
||||||
reaction(() => this.options, opts => this.editor.updateOptions(opts)),
|
reaction(() => this.options, opts => this.editor.updateOptions(opts)),
|
||||||
|
|
||||||
() => onDidLayoutChangeDisposer.dispose(),
|
() => onDidLayoutChangeDisposer.dispose(),
|
||||||
|
|||||||
@ -30,7 +30,7 @@ const createExtensionInstanceInjectable = getInjectable({
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (ExtensionClass, extension) => {
|
return (ExtensionClass, extension) => {
|
||||||
const instance = new ExtensionClass(extension) as LensRendererExtension;
|
const instance = new ExtensionClass(extension as any) as LensRendererExtension;
|
||||||
|
|
||||||
(instance as Writable<LensRendererExtension>)[lensExtensionDependencies] = deps;
|
(instance as Writable<LensRendererExtension>)[lensExtensionDependencies] = deps;
|
||||||
|
|
||||||
|
|||||||
@ -18,9 +18,7 @@ export const applicationInformationFakeInjectable = getInjectable({
|
|||||||
bundledKubectlVersion: "1.23.3",
|
bundledKubectlVersion: "1.23.3",
|
||||||
bundledHelmVersion: "3.7.2",
|
bundledHelmVersion: "3.7.2",
|
||||||
sentryDsn: "",
|
sentryDsn: "",
|
||||||
contentSecurityPolicy:
|
contentSecurityPolicy: "script-src 'unsafe-eval' 'self'; frame-src http://*.localhost:*/; img-src * data:",
|
||||||
"script-src 'unsafe-eval' 'self'; frame-src http://*.localhost:*/; img-src * data:",
|
|
||||||
|
|
||||||
welcomeRoute: "/welcome",
|
welcomeRoute: "/welcome",
|
||||||
copyright: "some-copyright-information",
|
copyright: "some-copyright-information",
|
||||||
description: "some-descriptive-text",
|
description: "some-descriptive-text",
|
||||||
@ -3,12 +3,12 @@
|
|||||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||||
*/
|
*/
|
||||||
import { getInjectable } from "@ogre-tools/injectable";
|
import { getInjectable } from "@ogre-tools/injectable";
|
||||||
import { nodeEnvInjectionToken } from "./node-env-injection-token";
|
import { nodeEnvInjectionToken } from "../main/library";
|
||||||
|
|
||||||
const nodeEnvFakeInjectable = getInjectable({
|
const nodeEnvForTestingEnvInjectable = getInjectable({
|
||||||
id: "node-env-fake",
|
id: "node-env-for-testing-env",
|
||||||
instantiate: () => "production",
|
instantiate: () => "production",
|
||||||
injectionToken: nodeEnvInjectionToken,
|
injectionToken: nodeEnvInjectionToken,
|
||||||
});
|
});
|
||||||
|
|
||||||
export default nodeEnvFakeInjectable;
|
export default nodeEnvForTestingEnvInjectable;
|
||||||
@ -10,7 +10,6 @@ import ForkTsCheckerPlugin from "fork-ts-checker-webpack-plugin";
|
|||||||
import { iconsAndImagesWebpackRules } from "./renderer";
|
import { iconsAndImagesWebpackRules } from "./renderer";
|
||||||
import { DefinePlugin } from "webpack";
|
import { DefinePlugin } from "webpack";
|
||||||
import { buildDir, isDevelopment } from "./vars";
|
import { buildDir, isDevelopment } from "./vars";
|
||||||
import { platform } from "process";
|
|
||||||
|
|
||||||
const webpackLensMain = (): webpack.Configuration => {
|
const webpackLensMain = (): webpack.Configuration => {
|
||||||
return {
|
return {
|
||||||
@ -67,8 +66,8 @@ const webpackLensMain = (): webpack.Configuration => {
|
|||||||
},
|
},
|
||||||
plugins: [
|
plugins: [
|
||||||
new DefinePlugin({
|
new DefinePlugin({
|
||||||
CONTEXT_MATCHER_FOR_NON_FEATURES: `/\\.injectable(\\.${platform})?\\.tsx?$/`,
|
CONTEXT_MATCHER_FOR_NON_FEATURES: `/\\.injectable\\.tsx?$/`,
|
||||||
CONTEXT_MATCHER_FOR_FEATURES: `/\\/(main|common)\\/.+\\.injectable(\\.${platform})?\\.tsx?$/`,
|
CONTEXT_MATCHER_FOR_FEATURES: `/\\/(main|common)\\/.+\\.injectable\\.tsx?$/`,
|
||||||
}),
|
}),
|
||||||
new ForkTsCheckerPlugin({
|
new ForkTsCheckerPlugin({
|
||||||
typescript: {
|
typescript: {
|
||||||
|
|||||||
@ -12,7 +12,6 @@ import type { WebpackPluginInstance } from "webpack";
|
|||||||
import { optimize, DefinePlugin } from "webpack";
|
import { optimize, DefinePlugin } from "webpack";
|
||||||
import nodeExternals from "webpack-node-externals";
|
import nodeExternals from "webpack-node-externals";
|
||||||
import { isDevelopment, buildDir, sassCommonVars } from "./vars";
|
import { isDevelopment, buildDir, sassCommonVars } from "./vars";
|
||||||
import { platform } from "process";
|
|
||||||
|
|
||||||
export function webpackLensRenderer(): webpack.Configuration {
|
export function webpackLensRenderer(): webpack.Configuration {
|
||||||
return {
|
return {
|
||||||
@ -84,8 +83,8 @@ export function webpackLensRenderer(): webpack.Configuration {
|
|||||||
|
|
||||||
plugins: [
|
plugins: [
|
||||||
new DefinePlugin({
|
new DefinePlugin({
|
||||||
CONTEXT_MATCHER_FOR_NON_FEATURES: `/\\.injectable(\\.${platform})?\\.tsx?$/`,
|
CONTEXT_MATCHER_FOR_NON_FEATURES: `/\\.injectable\\.tsx?$/`,
|
||||||
CONTEXT_MATCHER_FOR_FEATURES: `/\\/(renderer|common)\\/.+\\.injectable(\\.${platform})?\\.tsx?$/`,
|
CONTEXT_MATCHER_FOR_FEATURES: `/\\/(renderer|common)\\/.+\\.injectable\\.tsx?$/`,
|
||||||
}),
|
}),
|
||||||
new ForkTsCheckerPlugin({}),
|
new ForkTsCheckerPlugin({}),
|
||||||
|
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"printWidth": 100,
|
"printWidth": 120,
|
||||||
"tabWidth": 2,
|
"tabWidth": 2,
|
||||||
"useTabs": false,
|
"useTabs": false,
|
||||||
"semi": true,
|
"semi": true,
|
||||||
|
|||||||
@ -0,0 +1,14 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||||
|
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||||
|
*/
|
||||||
|
import { lensBuildEnvironmentInjectionToken } from "@k8slens/application";
|
||||||
|
import { getInjectable } from "@ogre-tools/injectable";
|
||||||
|
|
||||||
|
const lensBuildEnvironmentInjectable = getInjectable({
|
||||||
|
id: "lens-build-environment",
|
||||||
|
instantiate: () => "unknown",
|
||||||
|
injectionToken: lensBuildEnvironmentInjectionToken,
|
||||||
|
});
|
||||||
|
|
||||||
|
export default lensBuildEnvironmentInjectable;
|
||||||
@ -50,8 +50,8 @@ const main: webpack.Configuration = ({
|
|||||||
},
|
},
|
||||||
plugins: [
|
plugins: [
|
||||||
new DefinePlugin({
|
new DefinePlugin({
|
||||||
CONTEXT_MATCHER_FOR_NON_FEATURES: `/\\.injectable(\\.${platform})?\\.tsx?$/`,
|
CONTEXT_MATCHER_FOR_NON_FEATURES: `/\\.injectable\\.tsx?$/`,
|
||||||
CONTEXT_MATCHER_FOR_FEATURES: `/\\/(renderer|common)\\/.+\\.injectable(\\.${platform})?\\.tsx?$/`,
|
CONTEXT_MATCHER_FOR_FEATURES: `/\\/(renderer|common)\\/.+\\.injectable\\.tsx?$/`,
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|||||||
@ -81,8 +81,8 @@
|
|||||||
|
|
||||||
plugins: [
|
plugins: [
|
||||||
new DefinePlugin({
|
new DefinePlugin({
|
||||||
CONTEXT_MATCHER_FOR_NON_FEATURES: `/\\.injectable(\\.${platform})?\\.tsx?$/`,
|
CONTEXT_MATCHER_FOR_NON_FEATURES: `/\\.injectable\\.tsx?$/`,
|
||||||
CONTEXT_MATCHER_FOR_FEATURES: `/\\/(renderer|common)\\/.+\\.injectable(\\.${platform})?\\.tsx?$/`,
|
CONTEXT_MATCHER_FOR_FEATURES: `/\\/(renderer|common)\\/.+\\.injectable\\.tsx?$/`,
|
||||||
}),
|
}),
|
||||||
new ForkTsCheckerPlugin(),
|
new ForkTsCheckerPlugin(),
|
||||||
|
|
||||||
|
|||||||
@ -7,3 +7,5 @@ export { startApplicationInjectionToken } from "./src/start-application/start-ap
|
|||||||
|
|
||||||
export { applicationInformationToken } from "./src/application-information-token.no-coverage";
|
export { applicationInformationToken } from "./src/application-information-token.no-coverage";
|
||||||
export type { ApplicationInformation } from "./src/application-information-token.no-coverage";
|
export type { ApplicationInformation } from "./src/application-information-token.no-coverage";
|
||||||
|
|
||||||
|
export { lensBuildEnvironmentInjectionToken } from "./src/environment-token";
|
||||||
|
|||||||
@ -0,0 +1,22 @@
|
|||||||
|
import { createContainer, DiContainer, getInjectable } from "@ogre-tools/injectable";
|
||||||
|
import { lensBuildEnvironmentInjectionToken } from "./environment-token";
|
||||||
|
|
||||||
|
describe("environment-token coverage tests", () => {
|
||||||
|
let di: DiContainer;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
di = createContainer("irrelevant");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should be able to specify a build environment", () => {
|
||||||
|
di.register(
|
||||||
|
getInjectable({
|
||||||
|
id: "some-id",
|
||||||
|
instantiate: () => "some-value",
|
||||||
|
injectionToken: lensBuildEnvironmentInjectionToken,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(di.inject(lensBuildEnvironmentInjectionToken)).toBe("some-value");
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -0,0 +1,9 @@
|
|||||||
|
/**
|
||||||
|
* 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 lensBuildEnvironmentInjectionToken = getInjectionToken<string>({
|
||||||
|
id: "lens-build-environment-token",
|
||||||
|
});
|
||||||
@ -13,9 +13,7 @@ const startApplicationInjectable = getInjectable({
|
|||||||
|
|
||||||
instantiate: (di): StartApplication => {
|
instantiate: (di): StartApplication => {
|
||||||
const runManyAsync = runManyFor(di);
|
const runManyAsync = runManyFor(di);
|
||||||
const beforeApplicationIsLoading = runManyAsync(
|
const beforeApplicationIsLoading = runManyAsync(timeSlots.beforeApplicationIsLoadingInjectionToken);
|
||||||
timeSlots.beforeApplicationIsLoadingInjectionToken,
|
|
||||||
);
|
|
||||||
const onLoadOfApplication = runManyAsync(timeSlots.onLoadOfApplicationInjectionToken);
|
const onLoadOfApplication = runManyAsync(timeSlots.onLoadOfApplicationInjectionToken);
|
||||||
const afterApplicationIsLoaded = runManyAsync(timeSlots.afterApplicationIsLoadedInjectionToken);
|
const afterApplicationIsLoaded = runManyAsync(timeSlots.afterApplicationIsLoadedInjectionToken);
|
||||||
|
|
||||||
|
|||||||
@ -1,9 +1,4 @@
|
|||||||
import {
|
import { DiContainer, getInjectable, instantiationDecoratorToken, lifecycleEnum } from "@ogre-tools/injectable";
|
||||||
DiContainer,
|
|
||||||
getInjectable,
|
|
||||||
instantiationDecoratorToken,
|
|
||||||
lifecycleEnum,
|
|
||||||
} from "@ogre-tools/injectable";
|
|
||||||
import { startApplicationInjectionToken } from "@k8slens/application";
|
import { startApplicationInjectionToken } from "@k8slens/application";
|
||||||
import whenAppIsReadyInjectable from "./when-app-is-ready.injectable";
|
import whenAppIsReadyInjectable from "./when-app-is-ready.injectable";
|
||||||
import { beforeAnythingInjectionToken, beforeElectronIsReadyInjectionToken } from "./time-slots";
|
import { beforeAnythingInjectionToken, beforeElectronIsReadyInjectionToken } from "./time-slots";
|
||||||
|
|||||||
@ -1,10 +1,7 @@
|
|||||||
import { createContainer, DiContainer, getInjectable } from "@ogre-tools/injectable";
|
import { createContainer, DiContainer, getInjectable } from "@ogre-tools/injectable";
|
||||||
import { registerFeature } from "@k8slens/feature-core";
|
import { registerFeature } from "@k8slens/feature-core";
|
||||||
import { applicationFeatureForElectronMain } from "./feature";
|
import { applicationFeatureForElectronMain } from "./feature";
|
||||||
import {
|
import { beforeApplicationIsLoadingInjectionToken, startApplicationInjectionToken } from "@k8slens/application";
|
||||||
beforeApplicationIsLoadingInjectionToken,
|
|
||||||
startApplicationInjectionToken,
|
|
||||||
} from "@k8slens/application";
|
|
||||||
import asyncFn, { AsyncFnMock } from "@async-fn/jest";
|
import asyncFn, { AsyncFnMock } from "@async-fn/jest";
|
||||||
import whenAppIsReadyInjectable from "./start-application/when-app-is-ready.injectable";
|
import whenAppIsReadyInjectable from "./start-application/when-app-is-ready.injectable";
|
||||||
import * as timeSlots from "./start-application/time-slots";
|
import * as timeSlots from "./start-application/time-slots";
|
||||||
|
|||||||
@ -1,13 +1,13 @@
|
|||||||
import { getInjectionToken } from "@ogre-tools/injectable";
|
import { getInjectionToken } from "@ogre-tools/injectable";
|
||||||
import type {
|
import type {
|
||||||
LensExtensionConstructor,
|
BundledLensExtensionConstructor,
|
||||||
LensExtensionManifest,
|
BundledLensExtensionManifest,
|
||||||
} from "./lens-extension";
|
} from "./lens-extension";
|
||||||
|
|
||||||
export interface BundledExtension {
|
export interface BundledExtension {
|
||||||
readonly manifest: LensExtensionManifest;
|
readonly manifest: BundledLensExtensionManifest;
|
||||||
main: () => LensExtensionConstructor | null;
|
main: () => Promise<BundledLensExtensionConstructor | null>;
|
||||||
renderer: () => LensExtensionConstructor | null;
|
renderer: () => Promise<BundledLensExtensionConstructor | null>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const bundledExtensionInjectionToken =
|
export const bundledExtensionInjectionToken =
|
||||||
|
|||||||
@ -1,26 +1,39 @@
|
|||||||
export type LensExtensionId = string;
|
export type LensExtensionId = string;
|
||||||
|
|
||||||
export type LensExtensionConstructor = new (
|
export type LensExtensionConstructor = new (
|
||||||
ext: InstalledExtension
|
ext: InstalledExtension
|
||||||
) => LegacyLensExtension;
|
) => LegacyLensExtension;
|
||||||
|
export type BundledLensExtensionConstructor = new (
|
||||||
|
ext: BundledInstalledExtension
|
||||||
|
) => LegacyLensExtension;
|
||||||
|
|
||||||
export interface InstalledExtension {
|
export interface BaseInstalledExtension {
|
||||||
id: LensExtensionId;
|
readonly id: LensExtensionId;
|
||||||
|
|
||||||
readonly manifest: LensExtensionManifest;
|
|
||||||
|
|
||||||
// Absolute path to the non-symlinked source folder,
|
// Absolute path to the non-symlinked source folder,
|
||||||
// e.g. "/Users/user/.k8slens/extensions/helloworld"
|
// e.g. "/Users/user/.k8slens/extensions/helloworld"
|
||||||
readonly absolutePath: string;
|
readonly absolutePath: string;
|
||||||
|
// Absolute to the symlinked package.json file
|
||||||
/**
|
|
||||||
* Absolute to the symlinked package.json file
|
|
||||||
*/
|
|
||||||
readonly manifestPath: string;
|
readonly manifestPath: string;
|
||||||
readonly isBundled: boolean;
|
}
|
||||||
|
|
||||||
|
export interface BundledInstalledExtension extends BaseInstalledExtension {
|
||||||
|
readonly manifest: BundledLensExtensionManifest;
|
||||||
|
readonly isBundled: true;
|
||||||
|
readonly isCompatible: true;
|
||||||
|
readonly isEnabled: true;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ExternalInstalledExtension extends BaseInstalledExtension {
|
||||||
|
readonly manifest: LensExtensionManifest;
|
||||||
|
readonly isBundled: false;
|
||||||
readonly isCompatible: boolean;
|
readonly isCompatible: boolean;
|
||||||
isEnabled: boolean;
|
isEnabled: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type InstalledExtension =
|
||||||
|
| BundledInstalledExtension
|
||||||
|
| ExternalInstalledExtension;
|
||||||
|
|
||||||
export interface LegacyLensExtension {
|
export interface LegacyLensExtension {
|
||||||
readonly id: LensExtensionId;
|
readonly id: LensExtensionId;
|
||||||
readonly manifest: LensExtensionManifest;
|
readonly manifest: LensExtensionManifest;
|
||||||
@ -38,22 +51,11 @@ export interface LegacyLensExtension {
|
|||||||
activate(): Promise<void>;
|
activate(): Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface LensExtensionManifest {
|
export interface BundledLensExtensionManifest {
|
||||||
name: string;
|
name: string;
|
||||||
version: string;
|
version: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
|
publishConfig?: Partial<Record<string, 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;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Specify extension name used for persisting data.
|
* Specify extension name used for persisting data.
|
||||||
@ -61,3 +63,17 @@ export interface LensExtensionManifest {
|
|||||||
*/
|
*/
|
||||||
storeName?: string;
|
storeName?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface LensExtensionManifest extends BundledLensExtensionManifest {
|
||||||
|
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
|
||||||
|
[x: string]: string | undefined;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|||||||
@ -2,9 +2,7 @@ import type { DiContainer } from "@ogre-tools/injectable";
|
|||||||
import type { Feature } from "./feature";
|
import type { Feature } from "./feature";
|
||||||
import { featureContextMapInjectable } from "./feature-context-map-injectable";
|
import { featureContextMapInjectable } from "./feature-context-map-injectable";
|
||||||
|
|
||||||
const getDependingFeaturesFor = (
|
const getDependingFeaturesFor = (featureContextMap: Map<Feature, { dependedBy: Map<Feature, number> }>) => {
|
||||||
featureContextMap: Map<Feature, { dependedBy: Map<Feature, number> }>,
|
|
||||||
) => {
|
|
||||||
const getDependingFeaturesForRecursion = (feature: Feature, atRoot = true): string[] => {
|
const getDependingFeaturesForRecursion = (feature: Feature, atRoot = true): string[] => {
|
||||||
const context = featureContextMap.get(feature);
|
const context = featureContextMap.get(feature);
|
||||||
|
|
||||||
@ -36,11 +34,9 @@ const deregisterFeatureRecursed = (di: DiContainer, feature: Feature, dependedBy
|
|||||||
const dependingFeatures = getDependingFeatures(feature);
|
const dependingFeatures = getDependingFeatures(feature);
|
||||||
|
|
||||||
if (!dependedBy && dependingFeatures.length) {
|
if (!dependedBy && dependingFeatures.length) {
|
||||||
throw new Error(
|
const names = dependingFeatures.join(", ");
|
||||||
`Tried to deregister Feature "${
|
|
||||||
feature.id
|
throw new Error(`Tried to deregister Feature "${feature.id}", but it is the dependency of Features "${names}"`);
|
||||||
}", but it is the dependency of Features "${dependingFeatures.join(", ")}"`,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (dependedBy) {
|
if (dependedBy) {
|
||||||
|
|||||||
@ -59,9 +59,7 @@ describe("feature-dependencies", () => {
|
|||||||
|
|
||||||
expect(() => {
|
expect(() => {
|
||||||
deregisterFeature(di, someDependencyFeature);
|
deregisterFeature(di, someDependencyFeature);
|
||||||
}).toThrow(
|
}).toThrow('Tried to deregister feature "some-dependency-feature", but it was not registered.');
|
||||||
'Tried to deregister feature "some-dependency-feature", but it was not registered.',
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("given the parent Feature is deregistered, when injecting an injectable from the dependency Feature, throws", () => {
|
it("given the parent Feature is deregistered, when injecting an injectable from the dependency Feature, throws", () => {
|
||||||
@ -104,9 +102,7 @@ describe("feature-dependencies", () => {
|
|||||||
it("when the first Feature is deregistered, throws", () => {
|
it("when the first Feature is deregistered, throws", () => {
|
||||||
expect(() => {
|
expect(() => {
|
||||||
deregisterFeature(di, someFeature1);
|
deregisterFeature(di, someFeature1);
|
||||||
}).toThrow(
|
}).toThrow('Tried to deregister Feature "some-feature-1", but it is the dependency of Features "some-feature-2"');
|
||||||
'Tried to deregister Feature "some-feature-1", but it is the dependency of Features "some-feature-2"',
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("given the second Feature is deregistered, when injecting an injectable from the first Feature, still does so", () => {
|
it("given the second Feature is deregistered, when injecting an injectable from the first Feature, still does so", () => {
|
||||||
@ -180,9 +176,7 @@ describe("feature-dependencies", () => {
|
|||||||
|
|
||||||
expect(() => {
|
expect(() => {
|
||||||
di.inject(someInjectableInDependencyFeature);
|
di.inject(someInjectableInDependencyFeature);
|
||||||
}).toThrow(
|
}).toThrow('Tried to inject non-registered injectable "irrelevant" -> "some-injectable-in-dependency-feature".');
|
||||||
'Tried to inject non-registered injectable "irrelevant" -> "some-injectable-in-dependency-feature".',
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -256,9 +250,7 @@ describe("feature-dependencies", () => {
|
|||||||
|
|
||||||
expect(() => {
|
expect(() => {
|
||||||
di.inject(someInjectableInDependencyFeature);
|
di.inject(someInjectableInDependencyFeature);
|
||||||
}).toThrow(
|
}).toThrow('Tried to inject non-registered injectable "irrelevant" -> "some-injectable-in-dependency-feature".');
|
||||||
'Tried to inject non-registered injectable "irrelevant" -> "some-injectable-in-dependency-feature".',
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -1,10 +1,7 @@
|
|||||||
import type { DiContainer } from "@ogre-tools/injectable";
|
import type { DiContainer } from "@ogre-tools/injectable";
|
||||||
import { getInjectable } from "@ogre-tools/injectable";
|
import { getInjectable } from "@ogre-tools/injectable";
|
||||||
import type { Feature } from "./feature";
|
import type { Feature } from "./feature";
|
||||||
import {
|
import { featureContextMapInjectable, featureContextMapInjectionToken } from "./feature-context-map-injectable";
|
||||||
featureContextMapInjectable,
|
|
||||||
featureContextMapInjectionToken,
|
|
||||||
} from "./feature-context-map-injectable";
|
|
||||||
|
|
||||||
const createFeatureContext = (feature: Feature, di: DiContainer) => {
|
const createFeatureContext = (feature: Feature, di: DiContainer) => {
|
||||||
const featureContextInjectable = getInjectable({
|
const featureContextInjectable = getInjectable({
|
||||||
@ -58,10 +55,9 @@ const registerFeatureRecursed = (di: DiContainer, feature: Feature, dependedBy?:
|
|||||||
|
|
||||||
if (dependedBy) {
|
if (dependedBy) {
|
||||||
const oldNumberOfDependents = featureContext.dependedBy.get(dependedBy) || 0;
|
const oldNumberOfDependents = featureContext.dependedBy.get(dependedBy) || 0;
|
||||||
|
const newNumberOfDependents = oldNumberOfDependents + 1;
|
||||||
|
|
||||||
const newNumberOfDependants = oldNumberOfDependents + 1;
|
featureContext.dependedBy.set(dependedBy, newNumberOfDependents);
|
||||||
|
|
||||||
featureContext.dependedBy.set(dependedBy, newNumberOfDependants);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!existingFeatureContext) {
|
if (!existingFeatureContext) {
|
||||||
|
|||||||
@ -21,15 +21,9 @@ export {
|
|||||||
getMessageChannelListenerInjectable,
|
getMessageChannelListenerInjectable,
|
||||||
} from "./message/message-channel-listener-injection-token";
|
} from "./message/message-channel-listener-injection-token";
|
||||||
|
|
||||||
export type {
|
export type { RequestChannel, RequestChannelHandler } from "./request/request-channel-listener-injection-token";
|
||||||
RequestChannel,
|
|
||||||
RequestChannelHandler,
|
|
||||||
} from "./request/request-channel-listener-injection-token";
|
|
||||||
|
|
||||||
export type {
|
export type { RequestFromChannel, ChannelRequester } from "./request/request-from-channel-injection-token";
|
||||||
RequestFromChannel,
|
|
||||||
ChannelRequester,
|
|
||||||
} from "./request/request-from-channel-injection-token";
|
|
||||||
|
|
||||||
export type { EnlistMessageChannelListener } from "./message/enlist-message-channel-listener-injection-token";
|
export type { EnlistMessageChannelListener } from "./message/enlist-message-channel-listener-injection-token";
|
||||||
export { enlistMessageChannelListenerInjectionToken } from "./message/enlist-message-channel-listener-injection-token";
|
export { enlistMessageChannelListenerInjectionToken } from "./message/enlist-message-channel-listener-injection-token";
|
||||||
|
|||||||
@ -21,9 +21,7 @@ export const listeningOfChannelsInjectionToken = getInjectionToken<ListeningOfCh
|
|||||||
id: "listening-of-channels-injection-token",
|
id: "listening-of-channels-injection-token",
|
||||||
});
|
});
|
||||||
|
|
||||||
const listening = <
|
const listening = <T extends { id: string; channel: MessageChannel<any> | RequestChannel<any, any> }>(
|
||||||
T extends { id: string; channel: MessageChannel<any> | RequestChannel<any, any> },
|
|
||||||
>(
|
|
||||||
channelListeners: IComputedValue<T[]>,
|
channelListeners: IComputedValue<T[]>,
|
||||||
enlistChannelListener: (listener: T) => () => void,
|
enlistChannelListener: (listener: T) => () => void,
|
||||||
getId: (listener: T) => string,
|
getId: (listener: T) => string,
|
||||||
@ -33,9 +31,7 @@ const listening = <
|
|||||||
const reactionDisposer = reaction(
|
const reactionDisposer = reaction(
|
||||||
() => channelListeners.get(),
|
() => channelListeners.get(),
|
||||||
(newValues, oldValues = []) => {
|
(newValues, oldValues = []) => {
|
||||||
const addedListeners = newValues.filter(
|
const addedListeners = newValues.filter((newValue) => !oldValues.some((oldValue) => oldValue.id === newValue.id));
|
||||||
(newValue) => !oldValues.some((oldValue) => oldValue.id === newValue.id),
|
|
||||||
);
|
|
||||||
|
|
||||||
const removedListeners = oldValues.filter(
|
const removedListeners = oldValues.filter(
|
||||||
(oldValue) => !newValues.some((newValue) => newValue.id === oldValue.id),
|
(oldValue) => !newValues.some((newValue) => newValue.id === oldValue.id),
|
||||||
@ -45,9 +41,7 @@ const listening = <
|
|||||||
const id = getId(listener);
|
const id = getId(listener);
|
||||||
|
|
||||||
if (listenerDisposers.has(id)) {
|
if (listenerDisposers.has(id)) {
|
||||||
throw new Error(
|
throw new Error(`Tried to add listener for channel "${listener.channel.id}" but listener already exists.`);
|
||||||
`Tried to add listener for channel "${listener.channel.id}" but listener already exists.`,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const disposer = enlistChannelListener(listener);
|
const disposer = enlistChannelListener(listener);
|
||||||
|
|||||||
@ -1,16 +1,10 @@
|
|||||||
import type { Disposer } from "@k8slens/utilities";
|
import type { Disposer } from "@k8slens/utilities";
|
||||||
import { getInjectionToken } from "@ogre-tools/injectable";
|
import { getInjectionToken } from "@ogre-tools/injectable";
|
||||||
|
|
||||||
import type {
|
import type { MessageChannel, MessageChannelListener } from "./message-channel-listener-injection-token";
|
||||||
MessageChannel,
|
|
||||||
MessageChannelListener,
|
|
||||||
} from "./message-channel-listener-injection-token";
|
|
||||||
|
|
||||||
export type EnlistMessageChannelListener = <T>(
|
export type EnlistMessageChannelListener = <T>(listener: MessageChannelListener<MessageChannel<T>>) => Disposer;
|
||||||
listener: MessageChannelListener<MessageChannel<T>>,
|
|
||||||
) => Disposer;
|
|
||||||
|
|
||||||
export const enlistMessageChannelListenerInjectionToken =
|
export const enlistMessageChannelListenerInjectionToken = getInjectionToken<EnlistMessageChannelListener>({
|
||||||
getInjectionToken<EnlistMessageChannelListener>({
|
id: "listening-to-a-message-channel",
|
||||||
id: "listening-to-a-message-channel",
|
});
|
||||||
});
|
|
||||||
|
|||||||
@ -18,9 +18,7 @@ export interface MessageChannelListener<Channel> {
|
|||||||
handler: MessageChannelHandler<Channel>;
|
handler: MessageChannelHandler<Channel>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const messageChannelListenerInjectionToken = getInjectionToken<
|
export const messageChannelListenerInjectionToken = getInjectionToken<MessageChannelListener<MessageChannel<unknown>>>({
|
||||||
MessageChannelListener<MessageChannel<unknown>>
|
|
||||||
>({
|
|
||||||
id: "message-channel-listener",
|
id: "message-channel-listener",
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -31,10 +29,7 @@ export interface GetMessageChannelListenerInfo<Channel extends MessageChannel<Me
|
|||||||
causesSideEffects?: boolean;
|
causesSideEffects?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getMessageChannelListenerInjectable = <
|
export const getMessageChannelListenerInjectable = <Channel extends MessageChannel<Message>, Message>(
|
||||||
Channel extends MessageChannel<Message>,
|
|
||||||
Message,
|
|
||||||
>(
|
|
||||||
info: GetMessageChannelListenerInfo<Channel, Message>,
|
info: GetMessageChannelListenerInfo<Channel, Message>,
|
||||||
) =>
|
) =>
|
||||||
getInjectable({
|
getInjectable({
|
||||||
|
|||||||
@ -1,16 +1,12 @@
|
|||||||
import type { Disposer } from "@k8slens/utilities/index";
|
import type { Disposer } from "@k8slens/utilities/index";
|
||||||
import { getInjectionToken } from "@ogre-tools/injectable";
|
import { getInjectionToken } from "@ogre-tools/injectable";
|
||||||
|
|
||||||
import type {
|
import type { RequestChannel, RequestChannelListener } from "./request-channel-listener-injection-token";
|
||||||
RequestChannel,
|
|
||||||
RequestChannelListener,
|
|
||||||
} from "./request-channel-listener-injection-token";
|
|
||||||
|
|
||||||
export type EnlistRequestChannelListener = <Request, Response>(
|
export type EnlistRequestChannelListener = <Request, Response>(
|
||||||
listener: RequestChannelListener<RequestChannel<Request, Response>>,
|
listener: RequestChannelListener<RequestChannel<Request, Response>>,
|
||||||
) => Disposer;
|
) => Disposer;
|
||||||
|
|
||||||
export const enlistRequestChannelListenerInjectionToken =
|
export const enlistRequestChannelListenerInjectionToken = getInjectionToken<EnlistRequestChannelListener>({
|
||||||
getInjectionToken<EnlistRequestChannelListener>({
|
id: "listening-to-a-request-channel",
|
||||||
id: "listening-to-a-request-channel",
|
});
|
||||||
});
|
|
||||||
|
|||||||
@ -1,7 +1,5 @@
|
|||||||
import type { RequestChannel } from "./request-channel-listener-injection-token";
|
import type { RequestChannel } from "./request-channel-listener-injection-token";
|
||||||
|
|
||||||
export const getRequestChannel = <Request, Response>(
|
export const getRequestChannel = <Request, Response>(id: string): RequestChannel<Request, Response> => ({
|
||||||
id: string,
|
|
||||||
): RequestChannel<Request, Response> => ({
|
|
||||||
id,
|
id,
|
||||||
});
|
});
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user