1
0
mirror of https://github.com/lensapp/lens.git synced 2025-05-20 05:10:56 +00:00

Merge branch 'master' into remove-CatalogEntityItem

This commit is contained in:
Alex Andreev 2021-12-23 13:32:24 +03:00
commit 52ca977b8c
104 changed files with 1018 additions and 608 deletions

View File

@ -71,7 +71,8 @@ describe("preferences page tests", () => {
} }
}, 10*60*1000); }, 10*60*1000);
utils.itIf(process.platform !== "win32")("ensures helm repos", async () => { // Skipping, but will turn it on again in the follow up PR
it.skip("ensures helm repos", async () => {
await window.click("[data-testid=kubernetes-tab]"); await window.click("[data-testid=kubernetes-tab]");
await window.waitForSelector("[data-testid=repository-name]", { await window.waitForSelector("[data-testid=repository-name]", {
timeout: 140_000, timeout: 140_000,

View File

@ -196,8 +196,8 @@
"@hapi/call": "^8.0.1", "@hapi/call": "^8.0.1",
"@hapi/subtext": "^7.0.3", "@hapi/subtext": "^7.0.3",
"@kubernetes/client-node": "^0.16.1", "@kubernetes/client-node": "^0.16.1",
"@ogre-tools/injectable": "^1.4.1", "@ogre-tools/injectable": "1.5.0",
"@ogre-tools/injectable-react": "^1.4.1", "@ogre-tools/injectable-react": "1.5.2",
"@sentry/electron": "^2.5.4", "@sentry/electron": "^2.5.4",
"@sentry/integrations": "^6.15.0", "@sentry/integrations": "^6.15.0",
"abort-controller": "^3.0.0", "abort-controller": "^3.0.0",
@ -324,8 +324,8 @@
"@types/webpack-dev-server": "^3.11.6", "@types/webpack-dev-server": "^3.11.6",
"@types/webpack-env": "^1.16.3", "@types/webpack-env": "^1.16.3",
"@types/webpack-node-externals": "^1.7.1", "@types/webpack-node-externals": "^1.7.1",
"@typescript-eslint/eslint-plugin": "^4.33.0", "@typescript-eslint/eslint-plugin": "^5.7.0",
"@typescript-eslint/parser": "^4.33.0", "@typescript-eslint/parser": "^5.7.0",
"ansi_up": "^5.1.0", "ansi_up": "^5.1.0",
"chart.js": "^2.9.4", "chart.js": "^2.9.4",
"circular-dependency-plugin": "^5.2.2", "circular-dependency-plugin": "^5.2.2",
@ -361,8 +361,7 @@
"nodemon": "^2.0.14", "nodemon": "^2.0.14",
"playwright": "^1.17.1", "playwright": "^1.17.1",
"postcss": "^8.4.5", "postcss": "^8.4.5",
"postcss-loader": "4.3.0", "postcss-loader": "^4.3.0",
"postinstall-postinstall": "^2.1.0",
"progress-bar-webpack-plugin": "^2.1.0", "progress-bar-webpack-plugin": "^2.1.0",
"randomcolor": "^0.6.2", "randomcolor": "^0.6.2",
"raw-loader": "^4.0.2", "raw-loader": "^4.0.2",
@ -373,11 +372,11 @@
"react-select-event": "^5.1.0", "react-select-event": "^5.1.0",
"react-table": "^7.7.0", "react-table": "^7.7.0",
"react-window": "^1.8.6", "react-window": "^1.8.6",
"sass": "^1.44.0", "sass": "^1.45.1",
"sass-loader": "^8.0.2", "sass-loader": "^10.2.0",
"sharp": "^0.29.3", "sharp": "^0.29.3",
"style-loader": "^2.0.0", "style-loader": "^2.0.0",
"tailwindcss": "^2.2.19", "tailwindcss": "^3.0.7",
"ts-jest": "26.5.6", "ts-jest": "26.5.6",
"ts-loader": "^7.0.5", "ts-loader": "^7.0.5",
"ts-node": "^10.4.0", "ts-node": "^10.4.0",

32
src/common/ipc/catalog.ts Normal file
View File

@ -0,0 +1,32 @@
/**
* Copyright (c) 2021 OpenLens Authors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
* the Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
export enum CatalogIpcEvents {
/**
* This is broadcast on whenever there is an update to any catalog item
*/
ITEMS = "catalog:items",
/**
* This can be sent from renderer to main to initialize a broadcast of ITEMS
*/
INIT = "catalog:init",
}

View File

@ -18,10 +18,4 @@
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/ */
export const BundledExtensionsLoaded = "extension-loader:bundled-extensions-loaded";
module.exports = {
plugins: [
require("tailwindcss/nesting"),
require("tailwindcss"),
],
};

View File

@ -27,3 +27,4 @@ export * from "./update-available.ipc";
export * from "./cluster.ipc"; export * from "./cluster.ipc";
export * from "./type-enforced-ipc"; export * from "./type-enforced-ipc";
export * from "./hotbar"; export * from "./hotbar";
export * from "./extension-loader.ipc";

View File

@ -22,7 +22,7 @@
import { isMac, isWindows } from "./vars"; import { isMac, isWindows } from "./vars";
import wincaAPI from "win-ca/api"; import wincaAPI from "win-ca/api";
import https from "https"; import https from "https";
import { promiseExec } from "./utils/promise-exec"; import { promiseExecFile } from "./utils/promise-exec";
// DST Root CA X3, which was expired on 9.30.2021 // DST Root CA X3, which was expired on 9.30.2021
export const DSTRootCAX3 = "-----BEGIN CERTIFICATE-----\nMIIDSjCCAjKgAwIBAgIQRK+wgNajJ7qJMDmGLvhAazANBgkqhkiG9w0BAQUFADA/\nMSQwIgYDVQQKExtEaWdpdGFsIFNpZ25hdHVyZSBUcnVzdCBDby4xFzAVBgNVBAMT\nDkRTVCBSb290IENBIFgzMB4XDTAwMDkzMDIxMTIxOVoXDTIxMDkzMDE0MDExNVow\nPzEkMCIGA1UEChMbRGlnaXRhbCBTaWduYXR1cmUgVHJ1c3QgQ28uMRcwFQYDVQQD\nEw5EU1QgUm9vdCBDQSBYMzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB\nAN+v6ZdQCINXtMxiZfaQguzH0yxrMMpb7NnDfcdAwRgUi+DoM3ZJKuM/IUmTrE4O\nrz5Iy2Xu/NMhD2XSKtkyj4zl93ewEnu1lcCJo6m67XMuegwGMoOifooUMM0RoOEq\nOLl5CjH9UL2AZd+3UWODyOKIYepLYYHsUmu5ouJLGiifSKOeDNoJjj4XLh7dIN9b\nxiqKqy69cK3FCxolkHRyxXtqqzTWMIn/5WgTe1QLyNau7Fqckh49ZLOMxt+/yUFw\n7BZy1SbsOFU5Q9D8/RhcQPGX69Wam40dutolucbY38EVAjqr2m7xPi71XAicPNaD\naeQQmxkqtilX4+U9m5/wAl0CAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNV\nHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFMSnsaR7LHH62+FLkHX/xBVghYkQMA0GCSqG\nSIb3DQEBBQUAA4IBAQCjGiybFwBcqR7uKGY3Or+Dxz9LwwmglSBd49lZRNI+DT69\nikugdB/OEIKcdBodfpga3csTS7MgROSR6cz8faXbauX+5v3gTt23ADq1cEmv8uXr\nAvHRAosZy5Q6XkjEGB5YGV8eAlrwDPGxrancWYaLbumR9YbK+rlmM6pZW87ipxZz\nR8srzJmwN0jP41ZL9c8PDHIyh8bwRLtTcm1D9SZImlJnt1ir/md2cXjbDaJWFBM5\nJDGFoqgCWjBH4d1QB7wCCZAA62RjYJsWvIjJEubSfZGL+T0yjWW06XyxV3bqxbYo\nOb8VZRzI9neWagqNdwvYkQsEjgfbKbYK7p2CNTUQ\n-----END CERTIFICATE-----\n"; export const DSTRootCAX3 = "-----BEGIN CERTIFICATE-----\nMIIDSjCCAjKgAwIBAgIQRK+wgNajJ7qJMDmGLvhAazANBgkqhkiG9w0BAQUFADA/\nMSQwIgYDVQQKExtEaWdpdGFsIFNpZ25hdHVyZSBUcnVzdCBDby4xFzAVBgNVBAMT\nDkRTVCBSb290IENBIFgzMB4XDTAwMDkzMDIxMTIxOVoXDTIxMDkzMDE0MDExNVow\nPzEkMCIGA1UEChMbRGlnaXRhbCBTaWduYXR1cmUgVHJ1c3QgQ28uMRcwFQYDVQQD\nEw5EU1QgUm9vdCBDQSBYMzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB\nAN+v6ZdQCINXtMxiZfaQguzH0yxrMMpb7NnDfcdAwRgUi+DoM3ZJKuM/IUmTrE4O\nrz5Iy2Xu/NMhD2XSKtkyj4zl93ewEnu1lcCJo6m67XMuegwGMoOifooUMM0RoOEq\nOLl5CjH9UL2AZd+3UWODyOKIYepLYYHsUmu5ouJLGiifSKOeDNoJjj4XLh7dIN9b\nxiqKqy69cK3FCxolkHRyxXtqqzTWMIn/5WgTe1QLyNau7Fqckh49ZLOMxt+/yUFw\n7BZy1SbsOFU5Q9D8/RhcQPGX69Wam40dutolucbY38EVAjqr2m7xPi71XAicPNaD\naeQQmxkqtilX4+U9m5/wAl0CAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNV\nHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFMSnsaR7LHH62+FLkHX/xBVghYkQMA0GCSqG\nSIb3DQEBBQUAA4IBAQCjGiybFwBcqR7uKGY3Or+Dxz9LwwmglSBd49lZRNI+DT69\nikugdB/OEIKcdBodfpga3csTS7MgROSR6cz8faXbauX+5v3gTt23ADq1cEmv8uXr\nAvHRAosZy5Q6XkjEGB5YGV8eAlrwDPGxrancWYaLbumR9YbK+rlmM6pZW87ipxZz\nR8srzJmwN0jP41ZL9c8PDHIyh8bwRLtTcm1D9SZImlJnt1ir/md2cXjbDaJWFBM5\nJDGFoqgCWjBH4d1QB7wCCZAA62RjYJsWvIjJEubSfZGL+T0yjWW06XyxV3bqxbYo\nOb8VZRzI9neWagqNdwvYkQsEjgfbKbYK7p2CNTUQ\n-----END CERTIFICATE-----\n";
@ -33,19 +33,25 @@ export function isCertActive(cert: string) {
return !isExpired; return !isExpired;
} }
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions/Cheatsheet#other_assertions
const certSplitPattern = /(?=-----BEGIN\sCERTIFICATE-----)/g;
async function execSecurity(...args: string[]): Promise<string[]> {
const { stdout } = await promiseExecFile("/usr/bin/security", args);
return stdout.split(certSplitPattern);
}
/** /**
* Get root CA certificate from MacOSX system keychain * Get root CA certificate from MacOSX system keychain
* Only return non-expred certificates. * Only return non-expred certificates.
*/ */
export async function getMacRootCA() { export async function getMacRootCA() {
// inspired mac-ca https://github.com/jfromaniello/mac-ca // inspired mac-ca https://github.com/jfromaniello/mac-ca
const args = "find-certificate -a -p"; const [trusted, rootCA] = await Promise.all([
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions/Cheatsheet#other_assertions execSecurity("find-certificate", "-a", "-p"),
const splitPattern = /(?=-----BEGIN\sCERTIFICATE-----)/g; execSecurity("find-certificate", "-a", "-p", "/System/Library/Keychains/SystemRootCertificates.keychain"),
const systemRootCertsPath = "/System/Library/Keychains/SystemRootCertificates.keychain"; ]);
const bin = "/usr/bin/security";
const trusted = (await promiseExec(`${bin} ${args}`)).stdout.toString().split(splitPattern);
const rootCA = (await promiseExec(`${bin} ${args} ${systemRootCertsPath}`)).stdout.toString().split(splitPattern);
return [...new Set([...trusted, ...rootCA])].filter(isCertActive); return [...new Set([...trusted, ...rootCA])].filter(isCertActive);
} }

View File

@ -306,6 +306,36 @@ const updateChannel: PreferenceDescription<string> = {
}, },
}; };
export enum ExtensionRegistryLocation {
DEFAULT = "default",
NPMRC = "npmrc",
CUSTOM = "custom",
}
export type ExtensionRegistry = {
location: ExtensionRegistryLocation.DEFAULT | ExtensionRegistryLocation.NPMRC;
customUrl?: undefined;
} | {
location: ExtensionRegistryLocation.CUSTOM,
customUrl: string;
};
export const defaultExtensionRegistryUrl = "https://registry.npmjs.org";
const extensionRegistryUrl: PreferenceDescription<ExtensionRegistry> = {
fromStore(val) {
return val ?? {
location: ExtensionRegistryLocation.DEFAULT,
};
},
toStore(val) {
if (val.location === ExtensionRegistryLocation.DEFAULT) {
return undefined;
}
return val;
},
};
type PreferencesModelType<field extends keyof typeof DESCRIPTORS> = typeof DESCRIPTORS[field] extends PreferenceDescription<infer T, any> ? T : never; type PreferencesModelType<field extends keyof typeof DESCRIPTORS> = typeof DESCRIPTORS[field] extends PreferenceDescription<infer T, any> ? T : never;
type UserStoreModelType<field extends keyof typeof DESCRIPTORS> = typeof DESCRIPTORS[field] extends PreferenceDescription<any, infer T> ? T : never; type UserStoreModelType<field extends keyof typeof DESCRIPTORS> = typeof DESCRIPTORS[field] extends PreferenceDescription<any, infer T> ? T : never;
@ -335,6 +365,7 @@ export const DESCRIPTORS = {
editorConfiguration, editorConfiguration,
terminalCopyOnSelect, terminalCopyOnSelect,
updateChannel, updateChannel,
extensionRegistryUrl,
}; };
export const CONSTANTS = { export const CONSTANTS = {

View File

@ -29,7 +29,7 @@ import { kubeConfigDefaultPath } from "../kube-helpers";
import { appEventBus } from "../event-bus"; import { appEventBus } from "../event-bus";
import path from "path"; import path from "path";
import { ObservableToggleSet, toJS } from "../../renderer/utils"; import { ObservableToggleSet, toJS } from "../../renderer/utils";
import { DESCRIPTORS, EditorConfiguration, KubeconfigSyncValue, UserPreferencesModel } from "./preferences-helpers"; import { DESCRIPTORS, EditorConfiguration, ExtensionRegistry, KubeconfigSyncValue, UserPreferencesModel } from "./preferences-helpers";
import logger from "../../main/logger"; import logger from "../../main/logger";
import { AppPaths } from "../app-paths"; import { AppPaths } from "../app-paths";
@ -75,6 +75,7 @@ export class UserStore extends BaseStore<UserStoreModel> /* implements UserStore
@observable kubectlBinariesPath?: string; @observable kubectlBinariesPath?: string;
@observable terminalCopyOnSelect: boolean; @observable terminalCopyOnSelect: boolean;
@observable updateChannel?: string; @observable updateChannel?: string;
@observable extensionRegistryUrl: ExtensionRegistry;
/** /**
* Download kubectl binaries matching cluster version * Download kubectl binaries matching cluster version
@ -201,6 +202,7 @@ export class UserStore extends BaseStore<UserStoreModel> /* implements UserStore
this.editorConfiguration = DESCRIPTORS.editorConfiguration.fromStore(preferences?.editorConfiguration); this.editorConfiguration = DESCRIPTORS.editorConfiguration.fromStore(preferences?.editorConfiguration);
this.terminalCopyOnSelect = DESCRIPTORS.terminalCopyOnSelect.fromStore(preferences?.terminalCopyOnSelect); this.terminalCopyOnSelect = DESCRIPTORS.terminalCopyOnSelect.fromStore(preferences?.terminalCopyOnSelect);
this.updateChannel = DESCRIPTORS.updateChannel.fromStore(preferences?.updateChannel); this.updateChannel = DESCRIPTORS.updateChannel.fromStore(preferences?.updateChannel);
this.extensionRegistryUrl = DESCRIPTORS.extensionRegistryUrl.fromStore(preferences?.extensionRegistryUrl);
} }
toJSON(): UserStoreModel { toJSON(): UserStoreModel {
@ -224,6 +226,7 @@ export class UserStore extends BaseStore<UserStoreModel> /* implements UserStore
editorConfiguration: DESCRIPTORS.editorConfiguration.toStore(this.editorConfiguration), editorConfiguration: DESCRIPTORS.editorConfiguration.toStore(this.editorConfiguration),
terminalCopyOnSelect: DESCRIPTORS.terminalCopyOnSelect.toStore(this.terminalCopyOnSelect), terminalCopyOnSelect: DESCRIPTORS.terminalCopyOnSelect.toStore(this.terminalCopyOnSelect),
updateChannel: DESCRIPTORS.updateChannel.toStore(this.updateChannel), updateChannel: DESCRIPTORS.updateChannel.toStore(this.updateChannel),
extensionRegistryUrl: DESCRIPTORS.extensionRegistryUrl.toStore(this.extensionRegistryUrl),
}, },
}; };

View File

@ -48,6 +48,7 @@ export * from "./n-fircate";
export * from "./objects"; export * from "./objects";
export * from "./openExternal"; export * from "./openExternal";
export * from "./paths"; export * from "./paths";
export * from "./promise-exec";
export * from "./reject-promise"; export * from "./reject-promise";
export * from "./singleton"; export * from "./singleton";
export * from "./sort-compare"; export * from "./sort-compare";

View File

@ -20,7 +20,6 @@
*/ */
import * as util from "util"; import * as util from "util";
import { exec, execFile } from "child_process"; import { execFile } from "child_process";
export const promiseExec = util.promisify(exec);
export const promiseExecFile = util.promisify(execFile); export const promiseExecFile = util.promisify(execFile);

View File

@ -25,10 +25,10 @@ import { isEqual } from "lodash";
import { action, computed, makeObservable, observable, observe, reaction, when } from "mobx"; import { action, computed, makeObservable, observable, observe, reaction, when } from "mobx";
import path from "path"; import path from "path";
import { AppPaths } from "../../common/app-paths"; import { AppPaths } from "../../common/app-paths";
import { ClusterStore } from "../../common/cluster-store";
import { broadcastMessage, ipcMainOn, ipcRendererOn, requestMain, ipcMainHandle } from "../../common/ipc"; import { broadcastMessage, ipcMainOn, ipcRendererOn, requestMain, ipcMainHandle } from "../../common/ipc";
import { Disposer, getHostedClusterId, toJS } from "../../common/utils"; import { Disposer, toJS } from "../../common/utils";
import logger from "../../main/logger"; import logger from "../../main/logger";
import type { KubernetesCluster } from "../common-api/catalog";
import type { InstalledExtension } from "../extension-discovery"; import type { InstalledExtension } from "../extension-discovery";
import { ExtensionsStore } from "../extensions-store"; import { ExtensionsStore } from "../extensions-store";
import type { LensExtension, LensExtensionConstructor, LensExtensionId } from "../lens-extension"; import type { LensExtension, LensExtensionConstructor, LensExtensionId } from "../lens-extension";
@ -255,7 +255,8 @@ export class ExtensionLoader {
loadOnClusterManagerRenderer() { loadOnClusterManagerRenderer() {
logger.debug(`${logModule}: load on main renderer (cluster manager)`); logger.debug(`${logModule}: load on main renderer (cluster manager)`);
this.autoInitExtensions(async (extension: LensRendererExtension) => {
return this.autoInitExtensions(async (extension: LensRendererExtension) => {
const removeItems = [ const removeItems = [
registries.GlobalPageRegistry.getInstance().add(extension.globalPages, extension), registries.GlobalPageRegistry.getInstance().add(extension.globalPages, extension),
registries.AppPreferenceRegistry.getInstance().add(extension.appPreferences), registries.AppPreferenceRegistry.getInstance().add(extension.appPreferences),
@ -280,12 +281,11 @@ export class ExtensionLoader {
}); });
} }
loadOnClusterRenderer() { loadOnClusterRenderer(entity: KubernetesCluster) {
logger.debug(`${logModule}: load on cluster renderer (dashboard)`); logger.debug(`${logModule}: load on cluster renderer (dashboard)`);
const cluster = ClusterStore.getInstance().getById(getHostedClusterId());
this.autoInitExtensions(async (extension: LensRendererExtension) => { this.autoInitExtensions(async (extension: LensRendererExtension) => {
if ((await extension.isEnabledForCluster(cluster)) === false) { if ((await extension.isEnabledForCluster(entity)) === false) {
return []; return [];
} }
@ -312,7 +312,9 @@ export class ExtensionLoader {
} }
protected autoInitExtensions(register: (ext: LensExtension) => Promise<Disposer[]>) { protected autoInitExtensions(register: (ext: LensExtension) => Promise<Disposer[]>) {
return reaction(() => this.toJSON(), installedExtensions => { const loadingExtensions: { isBundled: boolean, loaded: Promise<void> }[] = [];
reaction(() => this.toJSON(), installedExtensions => {
for (const [extId, extension] of installedExtensions) { for (const [extId, extension] of installedExtensions) {
const alreadyInit = this.instances.has(extId) || this.nonInstancesByName.has(extension.manifest.name); const alreadyInit = this.instances.has(extId) || this.nonInstancesByName.has(extension.manifest.name);
@ -327,7 +329,14 @@ export class ExtensionLoader {
const instance = new LensExtensionClass(extension); const instance = new LensExtensionClass(extension);
instance.enable(register); const loaded = instance.enable(register).catch((err) => {
logger.error(`${logModule}: failed to enable`, { ext: extension, err });
});
loadingExtensions.push({
isBundled: extension.isBundled,
loaded,
});
this.instances.set(extId, instance); this.instances.set(extId, instance);
} catch (err) { } catch (err) {
logger.error(`${logModule}: activation extension error`, { ext: extension, err }); logger.error(`${logModule}: activation extension error`, { ext: extension, err });
@ -339,6 +348,8 @@ export class ExtensionLoader {
}, { }, {
fireImmediately: true, fireImmediately: true,
}); });
return loadingExtensions;
} }
protected requireExtension(extension: InstalledExtension): LensExtensionConstructor | null { protected requireExtension(extension: InstalledExtension): LensExtensionConstructor | null {

View File

@ -20,13 +20,13 @@
*/ */
import type * as registries from "./registries"; import type * as registries from "./registries";
import type { Cluster } from "../main/cluster";
import { Disposers, LensExtension } from "./lens-extension"; import { Disposers, LensExtension } from "./lens-extension";
import { getExtensionPageUrl } from "./registries/page-registry"; import { getExtensionPageUrl } from "./registries/page-registry";
import type { CatalogEntity } from "../common/catalog"; import type { CatalogEntity } from "../common/catalog";
import type { Disposer } from "../common/utils"; import type { Disposer } from "../common/utils";
import { catalogEntityRegistry, EntityFilter } from "../renderer/api/catalog-entity-registry"; import { catalogEntityRegistry, EntityFilter } from "../renderer/api/catalog-entity-registry";
import { catalogCategoryRegistry, CategoryFilter } from "../renderer/api/catalog-category-registry"; import { catalogCategoryRegistry, CategoryFilter } from "../renderer/api/catalog-category-registry";
import type { KubernetesCluster } from "../common/catalog-entities";
export class LensRendererExtension extends LensExtension { export class LensRendererExtension extends LensExtension {
globalPages: registries.PageRegistration[] = []; globalPages: registries.PageRegistration[] = [];
@ -57,9 +57,12 @@ export class LensRendererExtension extends LensExtension {
} }
/** /**
* Defines if extension is enabled for a given cluster. Defaults to `true`. * Defines if extension is enabled for a given cluster. This method is only
* called when the extension is created within a cluster frame.
*
* The default implementation is to return `true`
*/ */
async isEnabledForCluster(cluster: Cluster): Promise<Boolean> { async isEnabledForCluster(cluster: KubernetesCluster): Promise<Boolean> {
return (void cluster) || true; return (void cluster) || true;
} }

View File

@ -20,22 +20,25 @@
*/ */
import { reaction } from "mobx"; import { reaction } from "mobx";
import { broadcastMessage } from "../common/ipc"; import { broadcastMessage, ipcMainOn } from "../common/ipc";
import type { CatalogEntityRegistry } from "./catalog"; import type { CatalogEntityRegistry } from "./catalog";
import "../common/catalog-entities/kubernetes-cluster"; import "../common/catalog-entities/kubernetes-cluster";
import { toJS } from "../common/utils"; import { disposer, toJS } from "../common/utils";
import { debounce } from "lodash"; import { debounce } from "lodash";
import type { CatalogEntity } from "../common/catalog"; import type { CatalogEntity } from "../common/catalog";
import { CatalogIpcEvents } from "../common/ipc/catalog";
const broadcaster = debounce((items: CatalogEntity[]) => { const broadcaster = debounce((items: CatalogEntity[]) => {
broadcastMessage("catalog:items", items); broadcastMessage(CatalogIpcEvents.ITEMS, items);
}, 1_000, { leading: true, trailing: true }); }, 1_000, { leading: true, trailing: true });
export function pushCatalogToRenderer(catalog: CatalogEntityRegistry) { export function pushCatalogToRenderer(catalog: CatalogEntityRegistry) {
return reaction(() => toJS(catalog.items), (items) => { return disposer(
ipcMainOn(CatalogIpcEvents.INIT, () => broadcaster(toJS(catalog.items))),
reaction(() => toJS(catalog.items), (items) => {
broadcaster(items); broadcaster(items);
}, { }, {
fireImmediately: true, fireImmediately: true,
}); }),
);
} }

View File

@ -22,161 +22,230 @@
import * as tempy from "tempy"; import * as tempy from "tempy";
import fse from "fs-extra"; import fse from "fs-extra";
import * as yaml from "js-yaml"; import * as yaml from "js-yaml";
import { promiseExec } from "../../common/utils/promise-exec"; import { promiseExecFile } from "../../common/utils/promise-exec";
import { helmCli } from "./helm-cli"; import { helmCli } from "./helm-cli";
import type { Cluster } from "../cluster";
import { toCamelCase } from "../../common/utils/camelCase"; import { toCamelCase } from "../../common/utils/camelCase";
import type { BaseEncodingOptions } from "fs";
import { execFile, ExecFileOptions } from "child_process";
export async function listReleases(pathToKubeconfig: string, namespace?: string) { async function execHelm(args: string[], options?: BaseEncodingOptions & ExecFileOptions): Promise<string> {
const helm = await helmCli.binaryPath(); const helmCliPath = await helmCli.binaryPath();
const namespaceFlag = namespace ? `-n ${namespace}` : "--all-namespaces";
try { try {
const { stdout } = await promiseExec(`"${helm}" ls --output json ${namespaceFlag} --kubeconfig ${pathToKubeconfig}`); const { stdout } = await promiseExecFile(helmCliPath, args, options);
const output = JSON.parse(stdout);
if (output.length == 0) { return stdout;
return output;
}
output.forEach((release: any, index: number) => {
output[index] = toCamelCase(release);
});
return output;
} catch (error) { } catch (error) {
throw error?.stderr || error; throw error?.stderr || error;
} }
} }
export async function listReleases(pathToKubeconfig: string, namespace?: string): Promise<Record<string, any>[]> {
const args = [
"ls",
"--all",
"--output", "json",
];
export async function installChart(chart: string, values: any, name: string | undefined, namespace: string, version: string, pathToKubeconfig: string) { if (namespace) {
const helm = await helmCli.binaryPath(); args.push("-n", namespace);
const fileName = tempy.file({ name: "values.yaml" }); } else {
args.push("--all-namespaces");
}
await fse.writeFile(fileName, yaml.dump(values)); args.push("--kubeconfig", pathToKubeconfig);
try { const output = JSON.parse(await execHelm(args));
let generateName = "";
if (!Array.isArray(output) || output.length == 0) {
return [];
}
return output.map(toCamelCase);
}
export async function installChart(chart: string, values: any, name: string | undefined = "", namespace: string, version: string, kubeconfigPath: string) {
const valuesFilePath = tempy.file({ name: "values.yaml" });
await fse.writeFile(valuesFilePath, yaml.dump(values));
const args = ["install"];
if (name) {
args.push(name);
}
args.push(
chart,
"--version", version,
"--values", valuesFilePath,
"--namespace", namespace,
"--kubeconfig", kubeconfigPath,
);
if (!name) { if (!name) {
generateName = "--generate-name"; args.push("--generate-name");
name = "";
} }
const { stdout } = await promiseExec(`"${helm}" install ${name} ${chart} --version ${version} -f ${fileName} --namespace ${namespace} --kubeconfig ${pathToKubeconfig} ${generateName}`);
const releaseName = stdout.split("\n")[0].split(" ")[1].trim(); try {
const output = await execHelm(args);
const releaseName = output.split("\n")[0].split(" ")[1].trim();
return { return {
log: stdout, log: output,
release: { release: {
name: releaseName, name: releaseName,
namespace, namespace,
}, },
}; };
} catch (error) {
throw error?.stderr || error;
} finally { } finally {
await fse.unlink(fileName); await fse.unlink(valuesFilePath);
} }
} }
export async function upgradeRelease(name: string, chart: string, values: any, namespace: string, version: string, cluster: Cluster) { export async function upgradeRelease(name: string, chart: string, values: any, namespace: string, version: string, kubeconfigPath: string, kubectlPath: string) {
const helm = await helmCli.binaryPath(); const valuesFilePath = tempy.file({ name: "values.yaml" });
const fileName = tempy.file({ name: "values.yaml" });
await fse.writeFile(fileName, yaml.dump(values)); await fse.writeFile(valuesFilePath, yaml.dump(values));
const args = [
"upgrade",
name,
chart,
"--version", version,
"--values", valuesFilePath,
"--namespace", namespace,
"--kubeconfig", kubeconfigPath,
];
try { try {
const proxyKubeconfig = await cluster.getProxyKubeconfigPath(); const output = await execHelm(args);
const { stdout } = await promiseExec(`"${helm}" upgrade ${name} ${chart} --version ${version} -f ${fileName} --namespace ${namespace} --kubeconfig ${proxyKubeconfig}`);
return { return {
log: stdout, log: output,
release: getRelease(name, namespace, cluster), release: getRelease(name, namespace, kubeconfigPath, kubectlPath),
}; };
} catch (error) {
throw error?.stderr || error;
} finally { } finally {
await fse.unlink(fileName); await fse.unlink(valuesFilePath);
} }
} }
export async function getRelease(name: string, namespace: string, cluster: Cluster) { export async function getRelease(name: string, namespace: string, kubeconfigPath: string, kubectlPath: string) {
try { const args = [
const helm = await helmCli.binaryPath(); "status",
const proxyKubeconfig = await cluster.getProxyKubeconfigPath(); name,
"--namespace", namespace,
"--kubeconfig", kubeconfigPath,
"--output", "json",
];
const { stdout } = await promiseExec(`"${helm}" status ${name} --output json --namespace ${namespace} --kubeconfig ${proxyKubeconfig}`, { const release = JSON.parse(await execHelm(args, {
maxBuffer: 32 * 1024 * 1024 * 1024, // 32 MiB maxBuffer: 32 * 1024 * 1024 * 1024, // 32 MiB
}); }));
const release = JSON.parse(stdout);
release.resources = await getResources(name, namespace, cluster); release.resources = await getResources(name, namespace, kubeconfigPath, kubectlPath);
return release; return release;
} catch (error) {
throw error?.stderr || error;
}
} }
export async function deleteRelease(name: string, namespace: string, pathToKubeconfig: string) { export async function deleteRelease(name: string, namespace: string, kubeconfigPath: string) {
try { return execHelm([
const helm = await helmCli.binaryPath(); "delete",
const { stdout } = await promiseExec(`"${helm}" delete ${name} --namespace ${namespace} --kubeconfig ${pathToKubeconfig}`); name,
"--namespace", namespace,
return stdout; "--kubeconfig", kubeconfigPath,
} catch (error) { ]);
throw error?.stderr || error;
}
} }
interface GetValuesOptions { interface GetValuesOptions {
namespace: string; namespace: string;
all?: boolean; all?: boolean;
pathToKubeconfig: string; kubeconfigPath: string;
} }
export async function getValues(name: string, { namespace, all = false, pathToKubeconfig }: GetValuesOptions) { export async function getValues(name: string, { namespace, all = false, kubeconfigPath }: GetValuesOptions) {
const args = [
"get",
"values",
name,
];
if (all) {
args.push("--all");
}
args.push(
"--output", "yaml",
"--namespace", namespace,
"--kubeconfig", kubeconfigPath,
);
return execHelm(args);
}
export async function getHistory(name: string, namespace: string, kubeconfigPath: string) {
return JSON.parse(await execHelm([
"history",
name,
"--output", "json",
"--namespace", namespace,
"--kubeconfig", kubeconfigPath,
]));
}
export async function rollback(name: string, namespace: string, revision: number, kubeconfigPath: string) {
return JSON.parse(await execHelm([
"rollback",
name,
"--namespace", namespace,
"--kubeconfig", kubeconfigPath,
]));
}
async function getResources(name: string, namespace: string, kubeconfigPath: string, kubectlPath: string) {
const helmArgs = [
"get",
"manifest",
name,
"--namespace", namespace,
"--kubeconfig", kubeconfigPath,
];
const kubectlArgs = [
"get",
"--namespace", namespace,
"--kubeconfig", kubeconfigPath,
"-f", "-",
"--output", "json",
];
try { try {
const helm = await helmCli.binaryPath(); const helmOutput = await execHelm(helmArgs);
const { stdout } = await promiseExec(`"${helm}" get values ${name} ${all ? "--all" : ""} --output yaml --namespace ${namespace} --kubeconfig ${pathToKubeconfig}`);
return stdout; return new Promise((resolve, reject) => {
} catch (error) { let stdout = "";
throw error?.stderr || error; let stderr = "";
const kubectl = execFile(kubectlPath, kubectlArgs);
kubectl
.on("exit", (code, signal) => {
if (typeof code === "number") {
if (code === 0) {
resolve(JSON.parse(stdout).items);
} else {
reject(stderr);
} }
} else {
reject(new Error(`Kubectl exited with signal ${signal}`));
} }
})
.on("error", reject);
export async function getHistory(name: string, namespace: string, pathToKubeconfig: string) { kubectl.stderr.on("data", output => stderr += output);
try { kubectl.stdout.on("data", output => stdout += output);
const helm = await helmCli.binaryPath(); kubectl.stdin.write(helmOutput);
const { stdout } = await promiseExec(`"${helm}" history ${name} --output json --namespace ${namespace} --kubeconfig ${pathToKubeconfig}`); kubectl.stdin.end();
});
return JSON.parse(stdout);
} catch (error) {
throw error?.stderr || error;
}
}
export async function rollback(name: string, namespace: string, revision: number, pathToKubeconfig: string) {
try {
const helm = await helmCli.binaryPath();
const { stdout } = await promiseExec(`"${helm}" rollback ${name} ${revision} --namespace ${namespace} --kubeconfig ${pathToKubeconfig}`);
return stdout;
} catch (error) {
throw error?.stderr || error;
}
}
async function getResources(name: string, namespace: string, cluster: Cluster) {
try {
const helm = await helmCli.binaryPath();
const kubectl = await cluster.ensureKubectl();
const kubectlPath = await kubectl.getPath();
const pathToKubeconfig = await cluster.getProxyKubeconfigPath();
const { stdout } = await promiseExec(`"${helm}" get manifest ${name} --namespace ${namespace} --kubeconfig ${pathToKubeconfig} | "${kubectlPath}" get -n ${namespace} --kubeconfig ${pathToKubeconfig} -f - -o=json`);
return JSON.parse(stdout).items;
} catch { } catch {
return []; return [];
} }

View File

@ -20,13 +20,14 @@
*/ */
import yaml from "js-yaml"; import yaml from "js-yaml";
import { readFile } from "fs-extra"; import { BaseEncodingOptions, readFile } from "fs-extra";
import { promiseExec } from "../../common/utils/promise-exec"; import { promiseExecFile } from "../../common/utils/promise-exec";
import { helmCli } from "./helm-cli"; import { helmCli } from "./helm-cli";
import { Singleton } from "../../common/utils/singleton"; import { Singleton } from "../../common/utils/singleton";
import { customRequestPromise } from "../../common/request"; import { customRequestPromise } from "../../common/request";
import orderBy from "lodash/orderBy"; import orderBy from "lodash/orderBy";
import logger from "../logger"; import logger from "../logger";
import type { ExecFileOptions } from "child_process";
export type HelmEnv = Record<string, string> & { export type HelmEnv = Record<string, string> & {
HELM_REPOSITORY_CACHE?: string; HELM_REPOSITORY_CACHE?: string;
@ -49,6 +50,18 @@ export interface HelmRepo {
password?: string, password?: string,
} }
async function execHelm(args: string[], options?: BaseEncodingOptions & ExecFileOptions): Promise<string> {
const helmCliPath = await helmCli.binaryPath();
try {
const { stdout } = await promiseExecFile(helmCliPath, args, options);
return stdout;
} catch (error) {
throw error?.stderr || error;
}
}
export class HelmRepoManager extends Singleton { export class HelmRepoManager extends Singleton {
protected repos: HelmRepo[]; protected repos: HelmRepo[];
protected helmEnv: HelmEnv; protected helmEnv: HelmEnv;
@ -77,11 +90,8 @@ export class HelmRepoManager extends Singleton {
} }
protected static async parseHelmEnv() { protected static async parseHelmEnv() {
const helm = await helmCli.binaryPath(); const output = await execHelm(["env"]);
const { stdout } = await promiseExec(`"${helm}" env`).catch((error) => { const lines = output.split(/\r?\n/); // split by new line feed
throw(error.stderr);
});
const lines = stdout.split(/\r?\n/); // split by new line feed
const env: HelmEnv = {}; const env: HelmEnv = {};
lines.forEach((line: string) => { lines.forEach((line: string) => {
@ -135,57 +145,73 @@ export class HelmRepoManager extends Singleton {
cacheFilePath: `${this.helmEnv.HELM_REPOSITORY_CACHE}/${repo.name}-index.yaml`, cacheFilePath: `${this.helmEnv.HELM_REPOSITORY_CACHE}/${repo.name}-index.yaml`,
})); }));
} catch (error) { } catch (error) {
logger.error(`[HELM]: repositories listing error "${error}"`); logger.error(`[HELM]: repositories listing error`, error);
return []; return [];
} }
} }
public static async update() { public static async update() {
const helm = await helmCli.binaryPath(); return execHelm([
const { stdout } = await promiseExec(`"${helm}" repo update`).catch((error) => { "repo",
return { stdout: error.stdout }; "update",
}); ]);
return stdout;
} }
public static async addRepo({ name, url }: HelmRepo) { public static async addRepo({ name, url }: HelmRepo) {
logger.info(`[HELM]: adding repo "${name}" from ${url}`); logger.info(`[HELM]: adding repo "${name}" from ${url}`);
const helm = await helmCli.binaryPath();
const { stdout } = await promiseExec(`"${helm}" repo add ${name} ${url}`).catch((error) => {
throw(error.stderr);
});
return stdout; return execHelm([
"repo",
"add",
name,
url,
]);
} }
public static async addCustomRepo(repoAttributes : HelmRepo) { public static async addCustomRepo({ name, url, insecureSkipTlsVerify, username, password, caFile, keyFile, certFile }: HelmRepo) {
logger.info(`[HELM]: adding repo "${repoAttributes.name}" from ${repoAttributes.url}`); logger.info(`[HELM]: adding repo ${name} from ${url}`);
const helm = await helmCli.binaryPath(); const args = [
"repo",
"add",
name,
url,
];
const insecureSkipTlsVerify = repoAttributes.insecureSkipTlsVerify ? " --insecure-skip-tls-verify" : ""; if (insecureSkipTlsVerify) {
const username = repoAttributes.username ? ` --username "${repoAttributes.username}"` : ""; args.push("--insecure-skip-tls-verify");
const password = repoAttributes.password ? ` --password "${repoAttributes.password}"` : ""; }
const caFile = repoAttributes.caFile ? ` --ca-file "${repoAttributes.caFile}"` : "";
const keyFile = repoAttributes.keyFile ? ` --key-file "${repoAttributes.keyFile}"` : "";
const certFile = repoAttributes.certFile ? ` --cert-file "${repoAttributes.certFile}"` : "";
const addRepoCommand = `"${helm}" repo add ${repoAttributes.name} ${repoAttributes.url}${insecureSkipTlsVerify}${username}${password}${caFile}${keyFile}${certFile}`; if (username) {
const { stdout } = await promiseExec(addRepoCommand).catch((error) => { args.push("--username", username);
throw(error.stderr); }
});
return stdout; if (password) {
args.push("--password", password);
}
if (caFile) {
args.push("--ca-file", caFile);
}
if (keyFile) {
args.push("--key-file", keyFile);
}
if (certFile) {
args.push("--cert-file", certFile);
}
return execHelm(args);
} }
public static async removeRepo({ name, url }: HelmRepo): Promise<string> { public static async removeRepo({ name, url }: HelmRepo): Promise<string> {
logger.info(`[HELM]: removing repo "${name}" from ${url}`); logger.info(`[HELM]: removing repo ${name} (${url})`);
const helm = await helmCli.binaryPath();
const { stdout } = await promiseExec(`"${helm}" repo remove ${name}`).catch((error) => {
throw(error.stderr);
});
return stdout; return execHelm([
"repo",
"remove",
name,
]);
} }
} }

View File

@ -65,13 +65,19 @@ class HelmService {
public async listReleases(cluster: Cluster, namespace: string = null) { public async listReleases(cluster: Cluster, namespace: string = null) {
const proxyKubeconfig = await cluster.getProxyKubeconfigPath(); const proxyKubeconfig = await cluster.getProxyKubeconfigPath();
logger.debug("list releases");
return listReleases(proxyKubeconfig, namespace); return listReleases(proxyKubeconfig, namespace);
} }
public async getRelease(cluster: Cluster, releaseName: string, namespace: string) { public async getRelease(cluster: Cluster, releaseName: string, namespace: string) {
const kubeconfigPath = await cluster.getProxyKubeconfigPath();
const kubectl = await cluster.ensureKubectl();
const kubectlPath = await kubectl.getPath();
logger.debug("Fetch release"); logger.debug("Fetch release");
return getRelease(releaseName, namespace, cluster); return getRelease(releaseName, namespace, kubeconfigPath, kubectlPath);
} }
public async getReleaseValues(releaseName: string, { cluster, namespace, all }: GetReleaseValuesArgs) { public async getReleaseValues(releaseName: string, { cluster, namespace, all }: GetReleaseValuesArgs) {
@ -79,7 +85,7 @@ class HelmService {
logger.debug("Fetch release values"); logger.debug("Fetch release values");
return getValues(releaseName, { namespace, all, pathToKubeconfig }); return getValues(releaseName, { namespace, all, kubeconfigPath: pathToKubeconfig });
} }
public async getReleaseHistory(cluster: Cluster, releaseName: string, namespace: string) { public async getReleaseHistory(cluster: Cluster, releaseName: string, namespace: string) {
@ -99,9 +105,13 @@ class HelmService {
} }
public async updateRelease(cluster: Cluster, releaseName: string, namespace: string, data: { chart: string; values: {}; version: string }) { public async updateRelease(cluster: Cluster, releaseName: string, namespace: string, data: { chart: string; values: {}; version: string }) {
const proxyKubeconfig = await cluster.getProxyKubeconfigPath();
const kubectl = await cluster.ensureKubectl();
const kubectlPath = await kubectl.getPath();
logger.debug("Upgrade release"); logger.debug("Upgrade release");
return upgradeRelease(releaseName, data.chart, data.values, namespace, data.version, cluster); return upgradeRelease(releaseName, data.chart, data.values, namespace, data.version, proxyKubeconfig, kubectlPath);
} }
public async rollback(cluster: Cluster, releaseName: string, namespace: string, revision: number) { public async rollback(cluster: Cluster, releaseName: string, namespace: string, revision: number) {

View File

@ -21,7 +21,7 @@
import path from "path"; import path from "path";
import fs from "fs"; import fs from "fs";
import { promiseExec } from "../common/utils/promise-exec"; import { promiseExecFile } from "../common/utils/promise-exec";
import logger from "./logger"; import logger from "./logger";
import { ensureDir, pathExists } from "fs-extra"; import { ensureDir, pathExists } from "fs-extra";
import * as lockFile from "proper-lockfile"; import * as lockFile from "proper-lockfile";
@ -199,7 +199,12 @@ export class Kubectl {
if (exists) { if (exists) {
try { try {
const { stdout } = await promiseExec(`"${path}" version --client=true -o json`); const args = [
"version",
"--client", "true",
"--output", "json",
];
const { stdout } = await promiseExecFile(path, args);
const output = JSON.parse(stdout); const output = JSON.parse(stdout);
if (!checkVersion) { if (!checkVersion) {

View File

@ -46,7 +46,7 @@ async function loadMetrics(promQueries: string[], cluster: Cluster, prometheusPa
return await getMetrics(cluster, prometheusPath, { query, ...queryParams }); return await getMetrics(cluster, prometheusPath, { query, ...queryParams });
} catch (error) { } catch (error) {
if (lastAttempt || (error?.statusCode >= 400 && error?.statusCode < 500)) { if (lastAttempt || (error?.statusCode >= 400 && error?.statusCode < 500)) {
logger.error("[Metrics]: metrics not available", error); logger.error("[Metrics]: metrics not available", error?.response ? error.response?.body : error);
throw new Error("Metrics not available"); throw new Error("Metrics not available");
} }

View File

@ -24,7 +24,7 @@ import { makeObservable, observable } from "mobx";
import { app, BrowserWindow, dialog, ipcMain, shell, webContents } from "electron"; import { app, BrowserWindow, dialog, ipcMain, shell, webContents } from "electron";
import windowStateKeeper from "electron-window-state"; import windowStateKeeper from "electron-window-state";
import { appEventBus } from "../common/event-bus"; import { appEventBus } from "../common/event-bus";
import { ipcMainOn } from "../common/ipc"; import { BundledExtensionsLoaded, ipcMainOn } from "../common/ipc";
import { delay, iter, Singleton } from "../common/utils"; import { delay, iter, Singleton } from "../common/utils";
import { ClusterFrameInfo, clusterFrameMap } from "../common/cluster-frames"; import { ClusterFrameInfo, clusterFrameMap } from "../common/cluster-frames";
import { IpcRendererNavigationEvents } from "../renderer/navigation/events"; import { IpcRendererNavigationEvents } from "../renderer/navigation/events";
@ -181,7 +181,7 @@ export class WindowManager extends Singleton {
if (!this.mainWindow) { if (!this.mainWindow) {
viewHasLoaded = new Promise<void>(resolve => { viewHasLoaded = new Promise<void>(resolve => {
ipcMain.once(IpcRendererNavigationEvents.LOADED, () => resolve()); ipcMain.once(BundledExtensionsLoaded, () => resolve());
}); });
await this.initMainWindow(showSplash); await this.initMainWindow(showSplash);
} }

View File

@ -30,6 +30,8 @@ import { once } from "lodash";
import logger from "../../common/logger"; import logger from "../../common/logger";
import { catalogEntityRunContext } from "./catalog-entity"; import { catalogEntityRunContext } from "./catalog-entity";
import { CatalogRunEvent } from "../../common/catalog/catalog-run-event"; import { CatalogRunEvent } from "../../common/catalog/catalog-run-event";
import { ipcRenderer } from "electron";
import { CatalogIpcEvents } from "../../common/ipc/catalog";
export type EntityFilter = (entity: CatalogEntity) => any; export type EntityFilter = (entity: CatalogEntity) => any;
export type CatalogEntityOnBeforeRun = (event: CatalogRunEvent) => void | Promise<void>; export type CatalogEntityOnBeforeRun = (event: CatalogRunEvent) => void | Promise<void>;
@ -70,9 +72,12 @@ export class CatalogEntityRegistry {
} }
init() { init() {
ipcRendererOn("catalog:items", (event, items: (CatalogEntityData & CatalogEntityKindData)[]) => { ipcRendererOn(CatalogIpcEvents.ITEMS, (event, items: (CatalogEntityData & CatalogEntityKindData)[]) => {
this.updateItems(items); this.updateItems(items);
}); });
// Make sure that we get items ASAP and not the next time one of them changes
ipcRenderer.send(CatalogIpcEvents.INIT);
} }
@action updateItems(items: (CatalogEntityData & CatalogEntityKindData)[]) { @action updateItems(items: (CatalogEntityData & CatalogEntityKindData)[]) {

View File

@ -19,7 +19,7 @@
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/ */
import React from "react"; import React from "react";
import { observable, makeObservable } from "mobx"; import { observable, makeObservable, when } from "mobx";
import { disposeOnUnmount, observer } from "mobx-react"; import { disposeOnUnmount, observer } from "mobx-react";
import { Redirect, Route, Router, Switch } from "react-router"; import { Redirect, Route, Router, Switch } from "react-router";
import { history } from "./navigation"; import { history } from "./navigation";
@ -74,6 +74,7 @@ import { PortForwardDialog } from "./port-forward";
import { DeleteClusterDialog } from "./components/delete-cluster-dialog"; import { DeleteClusterDialog } from "./components/delete-cluster-dialog";
import { WorkloadsOverview } from "./components/+workloads-overview/overview"; import { WorkloadsOverview } from "./components/+workloads-overview/overview";
import { KubeObjectListLayout } from "./components/kube-object-list-layout"; import { KubeObjectListLayout } from "./components/kube-object-list-layout";
import type { KubernetesCluster } from "../common/catalog-entities";
@observer @observer
export class ClusterFrame extends React.Component { export class ClusterFrame extends React.Component {
@ -101,7 +102,18 @@ export class ClusterFrame extends React.Component {
catalogEntityRegistry.activeEntity = ClusterFrame.clusterId; catalogEntityRegistry.activeEntity = ClusterFrame.clusterId;
extensionLoader.loadOnClusterRenderer(); // Only load the extensions once the catalog has been populated
when(
() => Boolean(catalogEntityRegistry.activeEntity),
() => extensionLoader.loadOnClusterRenderer(catalogEntityRegistry.activeEntity as KubernetesCluster),
{
timeout: 15_000,
onError: (error) => {
console.warn("[CLUSTER-FRAME]: error from activeEntity when()", error);
Notifications.error("Failed to get KubernetesCluster for this view. Extensions will not be loaded.");
},
},
);
setTimeout(() => { setTimeout(() => {
appEventBus.emit({ appEventBus.emit({

View File

@ -19,7 +19,7 @@
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/ */
import styles from "./add-cluster.module.css"; import styles from "./add-cluster.module.scss";
import type { KubeConfig } from "@kubernetes/client-node"; import type { KubeConfig } from "@kubernetes/client-node";
import fse from "fs-extra"; import fse from "fs-extra";

View File

@ -19,7 +19,7 @@
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/ */
import styles from "./catalog-entity-details.module.css"; import styles from "./catalog-entity-details.module.scss";
import React, { Component } from "react"; import React, { Component } from "react";
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import { Drawer, DrawerItem } from "../drawer"; import { Drawer, DrawerItem } from "../drawer";

View File

@ -0,0 +1,116 @@
/**
* Copyright (c) 2021 OpenLens Authors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
* the Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
import styles from "./catalog.module.scss";
import React from "react";
import { action, computed } from "mobx";
import { CatalogEntity } from "../../api/catalog-entity";
import type { ItemObject } from "../../../common/item.store";
import { Badge } from "../badge";
import { navigation } from "../../navigation";
import { searchUrlParam } from "../input";
import { KubeObject } from "../../../common/k8s-api/kube-object";
import type { CatalogEntityRegistry } from "../../api/catalog-entity-registry";
export class CatalogEntityItem<T extends CatalogEntity> implements ItemObject {
constructor(public entity: T, private registry: CatalogEntityRegistry) {
if (!(entity instanceof CatalogEntity)) {
throw Object.assign(new TypeError("CatalogEntityItem cannot wrap a non-CatalogEntity type"), { typeof: typeof entity, prototype: Object.getPrototypeOf(entity) });
}
}
get kind() {
return this.entity.kind;
}
get apiVersion() {
return this.entity.apiVersion;
}
get name() {
return this.entity.metadata.name;
}
getName() {
return this.entity.metadata.name;
}
get id() {
return this.entity.metadata.uid;
}
getId() {
return this.id;
}
@computed get phase() {
return this.entity.status.phase;
}
get enabled() {
return this.entity.status.enabled ?? true;
}
get labels() {
return KubeObject.stringifyLabels(this.entity.metadata.labels);
}
getLabelBadges(onClick?: React.MouseEventHandler<any>) {
return this.labels
.map(label => (
<Badge
scrollable
className={styles.badge}
key={label}
label={label}
title={label}
onClick={(event) => {
navigation.searchParams.set(searchUrlParam.name, label);
onClick?.(event);
event.stopPropagation();
}}
expandable={false}
/>
));
}
get source() {
return this.entity.metadata.source || "unknown";
}
get searchFields() {
return [
this.name,
this.id,
this.phase,
`source=${this.source}`,
...this.labels,
];
}
onRun() {
this.registry.onRun(this.entity);
}
@action
async onContextMenuOpen(ctx: any) {
return this.entity.onContextMenuOpen(ctx);
}
}

View File

@ -19,8 +19,8 @@
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/ */
import treeStyles from "./catalog-tree.module.css"; import treeStyles from "./catalog-tree.module.scss";
import styles from "./catalog-menu.module.css"; import styles from "./catalog-menu.module.scss";
import React from "react"; import React from "react";
import { TreeItem, TreeItemProps, TreeView } from "@material-ui/lab"; import { TreeItem, TreeItemProps, TreeView } from "@material-ui/lab";

View File

@ -19,7 +19,7 @@
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/ */
import styles from "./catalog.module.css"; import styles from "./catalog.module.scss";
import React from "react"; import React from "react";
import { disposeOnUnmount, observer } from "mobx-react"; import { disposeOnUnmount, observer } from "mobx-react";

View File

@ -19,7 +19,7 @@
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/ */
import styles from "./cluster-issues.module.css"; import styles from "./cluster-issues.module.scss";
import React from "react"; import React from "react";
import { observer } from "mobx-react"; import { observer } from "mobx-react";

View File

@ -19,7 +19,7 @@
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/ */
import styles from "./cluster-metrics.module.css"; import styles from "./cluster-metrics.module.scss";
import React from "react"; import React from "react";
import { observer } from "mobx-react"; import { observer } from "mobx-react";

View File

@ -19,7 +19,7 @@
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/ */
import styles from "./cluster-overview.module.css"; import styles from "./cluster-overview.module.scss";
import React from "react"; import React from "react";
import { reaction } from "mobx"; import { reaction } from "mobx";

View File

@ -19,7 +19,7 @@
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/ */
import styles from "./cluster-pie-charts.module.css"; import styles from "./cluster-pie-charts.module.scss";
import React from "react"; import React from "react";
import { observer } from "mobx-react"; import { observer } from "mobx-react";

View File

@ -19,7 +19,7 @@
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/ */
import styles from "./entity-settings.module.css"; import styles from "./entity-settings.module.scss";
import React from "react"; import React from "react";
import { observable, makeObservable } from "mobx"; import { observable, makeObservable } from "mobx";

View File

@ -22,10 +22,12 @@ import type { Injectable } from "@ogre-tools/injectable";
import { lifecycleEnum } from "@ogre-tools/injectable"; import { lifecycleEnum } from "@ogre-tools/injectable";
import { attemptInstallByInfo, ExtensionInfo } from "./attempt-install-by-info"; import { attemptInstallByInfo, ExtensionInfo } from "./attempt-install-by-info";
import attemptInstallInjectable from "../attempt-install/attempt-install.injectable"; import attemptInstallInjectable from "../attempt-install/attempt-install.injectable";
import getBaseRegistryUrlInjectable from "../get-base-registry-url/get-base-registry-url.injectable";
const attemptInstallByInfoInjectable: Injectable<(extensionInfo: ExtensionInfo) => Promise<void>, {}> = { const attemptInstallByInfoInjectable: Injectable<(extensionInfo: ExtensionInfo) => Promise<void>, {}> = {
getDependencies: di => ({ getDependencies: di => ({
attemptInstall: di.inject(attemptInstallInjectable), attemptInstall: di.inject(attemptInstallInjectable),
getBaseRegistryUrl: di.inject(getBaseRegistryUrlInjectable),
}), }),
instantiate: attemptInstallByInfo, instantiate: attemptInstallByInfo,

View File

@ -36,32 +36,39 @@ export interface ExtensionInfo {
} }
export interface Dependencies { export interface Dependencies {
attemptInstall: (request: InstallRequest, d: ExtendableDisposer) => Promise<void> attemptInstall: (request: InstallRequest, d: ExtendableDisposer) => Promise<void>;
getBaseRegistryUrl: () => Promise<string>;
} }
export const attemptInstallByInfo = ({ attemptInstall }: Dependencies) => async ({ export const attemptInstallByInfo = ({ attemptInstall, getBaseRegistryUrl }: Dependencies) => async ({
name, name,
version, version,
requireConfirmation = false, requireConfirmation = false,
}: ExtensionInfo) => { }: ExtensionInfo) => {
const disposer = ExtensionInstallationStateStore.startPreInstall(); const disposer = ExtensionInstallationStateStore.startPreInstall();
const registryUrl = new URLParse("https://registry.npmjs.com") const baseUrl = await getBaseRegistryUrl();
.set("pathname", name) const registryUrl = new URLParse(baseUrl).set("pathname", name).toString();
.toString(); let json: any;
const { promise } = downloadJson({ url: registryUrl });
const json = await promise.catch(console.error);
if ( try {
!json || json = await downloadJson({ url: registryUrl }).promise;
json.error ||
typeof json.versions !== "object" || if (!json || json.error || typeof json.versions !== "object" || !json.versions) {
!json.versions
) {
const message = json?.error ? `: ${json.error}` : ""; const message = json?.error ? `: ${json.error}` : "";
Notifications.error( Notifications.error(`Failed to get registry information for that extension${message}`);
`Failed to get registry information for that extension${message}`,
); return disposer();
}
} catch (error) {
if (error instanceof SyntaxError) {
// assume invalid JSON
console.warn("Set registry has invalid json", { url: baseUrl }, error);
Notifications.error("Failed to get valid registry information for that extension. Registry did not return valid JSON");
} else {
console.error("Failed to download registry information", error);
Notifications.error(`Failed to get valid registry information for that extension. ${error}`);
}
return disposer(); return disposer();
} }

View File

@ -49,8 +49,7 @@ import type { LensExtensionId } from "../../../extensions/lens-extension";
import installOnDropInjectable from "./install-on-drop/install-on-drop.injectable"; import installOnDropInjectable from "./install-on-drop/install-on-drop.injectable";
import { supportedExtensionFormats } from "./supported-extension-formats"; import { supportedExtensionFormats } from "./supported-extension-formats";
interface Props { interface Dependencies {
dependencies: {
userExtensions: IComputedValue<InstalledExtension[]>; userExtensions: IComputedValue<InstalledExtension[]>;
enableExtension: (id: LensExtensionId) => void; enableExtension: (id: LensExtensionId) => void;
disableExtension: (id: LensExtensionId) => void; disableExtension: (id: LensExtensionId) => void;
@ -58,25 +57,20 @@ interface Props {
installFromInput: (input: string) => Promise<void>; installFromInput: (input: string) => Promise<void>;
installFromSelectFileDialog: () => Promise<void>; installFromSelectFileDialog: () => Promise<void>;
installOnDrop: (files: File[]) => Promise<void>; installOnDrop: (files: File[]) => Promise<void>;
};
} }
@observer @observer
class NonInjectedExtensions extends React.Component<Props> { class NonInjectedExtensions extends React.Component<Dependencies> {
@observable installPath = ""; @observable installPath = "";
constructor(props: Props) { constructor(props: Dependencies) {
super(props); super(props);
makeObservable(this); makeObservable(this);
} }
get dependencies() {
return this.props.dependencies;
}
componentDidMount() { componentDidMount() {
disposeOnUnmount(this, [ disposeOnUnmount(this, [
reaction(() => this.dependencies.userExtensions.get().length, (curSize, prevSize) => { reaction(() => this.props.userExtensions.get().length, (curSize, prevSize) => {
if (curSize > prevSize) { if (curSize > prevSize) {
disposeOnUnmount(this, [ disposeOnUnmount(this, [
when(() => !ExtensionInstallationStateStore.anyInstalling, () => this.installPath = ""), when(() => !ExtensionInstallationStateStore.anyInstalling, () => this.installPath = ""),
@ -87,10 +81,10 @@ class NonInjectedExtensions extends React.Component<Props> {
} }
render() { render() {
const userExtensions = this.dependencies.userExtensions.get(); const userExtensions = this.props.userExtensions.get();
return ( return (
<DropFileInput onDropFiles={this.dependencies.installOnDrop}> <DropFileInput onDropFiles={this.props.installOnDrop}>
<SettingLayout className="Extensions" contentGaps={false}> <SettingLayout className="Extensions" contentGaps={false}>
<section> <section>
<h1>Extensions</h1> <h1>Extensions</h1>
@ -106,8 +100,8 @@ class NonInjectedExtensions extends React.Component<Props> {
<Install <Install
supportedFormats={supportedExtensionFormats} supportedFormats={supportedExtensionFormats}
onChange={value => (this.installPath = value)} onChange={value => (this.installPath = value)}
installFromInput={() => this.dependencies.installFromInput(this.installPath)} installFromInput={() => this.props.installFromInput(this.installPath)}
installFromSelectFileDialog={this.dependencies.installFromSelectFileDialog} installFromSelectFileDialog={this.props.installFromSelectFileDialog}
installPath={this.installPath} installPath={this.installPath}
/> />
@ -115,9 +109,9 @@ class NonInjectedExtensions extends React.Component<Props> {
<InstalledExtensions <InstalledExtensions
extensions={userExtensions} extensions={userExtensions}
enable={this.dependencies.enableExtension} enable={this.props.enableExtension}
disable={this.dependencies.disableExtension} disable={this.props.disableExtension}
uninstall={this.dependencies.confirmUninstallExtension} uninstall={this.props.confirmUninstallExtension}
/> />
</section> </section>
</SettingLayout> </SettingLayout>
@ -126,10 +120,10 @@ class NonInjectedExtensions extends React.Component<Props> {
} }
} }
export const Extensions = withInjectables<Dependencies>(
export const Extensions = withInjectables(NonInjectedExtensions, { NonInjectedExtensions,
getProps: di => ({ {
dependencies: { getProps: (di) => ({
userExtensions: di.inject(userExtensionsInjectable), userExtensions: di.inject(userExtensionsInjectable),
enableExtension: di.inject(enableExtensionInjectable), enableExtension: di.inject(enableExtensionInjectable),
disableExtension: di.inject(disableExtensionInjectable), disableExtension: di.inject(disableExtensionInjectable),
@ -140,6 +134,6 @@ export const Extensions = withInjectables(NonInjectedExtensions, {
installFromSelectFileDialog: di.inject( installFromSelectFileDialog: di.inject(
installFromSelectFileDialogInjectable, installFromSelectFileDialogInjectable,
), ),
},
}), }),
}); },
);

View File

@ -0,0 +1,36 @@
/**
* Copyright (c) 2021 OpenLens Authors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
* the Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
import { Injectable, lifecycleEnum } from "@ogre-tools/injectable";
import { UserStore } from "../../../../common/user-store";
import { Dependencies, getBaseRegistryUrl } from "./get-base-registry-url";
const getBaseRegistryUrlInjectable: Injectable<() => Promise<string>, Dependencies> = {
getDependencies: () => ({
// TODO: use injection
getRegistryUrlPreference: () => UserStore.getInstance().extensionRegistryUrl,
}),
instantiate: getBaseRegistryUrl,
lifecycle: lifecycleEnum.singleton,
};
export default getBaseRegistryUrlInjectable;

View File

@ -0,0 +1,57 @@
/**
* Copyright (c) 2021 OpenLens Authors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
* the Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
import React from "react";
import { defaultExtensionRegistryUrl, ExtensionRegistry, ExtensionRegistryLocation } from "../../../../common/user-store/preferences-helpers";
import { promiseExecFile } from "../../../utils";
import { Notifications } from "../../notifications";
export interface Dependencies {
getRegistryUrlPreference: () => ExtensionRegistry,
}
export const getBaseRegistryUrl = ({ getRegistryUrlPreference }: Dependencies) => async () => {
const extensionRegistryUrl = getRegistryUrlPreference();
switch (extensionRegistryUrl.location) {
case ExtensionRegistryLocation.CUSTOM:
return extensionRegistryUrl.customUrl;
case ExtensionRegistryLocation.NPMRC: {
try {
const filteredEnv = Object.fromEntries(
Object.entries(process.env)
.filter(([key]) => !key.startsWith("npm")),
);
const { stdout } = await promiseExecFile("npm", ["config", "get", "registry"], { env: filteredEnv });
return stdout.trim();
} catch (error) {
Notifications.error(<p>Failed to get configured registry from <code>.npmrc</code>. Falling back to default registry</p>);
console.warn("[EXTENSIONS]: failed to get configured registry from .npmrc", error);
// fallthrough
}
}
default:
case ExtensionRegistryLocation.DEFAULT:
return defaultExtensionRegistryUrl;
}
};

View File

@ -19,7 +19,7 @@
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/ */
import styles from "./install.module.css"; import styles from "./install.module.scss";
import React from "react"; import React from "react";
import { prevDefault } from "../../utils"; import { prevDefault } from "../../utils";
import { Button } from "../button"; import { Button } from "../button";

View File

@ -19,7 +19,7 @@
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/ */
import styles from "./installed-extensions.module.css"; import styles from "./installed-extensions.module.scss";
import React, { useMemo } from "react"; import React, { useMemo } from "react";
import { ExtensionDiscovery, InstalledExtension } from "../../../extensions/extension-discovery"; import { ExtensionDiscovery, InstalledExtension } from "../../../extensions/extension-discovery";
import { Icon } from "../icon"; import { Icon } from "../icon";

View File

@ -19,7 +19,7 @@
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/ */
import styles from "./notice.module.css"; import styles from "./notice.module.scss";
import React, { DOMAttributes } from "react"; import React, { DOMAttributes } from "react";
import { cssNames } from "../../utils"; import { cssNames } from "../../utils";

View File

@ -19,7 +19,7 @@
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/ */
import styles from "./network-policy-details.module.css"; import styles from "./network-policy-details.module.scss";
import React from "react"; import React from "react";
import { DrawerItem, DrawerTitle } from "../drawer"; import { DrawerItem, DrawerTitle } from "../drawer";

View File

@ -172,7 +172,7 @@ export class AddHelmRepoDialog extends React.Component<Props> {
close={this.close} close={this.close}
> >
<Wizard header={header} done={this.close}> <Wizard header={header} done={this.close}>
<WizardStep contentClass="flow column" nextLabel="Add" next={()=>{this.addCustomRepo();}}> <WizardStep contentClass="flow column" nextLabel="Add" next={() => this.addCustomRepo()}>
<div className="flex column gaps"> <div className="flex column gaps">
<Input <Input
autoFocus required autoFocus required

View File

@ -29,7 +29,11 @@ import { Input } from "../input";
import { isWindows } from "../../../common/vars"; import { isWindows } from "../../../common/vars";
import { FormSwitch, Switcher } from "../switch"; import { FormSwitch, Switcher } from "../switch";
import moment from "moment-timezone"; import moment from "moment-timezone";
import { CONSTANTS } from "../../../common/user-store/preferences-helpers"; import { CONSTANTS, defaultExtensionRegistryUrl, ExtensionRegistryLocation } from "../../../common/user-store/preferences-helpers";
import { action } from "mobx";
import { isUrl } from "../input/input_validators";
import { AppPreferenceRegistry } from "../../../extensions/registries";
import { ExtensionSettings } from "./extension-settings";
const timezoneOptions: SelectOption<string>[] = moment.tz.names().map(zone => ({ const timezoneOptions: SelectOption<string>[] = moment.tz.names().map(zone => ({
label: zone, label: zone,
@ -41,6 +45,7 @@ const updateChannelOptions: SelectOption<string>[] = Array.from(
); );
export const Application = observer(() => { export const Application = observer(() => {
const userStore = UserStore.getInstance();
const defaultShell = process.env.SHELL const defaultShell = process.env.SHELL
|| process.env.PTYSHELL || process.env.PTYSHELL
|| ( || (
@ -49,7 +54,9 @@ export const Application = observer(() => {
: "System default shell" : "System default shell"
); );
const [shell, setShell] = React.useState(UserStore.getInstance().shell || ""); const [customUrl, setCustomUrl] = React.useState(userStore.extensionRegistryUrl.customUrl || "");
const [shell, setShell] = React.useState(userStore.shell || "");
const extensionSettings = AppPreferenceRegistry.getInstance().getItems().filter((preference) => preference.showInPreferencesTab === "application");
return ( return (
<section id="application"> <section id="application">
@ -58,8 +65,8 @@ export const Application = observer(() => {
<SubTitle title="Theme"/> <SubTitle title="Theme"/>
<Select <Select
options={ThemeStore.getInstance().themeOptions} options={ThemeStore.getInstance().themeOptions}
value={UserStore.getInstance().colorTheme} value={userStore.colorTheme}
onChange={({ value }: SelectOption) => UserStore.getInstance().colorTheme = value} onChange={({ value }) => userStore.colorTheme = value}
themeName="lens" themeName="lens"
/> />
</section> </section>
@ -72,8 +79,8 @@ export const Application = observer(() => {
theme="round-black" theme="round-black"
placeholder={defaultShell} placeholder={defaultShell}
value={shell} value={shell}
onChange={v => setShell(v)} onChange={setShell}
onBlur={() => UserStore.getInstance().shell = shell} onBlur={() => userStore.shell = shell}
/> />
</section> </section>
@ -83,8 +90,8 @@ export const Application = observer(() => {
label="Copy on select and paste on right-click" label="Copy on select and paste on right-click"
control={ control={
<Switcher <Switcher
checked={UserStore.getInstance().terminalCopyOnSelect} checked={userStore.terminalCopyOnSelect}
onChange={v => UserStore.getInstance().terminalCopyOnSelect = v.target.checked} onChange={v => userStore.terminalCopyOnSelect = v.target.checked}
name="terminalCopyOnSelect" name="terminalCopyOnSelect"
/> />
} }
@ -93,13 +100,46 @@ export const Application = observer(() => {
<hr/> <hr/>
<section id="extensionRegistryUrl">
<SubTitle title="Extension Install Registry" />
<Select
options={Object.values(ExtensionRegistryLocation)}
value={userStore.extensionRegistryUrl.location}
onChange={action(({ value }) => {
userStore.extensionRegistryUrl.location = value;
if (userStore.extensionRegistryUrl.location === ExtensionRegistryLocation.CUSTOM) {
userStore.extensionRegistryUrl.customUrl = "";
}
})}
themeName="lens"
/>
<p className="mt-4 mb-5 leading-relaxed">
This setting is to change the registry URL for installing extensions by name.{" "}
If you are unable to access the default registry ({defaultExtensionRegistryUrl}){" "}
you can change it in your <b>.npmrc</b>&nbsp;file or in the input below.
</p>
<Input
theme="round-black"
validators={isUrl}
value={customUrl}
onChange={setCustomUrl}
onBlur={() => userStore.extensionRegistryUrl.customUrl = customUrl}
placeholder="Custom Extension Registry URL..."
disabled={userStore.extensionRegistryUrl.location !== ExtensionRegistryLocation.CUSTOM}
/>
</section>
<hr/>
<section id="other"> <section id="other">
<SubTitle title="Start-up"/> <SubTitle title="Start-up"/>
<FormSwitch <FormSwitch
control={ control={
<Switcher <Switcher
checked={UserStore.getInstance().openAtLogin} checked={userStore.openAtLogin}
onChange={v => UserStore.getInstance().openAtLogin = v.target.checked} onChange={v => userStore.openAtLogin = v.target.checked}
name="startup" name="startup"
/> />
} }
@ -109,12 +149,16 @@ export const Application = observer(() => {
<hr /> <hr />
{extensionSettings.map(setting => (
<ExtensionSettings key={setting.id} setting={setting} size="normal" />
))}
<section id="update-channel"> <section id="update-channel">
<SubTitle title="Update Channel"/> <SubTitle title="Update Channel"/>
<Select <Select
options={updateChannelOptions} options={updateChannelOptions}
value={UserStore.getInstance().updateChannel} value={userStore.updateChannel}
onChange={({ value }: SelectOption) => UserStore.getInstance().updateChannel = value} onChange={({ value }) => userStore.updateChannel = value}
themeName="lens" themeName="lens"
/> />
</section> </section>
@ -125,8 +169,8 @@ export const Application = observer(() => {
<SubTitle title="Locale Timezone" /> <SubTitle title="Locale Timezone" />
<Select <Select
options={timezoneOptions} options={timezoneOptions}
value={UserStore.getInstance().localeTimezone} value={userStore.localeTimezone}
onChange={({ value }: SelectOption) => UserStore.getInstance().setLocaleTimezone(value)} onChange={({ value }) => userStore.setLocaleTimezone(value)}
themeName="lens" themeName="lens"
/> />
</section> </section>

View File

@ -0,0 +1,50 @@
/**
* Copyright (c) 2021 OpenLens Authors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
* the Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
import { SubTitle } from "../layout/sub-title";
import type { RegisteredAppPreference } from "../../../extensions/registries/app-preference-registry";
import React from "react";
import { cssNames } from "../../../renderer/utils";
interface ExtensionSettingsProps {
setting: RegisteredAppPreference;
size: "small" | "normal"
}
export function ExtensionSettings({ setting, size }: ExtensionSettingsProps) {
const {
title,
id,
components: { Hint, Input },
} = setting;
return (
<React.Fragment>
<section id={id} className={cssNames(size)}>
<SubTitle title={title} />
<Input />
<div className="hint">
<Hint />
</div>
</section>
<hr className={cssNames(size)} />
</React.Fragment>
);
}

View File

@ -22,16 +22,16 @@
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import React from "react"; import React from "react";
import { AppPreferenceRegistry } from "../../../extensions/registries"; import { AppPreferenceRegistry } from "../../../extensions/registries";
import { ExtensionSettings } from "./preferences"; import { ExtensionSettings } from "./extension-settings";
export const Extensions = observer(() => { export const Extensions = observer(() => {
const extensions = AppPreferenceRegistry.getInstance().getItems(); const settings = AppPreferenceRegistry.getInstance().getItems();
return ( return (
<section id="extensions"> <section id="extensions">
<h2>Extensions</h2> <h2>Extensions</h2>
{extensions.filter(e => !e.showInPreferencesTab).map((extension) => {settings.filter(e => !e.showInPreferencesTab).map((setting) =>
<ExtensionSettings key={extension.id} {...extension}/>, <ExtensionSettings key={setting.id} setting={setting} size="small" />,
)} )}
</section> </section>
); );

View File

@ -19,7 +19,7 @@
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/ */
import styles from "./helm-charts.module.css"; import styles from "./helm-charts.module.scss";
import React from "react"; import React from "react";
import { action, computed, observable, makeObservable } from "mobx"; import { action, computed, observable, makeObservable } from "mobx";

View File

@ -24,7 +24,6 @@ import { makeObservable, observable } from "mobx";
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import React from "react"; import React from "react";
import { matchPath, Redirect, Route, RouteProps, Switch } from "react-router"; import { matchPath, Redirect, Route, RouteProps, Switch } from "react-router";
import { import {
appRoute, appRoute,
appURL, appURL,
@ -40,10 +39,9 @@ import {
telemetryRoute, telemetryRoute,
telemetryURL, telemetryURL,
} from "../../../common/routes"; } from "../../../common/routes";
import { AppPreferenceRegistry, RegisteredAppPreference } from "../../../extensions/registries/app-preference-registry"; import { AppPreferenceRegistry } from "../../../extensions/registries/app-preference-registry";
import { navigateWithoutHistoryChange, navigation } from "../../navigation"; import { navigateWithoutHistoryChange, navigation } from "../../navigation";
import { SettingLayout } from "../layout/setting-layout"; import { SettingLayout } from "../layout/setting-layout";
import { SubTitle } from "../layout/sub-title";
import { Tab, Tabs } from "../tabs"; import { Tab, Tabs } from "../tabs";
import { Application } from "./application"; import { Application } from "./application";
import { Kubernetes } from "./kubernetes"; import { Kubernetes } from "./kubernetes";
@ -105,18 +103,3 @@ export class Preferences extends React.Component {
); );
} }
} }
export function ExtensionSettings({ title, id, components: { Hint, Input }}: RegisteredAppPreference) {
return (
<React.Fragment>
<section id={id} className="small">
<SubTitle title={title}/>
<Input/>
<div className="hint">
<Hint/>
</div>
</section>
<hr className="small"/>
</React.Fragment>
);
}

View File

@ -18,7 +18,7 @@
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/ */
import styles from "./removable-item.module.css"; import styles from "./removable-item.module.scss";
import React, { DOMAttributes } from "react"; import React, { DOMAttributes } from "react";

View File

@ -25,7 +25,7 @@ import { sentryDsn } from "../../../common/vars";
import { AppPreferenceRegistry } from "../../../extensions/registries"; import { AppPreferenceRegistry } from "../../../extensions/registries";
import { Checkbox } from "../checkbox"; import { Checkbox } from "../checkbox";
import { SubTitle } from "../layout/sub-title"; import { SubTitle } from "../layout/sub-title";
import { ExtensionSettings } from "./preferences"; import { ExtensionSettings } from "./extension-settings";
export const Telemetry = observer(() => { export const Telemetry = observer(() => {
const extensions = AppPreferenceRegistry.getInstance().getItems(); const extensions = AppPreferenceRegistry.getInstance().getItems();
@ -34,7 +34,7 @@ export const Telemetry = observer(() => {
return ( return (
<section id="telemetry"> <section id="telemetry">
<h2 data-testid="telemetry-header">Telemetry</h2> <h2 data-testid="telemetry-header">Telemetry</h2>
{telemetryExtensions.map((extension) => <ExtensionSettings key={extension.id} {...extension}/>)} {telemetryExtensions.map((extension) => <ExtensionSettings key={extension.id} setting={extension} size="small" />)}
{sentryDsn ? ( {sentryDsn ? (
<React.Fragment key='sentry'> <React.Fragment key='sentry'>
<section id='sentry' className="small"> <section id='sentry' className="small">

View File

@ -19,9 +19,12 @@
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/ */
@import "tailwindcss/utilities"; @tailwind base;
@import "~flex.box"; @tailwind components;
@import "fonts"; @tailwind utilities;
@import "~flex.box"; // todo: replace with tailwind's flexbox classes
@import "./fonts";
@import "../themes/theme-vars"; @import "../themes/theme-vars";
:root { :root {

View File

@ -19,7 +19,7 @@
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/ */
import styles from "./avatar.module.css"; import styles from "./avatar.module.scss";
import React, { HTMLAttributes, ImgHTMLAttributes } from "react"; import React, { HTMLAttributes, ImgHTMLAttributes } from "react";
import randomColor from "randomcolor"; import randomColor from "randomcolor";

View File

@ -19,7 +19,7 @@
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/ */
import styles from "./badge.module.css"; import styles from "./badge.module.scss";
import React from "react"; import React from "react";
import { computed, makeObservable, observable } from "mobx"; import { computed, makeObservable, observable } from "mobx";

View File

@ -19,7 +19,7 @@
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/ */
import styles from "./bottom-bar.module.css"; import styles from "./bottom-bar.module.scss";
import React from "react"; import React from "react";
import { observer } from "mobx-react"; import { observer } from "mobx-react";

View File

@ -19,7 +19,7 @@
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/ */
import styles from "./cluster-status.module.css"; import styles from "./cluster-status.module.scss";
import { computed, observable, makeObservable } from "mobx"; import { computed, observable, makeObservable } from "mobx";
import { disposeOnUnmount, observer } from "mobx-react"; import { disposeOnUnmount, observer } from "mobx-react";

View File

@ -18,7 +18,7 @@
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/ */
import styles from "./delete-cluster-dialog.module.css"; import styles from "./delete-cluster-dialog.module.scss";
import { computed, makeObservable, observable } from "mobx"; import { computed, makeObservable, observable } from "mobx";
import { observer } from "mobx-react"; import { observer } from "mobx-react";

View File

@ -19,7 +19,7 @@
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/ */
import styles from "./editor-panel.module.css"; import styles from "./editor-panel.module.scss";
import throttle from "lodash/throttle"; import throttle from "lodash/throttle";
import React from "react"; import React from "react";
import { makeObservable, observable, reaction } from "mobx"; import { makeObservable, observable, reaction } from "mobx";

View File

@ -19,7 +19,7 @@
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/ */
import styles from "./hotbar-entity-icon.module.css"; import styles from "./hotbar-entity-icon.module.scss";
import React, { HTMLAttributes } from "react"; import React, { HTMLAttributes } from "react";
import { makeObservable, observable } from "mobx"; import { makeObservable, observable } from "mobx";

View File

@ -19,7 +19,7 @@
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/ */
import styles from "./hotbar-icon.module.css"; import styles from "./hotbar-icon.module.scss";
import React, { useState } from "react"; import React, { useState } from "react";

View File

@ -32,38 +32,28 @@ import hideDetailsInjectable from "./dependencies/hide-details.injectable";
import kubeObjectMenuItemsInjectable from "./dependencies/kube-object-menu-items/kube-object-menu-items.injectable"; import kubeObjectMenuItemsInjectable from "./dependencies/kube-object-menu-items/kube-object-menu-items.injectable";
import apiManagerInjectable from "./dependencies/api-manager.injectable"; import apiManagerInjectable from "./dependencies/api-manager.injectable";
// TODO: Replace with KubeObjectMenuProps2 export interface KubeObjectMenuProps<TKubeObject extends KubeObject> extends MenuActionsProps {
export interface KubeObjectMenuProps<TKubeObject> extends MenuActionsProps {
object: TKubeObject | null | undefined; object: TKubeObject | null | undefined;
editable?: boolean; editable?: boolean;
removable?: boolean; removable?: boolean;
} }
interface KubeObjectMenuProps2 extends MenuActionsProps { interface Dependencies {
object: KubeObject | null | undefined;
editable?: boolean;
removable?: boolean;
dependencies: {
apiManager: ApiManager; apiManager: ApiManager;
kubeObjectMenuItems: React.ElementType[]; kubeObjectMenuItems: React.ElementType[];
clusterName: string; clusterName: string;
hideDetails: () => void; hideDetails: () => void;
editResourceTab: (kubeObject: KubeObject) => void; editResourceTab: (kubeObject: KubeObject) => void;
};
} }
class NonInjectedKubeObjectMenu extends React.Component<KubeObjectMenuProps2> { class NonInjectedKubeObjectMenu<TKubeObject extends KubeObject> extends React.Component<KubeObjectMenuProps<TKubeObject> & Dependencies> {
get dependencies() {
return this.props.dependencies;
}
get store() { get store() {
const { object } = this.props; const { object } = this.props;
if (!object) return null; if (!object) return null;
return this.props.dependencies.apiManager.getStore(object.selfLink); return this.props.apiManager.getStore(object.selfLink);
} }
get isEditable() { get isEditable() {
@ -76,13 +66,13 @@ class NonInjectedKubeObjectMenu extends React.Component<KubeObjectMenuProps2> {
@boundMethod @boundMethod
async update() { async update() {
this.props.dependencies.hideDetails(); this.props.hideDetails();
this.props.dependencies.editResourceTab(this.props.object); this.props.editResourceTab(this.props.object);
} }
@boundMethod @boundMethod
async remove() { async remove() {
this.props.dependencies.hideDetails(); this.props.hideDetails();
const { object, removeAction } = this.props; const { object, removeAction } = this.props;
if (removeAction) await removeAction(); if (removeAction) await removeAction();
@ -103,7 +93,7 @@ class NonInjectedKubeObjectMenu extends React.Component<KubeObjectMenuProps2> {
return ( return (
<p> <p>
Remove {object.kind} <b>{breadcrumb}</b> from <b>{this.dependencies.clusterName}</b>? Remove {object.kind} <b>{breadcrumb}</b> from <b>{this.props.clusterName}</b>?
</p> </p>
); );
} }
@ -111,7 +101,7 @@ class NonInjectedKubeObjectMenu extends React.Component<KubeObjectMenuProps2> {
getMenuItems(): React.ReactChild[] { getMenuItems(): React.ReactChild[] {
const { object, toolbar } = this.props; const { object, toolbar } = this.props;
return this.props.dependencies.kubeObjectMenuItems.map((MenuItem, index) => ( return this.props.kubeObjectMenuItems.map((MenuItem, index) => (
<MenuItem object={object} toolbar={toolbar} key={`menu-item-${index}`} /> <MenuItem object={object} toolbar={toolbar} key={`menu-item-${index}`} />
)); ));
} }
@ -134,9 +124,13 @@ class NonInjectedKubeObjectMenu extends React.Component<KubeObjectMenuProps2> {
} }
} }
export const KubeObjectMenu = withInjectables(NonInjectedKubeObjectMenu, { export function KubeObjectMenu<T extends KubeObject>(
props: KubeObjectMenuProps<T>,
) {
return withInjectables<Dependencies, KubeObjectMenuProps<T>>(
NonInjectedKubeObjectMenu,
{
getProps: (di, props) => ({ getProps: (di, props) => ({
dependencies: {
clusterName: di.inject(clusterNameInjectable), clusterName: di.inject(clusterNameInjectable),
apiManager: di.inject(apiManagerInjectable), apiManager: di.inject(apiManagerInjectable),
editResourceTab: di.inject(editResourceTabInjectable), editResourceTab: di.inject(editResourceTabInjectable),
@ -145,8 +139,8 @@ export const KubeObjectMenu = withInjectables(NonInjectedKubeObjectMenu, {
kubeObjectMenuItems: di.inject(kubeObjectMenuItemsInjectable, { kubeObjectMenuItems: di.inject(kubeObjectMenuItemsInjectable, {
kubeObject: props.object, kubeObject: props.object,
}), }),
},
...props, ...props,
}), }),
}); },
)(props);
}

View File

@ -19,7 +19,7 @@
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/ */
import styles from "./kubeconfig-dialog.module.css"; import styles from "./kubeconfig-dialog.module.scss";
import React from "react"; import React from "react";
import { makeObservable, observable } from "mobx"; import { makeObservable, observable } from "mobx";
import { observer } from "mobx-react"; import { observer } from "mobx-react";

View File

@ -19,7 +19,7 @@
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/ */
import styles from "./main-layout.module.css"; import styles from "./main-layout.module.scss";
import React from "react"; import React from "react";
import { observer } from "mobx-react"; import { observer } from "mobx-react";

View File

@ -19,7 +19,7 @@
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/ */
import styles from "./sidebar-cluster.module.css"; import styles from "./sidebar-cluster.module.scss";
import { observable } from "mobx"; import { observable } from "mobx";
import React, { useState } from "react"; import React, { useState } from "react";
import { HotbarStore } from "../../../common/hotbar-store"; import { HotbarStore } from "../../../common/hotbar-store";

View File

@ -19,7 +19,7 @@
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/ */
import styles from "./sidebar.module.css"; import styles from "./sidebar.module.scss";
import type { TabLayoutRoute } from "./tab-layout"; import type { TabLayoutRoute } from "./tab-layout";
import React from "react"; import React from "react";

View File

@ -19,7 +19,7 @@
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/ */
import styles from "./topbar.module.css"; import styles from "./topbar.module.scss";
import React, { useEffect, useMemo, useRef } from "react"; import React, { useEffect, useMemo, useRef } from "react";
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import { TopBarRegistry } from "../../../extensions/registries"; import { TopBarRegistry } from "../../../extensions/registries";

View File

@ -19,7 +19,7 @@
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/ */
import styles from "./list.module.css"; import styles from "./list.module.scss";
import React, { useState } from "react"; import React, { useState } from "react";
import { SearchInput } from "../input"; import { SearchInput } from "../input";

View File

@ -19,7 +19,7 @@
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/ */
import styles from "./monaco-editor.module.css"; import styles from "./monaco-editor.module.scss";
import React from "react"; import React from "react";
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import { action, computed, makeObservable, observable, reaction } from "mobx"; import { action, computed, makeObservable, observable, reaction } from "mobx";

View File

@ -19,7 +19,7 @@
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/ */
import styles from "./react-table.module.css"; import styles from "./react-table.module.scss";
import React from "react"; import React from "react";
import { useCallback, useMemo } from "react"; import { useCallback, useMemo } from "react";
import { useFlexLayout, useSortBy, useTable, UseTableOptions } from "react-table"; import { useFlexLayout, useSortBy, useTable, UseTableOptions } from "react-table";

View File

@ -29,7 +29,7 @@ import { ErrorBoundary } from "./components/error-boundary";
import { Notifications } from "./components/notifications"; import { Notifications } from "./components/notifications";
import { ConfirmDialog } from "./components/confirm-dialog"; import { ConfirmDialog } from "./components/confirm-dialog";
import type { ExtensionLoader } from "../extensions/extension-loader"; import type { ExtensionLoader } from "../extensions/extension-loader";
import { broadcastMessage } from "../common/ipc"; import { broadcastMessage, BundledExtensionsLoaded } from "../common/ipc";
import { CommandContainer } from "./components/command-palette/command-container"; import { CommandContainer } from "./components/command-palette/command-container";
import { registerIpcListeners } from "./ipc"; import { registerIpcListeners } from "./ipc";
import { ipcRenderer } from "electron"; import { ipcRenderer } from "electron";
@ -39,6 +39,7 @@ import logger from "../common/logger";
import { unmountComponentAtNode } from "react-dom"; import { unmountComponentAtNode } from "react-dom";
import { ClusterFrameHandler } from "./components/cluster-manager/lens-views"; import { ClusterFrameHandler } from "./components/cluster-manager/lens-views";
import type { LensProtocolRouterRenderer } from "./protocol-handler"; import type { LensProtocolRouterRenderer } from "./protocol-handler";
import { delay } from "./utils";
injectSystemCAs(); injectSystemCAs();
@ -54,8 +55,21 @@ export class RootFrame extends React.Component {
lensProtocolRouterRendererInjectable: LensProtocolRouterRenderer, lensProtocolRouterRendererInjectable: LensProtocolRouterRenderer,
) { ) {
catalogEntityRegistry.init(); catalogEntityRegistry.init();
extensionLoader.loadOnClusterManagerRenderer();
try {
// maximum time to let bundled extensions finish loading
const timeout = delay(10000);
const loadingExtensions = extensionLoader.loadOnClusterManagerRenderer();
const loadingBundledExtensions = loadingExtensions.filter(e => e.isBundled).map(e => e.loaded);
const bundledExtensionsFinished = Promise.all(loadingBundledExtensions);
await Promise.race([bundledExtensionsFinished, timeout]);
} finally {
ipcRenderer.send(BundledExtensionsLoaded);
}
lensProtocolRouterRendererInjectable.init(); lensProtocolRouterRendererInjectable.init();
bindProtocolAddRouteHandlers(); bindProtocolAddRouteHandlers();
window.addEventListener("offline", () => broadcastMessage("network:offline")); window.addEventListener("offline", () => broadcastMessage("network:offline"));

View File

@ -20,8 +20,7 @@
*/ */
module.exports = { module.exports = {
mode: "jit", content: ["src/**/*.tsx"],
purge: ["src/**/*.tsx"],
darkMode: "class", darkMode: "class",
theme: { theme: {
fontFamily: { fontFamily: {

Some files were not shown because too many files have changed in this diff Show More