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:
commit
62b45875c4
16
Makefile
16
Makefile
@ -113,19 +113,17 @@ docs:
|
||||
|
||||
.PHONY: clean-extensions
|
||||
clean-extensions:
|
||||
$(foreach dir, $(wildcard $(EXTENSIONS_DIR)/*), rm -rf $(dir)/dist)
|
||||
$(foreach dir, $(wildcard $(EXTENSIONS_DIR)/*), rm -rf $(dir)/node_modules)
|
||||
$(foreach dir, $(wildcard $(EXTENSIONS_DIR)/*), rm $(dir)/*.tgz || true)
|
||||
rm -rf $(EXTENSIONS_DIR)/*/{dist,node_modules,*.tgz}
|
||||
|
||||
.PHONY: clean-npm
|
||||
clean-npm:
|
||||
rm -rf src/extensions/npm/extensions/dist
|
||||
rm -rf src/extensions/npm/extensions/__mocks__
|
||||
rm -rf src/extensions/npm/extensions/node_modules
|
||||
rm -rf src/extensions/npm/extensions/{dist,__mocks__,node_modules}
|
||||
|
||||
.PHONY: clean
|
||||
clean: clean-npm clean-extensions
|
||||
rm -rf binaries/client
|
||||
rm -rf dist/*
|
||||
rm -rf static/build/*
|
||||
rm -rf node_modules/
|
||||
rm -rf dist
|
||||
rm -rf static/build
|
||||
rm -rf node_modules
|
||||
rm -rf site
|
||||
rm -rf docs/extensions/api
|
||||
|
||||
@ -2,6 +2,6 @@
|
||||
|
||||
## APIs
|
||||
|
||||
- [Common](modules/common.md)
|
||||
- [Main](modules/main.md)
|
||||
- [Renderer](modules/renderer.md)
|
||||
- [Common](modules/Common.md)
|
||||
- [Main](modules/Main.md)
|
||||
- [Renderer](modules/Renderer.md)
|
||||
|
||||
@ -33,7 +33,7 @@ describe("preferences page tests", () => {
|
||||
|
||||
beforeEach(async () => {
|
||||
let app: ElectronApplication;
|
||||
|
||||
|
||||
({ window, cleanup, app } = await utils.start());
|
||||
await utils.clickWelcomeButton(window);
|
||||
|
||||
@ -74,7 +74,7 @@ describe("preferences page tests", () => {
|
||||
utils.itIf(process.platform !== "win32")("ensures helm repos", async () => {
|
||||
await window.click("[data-testid=kubernetes-tab]");
|
||||
await window.waitForSelector("[data-testid=repository-name]", {
|
||||
timeout: 100_000,
|
||||
timeout: 140_000,
|
||||
});
|
||||
await window.click("#HelmRepoSelect");
|
||||
await window.waitForSelector("div.Select__option");
|
||||
|
||||
@ -93,7 +93,7 @@ export async function start() {
|
||||
}
|
||||
|
||||
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() {
|
||||
|
||||
33
package.json
33
package.json
@ -3,7 +3,7 @@
|
||||
"productName": "OpenLens",
|
||||
"description": "OpenLens - Open Source IDE for Kubernetes",
|
||||
"homepage": "https://github.com/lensapp/lens",
|
||||
"version": "5.3.0-alpha.0",
|
||||
"version": "5.3.0-alpha.2",
|
||||
"main": "static/build/main.js",
|
||||
"copyright": "© 2021 OpenLens Authors",
|
||||
"license": "MIT",
|
||||
@ -179,7 +179,7 @@
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"@electron/remote": "^1.2.1",
|
||||
"@electron/remote": "^1.2.2",
|
||||
"@hapi/call": "^8.0.1",
|
||||
"@hapi/subtext": "^7.0.3",
|
||||
"@kubernetes/client-node": "^0.15.1",
|
||||
@ -193,8 +193,7 @@
|
||||
"byline": "^5.0.0",
|
||||
"chalk": "^4.1.0",
|
||||
"chokidar": "^3.4.3",
|
||||
"command-exists": "1.2.9",
|
||||
"conf": "^7.0.1",
|
||||
"conf": "^7.1.2",
|
||||
"crypto-js": "^4.1.1",
|
||||
"electron-devtools-installer": "^3.2.0",
|
||||
"electron-updater": "^4.6.0",
|
||||
@ -251,7 +250,7 @@
|
||||
"winston": "^3.3.3",
|
||||
"winston-console-format": "^1.0.8",
|
||||
"winston-transport-browserconsole": "^1.0.5",
|
||||
"ws": "^7.4.6"
|
||||
"ws": "^7.5.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@emeraldpay/hashicon-react": "^0.4.0",
|
||||
@ -259,13 +258,13 @@
|
||||
"@material-ui/icons": "^4.11.2",
|
||||
"@material-ui/lab": "^4.0.0-alpha.60",
|
||||
"@pmmmwh/react-refresh-webpack-plugin": "^0.4.3",
|
||||
"@sentry/react": "^6.8.0",
|
||||
"@sentry/react": "^6.13.3",
|
||||
"@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/react": "^11.2.6",
|
||||
"@testing-library/user-event": "^13.2.1",
|
||||
"@types/byline": "^4.2.32",
|
||||
"@types/byline": "^4.2.33",
|
||||
"@types/chart.js": "^2.9.34",
|
||||
"@types/color": "^3.0.2",
|
||||
"@types/crypto-js": "^3.1.47",
|
||||
@ -279,7 +278,7 @@
|
||||
"@types/http-proxy": "^1.17.7",
|
||||
"@types/jest": "^26.0.24",
|
||||
"@types/js-yaml": "^3.12.4",
|
||||
"@types/jsdom": "^16.2.4",
|
||||
"@types/jsdom": "^16.2.13",
|
||||
"@types/jsonpath": "^0.2.0",
|
||||
"@types/lodash": "^4.14.155",
|
||||
"@types/marked": "^2.0.4",
|
||||
@ -287,13 +286,13 @@
|
||||
"@types/mini-css-extract-plugin": "^0.9.1",
|
||||
"@types/mock-fs": "^4.13.1",
|
||||
"@types/module-alias": "^2.0.1",
|
||||
"@types/node": "14.17.14",
|
||||
"@types/node": "14.17.26",
|
||||
"@types/node-fetch": "^2.5.12",
|
||||
"@types/npm": "^2.0.32",
|
||||
"@types/progress-bar-webpack-plugin": "^2.1.2",
|
||||
"@types/proper-lockfile": "^4.1.2",
|
||||
"@types/randomcolor": "^0.5.6",
|
||||
"@types/react": "^17.0.0",
|
||||
"@types/react": "^17.0.29",
|
||||
"@types/react-beautiful-dnd": "^13.1.1",
|
||||
"@types/react-dom": "^17.0.9",
|
||||
"@types/react-router-dom": "^5.3.1",
|
||||
@ -301,7 +300,7 @@
|
||||
"@types/react-table": "^7.7.6",
|
||||
"@types/react-virtualized-auto-sizer": "^1.0.1",
|
||||
"@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-promise-native": "^1.0.18",
|
||||
"@types/semver": "^7.2.0",
|
||||
@ -327,7 +326,7 @@
|
||||
"concurrently": "^5.2.0",
|
||||
"css-loader": "^5.2.7",
|
||||
"deepdash": "^5.3.9",
|
||||
"dompurify": "^2.3.1",
|
||||
"dompurify": "^2.3.3",
|
||||
"electron": "^13.5.1",
|
||||
"electron-builder": "^22.11.11",
|
||||
"electron-notarize": "^0.3.0",
|
||||
@ -335,7 +334,7 @@
|
||||
"esbuild-loader": "^2.15.1",
|
||||
"eslint": "^7.32.0",
|
||||
"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-unused-imports": "^1.0.1",
|
||||
"file-loader": "^6.2.0",
|
||||
@ -353,7 +352,7 @@
|
||||
"mini-css-extract-plugin": "^1.6.2",
|
||||
"node-gyp": "7.1.2",
|
||||
"node-loader": "^1.0.3",
|
||||
"nodemon": "^2.0.12",
|
||||
"nodemon": "^2.0.13",
|
||||
"playwright": "^1.14.0",
|
||||
"postcss": "^8.3.6",
|
||||
"postcss-loader": "4.3.0",
|
||||
@ -373,14 +372,14 @@
|
||||
"sass-loader": "^8.0.2",
|
||||
"sharp": "^0.29.1",
|
||||
"style-loader": "^2.0.0",
|
||||
"tailwindcss": "^2.2.4",
|
||||
"tailwindcss": "^2.2.17",
|
||||
"ts-jest": "26.5.6",
|
||||
"ts-loader": "^7.0.5",
|
||||
"ts-node": "^10.2.1",
|
||||
"type-fest": "^1.0.2",
|
||||
"typed-emitter": "^1.3.1",
|
||||
"typedoc": "0.22.5",
|
||||
"typedoc-plugin-markdown": "^3.9.0",
|
||||
"typedoc-plugin-markdown": "^3.11.3",
|
||||
"typeface-roboto": "^1.1.13",
|
||||
"typescript": "^4.4.3",
|
||||
"typescript-plugin-css-modules": "^3.4.0",
|
||||
|
||||
@ -20,7 +20,7 @@
|
||||
*/
|
||||
|
||||
import { KubeConfig } from "@kubernetes/client-node";
|
||||
import { validateKubeConfig, loadConfigFromString, getNodeWarningConditions } from "../kube-helpers";
|
||||
import { validateKubeConfig, loadConfigFromString } from "../kube-helpers";
|
||||
|
||||
const kubeconfig = `
|
||||
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");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@ -54,15 +54,24 @@ export interface KubernetesClusterSpec extends CatalogEntitySpec {
|
||||
accessibleNamespaces?: string[];
|
||||
}
|
||||
|
||||
export enum LensKubernetesClusterStatus {
|
||||
DELETING = "deleting",
|
||||
CONNECTING = "connecting",
|
||||
CONNECTED = "connected",
|
||||
DISCONNECTED = "disconnected"
|
||||
}
|
||||
|
||||
export interface KubernetesClusterMetadata extends CatalogEntityMetadata {
|
||||
distro?: 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 interface KubernetesClusterStatus extends CatalogEntityStatus {
|
||||
phase: KubernetesClusterStatusPhase;
|
||||
}
|
||||
|
||||
export class KubernetesCluster extends CatalogEntity<KubernetesClusterMetadata, KubernetesClusterStatus, KubernetesClusterSpec> {
|
||||
@ -110,15 +119,15 @@ export class KubernetesCluster extends CatalogEntity<KubernetesClusterMetadata,
|
||||
}
|
||||
|
||||
switch (this.status.phase) {
|
||||
case "connected":
|
||||
case "connecting":
|
||||
case LensKubernetesClusterStatus.CONNECTED:
|
||||
case LensKubernetesClusterStatus.CONNECTING:
|
||||
context.menuItems.push({
|
||||
title: "Disconnect",
|
||||
icon: "link_off",
|
||||
onClick: () => requestMain(clusterDisconnectHandler, this.metadata.uid)
|
||||
});
|
||||
break;
|
||||
case "disconnected":
|
||||
case LensKubernetesClusterStatus.DISCONNECTED:
|
||||
context.menuItems.push({
|
||||
title: "Connect",
|
||||
icon: "link",
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -20,92 +20,108 @@
|
||||
*/
|
||||
|
||||
import { CustomResourceDefinition } from "../endpoints";
|
||||
import type { KubeObjectMetadata } from "../kube-object";
|
||||
|
||||
describe("Crds", () => {
|
||||
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({
|
||||
apiVersion: "foo",
|
||||
apiVersion: "apiextensions.k8s.io/v1",
|
||||
kind: "CustomResourceDefinition",
|
||||
metadata: {} as KubeObjectMetadata,
|
||||
metadata: {
|
||||
name: "foo",
|
||||
resourceVersion: "12345",
|
||||
uid: "12345",
|
||||
},
|
||||
spec: {
|
||||
versions: [
|
||||
{
|
||||
name: "123",
|
||||
served: false,
|
||||
storage: false,
|
||||
},
|
||||
{
|
||||
name: "1234",
|
||||
served: false,
|
||||
storage: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
crd.spec = {
|
||||
versions: [
|
||||
{
|
||||
name: "123",
|
||||
served: 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");
|
||||
});
|
||||
|
||||
it("should get the first version name from the list of versions (length 2)", () => {
|
||||
it("should should get the version that is both served and stored even with version field", () => {
|
||||
const crd = new CustomResourceDefinition({
|
||||
apiVersion: "foo",
|
||||
apiVersion: "apiextensions.k8s.io/v1",
|
||||
kind: "CustomResourceDefinition",
|
||||
metadata: {} as KubeObjectMetadata,
|
||||
metadata: {
|
||||
name: "foo",
|
||||
resourceVersion: "12345",
|
||||
uid: "12345",
|
||||
},
|
||||
spec: {
|
||||
version: "abc",
|
||||
versions: [
|
||||
{
|
||||
name: "123",
|
||||
served: true,
|
||||
storage: true,
|
||||
},
|
||||
{
|
||||
name: "1234",
|
||||
served: false,
|
||||
storage: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
crd.spec = {
|
||||
versions: [
|
||||
{
|
||||
name: "123",
|
||||
served: false,
|
||||
storage: false,
|
||||
},
|
||||
{
|
||||
name: "1234",
|
||||
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) even with version field", () => {
|
||||
it("should get the version name from the version field", () => {
|
||||
const crd = new CustomResourceDefinition({
|
||||
apiVersion: "foo",
|
||||
apiVersion: "apiextensions.k8s.io/v1beta1",
|
||||
kind: "CustomResourceDefinition",
|
||||
metadata: {} as KubeObjectMetadata,
|
||||
metadata: {
|
||||
name: "foo",
|
||||
resourceVersion: "12345",
|
||||
uid: "12345",
|
||||
},
|
||||
spec: {
|
||||
version: "abc",
|
||||
}
|
||||
});
|
||||
|
||||
crd.spec = {
|
||||
version: "abc",
|
||||
versions: [
|
||||
{
|
||||
name: "123",
|
||||
served: false,
|
||||
storage: false,
|
||||
},
|
||||
{
|
||||
name: "1234",
|
||||
served: false,
|
||||
storage: false,
|
||||
}
|
||||
]
|
||||
} as any;
|
||||
|
||||
expect(crd.getVersion()).toBe("123");
|
||||
});
|
||||
|
||||
it("should get the first version name from the version field", () => {
|
||||
const crd = new CustomResourceDefinition({
|
||||
apiVersion: "foo",
|
||||
kind: "CustomResourceDefinition",
|
||||
metadata: {} as KubeObjectMetadata,
|
||||
});
|
||||
|
||||
crd.spec = {
|
||||
version: "abc"
|
||||
} as any;
|
||||
|
||||
expect(crd.getVersion()).toBe("abc");
|
||||
});
|
||||
});
|
||||
|
||||
@ -19,10 +19,11 @@
|
||||
* 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 { crdResourcesURL } from "../../routes";
|
||||
import { isClusterPageContext } from "../../utils/cluster-id-url-parsing";
|
||||
import type { KubeJsonApiData } from "../kube-json-api";
|
||||
|
||||
type AdditionalPrinterColumnsCommon = {
|
||||
name: string;
|
||||
@ -39,10 +40,21 @@ type AdditionalPrinterColumnsV1Beta = AdditionalPrinterColumnsCommon & {
|
||||
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 {
|
||||
spec: {
|
||||
group: string;
|
||||
version?: string; // deprecated in v1 api
|
||||
/**
|
||||
* @deprecated for apiextensions.k8s.io/v1 but used previously
|
||||
*/
|
||||
version?: string;
|
||||
names: {
|
||||
plural: string;
|
||||
singular: string;
|
||||
@ -50,19 +62,19 @@ export interface CustomResourceDefinition {
|
||||
listKind: string;
|
||||
};
|
||||
scope: "Namespaced" | "Cluster" | string;
|
||||
validation?: any;
|
||||
versions?: {
|
||||
name: string;
|
||||
served: boolean;
|
||||
storage: boolean;
|
||||
schema?: unknown; // required in v1 but not present in v1beta
|
||||
additionalPrinterColumns?: AdditionalPrinterColumnsV1[]
|
||||
}[];
|
||||
/**
|
||||
* @deprecated for apiextensions.k8s.io/v1 but used previously
|
||||
*/
|
||||
validation?: object;
|
||||
versions?: CRDVersion[];
|
||||
conversion: {
|
||||
strategy?: string;
|
||||
webhook?: any;
|
||||
};
|
||||
additionalPrinterColumns?: AdditionalPrinterColumnsV1Beta[]; // removed in v1
|
||||
/**
|
||||
* @deprecated for apiextensions.k8s.io/v1 but used previously
|
||||
*/
|
||||
additionalPrinterColumns?: AdditionalPrinterColumnsV1Beta[];
|
||||
};
|
||||
status: {
|
||||
conditions: {
|
||||
@ -83,11 +95,23 @@ export interface CustomResourceDefinition {
|
||||
};
|
||||
}
|
||||
|
||||
export interface CRDApiData extends KubeJsonApiData {
|
||||
spec: object; // TODO: make better
|
||||
}
|
||||
|
||||
export class CustomResourceDefinition extends KubeObject {
|
||||
static kind = "CustomResourceDefinition";
|
||||
static namespaced = false;
|
||||
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() {
|
||||
return crdResourcesURL({
|
||||
params: {
|
||||
@ -125,9 +149,36 @@ export class CustomResourceDefinition extends KubeObject {
|
||||
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() {
|
||||
// v1 has removed the spec.version property, if it is present it must match the first version
|
||||
return this.spec.versions?.[0]?.name ?? this.spec.version;
|
||||
return this.getPreferedVersion().name;
|
||||
}
|
||||
|
||||
isNamespaced() {
|
||||
@ -147,17 +198,14 @@ export class CustomResourceDefinition extends KubeObject {
|
||||
}
|
||||
|
||||
getPrinterColumns(ignorePriority = true): AdditionalPrinterColumnsV1[] {
|
||||
const columns = this.spec.versions?.find(a => this.getVersion() == a.name)?.additionalPrinterColumns
|
||||
?? this.spec.additionalPrinterColumns?.map(({ JSONPath, ...rest }) => ({ ...rest, jsonPath: JSONPath })) // map to V1 shape
|
||||
?? [];
|
||||
const columns = this.getPreferedVersion().additionalPrinterColumns ?? [];
|
||||
|
||||
return columns
|
||||
.filter(column => column.name != "Age")
|
||||
.filter(column => ignorePriority ? true : !column.priority);
|
||||
.filter(column => column.name != "Age" && (ignorePriority || !column.priority));
|
||||
}
|
||||
|
||||
getValidation() {
|
||||
return JSON.stringify(this.spec.validation ?? this.spec.versions?.[0]?.schema, null, 2);
|
||||
return JSON.stringify(this.getPreferedVersion().schema, null, 2);
|
||||
}
|
||||
|
||||
getConditions() {
|
||||
|
||||
@ -56,6 +56,21 @@ export interface INodeMetrics<T = IMetrics> {
|
||||
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 {
|
||||
spec: {
|
||||
podCIDR?: string;
|
||||
@ -65,12 +80,7 @@ export interface Node {
|
||||
* @deprecated see https://issues.k8s.io/61966
|
||||
*/
|
||||
externalID?: string;
|
||||
taints?: {
|
||||
key: string;
|
||||
value: string;
|
||||
effect: string;
|
||||
timeAdded: string;
|
||||
}[];
|
||||
taints?: NodeTaint[];
|
||||
unschedulable?: boolean;
|
||||
};
|
||||
status: {
|
||||
|
||||
@ -27,8 +27,7 @@ import { stringify } from "querystring";
|
||||
import { EventEmitter } from "../../common/event-emitter";
|
||||
import logger from "../../common/logger";
|
||||
|
||||
export interface JsonApiData {
|
||||
}
|
||||
export interface JsonApiData {}
|
||||
|
||||
export interface JsonApiError {
|
||||
code?: number;
|
||||
|
||||
@ -19,24 +19,16 @@
|
||||
* 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 path from "path";
|
||||
import os from "os";
|
||||
import yaml from "js-yaml";
|
||||
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 { resolvePath } from "./utils";
|
||||
import Joi from "joi";
|
||||
|
||||
export type KubeConfigValidationOpts = {
|
||||
validateCluster?: boolean;
|
||||
validateUser?: boolean;
|
||||
validateExec?: boolean;
|
||||
};
|
||||
|
||||
export const kubeConfigDefaultPath = path.join(os.homedir(), ".kube", "config");
|
||||
|
||||
export function loadConfigFromFileSync(filePath: string): ConfigResult {
|
||||
@ -86,35 +78,34 @@ const contextSchema = Joi.object({
|
||||
}),
|
||||
});
|
||||
|
||||
const kubeConfigSchema = Joi
|
||||
.object({
|
||||
users: Joi
|
||||
.array()
|
||||
.items(userSchema)
|
||||
.optional(),
|
||||
clusters: Joi
|
||||
.array()
|
||||
.items(clusterSchema)
|
||||
.optional(),
|
||||
contexts: Joi
|
||||
.array()
|
||||
.items(contextSchema)
|
||||
.optional(),
|
||||
"current-context": Joi
|
||||
.string()
|
||||
.min(1)
|
||||
.optional(),
|
||||
})
|
||||
const kubeConfigSchema = Joi.object({
|
||||
users: Joi
|
||||
.array()
|
||||
.items(userSchema)
|
||||
.optional(),
|
||||
clusters: Joi
|
||||
.array()
|
||||
.items(clusterSchema)
|
||||
.optional(),
|
||||
contexts: Joi
|
||||
.array()
|
||||
.items(contextSchema)
|
||||
.optional(),
|
||||
"current-context": Joi
|
||||
.string()
|
||||
.min(1)
|
||||
.optional(),
|
||||
})
|
||||
.required();
|
||||
|
||||
export interface KubeConfigOptions {
|
||||
interface KubeConfigOptions {
|
||||
clusters: Cluster[];
|
||||
users: User[];
|
||||
contexts: Context[];
|
||||
currentContext?: string;
|
||||
}
|
||||
|
||||
export interface OptionsResult {
|
||||
interface OptionsResult {
|
||||
options: KubeConfigOptions;
|
||||
error: Joi.ValidationError;
|
||||
}
|
||||
@ -132,7 +123,12 @@ function loadToOptions(rawYaml: string): OptionsResult {
|
||||
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 users = newUsers(rawUsers);
|
||||
const contexts = newContexts(rawContexts);
|
||||
@ -175,66 +171,78 @@ export interface SplitConfigEntry {
|
||||
* Breaks kube config into several configs. Each context as it own KubeConfig object
|
||||
*/
|
||||
export function splitConfig(kubeConfig: KubeConfig): SplitConfigEntry[] {
|
||||
const { contexts = [] } = kubeConfig;
|
||||
|
||||
return contexts.map(context => {
|
||||
return kubeConfig.getContexts().map(ctx => {
|
||||
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);
|
||||
config.users = [kubeConfig.getUser(context.user)].filter(Boolean);
|
||||
config.contexts = [kubeConfig.getContextObject(context.name)].filter(Boolean);
|
||||
config.setCurrentContext(context.name);
|
||||
if (cluster) {
|
||||
config.addCluster(cluster);
|
||||
}
|
||||
|
||||
if (user) {
|
||||
config.addUser(user);
|
||||
}
|
||||
|
||||
if (context) {
|
||||
config.addContext(context);
|
||||
}
|
||||
|
||||
config.setCurrentContext(ctx.name);
|
||||
|
||||
return {
|
||||
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 {
|
||||
const clusters = kubeConfig.clusters.map(cluster => ({
|
||||
name: cluster.name,
|
||||
cluster: {
|
||||
"certificate-authority-data": cluster.caData,
|
||||
"certificate-authority": cluster.caFile,
|
||||
server: cluster.server,
|
||||
"insecure-skip-tls-verify": cluster.skipTLSVerify
|
||||
}
|
||||
}));
|
||||
const contexts = kubeConfig.contexts.map(context => ({
|
||||
name: context.name,
|
||||
context: {
|
||||
cluster: context.cluster,
|
||||
user: context.user,
|
||||
namespace: context.namespace
|
||||
}
|
||||
}));
|
||||
const users = kubeConfig.users.map(user => ({
|
||||
name: user.name,
|
||||
user: {
|
||||
"client-certificate-data": user.certData,
|
||||
"client-certificate": user.certFile,
|
||||
"client-key-data": user.keyData,
|
||||
"client-key": user.keyFile,
|
||||
"auth-provider": user.authProvider,
|
||||
exec: user.exec,
|
||||
token: user.token,
|
||||
username: user.username,
|
||||
password: user.password
|
||||
}
|
||||
}));
|
||||
const config = {
|
||||
apiVersion: "v1",
|
||||
kind: "Config",
|
||||
preferences: {},
|
||||
"current-context": kubeConfig.currentContext,
|
||||
clusters: kubeConfig.clusters.map(cluster => {
|
||||
return {
|
||||
name: cluster.name,
|
||||
cluster: {
|
||||
"certificate-authority-data": cluster.caData,
|
||||
"certificate-authority": cluster.caFile,
|
||||
server: cluster.server,
|
||||
"insecure-skip-tls-verify": cluster.skipTLSVerify
|
||||
}
|
||||
};
|
||||
}),
|
||||
contexts: kubeConfig.contexts.map(context => {
|
||||
return {
|
||||
name: context.name,
|
||||
context: {
|
||||
cluster: context.cluster,
|
||||
user: context.user,
|
||||
namespace: context.namespace
|
||||
}
|
||||
};
|
||||
}),
|
||||
users: kubeConfig.users.map(user => {
|
||||
return {
|
||||
name: user.name,
|
||||
user: {
|
||||
"client-certificate-data": user.certData,
|
||||
"client-certificate": user.certFile,
|
||||
"client-key-data": user.keyData,
|
||||
"client-key": user.keyFile,
|
||||
"auth-provider": user.authProvider,
|
||||
exec: user.exec,
|
||||
token: user.token,
|
||||
username: user.username,
|
||||
password: user.password
|
||||
}
|
||||
};
|
||||
})
|
||||
clusters,
|
||||
contexts,
|
||||
users,
|
||||
};
|
||||
|
||||
logger.debug("Dumping KubeConfig:", config);
|
||||
@ -243,70 +251,25 @@ export function dumpConfigYaml(kubeConfig: Partial<KubeConfig>): string {
|
||||
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)
|
||||
*
|
||||
* 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 {
|
||||
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
|
||||
export function validateKubeConfig(config: KubeConfig, contextName: string): Error | undefined {
|
||||
const contextObject = config.getContextObject(contextName);
|
||||
|
||||
const { validateUser = true, validateCluster = true, validateExec = true } = validationOpts;
|
||||
|
||||
const contextObject = config.getContextObject(contextName);
|
||||
|
||||
// Validate the Context Object
|
||||
if (!contextObject) {
|
||||
return new Error(`No valid context object provided in kubeconfig for context '${contextName}'`);
|
||||
}
|
||||
|
||||
// Validate the Cluster Object
|
||||
if (validateCluster && !config.getCluster(contextObject.cluster)) {
|
||||
return new Error(`No valid cluster object provided in kubeconfig for context '${contextName}'`);
|
||||
}
|
||||
|
||||
const user = config.getUser(contextObject.user);
|
||||
|
||||
// Validate the User Object
|
||||
if (validateUser && !user) {
|
||||
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;
|
||||
} catch (error) {
|
||||
return error;
|
||||
if (!contextObject) {
|
||||
return new Error(`No valid context object provided in kubeconfig for context '${contextName}'`);
|
||||
}
|
||||
|
||||
if (!config.getCluster(contextObject.cluster)) {
|
||||
return new Error(`No valid cluster object provided in kubeconfig for context '${contextName}'`);
|
||||
}
|
||||
|
||||
if (!config.getUser(contextObject.user)) {
|
||||
return new Error(`No valid user object provided in kubeconfig for context '${contextName}'`);
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
@ -23,9 +23,10 @@ import moment from "moment-timezone";
|
||||
import path from "path";
|
||||
import os from "os";
|
||||
import { ThemeStore } from "../../renderer/theme.store";
|
||||
import { ObservableToggleSet } from "../utils";
|
||||
import { getAppVersion, ObservableToggleSet } from "../utils";
|
||||
import type {monaco} from "react-monaco-editor";
|
||||
import merge from "lodash/merge";
|
||||
import { SemVer } from "semver";
|
||||
|
||||
export interface KubeconfigSyncEntry extends KubeconfigSyncValue {
|
||||
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 UserStoreModelType<field extends keyof typeof DESCRIPTORS> = typeof DESCRIPTORS[field] extends PreferenceDescription<any, infer T> ? T : never;
|
||||
|
||||
@ -288,4 +315,9 @@ export const DESCRIPTORS = {
|
||||
syncKubeconfigEntries,
|
||||
editorConfiguration,
|
||||
terminalCopyOnSelect,
|
||||
updateChannel,
|
||||
};
|
||||
|
||||
export const CONSTANTS = {
|
||||
updateChannels,
|
||||
};
|
||||
|
||||
@ -20,7 +20,7 @@
|
||||
*/
|
||||
|
||||
import { app, ipcMain } from "electron";
|
||||
import semver from "semver";
|
||||
import semver, { SemVer } from "semver";
|
||||
import { action, computed, observable, reaction, makeObservable } from "mobx";
|
||||
import { BaseStore } from "../base-store";
|
||||
import migrations from "../../migrations/user-store";
|
||||
@ -75,6 +75,7 @@ export class UserStore extends BaseStore<UserStoreModel> /* implements UserStore
|
||||
@observable downloadBinariesPath?: string;
|
||||
@observable kubectlBinariesPath?: string;
|
||||
@observable terminalCopyOnSelect: boolean;
|
||||
@observable updateChannel?: string;
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
|
||||
readonly isAllowedToDowngrade = new SemVer(getAppVersion()).prerelease[0] !== "latest";
|
||||
|
||||
startMainReactions() {
|
||||
// track telemetry availability
|
||||
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.editorConfiguration = DESCRIPTORS.editorConfiguration.fromStore(preferences?.editorConfiguration);
|
||||
this.terminalCopyOnSelect = DESCRIPTORS.terminalCopyOnSelect.fromStore(preferences?.terminalCopyOnSelect);
|
||||
this.updateChannel = DESCRIPTORS.updateChannel.fromStore(preferences?.updateChannel);
|
||||
}
|
||||
|
||||
toJSON(): UserStoreModel {
|
||||
@ -240,6 +244,7 @@ export class UserStore extends BaseStore<UserStoreModel> /* implements UserStore
|
||||
syncKubeconfigEntries: DESCRIPTORS.syncKubeconfigEntries.toStore(this.syncKubeconfigEntries),
|
||||
editorConfiguration: DESCRIPTORS.editorConfiguration.toStore(this.editorConfiguration),
|
||||
terminalCopyOnSelect: DESCRIPTORS.terminalCopyOnSelect.toStore(this.terminalCopyOnSelect),
|
||||
updateChannel: DESCRIPTORS.updateChannel.toStore(this.updateChannel),
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@ -19,5 +19,22 @@
|
||||
* 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";
|
||||
|
||||
@ -27,8 +27,8 @@ import { areArgsUpdateAvailableToBackchannel, AutoUpdateLogPrefix, broadcastMess
|
||||
import { once } from "lodash";
|
||||
import { ipcMain } from "electron";
|
||||
import { nextUpdateChannel } from "./utils/update-channel";
|
||||
import { UserStore } from "../common/user-store";
|
||||
|
||||
const updateChannel = autoUpdater.channel;
|
||||
let installVersion: null | string = null;
|
||||
|
||||
export function isAutoUpdateEnabled() {
|
||||
@ -58,9 +58,13 @@ export const startUpdateChecking = once(function (interval = 1000 * 60 * 60 * 24
|
||||
return;
|
||||
}
|
||||
|
||||
const userStore = UserStore.getInstance();
|
||||
|
||||
autoUpdater.logger = logger;
|
||||
autoUpdater.autoDownload = false;
|
||||
autoUpdater.autoInstallOnAppQuit = false;
|
||||
autoUpdater.channel = userStore.updateChannel;
|
||||
autoUpdater.allowDowngrade = userStore.isAllowedToDowngrade;
|
||||
|
||||
autoUpdater
|
||||
.on("update-available", (info: UpdateInfo) => {
|
||||
@ -104,7 +108,7 @@ export const startUpdateChecking = once(function (interval = 1000 * 60 * 60 * 24
|
||||
}
|
||||
})
|
||||
.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`);
|
||||
|
||||
|
||||
@ -27,7 +27,7 @@ import logger from "./logger";
|
||||
import { apiKubePrefix } from "../common/vars";
|
||||
import { getClusterIdFromHost, Singleton } from "../common/utils";
|
||||
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 { once } from "lodash";
|
||||
import { ClusterStore } from "../common/cluster-store";
|
||||
@ -35,6 +35,8 @@ import type { ClusterId } from "../common/cluster-types";
|
||||
|
||||
const logPrefix = "[CLUSTER-MANAGER]:";
|
||||
|
||||
const lensSpecificClusterStatuses: Set<string> = new Set(Object.values(LensKubernetesClusterStatus));
|
||||
|
||||
export class ClusterManager extends Singleton {
|
||||
private store = ClusterStore.getInstance();
|
||||
deleting = observable.set<ClusterId>();
|
||||
@ -90,6 +92,8 @@ export class ClusterManager extends Singleton {
|
||||
|
||||
@action
|
||||
protected updateCatalog(clusters: Cluster[]) {
|
||||
logger.debug("[CLUSTER-MANAGER]: updating catalog from cluster store");
|
||||
|
||||
for (const cluster of clusters) {
|
||||
this.updateEntityFromCluster(cluster);
|
||||
}
|
||||
@ -144,23 +148,28 @@ export class ClusterManager extends Singleton {
|
||||
@action
|
||||
protected updateEntityStatus(entity: KubernetesCluster, cluster?: Cluster) {
|
||||
if (this.deleting.has(entity.getId())) {
|
||||
entity.status.phase = "deleting";
|
||||
entity.status.phase = LensKubernetesClusterStatus.DELETING;
|
||||
entity.status.enabled = false;
|
||||
} else {
|
||||
entity.status.phase = ((): KubernetesClusterStatusPhase => {
|
||||
entity.status.phase = (() => {
|
||||
if (!cluster) {
|
||||
return "disconnected";
|
||||
return LensKubernetesClusterStatus.DISCONNECTED;
|
||||
}
|
||||
|
||||
if (cluster.accessible) {
|
||||
return "connected";
|
||||
return LensKubernetesClusterStatus.CONNECTED;
|
||||
}
|
||||
|
||||
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;
|
||||
@ -272,7 +281,9 @@ export function catalogEntityFromCluster(cluster: Cluster) {
|
||||
icon: {}
|
||||
},
|
||||
status: {
|
||||
phase: cluster.disconnected ? "disconnected" : "connected",
|
||||
phase: cluster.disconnected
|
||||
? LensKubernetesClusterStatus.DISCONNECTED
|
||||
: LensKubernetesClusterStatus.CONNECTED,
|
||||
reason: "",
|
||||
message: "",
|
||||
active: !cluster.disconnected
|
||||
|
||||
@ -394,7 +394,6 @@ export class Cluster implements ClusterModel, ClusterState {
|
||||
* @internal
|
||||
*/
|
||||
@action disconnect() {
|
||||
logger.info(`[CLUSTER]: disconnect`, this.getMeta());
|
||||
this.unbindEvents();
|
||||
this.contextHandler?.stopServer();
|
||||
this.disconnected = true;
|
||||
@ -405,6 +404,7 @@ export class Cluster implements ClusterModel, ClusterState {
|
||||
this.allowedNamespaces = [];
|
||||
this.resourceAccessStatuses.clear();
|
||||
this.pushState();
|
||||
logger.info(`[CLUSTER]: disconnect`, this.getMeta());
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@ -43,8 +43,8 @@ export async function listReleases(pathToKubeconfig: string, namespace?: string)
|
||||
});
|
||||
|
||||
return output;
|
||||
} catch ({ stderr }) {
|
||||
throw stderr;
|
||||
} catch (error) {
|
||||
throw error?.stderr || error;
|
||||
}
|
||||
}
|
||||
|
||||
@ -72,8 +72,8 @@ export async function installChart(chart: string, values: any, name: string | un
|
||||
namespace
|
||||
}
|
||||
};
|
||||
} catch ({ stderr }) {
|
||||
throw stderr;
|
||||
} catch (error) {
|
||||
throw error?.stderr || error;
|
||||
} finally {
|
||||
await fse.unlink(fileName);
|
||||
}
|
||||
@ -93,8 +93,8 @@ export async function upgradeRelease(name: string, chart: string, values: any, n
|
||||
log: stdout,
|
||||
release: getRelease(name, namespace, cluster)
|
||||
};
|
||||
} catch ({ stderr }) {
|
||||
throw stderr;
|
||||
} catch (error) {
|
||||
throw error?.stderr || error;
|
||||
} finally {
|
||||
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);
|
||||
|
||||
return release;
|
||||
} catch ({ stderr }) {
|
||||
throw stderr;
|
||||
} catch (error) {
|
||||
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}`);
|
||||
|
||||
return stdout;
|
||||
} catch ({ stderr }) {
|
||||
throw stderr;
|
||||
} catch (error) {
|
||||
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}`);
|
||||
|
||||
return stdout;
|
||||
} catch ({ stderr }) {
|
||||
throw stderr;
|
||||
} catch (error) {
|
||||
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}`);
|
||||
|
||||
return JSON.parse(stdout);
|
||||
} catch ({ stderr }) {
|
||||
throw stderr;
|
||||
} catch (error) {
|
||||
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}`);
|
||||
|
||||
return stdout;
|
||||
} catch ({ stderr }) {
|
||||
throw stderr;
|
||||
} catch (error) {
|
||||
throw error?.stderr || error;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -147,13 +147,18 @@ export function buildMenu(windowManager: WindowManager) {
|
||||
}
|
||||
}
|
||||
]),
|
||||
|
||||
{ type: "separator" },
|
||||
{
|
||||
role: "close",
|
||||
label: "Close Window"
|
||||
},
|
||||
|
||||
...(isMac ? [
|
||||
{
|
||||
role: "close",
|
||||
label: "Close Window",
|
||||
accelerator: "Shift+Cmd+W"
|
||||
}
|
||||
] as MenuItemConstructorOptions[] : []),
|
||||
|
||||
...ignoreOnMac([
|
||||
{ type: "separator" },
|
||||
{
|
||||
label: "Exit",
|
||||
accelerator: "Alt+F4",
|
||||
|
||||
@ -41,7 +41,7 @@ export class HelmApiRoute {
|
||||
|
||||
respondJson(response, chart);
|
||||
} catch (error) {
|
||||
respondText(response, error, 422);
|
||||
respondText(response, error?.toString() || "Error getting chart", 422);
|
||||
}
|
||||
}
|
||||
|
||||
@ -53,7 +53,7 @@ export class HelmApiRoute {
|
||||
|
||||
respondJson(response, values);
|
||||
} 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);
|
||||
} catch (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);
|
||||
} catch (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);
|
||||
} catch (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);
|
||||
} catch(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);
|
||||
} catch (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);
|
||||
} catch (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);
|
||||
} catch (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);
|
||||
} catch (error) {
|
||||
logger.debug(error);
|
||||
respondText(response, error, 422);
|
||||
respondText(response, error?.toString() || "Error deleting release", 422);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -33,17 +33,17 @@ describe("nextUpdateChannel", () => {
|
||||
expect(nextUpdateChannel("latest", "alpha")).toEqual("beta");
|
||||
});
|
||||
|
||||
it("returns rc if current channel is beta", () => {
|
||||
expect(nextUpdateChannel("alpha", "beta")).toEqual("rc");
|
||||
expect(nextUpdateChannel("beta", "beta")).toEqual("rc");
|
||||
expect(nextUpdateChannel("rc", "beta")).toEqual("rc");
|
||||
expect(nextUpdateChannel("latest", "beta")).toEqual("rc");
|
||||
it("returns latest if current channel is beta", () => {
|
||||
expect(nextUpdateChannel("alpha", "beta")).toEqual("latest");
|
||||
expect(nextUpdateChannel("beta", "beta")).toEqual("latest");
|
||||
expect(nextUpdateChannel("rc", "beta")).toEqual("latest");
|
||||
expect(nextUpdateChannel("latest", "beta")).toEqual("latest");
|
||||
});
|
||||
|
||||
it("returns latest if current channel is rc", () => {
|
||||
expect(nextUpdateChannel("alpha", "rc")).toEqual("latest");
|
||||
expect(nextUpdateChannel("beta", "rc")).toEqual("latest");
|
||||
expect(nextUpdateChannel("rc", "rc")).toEqual("latest");
|
||||
it("returns default if current channel is unknown", () => {
|
||||
expect(nextUpdateChannel("alpha", "rc")).toEqual("alpha");
|
||||
expect(nextUpdateChannel("beta", "rc")).toEqual("beta");
|
||||
expect(nextUpdateChannel("rc", "rc")).toEqual("rc");
|
||||
expect(nextUpdateChannel("latest", "rc")).toEqual("latest");
|
||||
});
|
||||
});
|
||||
|
||||
@ -19,14 +19,19 @@
|
||||
* 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 {
|
||||
if (channel === "alpha") {
|
||||
return "beta";
|
||||
} else if (channel === "beta") {
|
||||
return "rc";
|
||||
} else if (channel === "rc") {
|
||||
return "latest";
|
||||
} else {
|
||||
return defaultChannel;
|
||||
switch (channel) {
|
||||
case "alpha":
|
||||
return "beta";
|
||||
case "beta":
|
||||
return "latest"; // there is no RC currently
|
||||
default:
|
||||
return defaultChannel;
|
||||
}
|
||||
}
|
||||
|
||||
@ -23,8 +23,8 @@ import "./helm-chart-details.scss";
|
||||
|
||||
import React, { Component } from "react";
|
||||
import { getChartDetails, HelmChart } from "../../../common/k8s-api/endpoints/helm-charts.api";
|
||||
import { observable, autorun, makeObservable } from "mobx";
|
||||
import { observer } from "mobx-react";
|
||||
import { observable, makeObservable, reaction } from "mobx";
|
||||
import { disposeOnUnmount, observer } from "mobx-react";
|
||||
import { Drawer, DrawerItem } from "../drawer";
|
||||
import { boundMethod, stopPropagation } from "../../utils";
|
||||
import { MarkdownViewer } from "../markdown-viewer";
|
||||
@ -64,20 +64,24 @@ export class HelmChartDetails extends Component<Props> {
|
||||
this.abortController?.abort();
|
||||
}
|
||||
|
||||
chartUpdater = autorun(() => {
|
||||
this.selectedChart = null;
|
||||
const { chart: { name, repo, version } } = this.props;
|
||||
componentDidMount() {
|
||||
disposeOnUnmount(this, [
|
||||
reaction(() => this.props.chart, async ({ name, repo, version }) => {
|
||||
try {
|
||||
const { readme, versions } = await getChartDetails(repo, name, { version });
|
||||
|
||||
getChartDetails(repo, name, { version })
|
||||
.then(result => {
|
||||
this.readme = result.readme;
|
||||
this.chartVersions = result.versions;
|
||||
this.selectedChart = result.versions[0];
|
||||
})
|
||||
.catch(error => {
|
||||
this.error = error;
|
||||
});
|
||||
});
|
||||
this.readme = readme;
|
||||
this.chartVersions = versions;
|
||||
this.selectedChart = versions[0];
|
||||
} catch (error) {
|
||||
this.error = error;
|
||||
this.selectedChart = null;
|
||||
}
|
||||
}, {
|
||||
fireImmediately: true,
|
||||
}),
|
||||
]);
|
||||
}
|
||||
|
||||
@boundMethod
|
||||
async onVersionChange({ value: chart }: SelectOption<HelmChart>) {
|
||||
@ -135,7 +139,7 @@ export class HelmChartDetails extends Component<Props> {
|
||||
value: chart,
|
||||
}))}
|
||||
isOptionDisabled={({ value: chart }) => chart.deprecated}
|
||||
value={selectedChart.getVersion()}
|
||||
value={selectedChart}
|
||||
onChange={onVersionChange}
|
||||
/>
|
||||
</DrawerItem>
|
||||
@ -170,10 +174,6 @@ export class HelmChartDetails extends Component<Props> {
|
||||
}
|
||||
|
||||
renderContent() {
|
||||
if (!this.selectedChart) {
|
||||
return <Spinner center />;
|
||||
}
|
||||
|
||||
if (this.error) {
|
||||
return (
|
||||
<div className="box grow">
|
||||
@ -182,6 +182,10 @@ export class HelmChartDetails extends Component<Props> {
|
||||
);
|
||||
}
|
||||
|
||||
if (!this.selectedChart) {
|
||||
return <Spinner center />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="box grow">
|
||||
{this.renderIntroduction()}
|
||||
|
||||
@ -26,6 +26,11 @@
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.loading-error {
|
||||
color: red;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.status {
|
||||
@include release-status-bgs;
|
||||
}
|
||||
|
||||
@ -56,12 +56,13 @@ interface Props {
|
||||
|
||||
@observer
|
||||
export class ReleaseDetails extends Component<Props> {
|
||||
@observable details: IReleaseDetails;
|
||||
@observable details: IReleaseDetails | null = null;
|
||||
@observable values = "";
|
||||
@observable valuesLoading = false;
|
||||
@observable showOnlyUserSuppliedValues = true;
|
||||
@observable saving = false;
|
||||
@observable releaseSecret: Secret;
|
||||
@observable error?: string = undefined;
|
||||
|
||||
componentDidMount() {
|
||||
disposeOnUnmount(this, [
|
||||
@ -96,9 +97,13 @@ export class ReleaseDetails extends Component<Props> {
|
||||
|
||||
async loadDetails() {
|
||||
const { release } = this.props;
|
||||
|
||||
this.details = null;
|
||||
this.details = await getRelease(release.getName(), release.getNs());
|
||||
|
||||
try {
|
||||
this.details = null;
|
||||
this.details = await getRelease(release.getName(), release.getNs());
|
||||
} catch (error) {
|
||||
this.error = `Failed to get release details: ${error}`;
|
||||
}
|
||||
}
|
||||
|
||||
async loadValues() {
|
||||
@ -237,11 +242,18 @@ export class ReleaseDetails extends Component<Props> {
|
||||
|
||||
renderContent() {
|
||||
const { release } = this.props;
|
||||
const { details } = this;
|
||||
|
||||
if (!release) return null;
|
||||
|
||||
if (!details) {
|
||||
if (this.error) {
|
||||
return (
|
||||
<div className="loading-error">
|
||||
{this.error}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!this.details) {
|
||||
return <Spinner center/>;
|
||||
}
|
||||
|
||||
|
||||
@ -79,6 +79,7 @@ export class HelmReleaseMenu extends React.Component<Props> {
|
||||
{...menuProps}
|
||||
className={cssNames("HelmReleaseMenu", className)}
|
||||
removeAction={this.remove}
|
||||
removeConfirmationMessage={() => <p>Remove Helm Release <b>{release.name}</b>?</p>}
|
||||
>
|
||||
{this.renderContent()}
|
||||
</MenuActions>
|
||||
|
||||
@ -30,7 +30,7 @@ import { Badge } from "../badge";
|
||||
import { ResourceMetrics } from "../resource-metrics";
|
||||
import { podsStore } from "../+workloads-pods/pods.store";
|
||||
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 { makeObservable, observable, reaction } from "mobx";
|
||||
import { PodDetailsList } from "../+workloads-pods/pod-details-list";
|
||||
@ -132,11 +132,7 @@ export class NodeDetails extends React.Component<Props> {
|
||||
/>
|
||||
{taints.length > 0 && (
|
||||
<DrawerItem name="Taints" labelsOnly>
|
||||
{
|
||||
taints.map(({ key, effect, value }) => (
|
||||
<Badge key={key} label={`${key}=${value}:${effect}`} />
|
||||
))
|
||||
}
|
||||
{taints.map(taint => <Badge key={taint.key} label={formatNodeTaint(taint)} />)}
|
||||
</DrawerItem>
|
||||
)}
|
||||
{conditions &&
|
||||
|
||||
@ -27,7 +27,7 @@ import { cssNames, interval } from "../../utils";
|
||||
import { TabLayout } from "../layout/tab-layout";
|
||||
import { nodesStore } from "./nodes.store";
|
||||
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 { bytesToUnits } from "../../../common/utils/convertMemory";
|
||||
import { Tooltip, TooltipPosition } from "../tooltip";
|
||||
@ -227,6 +227,7 @@ export class Nodes extends React.Component<Props> {
|
||||
]}
|
||||
renderTableContents={node => {
|
||||
const tooltipId = `node-taints-${node.getId()}`;
|
||||
const taints = node.getTaints();
|
||||
|
||||
return [
|
||||
<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.renderDiskUsage(node),
|
||||
<>
|
||||
<span id={tooltipId}>{node.getTaints().length}</span>
|
||||
<span id={tooltipId}>{taints.length}</span>
|
||||
<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>
|
||||
</>,
|
||||
node.getRoleLabels(),
|
||||
|
||||
@ -29,11 +29,16 @@ import { Input } from "../input";
|
||||
import { isWindows } from "../../../common/vars";
|
||||
import { FormSwitch, Switcher } from "../switch";
|
||||
import moment from "moment-timezone";
|
||||
import { CONSTANTS } from "../../../common/user-store/preferences-helpers";
|
||||
|
||||
const timezoneOptions: SelectOption<string>[] = moment.tz.names().map(zone => ({
|
||||
label: zone,
|
||||
value: zone,
|
||||
}));
|
||||
const updateChannelOptions: SelectOption<string>[] = Array.from(
|
||||
CONSTANTS.updateChannels.entries(),
|
||||
([value, { label }]) => ({ value, label }),
|
||||
);
|
||||
|
||||
export const Application = observer(() => {
|
||||
const defaultShell = process.env.SHELL
|
||||
@ -104,6 +109,18 @@ export const Application = observer(() => {
|
||||
|
||||
<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">
|
||||
<SubTitle title="Locale Timezone" />
|
||||
<Select
|
||||
|
||||
@ -19,14 +19,23 @@
|
||||
* 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>;
|
||||
declare function commandExists(
|
||||
commandName: string,
|
||||
cb: (error: null, exists: boolean) => void
|
||||
): void;
|
||||
describe("formatNodeTaint tests", () => {
|
||||
it("should use value if defined", () => {
|
||||
expect(formatNodeTaint({
|
||||
effect: "Foo",
|
||||
key: "hello",
|
||||
timeAdded: "pre",
|
||||
value: "a"
|
||||
})).toBe("hello=a:Foo");
|
||||
});
|
||||
|
||||
declare namespace commandExists {
|
||||
function sync(commandName: string): boolean;
|
||||
}
|
||||
it("should not use value if not defined", () => {
|
||||
expect(formatNodeTaint({
|
||||
effect: "Foo",
|
||||
key: "hello",
|
||||
timeAdded: "pre",
|
||||
})).toBe("hello:Foo");
|
||||
});
|
||||
});
|
||||
@ -29,6 +29,7 @@ import { Tab, TabProps } from "../tabs";
|
||||
import { Icon } from "../icon";
|
||||
import { Menu, MenuItem } from "../menu";
|
||||
import { observable, makeObservable } from "mobx";
|
||||
import { isMac } from "../../../common/vars";
|
||||
|
||||
export interface DockTabProps extends TabProps<DockTabModel> {
|
||||
moreActions?: React.ReactNode;
|
||||
@ -94,7 +95,7 @@ export class DockTab extends React.Component<DockTabProps> {
|
||||
{!pinned && (
|
||||
<Icon
|
||||
small material="close"
|
||||
tooltip="Close (Ctrl+Shift+W)"
|
||||
tooltip={`Close ${isMac ? "⌘+W" : "Ctrl+W"}`}
|
||||
onClick={prevDefault(this.close)}
|
||||
/>
|
||||
)}
|
||||
|
||||
@ -27,6 +27,16 @@
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
&:not(:focus-within) .DockTab.active {
|
||||
&::after {
|
||||
color: var(--halfGray);
|
||||
}
|
||||
|
||||
&:hover::after {
|
||||
color: var(--line-color-active);
|
||||
}
|
||||
}
|
||||
|
||||
&.isOpen {
|
||||
&.fullSize {
|
||||
position: fixed;
|
||||
|
||||
@ -46,19 +46,31 @@ interface Props {
|
||||
|
||||
@observer
|
||||
export class Dock extends React.Component<Props> {
|
||||
onKeydown = (evt: React.KeyboardEvent<HTMLElement>) => {
|
||||
const { close, closeTab, selectedTab } = dockStore;
|
||||
private element = React.createRef<HTMLDivElement>();
|
||||
|
||||
if (!selectedTab) return;
|
||||
const { code, ctrlKey, shiftKey } = evt.nativeEvent;
|
||||
componentDidMount() {
|
||||
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") {
|
||||
close();
|
||||
}
|
||||
|
||||
if (ctrlKey && code === "KeyW") {
|
||||
if (selectedTab.pinned) close();
|
||||
else closeTab(selectedTab.id);
|
||||
if ((ctrlKey && code === "KeyW") || (metaKey && code === "KeyW")) {
|
||||
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();
|
||||
selectTab(tab.id);
|
||||
this.element?.current.focus();
|
||||
};
|
||||
|
||||
renderTab(tab: DockTab) {
|
||||
@ -105,7 +118,7 @@ export class Dock extends React.Component<Props> {
|
||||
return (
|
||||
<div
|
||||
className={cssNames("Dock", className, { isOpen, fullSize })}
|
||||
onKeyDown={this.onKeydown}
|
||||
ref={this.element}
|
||||
tabIndex={-1}
|
||||
>
|
||||
<ResizingAnchor
|
||||
|
||||
@ -222,7 +222,7 @@ export class Terminal {
|
||||
};
|
||||
|
||||
keyHandler = (evt: KeyboardEvent): boolean => {
|
||||
const { code, ctrlKey, type, metaKey } = evt;
|
||||
const { code, ctrlKey, metaKey } = evt;
|
||||
|
||||
// Handle custom hotkey bindings
|
||||
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;
|
||||
};
|
||||
}
|
||||
|
||||
@ -30,6 +30,7 @@ import { navigate } from "../../navigation";
|
||||
import { cssNames, IClassName } from "../../utils";
|
||||
import { Icon } from "../icon";
|
||||
import { HotbarIcon } from "./hotbar-icon";
|
||||
import { LensKubernetesClusterStatus } from "../../../common/catalog-entities/kubernetes-cluster";
|
||||
|
||||
interface Props extends DOMAttributes<HTMLElement> {
|
||||
entity: CatalogEntity;
|
||||
@ -77,7 +78,7 @@ export class HotbarEntityIcon extends React.Component<Props> {
|
||||
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} />;
|
||||
}
|
||||
|
||||
@ -193,7 +193,7 @@ export class Sidebar extends React.Component<Props> {
|
||||
source={metadata.source}
|
||||
src={spec.icon?.src}
|
||||
className="mr-5"
|
||||
onClick={() => navigate(routes.clusterURL())}
|
||||
onClick={() => navigate("/")}
|
||||
/>
|
||||
<div className={styles.clusterName}>
|
||||
{metadata.name}
|
||||
|
||||
@ -25,7 +25,7 @@
|
||||
grid-template-rows: var(--main-layout-header);
|
||||
grid-template-areas: "title controls";
|
||||
background-color: var(--layoutBackground);
|
||||
z-index: 1;
|
||||
z-index: 2;
|
||||
width: 100%;
|
||||
grid-area: topbar;
|
||||
}
|
||||
|
||||
@ -28,6 +28,7 @@ import { StorageHelper } from "./storageHelper";
|
||||
import { ClusterStore } from "../../common/cluster-store";
|
||||
import logger from "../../main/logger";
|
||||
import { getHostedClusterId, getPath } from "../../common/utils";
|
||||
import { isTestEnv } from "../../common/vars";
|
||||
|
||||
const storage = observable({
|
||||
initialized: false,
|
||||
@ -62,7 +63,9 @@ export function createAppStorage<T>(key: string, defaultValue: T, clusterId?: st
|
||||
.then(data => storage.data = data)
|
||||
.catch(() => null) // ignore empty / non-existing / invalid json files
|
||||
.finally(() => {
|
||||
logger.info(`${logPrefix} loading finished for ${filePath}`);
|
||||
if (!isTestEnv) {
|
||||
logger.info(`${logPrefix} loading finished for ${filePath}`);
|
||||
}
|
||||
storage.loaded = true;
|
||||
});
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user