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:
commit
52ca977b8c
@ -71,7 +71,8 @@ describe("preferences page tests", () => {
|
||||
}
|
||||
}, 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.waitForSelector("[data-testid=repository-name]", {
|
||||
timeout: 140_000,
|
||||
|
||||
17
package.json
17
package.json
@ -196,8 +196,8 @@
|
||||
"@hapi/call": "^8.0.1",
|
||||
"@hapi/subtext": "^7.0.3",
|
||||
"@kubernetes/client-node": "^0.16.1",
|
||||
"@ogre-tools/injectable": "^1.4.1",
|
||||
"@ogre-tools/injectable-react": "^1.4.1",
|
||||
"@ogre-tools/injectable": "1.5.0",
|
||||
"@ogre-tools/injectable-react": "1.5.2",
|
||||
"@sentry/electron": "^2.5.4",
|
||||
"@sentry/integrations": "^6.15.0",
|
||||
"abort-controller": "^3.0.0",
|
||||
@ -324,8 +324,8 @@
|
||||
"@types/webpack-dev-server": "^3.11.6",
|
||||
"@types/webpack-env": "^1.16.3",
|
||||
"@types/webpack-node-externals": "^1.7.1",
|
||||
"@typescript-eslint/eslint-plugin": "^4.33.0",
|
||||
"@typescript-eslint/parser": "^4.33.0",
|
||||
"@typescript-eslint/eslint-plugin": "^5.7.0",
|
||||
"@typescript-eslint/parser": "^5.7.0",
|
||||
"ansi_up": "^5.1.0",
|
||||
"chart.js": "^2.9.4",
|
||||
"circular-dependency-plugin": "^5.2.2",
|
||||
@ -361,8 +361,7 @@
|
||||
"nodemon": "^2.0.14",
|
||||
"playwright": "^1.17.1",
|
||||
"postcss": "^8.4.5",
|
||||
"postcss-loader": "4.3.0",
|
||||
"postinstall-postinstall": "^2.1.0",
|
||||
"postcss-loader": "^4.3.0",
|
||||
"progress-bar-webpack-plugin": "^2.1.0",
|
||||
"randomcolor": "^0.6.2",
|
||||
"raw-loader": "^4.0.2",
|
||||
@ -373,11 +372,11 @@
|
||||
"react-select-event": "^5.1.0",
|
||||
"react-table": "^7.7.0",
|
||||
"react-window": "^1.8.6",
|
||||
"sass": "^1.44.0",
|
||||
"sass-loader": "^8.0.2",
|
||||
"sass": "^1.45.1",
|
||||
"sass-loader": "^10.2.0",
|
||||
"sharp": "^0.29.3",
|
||||
"style-loader": "^2.0.0",
|
||||
"tailwindcss": "^2.2.19",
|
||||
"tailwindcss": "^3.0.7",
|
||||
"ts-jest": "26.5.6",
|
||||
"ts-loader": "^7.0.5",
|
||||
"ts-node": "^10.4.0",
|
||||
|
||||
32
src/common/ipc/catalog.ts
Normal file
32
src/common/ipc/catalog.ts
Normal 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",
|
||||
}
|
||||
@ -18,10 +18,4 @@
|
||||
* 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.
|
||||
*/
|
||||
|
||||
module.exports = {
|
||||
plugins: [
|
||||
require("tailwindcss/nesting"),
|
||||
require("tailwindcss"),
|
||||
],
|
||||
};
|
||||
export const BundledExtensionsLoaded = "extension-loader:bundled-extensions-loaded";
|
||||
@ -27,3 +27,4 @@ export * from "./update-available.ipc";
|
||||
export * from "./cluster.ipc";
|
||||
export * from "./type-enforced-ipc";
|
||||
export * from "./hotbar";
|
||||
export * from "./extension-loader.ipc";
|
||||
|
||||
@ -22,7 +22,7 @@
|
||||
import { isMac, isWindows } from "./vars";
|
||||
import wincaAPI from "win-ca/api";
|
||||
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
|
||||
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;
|
||||
}
|
||||
|
||||
// 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
|
||||
* Only return non-expred certificates.
|
||||
*/
|
||||
export async function getMacRootCA() {
|
||||
// inspired mac-ca https://github.com/jfromaniello/mac-ca
|
||||
const args = "find-certificate -a -p";
|
||||
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions/Cheatsheet#other_assertions
|
||||
const splitPattern = /(?=-----BEGIN\sCERTIFICATE-----)/g;
|
||||
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);
|
||||
const [trusted, rootCA] = await Promise.all([
|
||||
execSecurity("find-certificate", "-a", "-p"),
|
||||
execSecurity("find-certificate", "-a", "-p", "/System/Library/Keychains/SystemRootCertificates.keychain"),
|
||||
]);
|
||||
|
||||
return [...new Set([...trusted, ...rootCA])].filter(isCertActive);
|
||||
}
|
||||
|
||||
@ -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 UserStoreModelType<field extends keyof typeof DESCRIPTORS> = typeof DESCRIPTORS[field] extends PreferenceDescription<any, infer T> ? T : never;
|
||||
|
||||
@ -335,6 +365,7 @@ export const DESCRIPTORS = {
|
||||
editorConfiguration,
|
||||
terminalCopyOnSelect,
|
||||
updateChannel,
|
||||
extensionRegistryUrl,
|
||||
};
|
||||
|
||||
export const CONSTANTS = {
|
||||
|
||||
@ -29,7 +29,7 @@ import { kubeConfigDefaultPath } from "../kube-helpers";
|
||||
import { appEventBus } from "../event-bus";
|
||||
import path from "path";
|
||||
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 { AppPaths } from "../app-paths";
|
||||
|
||||
@ -75,6 +75,7 @@ export class UserStore extends BaseStore<UserStoreModel> /* implements UserStore
|
||||
@observable kubectlBinariesPath?: string;
|
||||
@observable terminalCopyOnSelect: boolean;
|
||||
@observable updateChannel?: string;
|
||||
@observable extensionRegistryUrl: ExtensionRegistry;
|
||||
|
||||
/**
|
||||
* 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.terminalCopyOnSelect = DESCRIPTORS.terminalCopyOnSelect.fromStore(preferences?.terminalCopyOnSelect);
|
||||
this.updateChannel = DESCRIPTORS.updateChannel.fromStore(preferences?.updateChannel);
|
||||
this.extensionRegistryUrl = DESCRIPTORS.extensionRegistryUrl.fromStore(preferences?.extensionRegistryUrl);
|
||||
}
|
||||
|
||||
toJSON(): UserStoreModel {
|
||||
@ -224,6 +226,7 @@ export class UserStore extends BaseStore<UserStoreModel> /* implements UserStore
|
||||
editorConfiguration: DESCRIPTORS.editorConfiguration.toStore(this.editorConfiguration),
|
||||
terminalCopyOnSelect: DESCRIPTORS.terminalCopyOnSelect.toStore(this.terminalCopyOnSelect),
|
||||
updateChannel: DESCRIPTORS.updateChannel.toStore(this.updateChannel),
|
||||
extensionRegistryUrl: DESCRIPTORS.extensionRegistryUrl.toStore(this.extensionRegistryUrl),
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@ -48,6 +48,7 @@ export * from "./n-fircate";
|
||||
export * from "./objects";
|
||||
export * from "./openExternal";
|
||||
export * from "./paths";
|
||||
export * from "./promise-exec";
|
||||
export * from "./reject-promise";
|
||||
export * from "./singleton";
|
||||
export * from "./sort-compare";
|
||||
|
||||
@ -20,7 +20,6 @@
|
||||
*/
|
||||
|
||||
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);
|
||||
|
||||
@ -25,10 +25,10 @@ import { isEqual } from "lodash";
|
||||
import { action, computed, makeObservable, observable, observe, reaction, when } from "mobx";
|
||||
import path from "path";
|
||||
import { AppPaths } from "../../common/app-paths";
|
||||
import { ClusterStore } from "../../common/cluster-store";
|
||||
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 type { KubernetesCluster } from "../common-api/catalog";
|
||||
import type { InstalledExtension } from "../extension-discovery";
|
||||
import { ExtensionsStore } from "../extensions-store";
|
||||
import type { LensExtension, LensExtensionConstructor, LensExtensionId } from "../lens-extension";
|
||||
@ -255,7 +255,8 @@ export class ExtensionLoader {
|
||||
|
||||
loadOnClusterManagerRenderer() {
|
||||
logger.debug(`${logModule}: load on main renderer (cluster manager)`);
|
||||
this.autoInitExtensions(async (extension: LensRendererExtension) => {
|
||||
|
||||
return this.autoInitExtensions(async (extension: LensRendererExtension) => {
|
||||
const removeItems = [
|
||||
registries.GlobalPageRegistry.getInstance().add(extension.globalPages, extension),
|
||||
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)`);
|
||||
const cluster = ClusterStore.getInstance().getById(getHostedClusterId());
|
||||
|
||||
this.autoInitExtensions(async (extension: LensRendererExtension) => {
|
||||
if ((await extension.isEnabledForCluster(cluster)) === false) {
|
||||
if ((await extension.isEnabledForCluster(entity)) === false) {
|
||||
return [];
|
||||
}
|
||||
|
||||
@ -312,7 +312,9 @@ export class ExtensionLoader {
|
||||
}
|
||||
|
||||
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) {
|
||||
const alreadyInit = this.instances.has(extId) || this.nonInstancesByName.has(extension.manifest.name);
|
||||
|
||||
@ -327,7 +329,14 @@ export class ExtensionLoader {
|
||||
|
||||
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);
|
||||
} catch (err) {
|
||||
logger.error(`${logModule}: activation extension error`, { ext: extension, err });
|
||||
@ -339,6 +348,8 @@ export class ExtensionLoader {
|
||||
}, {
|
||||
fireImmediately: true,
|
||||
});
|
||||
|
||||
return loadingExtensions;
|
||||
}
|
||||
|
||||
protected requireExtension(extension: InstalledExtension): LensExtensionConstructor | null {
|
||||
|
||||
@ -20,13 +20,13 @@
|
||||
*/
|
||||
|
||||
import type * as registries from "./registries";
|
||||
import type { Cluster } from "../main/cluster";
|
||||
import { Disposers, LensExtension } from "./lens-extension";
|
||||
import { getExtensionPageUrl } from "./registries/page-registry";
|
||||
import type { CatalogEntity } from "../common/catalog";
|
||||
import type { Disposer } from "../common/utils";
|
||||
import { catalogEntityRegistry, EntityFilter } from "../renderer/api/catalog-entity-registry";
|
||||
import { catalogCategoryRegistry, CategoryFilter } from "../renderer/api/catalog-category-registry";
|
||||
import type { KubernetesCluster } from "../common/catalog-entities";
|
||||
|
||||
export class LensRendererExtension extends LensExtension {
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
@ -20,22 +20,25 @@
|
||||
*/
|
||||
|
||||
import { reaction } from "mobx";
|
||||
import { broadcastMessage } from "../common/ipc";
|
||||
import { broadcastMessage, ipcMainOn } from "../common/ipc";
|
||||
import type { CatalogEntityRegistry } from "./catalog";
|
||||
import "../common/catalog-entities/kubernetes-cluster";
|
||||
import { toJS } from "../common/utils";
|
||||
import { disposer, toJS } from "../common/utils";
|
||||
import { debounce } from "lodash";
|
||||
import type { CatalogEntity } from "../common/catalog";
|
||||
|
||||
import { CatalogIpcEvents } from "../common/ipc/catalog";
|
||||
|
||||
const broadcaster = debounce((items: CatalogEntity[]) => {
|
||||
broadcastMessage("catalog:items", items);
|
||||
broadcastMessage(CatalogIpcEvents.ITEMS, items);
|
||||
}, 1_000, { leading: true, trailing: true });
|
||||
|
||||
export function pushCatalogToRenderer(catalog: CatalogEntityRegistry) {
|
||||
return reaction(() => toJS(catalog.items), (items) => {
|
||||
broadcaster(items);
|
||||
}, {
|
||||
fireImmediately: true,
|
||||
});
|
||||
return disposer(
|
||||
ipcMainOn(CatalogIpcEvents.INIT, () => broadcaster(toJS(catalog.items))),
|
||||
reaction(() => toJS(catalog.items), (items) => {
|
||||
broadcaster(items);
|
||||
}, {
|
||||
fireImmediately: true,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
@ -22,161 +22,230 @@
|
||||
import * as tempy from "tempy";
|
||||
import fse from "fs-extra";
|
||||
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 type { Cluster } from "../cluster";
|
||||
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) {
|
||||
const helm = await helmCli.binaryPath();
|
||||
const namespaceFlag = namespace ? `-n ${namespace}` : "--all-namespaces";
|
||||
async function execHelm(args: string[], options?: BaseEncodingOptions & ExecFileOptions): Promise<string> {
|
||||
const helmCliPath = await helmCli.binaryPath();
|
||||
|
||||
try {
|
||||
const { stdout } = await promiseExec(`"${helm}" ls --output json ${namespaceFlag} --kubeconfig ${pathToKubeconfig}`);
|
||||
const output = JSON.parse(stdout);
|
||||
const { stdout } = await promiseExecFile(helmCliPath, args, options);
|
||||
|
||||
if (output.length == 0) {
|
||||
return output;
|
||||
}
|
||||
output.forEach((release: any, index: number) => {
|
||||
output[index] = toCamelCase(release);
|
||||
});
|
||||
|
||||
return output;
|
||||
return stdout;
|
||||
} catch (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) {
|
||||
const helm = await helmCli.binaryPath();
|
||||
const fileName = tempy.file({ name: "values.yaml" });
|
||||
if (namespace) {
|
||||
args.push("-n", namespace);
|
||||
} else {
|
||||
args.push("--all-namespaces");
|
||||
}
|
||||
|
||||
await fse.writeFile(fileName, yaml.dump(values));
|
||||
args.push("--kubeconfig", pathToKubeconfig);
|
||||
|
||||
const output = JSON.parse(await execHelm(args));
|
||||
|
||||
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) {
|
||||
args.push("--generate-name");
|
||||
}
|
||||
|
||||
try {
|
||||
let generateName = "";
|
||||
|
||||
if (!name) {
|
||||
generateName = "--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();
|
||||
const output = await execHelm(args);
|
||||
const releaseName = output.split("\n")[0].split(" ")[1].trim();
|
||||
|
||||
return {
|
||||
log: stdout,
|
||||
log: output,
|
||||
release: {
|
||||
name: releaseName,
|
||||
namespace,
|
||||
},
|
||||
};
|
||||
} catch (error) {
|
||||
throw error?.stderr || error;
|
||||
} 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) {
|
||||
const helm = await helmCli.binaryPath();
|
||||
const fileName = tempy.file({ name: "values.yaml" });
|
||||
export async function upgradeRelease(name: string, chart: string, values: any, namespace: string, version: string, kubeconfigPath: string, kubectlPath: string) {
|
||||
const valuesFilePath = 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 {
|
||||
const proxyKubeconfig = await cluster.getProxyKubeconfigPath();
|
||||
const { stdout } = await promiseExec(`"${helm}" upgrade ${name} ${chart} --version ${version} -f ${fileName} --namespace ${namespace} --kubeconfig ${proxyKubeconfig}`);
|
||||
const output = await execHelm(args);
|
||||
|
||||
return {
|
||||
log: stdout,
|
||||
release: getRelease(name, namespace, cluster),
|
||||
log: output,
|
||||
release: getRelease(name, namespace, kubeconfigPath, kubectlPath),
|
||||
};
|
||||
} catch (error) {
|
||||
throw error?.stderr || error;
|
||||
} finally {
|
||||
await fse.unlink(fileName);
|
||||
await fse.unlink(valuesFilePath);
|
||||
}
|
||||
}
|
||||
|
||||
export async function getRelease(name: string, namespace: string, cluster: Cluster) {
|
||||
try {
|
||||
const helm = await helmCli.binaryPath();
|
||||
const proxyKubeconfig = await cluster.getProxyKubeconfigPath();
|
||||
export async function getRelease(name: string, namespace: string, kubeconfigPath: string, kubectlPath: string) {
|
||||
const args = [
|
||||
"status",
|
||||
name,
|
||||
"--namespace", namespace,
|
||||
"--kubeconfig", kubeconfigPath,
|
||||
"--output", "json",
|
||||
];
|
||||
|
||||
const { stdout } = await promiseExec(`"${helm}" status ${name} --output json --namespace ${namespace} --kubeconfig ${proxyKubeconfig}`, {
|
||||
maxBuffer: 32 * 1024 * 1024 * 1024, // 32 MiB
|
||||
});
|
||||
const release = JSON.parse(stdout);
|
||||
const release = JSON.parse(await execHelm(args, {
|
||||
maxBuffer: 32 * 1024 * 1024 * 1024, // 32 MiB
|
||||
}));
|
||||
|
||||
release.resources = await getResources(name, namespace, cluster);
|
||||
release.resources = await getResources(name, namespace, kubeconfigPath, kubectlPath);
|
||||
|
||||
return release;
|
||||
} catch (error) {
|
||||
throw error?.stderr || error;
|
||||
}
|
||||
return release;
|
||||
}
|
||||
|
||||
export async function deleteRelease(name: string, namespace: string, pathToKubeconfig: string) {
|
||||
try {
|
||||
const helm = await helmCli.binaryPath();
|
||||
const { stdout } = await promiseExec(`"${helm}" delete ${name} --namespace ${namespace} --kubeconfig ${pathToKubeconfig}`);
|
||||
|
||||
return stdout;
|
||||
} catch (error) {
|
||||
throw error?.stderr || error;
|
||||
}
|
||||
export async function deleteRelease(name: string, namespace: string, kubeconfigPath: string) {
|
||||
return execHelm([
|
||||
"delete",
|
||||
name,
|
||||
"--namespace", namespace,
|
||||
"--kubeconfig", kubeconfigPath,
|
||||
]);
|
||||
}
|
||||
|
||||
interface GetValuesOptions {
|
||||
namespace: string;
|
||||
all?: boolean;
|
||||
pathToKubeconfig: string;
|
||||
kubeconfigPath: string;
|
||||
}
|
||||
|
||||
export async function getValues(name: string, { namespace, all = false, pathToKubeconfig }: GetValuesOptions) {
|
||||
try {
|
||||
const helm = await helmCli.binaryPath();
|
||||
const { stdout } = await promiseExec(`"${helm}" get values ${name} ${all ? "--all" : ""} --output yaml --namespace ${namespace} --kubeconfig ${pathToKubeconfig}`);
|
||||
export async function getValues(name: string, { namespace, all = false, kubeconfigPath }: GetValuesOptions) {
|
||||
const args = [
|
||||
"get",
|
||||
"values",
|
||||
name,
|
||||
];
|
||||
|
||||
return stdout;
|
||||
} catch (error) {
|
||||
throw error?.stderr || error;
|
||||
if (all) {
|
||||
args.push("--all");
|
||||
}
|
||||
|
||||
args.push(
|
||||
"--output", "yaml",
|
||||
"--namespace", namespace,
|
||||
"--kubeconfig", kubeconfigPath,
|
||||
);
|
||||
|
||||
return execHelm(args);
|
||||
}
|
||||
|
||||
export async function getHistory(name: string, namespace: string, pathToKubeconfig: string) {
|
||||
try {
|
||||
const helm = await helmCli.binaryPath();
|
||||
const { stdout } = await promiseExec(`"${helm}" history ${name} --output json --namespace ${namespace} --kubeconfig ${pathToKubeconfig}`);
|
||||
|
||||
return JSON.parse(stdout);
|
||||
} catch (error) {
|
||||
throw error?.stderr || error;
|
||||
}
|
||||
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, 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;
|
||||
}
|
||||
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, 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`);
|
||||
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",
|
||||
];
|
||||
|
||||
return JSON.parse(stdout).items;
|
||||
try {
|
||||
const helmOutput = await execHelm(helmArgs);
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
let stdout = "";
|
||||
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);
|
||||
|
||||
kubectl.stderr.on("data", output => stderr += output);
|
||||
kubectl.stdout.on("data", output => stdout += output);
|
||||
kubectl.stdin.write(helmOutput);
|
||||
kubectl.stdin.end();
|
||||
});
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
|
||||
@ -20,13 +20,14 @@
|
||||
*/
|
||||
|
||||
import yaml from "js-yaml";
|
||||
import { readFile } from "fs-extra";
|
||||
import { promiseExec } from "../../common/utils/promise-exec";
|
||||
import { BaseEncodingOptions, readFile } from "fs-extra";
|
||||
import { promiseExecFile } from "../../common/utils/promise-exec";
|
||||
import { helmCli } from "./helm-cli";
|
||||
import { Singleton } from "../../common/utils/singleton";
|
||||
import { customRequestPromise } from "../../common/request";
|
||||
import orderBy from "lodash/orderBy";
|
||||
import logger from "../logger";
|
||||
import type { ExecFileOptions } from "child_process";
|
||||
|
||||
export type HelmEnv = Record<string, string> & {
|
||||
HELM_REPOSITORY_CACHE?: string;
|
||||
@ -49,6 +50,18 @@ export interface HelmRepo {
|
||||
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 {
|
||||
protected repos: HelmRepo[];
|
||||
protected helmEnv: HelmEnv;
|
||||
@ -77,11 +90,8 @@ export class HelmRepoManager extends Singleton {
|
||||
}
|
||||
|
||||
protected static async parseHelmEnv() {
|
||||
const helm = await helmCli.binaryPath();
|
||||
const { stdout } = await promiseExec(`"${helm}" env`).catch((error) => {
|
||||
throw(error.stderr);
|
||||
});
|
||||
const lines = stdout.split(/\r?\n/); // split by new line feed
|
||||
const output = await execHelm(["env"]);
|
||||
const lines = output.split(/\r?\n/); // split by new line feed
|
||||
const env: HelmEnv = {};
|
||||
|
||||
lines.forEach((line: string) => {
|
||||
@ -135,57 +145,73 @@ export class HelmRepoManager extends Singleton {
|
||||
cacheFilePath: `${this.helmEnv.HELM_REPOSITORY_CACHE}/${repo.name}-index.yaml`,
|
||||
}));
|
||||
} catch (error) {
|
||||
logger.error(`[HELM]: repositories listing error "${error}"`);
|
||||
logger.error(`[HELM]: repositories listing error`, error);
|
||||
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
public static async update() {
|
||||
const helm = await helmCli.binaryPath();
|
||||
const { stdout } = await promiseExec(`"${helm}" repo update`).catch((error) => {
|
||||
return { stdout: error.stdout };
|
||||
});
|
||||
|
||||
return stdout;
|
||||
return execHelm([
|
||||
"repo",
|
||||
"update",
|
||||
]);
|
||||
}
|
||||
|
||||
public static async addRepo({ name, url }: HelmRepo) {
|
||||
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) {
|
||||
logger.info(`[HELM]: adding repo "${repoAttributes.name}" from ${repoAttributes.url}`);
|
||||
const helm = await helmCli.binaryPath();
|
||||
public static async addCustomRepo({ name, url, insecureSkipTlsVerify, username, password, caFile, keyFile, certFile }: HelmRepo) {
|
||||
logger.info(`[HELM]: adding repo ${name} from ${url}`);
|
||||
const args = [
|
||||
"repo",
|
||||
"add",
|
||||
name,
|
||||
url,
|
||||
];
|
||||
|
||||
const insecureSkipTlsVerify = repoAttributes.insecureSkipTlsVerify ? " --insecure-skip-tls-verify" : "";
|
||||
const username = repoAttributes.username ? ` --username "${repoAttributes.username}"` : "";
|
||||
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}"` : "";
|
||||
if (insecureSkipTlsVerify) {
|
||||
args.push("--insecure-skip-tls-verify");
|
||||
}
|
||||
|
||||
const addRepoCommand = `"${helm}" repo add ${repoAttributes.name} ${repoAttributes.url}${insecureSkipTlsVerify}${username}${password}${caFile}${keyFile}${certFile}`;
|
||||
const { stdout } = await promiseExec(addRepoCommand).catch((error) => {
|
||||
throw(error.stderr);
|
||||
});
|
||||
if (username) {
|
||||
args.push("--username", username);
|
||||
}
|
||||
|
||||
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> {
|
||||
logger.info(`[HELM]: removing repo "${name}" from ${url}`);
|
||||
const helm = await helmCli.binaryPath();
|
||||
const { stdout } = await promiseExec(`"${helm}" repo remove ${name}`).catch((error) => {
|
||||
throw(error.stderr);
|
||||
});
|
||||
logger.info(`[HELM]: removing repo ${name} (${url})`);
|
||||
|
||||
return stdout;
|
||||
return execHelm([
|
||||
"repo",
|
||||
"remove",
|
||||
name,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@ -65,13 +65,19 @@ class HelmService {
|
||||
public async listReleases(cluster: Cluster, namespace: string = null) {
|
||||
const proxyKubeconfig = await cluster.getProxyKubeconfigPath();
|
||||
|
||||
logger.debug("list releases");
|
||||
|
||||
return listReleases(proxyKubeconfig, namespace);
|
||||
}
|
||||
|
||||
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");
|
||||
|
||||
return getRelease(releaseName, namespace, cluster);
|
||||
return getRelease(releaseName, namespace, kubeconfigPath, kubectlPath);
|
||||
}
|
||||
|
||||
public async getReleaseValues(releaseName: string, { cluster, namespace, all }: GetReleaseValuesArgs) {
|
||||
@ -79,7 +85,7 @@ class HelmService {
|
||||
|
||||
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) {
|
||||
@ -99,9 +105,13 @@ class HelmService {
|
||||
}
|
||||
|
||||
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");
|
||||
|
||||
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) {
|
||||
|
||||
@ -21,7 +21,7 @@
|
||||
|
||||
import path from "path";
|
||||
import fs from "fs";
|
||||
import { promiseExec } from "../common/utils/promise-exec";
|
||||
import { promiseExecFile } from "../common/utils/promise-exec";
|
||||
import logger from "./logger";
|
||||
import { ensureDir, pathExists } from "fs-extra";
|
||||
import * as lockFile from "proper-lockfile";
|
||||
@ -199,7 +199,12 @@ export class Kubectl {
|
||||
|
||||
if (exists) {
|
||||
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);
|
||||
|
||||
if (!checkVersion) {
|
||||
|
||||
@ -46,7 +46,7 @@ async function loadMetrics(promQueries: string[], cluster: Cluster, prometheusPa
|
||||
return await getMetrics(cluster, prometheusPath, { query, ...queryParams });
|
||||
} catch (error) {
|
||||
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");
|
||||
}
|
||||
|
||||
|
||||
@ -24,7 +24,7 @@ import { makeObservable, observable } from "mobx";
|
||||
import { app, BrowserWindow, dialog, ipcMain, shell, webContents } from "electron";
|
||||
import windowStateKeeper from "electron-window-state";
|
||||
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 { ClusterFrameInfo, clusterFrameMap } from "../common/cluster-frames";
|
||||
import { IpcRendererNavigationEvents } from "../renderer/navigation/events";
|
||||
@ -181,7 +181,7 @@ export class WindowManager extends Singleton {
|
||||
|
||||
if (!this.mainWindow) {
|
||||
viewHasLoaded = new Promise<void>(resolve => {
|
||||
ipcMain.once(IpcRendererNavigationEvents.LOADED, () => resolve());
|
||||
ipcMain.once(BundledExtensionsLoaded, () => resolve());
|
||||
});
|
||||
await this.initMainWindow(showSplash);
|
||||
}
|
||||
|
||||
@ -30,6 +30,8 @@ import { once } from "lodash";
|
||||
import logger from "../../common/logger";
|
||||
import { catalogEntityRunContext } from "./catalog-entity";
|
||||
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 CatalogEntityOnBeforeRun = (event: CatalogRunEvent) => void | Promise<void>;
|
||||
@ -70,9 +72,12 @@ export class CatalogEntityRegistry {
|
||||
}
|
||||
|
||||
init() {
|
||||
ipcRendererOn("catalog:items", (event, items: (CatalogEntityData & CatalogEntityKindData)[]) => {
|
||||
ipcRendererOn(CatalogIpcEvents.ITEMS, (event, items: (CatalogEntityData & CatalogEntityKindData)[]) => {
|
||||
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)[]) {
|
||||
|
||||
@ -19,7 +19,7 @@
|
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
import React from "react";
|
||||
import { observable, makeObservable } from "mobx";
|
||||
import { observable, makeObservable, when } from "mobx";
|
||||
import { disposeOnUnmount, observer } from "mobx-react";
|
||||
import { Redirect, Route, Router, Switch } from "react-router";
|
||||
import { history } from "./navigation";
|
||||
@ -74,6 +74,7 @@ import { PortForwardDialog } from "./port-forward";
|
||||
import { DeleteClusterDialog } from "./components/delete-cluster-dialog";
|
||||
import { WorkloadsOverview } from "./components/+workloads-overview/overview";
|
||||
import { KubeObjectListLayout } from "./components/kube-object-list-layout";
|
||||
import type { KubernetesCluster } from "../common/catalog-entities";
|
||||
|
||||
@observer
|
||||
export class ClusterFrame extends React.Component {
|
||||
@ -101,7 +102,18 @@ export class ClusterFrame extends React.Component {
|
||||
|
||||
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(() => {
|
||||
appEventBus.emit({
|
||||
|
||||
@ -19,7 +19,7 @@
|
||||
* 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 fse from "fs-extra";
|
||||
|
||||
@ -19,7 +19,7 @@
|
||||
* 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 { observer } from "mobx-react";
|
||||
import { Drawer, DrawerItem } from "../drawer";
|
||||
|
||||
116
src/renderer/components/+catalog/catalog-entity-item.tsx
Normal file
116
src/renderer/components/+catalog/catalog-entity-item.tsx
Normal 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);
|
||||
}
|
||||
}
|
||||
@ -19,8 +19,8 @@
|
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
import treeStyles from "./catalog-tree.module.css";
|
||||
import styles from "./catalog-menu.module.css";
|
||||
import treeStyles from "./catalog-tree.module.scss";
|
||||
import styles from "./catalog-menu.module.scss";
|
||||
|
||||
import React from "react";
|
||||
import { TreeItem, TreeItemProps, TreeView } from "@material-ui/lab";
|
||||
|
||||
@ -19,7 +19,7 @@
|
||||
* 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 { disposeOnUnmount, observer } from "mobx-react";
|
||||
|
||||
@ -19,7 +19,7 @@
|
||||
* 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 { observer } from "mobx-react";
|
||||
|
||||
@ -19,7 +19,7 @@
|
||||
* 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 { observer } from "mobx-react";
|
||||
|
||||
@ -19,7 +19,7 @@
|
||||
* 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 { reaction } from "mobx";
|
||||
|
||||
@ -19,7 +19,7 @@
|
||||
* 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 { observer } from "mobx-react";
|
||||
|
||||
@ -19,7 +19,7 @@
|
||||
* 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 { observable, makeObservable } from "mobx";
|
||||
|
||||
@ -22,10 +22,12 @@ import type { Injectable } from "@ogre-tools/injectable";
|
||||
import { lifecycleEnum } from "@ogre-tools/injectable";
|
||||
import { attemptInstallByInfo, ExtensionInfo } from "./attempt-install-by-info";
|
||||
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>, {}> = {
|
||||
getDependencies: di => ({
|
||||
attemptInstall: di.inject(attemptInstallInjectable),
|
||||
getBaseRegistryUrl: di.inject(getBaseRegistryUrlInjectable),
|
||||
}),
|
||||
|
||||
instantiate: attemptInstallByInfo,
|
||||
|
||||
@ -36,32 +36,39 @@ export interface ExtensionInfo {
|
||||
}
|
||||
|
||||
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,
|
||||
version,
|
||||
requireConfirmation = false,
|
||||
}: ExtensionInfo) => {
|
||||
const disposer = ExtensionInstallationStateStore.startPreInstall();
|
||||
const registryUrl = new URLParse("https://registry.npmjs.com")
|
||||
.set("pathname", name)
|
||||
.toString();
|
||||
const { promise } = downloadJson({ url: registryUrl });
|
||||
const json = await promise.catch(console.error);
|
||||
const baseUrl = await getBaseRegistryUrl();
|
||||
const registryUrl = new URLParse(baseUrl).set("pathname", name).toString();
|
||||
let json: any;
|
||||
|
||||
if (
|
||||
!json ||
|
||||
json.error ||
|
||||
typeof json.versions !== "object" ||
|
||||
!json.versions
|
||||
) {
|
||||
const message = json?.error ? `: ${json.error}` : "";
|
||||
try {
|
||||
json = await downloadJson({ url: registryUrl }).promise;
|
||||
|
||||
Notifications.error(
|
||||
`Failed to get registry information for that extension${message}`,
|
||||
);
|
||||
if (!json || json.error || typeof json.versions !== "object" || !json.versions) {
|
||||
const message = json?.error ? `: ${json.error}` : "";
|
||||
|
||||
Notifications.error(`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();
|
||||
}
|
||||
|
||||
@ -49,34 +49,28 @@ import type { LensExtensionId } from "../../../extensions/lens-extension";
|
||||
import installOnDropInjectable from "./install-on-drop/install-on-drop.injectable";
|
||||
import { supportedExtensionFormats } from "./supported-extension-formats";
|
||||
|
||||
interface Props {
|
||||
dependencies: {
|
||||
userExtensions: IComputedValue<InstalledExtension[]>;
|
||||
enableExtension: (id: LensExtensionId) => void;
|
||||
disableExtension: (id: LensExtensionId) => void;
|
||||
confirmUninstallExtension: (extension: InstalledExtension) => Promise<void>;
|
||||
installFromInput: (input: string) => Promise<void>;
|
||||
installFromSelectFileDialog: () => Promise<void>;
|
||||
installOnDrop: (files: File[]) => Promise<void>;
|
||||
};
|
||||
interface Dependencies {
|
||||
userExtensions: IComputedValue<InstalledExtension[]>;
|
||||
enableExtension: (id: LensExtensionId) => void;
|
||||
disableExtension: (id: LensExtensionId) => void;
|
||||
confirmUninstallExtension: (extension: InstalledExtension) => Promise<void>;
|
||||
installFromInput: (input: string) => Promise<void>;
|
||||
installFromSelectFileDialog: () => Promise<void>;
|
||||
installOnDrop: (files: File[]) => Promise<void>;
|
||||
}
|
||||
|
||||
@observer
|
||||
class NonInjectedExtensions extends React.Component<Props> {
|
||||
class NonInjectedExtensions extends React.Component<Dependencies> {
|
||||
@observable installPath = "";
|
||||
|
||||
constructor(props: Props) {
|
||||
constructor(props: Dependencies) {
|
||||
super(props);
|
||||
makeObservable(this);
|
||||
}
|
||||
|
||||
get dependencies() {
|
||||
return this.props.dependencies;
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
disposeOnUnmount(this, [
|
||||
reaction(() => this.dependencies.userExtensions.get().length, (curSize, prevSize) => {
|
||||
reaction(() => this.props.userExtensions.get().length, (curSize, prevSize) => {
|
||||
if (curSize > prevSize) {
|
||||
disposeOnUnmount(this, [
|
||||
when(() => !ExtensionInstallationStateStore.anyInstalling, () => this.installPath = ""),
|
||||
@ -87,10 +81,10 @@ class NonInjectedExtensions extends React.Component<Props> {
|
||||
}
|
||||
|
||||
render() {
|
||||
const userExtensions = this.dependencies.userExtensions.get();
|
||||
const userExtensions = this.props.userExtensions.get();
|
||||
|
||||
return (
|
||||
<DropFileInput onDropFiles={this.dependencies.installOnDrop}>
|
||||
<DropFileInput onDropFiles={this.props.installOnDrop}>
|
||||
<SettingLayout className="Extensions" contentGaps={false}>
|
||||
<section>
|
||||
<h1>Extensions</h1>
|
||||
@ -106,8 +100,8 @@ class NonInjectedExtensions extends React.Component<Props> {
|
||||
<Install
|
||||
supportedFormats={supportedExtensionFormats}
|
||||
onChange={value => (this.installPath = value)}
|
||||
installFromInput={() => this.dependencies.installFromInput(this.installPath)}
|
||||
installFromSelectFileDialog={this.dependencies.installFromSelectFileDialog}
|
||||
installFromInput={() => this.props.installFromInput(this.installPath)}
|
||||
installFromSelectFileDialog={this.props.installFromSelectFileDialog}
|
||||
installPath={this.installPath}
|
||||
/>
|
||||
|
||||
@ -115,9 +109,9 @@ class NonInjectedExtensions extends React.Component<Props> {
|
||||
|
||||
<InstalledExtensions
|
||||
extensions={userExtensions}
|
||||
enable={this.dependencies.enableExtension}
|
||||
disable={this.dependencies.disableExtension}
|
||||
uninstall={this.dependencies.confirmUninstallExtension}
|
||||
enable={this.props.enableExtension}
|
||||
disable={this.props.disableExtension}
|
||||
uninstall={this.props.confirmUninstallExtension}
|
||||
/>
|
||||
</section>
|
||||
</SettingLayout>
|
||||
@ -126,10 +120,10 @@ class NonInjectedExtensions extends React.Component<Props> {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export const Extensions = withInjectables(NonInjectedExtensions, {
|
||||
getProps: di => ({
|
||||
dependencies: {
|
||||
export const Extensions = withInjectables<Dependencies>(
|
||||
NonInjectedExtensions,
|
||||
{
|
||||
getProps: (di) => ({
|
||||
userExtensions: di.inject(userExtensionsInjectable),
|
||||
enableExtension: di.inject(enableExtensionInjectable),
|
||||
disableExtension: di.inject(disableExtensionInjectable),
|
||||
@ -140,6 +134,6 @@ export const Extensions = withInjectables(NonInjectedExtensions, {
|
||||
installFromSelectFileDialog: di.inject(
|
||||
installFromSelectFileDialogInjectable,
|
||||
),
|
||||
},
|
||||
}),
|
||||
});
|
||||
}),
|
||||
},
|
||||
);
|
||||
|
||||
@ -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;
|
||||
@ -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;
|
||||
}
|
||||
};
|
||||
@ -19,7 +19,7 @@
|
||||
* 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 { prevDefault } from "../../utils";
|
||||
import { Button } from "../button";
|
||||
|
||||
@ -19,7 +19,7 @@
|
||||
* 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 { ExtensionDiscovery, InstalledExtension } from "../../../extensions/extension-discovery";
|
||||
import { Icon } from "../icon";
|
||||
|
||||
@ -19,7 +19,7 @@
|
||||
* 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 { cssNames } from "../../utils";
|
||||
|
||||
|
||||
@ -19,7 +19,7 @@
|
||||
* 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 { DrawerItem, DrawerTitle } from "../drawer";
|
||||
|
||||
@ -121,7 +121,7 @@ export class AddHelmRepoDialog extends React.Component<Props> {
|
||||
<div className="flex gaps align-center">
|
||||
<Input
|
||||
placeholder={placeholder}
|
||||
validators = {isPath}
|
||||
validators={isPath}
|
||||
className="box grow"
|
||||
value={this.getFilePath(fileType)}
|
||||
onChange={v => this.setFilepath(fileType, v)}
|
||||
@ -172,7 +172,7 @@ export class AddHelmRepoDialog extends React.Component<Props> {
|
||||
close={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">
|
||||
<Input
|
||||
autoFocus required
|
||||
|
||||
@ -29,7 +29,11 @@ import { Input } from "../input";
|
||||
import { isWindows } from "../../../common/vars";
|
||||
import { FormSwitch, Switcher } from "../switch";
|
||||
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 => ({
|
||||
label: zone,
|
||||
@ -41,6 +45,7 @@ const updateChannelOptions: SelectOption<string>[] = Array.from(
|
||||
);
|
||||
|
||||
export const Application = observer(() => {
|
||||
const userStore = UserStore.getInstance();
|
||||
const defaultShell = process.env.SHELL
|
||||
|| process.env.PTYSHELL
|
||||
|| (
|
||||
@ -49,7 +54,9 @@ export const Application = observer(() => {
|
||||
: "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 (
|
||||
<section id="application">
|
||||
@ -58,8 +65,8 @@ export const Application = observer(() => {
|
||||
<SubTitle title="Theme"/>
|
||||
<Select
|
||||
options={ThemeStore.getInstance().themeOptions}
|
||||
value={UserStore.getInstance().colorTheme}
|
||||
onChange={({ value }: SelectOption) => UserStore.getInstance().colorTheme = value}
|
||||
value={userStore.colorTheme}
|
||||
onChange={({ value }) => userStore.colorTheme = value}
|
||||
themeName="lens"
|
||||
/>
|
||||
</section>
|
||||
@ -72,8 +79,8 @@ export const Application = observer(() => {
|
||||
theme="round-black"
|
||||
placeholder={defaultShell}
|
||||
value={shell}
|
||||
onChange={v => setShell(v)}
|
||||
onBlur={() => UserStore.getInstance().shell = shell}
|
||||
onChange={setShell}
|
||||
onBlur={() => userStore.shell = shell}
|
||||
/>
|
||||
</section>
|
||||
|
||||
@ -83,8 +90,8 @@ export const Application = observer(() => {
|
||||
label="Copy on select and paste on right-click"
|
||||
control={
|
||||
<Switcher
|
||||
checked={UserStore.getInstance().terminalCopyOnSelect}
|
||||
onChange={v => UserStore.getInstance().terminalCopyOnSelect = v.target.checked}
|
||||
checked={userStore.terminalCopyOnSelect}
|
||||
onChange={v => userStore.terminalCopyOnSelect = v.target.checked}
|
||||
name="terminalCopyOnSelect"
|
||||
/>
|
||||
}
|
||||
@ -93,13 +100,46 @@ export const Application = observer(() => {
|
||||
|
||||
<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> 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">
|
||||
<SubTitle title="Start-up"/>
|
||||
<FormSwitch
|
||||
control={
|
||||
<Switcher
|
||||
checked={UserStore.getInstance().openAtLogin}
|
||||
onChange={v => UserStore.getInstance().openAtLogin = v.target.checked}
|
||||
checked={userStore.openAtLogin}
|
||||
onChange={v => userStore.openAtLogin = v.target.checked}
|
||||
name="startup"
|
||||
/>
|
||||
}
|
||||
@ -109,12 +149,16 @@ export const Application = observer(() => {
|
||||
|
||||
<hr />
|
||||
|
||||
{extensionSettings.map(setting => (
|
||||
<ExtensionSettings key={setting.id} setting={setting} size="normal" />
|
||||
))}
|
||||
|
||||
<section id="update-channel">
|
||||
<SubTitle title="Update Channel"/>
|
||||
<Select
|
||||
options={updateChannelOptions}
|
||||
value={UserStore.getInstance().updateChannel}
|
||||
onChange={({ value }: SelectOption) => UserStore.getInstance().updateChannel = value}
|
||||
value={userStore.updateChannel}
|
||||
onChange={({ value }) => userStore.updateChannel = value}
|
||||
themeName="lens"
|
||||
/>
|
||||
</section>
|
||||
@ -125,8 +169,8 @@ export const Application = observer(() => {
|
||||
<SubTitle title="Locale Timezone" />
|
||||
<Select
|
||||
options={timezoneOptions}
|
||||
value={UserStore.getInstance().localeTimezone}
|
||||
onChange={({ value }: SelectOption) => UserStore.getInstance().setLocaleTimezone(value)}
|
||||
value={userStore.localeTimezone}
|
||||
onChange={({ value }) => userStore.setLocaleTimezone(value)}
|
||||
themeName="lens"
|
||||
/>
|
||||
</section>
|
||||
|
||||
50
src/renderer/components/+preferences/extension-settings.tsx
Normal file
50
src/renderer/components/+preferences/extension-settings.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@ -22,16 +22,16 @@
|
||||
import { observer } from "mobx-react";
|
||||
import React from "react";
|
||||
import { AppPreferenceRegistry } from "../../../extensions/registries";
|
||||
import { ExtensionSettings } from "./preferences";
|
||||
import { ExtensionSettings } from "./extension-settings";
|
||||
|
||||
export const Extensions = observer(() => {
|
||||
const extensions = AppPreferenceRegistry.getInstance().getItems();
|
||||
const settings = AppPreferenceRegistry.getInstance().getItems();
|
||||
|
||||
return (
|
||||
<section id="extensions">
|
||||
<h2>Extensions</h2>
|
||||
{extensions.filter(e => !e.showInPreferencesTab).map((extension) =>
|
||||
<ExtensionSettings key={extension.id} {...extension}/>,
|
||||
{settings.filter(e => !e.showInPreferencesTab).map((setting) =>
|
||||
<ExtensionSettings key={setting.id} setting={setting} size="small" />,
|
||||
)}
|
||||
</section>
|
||||
);
|
||||
|
||||
@ -19,7 +19,7 @@
|
||||
* 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 { action, computed, observable, makeObservable } from "mobx";
|
||||
|
||||
@ -24,7 +24,6 @@ import { makeObservable, observable } from "mobx";
|
||||
import { observer } from "mobx-react";
|
||||
import React from "react";
|
||||
import { matchPath, Redirect, Route, RouteProps, Switch } from "react-router";
|
||||
|
||||
import {
|
||||
appRoute,
|
||||
appURL,
|
||||
@ -40,10 +39,9 @@ import {
|
||||
telemetryRoute,
|
||||
telemetryURL,
|
||||
} 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 { SettingLayout } from "../layout/setting-layout";
|
||||
import { SubTitle } from "../layout/sub-title";
|
||||
import { Tab, Tabs } from "../tabs";
|
||||
import { Application } from "./application";
|
||||
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>
|
||||
);
|
||||
}
|
||||
|
||||
@ -18,7 +18,7 @@
|
||||
* 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 "./removable-item.module.css";
|
||||
import styles from "./removable-item.module.scss";
|
||||
|
||||
import React, { DOMAttributes } from "react";
|
||||
|
||||
|
||||
@ -25,7 +25,7 @@ import { sentryDsn } from "../../../common/vars";
|
||||
import { AppPreferenceRegistry } from "../../../extensions/registries";
|
||||
import { Checkbox } from "../checkbox";
|
||||
import { SubTitle } from "../layout/sub-title";
|
||||
import { ExtensionSettings } from "./preferences";
|
||||
import { ExtensionSettings } from "./extension-settings";
|
||||
|
||||
export const Telemetry = observer(() => {
|
||||
const extensions = AppPreferenceRegistry.getInstance().getItems();
|
||||
@ -34,7 +34,7 @@ export const Telemetry = observer(() => {
|
||||
return (
|
||||
<section id="telemetry">
|
||||
<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 ? (
|
||||
<React.Fragment key='sentry'>
|
||||
<section id='sentry' className="small">
|
||||
|
||||
@ -19,9 +19,12 @@
|
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
@import "tailwindcss/utilities";
|
||||
@import "~flex.box";
|
||||
@import "fonts";
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
@import "~flex.box"; // todo: replace with tailwind's flexbox classes
|
||||
@import "./fonts";
|
||||
@import "../themes/theme-vars";
|
||||
|
||||
:root {
|
||||
|
||||
@ -19,7 +19,7 @@
|
||||
* 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 randomColor from "randomcolor";
|
||||
|
||||
@ -19,7 +19,7 @@
|
||||
* 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 { computed, makeObservable, observable } from "mobx";
|
||||
|
||||
@ -19,7 +19,7 @@
|
||||
* 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 { observer } from "mobx-react";
|
||||
|
||||
@ -19,7 +19,7 @@
|
||||
* 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 { disposeOnUnmount, observer } from "mobx-react";
|
||||
|
||||
@ -18,7 +18,7 @@
|
||||
* 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 "./delete-cluster-dialog.module.css";
|
||||
import styles from "./delete-cluster-dialog.module.scss";
|
||||
|
||||
import { computed, makeObservable, observable } from "mobx";
|
||||
import { observer } from "mobx-react";
|
||||
|
||||
@ -19,7 +19,7 @@
|
||||
* 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 React from "react";
|
||||
import { makeObservable, observable, reaction } from "mobx";
|
||||
|
||||
@ -19,7 +19,7 @@
|
||||
* 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 { makeObservable, observable } from "mobx";
|
||||
|
||||
@ -19,7 +19,7 @@
|
||||
* 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";
|
||||
|
||||
|
||||
@ -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 apiManagerInjectable from "./dependencies/api-manager.injectable";
|
||||
|
||||
// TODO: Replace with KubeObjectMenuProps2
|
||||
export interface KubeObjectMenuProps<TKubeObject> extends MenuActionsProps {
|
||||
export interface KubeObjectMenuProps<TKubeObject extends KubeObject> extends MenuActionsProps {
|
||||
object: TKubeObject | null | undefined;
|
||||
editable?: boolean;
|
||||
removable?: boolean;
|
||||
}
|
||||
|
||||
interface KubeObjectMenuProps2 extends MenuActionsProps {
|
||||
object: KubeObject | null | undefined;
|
||||
editable?: boolean;
|
||||
removable?: boolean;
|
||||
|
||||
dependencies: {
|
||||
apiManager: ApiManager;
|
||||
kubeObjectMenuItems: React.ElementType[];
|
||||
clusterName: string;
|
||||
hideDetails: () => void;
|
||||
editResourceTab: (kubeObject: KubeObject) => void;
|
||||
};
|
||||
interface Dependencies {
|
||||
apiManager: ApiManager;
|
||||
kubeObjectMenuItems: React.ElementType[];
|
||||
clusterName: string;
|
||||
hideDetails: () => void;
|
||||
editResourceTab: (kubeObject: KubeObject) => void;
|
||||
}
|
||||
|
||||
class NonInjectedKubeObjectMenu extends React.Component<KubeObjectMenuProps2> {
|
||||
get dependencies() {
|
||||
return this.props.dependencies;
|
||||
}
|
||||
|
||||
class NonInjectedKubeObjectMenu<TKubeObject extends KubeObject> extends React.Component<KubeObjectMenuProps<TKubeObject> & Dependencies> {
|
||||
|
||||
get store() {
|
||||
const { object } = this.props;
|
||||
|
||||
if (!object) return null;
|
||||
|
||||
return this.props.dependencies.apiManager.getStore(object.selfLink);
|
||||
return this.props.apiManager.getStore(object.selfLink);
|
||||
}
|
||||
|
||||
get isEditable() {
|
||||
@ -76,13 +66,13 @@ class NonInjectedKubeObjectMenu extends React.Component<KubeObjectMenuProps2> {
|
||||
|
||||
@boundMethod
|
||||
async update() {
|
||||
this.props.dependencies.hideDetails();
|
||||
this.props.dependencies.editResourceTab(this.props.object);
|
||||
this.props.hideDetails();
|
||||
this.props.editResourceTab(this.props.object);
|
||||
}
|
||||
|
||||
@boundMethod
|
||||
async remove() {
|
||||
this.props.dependencies.hideDetails();
|
||||
this.props.hideDetails();
|
||||
const { object, removeAction } = this.props;
|
||||
|
||||
if (removeAction) await removeAction();
|
||||
@ -103,7 +93,7 @@ class NonInjectedKubeObjectMenu extends React.Component<KubeObjectMenuProps2> {
|
||||
|
||||
return (
|
||||
<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>
|
||||
);
|
||||
}
|
||||
@ -111,7 +101,7 @@ class NonInjectedKubeObjectMenu extends React.Component<KubeObjectMenuProps2> {
|
||||
getMenuItems(): React.ReactChild[] {
|
||||
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}`} />
|
||||
));
|
||||
}
|
||||
@ -134,19 +124,23 @@ class NonInjectedKubeObjectMenu extends React.Component<KubeObjectMenuProps2> {
|
||||
}
|
||||
}
|
||||
|
||||
export const KubeObjectMenu = withInjectables(NonInjectedKubeObjectMenu, {
|
||||
getProps: (di, props) => ({
|
||||
dependencies: {
|
||||
clusterName: di.inject(clusterNameInjectable),
|
||||
apiManager: di.inject(apiManagerInjectable),
|
||||
editResourceTab: di.inject(editResourceTabInjectable),
|
||||
hideDetails: di.inject(hideDetailsInjectable),
|
||||
export function KubeObjectMenu<T extends KubeObject>(
|
||||
props: KubeObjectMenuProps<T>,
|
||||
) {
|
||||
return withInjectables<Dependencies, KubeObjectMenuProps<T>>(
|
||||
NonInjectedKubeObjectMenu,
|
||||
{
|
||||
getProps: (di, props) => ({
|
||||
clusterName: di.inject(clusterNameInjectable),
|
||||
apiManager: di.inject(apiManagerInjectable),
|
||||
editResourceTab: di.inject(editResourceTabInjectable),
|
||||
hideDetails: di.inject(hideDetailsInjectable),
|
||||
|
||||
kubeObjectMenuItems: di.inject(kubeObjectMenuItemsInjectable, {
|
||||
kubeObject: props.object,
|
||||
kubeObjectMenuItems: di.inject(kubeObjectMenuItemsInjectable, {
|
||||
kubeObject: props.object,
|
||||
}),
|
||||
...props,
|
||||
}),
|
||||
},
|
||||
|
||||
...props,
|
||||
}),
|
||||
});
|
||||
)(props);
|
||||
}
|
||||
|
||||
@ -19,7 +19,7 @@
|
||||
* 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 { makeObservable, observable } from "mobx";
|
||||
import { observer } from "mobx-react";
|
||||
|
||||
@ -19,7 +19,7 @@
|
||||
* 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 { observer } from "mobx-react";
|
||||
|
||||
@ -19,7 +19,7 @@
|
||||
* 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 React, { useState } from "react";
|
||||
import { HotbarStore } from "../../../common/hotbar-store";
|
||||
|
||||
@ -19,7 +19,7 @@
|
||||
* 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 React from "react";
|
||||
|
||||
@ -19,7 +19,7 @@
|
||||
* 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 { observer } from "mobx-react";
|
||||
import { TopBarRegistry } from "../../../extensions/registries";
|
||||
|
||||
@ -19,7 +19,7 @@
|
||||
* 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 { SearchInput } from "../input";
|
||||
|
||||
|
||||
@ -19,7 +19,7 @@
|
||||
* 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 { observer } from "mobx-react";
|
||||
import { action, computed, makeObservable, observable, reaction } from "mobx";
|
||||
|
||||
@ -19,7 +19,7 @@
|
||||
* 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 { useCallback, useMemo } from "react";
|
||||
import { useFlexLayout, useSortBy, useTable, UseTableOptions } from "react-table";
|
||||
|
||||
@ -29,7 +29,7 @@ import { ErrorBoundary } from "./components/error-boundary";
|
||||
import { Notifications } from "./components/notifications";
|
||||
import { ConfirmDialog } from "./components/confirm-dialog";
|
||||
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 { registerIpcListeners } from "./ipc";
|
||||
import { ipcRenderer } from "electron";
|
||||
@ -39,6 +39,7 @@ import logger from "../common/logger";
|
||||
import { unmountComponentAtNode } from "react-dom";
|
||||
import { ClusterFrameHandler } from "./components/cluster-manager/lens-views";
|
||||
import type { LensProtocolRouterRenderer } from "./protocol-handler";
|
||||
import { delay } from "./utils";
|
||||
|
||||
injectSystemCAs();
|
||||
|
||||
@ -54,8 +55,21 @@ export class RootFrame extends React.Component {
|
||||
lensProtocolRouterRendererInjectable: LensProtocolRouterRenderer,
|
||||
) {
|
||||
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();
|
||||
|
||||
bindProtocolAddRouteHandlers();
|
||||
|
||||
window.addEventListener("offline", () => broadcastMessage("network:offline"));
|
||||
|
||||
@ -20,8 +20,7 @@
|
||||
*/
|
||||
|
||||
module.exports = {
|
||||
mode: "jit",
|
||||
purge: ["src/**/*.tsx"],
|
||||
content: ["src/**/*.tsx"],
|
||||
darkMode: "class",
|
||||
theme: {
|
||||
fontFamily: {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user