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

Merge branch 'master' into catalog-entity-pin-icons

This commit is contained in:
Alex Andreev 2021-10-15 08:48:40 +03:00
commit 62b45875c4
41 changed files with 915 additions and 830 deletions

View File

@ -113,19 +113,17 @@ docs:
.PHONY: clean-extensions .PHONY: clean-extensions
clean-extensions: clean-extensions:
$(foreach dir, $(wildcard $(EXTENSIONS_DIR)/*), rm -rf $(dir)/dist) rm -rf $(EXTENSIONS_DIR)/*/{dist,node_modules,*.tgz}
$(foreach dir, $(wildcard $(EXTENSIONS_DIR)/*), rm -rf $(dir)/node_modules)
$(foreach dir, $(wildcard $(EXTENSIONS_DIR)/*), rm $(dir)/*.tgz || true)
.PHONY: clean-npm .PHONY: clean-npm
clean-npm: clean-npm:
rm -rf src/extensions/npm/extensions/dist rm -rf src/extensions/npm/extensions/{dist,__mocks__,node_modules}
rm -rf src/extensions/npm/extensions/__mocks__
rm -rf src/extensions/npm/extensions/node_modules
.PHONY: clean .PHONY: clean
clean: clean-npm clean-extensions clean: clean-npm clean-extensions
rm -rf binaries/client rm -rf binaries/client
rm -rf dist/* rm -rf dist
rm -rf static/build/* rm -rf static/build
rm -rf node_modules/ rm -rf node_modules
rm -rf site
rm -rf docs/extensions/api

View File

@ -2,6 +2,6 @@
## APIs ## APIs
- [Common](modules/common.md) - [Common](modules/Common.md)
- [Main](modules/main.md) - [Main](modules/Main.md)
- [Renderer](modules/renderer.md) - [Renderer](modules/Renderer.md)

View File

@ -74,7 +74,7 @@ describe("preferences page tests", () => {
utils.itIf(process.platform !== "win32")("ensures helm repos", async () => { utils.itIf(process.platform !== "win32")("ensures helm repos", async () => {
await window.click("[data-testid=kubernetes-tab]"); await window.click("[data-testid=kubernetes-tab]");
await window.waitForSelector("[data-testid=repository-name]", { await window.waitForSelector("[data-testid=repository-name]", {
timeout: 100_000, timeout: 140_000,
}); });
await window.click("#HelmRepoSelect"); await window.click("#HelmRepoSelect");
await window.waitForSelector("div.Select__option"); await window.waitForSelector("div.Select__option");

View File

@ -93,7 +93,7 @@ export async function start() {
} }
export async function clickWelcomeButton(window: Page) { export async function clickWelcomeButton(window: Page) {
await window.click("#hotbarIcon-catalog-entity .Icon"); await window.click("[data-testid=welcome-menu-container] li a");
} }
function minikubeEntityId() { function minikubeEntityId() {

View File

@ -3,7 +3,7 @@
"productName": "OpenLens", "productName": "OpenLens",
"description": "OpenLens - Open Source IDE for Kubernetes", "description": "OpenLens - Open Source IDE for Kubernetes",
"homepage": "https://github.com/lensapp/lens", "homepage": "https://github.com/lensapp/lens",
"version": "5.3.0-alpha.0", "version": "5.3.0-alpha.2",
"main": "static/build/main.js", "main": "static/build/main.js",
"copyright": "© 2021 OpenLens Authors", "copyright": "© 2021 OpenLens Authors",
"license": "MIT", "license": "MIT",
@ -179,7 +179,7 @@
} }
}, },
"dependencies": { "dependencies": {
"@electron/remote": "^1.2.1", "@electron/remote": "^1.2.2",
"@hapi/call": "^8.0.1", "@hapi/call": "^8.0.1",
"@hapi/subtext": "^7.0.3", "@hapi/subtext": "^7.0.3",
"@kubernetes/client-node": "^0.15.1", "@kubernetes/client-node": "^0.15.1",
@ -193,8 +193,7 @@
"byline": "^5.0.0", "byline": "^5.0.0",
"chalk": "^4.1.0", "chalk": "^4.1.0",
"chokidar": "^3.4.3", "chokidar": "^3.4.3",
"command-exists": "1.2.9", "conf": "^7.1.2",
"conf": "^7.0.1",
"crypto-js": "^4.1.1", "crypto-js": "^4.1.1",
"electron-devtools-installer": "^3.2.0", "electron-devtools-installer": "^3.2.0",
"electron-updater": "^4.6.0", "electron-updater": "^4.6.0",
@ -251,7 +250,7 @@
"winston": "^3.3.3", "winston": "^3.3.3",
"winston-console-format": "^1.0.8", "winston-console-format": "^1.0.8",
"winston-transport-browserconsole": "^1.0.5", "winston-transport-browserconsole": "^1.0.5",
"ws": "^7.4.6" "ws": "^7.5.5"
}, },
"devDependencies": { "devDependencies": {
"@emeraldpay/hashicon-react": "^0.4.0", "@emeraldpay/hashicon-react": "^0.4.0",
@ -259,13 +258,13 @@
"@material-ui/icons": "^4.11.2", "@material-ui/icons": "^4.11.2",
"@material-ui/lab": "^4.0.0-alpha.60", "@material-ui/lab": "^4.0.0-alpha.60",
"@pmmmwh/react-refresh-webpack-plugin": "^0.4.3", "@pmmmwh/react-refresh-webpack-plugin": "^0.4.3",
"@sentry/react": "^6.8.0", "@sentry/react": "^6.13.3",
"@sentry/types": "^6.8.0", "@sentry/types": "^6.8.0",
"@testing-library/dom": "^8.2.0", "@testing-library/dom": "^8.9.0",
"@testing-library/jest-dom": "^5.14.1", "@testing-library/jest-dom": "^5.14.1",
"@testing-library/react": "^11.2.6", "@testing-library/react": "^11.2.6",
"@testing-library/user-event": "^13.2.1", "@testing-library/user-event": "^13.2.1",
"@types/byline": "^4.2.32", "@types/byline": "^4.2.33",
"@types/chart.js": "^2.9.34", "@types/chart.js": "^2.9.34",
"@types/color": "^3.0.2", "@types/color": "^3.0.2",
"@types/crypto-js": "^3.1.47", "@types/crypto-js": "^3.1.47",
@ -279,7 +278,7 @@
"@types/http-proxy": "^1.17.7", "@types/http-proxy": "^1.17.7",
"@types/jest": "^26.0.24", "@types/jest": "^26.0.24",
"@types/js-yaml": "^3.12.4", "@types/js-yaml": "^3.12.4",
"@types/jsdom": "^16.2.4", "@types/jsdom": "^16.2.13",
"@types/jsonpath": "^0.2.0", "@types/jsonpath": "^0.2.0",
"@types/lodash": "^4.14.155", "@types/lodash": "^4.14.155",
"@types/marked": "^2.0.4", "@types/marked": "^2.0.4",
@ -287,13 +286,13 @@
"@types/mini-css-extract-plugin": "^0.9.1", "@types/mini-css-extract-plugin": "^0.9.1",
"@types/mock-fs": "^4.13.1", "@types/mock-fs": "^4.13.1",
"@types/module-alias": "^2.0.1", "@types/module-alias": "^2.0.1",
"@types/node": "14.17.14", "@types/node": "14.17.26",
"@types/node-fetch": "^2.5.12", "@types/node-fetch": "^2.5.12",
"@types/npm": "^2.0.32", "@types/npm": "^2.0.32",
"@types/progress-bar-webpack-plugin": "^2.1.2", "@types/progress-bar-webpack-plugin": "^2.1.2",
"@types/proper-lockfile": "^4.1.2", "@types/proper-lockfile": "^4.1.2",
"@types/randomcolor": "^0.5.6", "@types/randomcolor": "^0.5.6",
"@types/react": "^17.0.0", "@types/react": "^17.0.29",
"@types/react-beautiful-dnd": "^13.1.1", "@types/react-beautiful-dnd": "^13.1.1",
"@types/react-dom": "^17.0.9", "@types/react-dom": "^17.0.9",
"@types/react-router-dom": "^5.3.1", "@types/react-router-dom": "^5.3.1",
@ -301,7 +300,7 @@
"@types/react-table": "^7.7.6", "@types/react-table": "^7.7.6",
"@types/react-virtualized-auto-sizer": "^1.0.1", "@types/react-virtualized-auto-sizer": "^1.0.1",
"@types/react-window": "^1.8.5", "@types/react-window": "^1.8.5",
"@types/readable-stream": "^2.3.9", "@types/readable-stream": "^2.3.11",
"@types/request": "^2.48.7", "@types/request": "^2.48.7",
"@types/request-promise-native": "^1.0.18", "@types/request-promise-native": "^1.0.18",
"@types/semver": "^7.2.0", "@types/semver": "^7.2.0",
@ -327,7 +326,7 @@
"concurrently": "^5.2.0", "concurrently": "^5.2.0",
"css-loader": "^5.2.7", "css-loader": "^5.2.7",
"deepdash": "^5.3.9", "deepdash": "^5.3.9",
"dompurify": "^2.3.1", "dompurify": "^2.3.3",
"electron": "^13.5.1", "electron": "^13.5.1",
"electron-builder": "^22.11.11", "electron-builder": "^22.11.11",
"electron-notarize": "^0.3.0", "electron-notarize": "^0.3.0",
@ -335,7 +334,7 @@
"esbuild-loader": "^2.15.1", "esbuild-loader": "^2.15.1",
"eslint": "^7.32.0", "eslint": "^7.32.0",
"eslint-plugin-header": "^3.1.1", "eslint-plugin-header": "^3.1.1",
"eslint-plugin-react": "^7.24.0", "eslint-plugin-react": "^7.26.1",
"eslint-plugin-react-hooks": "^4.2.0", "eslint-plugin-react-hooks": "^4.2.0",
"eslint-plugin-unused-imports": "^1.0.1", "eslint-plugin-unused-imports": "^1.0.1",
"file-loader": "^6.2.0", "file-loader": "^6.2.0",
@ -353,7 +352,7 @@
"mini-css-extract-plugin": "^1.6.2", "mini-css-extract-plugin": "^1.6.2",
"node-gyp": "7.1.2", "node-gyp": "7.1.2",
"node-loader": "^1.0.3", "node-loader": "^1.0.3",
"nodemon": "^2.0.12", "nodemon": "^2.0.13",
"playwright": "^1.14.0", "playwright": "^1.14.0",
"postcss": "^8.3.6", "postcss": "^8.3.6",
"postcss-loader": "4.3.0", "postcss-loader": "4.3.0",
@ -373,14 +372,14 @@
"sass-loader": "^8.0.2", "sass-loader": "^8.0.2",
"sharp": "^0.29.1", "sharp": "^0.29.1",
"style-loader": "^2.0.0", "style-loader": "^2.0.0",
"tailwindcss": "^2.2.4", "tailwindcss": "^2.2.17",
"ts-jest": "26.5.6", "ts-jest": "26.5.6",
"ts-loader": "^7.0.5", "ts-loader": "^7.0.5",
"ts-node": "^10.2.1", "ts-node": "^10.2.1",
"type-fest": "^1.0.2", "type-fest": "^1.0.2",
"typed-emitter": "^1.3.1", "typed-emitter": "^1.3.1",
"typedoc": "0.22.5", "typedoc": "0.22.5",
"typedoc-plugin-markdown": "^3.9.0", "typedoc-plugin-markdown": "^3.11.3",
"typeface-roboto": "^1.1.13", "typeface-roboto": "^1.1.13",
"typescript": "^4.4.3", "typescript": "^4.4.3",
"typescript-plugin-css-modules": "^3.4.0", "typescript-plugin-css-modules": "^3.4.0",

View File

@ -20,7 +20,7 @@
*/ */
import { KubeConfig } from "@kubernetes/client-node"; import { KubeConfig } from "@kubernetes/client-node";
import { validateKubeConfig, loadConfigFromString, getNodeWarningConditions } from "../kube-helpers"; import { validateKubeConfig, loadConfigFromString } from "../kube-helpers";
const kubeconfig = ` const kubeconfig = `
apiVersion: v1 apiVersion: v1
@ -120,38 +120,6 @@ describe("kube helpers", () => {
); );
}); });
}); });
describe("with invalid exec command", () => {
it("returns an error", () => {
expect(String(validateKubeConfig(kc, "invalidExec"))).toEqual(
expect.stringContaining("User Exec command \"foo\" not found on host. Please ensure binary is found in PATH or use absolute path to binary in Kubeconfig")
);
});
});
});
describe("with validateCluster as false", () => {
describe("with invalid cluster object", () => {
it("does not return an error", () => {
expect(validateKubeConfig(kc, "invalidCluster", { validateCluster: false })).toBeUndefined();
});
});
});
describe("with validateUser as false", () => {
describe("with invalid user object", () => {
it("does not return an error", () => {
expect(validateKubeConfig(kc, "invalidUser", { validateUser: false })).toBeUndefined();
});
});
});
describe("with validateExec as false", () => {
describe("with invalid exec object", () => {
it("does not return an error", () => {
expect(validateKubeConfig(kc, "invalidExec", { validateExec: false })).toBeUndefined();
});
});
}); });
}); });
@ -280,43 +248,4 @@ describe("kube helpers", () => {
}); });
}); });
}); });
describe("getNodeWarningConditions", () => {
it("should return an empty array if no status or no conditions", () => {
expect(getNodeWarningConditions({}).length).toBe(0);
});
it("should return an empty array if all conditions are good", () => {
expect(getNodeWarningConditions({
status: {
conditions: [
{
type: "Ready",
status: "foobar"
}
]
}
}).length).toBe(0);
});
it("should all not ready conditions", () => {
const conds = getNodeWarningConditions({
status: {
conditions: [
{
type: "Ready",
status: "foobar"
},
{
type: "NotReady",
status: "true"
},
]
}
});
expect(conds.length).toBe(1);
expect(conds[0].type).toBe("NotReady");
});
});
}); });

