mirror of
https://github.com/lensapp/lens.git
synced 2025-05-20 05:10:56 +00:00
Release/v5.3.4 (#4721)
* Use electron.clipboard for all clipboard uses (#4535) Signed-off-by: Jim Ehrismann <jehrismann@mirantis.com> * Fix ERR_UNSAFE_PORT from LensProxy (#4558) * Fix ERR_UNSAFE_PORT from LensProxy - Use the current list of ports from chromium as it is much easier to just reject using one of those instead of trying to handle the ERR_UNSAFE_PORT laod error from a BrowserWindow.on("did-fail-load") Signed-off-by: Sebastian Malton <sebastian@malton.name> * Move all port handling into LensProxy Signed-off-by: Sebastian Malton <sebastian@malton.name> * don't use so many exceptions Signed-off-by: Sebastian Malton <sebastian@malton.name> Signed-off-by: Jim Ehrismann <jehrismann@mirantis.com> * Fix not being able to clear set cluster icon (#4555) Signed-off-by: Jim Ehrismann <jehrismann@mirantis.com> * Fix extension engine range not working for some ^ ranges (#4554) Signed-off-by: Jim Ehrismann <jehrismann@mirantis.com> * Fix crash on NetworkPolicy when matchLabels is missing (#4500) Signed-off-by: Jim Ehrismann <jehrismann@mirantis.com> * Replace all uses of promiseExec with promiseExecFile (#4514) Signed-off-by: Jim Ehrismann <jehrismann@mirantis.com> * Less noisy metrics-not-available error logging (#4602) Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com> Signed-off-by: Jim Ehrismann <jehrismann@mirantis.com> * Fix close button overflow in Preferences (#4611) * Adding basic colors to tailwind theme Signed-off-by: Alex Andreev <alex.andreev.email@gmail.com> * Using tailwind inline to style close button Signed-off-by: Alex Andreev <alex.andreev.email@gmail.com> * Make Select look similar to inputs Signed-off-by: Alex Andreev <alex.andreev.email@gmail.com> * Moving styles into separate module Signed-off-by: Alex Andreev <alex.andreev.email@gmail.com> * Convert tailwind commands to css Signed-off-by: Alex Andreev <alex.andreev.email@gmail.com> Signed-off-by: Jim Ehrismann <jehrismann@mirantis.com> * Fix prometheus operator metrics work out of the box (#4617) Signed-off-by: Lauri Nevala <lauri.nevala@gmail.com> Signed-off-by: Jim Ehrismann <jehrismann@mirantis.com> * Fix CRD.getPreferedVersion() to work based on apiVersion (#4553) * Fix CRD.getPreferedVersion() to work based on apiVersion Signed-off-by: Sebastian Malton <sebastian@malton.name> * Add tests Signed-off-by: Sebastian Malton <sebastian@malton.name> Signed-off-by: Jim Ehrismann <jehrismann@mirantis.com> * Fix crash for KubeObjectStore.loadAll() (#4675) Signed-off-by: Sebastian Malton <sebastian@malton.name> Signed-off-by: Jim Ehrismann <jehrismann@mirantis.com> * Convert CloseButton styles out from css modules (#4723) * Convert CloseButton styles out from css modules Signed-off-by: Alex Andreev <alex.andreev.email@gmail.com> * Fix close button styling Signed-off-by: Alex Andreev <alex.andreev.email@gmail.com> * release v5.3.4 Signed-off-by: Jim Ehrismann <jehrismann@mirantis.com> Co-authored-by: Sebastian Malton <sebastian@malton.name> Co-authored-by: Jari Kolehmainen <jari.kolehmainen@gmail.com> Co-authored-by: Alex Andreev <alex.andreev.email@gmail.com> Co-authored-by: Lauri Nevala <lauri.nevala@gmail.com>
This commit is contained in:
parent
0a6bafc1be
commit
e8d8f8f610
@ -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,
|
||||
|
||||
@ -3,7 +3,7 @@
|
||||
"productName": "OpenLens",
|
||||
"description": "OpenLens - Open Source IDE for Kubernetes",
|
||||
"homepage": "https://github.com/lensapp/lens",
|
||||
"version": "5.3.3",
|
||||
"version": "5.3.4",
|
||||
"main": "static/build/main.js",
|
||||
"copyright": "© 2021 OpenLens Authors",
|
||||
"license": "MIT",
|
||||
|
||||
@ -90,7 +90,11 @@ export interface ClusterPreferences extends ClusterPrometheusPreferences {
|
||||
terminalCWD?: string;
|
||||
clusterName?: string;
|
||||
iconOrder?: number;
|
||||
icon?: string;
|
||||
/**
|
||||
* The <img> src for the cluster. If set to `null` that means that it was
|
||||
* cleared by preferences.
|
||||
*/
|
||||
icon?: string | null;
|
||||
httpsProxy?: string;
|
||||
hiddenMetrics?: string[];
|
||||
nodeShellImage?: string;
|
||||
|
||||
@ -19,10 +19,10 @@
|
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
import { CustomResourceDefinition } from "../endpoints";
|
||||
import { CustomResourceDefinition, CustomResourceDefinitionSpec } from "../endpoints";
|
||||
|
||||
describe("Crds", () => {
|
||||
describe("getVersion", () => {
|
||||
describe("getVersion()", () => {
|
||||
it("should throw if none of the versions are served", () => {
|
||||
const crd = new CustomResourceDefinition({
|
||||
apiVersion: "apiextensions.k8s.io/v1",
|
||||
@ -136,7 +136,7 @@ describe("Crds", () => {
|
||||
expect(crd.getVersion()).toBe("123");
|
||||
});
|
||||
|
||||
it("should get the version name from the version field", () => {
|
||||
it("should get the version name from the version field, ignoring versions on v1beta", () => {
|
||||
const crd = new CustomResourceDefinition({
|
||||
apiVersion: "apiextensions.k8s.io/v1beta1",
|
||||
kind: "CustomResourceDefinition",
|
||||
@ -147,7 +147,14 @@ describe("Crds", () => {
|
||||
},
|
||||
spec: {
|
||||
version: "abc",
|
||||
},
|
||||
versions: [
|
||||
{
|
||||
name: "foobar",
|
||||
served: true,
|
||||
storage: true,
|
||||
},
|
||||
],
|
||||
} as CustomResourceDefinitionSpec,
|
||||
});
|
||||
|
||||
expect(crd.getVersion()).toBe("abc");
|
||||
|
||||
@ -48,34 +48,36 @@ export interface CRDVersion {
|
||||
additionalPrinterColumns?: AdditionalPrinterColumnsV1[];
|
||||
}
|
||||
|
||||
export interface CustomResourceDefinition {
|
||||
spec: {
|
||||
group: string;
|
||||
/**
|
||||
* @deprecated for apiextensions.k8s.io/v1 but used previously
|
||||
*/
|
||||
version?: string;
|
||||
names: {
|
||||
plural: string;
|
||||
singular: string;
|
||||
kind: string;
|
||||
listKind: string;
|
||||
};
|
||||
scope: "Namespaced" | "Cluster" | string;
|
||||
/**
|
||||
* @deprecated for apiextensions.k8s.io/v1 but used previously
|
||||
*/
|
||||
validation?: object;
|
||||
versions?: CRDVersion[];
|
||||
conversion: {
|
||||
strategy?: string;
|
||||
webhook?: any;
|
||||
};
|
||||
/**
|
||||
* @deprecated for apiextensions.k8s.io/v1 but used previously
|
||||
*/
|
||||
additionalPrinterColumns?: AdditionalPrinterColumnsV1Beta[];
|
||||
export interface CustomResourceDefinitionSpec {
|
||||
group: string;
|
||||
/**
|
||||
* @deprecated for apiextensions.k8s.io/v1 but used in v1beta1
|
||||
*/
|
||||
version?: string;
|
||||
names: {
|
||||
plural: string;
|
||||
singular: string;
|
||||
kind: string;
|
||||
listKind: string;
|
||||
};
|
||||
scope: "Namespaced" | "Cluster";
|
||||
/**
|
||||
* @deprecated for apiextensions.k8s.io/v1 but used in v1beta1
|
||||
*/
|
||||
validation?: object;
|
||||
versions?: CRDVersion[];
|
||||
conversion: {
|
||||
strategy?: string;
|
||||
webhook?: any;
|
||||
};
|
||||
/**
|
||||
* @deprecated for apiextensions.k8s.io/v1 but used in v1beta1
|
||||
*/
|
||||
additionalPrinterColumns?: AdditionalPrinterColumnsV1Beta[];
|
||||
}
|
||||
|
||||
export interface CustomResourceDefinition {
|
||||
spec: CustomResourceDefinitionSpec;
|
||||
status: {
|
||||
conditions: {
|
||||
lastTransitionTime: string;
|
||||
@ -150,27 +152,30 @@ export class CustomResourceDefinition extends KubeObject {
|
||||
}
|
||||
|
||||
getPreferedVersion(): CRDVersion {
|
||||
// Prefer the modern `versions` over the legacy `version`
|
||||
if (this.spec.versions) {
|
||||
for (const version of this.spec.versions) {
|
||||
if (version.storage) {
|
||||
return version;
|
||||
}
|
||||
}
|
||||
} else if (this.spec.version) {
|
||||
const { additionalPrinterColumns: apc } = this.spec;
|
||||
const additionalPrinterColumns = apc?.map(({ JSONPath, ...apc }) => ({ ...apc, jsonPath: JSONPath }));
|
||||
const { apiVersion } = this;
|
||||
|
||||
return {
|
||||
name: this.spec.version,
|
||||
served: true,
|
||||
storage: true,
|
||||
schema: this.spec.validation,
|
||||
additionalPrinterColumns,
|
||||
};
|
||||
switch (apiVersion) {
|
||||
case "apiextensions.k8s.io/v1":
|
||||
for (const version of this.spec.versions) {
|
||||
if (version.storage) {
|
||||
return version;
|
||||
}
|
||||
}
|
||||
break;
|
||||
case "apiextensions.k8s.io/v1beta1":
|
||||
const { additionalPrinterColumns: apc } = this.spec;
|
||||
const additionalPrinterColumns = apc?.map(({ JSONPath, ...apc }) => ({ ...apc, jsonPath: JSONPath }));
|
||||
|
||||
return {
|
||||
name: this.spec.version,
|
||||
served: true,
|
||||
storage: true,
|
||||
schema: this.spec.validation,
|
||||
additionalPrinterColumns,
|
||||
};
|
||||
}
|
||||
|
||||
throw new Error(`Failed to find a version for CustomResourceDefinition ${this.metadata.name}`);
|
||||
throw new Error(`Unknown apiVersion=${apiVersion}: Failed to find a version for CustomResourceDefinition ${this.metadata.name}`);
|
||||
}
|
||||
|
||||
getVersion() {
|
||||
@ -197,7 +202,7 @@ export class CustomResourceDefinition extends KubeObject {
|
||||
const columns = this.getPreferedVersion().additionalPrinterColumns ?? [];
|
||||
|
||||
return columns
|
||||
.filter(column => column.name != "Age" && (ignorePriority || !column.priority));
|
||||
.filter(column => column.name.toLowerCase() != "age" && (ignorePriority || !column.priority));
|
||||
}
|
||||
|
||||
getValidation() {
|
||||
|
||||
@ -235,8 +235,9 @@ export abstract class KubeObjectStore<T extends KubeObject> extends ItemStore<T>
|
||||
}
|
||||
|
||||
@action
|
||||
async loadAll({ namespaces = this.context.contextNamespaces, merge = true, reqInit, onLoadFailure }: KubeObjectStoreLoadAllParams = {}): Promise<void | T[]> {
|
||||
async loadAll({ namespaces, merge = true, reqInit, onLoadFailure }: KubeObjectStoreLoadAllParams = {}): Promise<void | T[]> {
|
||||
await this.contextReady;
|
||||
namespaces ??= this.context.contextNamespaces;
|
||||
this.isLoading = true;
|
||||
|
||||
try {
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -19,29 +19,24 @@
|
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
import { isCompatibleExtension } from "../extension-compatibility";
|
||||
import { rawIsCompatibleExtension } from "../extension-compatibility";
|
||||
import { Console } from "console";
|
||||
import { stdout, stderr } from "process";
|
||||
import type { LensExtensionManifest } from "../lens-extension";
|
||||
import { appSemVer } from "../../common/vars";
|
||||
import { SemVer } from "semver";
|
||||
|
||||
console = new Console(stdout, stderr);
|
||||
|
||||
describe("extension compatibility", () => {
|
||||
describe("appSemVer with no prerelease tag", () => {
|
||||
beforeAll(() => {
|
||||
appSemVer.major = 5;
|
||||
appSemVer.minor = 0;
|
||||
appSemVer.patch = 3;
|
||||
appSemVer.prerelease = [];
|
||||
});
|
||||
const isCompatibleExtension = rawIsCompatibleExtension(new SemVer("5.0.3"));
|
||||
|
||||
it("has no extension comparator", () => {
|
||||
const manifest = { name: "extensionName", version: "0.0.1" };
|
||||
|
||||
expect(isCompatibleExtension(manifest)).toBe(false);
|
||||
});
|
||||
|
||||
|
||||
it.each([
|
||||
{
|
||||
comparator: "",
|
||||
@ -83,19 +78,32 @@ describe("extension compatibility", () => {
|
||||
});
|
||||
|
||||
describe("appSemVer with prerelease tag", () => {
|
||||
beforeAll(() => {
|
||||
appSemVer.major = 5;
|
||||
appSemVer.minor = 0;
|
||||
appSemVer.patch = 3;
|
||||
appSemVer.prerelease = ["beta", 3];
|
||||
const isCompatibleExtension = rawIsCompatibleExtension(new SemVer("5.0.3-beta.3"));
|
||||
|
||||
it("^5.1.0 should work when lens' version is 5.1.0-latest.123456789", () => {
|
||||
const comparer = rawIsCompatibleExtension(new SemVer("5.1.0-latest.123456789"));
|
||||
|
||||
expect(comparer({ name: "extensionName", version: "0.0.1", engines: { lens: "^5.1.0" }})).toBe(true);
|
||||
});
|
||||
|
||||
|
||||
it("^5.1.0 should not when lens' version is 5.1.0-beta.1.123456789", () => {
|
||||
const comparer = rawIsCompatibleExtension(new SemVer("5.1.0-beta.123456789"));
|
||||
|
||||
expect(comparer({ name: "extensionName", version: "0.0.1", engines: { lens: "^5.1.0" }})).toBe(false);
|
||||
});
|
||||
|
||||
it("^5.1.0 should not when lens' version is 5.1.0-alpha.1.123456789", () => {
|
||||
const comparer = rawIsCompatibleExtension(new SemVer("5.1.0-alpha.123456789"));
|
||||
|
||||
expect(comparer({ name: "extensionName", version: "0.0.1", engines: { lens: "^5.1.0" }})).toBe(false);
|
||||
});
|
||||
|
||||
it("has no extension comparator", () => {
|
||||
const manifest = { name: "extensionName", version: "0.0.1" };
|
||||
|
||||
expect(isCompatibleExtension(manifest)).toBe(false);
|
||||
});
|
||||
|
||||
|
||||
it.each([
|
||||
{
|
||||
comparator: "",
|
||||
@ -130,9 +138,7 @@ describe("extension compatibility", () => {
|
||||
expected: false,
|
||||
},
|
||||
])("extension comparator test: %p", ({ comparator, expected }) => {
|
||||
const manifest: LensExtensionManifest = { name: "extensionName", version: "0.0.1", engines: { lens: comparator }};
|
||||
|
||||
expect(isCompatibleExtension(manifest)).toBe(expected);
|
||||
expect(isCompatibleExtension({ name: "extensionName", version: "0.0.1", engines: { lens: comparator }})).toBe(expected);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@ -19,19 +19,47 @@
|
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
import semver from "semver";
|
||||
import semver, { SemVer } from "semver";
|
||||
import { appSemVer, isProduction } from "../common/vars";
|
||||
import type { LensExtensionManifest } from "./lens-extension";
|
||||
|
||||
export function isCompatibleExtension(manifest: LensExtensionManifest): boolean {
|
||||
if (manifest.engines?.lens) {
|
||||
/* include Lens's prerelease tag in the matching so the extension's compatibility is not limited by it */
|
||||
return semver.satisfies(appSemVer, manifest.engines.lens, { includePrerelease: true });
|
||||
export function rawIsCompatibleExtension(version: SemVer): (manifest: LensExtensionManifest) => boolean {
|
||||
const { major, minor, patch, prerelease: oldPrelease } = version;
|
||||
let prerelease = "";
|
||||
|
||||
if (oldPrelease.length > 0) {
|
||||
const [first] = oldPrelease;
|
||||
|
||||
if (first === "alpha" || first === "beta" || first === "rc") {
|
||||
/**
|
||||
* Strip the build IDs and "latest" prerelease tag as that is not really
|
||||
* a part of API version
|
||||
*/
|
||||
prerelease = `-${oldPrelease.slice(0, 2).join(".")}`;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
/**
|
||||
* We unfortunately have to format as string because the constructor only
|
||||
* takes an instance or a string.
|
||||
*/
|
||||
const strippedVersion = new SemVer(`${major}.${minor}.${patch}${prerelease}`, { includePrerelease: true });
|
||||
|
||||
return (manifest: LensExtensionManifest): boolean => {
|
||||
if (manifest.engines?.lens) {
|
||||
/**
|
||||
* include Lens's prerelease tag in the matching so the extension's
|
||||
* compatibility is not limited by it
|
||||
*/
|
||||
return semver.satisfies(strippedVersion, manifest.engines.lens, { includePrerelease: true });
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
}
|
||||
|
||||
export const isCompatibleExtension = rawIsCompatibleExtension(appSemVer);
|
||||
|
||||
export function isCompatibleBundledExtension(manifest: LensExtensionManifest): boolean {
|
||||
return !isProduction || manifest.version === appSemVer.raw;
|
||||
}
|
||||
|
||||
@ -136,11 +136,16 @@ export class ClusterManager extends Singleton {
|
||||
entity.spec.metrics.prometheus = prometheus;
|
||||
}
|
||||
|
||||
// Only set the icon if the preference is set. If the preference is not set
|
||||
// then let the source determine if a cluster has an icon.
|
||||
if (cluster.preferences.icon) {
|
||||
entity.spec.icon ??= {};
|
||||
entity.spec.icon.src = cluster.preferences.icon;
|
||||
} else if (cluster.preferences.icon === null) {
|
||||
/**
|
||||
* NOTE: only clear the icon if set to `null` by ClusterIconSettings.
|
||||
* We can then also clear that value too
|
||||
*/
|
||||
entity.spec.icon = undefined;
|
||||
cluster.preferences.icon = undefined;
|
||||
}
|
||||
|
||||
catalogEntityRegistry.items.splice(index, 1, entity);
|
||||
@ -177,7 +182,8 @@ export class ClusterManager extends Singleton {
|
||||
}
|
||||
}
|
||||
|
||||
@action syncClustersFromCatalog(entities: KubernetesCluster[]) {
|
||||
@action
|
||||
protected syncClustersFromCatalog(entities: KubernetesCluster[]) {
|
||||
for (const entity of entities) {
|
||||
const cluster = this.store.getById(entity.metadata.uid);
|
||||
|
||||
|
||||
@ -22,161 +22,229 @@
|
||||
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",
|
||||
"--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) {
|
||||
|
||||
@ -50,6 +50,24 @@ export function isLongRunningRequest(reqUrl: string) {
|
||||
return getBoolean(url.searchParams, watchParam) || getBoolean(url.searchParams, followParam);
|
||||
}
|
||||
|
||||
/**
|
||||
* This is the list of ports that chrome considers unsafe to allow HTTP
|
||||
* conntections to. Because they are the standard ports for processes that are
|
||||
* too forgiving in the connection types they accept.
|
||||
*
|
||||
* If we get one of these ports, the easiest thing to do is to just try again.
|
||||
*
|
||||
* Source: https://chromium.googlesource.com/chromium/src.git/+/refs/heads/main/net/base/port_util.cc
|
||||
*/
|
||||
const disallowedPorts = new Set([
|
||||
1, 7, 9, 11, 13, 15, 17, 19, 20, 21, 22, 23, 25, 37, 42, 43, 53, 69, 77, 79,
|
||||
87, 95, 101, 102, 103, 104, 109, 110, 111, 113, 115, 117, 119, 123, 135, 137,
|
||||
139, 143, 161, 179, 389, 427, 465, 512, 513, 514, 515, 526, 530, 531, 532,
|
||||
540, 548, 554, 556, 563, 587, 601, 636, 989, 990, 993, 995, 1719, 1720, 1723,
|
||||
2049, 3659, 4045, 5060, 5061, 6000, 6566, 6665, 6666, 6667, 6668, 6669, 6697,
|
||||
10080,
|
||||
]);
|
||||
|
||||
export class LensProxy extends Singleton {
|
||||
protected origin: string;
|
||||
protected proxyServer: http.Server;
|
||||
@ -91,12 +109,13 @@ export class LensProxy extends Singleton {
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts the lens proxy.
|
||||
* @resolves After the server is listening
|
||||
* @rejects if there is an error before that happens
|
||||
* Starts to listen on an OS provided port. Will reject if the server throws
|
||||
* an error.
|
||||
*
|
||||
* Resolves with the port number that was picked
|
||||
*/
|
||||
listen(): Promise<void> {
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
private attemptToListen(): Promise<number> {
|
||||
return new Promise<number>((resolve, reject) => {
|
||||
this.proxyServer.listen(0, "127.0.0.1");
|
||||
|
||||
this.proxyServer
|
||||
@ -113,7 +132,7 @@ export class LensProxy extends Singleton {
|
||||
|
||||
this.port = port;
|
||||
appEventBus.emit({ name: "lens-proxy", action: "listen", params: { port }});
|
||||
resolve();
|
||||
resolve(port);
|
||||
})
|
||||
.once("error", (error) => {
|
||||
logger.info(`[LENS-PROXY]: Proxy server failed to start: ${error}`);
|
||||
@ -122,8 +141,40 @@ export class LensProxy extends Singleton {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts the lens proxy.
|
||||
* @resolves After the server is listening on a good port
|
||||
* @rejects if there is an error before that happens
|
||||
*/
|
||||
async listen(): Promise<void> {
|
||||
const seenPorts = new Set<number>();
|
||||
|
||||
while(true) {
|
||||
this.proxyServer?.close();
|
||||
const port = await this.attemptToListen();
|
||||
|
||||
if (!disallowedPorts.has(port)) {
|
||||
// We didn't get a port that would result in an ERR_UNSAFE_PORT error, use it
|
||||
return;
|
||||
}
|
||||
|
||||
logger.warn(`[LENS-PROXY]: Proxy server has with port known to be considered unsafe to connect to by chrome, restarting...`);
|
||||
|
||||
if (seenPorts.has(port)) {
|
||||
/**
|
||||
* Assume that if we have seen the port before, then the OS has looped
|
||||
* through all the ports possible and we will not be able to get a safe
|
||||
* port.
|
||||
*/
|
||||
throw new Error("Failed to start LensProxy due to seeing too many unsafe ports. Please restart Lens.");
|
||||
} else {
|
||||
seenPorts.add(port);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
close() {
|
||||
logger.info("Closing proxy server");
|
||||
logger.info("[LENS-PROXY]: Closing server");
|
||||
this.proxyServer.close();
|
||||
this.closed = true;
|
||||
}
|
||||
|
||||
@ -38,7 +38,7 @@ export class PrometheusOperator extends PrometheusProvider {
|
||||
case "cluster":
|
||||
switch (queryName) {
|
||||
case "memoryUsage":
|
||||
return `sum(node_memory_MemTotal_bytes - (node_memory_MemFree_bytes + node_memory_Buffers_bytes + node_memory_Cached_bytes))`.replace(/_bytes/g, `_bytes{node=~"${opts.nodes}"}`);
|
||||
return `sum(node_memory_MemTotal_bytes - (node_memory_MemFree_bytes + node_memory_Buffers_bytes + node_memory_Cached_bytes))`.replace(/_bytes/g, `_bytes * on (pod,namespace) group_left(node) kube_pod_info{node=~"${opts.nodes}"}`);
|
||||
case "workloadMemoryUsage":
|
||||
return `sum(container_memory_working_set_bytes{container!="", instance=~"${opts.nodes}"}) by (component)`;
|
||||
case "memoryRequests":
|
||||
@ -50,7 +50,7 @@ export class PrometheusOperator extends PrometheusProvider {
|
||||
case "memoryAllocatableCapacity":
|
||||
return `sum(kube_node_status_allocatable{node=~"${opts.nodes}", resource="memory"})`;
|
||||
case "cpuUsage":
|
||||
return `sum(rate(node_cpu_seconds_total{node=~"${opts.nodes}", mode=~"user|system"}[${this.rateAccuracy}]))`;
|
||||
return `sum(rate(node_cpu_seconds_total{mode=~"user|system"}[${this.rateAccuracy}])* on (pod,namespace) group_left(node) kube_pod_info{node=~"${opts.nodes}"})`;
|
||||
case "cpuRequests":
|
||||
return `sum(kube_pod_container_resource_requests{node=~"${opts.nodes}", resource="cpu"})`;
|
||||
case "cpuLimits":
|
||||
@ -66,31 +66,31 @@ export class PrometheusOperator extends PrometheusProvider {
|
||||
case "podAllocatableCapacity":
|
||||
return `sum(kube_node_status_allocatable{node=~"${opts.nodes}", resource="pods"})`;
|
||||
case "fsSize":
|
||||
return `sum(node_filesystem_size_bytes{node=~"${opts.nodes}", mountpoint="/"}) by (node)`;
|
||||
return `sum(node_filesystem_size_bytes{mountpoint="/"} * on (pod,namespace) group_left(node) kube_pod_info{node=~"${opts.nodes}"})`;
|
||||
case "fsUsage":
|
||||
return `sum(node_filesystem_size_bytes{node=~"${opts.nodes}", mountpoint="/"} - node_filesystem_avail_bytes{node=~"${opts.nodes}", mountpoint="/"}) by (node)`;
|
||||
return `sum(node_filesystem_size_bytes{mountpoint="/"} * on (pod,namespace) group_left(node) kube_pod_info{node=~"${opts.nodes}"} - node_filesystem_avail_bytes{mountpoint="/"} * on (pod,namespace) group_left(node) kube_pod_info{node=~"${opts.nodes}"})`;
|
||||
}
|
||||
break;
|
||||
case "nodes":
|
||||
switch (queryName) {
|
||||
case "memoryUsage":
|
||||
return `sum (node_memory_MemTotal_bytes - (node_memory_MemFree_bytes + node_memory_Buffers_bytes + node_memory_Cached_bytes)) by (node)`;
|
||||
return `sum((node_memory_MemTotal_bytes - (node_memory_MemFree_bytes + node_memory_Buffers_bytes + node_memory_Cached_bytes)) * on (pod, namespace) group_left(node) kube_pod_info) by (node)`;
|
||||
case "workloadMemoryUsage":
|
||||
return `sum(container_memory_working_set_bytes{container!=""}) by (node)`;
|
||||
return `sum(container_memory_working_set_bytes{container!="POD", container!=""}) by (node)`;
|
||||
case "memoryCapacity":
|
||||
return `sum(kube_node_status_capacity{resource="memory"}) by (node)`;
|
||||
case "memoryAllocatableCapacity":
|
||||
return `sum(kube_node_status_allocatable{resource="memory"}) by (node)`;
|
||||
case "cpuUsage":
|
||||
return `sum(rate(node_cpu_seconds_total{mode=~"user|system"}[${this.rateAccuracy}])) by(node)`;
|
||||
return `sum(rate(node_cpu_seconds_total{mode=~"user|system"}[${this.rateAccuracy}]) * on (pod, namespace) group_left(node) kube_pod_info) by (node)`;
|
||||
case "cpuCapacity":
|
||||
return `sum(kube_node_status_allocatable{resource="cpu"}) by (node)`;
|
||||
case "cpuAllocatableCapacity":
|
||||
return `sum(kube_node_status_allocatable{resource="cpu"}) by (node)`;
|
||||
case "fsSize":
|
||||
return `sum(node_filesystem_size_bytes{mountpoint="/"}) by (node)`;
|
||||
return `sum(node_filesystem_size_bytes{mountpoint="/"} * on (pod,namespace) group_left(node) kube_pod_info) by (node)`;
|
||||
case "fsUsage":
|
||||
return `sum(node_filesystem_size_bytes{mountpoint="/"} - node_filesystem_avail_bytes{mountpoint="/"}) by (node)`;
|
||||
return `sum((node_filesystem_size_bytes{mountpoint="/"} - node_filesystem_avail_bytes{mountpoint="/"}) * on (pod, namespace) group_left(node) kube_pod_info) by (node)`;
|
||||
}
|
||||
break;
|
||||
case "pods":
|
||||
|
||||
@ -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");
|
||||
}
|
||||
|
||||
|
||||
@ -53,4 +53,19 @@ describe("NetworkPolicyDetails", () => {
|
||||
expect(await findByTestId(container, "egress-0")).toBeInstanceOf(HTMLElement);
|
||||
expect(await findByText(container, "foo: bar")).toBeInstanceOf(HTMLElement);
|
||||
});
|
||||
|
||||
it("should not crash if egress nodeSelector doesn't have matchLabels", async () => {
|
||||
const spec: NetworkPolicySpec = {
|
||||
egress: [{
|
||||
to: [{
|
||||
namespaceSelector: {},
|
||||
}],
|
||||
}],
|
||||
podSelector: {},
|
||||
};
|
||||
const policy = new NetworkPolicy({ metadata: {} as any, spec } as any);
|
||||
const { container } = render(<NetworkPolicyDetails object={policy} />);
|
||||
|
||||
expect(container).toBeInstanceOf(HTMLElement);
|
||||
});
|
||||
});
|
||||
|
||||
@ -29,4 +29,13 @@
|
||||
padding-bottom: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
ul.policySelectorList {
|
||||
list-style: disc;
|
||||
}
|
||||
|
||||
.policySelectorList ul {
|
||||
list-style: circle;
|
||||
list-style-position: inside;
|
||||
}
|
||||
}
|
||||
|
||||
@ -23,13 +23,15 @@ import styles from "./network-policy-details.module.css";
|
||||
|
||||
import React from "react";
|
||||
import { DrawerItem, DrawerTitle } from "../drawer";
|
||||
import { IPolicyIpBlock, IPolicySelector, NetworkPolicy, NetworkPolicyPeer, NetworkPolicyPort } from "../../../common/k8s-api/endpoints/network-policy.api";
|
||||
import { IPolicyIpBlock, NetworkPolicy, NetworkPolicyPeer, NetworkPolicyPort } from "../../../common/k8s-api/endpoints/network-policy.api";
|
||||
import { Badge } from "../badge";
|
||||
import { SubTitle } from "../layout/sub-title";
|
||||
import { observer } from "mobx-react";
|
||||
import type { KubeObjectDetailsProps } from "../kube-object-details";
|
||||
import { KubeObjectMeta } from "../kube-object-meta";
|
||||
import logger from "../../../common/logger";
|
||||
import type { LabelMatchExpression, LabelSelector } from "../../../common/k8s-api/kube-object";
|
||||
import { isEmpty } from "lodash";
|
||||
|
||||
interface Props extends KubeObjectDetailsProps<NetworkPolicy> {
|
||||
}
|
||||
@ -60,20 +62,57 @@ export class NetworkPolicyDetails extends React.Component<Props> {
|
||||
);
|
||||
}
|
||||
|
||||
renderIPolicySelector(name: string, selector: IPolicySelector | undefined) {
|
||||
renderMatchLabels(matchLabels: Record<string, string | undefined> | undefined) {
|
||||
if (!matchLabels) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return Object.entries(matchLabels)
|
||||
.map(([key, value]) => <li key={key}>{key}: {value}</li>);
|
||||
}
|
||||
|
||||
renderMatchExpressions(matchExpressions: LabelMatchExpression[] | undefined) {
|
||||
if (!matchExpressions) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return matchExpressions.map(expr => {
|
||||
switch (expr.operator) {
|
||||
case "DoesNotExist":
|
||||
case "Exists":
|
||||
return <li key={expr.key}>{expr.key} ({expr.operator})</li>;
|
||||
case "In":
|
||||
case "NotIn":
|
||||
return (
|
||||
<li key={expr.key}>
|
||||
{expr.key}({expr.operator})
|
||||
<ul>
|
||||
{expr.values.map((value, index) => <li key={index}>{value}</li>)}
|
||||
</ul>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
renderIPolicySelector(name: string, selector: LabelSelector | undefined) {
|
||||
if (!selector) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { matchLabels, matchExpressions } = selector;
|
||||
|
||||
return (
|
||||
<DrawerItem name={name}>
|
||||
{
|
||||
Object
|
||||
.entries(selector.matchLabels)
|
||||
.map(data => data.join(": "))
|
||||
.join(", ")
|
||||
|| "(empty)"
|
||||
}
|
||||
<ul className={styles.policySelectorList}>
|
||||
{this.renderMatchLabels(matchLabels)}
|
||||
{this.renderMatchExpressions(matchExpressions)}
|
||||
{
|
||||
(isEmpty(matchLabels) && isEmpty(matchExpressions)) && (
|
||||
<li>(empty)</li>
|
||||
)
|
||||
}
|
||||
</ul>
|
||||
</DrawerItem>
|
||||
);
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -1,88 +0,0 @@
|
||||
/**
|
||||
* 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 "./clipboard.scss";
|
||||
import React from "react";
|
||||
import { findDOMNode } from "react-dom";
|
||||
import { boundMethod } from "../../../common/utils";
|
||||
import { Notifications } from "../notifications";
|
||||
import { copyToClipboard } from "../../utils/copyToClipboard";
|
||||
import logger from "../../../main/logger";
|
||||
import { cssNames } from "../../utils";
|
||||
|
||||
export interface CopyToClipboardProps {
|
||||
resetSelection?: boolean;
|
||||
showNotification?: boolean;
|
||||
cssSelectorLimit?: string; // allows to copy partial content with css-selector in children-element context
|
||||
getNotificationMessage?(copiedText: string): React.ReactNode;
|
||||
}
|
||||
|
||||
export const defaultProps: Partial<CopyToClipboardProps> = {
|
||||
getNotificationMessage(copiedText: string) {
|
||||
return <p>Copied to clipboard: <em>{copiedText}</em></p>;
|
||||
},
|
||||
};
|
||||
|
||||
export class Clipboard extends React.Component<CopyToClipboardProps> {
|
||||
static displayName = "Clipboard";
|
||||
static defaultProps = defaultProps as object;
|
||||
|
||||
get rootElem(): HTMLElement {
|
||||
// eslint-disable-next-line react/no-find-dom-node
|
||||
return findDOMNode(this) as HTMLElement;
|
||||
}
|
||||
|
||||
get rootReactElem(): React.ReactElement<React.HTMLProps<any>> {
|
||||
return React.Children.only(this.props.children) as React.ReactElement;
|
||||
}
|
||||
|
||||
@boundMethod
|
||||
onClick(evt: React.MouseEvent) {
|
||||
if (this.rootReactElem.props.onClick) {
|
||||
this.rootReactElem.props.onClick(evt); // pass event to children-root-element if any
|
||||
}
|
||||
const { showNotification, resetSelection, getNotificationMessage, cssSelectorLimit } = this.props;
|
||||
const contentElem = this.rootElem.querySelector<any>(cssSelectorLimit) || this.rootElem;
|
||||
|
||||
if (contentElem) {
|
||||
const { copiedText, copied } = copyToClipboard(contentElem, { resetSelection });
|
||||
|
||||
if (copied && showNotification) {
|
||||
Notifications.ok(getNotificationMessage(copiedText));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
try {
|
||||
const rootElem = this.rootReactElem;
|
||||
|
||||
return React.cloneElement(rootElem, {
|
||||
className: cssNames(Clipboard.displayName, rootElem.props.className),
|
||||
onClick: this.onClick,
|
||||
});
|
||||
} catch (err) {
|
||||
logger.error(`Invalid usage components/CopyToClick usage. Children must contain root html element.`, { err: String(err) });
|
||||
|
||||
return this.rootReactElem;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -65,7 +65,11 @@ export class ClusterIconSetting extends React.Component<Props> {
|
||||
}
|
||||
|
||||
clearIcon() {
|
||||
this.props.cluster.preferences.icon = undefined;
|
||||
/**
|
||||
* NOTE: this needs to be `null` rather than `undefined` so that we can
|
||||
* tell the difference between it not being there and being cleared.
|
||||
*/
|
||||
this.props.cluster.preferences.icon = null;
|
||||
}
|
||||
|
||||
@boundMethod
|
||||
|
||||
@ -24,10 +24,10 @@ import "./logs-dialog.scss";
|
||||
import React from "react";
|
||||
import { Dialog, DialogProps } from "../dialog";
|
||||
import { Wizard, WizardStep } from "../wizard";
|
||||
import { copyToClipboard } from "../../utils";
|
||||
import { Notifications } from "../notifications";
|
||||
import { Button } from "../button";
|
||||
import { Icon } from "../icon";
|
||||
import { clipboard } from "electron";
|
||||
|
||||
// todo: make as external BrowserWindow (?)
|
||||
|
||||
@ -40,9 +40,8 @@ export class LogsDialog extends React.Component<Props> {
|
||||
public logsElem: HTMLElement;
|
||||
|
||||
copyToClipboard = () => {
|
||||
if (copyToClipboard(this.logsElem)) {
|
||||
Notifications.ok(`Logs copied to clipboard.`);
|
||||
}
|
||||
clipboard.writeText(this.props.logs);
|
||||
Notifications.ok(`Logs copied to clipboard.`);
|
||||
};
|
||||
|
||||
render() {
|
||||
|
||||
@ -25,7 +25,7 @@ import { makeObservable, observable } from "mobx";
|
||||
import { observer } from "mobx-react";
|
||||
import yaml from "js-yaml";
|
||||
import type { ServiceAccount } from "../../../common/k8s-api/endpoints";
|
||||
import { copyToClipboard, saveFileDialog } from "../../utils";
|
||||
import { saveFileDialog } from "../../utils";
|
||||
import { Button } from "../button";
|
||||
import { Dialog, DialogProps } from "../dialog";
|
||||
import { Icon } from "../icon";
|
||||
@ -33,6 +33,7 @@ import { Notifications } from "../notifications";
|
||||
import { Wizard, WizardStep } from "../wizard";
|
||||
import { apiBase } from "../../api";
|
||||
import { MonacoEditor } from "../monaco-editor";
|
||||
import { clipboard } from "electron";
|
||||
|
||||
interface IKubeconfigDialogData {
|
||||
title?: React.ReactNode;
|
||||
@ -49,7 +50,6 @@ const dialogState = observable.object({
|
||||
|
||||
@observer
|
||||
export class KubeConfigDialog extends React.Component<Props> {
|
||||
@observable.ref configTextArea: HTMLTextAreaElement; // required for coping config text
|
||||
@observable config = ""; // parsed kubeconfig in yaml format
|
||||
|
||||
constructor(props: Props) {
|
||||
@ -89,9 +89,8 @@ export class KubeConfigDialog extends React.Component<Props> {
|
||||
}
|
||||
|
||||
copyToClipboard = () => {
|
||||
if (this.config && copyToClipboard(this.configTextArea)) {
|
||||
Notifications.ok("Config copied to clipboard");
|
||||
}
|
||||
clipboard.writeText(this.config);
|
||||
Notifications.ok("Config copied to clipboard");
|
||||
};
|
||||
|
||||
download = () => {
|
||||
@ -131,11 +130,6 @@ export class KubeConfigDialog extends React.Component<Props> {
|
||||
className={styles.editor}
|
||||
value={yamlConfig}
|
||||
/>
|
||||
<textarea
|
||||
className={styles.configCopy}
|
||||
readOnly defaultValue={yamlConfig}
|
||||
ref={e => this.configTextArea = e}
|
||||
/>
|
||||
</WizardStep>
|
||||
</Wizard>
|
||||
</Dialog>
|
||||
|
||||
@ -19,4 +19,36 @@
|
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
export * from "./clipboard";
|
||||
.SettingsCloseButton {
|
||||
.closeIcon {
|
||||
width: 35px;
|
||||
height: 35px;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
cursor: pointer;
|
||||
border: 2px solid var(--textColorDimmed);
|
||||
border-radius: 50%;
|
||||
|
||||
&:hover {
|
||||
background-color: #72767d25;
|
||||
}
|
||||
|
||||
&:active {
|
||||
transform: translateY(1px);
|
||||
}
|
||||
|
||||
.Icon {
|
||||
color: var(--textColorAccent);
|
||||
opacity: 0.6;
|
||||
}
|
||||
}
|
||||
|
||||
.escLabel {
|
||||
text-align: center;
|
||||
margin-top: var(--margin);
|
||||
font-weight: bold;
|
||||
user-select: none;
|
||||
color: var(--textColorDimmed);
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
@ -19,6 +19,23 @@
|
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
.Clipboard {
|
||||
cursor: pointer;
|
||||
}
|
||||
import "./close-button.scss";
|
||||
|
||||
import React, { HTMLAttributes } from "react";
|
||||
import { Icon } from "../icon";
|
||||
|
||||
interface Props extends HTMLAttributes<HTMLDivElement> {
|
||||
}
|
||||
|
||||
export function CloseButton(props: Props) {
|
||||
return (
|
||||
<div className="SettingsCloseButton" {...props}>
|
||||
<div className="closeIcon" role="button" aria-label="Close">
|
||||
<Icon material="close" />
|
||||
</div>
|
||||
<div className="escLabel" aria-hidden="true">
|
||||
ESC
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -142,41 +142,7 @@
|
||||
}
|
||||
|
||||
> .toolsRegion {
|
||||
.fixedTools {
|
||||
position: fixed;
|
||||
top: 60px;
|
||||
|
||||
.closeBtn {
|
||||
width: 35px;
|
||||
height: 35px;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
border: 2px solid var(--textColorDimmed);
|
||||
border-radius: 50%;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
background-color: #72767d4d;
|
||||
}
|
||||
|
||||
&:active {
|
||||
transform: translateY(1px);
|
||||
}
|
||||
|
||||
.Icon {
|
||||
color: var(--textColorTertiary);
|
||||
}
|
||||
}
|
||||
|
||||
.esc {
|
||||
text-align: center;
|
||||
margin-top: 4px;
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
color: var(--textColorDimmed);
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
width: 45px;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -25,7 +25,7 @@ import React from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { cssNames, IClassName } from "../../utils";
|
||||
import { navigation } from "../../navigation";
|
||||
import { Icon } from "../icon";
|
||||
import { CloseButton } from "./close-button";
|
||||
|
||||
export interface SettingLayoutProps extends React.DOMAttributes<any> {
|
||||
className?: IClassName;
|
||||
@ -97,13 +97,8 @@ export class SettingLayout extends React.Component<SettingLayoutProps> {
|
||||
<div className="toolsRegion">
|
||||
{
|
||||
this.props.provideBackButtonNavigation && (
|
||||
<div className="fixedTools">
|
||||
<div className="closeBtn" role="button" aria-label="Close" onClick={back}>
|
||||
<Icon material="close" />
|
||||
</div>
|
||||
<div className="esc" aria-hidden="true">
|
||||
ESC
|
||||
</div>
|
||||
<div className="fixed top-[60px]">
|
||||
<CloseButton onClick={back}/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@ -228,10 +228,15 @@ html {
|
||||
}
|
||||
|
||||
.Select {
|
||||
&__value-container {
|
||||
margin-top: 2px;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
&__control {
|
||||
box-shadow: 0 0 0 1px var(--inputControlBorder);
|
||||
background: var(--inputControlBackground);
|
||||
border-radius: 5px;
|
||||
border-radius: var(--border-radius);
|
||||
}
|
||||
|
||||
&__single-value {
|
||||
|
||||
@ -1,52 +0,0 @@
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
|
||||
// Helper for selecting element's text content and copy in clipboard
|
||||
|
||||
export function copyToClipboard(elem: HTMLElement, { resetSelection = true } = {}) {
|
||||
let clearSelection: () => void;
|
||||
|
||||
if (isSelectable(elem)) {
|
||||
elem.select();
|
||||
clearSelection = () => elem.setSelectionRange(0, 0);
|
||||
} else {
|
||||
const selection = window.getSelection();
|
||||
|
||||
selection.selectAllChildren(elem);
|
||||
clearSelection = () => selection.removeAllRanges();
|
||||
}
|
||||
const selectedText = document.getSelection().toString();
|
||||
const isCopied = document.execCommand("copy");
|
||||
|
||||
if (resetSelection) {
|
||||
clearSelection();
|
||||
}
|
||||
|
||||
return {
|
||||
copied: isCopied,
|
||||
copiedText: selectedText,
|
||||
clearSelection,
|
||||
};
|
||||
}
|
||||
|
||||
function isSelectable(elem: HTMLElement): elem is HTMLInputElement {
|
||||
return !!(elem as HTMLInputElement).select;
|
||||
}
|
||||
@ -24,7 +24,6 @@
|
||||
export * from "../../common/utils";
|
||||
export * from "../../common/event-emitter";
|
||||
|
||||
export * from "./copyToClipboard";
|
||||
export * from "./createStorage";
|
||||
export * from "./cssNames";
|
||||
export * from "./cssVar";
|
||||
|
||||
@ -27,7 +27,14 @@ module.exports = {
|
||||
fontFamily: {
|
||||
sans: ["Roboto", "Helvetica", "Arial", "sans-serif"],
|
||||
},
|
||||
extend: {},
|
||||
extend: {
|
||||
colors: {
|
||||
textAccent: "var(--textColorAccent)",
|
||||
textPrimary: "var(--textColorPrimary)",
|
||||
textTertiary: "var(--textColorTertiary)",
|
||||
textDimmed: "var(--textColorDimmed)",
|
||||
},
|
||||
},
|
||||
},
|
||||
variants: {
|
||||
extend: {},
|
||||
|
||||
Loading…
Reference in New Issue
Block a user