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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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.
*/
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";

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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.
*/
import styles from "./install.module.css";
import styles from "./install.module.scss";
import React from "react";
import { prevDefault } from "../../utils";
import { Button } from "../button";

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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