View File

@ -54,15 +54,24 @@ export interface KubernetesClusterSpec extends CatalogEntitySpec {
accessibleNamespaces?: string[]; accessibleNamespaces?: string[];
} }
export enum LensKubernetesClusterStatus {
DELETING = "deleting",
CONNECTING = "connecting",
CONNECTED = "connected",
DISCONNECTED = "disconnected"
}
export interface KubernetesClusterMetadata extends CatalogEntityMetadata { export interface KubernetesClusterMetadata extends CatalogEntityMetadata {
distro?: string; distro?: string;
kubeVersion?: string; kubeVersion?: string;
} }
/**
* @deprecated This is no longer used as it is incorrect. Other sources can add more values
*/
export type KubernetesClusterStatusPhase = "connected" | "connecting" | "disconnected" | "deleting"; export type KubernetesClusterStatusPhase = "connected" | "connecting" | "disconnected" | "deleting";
export interface KubernetesClusterStatus extends CatalogEntityStatus { export interface KubernetesClusterStatus extends CatalogEntityStatus {
phase: KubernetesClusterStatusPhase;
} }
export class KubernetesCluster extends CatalogEntity<KubernetesClusterMetadata, KubernetesClusterStatus, KubernetesClusterSpec> { export class KubernetesCluster extends CatalogEntity<KubernetesClusterMetadata, KubernetesClusterStatus, KubernetesClusterSpec> {
@ -110,15 +119,15 @@ export class KubernetesCluster extends CatalogEntity<KubernetesClusterMetadata,
} }
switch (this.status.phase) { switch (this.status.phase) {
case "connected": case LensKubernetesClusterStatus.CONNECTED:
case "connecting": case LensKubernetesClusterStatus.CONNECTING:
context.menuItems.push({ context.menuItems.push({
title: "Disconnect", title: "Disconnect",
icon: "link_off", icon: "link_off",
onClick: () => requestMain(clusterDisconnectHandler, this.metadata.uid) onClick: () => requestMain(clusterDisconnectHandler, this.metadata.uid)
}); });
break; break;
case "disconnected": case LensKubernetesClusterStatus.DISCONNECTED:
context.menuItems.push({ context.menuItems.push({
title: "Connect", title: "Connect",
icon: "link", icon: "link",

View File

@ -1,34 +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.
*/
export class ExecValidationNotFoundError extends Error {
constructor(execPath: string, isAbsolute: boolean) {
super(`User Exec command "${execPath}" not found on host.`);
let message = `User Exec command "${execPath}" not found on host.`;
if (!isAbsolute) {
message += ` Please ensure binary is found in PATH or use absolute path to binary in Kubeconfig`;
}
this.message = message;
this.name = this.constructor.name;
Error.captureStackTrace(this, this.constructor);
}
}

View File

@ -20,38 +20,19 @@
*/ */
import { CustomResourceDefinition } from "../endpoints"; import { CustomResourceDefinition } from "../endpoints";
import type { KubeObjectMetadata } from "../kube-object";
describe("Crds", () => { describe("Crds", () => {
describe("getVersion", () => { describe("getVersion", () => {
it("should get the first version name from the list of versions", () => { it("should throw if none of the versions are served", () => {
const crd = new CustomResourceDefinition({ const crd = new CustomResourceDefinition({
apiVersion: "foo", apiVersion: "apiextensions.k8s.io/v1",
kind: "CustomResourceDefinition", kind: "CustomResourceDefinition",
metadata: {} as KubeObjectMetadata, metadata: {
}); name: "foo",
resourceVersion: "12345",
crd.spec = { uid: "12345",
versions: [ },
{ spec: {
name: "123",
served: false,
storage: false,
}
]
} as any;
expect(crd.getVersion()).toBe("123");
});
it("should get the first version name from the list of versions (length 2)", () => {
const crd = new CustomResourceDefinition({
apiVersion: "foo",
kind: "CustomResourceDefinition",
metadata: {} as KubeObjectMetadata,
});
crd.spec = {
versions: [ versions: [
{ {
name: "123", name: "123",
@ -62,50 +43,85 @@ describe("Crds", () => {
name: "1234", name: "1234",
served: false, served: false,
storage: false, storage: false,
} },
] ],
} as any; },
});
expect(() => crd.getVersion()).toThrowError("Failed to find a version for CustomResourceDefinition foo");
});
it("should should get the version that is both served and stored", () => {
const crd = new CustomResourceDefinition({
apiVersion: "apiextensions.k8s.io/v1",
kind: "CustomResourceDefinition",
metadata: {
name: "foo",
resourceVersion: "12345",
uid: "12345",
},
spec: {
versions: [
{
name: "123",
served: true,
storage: true,
},
{
name: "1234",
served: false,
storage: false,
},
],
},
});
expect(crd.getVersion()).toBe("123"); expect(crd.getVersion()).toBe("123");
}); });
it("should get the first version name from the list of versions (length 2) even with version field", () => { it("should should get the version that is both served and stored even with version field", () => {
const crd = new CustomResourceDefinition({ const crd = new CustomResourceDefinition({
apiVersion: "foo", apiVersion: "apiextensions.k8s.io/v1",
kind: "CustomResourceDefinition", kind: "CustomResourceDefinition",
metadata: {} as KubeObjectMetadata, metadata: {
}); name: "foo",
resourceVersion: "12345",
crd.spec = { uid: "12345",
},
spec: {
version: "abc", version: "abc",
versions: [ versions: [
{ {
name: "123", name: "123",
served: false, served: true,
storage: false, storage: true,
}, },
{ {
name: "1234", name: "1234",
served: false, served: false,
storage: false, storage: false,
} },
] ],
} as any; },
});
expect(crd.getVersion()).toBe("123"); expect(crd.getVersion()).toBe("123");
}); });
it("should get the first version name from the version field", () => { it("should get the version name from the version field", () => {
const crd = new CustomResourceDefinition({ const crd = new CustomResourceDefinition({
apiVersion: "foo", apiVersion: "apiextensions.k8s.io/v1beta1",
kind: "CustomResourceDefinition", kind: "CustomResourceDefinition",
metadata: {} as KubeObjectMetadata, metadata: {
name: "foo",
resourceVersion: "12345",
uid: "12345",
},
spec: {
version: "abc",
}
}); });
crd.spec = {
version: "abc"
} as any;
expect(crd.getVersion()).toBe("abc"); expect(crd.getVersion()).toBe("abc");
}); });
}); });

View File

@ -19,10 +19,11 @@
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/ */
import { KubeObject } from "../kube-object"; import { KubeCreationError, KubeObject } from "../kube-object";
import { KubeApi } from "../kube-api"; import { KubeApi } from "../kube-api";
import { crdResourcesURL } from "../../routes"; import { crdResourcesURL } from "../../routes";
import { isClusterPageContext } from "../../utils/cluster-id-url-parsing"; import { isClusterPageContext } from "../../utils/cluster-id-url-parsing";
import type { KubeJsonApiData } from "../kube-json-api";
type AdditionalPrinterColumnsCommon = { type AdditionalPrinterColumnsCommon = {
name: string; name: string;
@ -39,10 +40,21 @@ type AdditionalPrinterColumnsV1Beta = AdditionalPrinterColumnsCommon & {
JSONPath: string; JSONPath: string;
}; };
export interface CRDVersion {
name: string;
served: boolean;
storage: boolean;
schema?: object; // required in v1 but not present in v1beta
additionalPrinterColumns?: AdditionalPrinterColumnsV1[];
}
export interface CustomResourceDefinition { export interface CustomResourceDefinition {
spec: { spec: {
group: string; group: string;
version?: string; // deprecated in v1 api /**
* @deprecated for apiextensions.k8s.io/v1 but used previously
*/
version?: string;
names: { names: {
plural: string; plural: string;
singular: string; singular: string;
@ -50,19 +62,19 @@ export interface CustomResourceDefinition {
listKind: string; listKind: string;
}; };
scope: "Namespaced" | "Cluster" | string; scope: "Namespaced" | "Cluster" | string;
validation?: any; /**
versions?: { * @deprecated for apiextensions.k8s.io/v1 but used previously
name: string; */
served: boolean; validation?: object;
storage: boolean; versions?: CRDVersion[];
schema?: unknown; // required in v1 but not present in v1beta
additionalPrinterColumns?: AdditionalPrinterColumnsV1[]
}[];
conversion: { conversion: {
strategy?: string; strategy?: string;
webhook?: any; webhook?: any;
}; };
additionalPrinterColumns?: AdditionalPrinterColumnsV1Beta[]; // removed in v1 /**
* @deprecated for apiextensions.k8s.io/v1 but used previously
*/
additionalPrinterColumns?: AdditionalPrinterColumnsV1Beta[];
}; };
status: { status: {
conditions: { conditions: {
@ -83,11 +95,23 @@ export interface CustomResourceDefinition {
}; };
} }
export interface CRDApiData extends KubeJsonApiData {
spec: object; // TODO: make better
}
export class CustomResourceDefinition extends KubeObject { export class CustomResourceDefinition extends KubeObject {
static kind = "CustomResourceDefinition"; static kind = "CustomResourceDefinition";
static namespaced = false; static namespaced = false;
static apiBase = "/apis/apiextensions.k8s.io/v1/customresourcedefinitions"; static apiBase = "/apis/apiextensions.k8s.io/v1/customresourcedefinitions";
constructor(data: CRDApiData) {
super(data);
if (!data.spec || typeof data.spec !== "object") {
throw new KubeCreationError("Cannot create a CustomResourceDefinition from an object without spec", data);
}
}
getResourceUrl() { getResourceUrl() {
return crdResourcesURL({ return crdResourcesURL({
params: { params: {
@ -125,9 +149,36 @@ export class CustomResourceDefinition extends KubeObject {
return this.spec.scope; return this.spec.scope;
} }
getPreferedVersion(): CRDVersion {
// Prefer the modern `versions` over the legacy `version`
if (this.spec.versions) {
for (const version of this.spec.versions) {
/**
* If the version is not served then 404 errors will occur
* We should also prefer the storage version
*/
if (version.served && version.storage) {
return version;
}
}
} else if (this.spec.version) {
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}`);
}
getVersion() { getVersion() {
// v1 has removed the spec.version property, if it is present it must match the first version return this.getPreferedVersion().name;
return this.spec.versions?.[0]?.name ?? this.spec.version;
} }
isNamespaced() { isNamespaced() {
@ -147,17 +198,14 @@ export class CustomResourceDefinition extends KubeObject {
} }
getPrinterColumns(ignorePriority = true): AdditionalPrinterColumnsV1[] { getPrinterColumns(ignorePriority = true): AdditionalPrinterColumnsV1[] {
const columns = this.spec.versions?.find(a => this.getVersion() == a.name)?.additionalPrinterColumns const columns = this.getPreferedVersion().additionalPrinterColumns ?? [];
?? this.spec.additionalPrinterColumns?.map(({ JSONPath, ...rest }) => ({ ...rest, jsonPath: JSONPath })) // map to V1 shape
?? [];
return columns return columns
.filter(column => column.name != "Age") .filter(column => column.name != "Age" && (ignorePriority || !column.priority));
.filter(column => ignorePriority ? true : !column.priority);
} }
getValidation() { getValidation() {
return JSON.stringify(this.spec.validation ?? this.spec.versions?.[0]?.schema, null, 2); return JSON.stringify(this.getPreferedVersion().schema, null, 2);
} }
getConditions() { getConditions() {

View File

@ -56,6 +56,21 @@ export interface INodeMetrics<T = IMetrics> {
fsSize: T; fsSize: T;
} }
export interface NodeTaint {
key: string;
value?: string;
effect: string;
timeAdded: string;
}
export function formatNodeTaint(taint: NodeTaint): string {
if (taint.value) {
return `${taint.key}=${taint.value}:${taint.effect}`;
}
return `${taint.key}:${taint.effect}`;
}
export interface Node { export interface Node {
spec: { spec: {
podCIDR?: string; podCIDR?: string;
@ -65,12 +80,7 @@ export interface Node {
* @deprecated see https://issues.k8s.io/61966 * @deprecated see https://issues.k8s.io/61966
*/ */
externalID?: string; externalID?: string;
taints?: { taints?: NodeTaint[];
key: string;
value: string;
effect: string;
timeAdded: string;
}[];
unschedulable?: boolean; unschedulable?: boolean;
}; };
status: { status: {

View File

@ -27,8 +27,7 @@ import { stringify } from "querystring";
import { EventEmitter } from "../../common/event-emitter"; import { EventEmitter } from "../../common/event-emitter";
import logger from "../../common/logger"; import logger from "../../common/logger";
export interface JsonApiData { export interface JsonApiData {}
}
export interface JsonApiError { export interface JsonApiError {
code?: number; code?: number;

View File

@ -19,24 +19,16 @@
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/ */
import { KubeConfig, V1Node, V1Pod } from "@kubernetes/client-node"; import { KubeConfig } from "@kubernetes/client-node";
import fse from "fs-extra"; import fse from "fs-extra";
import path from "path"; import path from "path";
import os from "os"; import os from "os";
import yaml from "js-yaml"; import yaml from "js-yaml";
import logger from "../main/logger"; import logger from "../main/logger";
import commandExists from "command-exists";
import { ExecValidationNotFoundError } from "./custom-errors";
import { Cluster, Context, newClusters, newContexts, newUsers, User } from "@kubernetes/client-node/dist/config_types"; import { Cluster, Context, newClusters, newContexts, newUsers, User } from "@kubernetes/client-node/dist/config_types";
import { resolvePath } from "./utils"; import { resolvePath } from "./utils";
import Joi from "joi"; import Joi from "joi";
export type KubeConfigValidationOpts = {
validateCluster?: boolean;
validateUser?: boolean;
validateExec?: boolean;
};
export const kubeConfigDefaultPath = path.join(os.homedir(), ".kube", "config"); export const kubeConfigDefaultPath = path.join(os.homedir(), ".kube", "config");
export function loadConfigFromFileSync(filePath: string): ConfigResult { export function loadConfigFromFileSync(filePath: string): ConfigResult {
@ -86,8 +78,7 @@ const contextSchema = Joi.object({
}), }),
}); });
const kubeConfigSchema = Joi const kubeConfigSchema = Joi.object({
.object({
users: Joi users: Joi
.array() .array()
.items(userSchema) .items(userSchema)
@ -104,17 +95,17 @@ const kubeConfigSchema = Joi
.string() .string()
.min(1) .min(1)
.optional(), .optional(),
}) })
.required(); .required();
export interface KubeConfigOptions { interface KubeConfigOptions {
clusters: Cluster[]; clusters: Cluster[];
users: User[]; users: User[];
contexts: Context[]; contexts: Context[];
currentContext?: string; currentContext?: string;
} }
export interface OptionsResult { interface OptionsResult {
options: KubeConfigOptions; options: KubeConfigOptions;
error: Joi.ValidationError; error: Joi.ValidationError;
} }
@ -132,7 +123,12 @@ function loadToOptions(rawYaml: string): OptionsResult {
arrays: true, arrays: true,
} }
}); });
const { clusters: rawClusters, users: rawUsers, contexts: rawContexts, "current-context": currentContext } = value ?? {}; const {
clusters: rawClusters,
users: rawUsers,
contexts: rawContexts,
"current-context": currentContext,
} = value ?? {};
const clusters = newClusters(rawClusters); const clusters = newClusters(rawClusters);
const users = newUsers(rawUsers); const users = newUsers(rawUsers);
const contexts = newContexts(rawContexts); const contexts = newContexts(rawContexts);
@ -175,31 +171,40 @@ export interface SplitConfigEntry {
* Breaks kube config into several configs. Each context as it own KubeConfig object * Breaks kube config into several configs. Each context as it own KubeConfig object
*/ */
export function splitConfig(kubeConfig: KubeConfig): SplitConfigEntry[] { export function splitConfig(kubeConfig: KubeConfig): SplitConfigEntry[] {
const { contexts = [] } = kubeConfig; return kubeConfig.getContexts().map(ctx => {
return contexts.map(context => {
const config = new KubeConfig(); const config = new KubeConfig();
const cluster = kubeConfig.getCluster(ctx.cluster);
const user = kubeConfig.getUser(ctx.user);
const context = kubeConfig.getContextObject(ctx.name);
config.clusters = [kubeConfig.getCluster(context.cluster)].filter(Boolean); if (cluster) {
config.users = [kubeConfig.getUser(context.user)].filter(Boolean); config.addCluster(cluster);
config.contexts = [kubeConfig.getContextObject(context.name)].filter(Boolean); }
config.setCurrentContext(context.name);
if (user) {
config.addUser(user);
}
if (context) {
config.addContext(context);
}
config.setCurrentContext(ctx.name);
return { return {
config, config,
error: validateKubeConfig(config, context.name)?.toString(), error: validateKubeConfig(config, ctx.name)?.toString(),
}; };
}); });
} }
/**
* Pretty format the object as human readable yaml, such as would be on the filesystem
* @param kubeConfig The kubeconfig object to format as pretty yaml
* @returns The yaml representation of the kubeconfig object
*/
export function dumpConfigYaml(kubeConfig: Partial<KubeConfig>): string { export function dumpConfigYaml(kubeConfig: Partial<KubeConfig>): string {
const config = { const clusters = kubeConfig.clusters.map(cluster => ({
apiVersion: "v1",
kind: "Config",
preferences: {},
"current-context": kubeConfig.currentContext,
clusters: kubeConfig.clusters.map(cluster => {
return {
name: cluster.name, name: cluster.name,
cluster: { cluster: {
"certificate-authority-data": cluster.caData, "certificate-authority-data": cluster.caData,
@ -207,20 +212,16 @@ export function dumpConfigYaml(kubeConfig: Partial<KubeConfig>): string {
server: cluster.server, server: cluster.server,
"insecure-skip-tls-verify": cluster.skipTLSVerify "insecure-skip-tls-verify": cluster.skipTLSVerify
} }
}; }));
}), const contexts = kubeConfig.contexts.map(context => ({
contexts: kubeConfig.contexts.map(context => {
return {
name: context.name, name: context.name,
context: { context: {
cluster: context.cluster, cluster: context.cluster,
user: context.user, user: context.user,
namespace: context.namespace namespace: context.namespace
} }
}; }));
}), const users = kubeConfig.users.map(user => ({
users: kubeConfig.users.map(user => {
return {
name: user.name, name: user.name,
user: { user: {
"client-certificate-data": user.certData, "client-certificate-data": user.certData,
@ -233,8 +234,15 @@ export function dumpConfigYaml(kubeConfig: Partial<KubeConfig>): string {
username: user.username, username: user.username,
password: user.password password: user.password
} }
}; }));
}) const config = {
apiVersion: "v1",
kind: "Config",
preferences: {},
"current-context": kubeConfig.currentContext,
clusters,
contexts,
users,
}; };
logger.debug("Dumping KubeConfig:", config); logger.debug("Dumping KubeConfig:", config);
@ -243,70 +251,25 @@ export function dumpConfigYaml(kubeConfig: Partial<KubeConfig>): string {
return yaml.safeDump(config, { skipInvalid: true }); return yaml.safeDump(config, { skipInvalid: true });
} }
export function podHasIssues(pod: V1Pod) {
// Logic adapted from dashboard
const notReady = !!pod.status.conditions.find(condition => {
return condition.type == "Ready" && condition.status !== "True";
});
return (
notReady ||
pod.status.phase !== "Running" ||
pod.spec.priority > 500000 // We're interested in high priority pods events regardless of their running status
);
}
export function getNodeWarningConditions(node: V1Node) {
return node.status?.conditions?.filter(c =>
c.status.toLowerCase() === "true" && c.type !== "Ready" && c.type !== "HostUpgrades"
) ?? [];
}
/** /**
* Checks if `config` has valid `Context`, `User`, `Cluster`, and `exec` fields (if present when required) * Checks if `config` has valid `Context`, `User`, `Cluster`, and `exec` fields (if present when required)
* *
* Note: This function returns an error instead of throwing it, returning `undefined` if the validation passes * Note: This function returns an error instead of throwing it, returning `undefined` if the validation passes
*/ */
export function validateKubeConfig(config: KubeConfig, contextName: string, validationOpts: KubeConfigValidationOpts = {}): Error | undefined { export function validateKubeConfig(config: KubeConfig, contextName: string): Error | undefined {
try {
// we only receive a single context, cluster & user object here so lets validate them as this
// will be called when we add a new cluster to Lens
const { validateUser = true, validateCluster = true, validateExec = true } = validationOpts;
const contextObject = config.getContextObject(contextName); const contextObject = config.getContextObject(contextName);
// Validate the Context Object
if (!contextObject) { if (!contextObject) {
return new Error(`No valid context object provided in kubeconfig for context '${contextName}'`); return new Error(`No valid context object provided in kubeconfig for context '${contextName}'`);
} }
// Validate the Cluster Object if (!config.getCluster(contextObject.cluster)) {
if (validateCluster && !config.getCluster(contextObject.cluster)) {
return new Error(`No valid cluster object provided in kubeconfig for context '${contextName}'`); return new Error(`No valid cluster object provided in kubeconfig for context '${contextName}'`);
} }
const user = config.getUser(contextObject.user); if (!config.getUser(contextObject.user)) {
// Validate the User Object
if (validateUser && !user) {
return new Error(`No valid user object provided in kubeconfig for context '${contextName}'`); return new Error(`No valid user object provided in kubeconfig for context '${contextName}'`);
} }
// Validate exec command if present
if (validateExec && user?.exec) {
const execCommand = user.exec["command"];
// check if the command is absolute or not
const isAbsolute = path.isAbsolute(execCommand);
// validate the exec struct in the user object, start with the command field
if (!commandExists.sync(execCommand)) {
return new ExecValidationNotFoundError(execCommand, isAbsolute);
}
}
return undefined; return undefined;
} catch (error) {
return error;
}
} }

View File

@ -23,9 +23,10 @@ import moment from "moment-timezone";
import path from "path"; import path from "path";
import os from "os"; import os from "os";
import { ThemeStore } from "../../renderer/theme.store"; import { ThemeStore } from "../../renderer/theme.store";
import { ObservableToggleSet } from "../utils"; import { getAppVersion, ObservableToggleSet } from "../utils";
import type {monaco} from "react-monaco-editor"; import type {monaco} from "react-monaco-editor";
import merge from "lodash/merge"; import merge from "lodash/merge";
import { SemVer } from "semver";
export interface KubeconfigSyncEntry extends KubeconfigSyncValue { export interface KubeconfigSyncEntry extends KubeconfigSyncValue {
filePath: string; filePath: string;
@ -260,6 +261,32 @@ const editorConfiguration: PreferenceDescription<EditorConfiguration, EditorConf
}, },
}; };
const updateChannels = new Map([
["latest", {
label: "Stable"
}],
["beta", {
label: "Beta"
}],
["alpha", {
label: "Alpha"
}],
]);
const defaultUpdateChannel = new SemVer(getAppVersion()).prerelease[0]?.toString() || "latest";
const updateChannel: PreferenceDescription<string> = {
fromStore(val) {
return updateChannels.has(val) ? val : defaultUpdateChannel;
},
toStore(val) {
if (!updateChannels.has(val) || val === defaultUpdateChannel) {
return undefined;
}
return val;
}
};
type PreferencesModelType<field extends keyof typeof DESCRIPTORS> = typeof DESCRIPTORS[field] extends PreferenceDescription<infer T, any> ? T : never; type PreferencesModelType<field extends keyof typeof DESCRIPTORS> = typeof DESCRIPTORS[field] extends PreferenceDescription<infer T, any> ? T : never;
type UserStoreModelType<field extends keyof typeof DESCRIPTORS> = typeof DESCRIPTORS[field] extends PreferenceDescription<any, infer T> ? T : never; type UserStoreModelType<field extends keyof typeof DESCRIPTORS> = typeof DESCRIPTORS[field] extends PreferenceDescription<any, infer T> ? T : never;
@ -288,4 +315,9 @@ export const DESCRIPTORS = {
syncKubeconfigEntries, syncKubeconfigEntries,
editorConfiguration, editorConfiguration,
terminalCopyOnSelect, terminalCopyOnSelect,
updateChannel,
};
export const CONSTANTS = {
updateChannels,
}; };

View File

@ -20,7 +20,7 @@
*/ */
import { app, ipcMain } from "electron"; import { app, ipcMain } from "electron";
import semver from "semver"; import semver, { SemVer } from "semver";
import { action, computed, observable, reaction, makeObservable } from "mobx"; import { action, computed, observable, reaction, makeObservable } from "mobx";
import { BaseStore } from "../base-store"; import { BaseStore } from "../base-store";
import migrations from "../../migrations/user-store"; import migrations from "../../migrations/user-store";
@ -75,6 +75,7 @@ export class UserStore extends BaseStore<UserStoreModel> /* implements UserStore
@observable downloadBinariesPath?: string; @observable downloadBinariesPath?: string;
@observable kubectlBinariesPath?: string; @observable kubectlBinariesPath?: string;
@observable terminalCopyOnSelect: boolean; @observable terminalCopyOnSelect: boolean;
@observable updateChannel?: string;
/** /**
* Download kubectl binaries matching cluster version * Download kubectl binaries matching cluster version
@ -106,6 +107,8 @@ export class UserStore extends BaseStore<UserStoreModel> /* implements UserStore
return this.shell || process.env.SHELL || process.env.PTYSHELL; return this.shell || process.env.SHELL || process.env.PTYSHELL;
} }
readonly isAllowedToDowngrade = new SemVer(getAppVersion()).prerelease[0] !== "latest";
startMainReactions() { startMainReactions() {
// track telemetry availability // track telemetry availability
reaction(() => this.allowTelemetry, allowed => { reaction(() => this.allowTelemetry, allowed => {
@ -218,6 +221,7 @@ export class UserStore extends BaseStore<UserStoreModel> /* implements UserStore
this.syncKubeconfigEntries.replace(DESCRIPTORS.syncKubeconfigEntries.fromStore(preferences?.syncKubeconfigEntries)); this.syncKubeconfigEntries.replace(DESCRIPTORS.syncKubeconfigEntries.fromStore(preferences?.syncKubeconfigEntries));
this.editorConfiguration = DESCRIPTORS.editorConfiguration.fromStore(preferences?.editorConfiguration); this.editorConfiguration = DESCRIPTORS.editorConfiguration.fromStore(preferences?.editorConfiguration);
this.terminalCopyOnSelect = DESCRIPTORS.terminalCopyOnSelect.fromStore(preferences?.terminalCopyOnSelect); this.terminalCopyOnSelect = DESCRIPTORS.terminalCopyOnSelect.fromStore(preferences?.terminalCopyOnSelect);
this.updateChannel = DESCRIPTORS.updateChannel.fromStore(preferences?.updateChannel);
} }
toJSON(): UserStoreModel { toJSON(): UserStoreModel {
@ -240,6 +244,7 @@ export class UserStore extends BaseStore<UserStoreModel> /* implements UserStore
syncKubeconfigEntries: DESCRIPTORS.syncKubeconfigEntries.toStore(this.syncKubeconfigEntries), syncKubeconfigEntries: DESCRIPTORS.syncKubeconfigEntries.toStore(this.syncKubeconfigEntries),
editorConfiguration: DESCRIPTORS.editorConfiguration.toStore(this.editorConfiguration), editorConfiguration: DESCRIPTORS.editorConfiguration.toStore(this.editorConfiguration),
terminalCopyOnSelect: DESCRIPTORS.terminalCopyOnSelect.toStore(this.terminalCopyOnSelect), terminalCopyOnSelect: DESCRIPTORS.terminalCopyOnSelect.toStore(this.terminalCopyOnSelect),
updateChannel: DESCRIPTORS.updateChannel.toStore(this.updateChannel),
}, },
}; };

View File

@ -19,5 +19,22 @@
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/ */
export * from "../../common/catalog-entities"; export {
KubernetesCluster,
kubernetesClusterCategory,
GeneralEntity,
WebLink,
} from "../../common/catalog-entities";
export type {
KubernetesClusterPrometheusMetrics,
KubernetesClusterSpec,
KubernetesClusterMetadata,
WebLinkSpec,
WebLinkStatus,
WebLinkStatusPhase,
KubernetesClusterStatusPhase,
KubernetesClusterStatus,
} from "../../common/catalog-entities";
export * from "../../common/catalog/catalog-entity"; export * from "../../common/catalog/catalog-entity";

View File

@ -27,8 +27,8 @@ import { areArgsUpdateAvailableToBackchannel, AutoUpdateLogPrefix, broadcastMess
import { once } from "lodash"; import { once } from "lodash";
import { ipcMain } from "electron"; import { ipcMain } from "electron";
import { nextUpdateChannel } from "./utils/update-channel"; import { nextUpdateChannel } from "./utils/update-channel";
import { UserStore } from "../common/user-store";
const updateChannel = autoUpdater.channel;
let installVersion: null | string = null; let installVersion: null | string = null;
export function isAutoUpdateEnabled() { export function isAutoUpdateEnabled() {
@ -58,9 +58,13 @@ export const startUpdateChecking = once(function (interval = 1000 * 60 * 60 * 24
return; return;
} }
const userStore = UserStore.getInstance();
autoUpdater.logger = logger; autoUpdater.logger = logger;
autoUpdater.autoDownload = false; autoUpdater.autoDownload = false;
autoUpdater.autoInstallOnAppQuit = false; autoUpdater.autoInstallOnAppQuit = false;
autoUpdater.channel = userStore.updateChannel;
autoUpdater.allowDowngrade = userStore.isAllowedToDowngrade;
autoUpdater autoUpdater
.on("update-available", (info: UpdateInfo) => { .on("update-available", (info: UpdateInfo) => {
@ -104,7 +108,7 @@ export const startUpdateChecking = once(function (interval = 1000 * 60 * 60 * 24
} }
}) })
.on("update-not-available", () => { .on("update-not-available", () => {
const nextChannel = nextUpdateChannel(updateChannel, autoUpdater.channel); const nextChannel = nextUpdateChannel(userStore.updateChannel, autoUpdater.channel);
logger.info(`${AutoUpdateLogPrefix}: update not available from ${autoUpdater.channel}, will check ${nextChannel} channel next`); logger.info(`${AutoUpdateLogPrefix}: update not available from ${autoUpdater.channel}, will check ${nextChannel} channel next`);

View File

@ -27,7 +27,7 @@ import logger from "./logger";
import { apiKubePrefix } from "../common/vars"; import { apiKubePrefix } from "../common/vars";
import { getClusterIdFromHost, Singleton } from "../common/utils"; import { getClusterIdFromHost, Singleton } from "../common/utils";
import { catalogEntityRegistry } from "./catalog"; import { catalogEntityRegistry } from "./catalog";
import { KubernetesCluster, KubernetesClusterPrometheusMetrics, KubernetesClusterStatusPhase } from "../common/catalog-entities/kubernetes-cluster"; import { KubernetesCluster, KubernetesClusterPrometheusMetrics, LensKubernetesClusterStatus } from "../common/catalog-entities/kubernetes-cluster";
import { ipcMainOn } from "../common/ipc"; import { ipcMainOn } from "../common/ipc";
import { once } from "lodash"; import { once } from "lodash";
import { ClusterStore } from "../common/cluster-store"; import { ClusterStore } from "../common/cluster-store";
@ -35,6 +35,8 @@ import type { ClusterId } from "../common/cluster-types";
const logPrefix = "[CLUSTER-MANAGER]:"; const logPrefix = "[CLUSTER-MANAGER]:";
const lensSpecificClusterStatuses: Set<string> = new Set(Object.values(LensKubernetesClusterStatus));
export class ClusterManager extends Singleton { export class ClusterManager extends Singleton {
private store = ClusterStore.getInstance(); private store = ClusterStore.getInstance();
deleting = observable.set<ClusterId>(); deleting = observable.set<ClusterId>();
@ -90,6 +92,8 @@ export class ClusterManager extends Singleton {
@action @action
protected updateCatalog(clusters: Cluster[]) { protected updateCatalog(clusters: Cluster[]) {
logger.debug("[CLUSTER-MANAGER]: updating catalog from cluster store");
for (const cluster of clusters) { for (const cluster of clusters) {
this.updateEntityFromCluster(cluster); this.updateEntityFromCluster(cluster);
} }
@ -144,23 +148,28 @@ export class ClusterManager extends Singleton {
@action @action
protected updateEntityStatus(entity: KubernetesCluster, cluster?: Cluster) { protected updateEntityStatus(entity: KubernetesCluster, cluster?: Cluster) {
if (this.deleting.has(entity.getId())) { if (this.deleting.has(entity.getId())) {
entity.status.phase = "deleting"; entity.status.phase = LensKubernetesClusterStatus.DELETING;
entity.status.enabled = false; entity.status.enabled = false;
} else { } else {
entity.status.phase = ((): KubernetesClusterStatusPhase => { entity.status.phase = (() => {
if (!cluster) { if (!cluster) {
return "disconnected"; return LensKubernetesClusterStatus.DISCONNECTED;
} }
if (cluster.accessible) { if (cluster.accessible) {
return "connected"; return LensKubernetesClusterStatus.CONNECTED;
} }
if (!cluster.disconnected) { if (!cluster.disconnected) {
return "connecting"; return LensKubernetesClusterStatus.CONNECTING;
} }
return "disconnected"; // Extensions are not allowed to use the Lens specific status phases
if (!lensSpecificClusterStatuses.has(entity?.status?.phase)) {
return entity.status.phase;
}
return LensKubernetesClusterStatus.DISCONNECTED;
})(); })();
entity.status.enabled = true; entity.status.enabled = true;
@ -272,7 +281,9 @@ export function catalogEntityFromCluster(cluster: Cluster) {
icon: {} icon: {}
}, },
status: { status: {
phase: cluster.disconnected ? "disconnected" : "connected", phase: cluster.disconnected
? LensKubernetesClusterStatus.DISCONNECTED
: LensKubernetesClusterStatus.CONNECTED,
reason: "", reason: "",
message: "", message: "",
active: !cluster.disconnected active: !cluster.disconnected

View File

@ -394,7 +394,6 @@ export class Cluster implements ClusterModel, ClusterState {
* @internal * @internal
*/ */
@action disconnect() { @action disconnect() {
logger.info(`[CLUSTER]: disconnect`, this.getMeta());
this.unbindEvents(); this.unbindEvents();
this.contextHandler?.stopServer(); this.contextHandler?.stopServer();
this.disconnected = true; this.disconnected = true;
@ -405,6 +404,7 @@ export class Cluster implements ClusterModel, ClusterState {
this.allowedNamespaces = []; this.allowedNamespaces = [];
this.resourceAccessStatuses.clear(); this.resourceAccessStatuses.clear();
this.pushState(); this.pushState();
logger.info(`[CLUSTER]: disconnect`, this.getMeta());
} }
/** /**

View File

@ -43,8 +43,8 @@ export async function listReleases(pathToKubeconfig: string, namespace?: string)
}); });
return output; return output;
} catch ({ stderr }) { } catch (error) {
throw stderr; throw error?.stderr || error;
} }
} }
@ -72,8 +72,8 @@ export async function installChart(chart: string, values: any, name: string | un
namespace namespace
} }
}; };
} catch ({ stderr }) { } catch (error) {
throw stderr; throw error?.stderr || error;
} finally { } finally {
await fse.unlink(fileName); await fse.unlink(fileName);
} }
@ -93,8 +93,8 @@ export async function upgradeRelease(name: string, chart: string, values: any, n
log: stdout, log: stdout,
release: getRelease(name, namespace, cluster) release: getRelease(name, namespace, cluster)
}; };
} catch ({ stderr }) { } catch (error) {
throw stderr; throw error?.stderr || error;
} finally { } finally {
await fse.unlink(fileName); await fse.unlink(fileName);
} }
@ -111,8 +111,8 @@ export async function getRelease(name: string, namespace: string, cluster: Clust
release.resources = await getResources(name, namespace, cluster); release.resources = await getResources(name, namespace, cluster);
return release; return release;
} catch ({ stderr }) { } catch (error) {
throw stderr; throw error?.stderr || error;
} }
} }
@ -122,8 +122,8 @@ export async function deleteRelease(name: string, namespace: string, pathToKubec
const { stdout } = await promiseExec(`"${helm}" delete ${name} --namespace ${namespace} --kubeconfig ${pathToKubeconfig}`); const { stdout } = await promiseExec(`"${helm}" delete ${name} --namespace ${namespace} --kubeconfig ${pathToKubeconfig}`);
return stdout; return stdout;
} catch ({ stderr }) { } catch (error) {
throw stderr; throw error?.stderr || error;
} }
} }
@ -139,8 +139,8 @@ export async function getValues(name: string, { namespace, all = false, pathToKu
const { stdout } = await promiseExec(`"${helm}" get values ${name} ${all ? "--all" : ""} --output yaml --namespace ${namespace} --kubeconfig ${pathToKubeconfig}`); const { stdout } = await promiseExec(`"${helm}" get values ${name} ${all ? "--all" : ""} --output yaml --namespace ${namespace} --kubeconfig ${pathToKubeconfig}`);
return stdout; return stdout;
} catch ({ stderr }) { } catch (error) {
throw stderr; throw error?.stderr || error;
} }
} }
@ -150,8 +150,8 @@ export async function getHistory(name: string, namespace: string, pathToKubeconf
const { stdout } = await promiseExec(`"${helm}" history ${name} --output json --namespace ${namespace} --kubeconfig ${pathToKubeconfig}`); const { stdout } = await promiseExec(`"${helm}" history ${name} --output json --namespace ${namespace} --kubeconfig ${pathToKubeconfig}`);
return JSON.parse(stdout); return JSON.parse(stdout);
} catch ({ stderr }) { } catch (error) {
throw stderr; throw error?.stderr || error;
} }
} }
@ -161,8 +161,8 @@ export async function rollback(name: string, namespace: string, revision: number
const { stdout } = await promiseExec(`"${helm}" rollback ${name} ${revision} --namespace ${namespace} --kubeconfig ${pathToKubeconfig}`); const { stdout } = await promiseExec(`"${helm}" rollback ${name} ${revision} --namespace ${namespace} --kubeconfig ${pathToKubeconfig}`);
return stdout; return stdout;
} catch ({ stderr }) { } catch (error) {
throw stderr; throw error?.stderr || error;
} }
} }

View File

@ -147,13 +147,18 @@ export function buildMenu(windowManager: WindowManager) {
} }
} }
]), ]),
{ type: "separator" }, { type: "separator" },
...(isMac ? [
{ {
role: "close", role: "close",
label: "Close Window" label: "Close Window",
}, accelerator: "Shift+Cmd+W"
}
] as MenuItemConstructorOptions[] : []),
...ignoreOnMac([ ...ignoreOnMac([
{ type: "separator" },
{ {
label: "Exit", label: "Exit",
accelerator: "Alt+F4", accelerator: "Alt+F4",

View File

@ -41,7 +41,7 @@ export class HelmApiRoute {
respondJson(response, chart); respondJson(response, chart);
} catch (error) { } catch (error) {
respondText(response, error, 422); respondText(response, error?.toString() || "Error getting chart", 422);
} }
} }
@ -53,7 +53,7 @@ export class HelmApiRoute {
respondJson(response, values); respondJson(response, values);
} catch (error) { } catch (error) {
respondText(response, error, 422); respondText(response, error?.toString() || "Error getting chart values", 422);
} }
} }
@ -66,7 +66,7 @@ export class HelmApiRoute {
respondJson(response, result, 201); respondJson(response, result, 201);
} catch (error) { } catch (error) {
logger.debug(error); logger.debug(error);
respondText(response, error, 422); respondText(response, error?.toString() || "Error installing chart", 422);
} }
} }
@ -79,7 +79,7 @@ export class HelmApiRoute {
respondJson(response, result); respondJson(response, result);
} catch (error) { } catch (error) {
logger.debug(error); logger.debug(error);
respondText(response, error, 422); respondText(response, error?.toString() || "Error updating chart", 422);
} }
} }
@ -92,7 +92,7 @@ export class HelmApiRoute {
respondJson(response, result); respondJson(response, result);
} catch (error) { } catch (error) {
logger.debug(error); logger.debug(error);
respondText(response, error, 422); respondText(response, error?.toString() || "Error rolling back chart", 422);
} }
} }
@ -105,7 +105,7 @@ export class HelmApiRoute {
respondJson(response, result); respondJson(response, result);
} catch(error) { } catch(error) {
logger.debug(error); logger.debug(error);
respondText(response, error, 422); respondText(response, error?.toString() || "Error listing release", 422);
} }
} }
@ -118,7 +118,7 @@ export class HelmApiRoute {
respondJson(response, result); respondJson(response, result);
} catch (error) { } catch (error) {
logger.debug(error); logger.debug(error);
respondText(response, error, 422); respondText(response, error?.toString() || "Error getting release", 422);
} }
} }
@ -132,7 +132,7 @@ export class HelmApiRoute {
respondText(response, result); respondText(response, result);
} catch (error) { } catch (error) {
logger.debug(error); logger.debug(error);
respondText(response, error, 422); respondText(response, error?.toString() || "Error getting release values", 422);
} }
} }
@ -145,7 +145,7 @@ export class HelmApiRoute {
respondJson(response, result); respondJson(response, result);
} catch (error) { } catch (error) {
logger.debug(error); logger.debug(error);
respondText(response, error, 422); respondText(response, error?.toString() || "Error getting release history", 422);
} }
} }
@ -158,7 +158,7 @@ export class HelmApiRoute {
respondJson(response, result); respondJson(response, result);
} catch (error) { } catch (error) {
logger.debug(error); logger.debug(error);
respondText(response, error, 422); respondText(response, error?.toString() || "Error deleting release", 422);
} }
} }
} }

View File

@ -33,17 +33,17 @@ describe("nextUpdateChannel", () => {
expect(nextUpdateChannel("latest", "alpha")).toEqual("beta"); expect(nextUpdateChannel("latest", "alpha")).toEqual("beta");
}); });
it("returns rc if current channel is beta", () => { it("returns latest if current channel is beta", () => {
expect(nextUpdateChannel("alpha", "beta")).toEqual("rc"); expect(nextUpdateChannel("alpha", "beta")).toEqual("latest");
expect(nextUpdateChannel("beta", "beta")).toEqual("rc"); expect(nextUpdateChannel("beta", "beta")).toEqual("latest");
expect(nextUpdateChannel("rc", "beta")).toEqual("rc"); expect(nextUpdateChannel("rc", "beta")).toEqual("latest");
expect(nextUpdateChannel("latest", "beta")).toEqual("rc"); expect(nextUpdateChannel("latest", "beta")).toEqual("latest");
}); });
it("returns latest if current channel is rc", () => { it("returns default if current channel is unknown", () => {
expect(nextUpdateChannel("alpha", "rc")).toEqual("latest"); expect(nextUpdateChannel("alpha", "rc")).toEqual("alpha");
expect(nextUpdateChannel("beta", "rc")).toEqual("latest"); expect(nextUpdateChannel("beta", "rc")).toEqual("beta");
expect(nextUpdateChannel("rc", "rc")).toEqual("latest"); expect(nextUpdateChannel("rc", "rc")).toEqual("rc");
expect(nextUpdateChannel("latest", "rc")).toEqual("latest"); expect(nextUpdateChannel("latest", "rc")).toEqual("latest");
}); });
}); });

View File

@ -19,14 +19,19 @@
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/ */
/**
* Compute the next update channel from the current updating channel
* @param defaultChannel The default (initial) channel to check
* @param channel The current channel that did not have a new version associated with it
* @returns The channel name of the next release version
*/
export function nextUpdateChannel(defaultChannel: string, channel: string): string { export function nextUpdateChannel(defaultChannel: string, channel: string): string {
if (channel === "alpha") { switch (channel) {
case "alpha":
return "beta"; return "beta";
} else if (channel === "beta") { case "beta":
return "rc"; return "latest"; // there is no RC currently
} else if (channel === "rc") { default:
return "latest";
} else {
return defaultChannel; return defaultChannel;
} }
} }

View File

@ -23,8 +23,8 @@ import "./helm-chart-details.scss";
import React, { Component } from "react"; import React, { Component } from "react";
import { getChartDetails, HelmChart } from "../../../common/k8s-api/endpoints/helm-charts.api"; import { getChartDetails, HelmChart } from "../../../common/k8s-api/endpoints/helm-charts.api";
import { observable, autorun, makeObservable } from "mobx"; import { observable, makeObservable, reaction } from "mobx";
import { observer } from "mobx-react"; import { disposeOnUnmount, observer } from "mobx-react";
import { Drawer, DrawerItem } from "../drawer"; import { Drawer, DrawerItem } from "../drawer";
import { boundMethod, stopPropagation } from "../../utils"; import { boundMethod, stopPropagation } from "../../utils";
import { MarkdownViewer } from "../markdown-viewer"; import { MarkdownViewer } from "../markdown-viewer";
@ -64,20 +64,24 @@ export class HelmChartDetails extends Component<Props> {
this.abortController?.abort(); this.abortController?.abort();
} }
chartUpdater = autorun(() => { componentDidMount() {
this.selectedChart = null; disposeOnUnmount(this, [
const { chart: { name, repo, version } } = this.props; reaction(() => this.props.chart, async ({ name, repo, version }) => {
try {
const { readme, versions } = await getChartDetails(repo, name, { version });
getChartDetails(repo, name, { version }) this.readme = readme;
.then(result => { this.chartVersions = versions;
this.readme = result.readme; this.selectedChart = versions[0];
this.chartVersions = result.versions; } catch (error) {
this.selectedChart = result.versions[0];
})
.catch(error => {
this.error = error; this.error = error;
}); this.selectedChart = null;
}); }
}, {
fireImmediately: true,
}),
]);
}
@boundMethod @boundMethod
async onVersionChange({ value: chart }: SelectOption<HelmChart>) { async onVersionChange({ value: chart }: SelectOption<HelmChart>) {
@ -135,7 +139,7 @@ export class HelmChartDetails extends Component<Props> {
value: chart, value: chart,
}))} }))}
isOptionDisabled={({ value: chart }) => chart.deprecated} isOptionDisabled={({ value: chart }) => chart.deprecated}
value={selectedChart.getVersion()} value={selectedChart}
onChange={onVersionChange} onChange={onVersionChange}
/> />
</DrawerItem> </DrawerItem>
@ -170,10 +174,6 @@ export class HelmChartDetails extends Component<Props> {
} }
renderContent() { renderContent() {
if (!this.selectedChart) {
return <Spinner center />;
}
if (this.error) { if (this.error) {
return ( return (
<div className="box grow"> <div className="box grow">
@ -182,6 +182,10 @@ export class HelmChartDetails extends Component<Props> {
); );
} }
if (!this.selectedChart) {
return <Spinner center />;
}
return ( return (
<div className="box grow"> <div className="box grow">
{this.renderIntroduction()} {this.renderIntroduction()}

View File

@ -26,6 +26,11 @@
align-items: center; align-items: center;
} }
.loading-error {
color: red;
text-align: center;
}
.status { .status {
@include release-status-bgs; @include release-status-bgs;
} }

View File

@ -56,12 +56,13 @@ interface Props {
@observer @observer
export class ReleaseDetails extends Component<Props> { export class ReleaseDetails extends Component<Props> {
@observable details: IReleaseDetails; @observable details: IReleaseDetails | null = null;
@observable values = ""; @observable values = "";
@observable valuesLoading = false; @observable valuesLoading = false;
@observable showOnlyUserSuppliedValues = true; @observable showOnlyUserSuppliedValues = true;
@observable saving = false; @observable saving = false;
@observable releaseSecret: Secret; @observable releaseSecret: Secret;
@observable error?: string = undefined;
componentDidMount() { componentDidMount() {
disposeOnUnmount(this, [ disposeOnUnmount(this, [
@ -97,8 +98,12 @@ export class ReleaseDetails extends Component<Props> {
async loadDetails() { async loadDetails() {
const { release } = this.props; const { release } = this.props;
try {
this.details = null; this.details = null;
this.details = await getRelease(release.getName(), release.getNs()); this.details = await getRelease(release.getName(), release.getNs());
} catch (error) {
this.error = `Failed to get release details: ${error}`;
}
} }
async loadValues() { async loadValues() {
@ -237,11 +242,18 @@ export class ReleaseDetails extends Component<Props> {
renderContent() { renderContent() {
const { release } = this.props; const { release } = this.props;
const { details } = this;
if (!release) return null; if (!release) return null;
if (!details) { if (this.error) {
return (
<div className="loading-error">
{this.error}
</div>
);
}
if (!this.details) {
return <Spinner center/>; return <Spinner center/>;
} }

View File

@ -79,6 +79,7 @@ export class HelmReleaseMenu extends React.Component<Props> {
{...menuProps} {...menuProps}
className={cssNames("HelmReleaseMenu", className)} className={cssNames("HelmReleaseMenu", className)}
removeAction={this.remove} removeAction={this.remove}
removeConfirmationMessage={() => <p>Remove Helm Release <b>{release.name}</b>?</p>}
> >
{this.renderContent()} {this.renderContent()}
</MenuActions> </MenuActions>

View File

@ -30,7 +30,7 @@ import { Badge } from "../badge";
import { ResourceMetrics } from "../resource-metrics"; import { ResourceMetrics } from "../resource-metrics";
import { podsStore } from "../+workloads-pods/pods.store"; import { podsStore } from "../+workloads-pods/pods.store";
import type { KubeObjectDetailsProps } from "../kube-object-details"; import type { KubeObjectDetailsProps } from "../kube-object-details";
import { getMetricsByNodeNames, IClusterMetrics, Node } from "../../../common/k8s-api/endpoints"; import { formatNodeTaint, getMetricsByNodeNames, IClusterMetrics, Node } from "../../../common/k8s-api/endpoints";
import { NodeCharts } from "./node-charts"; import { NodeCharts } from "./node-charts";
import { makeObservable, observable, reaction } from "mobx"; import { makeObservable, observable, reaction } from "mobx";
import { PodDetailsList } from "../+workloads-pods/pod-details-list"; import { PodDetailsList } from "../+workloads-pods/pod-details-list";
@ -132,11 +132,7 @@ export class NodeDetails extends React.Component<Props> {
/> />
{taints.length > 0 && ( {taints.length > 0 && (
<DrawerItem name="Taints" labelsOnly> <DrawerItem name="Taints" labelsOnly>
{ {taints.map(taint => <Badge key={taint.key} label={formatNodeTaint(taint)} />)}
taints.map(({ key, effect, value }) => (
<Badge key={key} label={`${key}=${value}:${effect}`} />
))
}
</DrawerItem> </DrawerItem>
)} )}
{conditions && {conditions &&

View File

@ -27,7 +27,7 @@ import { cssNames, interval } from "../../utils";
import { TabLayout } from "../layout/tab-layout"; import { TabLayout } from "../layout/tab-layout";
import { nodesStore } from "./nodes.store"; import { nodesStore } from "./nodes.store";
import { KubeObjectListLayout } from "../kube-object-list-layout"; import { KubeObjectListLayout } from "../kube-object-list-layout";
import { getMetricsForAllNodes, INodeMetrics, Node } from "../../../common/k8s-api/endpoints/nodes.api"; import { formatNodeTaint, getMetricsForAllNodes, INodeMetrics, Node } from "../../../common/k8s-api/endpoints/nodes.api";
import { LineProgress } from "../line-progress"; import { LineProgress } from "../line-progress";
import { bytesToUnits } from "../../../common/utils/convertMemory"; import { bytesToUnits } from "../../../common/utils/convertMemory";
import { Tooltip, TooltipPosition } from "../tooltip"; import { Tooltip, TooltipPosition } from "../tooltip";
@ -227,6 +227,7 @@ export class Nodes extends React.Component<Props> {
]} ]}
renderTableContents={node => { renderTableContents={node => {
const tooltipId = `node-taints-${node.getId()}`; const tooltipId = `node-taints-${node.getId()}`;
const taints = node.getTaints();
return [ return [
<Badge flat key="name" label={node.getName()} tooltip={node.getName()} />, <Badge flat key="name" label={node.getName()} tooltip={node.getName()} />,
@ -235,9 +236,9 @@ export class Nodes extends React.Component<Props> {
this.renderMemoryUsage(node), this.renderMemoryUsage(node),
this.renderDiskUsage(node), this.renderDiskUsage(node),
<> <>
<span id={tooltipId}>{node.getTaints().length}</span> <span id={tooltipId}>{taints.length}</span>
<Tooltip targetId={tooltipId} tooltipOnParentHover={true} style={{ whiteSpace: "pre-line" }}> <Tooltip targetId={tooltipId} tooltipOnParentHover={true} style={{ whiteSpace: "pre-line" }}>
{node.getTaints().map(({ key, value, effect }) => `${key}=${value}:${effect}`).join("\n")} {taints.map(formatNodeTaint).join("\n")}
</Tooltip> </Tooltip>
</>, </>,
node.getRoleLabels(), node.getRoleLabels(),

View File

@ -29,11 +29,16 @@ import { Input } from "../input";
import { isWindows } from "../../../common/vars"; import { isWindows } from "../../../common/vars";
import { FormSwitch, Switcher } from "../switch"; import { FormSwitch, Switcher } from "../switch";
import moment from "moment-timezone"; import moment from "moment-timezone";
import { CONSTANTS } from "../../../common/user-store/preferences-helpers";
const timezoneOptions: SelectOption<string>[] = moment.tz.names().map(zone => ({ const timezoneOptions: SelectOption<string>[] = moment.tz.names().map(zone => ({
label: zone, label: zone,
value: zone, value: zone,
})); }));
const updateChannelOptions: SelectOption<string>[] = Array.from(
CONSTANTS.updateChannels.entries(),
([value, { label }]) => ({ value, label }),
);
export const Application = observer(() => { export const Application = observer(() => {
const defaultShell = process.env.SHELL const defaultShell = process.env.SHELL
@ -104,6 +109,18 @@ export const Application = observer(() => {
<hr /> <hr />
<section id="update-channel">
<SubTitle title="Update Channel"/>
<Select
options={updateChannelOptions}
value={UserStore.getInstance().updateChannel}
onChange={({ value }: SelectOption) => UserStore.getInstance().updateChannel = value}
themeName="lens"
/>
</section>
<hr />
<section id="locale"> <section id="locale">
<SubTitle title="Locale Timezone" /> <SubTitle title="Locale Timezone" />
<Select <Select

View File

@ -19,14 +19,23 @@
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/ */
export = commandExists; import { formatNodeTaint } from "../../../common/k8s-api/endpoints";
declare function commandExists(commandName: string): Promise<string>; describe("formatNodeTaint tests", () => {
declare function commandExists( it("should use value if defined", () => {
commandName: string, expect(formatNodeTaint({
cb: (error: null, exists: boolean) => void effect: "Foo",
): void; key: "hello",
timeAdded: "pre",
value: "a"
})).toBe("hello=a:Foo");
});
declare namespace commandExists { it("should not use value if not defined", () => {
function sync(commandName: string): boolean; expect(formatNodeTaint({
} effect: "Foo",
key: "hello",
timeAdded: "pre",
})).toBe("hello:Foo");
});
});

View File

@ -29,6 +29,7 @@ import { Tab, TabProps } from "../tabs";
import { Icon } from "../icon"; import { Icon } from "../icon";
import { Menu, MenuItem } from "../menu"; import { Menu, MenuItem } from "../menu";
import { observable, makeObservable } from "mobx"; import { observable, makeObservable } from "mobx";
import { isMac } from "../../../common/vars";
export interface DockTabProps extends TabProps<DockTabModel> { export interface DockTabProps extends TabProps<DockTabModel> {
moreActions?: React.ReactNode; moreActions?: React.ReactNode;
@ -94,7 +95,7 @@ export class DockTab extends React.Component<DockTabProps> {
{!pinned && ( {!pinned && (
<Icon <Icon
small material="close" small material="close"
tooltip="Close (Ctrl+Shift+W)" tooltip={`Close ${isMac ? "⌘+W" : "Ctrl+W"}`}
onClick={prevDefault(this.close)} onClick={prevDefault(this.close)}
/> />
)} )}

View File

@ -27,6 +27,16 @@
display: flex; display: flex;
flex-direction: column; flex-direction: column;
&:not(:focus-within) .DockTab.active {
&::after {
color: var(--halfGray);
}
&:hover::after {
color: var(--line-color-active);
}
}
&.isOpen { &.isOpen {
&.fullSize { &.fullSize {
position: fixed; position: fixed;

View File

@ -46,19 +46,31 @@ interface Props {
@observer @observer
export class Dock extends React.Component<Props> { export class Dock extends React.Component<Props> {
onKeydown = (evt: React.KeyboardEvent<HTMLElement>) => { private element = React.createRef<HTMLDivElement>();
const { close, closeTab, selectedTab } = dockStore;
if (!selectedTab) return; componentDidMount() {
const { code, ctrlKey, shiftKey } = evt.nativeEvent; document.addEventListener("keydown", this.onKeyDown);
}
componentWillUnmount() {
document.removeEventListener("keydown", this.onKeyDown);
}
onKeyDown = (evt: KeyboardEvent) => {
const { close, selectedTab, closeTab } = dockStore;
const { code, ctrlKey, metaKey, shiftKey } = evt;
// Determine if user working inside <Dock/> or using any other areas in app
const dockIsFocused = this.element?.current.contains(document.activeElement);
if (!selectedTab || !dockIsFocused) return;
if (shiftKey && code === "Escape") { if (shiftKey && code === "Escape") {
close(); close();
} }
if (ctrlKey && code === "KeyW") { if ((ctrlKey && code === "KeyW") || (metaKey && code === "KeyW")) {
if (selectedTab.pinned) close(); closeTab(selectedTab.id);
else closeTab(selectedTab.id); this.element?.current.focus(); // Avoid loosing focus when closing tab
} }
}; };
@ -67,6 +79,7 @@ export class Dock extends React.Component<Props> {
open(); open();
selectTab(tab.id); selectTab(tab.id);
this.element?.current.focus();
}; };
renderTab(tab: DockTab) { renderTab(tab: DockTab) {
@ -105,7 +118,7 @@ export class Dock extends React.Component<Props> {
return ( return (
<div <div
className={cssNames("Dock", className, { isOpen, fullSize })} className={cssNames("Dock", className, { isOpen, fullSize })}
onKeyDown={this.onKeydown} ref={this.element}
tabIndex={-1} tabIndex={-1}
> >
<ResizingAnchor <ResizingAnchor

View File

@ -222,7 +222,7 @@ export class Terminal {
}; };
keyHandler = (evt: KeyboardEvent): boolean => { keyHandler = (evt: KeyboardEvent): boolean => {
const { code, ctrlKey, type, metaKey } = evt; const { code, ctrlKey, metaKey } = evt;
// Handle custom hotkey bindings // Handle custom hotkey bindings
if (ctrlKey) { if (ctrlKey) {
@ -248,11 +248,6 @@ export class Terminal {
} }
} }
// Pass the event above in DOM for <Dock/> to handle common actions
if (!evt.defaultPrevented) {
this.elem.dispatchEvent(new KeyboardEvent(type, evt));
}
return true; return true;
}; };
} }

View File

@ -30,6 +30,7 @@ import { navigate } from "../../navigation";
import { cssNames, IClassName } from "../../utils"; import { cssNames, IClassName } from "../../utils";
import { Icon } from "../icon"; import { Icon } from "../icon";
import { HotbarIcon } from "./hotbar-icon"; import { HotbarIcon } from "./hotbar-icon";
import { LensKubernetesClusterStatus } from "../../../common/catalog-entities/kubernetes-cluster";
interface Props extends DOMAttributes<HTMLElement> { interface Props extends DOMAttributes<HTMLElement> {
entity: CatalogEntity; entity: CatalogEntity;
@ -77,7 +78,7 @@ export class HotbarEntityIcon extends React.Component<Props> {
return null; return null;
} }
const className = cssNames("led", { online: this.props.entity.status.phase == "connected"}); // TODO: make it more generic const className = cssNames("led", { online: this.props.entity.status.phase === LensKubernetesClusterStatus.CONNECTED}); // TODO: make it more generic
return <div className={className} />; return <div className={className} />;
} }

View File

@ -193,7 +193,7 @@ export class Sidebar extends React.Component<Props> {
source={metadata.source} source={metadata.source}
src={spec.icon?.src} src={spec.icon?.src}
className="mr-5" className="mr-5"
onClick={() => navigate(routes.clusterURL())} onClick={() => navigate("/")}
/> />
<div className={styles.clusterName}> <div className={styles.clusterName}>
{metadata.name} {metadata.name}

View File

@ -25,7 +25,7 @@
grid-template-rows: var(--main-layout-header); grid-template-rows: var(--main-layout-header);
grid-template-areas: "title controls"; grid-template-areas: "title controls";
background-color: var(--layoutBackground); background-color: var(--layoutBackground);
z-index: 1; z-index: 2;
width: 100%; width: 100%;
grid-area: topbar; grid-area: topbar;
} }

View File

@ -28,6 +28,7 @@ import { StorageHelper } from "./storageHelper";
import { ClusterStore } from "../../common/cluster-store"; import { ClusterStore } from "../../common/cluster-store";
import logger from "../../main/logger"; import logger from "../../main/logger";
import { getHostedClusterId, getPath } from "../../common/utils"; import { getHostedClusterId, getPath } from "../../common/utils";
import { isTestEnv } from "../../common/vars";
const storage = observable({ const storage = observable({
initialized: false, initialized: false,
@ -62,7 +63,9 @@ export function createAppStorage<T>(key: string, defaultValue: T, clusterId?: st
.then(data => storage.data = data) .then(data => storage.data = data)
.catch(() => null) // ignore empty / non-existing / invalid json files .catch(() => null) // ignore empty / non-existing / invalid json files
.finally(() => { .finally(() => {
if (!isTestEnv) {
logger.info(`${logPrefix} loading finished for ${filePath}`); logger.info(`${logPrefix} loading finished for ${filePath}`);
}
storage.loaded = true; storage.loaded = true;
}); });

675
yarn.lock

File diff suppressed because it is too large Load Diff