Merge branch 'master' into issue-3498

Signed-off-by: Sebastian Malton <sebastian@malton.name>
This commit is contained in:
Sebastian Malton committed 2023-02-15 12:18:13 -05:00
commit 8cce15a002
85 files changed
+9640 -5855

No files matched your search

+10 -13
View File
@@ -3,7 +3,7 @@
"productName": "",
"description": "Lens Desktop Core",
"homepage": "https://github.com/lensapp/lens",
"version": "6.4.0-beta.10",
"version": "6.4.0-beta.13",
"repository": {
"type": "git",
"url": "git+https://github.com/lensapp/lens.git"
@@ -125,9 +125,9 @@
},
"dependencies": {
"@astronautlabs/jsonpath": "^1.1.0",
"@hapi/call": "^9.0.0",
"@hapi/call": "^9.0.1",
"@hapi/subtext": "^7.0.4",
"@k8slens/node-fetch": "^6.4.0-beta.10",
"@k8slens/node-fetch": "^6.4.0-beta.13",
"@kubernetes/client-node": "^0.18.1",
"@material-ui/styles": "^4.11.5",
"@ogre-tools/fp": "^12.0.1",
@@ -200,10 +200,9 @@
"@material-ui/core": "^4.12.3",
"@material-ui/icons": "^4.11.2",
"@material-ui/lab": "^4.0.0-alpha.60",
"@pmmmwh/react-refresh-webpack-plugin": "^0.5.10",
"@sentry/types": "^6.19.7",
"@swc/cli": "^0.1.59",
"@swc/core": "^1.3.31",
"@swc/cli": "^0.1.61",
"@swc/core": "^1.3.35",
"@swc/jest": "^0.2.24",
"@testing-library/dom": "^7.31.2",
"@testing-library/jest-dom": "^5.16.5",
@@ -221,7 +220,6 @@
"@types/glob-to-regexp": "^0.4.1",
"@types/hapi__call": "^9.0.0",
"@types/hapi__subtext": "^7.0.0",
"@types/html-webpack-plugin": "^3.2.6",
"@types/http-proxy": "^1.17.9",
"@types/jest": "^28.1.6",
"@types/js-yaml": "^4.0.5",
@@ -254,8 +252,8 @@
"@types/webpack-dev-server": "^4.7.2",
"@types/webpack-env": "^1.18.0",
"@types/webpack-node-externals": "^2.5.3",
"@typescript-eslint/eslint-plugin": "^5.49.0",
"@typescript-eslint/parser": "^5.49.0",
"@typescript-eslint/eslint-plugin": "^5.52.0",
"@typescript-eslint/parser": "^5.52.0",
"adr": "^1.4.3",
"ansi_up": "^5.1.0",
"chalk": "^4.1.2",
@@ -267,11 +265,11 @@
"cross-env": "^7.0.3",
"css-loader": "^6.7.3",
"deepdash": "^5.3.9",
"dompurify": "^2.4.3",
"dompurify": "^2.4.4",
"electron": "^19.1.9",
"electron-builder": "^23.6.0",
"electron-notarize": "^0.3.0",
"esbuild": "^0.17.5",
"esbuild": "^0.17.7",
"esbuild-loader": "^2.21.0",
"eslint": "^8.33.0",
"eslint-import-resolver-typescript": "^3.5.3",
@@ -281,7 +279,6 @@
"eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-unused-imports": "^2.0.0",
"fork-ts-checker-webpack-plugin": "^7.3.0",
"html-webpack-plugin": "^5.5.0",
"identity-obj-proxy": "^3.0.0",
"ignore-loader": "^0.1.2",
"include-media": "^1.4.9",
@@ -313,7 +310,7 @@
"react-table": "^7.8.0",
"react-window": "^1.8.8",
"rimraf": "^4.1.2",
"sass": "^1.57.1",
"sass": "^1.58.0",
"sass-loader": "^12.6.0",
"style-loader": "^3.3.1",
"tailwindcss": "^3.2.4",
@@ -33,6 +33,20 @@ export class Namespace extends KubeObject<
getStatus() {
return this.status?.phase ?? "-";
}
isSubnamespace() {
return this.getAnnotations().find(annotation => annotation.includes("hnc.x-k8s.io/subnamespace-of"));
}
isChildOf(parentName: string) {
return this.getLabels().find(label => label === `${parentName}.tree.hnc.x-k8s.io/depth=1`);
}
isControlledByHNC() {
const hierarchicalNamesaceControllerLabel = "hnc.x-k8s.io/included-namespace=true";
return this.getLabels().find(label => label === hierarchicalNamesaceControllerLabel);
}
}
export class NamespaceApi extends KubeApi<Namespace> {
-2
View File
@@ -7,5 +7,3 @@
export { applicationInformationToken } from "./vars/application-information-token";
export type { ApplicationInformation } from "./vars/application-information-token";
export { bundledExtensionInjectionToken } from "../extensions/extension-discovery/bundled-extension-token";
export * as extensionApi from "../extensions/common-api";
@@ -3,20 +3,13 @@
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import { createLogger, format } from "winston";
import type { Logger } from "./logger";
import { loggerTransportInjectionToken } from "./logger/transports";
import winstonLoggerInjectable from "./winston-logger.injectable";
const loggerInjectable = getInjectable({
id: "logger",
instantiate: (di): Logger => {
const baseLogger = createLogger({
format: format.combine(
format.splat(),
format.simple(),
),
transports: di.injectMany(loggerTransportInjectionToken),
});
const baseLogger = di.inject(winstonLoggerInjectable);
return {
debug: (message, ...data) => baseLogger.debug(message, ...data),
@@ -23,7 +23,7 @@ export const defaultTerminalConfig: TerminalConfig = {
fontFamily: defaultTerminalFontFamily,
};
interface BaseEditorConfiguration extends Required<Pick<editor.IStandaloneEditorConstructionOptions, "minimap" | "tabSize" | "fontSize" | "fontFamily">> {
export interface BaseEditorConfiguration extends Required<Pick<editor.IStandaloneEditorConstructionOptions, "minimap" | "tabSize" | "fontSize" | "fontFamily">> {
lineNumbers: NonNullable<Exclude<editor.IStandaloneEditorConstructionOptions["lineNumbers"], Function>>;
}
@@ -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 { createLogger, format } from "winston";
import { loggerTransportInjectionToken } from "./logger/transports";
const winstonLoggerInjectable = getInjectable({
id: "winston-logger",
instantiate: (di) => createLogger({
format: format.combine(
format.splat(),
format.simple(),
),
transports: di.injectMany(loggerTransportInjectionToken),
}),
});
export default winstonLoggerInjectable;
@@ -0,0 +1,103 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { runInAction } from "mobx";
import type { ApplicationBuilder } from "../../renderer/components/test-utils/get-application-builder";
import { getApplicationBuilder } from "../../renderer/components/test-utils/get-application-builder";
import type { FakeExtensionOptions } from "../../renderer/components/test-utils/get-extension-fake";
import getHashInjectable from "../../extensions/extension-loader/file-system-provisioner-store/get-hash.injectable";
import fsInjectable from "../../common/fs/fs.injectable";
describe("configurable directories for extension files", () => {
let builder: ApplicationBuilder;
beforeEach(async () => {
builder = getApplicationBuilder();
builder.beforeApplicationStart(
(mainDi) => {
runInAction(() => {
mainDi.override(getHashInjectable, () => x => x);
});
},
);
await builder.startHidden();
const window = builder.applicationWindow.create("some-window-id");
await window.start();
});
describe("when extension with a specific store name is enabled", () => {
let testExtensionOptions: FakeExtensionOptions;
beforeEach(() => {
testExtensionOptions = {
id: "some-extension",
name: "some-extension-name",
mainOptions: {
manifest: {
name: "irrelevant",
storeName: "some-specific-store-name",
version: "0",
engines: {
lens: "0",
},
},
},
};
builder.extensions.enable(testExtensionOptions);
});
it("creates extension directory for specific store name", async () => {
const fs = builder.mainDi.inject(fsInjectable);
await builder.extensions.get("some-extension").main.getExtensionFileFolder();
const nonHashedExtensionDirectories = await fs.readdir("/some-directory-for-app-data/some-product-name/extension_data");
expect(nonHashedExtensionDirectories).toContain("some-specific-store-name");
});
});
describe("when extension with no specific store name is enabled", () => {
let testExtensionOptions: FakeExtensionOptions;
beforeEach(() => {
testExtensionOptions = {
id: "some-extension",
name: "some-extension-name",
mainOptions: {
manifest: {
name: "some-package-name",
storeName: undefined,
version: "0",
engines: {
lens: "0",
},
},
},
};
builder.extensions.enable(testExtensionOptions);
});
it("creates extension directory for package name", async () => {
const fs = builder.mainDi.inject(fsInjectable);
await builder.extensions.get("some-extension").main.getExtensionFileFolder();
const nonHashedExtensionDirectories = await fs.readdir("/some-directory-for-app-data/some-product-name/extension_data");
expect(nonHashedExtensionDirectories).toContain("some-package-name");
});
});
});
@@ -3,31 +3,21 @@
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
// APIs
import { App } from "./app";
import * as EventBus from "./event-bus";
import * as Store from "./stores";
import { Util } from "./utils";
import * as Catalog from "./catalog";
import * as Types from "./types";
import * as Proxy from "./proxy";
import loggerInjectable from "../../common/logger.injectable";
import { asLegacyGlobalForExtensionApi } from "../as-legacy-globals-for-extension-api/as-legacy-global-object-for-extension-api";
import type { Logger } from "../../common/logger";
import type { LensExtension, LensExtensionManifest } from "../lens-extension";
const logger = asLegacyGlobalForExtensionApi(loggerInjectable);
// APIs
export { App } from "./app";
export * as EventBus from "./event-bus";
export * as Store from "./stores";
export * as Util from "./utils";
export * as Catalog from "./catalog";
export * as Types from "./types";
export * as Proxy from "./proxy";
export {
App,
EventBus,
Catalog,
Store,
Types,
Util,
logger,
Proxy,
Logger,
LensExtension,
LensExtensionManifest,
};
export type { Logger } from "../../common/logger";
export type { LensExtension, LensExtensionManifest } from "../lens-extension";
export type { InstalledExtension } from "../extension-discovery/extension-discovery";
export type { PackageJson } from "type-fest";
export const logger = asLegacyGlobalForExtensionApi(loggerInjectable);
@@ -108,13 +108,16 @@ export type { CreateKubeApiForLocalClusterConfig as ILocalKubeApiConfig } from "
export {
KubeObject,
KubeStatus,
type OwnerReference,
type KubeObjectMetadata,
type NamespaceScopedMetadata,
type ClusterScopedMetadata,
type BaseKubeJsonApiObjectMetadata,
type KubeJsonApiObjectMetadata,
type KubeStatusData,
} from "../../common/k8s-api/kube-object";
export type {
OwnerReference,
KubeObjectMetadata,
NamespaceScopedMetadata,
ClusterScopedMetadata,
BaseKubeJsonApiObjectMetadata,
KubeJsonApiObjectMetadata,
KubeStatusData,
KubeJsonApiDataFor,
} from "../../common/k8s-api/kube-object";
export type {
@@ -177,11 +180,11 @@ export abstract class KubeObjectStore<
}
}
export {
type JsonPatch,
type KubeObjectStoreLoadAllParams,
type KubeObjectStoreLoadingParams,
type KubeObjectStoreSubscribeParams,
export type {
JsonPatch,
KubeObjectStoreLoadAllParams,
KubeObjectStoreLoadingParams,
KubeObjectStoreSubscribeParams,
} from "../../common/k8s-api/kube-object.store";
/**
@@ -1,17 +0,0 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
export type { StatusBarRegistration } from "../../renderer/components/status-bar/status-bar-registration";
export type { KubeObjectMenuRegistration, KubeObjectMenuComponents } from "../../renderer/components/kube-object-menu/kube-object-menu-registration";
export type { AppPreferenceRegistration, AppPreferenceComponents } from "../../features/preferences/renderer/compliance-for-legacy-extension-api/app-preference-registration";
export type { KubeObjectDetailRegistration, KubeObjectDetailComponents } from "../../renderer/components/kube-object-details/kube-object-detail-registration";
export type { KubeObjectStatusRegistration } from "../../renderer/components/kube-object-status-icon/kube-object-status-registration";
export type { PageRegistration, RegisteredPage, PageParams, PageComponentProps, PageComponents, PageTarget } from "../../renderer/routes/page-registration";
export type { ClusterPageMenuRegistration, ClusterPageMenuComponents } from "../../renderer/components/layout/cluster-page-menu";
export type { ProtocolHandlerRegistration, RouteParams as ProtocolRouteParams, RouteHandler as ProtocolRouteHandler } from "../../common/protocol-handler/registration";
export type { CustomCategoryViewProps, CustomCategoryViewComponents, CustomCategoryViewRegistration } from "../../renderer/components/+catalog/custom-views";
export type { ShellEnvModifier, ShellEnvContext } from "../../main/shell-session/shell-env-modifier/shell-env-modifier-registration";
export type { KubeObjectContextMenuItem, KubeObjectOnContextMenuOpenContext, KubeObjectOnContextMenuOpen, KubeObjectHandlers, KubeObjectHandlerRegistration } from "../../renderer/kube-object/handler";
export type { TrayMenuRegistration } from "../../main/tray/tray-menu-registration";
export type { MenuRegistration } from "../../features/application-menu/main/menu-registration";
@@ -3,8 +3,21 @@
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
export type IpcMainInvokeEvent = Electron.IpcMainInvokeEvent;
export type IpcRendererEvent = Electron.IpcRendererEvent;
export type IpcMainEvent = Electron.IpcMainEvent;
export * from "./registrations";
export type { StatusBarRegistration } from "../../renderer/components/status-bar/status-bar-registration";
export type { KubeObjectMenuRegistration, KubeObjectMenuComponents } from "../../renderer/components/kube-object-menu/kube-object-menu-registration";
export type { AppPreferenceRegistration, AppPreferenceComponents } from "../../features/preferences/renderer/compliance-for-legacy-extension-api/app-preference-registration";
export type { KubeObjectDetailRegistration, KubeObjectDetailComponents } from "../../renderer/components/kube-object-details/kube-object-detail-registration";
export type { KubeObjectStatusRegistration } from "../../renderer/components/kube-object-status-icon/kube-object-status-registration";
export type { PageRegistration, RegisteredPage, PageParams, PageComponentProps, PageComponents, PageTarget } from "../../renderer/routes/page-registration";
export type { ClusterPageMenuRegistration, ClusterPageMenuComponents } from "../../renderer/components/layout/cluster-page-menu";
export type { ProtocolHandlerRegistration, RouteParams as ProtocolRouteParams, RouteHandler as ProtocolRouteHandler } from "../../common/protocol-handler/registration";
export type { CustomCategoryViewProps, CustomCategoryViewComponents, CustomCategoryViewRegistration } from "../../renderer/components/+catalog/custom-views";
export type { ShellEnvModifier, ShellEnvContext } from "../../main/shell-session/shell-env-modifier/shell-env-modifier-registration";
export type { KubeObjectContextMenuItem, KubeObjectOnContextMenuOpenContext, KubeObjectOnContextMenuOpen, KubeObjectHandlers, KubeObjectHandlerRegistration } from "../../renderer/kube-object/handler";
export type { TrayMenuRegistration } from "../../main/tray/tray-menu-registration";
export type { MenuRegistration } from "../../features/application-menu/main/menu-registration";
@@ -7,20 +7,23 @@ import openLinkInBrowserInjectable from "../../common/utils/open-link-in-browser
import buildVersionInjectable from "../../main/vars/build-version/build-version.injectable";
import { asLegacyGlobalFunctionForExtensionApi } from "../as-legacy-globals-for-extension-api/as-legacy-global-function-for-extension-api";
import { getLegacyGlobalDiForExtensionApi } from "../as-legacy-globals-for-extension-api/legacy-global-di-for-extension-api";
import { Singleton } from "../../common/utils";
import { prevDefault, stopPropagation } from "../../renderer/utils/prevDefault";
import { cssNames } from "../../renderer/utils/cssNames";
export const Util = {
Singleton,
prevDefault,
stopPropagation,
cssNames,
openExternal: asLegacyGlobalFunctionForExtensionApi(openLinkInBrowserInjectable),
openBrowser: asLegacyGlobalFunctionForExtensionApi(openLinkInBrowserInjectable),
getAppVersion: () => {
const di = getLegacyGlobalDiForExtensionApi();
export { Singleton } from "../../common/utils";
export { prevDefault, stopPropagation } from "../../renderer/utils/prevDefault";
return di.inject(buildVersionInjectable).get();
},
} as const;
export type { IClassName, IgnoredClassNames } from "../../renderer/utils";
export { cssNames } from "../../renderer/utils/cssNames";
export type { OpenLinkInBrowser } from "../../common/utils/open-link-in-browser.injectable";
export const openExternal = asLegacyGlobalFunctionForExtensionApi(openLinkInBrowserInjectable);
export const openBrowser = asLegacyGlobalFunctionForExtensionApi(openLinkInBrowserInjectable);
export const getAppVersion = () => {
const di = getLegacyGlobalDiForExtensionApi();
return di.inject(buildVersionInjectable).get();
};
export { disposer } from "../../common/utils";
export type { Disposer, Disposable, ExtendableDisposer } from "../../common/utils";
@@ -216,9 +216,6 @@ export class ExtensionDiscovery {
const extension = await this.loadExtensionFromFolder(absPath);
if (extension) {
// Remove a broken symlink left by a previous installation if it exists.
await this.dependencies.removePath(extension.manifestPath);
// Install dependencies for the new extension
await this.dependencies.installExtension(extension.absolutePath);
@@ -0,0 +1,43 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import type { ObservableMap } from "mobx";
import { getInjectable } from "@ogre-tools/injectable";
import { getOrInsertWithAsync } from "../../../common/utils";
import randomBytesInjectable from "../../../common/utils/random-bytes.injectable";
import joinPathsInjectable from "../../../common/path/join-paths.injectable";
import directoryForExtensionDataInjectable from "./directory-for-extension-data.injectable";
import ensureDirInjectable from "../../../common/fs/ensure-dir.injectable";
import getHashInjectable from "./get-hash.injectable";
export type EnsureHashedDirectoryForExtension = (extensionName: string, registeredExtensions: ObservableMap<string, string>) => Promise<string>;
const ensureHashedDirectoryForExtensionInjectable = getInjectable({
id: "ensure-hashed-directory-for-extension",
instantiate: (di): EnsureHashedDirectoryForExtension => {
const randomBytes = di.inject(randomBytesInjectable);
const joinPaths = di.inject(joinPathsInjectable);
const directoryForExtensionData = di.inject(directoryForExtensionDataInjectable);
const ensureDirectory = di.inject(ensureDirInjectable);
const getHash = di.inject(getHashInjectable);
return async (extensionName, registeredExtensions) => {
const dirPath = await getOrInsertWithAsync(registeredExtensions, extensionName, async () => {
const salt = (await randomBytes(32)).toString("hex");
const hashedName = getHash(`${extensionName}/${salt}`);
return joinPaths(directoryForExtensionData, hashedName);
});
await ensureDirectory(dirPath);
return dirPath;
};
},
});
export default ensureHashedDirectoryForExtensionInjectable;
@@ -4,10 +4,6 @@
*/
import { getInjectable } from "@ogre-tools/injectable";
import { FileSystemProvisionerStore } from "./file-system-provisioner-store";
import directoryForExtensionDataInjectable from "./directory-for-extension-data.injectable";
import ensureDirectoryInjectable from "../../../common/fs/ensure-dir.injectable";
import joinPathsInjectable from "../../../common/path/join-paths.injectable";
import randomBytesInjectable from "../../../common/utils/random-bytes.injectable";
import directoryForUserDataInjectable from "../../../common/app-paths/directory-for-user-data/directory-for-user-data.injectable";
import getConfigurationFileModelInjectable from "../../../common/get-configuration-file-model/get-configuration-file-model.injectable";
import loggerInjectable from "../../../common/logger.injectable";
@@ -18,15 +14,12 @@ import getBasenameOfPathInjectable from "../../../common/path/get-basename.injec
import { enlistMessageChannelListenerInjectionToken } from "../../../common/utils/channel/enlist-message-channel-listener-injection-token";
import fileSystemProvisionerStoreMigrationVersionInjectable from "./migration-version.injectable";
import { sendMessageToChannelInjectionToken } from "../../../common/utils/channel/message-to-channel-injection-token";
import ensureHashedDirectoryForExtensionInjectable from "./ensure-hashed-directory-for-extension.injectable";
const fileSystemProvisionerStoreInjectable = getInjectable({
id: "file-system-provisioner-store",
instantiate: (di) => new FileSystemProvisionerStore({
directoryForExtensionData: di.inject(directoryForExtensionDataInjectable),
ensureDirectory: di.inject(ensureDirectoryInjectable),
joinPaths: di.inject(joinPathsInjectable),
randomBytes: di.inject(randomBytesInjectable),
directoryForUserData: di.inject(directoryForUserDataInjectable),
getConfigurationFileModel: di.inject(getConfigurationFileModelInjectable),
logger: di.inject(loggerInjectable),
@@ -38,6 +31,7 @@ const fileSystemProvisionerStoreInjectable = getInjectable({
enlistMessageChannelListener: di.inject(enlistMessageChannelListenerInjectionToken),
shouldDisableSyncInListener: di.inject(shouldBaseStoreDisableSyncInIpcListenerInjectionToken),
sendMessageToChannel: di.inject(sendMessageToChannelInjectionToken),
ensureHashedDirectoryForExtension: di.inject(ensureHashedDirectoryForExtensionInjectable),
}),
});
@@ -3,25 +3,19 @@
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { SHA256 } from "crypto-js";
import { action, makeObservable, observable } from "mobx";
import type { BaseStoreDependencies } from "../../../common/base-store/base-store";
import { BaseStore } from "../../../common/base-store/base-store";
import type { LensExtensionId } from "../../lens-extension";
import { getOrInsertWithAsync, toJS } from "../../../common/utils";
import type { EnsureDirectory } from "../../../common/fs/ensure-dir.injectable";
import type { JoinPaths } from "../../../common/path/join-paths.injectable";
import type { RandomBytes } from "../../../common/utils/random-bytes.injectable";
import { toJS } from "../../../common/utils";
import type { EnsureHashedDirectoryForExtension } from "./ensure-hashed-directory-for-extension.injectable";
interface FSProvisionModel {
extensions: Record<string, string>; // extension names to paths
}
interface Dependencies extends BaseStoreDependencies {
readonly directoryForExtensionData: string;
ensureDirectory: EnsureDirectory;
joinPaths: JoinPaths;
randomBytes: RandomBytes;
ensureHashedDirectoryForExtension: EnsureHashedDirectoryForExtension;
}
export class FileSystemProvisionerStore extends BaseStore<FSProvisionModel> {
@@ -43,16 +37,7 @@ export class FileSystemProvisionerStore extends BaseStore<FSProvisionModel> {
* @returns path to the folder that the extension can safely write files to.
*/
async requestDirectory(extensionName: string): Promise<string> {
const dirPath = await getOrInsertWithAsync(this.registeredExtensions, extensionName, async () => {
const salt = (await this.dependencies.randomBytes(32)).toString("hex");
const hashedName = SHA256(`${extensionName}/${salt}`).toString();
return this.dependencies.joinPaths(this.dependencies.directoryForExtensionData, hashedName);
});
await this.dependencies.ensureDirectory(dirPath);
return dirPath;
return this.dependencies.ensureHashedDirectoryForExtension(extensionName, this.registeredExtensions);
}
@action
@@ -0,0 +1,15 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import { SHA256 } from "crypto-js";
const getHashInjectable = getInjectable({
id: "get-hash",
instantiate: () => (text: string) => SHA256(text).toString(),
});
export default getHashInjectable;
@@ -101,6 +101,6 @@ export abstract class ExtensionStore<T extends object> extends BaseStore<T> {
protected cwd() {
assert(this.extension, "must call this.load() first");
return path.join(super.cwd(), "extension-store", this.extension.name);
return path.join(super.cwd(), "extension-store", this.extension.storeName);
}
}
+15 -2
View File
@@ -8,7 +8,7 @@ import { action, computed, makeObservable, observable } from "mobx";
import type { PackageJson } from "type-fest";
import { disposer } from "../common/utils";
import type { LensExtensionDependencies } from "./lens-extension-set-dependencies";
import type { ProtocolHandlerRegistration } from "./common-api/registrations";
import type { ProtocolHandlerRegistration } from "../common/protocol-handler/registration";
export type LensExtensionId = string; // path to manifest (package.json)
export type LensExtensionConstructor = new (...args: ConstructorParameters<typeof LensExtension>) => LensExtension;
@@ -27,6 +27,10 @@ export interface LensExtensionManifest extends PackageJson {
npm?: string;
node?: string;
};
// Specify extension name used for persisting data.
// Useful if extension is renamed but the data should not be lost.
storeName?: string;
}
export const lensExtensionDependencies = Symbol("lens-extension-dependencies");
@@ -62,7 +66,10 @@ export class LensExtension<
constructor({ id, manifest, manifestPath, isBundled }: InstalledExtension) {
makeObservable(this);
// id is the name of the manifest
this.id = id;
this.manifest = manifest;
this.manifestPath = manifestPath;
this.isBundled = !!isBundled;
@@ -80,6 +87,11 @@ export class LensExtension<
return this.manifest.description;
}
// Name of extension for persisting data
get storeName() {
return this.manifest.storeName || this.name;
}
/**
* @ignore
*/
@@ -93,7 +105,8 @@ export class LensExtension<
* folder name.
*/
async getExtensionFileFolder(): Promise<string> {
return this[lensExtensionDependencies].fileSystemProvisionerStore.requestDirectory(this.id);
// storeName is read from the manifest and has a fallback to the manifest name, which equals id
return this[lensExtensionDependencies].fileSystemProvisionerStore.requestDirectory(this.storeName);
}
@action
@@ -29,7 +29,8 @@ import type { KubeObjectDetailRegistration } from "../renderer/components/kube-o
import type { ClusterFrameChildComponent } from "../renderer/frames/cluster-frame/cluster-frame-child-component-injection-token";
import type { EntitySettingRegistration } from "../renderer/components/+entity-settings/extension-registrator.injectable";
import type { CatalogEntityDetailRegistration } from "../renderer/components/+catalog/entity-details/token";
import type { ClusterPageMenuRegistration, PageRegistration } from "./common-api/registrations";
import type { PageRegistration } from "../renderer/routes/page-registration";
import type { ClusterPageMenuRegistration } from "../renderer/components/layout/cluster-page-menu";
export class LensRendererExtension extends LensExtension<LensRendererExtensionDependencies> {
globalPages: PageRegistration[] = [];
@@ -9797,11 +9797,11 @@ exports[`cluster/namespaces - edit namespace from new tab when navigating to nam
class="footer"
>
<div
class="Dock"
class="Dock isOpen"
tabindex="-1"
>
<div
class="ResizingAnchor vertical leading disabled"
class="ResizingAnchor vertical leading"
/>
<div
class="tabs-container flex align-center"
@@ -9812,10 +9812,63 @@ exports[`cluster/namespaces - edit namespace from new tab when navigating to nam
>
<div
class="Tabs tabs"
/>
>
<div
class="Tab flex gaps align-center DockTab active"
data-testid="dock-tab-for-some-first-tab-id"
id="tab-some-first-tab-id"
role="tab"
tabindex="0"
>
<i
class="Icon material focusable small"
>
<span
class="icon"
data-icon-name="edit"
>
edit
</span>
</i>
<div
class="label"
>
<div
class="flex align-center"
>
<span
class="title"
>
Namespace: some-name
</span>
<div
class="close"
>
<i
class="Icon material interactive focusable small"
data-testid="dock-tab-close-for-some-first-tab-id"
tabindex="0"
>
<span
class="icon"
data-icon-name="close"
>
close
</span>
</i>
<div
data-testid="tooltip-content-for-dock-tab-close-for-some-first-tab-id"
>
Close ⌘+W
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div
class="toolbar flex gaps align-center box grow pl-0"
class="toolbar flex gaps align-center box grow"
>
<div
class="dock-menu box grow"
@@ -9836,6 +9889,118 @@ exports[`cluster/namespaces - edit namespace from new tab when navigating to nam
New tab
</div>
</div>
<i
class="Icon material interactive focusable"
tabindex="0"
>
<span
class="icon"
data-icon-name="fullscreen"
>
fullscreen
</span>
</i>
<div>
Fit to window
</div>
<i
class="Icon material interactive focusable"
tabindex="0"
>
<span
class="icon"
data-icon-name="keyboard_arrow_down"
>
keyboard_arrow_down
</span>
</i>
<div>
Minimize
</div>
</div>
</div>
<div
class="tab-content edit-resource"
data-testid="dock-tab-content-for-some-first-tab-id"
style="flex-basis: 300px;"
>
<div
class="EditResource flex column"
>
<div
class="InfoPanel flex gaps align-center"
>
<div
class="controls"
>
<div
class="resource-info flex gaps align-center"
>
<span>
Kind:
</span>
<div
class="badge"
>
Namespace
</div>
<span>
Name:
</span>
<div
class="badge"
>
some-name
</div>
<span>
Namespace:
</span>
<div
class="badge"
>
default
</div>
</div>
</div>
<div
class="flex gaps align-center"
/>
<button
class="Button plain"
data-testid="cancel-edit-resource-from-tab-for-some-first-tab-id"
type="button"
>
Cancel
</button>
<button
class="Button active outlined"
data-testid="save-edit-resource-from-tab-for-some-first-tab-id"
type="button"
>
Save
</button>
<button
class="Button primary active"
data-testid="save-and-close-edit-resource-from-tab-for-some-first-tab-id"
type="button"
>
Save & Close
</button>
</div>
<textarea
data-testid="monaco-editor-for-some-first-tab-id"
>
apiVersion: some-api-version
kind: Namespace
metadata:
uid: some-uid
name: some-name
resourceVersion: some-resource-version
selfLink: /apis/some-api-version/namespaces/some-uid
somePropertyToBeRemoved: some-value
somePropertyToBeChanged: some-old-value
</textarea>
</div>
</div>
</div>
@@ -401,8 +401,7 @@ metadata:
expect(rendered.baseElement).toMatchSnapshot();
});
// TODO: Not doable at the moment because info panel controls closing of the tab
xit("does not close the dock tab", () => {
it("does not close the dock tab", () => {
expect(
rendered.getByTestId("dock-tab-for-some-first-tab-id"),
).toBeInTheDocument();
@@ -443,6 +443,9 @@ exports[`extensions - navigation using application menu when navigating to exten
: you can drag and drop a tarball file to this area
</small>
</section>
<div
class="size-md"
/>
<div>
<div
class="Spinner singleColor center"
@@ -11,7 +11,7 @@ const apiBaseHostHeaderInjectable = getInjectable({
instantiate: (di) => {
const lensProxyPort = di.inject(lensProxyPortInjectable);
return `localhost:${lensProxyPort.get()}`;
return `lens.app:${lensProxyPort.get()}`;
},
injectionToken: apiBaseHostHeaderInjectionToken,
});
@@ -2,7 +2,6 @@
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import type { SelfSignedCert } from "selfsigned";
import { lensProxyCertificateChannel } from "../../common/certificate/lens-proxy-certificate-channel";
import { getRequestChannelListenerInjectable } from "../utils/channel/channel-listeners/listener-tokens";
import lensProxyCertificateInjectable from "../../common/certificate/lens-proxy-certificate.injectable";
@@ -10,7 +9,7 @@ import lensProxyCertificateInjectable from "../../common/certificate/lens-proxy-
const lensProxyCertificateRequestHandlerInjectable = getRequestChannelListenerInjectable({
channel: lensProxyCertificateChannel,
handler: (di) => {
const lensProxyCertificate = di.inject(lensProxyCertificateInjectable).get() as SelfSignedCert;
const lensProxyCertificate = di.inject(lensProxyCertificateInjectable).get();
return () => ({
cert: lensProxyCertificate.cert,
+2 -1
View File
@@ -10,5 +10,6 @@ export { beforeElectronIsReadyInjectionToken } from "./start-main-application/ru
export { onLoadOfApplicationInjectionToken } from "./start-main-application/runnable-tokens/on-load-of-application-injection-token";
export { createApp } from "./create-app";
export * as Mobx from "mobx";
export * as extensionApi from "../extensions/main-api";
export * as mainExtensionApi from "../extensions/main-api";
export * as commonExtensionApi from "../extensions/common-api";
export * as Pty from "node-pty";
@@ -27,6 +27,12 @@ export interface UrlSource {
}
export type ContentSource = RequireExactlyOne<FileSource & UrlSource>;
enum ChromiumNetError {
SUCCESS = 0,
FAILURE = 1,
RESULT_FROM_CHROMIUM,
}
export interface ElectronWindowConfiguration {
id: string;
title: string;
@@ -112,6 +118,15 @@ const createElectronWindowInjectable = getInjectable({
applicationWindowState.manage(browserWindow);
browserWindow.webContents.session.setCertificateVerifyProc((request, shouldBeTrusted) => {
const { certificate } = request;
const cert = new X509Certificate(certificate.data);
const shouldTrustCert = cert.raw.length === lensProxyX509Cert.raw.length
&& timingSafeEqual(cert.raw, lensProxyX509Cert.raw);
shouldBeTrusted(shouldTrustCert ? ChromiumNetError.SUCCESS : ChromiumNetError.RESULT_FROM_CHROMIUM);
});
browserWindow
.on("focus", () => {
configuration.onFocus?.();
@@ -126,13 +141,6 @@ const createElectronWindowInjectable = getInjectable({
.webContents.on("dom-ready", () => {
configuration.onDomReady?.();
})
.on("certificate-error", (event, url, error, certificate, shouldBeTrusted) => {
const cert = new X509Certificate(certificate.data);
const shouldTrustCert = cert.raw.length === lensProxyX509Cert.raw.length
&& timingSafeEqual(cert.raw, lensProxyX509Cert.raw);
shouldBeTrusted(shouldTrustCert);
})
.on("did-fail-load", (_event, code, desc) => {
logger.error(
`[CREATE-ELECTRON-WINDOW]: Failed to load window "${configuration.id}"`,
+3 -1
View File
@@ -54,7 +54,9 @@ export async function bootstrap(di: DiContainer) {
}
try {
await initializeApp(() => unmountComponentAtNode(rootElem));
await initializeApp(() => {
unmountComponentAtNode(rootElem);
});
} catch (error) {
console.error(`[BOOTSTRAP]: view initialization error: ${error}`, {
origin: location.href,
@@ -37,6 +37,7 @@ 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";
interface Dependencies {
userExtensions: IComputedValue<InstalledExtension[]>;
@@ -113,6 +114,8 @@ class NonInjectedExtensions extends React.Component<Dependencies> {
installPath={this.installPath}
/>
<Gutter size="md" />
<InstalledExtensions
extensions={userExtensions}
enable={this.props.enableExtension}
@@ -0,0 +1,937 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`<NamespaceTreeView /> collapses item by clicking minus button 1`] = `
<body>
<div>
<div
class="TreeView"
data-testid="namespace-tree-view"
>
<div
class="DrawerTitle title"
>
Tree View
</div>
<ul
aria-multiselectable="false"
class="MuiTreeView-root"
role="tree"
>
<li
aria-expanded="true"
class="MuiTreeItem-root Mui-expanded"
data-testid="namespace-levels-deep"
role="treeitem"
tabindex="0"
>
<div
class="MuiTreeItem-content"
>
<div
class="MuiTreeItem-iconContainer"
>
<svg
aria-hidden="true"
class="MuiSvgIcon-root"
data-testid="minus-square"
focusable="false"
style="width: 14px; height: 14px;"
viewBox="0 0 24 24"
>
<path
d="M22.047 22.074v0 0-20.147 0h-20.12v0 20.147 0h20.12zM22.047 24h-20.12q-.803 0-1.365-.562t-.562-1.365v-20.147q0-.776.562-1.351t1.365-.575h20.147q.776 0 1.351.575t.575 1.351v20.147q0 .803-.575 1.365t-1.378.562v0zM17.873 11.023h-11.826q-.375 0-.669.281t-.294.682v0q0 .401.294 .682t.669.281h11.826q.375 0 .669-.281t.294-.682v0q0-.401-.294-.682t-.669-.281z"
/>
</svg>
</div>
<div
class="MuiTypography-root MuiTreeItem-label label MuiTypography-body1"
>
levels-deep
</div>
</div>
<ul
class="MuiCollapse-root MuiTreeItem-group group MuiCollapse-entered"
role="group"
style="min-height: 0px;"
>
<div
class="MuiCollapse-wrapper"
>
<div
class="MuiCollapse-wrapperInner"
>
<li
class="MuiTreeItem-root Mui-expanded"
data-testid="namespace-level-deep-child-a"
role="treeitem"
tabindex="-1"
>
<div
class="MuiTreeItem-content"
>
<div
class="MuiTreeItem-iconContainer"
>
<div
style="opacity: 0.3;"
>
<svg
aria-hidden="true"
class="MuiSvgIcon-root"
data-testid="minus-square"
focusable="false"
style="width: 14px; height: 14px;"
viewBox="0 0 24 24"
>
<path
d="M22.047 22.074v0 0-20.147 0h-20.12v0 20.147 0h20.12zM22.047 24h-20.12q-.803 0-1.365-.562t-.562-1.365v-20.147q0-.776.562-1.351t1.365-.575h20.147q.776 0 1.351.575t.575 1.351v20.147q0 .803-.575 1.365t-1.378.562v0zM17.873 11.023h-11.826q-.375 0-.669.281t-.294.682v0q0 .401.294 .682t.669.281h11.826q.375 0 .669-.281t.294-.682v0q0-.401-.294-.682t-.669-.281z"
/>
</svg>
</div>
</div>
<div
class="MuiTypography-root MuiTreeItem-label label MuiTypography-body1"
>
level-deep-child-a
</div>
</div>
</li>
<li
aria-expanded="false"
class="MuiTreeItem-root"
data-testid="namespace-level-deep-child-b"
role="treeitem"
tabindex="-1"
>
<div
class="MuiTreeItem-content"
>
<div
class="MuiTreeItem-iconContainer"
>
<svg
aria-hidden="true"
class="MuiSvgIcon-root"
data-testid="plus-square"
focusable="false"
style="width: 14px; height: 14px;"
viewBox="0 0 24 24"
>
<path
d="M22.047 22.074v0 0-20.147 0h-20.12v0 20.147 0h20.12zM22.047 24h-20.12q-.803 0-1.365-.562t-.562-1.365v-20.147q0-.776.562-1.351t1.365-.575h20.147q.776 0 1.351.575t.575 1.351v20.147q0 .803-.575 1.365t-1.378.562v0zM17.873 12.977h-4.923v4.896q0 .401-.281.682t-.682.281v0q-.375 0-.669-.281t-.294-.682v-4.896h-4.923q-.401 0-.682-.294t-.281-.669v0q0-.401.281-.682t.682-.281h4.923v-4.896q0-.401.294-.682t.669-.281v0q.401 0 .682.281t.281.682v4.896h4.923q.401 0 .682.281t.281.682v0q0 .375-.281.669t-.682.294z"
/>
</svg>
</div>
<div
class="MuiTypography-root MuiTreeItem-label label MuiTypography-body1"
>
level-deep-child-b
</div>
</div>
<ul
class="MuiCollapse-root MuiTreeItem-group group"
role="group"
style="min-height: 0px; height: 0px; transition-duration: 300ms;"
>
<div
class="MuiCollapse-wrapper"
>
<div
class="MuiCollapse-wrapperInner"
>
<li
class="MuiTreeItem-root Mui-expanded"
data-testid="namespace-level-deep-subchild-a"
role="treeitem"
tabindex="-1"
>
<div
class="MuiTreeItem-content"
>
<div
class="MuiTreeItem-iconContainer"
>
<div
style="opacity: 0.3;"
>
<svg
aria-hidden="true"
class="MuiSvgIcon-root"
data-testid="minus-square"
focusable="false"
style="width: 14px; height: 14px;"
viewBox="0 0 24 24"
>
<path
d="M22.047 22.074v0 0-20.147 0h-20.12v0 20.147 0h20.12zM22.047 24h-20.12q-.803 0-1.365-.562t-.562-1.365v-20.147q0-.776.562-1.351t1.365-.575h20.147q.776 0 1.351.575t.575 1.351v20.147q0 .803-.575 1.365t-1.378.562v0zM17.873 11.023h-11.826q-.375 0-.669.281t-.294.682v0q0 .401.294 .682t.669.281h11.826q.375 0 .669-.281t.294-.682v0q0-.401-.294-.682t-.669-.281z"
/>
</svg>
</div>
</div>
<div
class="MuiTypography-root MuiTreeItem-label label MuiTypography-body1"
>
level-deep-subchild-a
</div>
</div>
</li>
</div>
</div>
</ul>
</li>
</div>
</div>
</ul>
</li>
</ul>
</div>
</div>
</body>
`;
exports[`<NamespaceTreeView /> expands item by clicking plus button 1`] = `
<body>
<div>
<div
class="TreeView"
data-testid="namespace-tree-view"
>
<div
class="DrawerTitle title"
>
Tree View
</div>
<ul
aria-multiselectable="false"
class="MuiTreeView-root"
role="tree"
>
<li
aria-expanded="true"
class="MuiTreeItem-root Mui-expanded"
data-testid="namespace-levels-deep"
role="treeitem"
tabindex="0"
>
<div
class="MuiTreeItem-content"
>
<div
class="MuiTreeItem-iconContainer"
>
<svg
aria-hidden="true"
class="MuiSvgIcon-root"
data-testid="minus-square"
focusable="false"
style="width: 14px; height: 14px;"
viewBox="0 0 24 24"
>
<path
d="M22.047 22.074v0 0-20.147 0h-20.12v0 20.147 0h20.12zM22.047 24h-20.12q-.803 0-1.365-.562t-.562-1.365v-20.147q0-.776.562-1.351t1.365-.575h20.147q.776 0 1.351.575t.575 1.351v20.147q0 .803-.575 1.365t-1.378.562v0zM17.873 11.023h-11.826q-.375 0-.669.281t-.294.682v0q0 .401.294 .682t.669.281h11.826q.375 0 .669-.281t.294-.682v0q0-.401-.294-.682t-.669-.281z"
/>
</svg>
</div>
<div
class="MuiTypography-root MuiTreeItem-label label MuiTypography-body1"
>
levels-deep
</div>
</div>
<ul
class="MuiCollapse-root MuiTreeItem-group group MuiCollapse-entered"
role="group"
style="min-height: 0px;"
>
<div
class="MuiCollapse-wrapper"
>
<div
class="MuiCollapse-wrapperInner"
>
<li
class="MuiTreeItem-root Mui-expanded"
data-testid="namespace-level-deep-child-a"
role="treeitem"
tabindex="-1"
>
<div
class="MuiTreeItem-content"
>
<div
class="MuiTreeItem-iconContainer"
>
<div
style="opacity: 0.3;"
>
<svg
aria-hidden="true"
class="MuiSvgIcon-root"
data-testid="minus-square"
focusable="false"
style="width: 14px; height: 14px;"
viewBox="0 0 24 24"
>
<path
d="M22.047 22.074v0 0-20.147 0h-20.12v0 20.147 0h20.12zM22.047 24h-20.12q-.803 0-1.365-.562t-.562-1.365v-20.147q0-.776.562-1.351t1.365-.575h20.147q.776 0 1.351.575t.575 1.351v20.147q0 .803-.575 1.365t-1.378.562v0zM17.873 11.023h-11.826q-.375 0-.669.281t-.294.682v0q0 .401.294 .682t.669.281h11.826q.375 0 .669-.281t.294-.682v0q0-.401-.294-.682t-.669-.281z"
/>
</svg>
</div>
</div>
<div
class="MuiTypography-root MuiTreeItem-label label MuiTypography-body1"
>
level-deep-child-a
</div>
</div>
</li>
<li
aria-expanded="true"
class="MuiTreeItem-root Mui-expanded"
data-testid="namespace-level-deep-child-b"
role="treeitem"
tabindex="-1"
>
<div
class="MuiTreeItem-content"
>
<div
class="MuiTreeItem-iconContainer"
>
<svg
aria-hidden="true"
class="MuiSvgIcon-root"
data-testid="minus-square"
focusable="false"
style="width: 14px; height: 14px;"
viewBox="0 0 24 24"
>
<path
d="M22.047 22.074v0 0-20.147 0h-20.12v0 20.147 0h20.12zM22.047 24h-20.12q-.803 0-1.365-.562t-.562-1.365v-20.147q0-.776.562-1.351t1.365-.575h20.147q.776 0 1.351.575t.575 1.351v20.147q0 .803-.575 1.365t-1.378.562v0zM17.873 11.023h-11.826q-.375 0-.669.281t-.294.682v0q0 .401.294 .682t.669.281h11.826q.375 0 .669-.281t.294-.682v0q0-.401-.294-.682t-.669-.281z"
/>
</svg>
</div>
<div
class="MuiTypography-root MuiTreeItem-label label MuiTypography-body1"
>
level-deep-child-b
</div>
</div>
<ul
class="MuiCollapse-root MuiTreeItem-group group"
role="group"
style="min-height: 0px; height: 0px; transition-duration: 300ms;"
>
<div
class="MuiCollapse-wrapper"
>
<div
class="MuiCollapse-wrapperInner"
>
<li
class="MuiTreeItem-root Mui-expanded"
data-testid="namespace-level-deep-subchild-a"
role="treeitem"
tabindex="-1"
>
<div
class="MuiTreeItem-content"
>
<div
class="MuiTreeItem-iconContainer"
>
<div
style="opacity: 0.3;"
>
<svg
aria-hidden="true"
class="MuiSvgIcon-root"
data-testid="minus-square"
focusable="false"
style="width: 14px; height: 14px;"
viewBox="0 0 24 24"
>
<path
d="M22.047 22.074v0 0-20.147 0h-20.12v0 20.147 0h20.12zM22.047 24h-20.12q-.803 0-1.365-.562t-.562-1.365v-20.147q0-.776.562-1.351t1.365-.575h20.147q.776 0 1.351.575t.575 1.351v20.147q0 .803-.575 1.365t-1.378.562v0zM17.873 11.023h-11.826q-.375 0-.669.281t-.294.682v0q0 .401.294 .682t.669.281h11.826q.375 0 .669-.281t.294-.682v0q0-.401-.294-.682t-.669-.281z"
/>
</svg>
</div>
</div>
<div
class="MuiTypography-root MuiTreeItem-label label MuiTypography-body1"
>
level-deep-subchild-a
</div>
</div>
</li>
</div>
</div>
</ul>
</li>
</div>
</div>
</ul>
</li>
</ul>
</div>
</div>
</body>
`;
exports[`<NamespaceTreeView /> renders 2 levels deep 1`] = `
<body>
<div>
<div
class="TreeView"
data-testid="namespace-tree-view"
>
<div
class="DrawerTitle title"
>
Tree View
</div>
<ul
aria-multiselectable="false"
class="MuiTreeView-root"
role="tree"
>
<li
aria-expanded="true"
class="MuiTreeItem-root Mui-expanded"
data-testid="namespace-levels-deep"
role="treeitem"
tabindex="0"
>
<div
class="MuiTreeItem-content"
>
<div
class="MuiTreeItem-iconContainer"
>
<svg
aria-hidden="true"
class="MuiSvgIcon-root"
data-testid="minus-square"
focusable="false"
style="width: 14px; height: 14px;"
viewBox="0 0 24 24"
>
<path
d="M22.047 22.074v0 0-20.147 0h-20.12v0 20.147 0h20.12zM22.047 24h-20.12q-.803 0-1.365-.562t-.562-1.365v-20.147q0-.776.562-1.351t1.365-.575h20.147q.776 0 1.351.575t.575 1.351v20.147q0 .803-.575 1.365t-1.378.562v0zM17.873 11.023h-11.826q-.375 0-.669.281t-.294.682v0q0 .401.294 .682t.669.281h11.826q.375 0 .669-.281t.294-.682v0q0-.401-.294-.682t-.669-.281z"
/>
</svg>
</div>
<div
class="MuiTypography-root MuiTreeItem-label label MuiTypography-body1"
>
levels-deep
</div>
</div>
<ul
class="MuiCollapse-root MuiTreeItem-group group MuiCollapse-entered"
role="group"
style="min-height: 0px;"
>
<div
class="MuiCollapse-wrapper"
>
<div
class="MuiCollapse-wrapperInner"
>
<li
class="MuiTreeItem-root Mui-expanded"
data-testid="namespace-level-deep-child-a"
role="treeitem"
tabindex="-1"
>
<div
class="MuiTreeItem-content"
>
<div
class="MuiTreeItem-iconContainer"
>
<div
style="opacity: 0.3;"
>
<svg
aria-hidden="true"
class="MuiSvgIcon-root"
data-testid="minus-square"
focusable="false"
style="width: 14px; height: 14px;"
viewBox="0 0 24 24"
>
<path
d="M22.047 22.074v0 0-20.147 0h-20.12v0 20.147 0h20.12zM22.047 24h-20.12q-.803 0-1.365-.562t-.562-1.365v-20.147q0-.776.562-1.351t1.365-.575h20.147q.776 0 1.351.575t.575 1.351v20.147q0 .803-.575 1.365t-1.378.562v0zM17.873 11.023h-11.826q-.375 0-.669.281t-.294.682v0q0 .401.294 .682t.669.281h11.826q.375 0 .669-.281t.294-.682v0q0-.401-.294-.682t-.669-.281z"
/>
</svg>
</div>
</div>
<div
class="MuiTypography-root MuiTreeItem-label label MuiTypography-body1"
>
level-deep-child-a
</div>
</div>
</li>
<li
aria-expanded="true"
class="MuiTreeItem-root Mui-expanded"
data-testid="namespace-level-deep-child-b"
role="treeitem"
tabindex="-1"
>
<div
class="MuiTreeItem-content"
>
<div
class="MuiTreeItem-iconContainer"
>
<svg
aria-hidden="true"
class="MuiSvgIcon-root"
data-testid="minus-square"
focusable="false"
style="width: 14px; height: 14px;"
viewBox="0 0 24 24"
>
<path
d="M22.047 22.074v0 0-20.147 0h-20.12v0 20.147 0h20.12zM22.047 24h-20.12q-.803 0-1.365-.562t-.562-1.365v-20.147q0-.776.562-1.351t1.365-.575h20.147q.776 0 1.351.575t.575 1.351v20.147q0 .803-.575 1.365t-1.378.562v0zM17.873 11.023h-11.826q-.375 0-.669.281t-.294.682v0q0 .401.294 .682t.669.281h11.826q.375 0 .669-.281t.294-.682v0q0-.401-.294-.682t-.669-.281z"
/>
</svg>
</div>
<div
class="MuiTypography-root MuiTreeItem-label label MuiTypography-body1"
>
level-deep-child-b
</div>
</div>
<ul
class="MuiCollapse-root MuiTreeItem-group group MuiCollapse-entered"
role="group"
style="min-height: 0px;"
>
<div
class="MuiCollapse-wrapper"
>
<div
class="MuiCollapse-wrapperInner"
>
<li
class="MuiTreeItem-root Mui-expanded"
data-testid="namespace-level-deep-subchild-a"
role="treeitem"
tabindex="-1"
>
<div
class="MuiTreeItem-content"
>
<div
class="MuiTreeItem-iconContainer"
>
<div
style="opacity: 0.3;"
>
<svg
aria-hidden="true"
class="MuiSvgIcon-root"
data-testid="minus-square"
focusable="false"
style="width: 14px; height: 14px;"
viewBox="0 0 24 24"
>
<path
d="M22.047 22.074v0 0-20.147 0h-20.12v0 20.147 0h20.12zM22.047 24h-20.12q-.803 0-1.365-.562t-.562-1.365v-20.147q0-.776.562-1.351t1.365-.575h20.147q.776 0 1.351.575t.575 1.351v20.147q0 .803-.575 1.365t-1.378.562v0zM17.873 11.023h-11.826q-.375 0-.669.281t-.294.682v0q0 .401.294 .682t.669.281h11.826q.375 0 .669-.281t.294-.682v0q0-.401-.294-.682t-.669-.281z"
/>
</svg>
</div>
</div>
<div
class="MuiTypography-root MuiTreeItem-label label MuiTypography-body1"
>
level-deep-subchild-a
</div>
</div>
</li>
</div>
</div>
</ul>
</li>
</div>
</div>
</ul>
</li>
</ul>
</div>
</div>
</body>
`;
exports[`<NamespaceTreeView /> renders namespace with 2 children namespaces 1`] = `
<body>
<div>
<div
class="TreeView"
data-testid="namespace-tree-view"
>
<div
class="DrawerTitle title"
>
Tree View
</div>
<ul
aria-multiselectable="false"
class="MuiTreeView-root"
role="tree"
>
<li
aria-expanded="true"
class="MuiTreeItem-root Mui-expanded"
data-testid="namespace-acme-org"
role="treeitem"
tabindex="0"
>
<div
class="MuiTreeItem-content"
>
<div
class="MuiTreeItem-iconContainer"
>
<svg
aria-hidden="true"
class="MuiSvgIcon-root"
data-testid="minus-square"
focusable="false"
style="width: 14px; height: 14px;"
viewBox="0 0 24 24"
>
<path
d="M22.047 22.074v0 0-20.147 0h-20.12v0 20.147 0h20.12zM22.047 24h-20.12q-.803 0-1.365-.562t-.562-1.365v-20.147q0-.776.562-1.351t1.365-.575h20.147q.776 0 1.351.575t.575 1.351v20.147q0 .803-.575 1.365t-1.378.562v0zM17.873 11.023h-11.826q-.375 0-.669.281t-.294.682v0q0 .401.294 .682t.669.281h11.826q.375 0 .669-.281t.294-.682v0q0-.401-.294-.682t-.669-.281z"
/>
</svg>
</div>
<div
class="MuiTypography-root MuiTreeItem-label label MuiTypography-body1"
>
acme-org
</div>
</div>
<ul
class="MuiCollapse-root MuiTreeItem-group group MuiCollapse-entered"
role="group"
style="min-height: 0px;"
>
<div
class="MuiCollapse-wrapper"
>
<div
class="MuiCollapse-wrapperInner"
>
<li
class="MuiTreeItem-root Mui-expanded"
data-testid="namespace-team-a"
role="treeitem"
tabindex="-1"
>
<div
class="MuiTreeItem-content"
>
<div
class="MuiTreeItem-iconContainer"
>
<div
style="opacity: 0.3;"
>
<svg
aria-hidden="true"
class="MuiSvgIcon-root"
data-testid="minus-square"
focusable="false"
style="width: 14px; height: 14px;"
viewBox="0 0 24 24"
>
<path
d="M22.047 22.074v0 0-20.147 0h-20.12v0 20.147 0h20.12zM22.047 24h-20.12q-.803 0-1.365-.562t-.562-1.365v-20.147q0-.776.562-1.351t1.365-.575h20.147q.776 0 1.351.575t.575 1.351v20.147q0 .803-.575 1.365t-1.378.562v0zM17.873 11.023h-11.826q-.375 0-.669.281t-.294.682v0q0 .401.294 .682t.669.281h11.826q.375 0 .669-.281t.294-.682v0q0-.401-.294-.682t-.669-.281z"
/>
</svg>
</div>
</div>
<div
class="MuiTypography-root MuiTreeItem-label label MuiTypography-body1"
>
team-a
</div>
</div>
</li>
<li
class="MuiTreeItem-root Mui-expanded"
data-testid="namespace-team-b"
role="treeitem"
tabindex="-1"
>
<div
class="MuiTreeItem-content"
>
<div
class="MuiTreeItem-iconContainer"
>
<div
style="opacity: 0.3;"
>
<svg
aria-hidden="true"
class="MuiSvgIcon-root"
data-testid="minus-square"
focusable="false"
style="width: 14px; height: 14px;"
viewBox="0 0 24 24"
>
<path
d="M22.047 22.074v0 0-20.147 0h-20.12v0 20.147 0h20.12zM22.047 24h-20.12q-.803 0-1.365-.562t-.562-1.365v-20.147q0-.776.562-1.351t1.365-.575h20.147q.776 0 1.351.575t.575 1.351v20.147q0 .803-.575 1.365t-1.378.562v0zM17.873 11.023h-11.826q-.375 0-.669.281t-.294.682v0q0 .401.294 .682t.669.281h11.826q.375 0 .669-.281t.294-.682v0q0-.401-.294-.682t-.669-.281z"
/>
</svg>
</div>
</div>
<div
class="MuiTypography-root MuiTreeItem-label label MuiTypography-body1"
>
team-b
</div>
</div>
</li>
</div>
</div>
</ul>
</li>
</ul>
</div>
</div>
</body>
`;
exports[`<NamespaceTreeView /> renders namespace with children namespaces and a subnamespace 1`] = `
<body>
<div>
<div
class="TreeView"
data-testid="namespace-tree-view"
>
<div
class="DrawerTitle title"
>
Tree View
</div>
<ul
aria-multiselectable="false"
class="MuiTreeView-root"
role="tree"
>
<li
aria-expanded="true"
class="MuiTreeItem-root Mui-expanded"
data-testid="namespace-org-a"
role="treeitem"
tabindex="0"
>
<div
class="MuiTreeItem-content"
>
<div
class="MuiTreeItem-iconContainer"
>
<svg
aria-hidden="true"
class="MuiSvgIcon-root"
data-testid="minus-square"
focusable="false"
style="width: 14px; height: 14px;"
viewBox="0 0 24 24"
>
<path
d="M22.047 22.074v0 0-20.147 0h-20.12v0 20.147 0h20.12zM22.047 24h-20.12q-.803 0-1.365-.562t-.562-1.365v-20.147q0-.776.562-1.351t1.365-.575h20.147q.776 0 1.351.575t.575 1.351v20.147q0 .803-.575 1.365t-1.378.562v0zM17.873 11.023h-11.826q-.375 0-.669.281t-.294.682v0q0 .401.294 .682t.669.281h11.826q.375 0 .669-.281t.294-.682v0q0-.401-.294-.682t-.669-.281z"
/>
</svg>
</div>
<div
class="MuiTypography-root MuiTreeItem-label label MuiTypography-body1"
>
org-a
</div>
</div>
<ul
class="MuiCollapse-root MuiTreeItem-group group MuiCollapse-entered"
role="group"
style="min-height: 0px;"
>
<div
class="MuiCollapse-wrapper"
>
<div
class="MuiCollapse-wrapperInner"
>
<li
class="MuiTreeItem-root Mui-expanded"
data-testid="namespace-team-c"
role="treeitem"
tabindex="-1"
>
<div
class="MuiTreeItem-content"
>
<div
class="MuiTreeItem-iconContainer"
>
<div
style="opacity: 0.3;"
>
<svg
aria-hidden="true"
class="MuiSvgIcon-root"
data-testid="minus-square"
focusable="false"
style="width: 14px; height: 14px;"
viewBox="0 0 24 24"
>
<path
d="M22.047 22.074v0 0-20.147 0h-20.12v0 20.147 0h20.12zM22.047 24h-20.12q-.803 0-1.365-.562t-.562-1.365v-20.147q0-.776.562-1.351t1.365-.575h20.147q.776 0 1.351.575t.575 1.351v20.147q0 .803-.575 1.365t-1.378.562v0zM17.873 11.023h-11.826q-.375 0-.669.281t-.294.682v0q0 .401.294 .682t.669.281h11.826q.375 0 .669-.281t.294-.682v0q0-.401-.294-.682t-.669-.281z"
/>
</svg>
</div>
</div>
<div
class="MuiTypography-root MuiTreeItem-label label MuiTypography-body1"
>
team-c
</div>
</div>
</li>
<li
class="MuiTreeItem-root Mui-expanded"
data-testid="namespace-service-1"
role="treeitem"
tabindex="-1"
>
<div
class="MuiTreeItem-content"
>
<div
class="MuiTreeItem-iconContainer"
>
<div
style="opacity: 0.3;"
>
<svg
aria-hidden="true"
class="MuiSvgIcon-root"
data-testid="minus-square"
focusable="false"
style="width: 14px; height: 14px;"
viewBox="0 0 24 24"
>
<path
d="M22.047 22.074v0 0-20.147 0h-20.12v0 20.147 0h20.12zM22.047 24h-20.12q-.803 0-1.365-.562t-.562-1.365v-20.147q0-.776.562-1.351t1.365-.575h20.147q.776 0 1.351.575t.575 1.351v20.147q0 .803-.575 1.365t-1.378.562v0zM17.873 11.023h-11.826q-.375 0-.669.281t-.294.682v0q0 .401.294 .682t.669.281h11.826q.375 0 .669-.281t.294-.682v0q0-.401-.294-.682t-.669-.281z"
/>
</svg>
</div>
</div>
<div
class="MuiTypography-root MuiTreeItem-label label MuiTypography-body1"
>
service-1
<span
class="subnamespaceBadge"
data-testid="namespace-details-badge-for-service-1"
id="namespace-details-badge-for-service-1"
>
S
</span>
</div>
</div>
</li>
</div>
</div>
</ul>
</li>
</ul>
</div>
</div>
</body>
`;
exports[`<NamespaceTreeView /> renders one namespace without children 1`] = `
<body>
<div>
<div
class="TreeView"
data-testid="namespace-tree-view"
>
<div
class="DrawerTitle title"
>
Tree View
</div>
<ul
aria-multiselectable="false"
class="MuiTreeView-root"
role="tree"
>
<li
class="MuiTreeItem-root"
data-testid="namespace-single-root"
role="treeitem"
tabindex="0"
>
<div
class="MuiTreeItem-content"
>
<div
class="MuiTreeItem-iconContainer"
>
<div
style="opacity: 0.3;"
>
<svg
aria-hidden="true"
class="MuiSvgIcon-root"
data-testid="minus-square"
focusable="false"
style="width: 14px; height: 14px;"
viewBox="0 0 24 24"
>
<path
d="M22.047 22.074v0 0-20.147 0h-20.12v0 20.147 0h20.12zM22.047 24h-20.12q-.803 0-1.365-.562t-.562-1.365v-20.147q0-.776.562-1.351t1.365-.575h20.147q.776 0 1.351.575t.575 1.351v20.147q0 .803-.575 1.365t-1.378.562v0zM17.873 11.023h-11.826q-.375 0-.669.281t-.294.682v0q0 .401.294 .682t.669.281h11.826q.375 0 .669-.281t.294-.682v0q0-.401-.294-.682t-.669-.281z"
/>
</svg>
</div>
</div>
<div
class="MuiTypography-root MuiTreeItem-label label MuiTypography-body1"
>
single-root
</div>
</div>
</li>
</ul>
</div>
</div>
</body>
`;
@@ -0,0 +1,18 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import namespaceStoreInjectable from "./store.injectable";
const hierarchicalNamespacesInjectable = getInjectable({
id: "hierarchical-namespaces",
instantiate: (di) => {
const namespaceStore = di.inject(namespaceStoreInjectable);
return namespaceStore.items.filter(item => item.isControlledByHNC());
},
});
export default hierarchicalNamespacesInjectable;
@@ -26,6 +26,9 @@ import limitRangeStoreInjectable from "../+config-limit-ranges/store.injectable"
import resourceQuotaStoreInjectable from "../+config-resource-quotas/store.injectable";
import type { Logger } from "../../../common/logger";
import loggerInjectable from "../../../common/logger.injectable";
import { NamespaceTreeView } from "./namespace-tree-view";
import namespaceStoreInjectable from "./store.injectable";
import type { NamespaceStore } from "./store";
export interface NamespaceDetailsProps extends KubeObjectDetailsProps<Namespace> {
}
@@ -35,6 +38,7 @@ interface Dependencies {
getDetailsUrl: GetDetailsUrl;
resourceQuotaStore: ResourceQuotaStore;
limitRangeStore: LimitRangeStore;
namespaceStore: NamespaceStore;
logger: Logger;
}
@@ -103,6 +107,10 @@ class NonInjectedNamespaceDetails extends React.Component<NamespaceDetailsProps
</Link>
))}
</DrawerItem>
{namespace.isControlledByHNC() && (
<NamespaceTreeView tree={this.props.namespaceStore.getNamespaceTree(namespace)}/>
)}
</div>
);
}
@@ -115,6 +123,7 @@ export const NamespaceDetails = withInjectables<Dependencies, NamespaceDetailsPr
getDetailsUrl: di.inject(getDetailsUrlInjectable),
limitRangeStore: di.inject(limitRangeStoreInjectable),
resourceQuotaStore: di.inject(resourceQuotaStoreInjectable),
namespaceStore: di.inject(namespaceStoreInjectable),
logger: di.inject(loggerInjectable),
}),
});
@@ -0,0 +1,205 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import type { DiContainer } from "@ogre-tools/injectable";
import { observable } from "mobx";
import directoryForKubeConfigsInjectable from "../../../common/app-paths/directory-for-kube-configs/directory-for-kube-configs.injectable";
import directoryForUserDataInjectable from "../../../common/app-paths/directory-for-user-data/directory-for-user-data.injectable";
import { Namespace } from "../../../common/k8s-api/endpoints";
import hostedClusterInjectable from "../../cluster-frame-context/hosted-cluster.injectable";
import createClusterInjectable from "../../cluster/create-cluster.injectable";
import { getDiForUnitTesting } from "../../getDiForUnitTesting";
import storesAndApisCanBeCreatedInjectable from "../../stores-apis-can-be-created.injectable";
import type { NamespaceStore } from "./store";
import namespaceStoreInjectable from "./store.injectable";
function createNamespace(name: string, labels?: Record<string, string>, annotations?: Record<string, string>): Namespace {
return new Namespace({
apiVersion: "v1",
kind: "Namespace",
metadata: {
name,
resourceVersion: "1",
selfLink: `/api/v1/namespaces/${name}`,
uid: `${name}`,
labels: {
...labels,
},
annotations: {
...annotations,
},
},
});
}
const singleRoot = createNamespace("single-root", {
"hnc.x-k8s.io/included-namespace": "true",
});
const acmeGroup = createNamespace("acme-org", {
"hnc.x-k8s.io/included-namespace": "true",
});
const orgA = createNamespace("org-a", {
"hnc.x-k8s.io/included-namespace": "true",
});
const teamA = createNamespace("team-a", {
"hnc.x-k8s.io/included-namespace": "true",
"acme-org.tree.hnc.x-k8s.io/depth": "1",
"kubernetes.io/metadata.name": "team-a",
"team-a.tree.hnc.x-k8s.io/depth": "0",
});
const teamB = createNamespace("team-b", {
"hnc.x-k8s.io/included-namespace": "true",
"acme-org.tree.hnc.x-k8s.io/depth": "1",
"kubernetes.io/metadata.name": "team-b",
"team-b.tree.hnc.x-k8s.io/depth": "0",
});
const teamC = createNamespace("team-c", {
"hnc.x-k8s.io/included-namespace": "true",
"org-a.tree.hnc.x-k8s.io/depth": "1",
"kubernetes.io/metadata.name": "team-c",
"team-c.tree.hnc.x-k8s.io/depth": "0",
});
const service1 = createNamespace("service-1", {
"hnc.x-k8s.io/included-namespace": "true",
"org-a.tree.hnc.x-k8s.io/depth": "1",
"kubernetes.io/metadata.name": "team-c",
"service-1.tree.hnc.x-k8s.io/depth": "0",
}, {
"hnc.x-k8s.io/subnamespace-of": "org-a",
});
const levelsDeep = createNamespace("levels-deep", {
"hnc.x-k8s.io/included-namespace": "true",
});
const levelDeepChildA = createNamespace("level-deep-child-a", {
"hnc.x-k8s.io/included-namespace": "true",
"levels-deep.tree.hnc.x-k8s.io/depth": "1",
"level-deep-child-a.tree.hnc.x-k8s.io/depth": "0",
});
const levelDeepChildB = createNamespace("level-deep-child-b", {
"hnc.x-k8s.io/included-namespace": "true",
"levels-deep.tree.hnc.x-k8s.io/depth": "1",
"level-deep-child-b.tree.hnc.x-k8s.io/depth": "0",
});
const levelDeepSubChildA = createNamespace("level-deep-subchild-a", {
"hnc.x-k8s.io/included-namespace": "true",
"levels-deep.tree.hnc.x-k8s.io/depth": "2",
"level-deep-child-b.tree.hnc.x-k8s.io/depth": "1",
"level-deep-subchild-a.tree.hnc.x-k8s.io/depth": "0",
});
describe("NamespaceStore", () => {
let di: DiContainer;
let namespaceStore: NamespaceStore;
beforeEach(async () => {
di = getDiForUnitTesting({ doGeneralOverrides: true });
di.override(directoryForUserDataInjectable, () => "/some-user-store-path");
di.override(directoryForKubeConfigsInjectable, () => "/some-kube-configs");
di.override(storesAndApisCanBeCreatedInjectable, () => true);
const createCluster = di.inject(createClusterInjectable);
di.override(hostedClusterInjectable, () => createCluster({
contextName: "some-context-name",
id: "some-cluster-id",
kubeConfigPath: "/some-path-to-a-kubeconfig",
}, {
clusterServerUrl: "https://localhost:8080",
}));
namespaceStore = di.inject(namespaceStoreInjectable);
namespaceStore.items = observable.array([
acmeGroup,
orgA,
teamA,
teamB,
teamC,
service1,
levelsDeep,
levelDeepChildA,
levelDeepChildB,
levelDeepSubChildA,
]);
});
it("returns tree for single node", () => {
const tree = namespaceStore.getNamespaceTree(service1);
expect(tree).toEqual({
id: "service-1",
namespace: service1,
children: [],
});
});
it("returns tree for namespace not listed in store", () => {
const tree = namespaceStore.getNamespaceTree(singleRoot);
expect(tree).toEqual({
id: "single-root",
namespace: singleRoot,
children: [],
});
});
it("return tree for namespace with children", () => {
const tree = namespaceStore.getNamespaceTree(acmeGroup);
expect(tree).toEqual({
id: "acme-org",
namespace: acmeGroup,
children: [
{
id: "team-a",
namespace: teamA,
children: [],
},
{
id: "team-b",
namespace: teamB,
children: [],
},
],
});
});
it("return tree for namespace with deep nested children", () => {
const tree = namespaceStore.getNamespaceTree(levelsDeep);
expect(tree).toEqual({
id: "levels-deep",
namespace: levelsDeep,
children: [
{
id: "level-deep-child-a",
namespace: levelDeepChildA,
children: [],
},
{
id: "level-deep-child-b",
namespace: levelDeepChildB,
children: [{
id: "level-deep-subchild-a",
namespace: levelDeepSubChildA,
children: [],
}],
},
],
});
});
});
@@ -0,0 +1,14 @@
.TreeView {
.group {
margin-inline-start: var(--margin);
padding-inline-start: calc(var(--padding) * 2);
border-inline-start: 1px dashed var(--borderColor);
}
.label {
font-size: inherit;
line-height: 1.8;
cursor: default;
background-color: transparent!important;
}
}
@@ -0,0 +1,334 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import type { DiContainer } from "@ogre-tools/injectable";
import { fireEvent } from "@testing-library/react";
import React from "react";
import { Namespace } from "../../../common/k8s-api/endpoints";
import { getDiForUnitTesting } from "../../getDiForUnitTesting";
import type { DiRender } from "../test-utils/renderFor";
import { renderFor } from "../test-utils/renderFor";
import hierarchicalNamespacesInjectable from "./hierarchical-namespaces.injectable";
import { NamespaceTreeView } from "./namespace-tree-view";
import type { NamespaceTree } from "./store";
jest.mock("react-router-dom", () => ({
Link: ({ children }: { children: React.ReactNode }) => children,
}));
function createNamespace(name: string, labels?: Record<string, string>, annotations?: Record<string, string>): Namespace {
return new Namespace({
apiVersion: "v1",
kind: "Namespace",
metadata: {
name,
resourceVersion: "1",
selfLink: `/api/v1/namespaces/${name}`,
uid: `${name}`,
labels: {
...labels,
},
annotations: {
...annotations,
},
},
});
}
const singleRoot = createNamespace("single-root", {
"hnc.x-k8s.io/included-namespace": "true",
});
const acmeGroup = createNamespace("acme-org", {
"hnc.x-k8s.io/included-namespace": "true",
});
const orgA = createNamespace("org-a", {
"hnc.x-k8s.io/included-namespace": "true",
});
const teamA = createNamespace("team-a", {
"hnc.x-k8s.io/included-namespace": "true",
"acme-org.tree.hnc.x-k8s.io/depth": "1",
"kubernetes.io/metadata.name": "team-a",
"team-a.tree.hnc.x-k8s.io/depth": "0",
});
const teamB = createNamespace("team-b", {
"hnc.x-k8s.io/included-namespace": "true",
"acme-org.tree.hnc.x-k8s.io/depth": "1",
"kubernetes.io/metadata.name": "team-b",
"team-b.tree.hnc.x-k8s.io/depth": "0",
});
const teamC = createNamespace("team-c", {
"hnc.x-k8s.io/included-namespace": "true",
"org-a.tree.hnc.x-k8s.io/depth": "1",
"kubernetes.io/metadata.name": "team-c",
"team-c.tree.hnc.x-k8s.io/depth": "0",
});
const service1 = createNamespace("service-1", {
"hnc.x-k8s.io/included-namespace": "true",
"org-a.tree.hnc.x-k8s.io/depth": "1",
"kubernetes.io/metadata.name": "team-c",
"service-1.tree.hnc.x-k8s.io/depth": "0",
}, {
"hnc.x-k8s.io/subnamespace-of": "org-a",
});
const levelsDeep = createNamespace("levels-deep", {
"hnc.x-k8s.io/included-namespace": "true",
});
const levelDeepChildA = createNamespace("level-deep-child-a", {
"hnc.x-k8s.io/included-namespace": "true",
"levels-deep.tree.hnc.x-k8s.io/depth": "1",
"level-deep-child-a.tree.hnc.x-k8s.io/depth": "0",
});
const levelDeepChildB = createNamespace("level-deep-child-b", {
"hnc.x-k8s.io/included-namespace": "true",
"levels-deep.tree.hnc.x-k8s.io/depth": "1",
"level-deep-child-b.tree.hnc.x-k8s.io/depth": "0",
});
const levelDeepSubChildA = createNamespace("level-deep-subchild-a", {
"hnc.x-k8s.io/included-namespace": "true",
"levels-deep.tree.hnc.x-k8s.io/depth": "2",
"level-deep-child-b.tree.hnc.x-k8s.io/depth": "1",
"level-deep-subchild-a.tree.hnc.x-k8s.io/depth": "0",
});
describe("<NamespaceTreeView />", () => {
let di: DiContainer;
let render: DiRender;
beforeEach(async () => {
di = getDiForUnitTesting({ doGeneralOverrides: true });
di.override(hierarchicalNamespacesInjectable, () => [
acmeGroup,
orgA,
teamA,
teamB,
teamC,
service1,
levelsDeep,
levelDeepChildA,
levelDeepChildB,
levelDeepSubChildA,
]);
render = renderFor(di);
});
it("renders one namespace without children", () => {
const tree: NamespaceTree = {
id: "single-root",
namespace: singleRoot,
};
const result = render(<NamespaceTreeView tree={tree} />);
expect(result.baseElement).toMatchSnapshot();
});
it("renders namespace with 2 children namespaces", () => {
const tree: NamespaceTree = {
id: "acme-org",
namespace: acmeGroup,
children: [
{
id: "team-a",
namespace: teamA,
},
{
id: "team-b",
namespace: teamB,
},
],
};
const result = render(<NamespaceTreeView tree={tree} />);
expect(result.baseElement).toMatchSnapshot();
});
it("renders namespace with children namespaces and a subnamespace", () => {
const tree: NamespaceTree = {
id: "org-a",
namespace: orgA,
children: [
{
id: "team-c",
namespace: teamC,
},
{
id: "service-1",
namespace: service1,
},
],
};
const result = render(<NamespaceTreeView tree={tree} />);
expect(result.baseElement).toMatchSnapshot();
});
it("renders an indicator badge for the subnamespace", () => {
const tree: NamespaceTree = {
id: "org-a",
namespace: orgA,
children: [
{
id: "team-c",
namespace: teamC,
},
{
id: "service-1",
namespace: service1,
},
],
};
const result = render(<NamespaceTreeView tree={tree} />);
expect(result.getByTestId("namespace-details-badge-for-service-1")).toBeInTheDocument();
});
it("does not render an indicator badge for the true namespace", () => {
const tree: NamespaceTree = {
id: "org-a",
namespace: orgA,
children: [
{
id: "team-c",
namespace: teamC,
},
{
id: "service-1",
namespace: service1,
},
],
};
const result = render(<NamespaceTreeView tree={tree} />);
const trueNamespace = result.getByTestId("namespace-team-c");
expect(trueNamespace.querySelector("[data-testid='namespace-details-badge-for-team-c']")).toBeNull();
});
it("renders 2 levels deep", () => {
const tree: NamespaceTree = {
id: "levels-deep",
namespace: levelsDeep,
children: [
{
id: "level-deep-child-a",
namespace: levelDeepChildA,
},
{
id: "level-deep-child-b",
namespace: levelDeepChildB,
children: [{
id: "level-deep-subchild-a",
namespace: levelDeepSubChildA,
}],
},
],
};
const result = render(<NamespaceTreeView tree={tree} />);
expect(result.baseElement).toMatchSnapshot();
});
it("expands children items by default", () => {
const tree: NamespaceTree = {
id: "levels-deep",
namespace: levelsDeep,
children: [
{
id: "level-deep-child-a",
namespace: levelDeepChildA,
},
{
id: "level-deep-child-b",
namespace: levelDeepChildB,
children: [{
id: "level-deep-subchild-a",
namespace: levelDeepSubChildA,
}],
},
],
};
const result = render(<NamespaceTreeView tree={tree} />);
const deepest = result.getByTestId("namespace-level-deep-child-b");
expect(deepest).toHaveAttribute("aria-expanded", "true");
});
it("collapses item by clicking minus button", () => {
const tree: NamespaceTree = {
id: "levels-deep",
namespace: levelsDeep,
children: [
{
id: "level-deep-child-a",
namespace: levelDeepChildA,
},
{
id: "level-deep-child-b",
namespace: levelDeepChildB,
children: [{
id: "level-deep-subchild-a",
namespace: levelDeepSubChildA,
}],
},
],
};
const result = render(<NamespaceTreeView tree={tree} />);
const levelB = result.getByTestId("namespace-level-deep-child-b");
const minusButton = levelB.querySelector("[data-testid='minus-square']");
if (minusButton) {
fireEvent.click(minusButton);
}
expect(result.baseElement).toMatchSnapshot();
});
it("expands item by clicking plus button", () => {
const tree: NamespaceTree = {
id: "levels-deep",
namespace: levelsDeep,
children: [
{
id: "level-deep-child-a",
namespace: levelDeepChildA,
},
{
id: "level-deep-child-b",
namespace: levelDeepChildB,
children: [{
id: "level-deep-subchild-a",
namespace: levelDeepSubChildA,
}],
},
],
};
const result = render(<NamespaceTreeView tree={tree} />);
const levelB = result.getByTestId("namespace-level-deep-child-b");
const minusButton = levelB.querySelector("[data-testid='minus-square']");
if (minusButton) {
fireEvent.click(minusButton);
}
const plusButton = levelB.querySelector("[data-testid='plus-square']");
if (plusButton) {
fireEvent.click(plusButton);
}
expect(result.baseElement).toMatchSnapshot();
});
});
@@ -0,0 +1,106 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import styles from "./namespace-tree-view.module.scss";
import { SvgIcon } from "@material-ui/core";
import { TreeItem, TreeView } from "@material-ui/lab";
import { withInjectables } from "@ogre-tools/injectable-react";
import React from "react";
import { Link } from "react-router-dom";
import type { Namespace } from "../../../common/k8s-api/endpoints";
import { DrawerTitle } from "../drawer";
import type { GetDetailsUrl } from "../kube-detail-params/get-details-url.injectable";
import getDetailsUrlInjectable from "../kube-detail-params/get-details-url.injectable";
import { SubnamespaceBadge } from "./subnamespace-badge";
import hierarchicalNamespacesInjectable from "./hierarchical-namespaces.injectable";
import { prevDefault } from "../../utils";
import type { NamespaceTree } from "./store";
interface NamespaceTreeViewProps {
tree: NamespaceTree;
}
interface Dependencies {
namespaces: Namespace[];
getDetailsUrl: GetDetailsUrl;
}
function NonInjectableNamespaceTreeView({ tree, namespaces, getDetailsUrl }: Dependencies & NamespaceTreeViewProps) {
const [expandedItems, setExpandedItems] = React.useState<string[]>(namespaces.map(ns => ns.getId()));
const classes = { group: styles.group, label: styles.label };
function renderTree(nodes: NamespaceTree) {
return (
<TreeItem
key={nodes.id}
nodeId={nodes.id}
data-testid={`namespace-${nodes.id}`}
classes={classes}
onIconClick={prevDefault(() => toggleNode(nodes.id))}
label={(
<>
<Link key={nodes.namespace.getId()} to={getDetailsUrl(nodes.namespace.selfLink)}>
{nodes.namespace.getName()}
</Link>
{" "}
{nodes.namespace.isSubnamespace() && (
<SubnamespaceBadge id={`namespace-details-badge-for-${nodes.namespace.getId()}`} />
)}
</>
)}
>
{Array.isArray(nodes.children) ? nodes.children.map((node) => renderTree(node)) : null}
</TreeItem>
);
}
function toggleNode(id: string) {
if (expandedItems.includes(id)) {
setExpandedItems(expandedItems.filter(item => item !== id));
} else {
setExpandedItems([...expandedItems, id]);
}
}
return (
<div data-testid="namespace-tree-view" className={styles.TreeView}>
<DrawerTitle>Tree View</DrawerTitle>
<TreeView
defaultExpanded={[tree.id]}
defaultCollapseIcon={<MinusSquareIcon />}
defaultExpandIcon={<PlusSquareIcon />}
defaultEndIcon={(<div style={{ opacity: 0.3 }}><MinusSquareIcon /></div>)}
expanded={expandedItems}
>
{renderTree(tree)}
</TreeView>
</div>
);
}
function MinusSquareIcon() {
return (
<SvgIcon style={{ width: 14, height: 14 }} data-testid="minus-square">
<path d="M22.047 22.074v0 0-20.147 0h-20.12v0 20.147 0h20.12zM22.047 24h-20.12q-.803 0-1.365-.562t-.562-1.365v-20.147q0-.776.562-1.351t1.365-.575h20.147q.776 0 1.351.575t.575 1.351v20.147q0 .803-.575 1.365t-1.378.562v0zM17.873 11.023h-11.826q-.375 0-.669.281t-.294.682v0q0 .401.294 .682t.669.281h11.826q.375 0 .669-.281t.294-.682v0q0-.401-.294-.682t-.669-.281z" />
</SvgIcon>
);
}
function PlusSquareIcon() {
return (
<SvgIcon style={{ width: 14, height: 14 }} data-testid="plus-square">
<path d="M22.047 22.074v0 0-20.147 0h-20.12v0 20.147 0h20.12zM22.047 24h-20.12q-.803 0-1.365-.562t-.562-1.365v-20.147q0-.776.562-1.351t1.365-.575h20.147q.776 0 1.351.575t.575 1.351v20.147q0 .803-.575 1.365t-1.378.562v0zM17.873 12.977h-4.923v4.896q0 .401-.281.682t-.682.281v0q-.375 0-.669-.281t-.294-.682v-4.896h-4.923q-.401 0-.682-.294t-.281-.669v0q0-.401.281-.682t.682-.281h4.923v-4.896q0-.401.294-.682t.669-.281v0q.401 0 .682.281t.281.682v4.896h4.923q.401 0 .682.281t.281.682v0q0 .375-.281.669t-.682.294z" />
</SvgIcon>
);
}
export const NamespaceTreeView = withInjectables<Dependencies, NamespaceTreeViewProps>(NonInjectableNamespaceTreeView, {
getProps: (di, props) => ({
namespaces: di.inject(hierarchicalNamespacesInjectable),
getDetailsUrl: di.inject(getDetailsUrlInjectable),
...props,
}),
});
@@ -22,4 +22,8 @@
@include namespaceStatus;
}
}
.subnamespaceBadge {
margin-inline-start: var(--margin);
}
}
@@ -16,6 +16,7 @@ import { withInjectables } from "@ogre-tools/injectable-react";
import namespaceStoreInjectable from "./store.injectable";
import { KubeObjectAge } from "../kube-object/age";
import openAddNamepaceDialogInjectable from "./add-dialog/open.injectable";
import { SubnamespaceBadge } from "./subnamespace-badge";
enum columnId {
name = "name",
@@ -55,7 +56,12 @@ const NonInjectedNamespacesRoute = ({ namespaceStore, openAddNamespaceDialog }:
{ title: "Status", className: "status", sortBy: columnId.status, id: columnId.status },
]}
renderTableContents={namespace => [
namespace.getName(),
<>
{namespace.getName()}
{namespace.isSubnamespace() && (
<SubnamespaceBadge className="subnamespaceBadge" id={`namespace-list-badge-for-${namespace.getId()}`} />
)}
</>,
<KubeObjectStatusIcon key="icon" object={namespace} />,
namespace.getLabels().map(label => (
<Badge
@@ -12,6 +12,12 @@ import { KubeObjectStore } from "../../../common/k8s-api/kube-object.store";
import type { NamespaceApi } from "../../../common/k8s-api/endpoints/namespace.api";
import { Namespace } from "../../../common/k8s-api/endpoints/namespace.api";
export interface NamespaceTree {
id: string;
namespace: Namespace;
children?: NamespaceTree[];
}
interface Dependencies extends KubeObjectStoreDependencies {
readonly storage: StorageLayer<string[] | undefined>;
readonly clusterConfiguredAccessibleNamespaces: IComputedValue<string[]>;
@@ -202,6 +208,16 @@ export class NamespaceStore extends KubeObjectStore<Namespace, NamespaceApi> {
this.selectAll();
}
getNamespaceTree(root: Namespace): NamespaceTree {
const children = this.items.filter(namespace => namespace.isChildOf(root.getName()));
return {
id: root.getId(),
namespace: root,
children: children.map(this.getNamespaceTree),
};
}
@action
async remove(item: Namespace) {
await super.remove(item);
@@ -0,0 +1,11 @@
.subnamespaceBadge {
border-radius: 3px;
padding: 0px 4px;
border: 1px solid cadetblue;
color: cadetblue;
display: inline-flex;
font-size: x-small;
font-weight: bold;
height: 16px;
align-items: center;
}
@@ -0,0 +1,31 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import styles from "./subnamespace-badge.module.scss";
import React from "react";
import { Tooltip } from "../tooltip";
import { cssNames } from "../../utils";
interface SubnamespaceBadgeProps extends React.HTMLAttributes<HTMLSpanElement> {
id: string;
}
export function SubnamespaceBadge({ id, className, ...other }: SubnamespaceBadgeProps) {
return (
<>
<span
className={cssNames(styles.subnamespaceBadge, className)}
data-testid={id}
id={id}
{...other}
>
S
</span>
<Tooltip targetId={id}>
Subnamespace
</Tooltip>
</>
);
}
@@ -76,7 +76,7 @@ class NonInjectedCreateResource extends React.Component<CreateResourceProps & De
this.error = error.toString();
};
create = async (): Promise<void> => {
create = async (): Promise<string | void> => {
const { apiManager, getDetailsUrl, navigate, requestKubeObjectCreation } = this.props;
if (this.error || !this.data?.trim()) {
@@ -98,7 +98,7 @@ class NonInjectedCreateResource extends React.Component<CreateResourceProps & De
this.props.logger.warn("Failed to create resource", { resource }, result.error);
this.props.showCheckedErrorNotification(result.error, "Unknown error occured while creating resources");
return;
throw result.error;
}
const { kind, apiVersion, metadata: { name, namespace }} = result.response;
@@ -122,7 +122,13 @@ class NonInjectedCreateResource extends React.Component<CreateResourceProps & De
));
});
await Promise.allSettled(creatingResources);
const results = await Promise.allSettled(creatingResources);
if (results.some(result => result.status === "rejected")) {
return;
}
return "All resources have been successfully created";
};
renderControls() {
@@ -174,5 +174,7 @@ export class EditResourceModel {
runInAction(() => {
this.editingResource.firstDraft = currentValue;
});
return result.response.toString();
};
}
@@ -93,18 +93,25 @@ class NonInjectedInfoPanel extends Component<InfoPanelProps & Dependencies> {
if (showNotifications && result) {
this.props.showSuccessNotification(result);
}
return result;
} catch (error) {
if (showNotifications) {
this.props.showCheckedErrorNotification(error, "Unknown error while submitting");
}
return false;
} finally {
this.waiting = false;
}
};
submitAndClose = async () => {
await this.submit();
this.close();
const result = await this.submit();
if (result) {
this.close();
}
};
close = () => {
@@ -12,6 +12,7 @@ import emitAppEventInjectable from "../../../../common/app-event-bus/emit-event.
import loadExtensionsInjectable from "../../load-extensions.injectable";
import loggerInjectable from "../../../../common/logger.injectable";
import showErrorNotificationInjectable from "../../../components/notifications/show-error-notification.injectable";
import closeRendererLogFileInjectable from "../../../logger/close-renderer-log-file.injectable";
const initClusterFrameInjectable = getInjectable({
id: "init-cluster-frame",
@@ -29,6 +30,7 @@ const initClusterFrameInjectable = getInjectable({
emitAppEvent: di.inject(emitAppEventInjectable),
logger: di.inject(loggerInjectable),
showErrorNotification: di.inject(showErrorNotificationInjectable),
closeFileLogging: di.inject(closeRendererLogFileInjectable),
});
},
});
@@ -2,6 +2,7 @@
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { once } from "lodash";
import type { Cluster } from "../../../../common/cluster/cluster";
import type { CatalogEntityRegistry } from "../../../api/catalog/entity/registry";
import type { ShowNotification } from "../../../components/notifications";
@@ -18,6 +19,7 @@ interface Dependencies {
emitAppEvent: EmitAppEvent;
logger: Logger;
showErrorNotification: ShowNotification;
closeFileLogging: () => void;
}
const logPrefix = "[CLUSTER-FRAME]:";
@@ -30,6 +32,7 @@ export const initClusterFrame = ({
emitAppEvent,
logger,
showErrorNotification,
closeFileLogging,
}: Dependencies) =>
async (unmountRoot: () => void) => {
await requestSetClusterFrameId(hostedCluster.id);
@@ -69,11 +72,14 @@ export const initClusterFrame = ({
});
});
window.onbeforeunload = () => {
const onCloseFrame = once(() => {
logger.info(
`${logPrefix} Unload dashboard, clusterId=${(hostedCluster.id)}, frameId=${frameRoutingId}`,
);
closeFileLogging();
unmountRoot();
};
});
window.addEventListener("beforeunload", onCloseFrame);
window.addEventListener("pagehide", onCloseFrame);
};
@@ -12,6 +12,7 @@ import loggerInjectable from "../../../common/logger.injectable";
import { delay } from "../../../common/utils";
import { broadcastMessage } from "../../../common/ipc";
import { bundledExtensionsLoaded } from "../../../common/ipc/extension-handling";
import closeRendererLogFileInjectable from "../../logger/close-renderer-log-file.injectable";
const initRootFrameInjectable = getInjectable({
id: "init-root-frame",
@@ -22,6 +23,7 @@ const initRootFrameInjectable = getInjectable({
const bindProtocolAddRouteHandlers = di.inject(bindProtocolAddRouteHandlersInjectable);
const lensProtocolRouterRenderer = di.inject(lensProtocolRouterRendererInjectable);
const logger = di.inject(loggerInjectable);
const closeRendererLogFile = di.inject(closeRendererLogFileInjectable);
return async (unmountRoot: () => void) => {
try {
@@ -55,7 +57,7 @@ const initRootFrameInjectable = getInjectable({
window.addEventListener("beforeunload", () => {
logger.info("[ROOT-FRAME]: Unload app");
closeRendererLogFile();
unmountRoot();
});
};
+2 -1
View File
@@ -12,5 +12,6 @@ export * as Mobx from "mobx";
export * as MobxReact from "mobx-react";
export * as ReactRouter from "react-router";
export * as ReactRouterDom from "react-router-dom";
export * as extensionApi from "../extensions/renderer-api";
export * as rendererExtensionApi from "../extensions/renderer-api";
export * as commonExtensionApi from "../extensions/common-api";
export { createApp } from "./create-app";
@@ -0,0 +1,22 @@
/**
* 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 winstonLoggerInjectable from "../../common/winston-logger.injectable";
import rendererFileLoggerTransportInjectable from "./file-transport.injectable";
const closeRendererLogFileInjectable = getInjectable({
id: "close-renderer-log-file",
instantiate: (di) => {
const winstonLogger = di.inject(winstonLoggerInjectable);
const fileLoggingTransport = di.inject(rendererFileLoggerTransportInjectable);
return () => {
fileLoggingTransport.close?.();
winstonLogger.remove(fileLoggingTransport);
};
},
});
export default closeRendererLogFileInjectable;
@@ -0,0 +1,44 @@
/**
* 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 { transports } from "winston";
import directoryForLogsInjectable from "../../common/app-paths/directory-for-logs.injectable";
import { loggerTransportInjectionToken } from "../../common/logger/transports";
import windowLocationInjectable from "../../common/k8s-api/window-location.injectable";
import currentlyInClusterFrameInjectable from "../routes/currently-in-cluster-frame.injectable";
import { getClusterIdFromHost } from "../utils";
const rendererFileLoggerTransportInjectable = getInjectable({
id: "renderer-file-logger-transport",
instantiate: (di) => {
let frameId: string;
const currentlyInClusterFrame = di.inject(
currentlyInClusterFrameInjectable,
);
if (currentlyInClusterFrame) {
const { host } = di.inject(windowLocationInjectable);
const clusterId = getClusterIdFromHost(host);
frameId = clusterId ? `cluster-${clusterId}` : "cluster";
} else {
frameId = "main";
}
return new transports.File({
handleExceptions: false,
level: "info",
filename: `lens-renderer-${frameId}.log`,
dirname: di.inject(directoryForLogsInjectable),
maxsize: 1024 * 1024,
maxFiles: 2,
tailable: true,
});
},
injectionToken: loggerTransportInjectionToken,
});
export default rendererFileLoggerTransportInjectable;
+1 -3
View File
@@ -6,9 +6,7 @@
import { iter } from "../../common/utils";
export type IgnoredClassNames = number | symbol | Function;
export type IClassName = string | string[] | IClassNameMap | undefined | null | false | IgnoredClassNames;
export type IClassNameMap = object;
export type IClassName = string | string[] | Record<string, any> | undefined | null | false | IgnoredClassNames;
export function cssNames(...classNames: IClassName[]): string {
const classNamesEnabled = new Map<string, boolean>();
+37
View File
@@ -0,0 +1,37 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import type webpack from "webpack";
import path from "path";
import ForkTsCheckerPlugin from "fork-ts-checker-webpack-plugin";
import webpackLensMain from "./main";
import { buildDir } from "./vars";
const webpackLensCommon = (): webpack.Configuration => {
const mainConfig = webpackLensMain();
return {
...mainConfig,
name: "lens-app-common",
entry: {
common: path.resolve(__dirname, "..", "src", "common", "library.ts"),
},
output: {
publicPath: "",
library: {
type: "commonjs2",
},
path: path.resolve(buildDir, "library"),
},
optimization: {
minimize: false,
},
plugins: [
new ForkTsCheckerPlugin({}),
],
};
};
export default webpackLensCommon;
+6 -109
View File
@@ -2,118 +2,15 @@
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import MiniCssExtractPlugin from "mini-css-extract-plugin";
import { platform } from "os";
import path from "path";
import { DefinePlugin, optimize } from "webpack";
import main from "./main";
import renderer, { iconsAndImagesWebpackRules } from "./renderer";
import { buildDir, isDevelopment } from "./vars";
const rendererConfig = renderer({ showVars: false });
const mainConfig = main();
import webpackLensCommon from "./common";
import webpackLensMain from "./main";
import { webpackLensRenderer } from "./renderer";
const config = [
{
...mainConfig,
entry: {
main: path.resolve(__dirname, "..", "src", "main", "library.ts"),
},
output: {
library: {
type: "commonjs2",
},
path: path.resolve(buildDir, "library"),
},
optimization: {
minimize: false,
},
module: {
parser: {
javascript: {
commonjsMagicComments: true,
},
},
rules: [
{
test: /\.node$/,
use: "node-loader",
},
{
test: /\.ts$/,
exclude: /node_modules/,
use: {
loader: "ts-loader",
options: {
compilerOptions: {
declaration: true,
sourceMap: false,
},
},
},
},
...iconsAndImagesWebpackRules(),
],
},
plugins: [
new DefinePlugin({
CONTEXT_MATCHER_FOR_NON_FEATURES: `/\\.injectable(\\.${platform})?\\.tsx?$/`,
CONTEXT_MATCHER_FOR_FEATURES: `/\\/(main|common)\\/.+\\.injectable(\\.${platform})?\\.tsx?$/`,
}),
],
},
{
...mainConfig,
name: "lens-app-common",
entry: {
common: path.resolve(__dirname, "..", "src", "common", "library.ts"),
},
output: {
publicPath: "",
library: {
type: "commonjs2",
},
path: path.resolve(buildDir, "library"),
},
optimization: {
minimize: false,
},
plugins: [],
},
{
...rendererConfig,
entry: {
renderer: path.resolve(__dirname, "..", "src", "renderer", "library.ts"),
},
output: {
library: {
type: "commonjs2",
},
path: path.resolve(buildDir, "library"),
},
optimization: {
minimize: false,
},
externals: [
...(rendererConfig.externals as any).filter(Boolean),
{
"monaco-editor": "commonjs monaco-editor",
},
],
plugins: [
new DefinePlugin({
CONTEXT_MATCHER_FOR_NON_FEATURES: `/\\.injectable(\\.${platform})?\\.tsx?$/`,
CONTEXT_MATCHER_FOR_FEATURES: `/\\/(renderer|common)\\/.+\\.injectable(\\.${platform})?\\.tsx?$/`,
}),
new MiniCssExtractPlugin({
filename: "[name].css",
runtime: isDevelopment,
}),
new optimize.LimitChunkCountPlugin({
maxChunks: 1,
}),
],
},
webpackLensMain(),
webpackLensRenderer(),
webpackLensCommon(),
];
export default config;
+35 -26
View File
@@ -5,25 +5,14 @@
import path from "path";
import type webpack from "webpack";
import ForkTsCheckerPlugin from "fork-ts-checker-webpack-plugin";
import nodeExternals from "webpack-node-externals";
import { getTypescriptLoader } from "./get-typescript-loader";
import CircularDependencyPlugin from "circular-dependency-plugin";
import ForkTsCheckerPlugin from "fork-ts-checker-webpack-plugin";
import { iconsAndImagesWebpackRules } from "./renderer";
import type { WebpackPluginInstance } from "webpack";
import { DefinePlugin } from "webpack";
import { additionalExternals, buildDir, isDevelopment, mainDir } from "./vars";
import { buildDir, isDevelopment } from "./vars";
import { platform } from "process";
const main = ({ showVars = true } = {}): webpack.Configuration => {
if (showVars) {
console.info("WEBPACK:main", {
isDevelopment,
mainDir,
buildDir,
});
}
const webpackLensMain = (): webpack.Configuration => {
return {
name: "lens-app-main",
context: __dirname,
@@ -32,18 +21,22 @@ const main = ({ showVars = true } = {}): webpack.Configuration => {
devtool: isDevelopment ? "cheap-module-source-map" : "source-map",
cache: isDevelopment ? { type: "filesystem" } : false,
entry: {
main: path.resolve(mainDir, "index.ts"),
main: path.resolve(__dirname, "..", "src", "main", "library.ts"),
},
output: {
libraryTarget: "global",
path: buildDir,
library: {
type: "commonjs2",
},
path: path.resolve(buildDir, "library"),
},
optimization: {
minimize: false,
},
resolve: {
extensions: [".json", ".js", ".ts"],
},
externals: [
nodeExternals(),
...additionalExternals,
],
module: {
parser: {
@@ -56,7 +49,19 @@ const main = ({ showVars = true } = {}): webpack.Configuration => {
test: /\.node$/,
use: "node-loader",
},
getTypescriptLoader({}, /\.ts$/),
{
test: /\.ts$/,
exclude: /node_modules/,
use: {
loader: "ts-loader",
options: {
transpileOnly: true,
compilerOptions: {
sourceMap: false,
},
},
},
},
...iconsAndImagesWebpackRules(),
],
},
@@ -65,14 +70,18 @@ const main = ({ showVars = true } = {}): webpack.Configuration => {
CONTEXT_MATCHER_FOR_NON_FEATURES: `/\\.injectable(\\.${platform})?\\.tsx?$/`,
CONTEXT_MATCHER_FOR_FEATURES: `/\\/(main|common)\\/.+\\.injectable(\\.${platform})?\\.tsx?$/`,
}),
new ForkTsCheckerPlugin(),
new CircularDependencyPlugin({
cwd: __dirname,
exclude: /node_modules/,
failOnError: true,
}) as unknown as WebpackPluginInstance,
new ForkTsCheckerPlugin({
typescript: {
mode: "write-dts",
configOverwrite: {
compilerOptions: {
declaration: true,
},
},
},
}),
],
};
};
export default main;
export default webpackLensMain;
+29 -61
View File
@@ -5,31 +5,16 @@
import path from "path";
import type webpack from "webpack";
import HtmlWebpackPlugin from "html-webpack-plugin";
import MiniCssExtractPlugin from "mini-css-extract-plugin";
import ForkTsCheckerPlugin from "fork-ts-checker-webpack-plugin";
import MonacoWebpackPlugin from "monaco-editor-webpack-plugin";
import CircularDependencyPlugin from "circular-dependency-plugin";
import ReactRefreshWebpackPlugin from "@pmmmwh/react-refresh-webpack-plugin";
import ForkTsCheckerPlugin from "fork-ts-checker-webpack-plugin";
import type { WebpackPluginInstance } from "webpack";
import { DefinePlugin } from "webpack";
import { getTypescriptLoader } from "./get-typescript-loader";
import { assetsFolderName, isDevelopment, rendererDir, buildDir, appName, htmlTemplate, publicPath, sassCommonVars, additionalExternals } from "./vars";
import { optimize, DefinePlugin } from "webpack";
import nodeExternals from "webpack-node-externals";
import { isDevelopment, buildDir, sassCommonVars } from "./vars";
import { platform } from "process";
export function webpackLensRenderer({ showVars = true } = {}): webpack.Configuration {
if (showVars) {
console.info("WEBPACK:renderer", {
assetsFolderName,
isDevelopment,
rendererDir,
buildDir,
appName,
htmlTemplate,
publicPath,
});
}
export function webpackLensRenderer(): webpack.Configuration {
return {
target: "electron-renderer",
name: "lens-app-renderer",
@@ -38,16 +23,13 @@ export function webpackLensRenderer({ showVars = true } = {}): webpack.Configura
devtool: isDevelopment ? "cheap-module-source-map" : "source-map",
cache: isDevelopment ? { type: "filesystem" } : false,
entry: {
[appName]: path.resolve(rendererDir, "index.ts"),
renderer: path.resolve(__dirname, "..", "src", "renderer", "library.ts"),
},
output: {
libraryTarget: "global",
globalObject: "this",
publicPath,
path: buildDir,
filename: "[name].js",
chunkFilename: "chunks/[name].js",
assetModuleFilename: `${assetsFolderName}/[name][ext][query]`,
library: {
type: "commonjs2",
},
path: path.resolve(buildDir, "library"),
},
watchOptions: {
ignored: /node_modules/, // https://webpack.js.org/configuration/watch/
@@ -64,10 +46,7 @@ export function webpackLensRenderer({ showVars = true } = {}): webpack.Configura
],
},
externals: [
{
"win-ca": "commonjs win-ca",
},
...additionalExternals,
nodeExternals(),
],
optimization: {
minimize: false,
@@ -83,11 +62,20 @@ export function webpackLensRenderer({ showVars = true } = {}): webpack.Configura
test: /\.node$/,
use: "node-loader",
},
getTypescriptLoader({
getCustomTransformers: () => ({
before: isDevelopment ? [require("react-refresh-typescript")()] : [],
}),
}),
{
test: /\.tsx?$/,
exclude: /node_modules/,
use: {
loader: "ts-loader",
options: {
transpileOnly: true,
compilerOptions: {
declaration: true,
sourceMap: false,
},
},
},
},
cssModulesWebpackRule(),
...iconsAndImagesWebpackRules(),
...fontsLoaderWebpackRules(),
@@ -99,25 +87,7 @@ export function webpackLensRenderer({ showVars = true } = {}): webpack.Configura
CONTEXT_MATCHER_FOR_NON_FEATURES: `/\\.injectable(\\.${platform})?\\.tsx?$/`,
CONTEXT_MATCHER_FOR_FEATURES: `/\\/(renderer|common)\\/.+\\.injectable(\\.${platform})?\\.tsx?$/`,
}),
new ForkTsCheckerPlugin(),
// see also: https://github.com/Microsoft/monaco-editor-webpack-plugin#options
new MonacoWebpackPlugin({
// publicPath: "/",
// filename: "[name].worker.js",
languages: ["json", "yaml"],
globalAPI: isDevelopment,
}),
new HtmlWebpackPlugin({
filename: "index.html",
template: htmlTemplate,
inject: true,
hash: true,
templateParameters: {
assetPath: `${publicPath}${assetsFolderName}`,
},
}),
new ForkTsCheckerPlugin({}),
new CircularDependencyPlugin({
cwd: __dirname,
@@ -129,11 +99,9 @@ export function webpackLensRenderer({ showVars = true } = {}): webpack.Configura
filename: "[name].css",
}),
...(
isDevelopment
? [new ReactRefreshWebpackPlugin({ overlay: false })]
: []
),
new optimize.LimitChunkCountPlugin({
maxChunks: 1,
}),
],
};
}
-3
View File
@@ -21,8 +21,5 @@ export const htmlTemplate = path.resolve(rendererDir, "template.html");
export const publicPath = "/build/";
export const sassCommonVars = path.resolve(rendererDir, "components/vars.scss");
export const webpackDevServerPort = Number(process.env.WEBPACK_DEV_SERVER_PORT) || 9191;
export const additionalExternals = (process.env.LENS_BUILD_ADDITIONAL_WEBPACK_EXTERNALS || "")
.split(",")
.map(s => s.trim());
assert(Number.isInteger(webpackDevServerPort), "WEBPACK_DEV_SERVER_PORT environment variable must only be an integer");
+432 -1035
View File
File diff suppressed because it is too large. Load diff