1
0
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:
Juho Heikka 2023-04-06 15:46:54 +03:00 committed by GitHub
commit e0040e1c1c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
116 changed files with 1089 additions and 1137 deletions

8
package-lock.json generated
View File

@ -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",

View File

@ -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"

View File

@ -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",

View File

@ -26,10 +26,7 @@ const NonInjectedKeyboardShortcutListener = ({
return <>{children}</>; return <>{children}</>;
}; };
export const KeyboardShortcutListener = withInjectables< export const KeyboardShortcutListener = withInjectables<Dependencies, KeyboardShortcutListenerProps>(
Dependencies,
KeyboardShortcutListenerProps
>(
NonInjectedKeyboardShortcutListener, NonInjectedKeyboardShortcutListener,
{ {

View File

@ -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]",

View File

@ -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",

View File

@ -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);
});
});

View File

@ -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())

View File

@ -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");
},
}
));
}, },
}); });

View File

@ -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) {

View File

@ -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",

View File

@ -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);

View File

@ -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;

View File

@ -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;

View File

@ -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",

View File

@ -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.
*/ */

View File

@ -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,

View File

@ -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;

View File

@ -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",

View File

@ -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);
} }

View File

@ -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() {

View File

@ -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",
}); });

View File

@ -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;

View File

@ -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;

View File

@ -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 () => []);

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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>

View File

@ -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",
},
}],
); );
}); });
}); });

View File

@ -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"
> >

View File

@ -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);
}, },
}); });

View File

@ -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}"),
}, },
}; };

View File

@ -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()) });
} }

View File

@ -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;

View File

@ -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,

View File

@ -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;

View File

@ -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);
}); });
}, },
}), }),

View File

@ -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;

View File

@ -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;
} }

View File

@ -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();

View File

@ -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),

View File

@ -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;

View File

@ -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";

View File

@ -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 {

View File

@ -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%");
}); });
}); });

View File

@ -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",

View File

@ -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;

View File

@ -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);

View File

@ -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;

View File

@ -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;

View File

@ -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;
}
};

View File

@ -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;

View File

@ -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;

View File

@ -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;
}
};

View File

@ -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),
}), }),
}); });

View File

@ -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),
}),
});

View File

@ -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),
}), }),
}); });

View File

@ -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>

View File

@ -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()]);
}, },
}); });

View File

@ -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 };
}; };
}, },

View File

@ -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 [];
}; };
} }

View File

@ -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}
/>
</> </>
) )
} }

View File

@ -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,
}; };

View File

@ -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(),

View File

@ -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;

View File

@ -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",

View File

@ -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;

View File

@ -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: {

View File

@ -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({}),

View File

@ -1,5 +1,5 @@
{ {
"printWidth": 100, "printWidth": 120,
"tabWidth": 2, "tabWidth": 2,
"useTabs": false, "useTabs": false,
"semi": true, "semi": true,

View File

@ -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;

View File

@ -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?$/`,
}), }),
], ],
}); });

View File

@ -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(),

View File

@ -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";

View File

@ -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");
});
});

View File

@ -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",
});

View File

@ -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);

View File

@ -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";

View File

@ -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";

View File

@ -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 =

View File

@ -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;
};
}

View File

@ -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) {

View File

@ -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".',
);
}); });
}); });
}); });

View File

@ -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) {

View File

@ -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";

View File

@ -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);

View File

@ -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", });
});

View File

@ -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({

View File

@ -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", });
});

View File

@ -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