mirror of
https://github.com/lensapp/lens.git
synced 2025-05-20 05:10:56 +00:00
Merge remote-tracking branch 'origin/master' into mobx-6.2
# Conflicts: # src/common/catalog/catalog-entity-registry.ts # src/common/user-store.ts # src/main/catalog-pusher.ts # src/main/cluster-manager.ts # src/main/cluster.ts # src/main/index.ts # src/renderer/bootstrap.tsx # src/renderer/kube-object.store.ts
This commit is contained in:
commit
04ea4120ed
4
Makefile
4
Makefile
@ -37,6 +37,10 @@ dev: binaries/client build-extensions static/build/LensDev.html
|
|||||||
lint:
|
lint:
|
||||||
yarn lint
|
yarn lint
|
||||||
|
|
||||||
|
.PHONY: release-version
|
||||||
|
release-version:
|
||||||
|
npm version $(CMD_ARGS) --git-tag-version false
|
||||||
|
|
||||||
.PHONY: test
|
.PHONY: test
|
||||||
test: binaries/client
|
test: binaries/client
|
||||||
yarn run jest $(or $(CMD_ARGS), "src")
|
yarn run jest $(or $(CMD_ARGS), "src")
|
||||||
|
|||||||
@ -53,6 +53,9 @@ export async function addMinikubeCluster(app: Application) {
|
|||||||
await app.client.click("div.Select__control"); // hide the context drop-down list (it might be obscuring the Add cluster(s) button)
|
await app.client.click("div.Select__control"); // hide the context drop-down list (it might be obscuring the Add cluster(s) button)
|
||||||
await app.client.click("button.primary"); // add minikube cluster
|
await app.client.click("button.primary"); // add minikube cluster
|
||||||
await app.client.waitUntilTextExists("div.TableCell", "minikube");
|
await app.client.waitUntilTextExists("div.TableCell", "minikube");
|
||||||
|
await app.client.waitForExist(".Input.SearchInput input");
|
||||||
|
await app.client.setValue(".Input.SearchInput input", "minikube");
|
||||||
|
await app.client.waitUntilTextExists("div.TableCell", "minikube");
|
||||||
await app.client.click("div.TableRow");
|
await app.client.click("div.TableRow");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
13
package.json
13
package.json
@ -2,7 +2,7 @@
|
|||||||
"name": "open-lens",
|
"name": "open-lens",
|
||||||
"productName": "OpenLens",
|
"productName": "OpenLens",
|
||||||
"description": "OpenLens - Open Source IDE for Kubernetes",
|
"description": "OpenLens - Open Source IDE for Kubernetes",
|
||||||
"version": "5.0.0-alpha.4",
|
"version": "5.0.0-beta.2",
|
||||||
"main": "static/build/main.js",
|
"main": "static/build/main.js",
|
||||||
"copyright": "© 2021 OpenLens Authors",
|
"copyright": "© 2021 OpenLens Authors",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
@ -39,7 +39,11 @@
|
|||||||
"lint:fix": "yarn run lint --fix",
|
"lint:fix": "yarn run lint --fix",
|
||||||
"mkdocs-serve-local": "docker build -t mkdocs-serve-local:latest mkdocs/ && docker run --rm -it -p 8000:8000 -v ${PWD}:/docs mkdocs-serve-local:latest",
|
"mkdocs-serve-local": "docker build -t mkdocs-serve-local:latest mkdocs/ && docker run --rm -it -p 8000:8000 -v ${PWD}:/docs mkdocs-serve-local:latest",
|
||||||
"verify-docs": "docker build -t mkdocs-serve-local:latest mkdocs/ && docker run --rm -v ${PWD}:/docs mkdocs-serve-local:latest build --strict",
|
"verify-docs": "docker build -t mkdocs-serve-local:latest mkdocs/ && docker run --rm -v ${PWD}:/docs mkdocs-serve-local:latest build --strict",
|
||||||
"typedocs-extensions-api": "yarn run typedoc --ignoreCompilerErrors --readme docs/extensions/typedoc-readme.md.tpl --name @k8slens/extensions --out docs/extensions/api --mode library --excludePrivate --hideBreadcrumbs --includes src/ src/extensions/extension-api.ts"
|
"typedocs-extensions-api": "yarn run typedoc --ignoreCompilerErrors --readme docs/extensions/typedoc-readme.md.tpl --name @k8slens/extensions --out docs/extensions/api --mode library --excludePrivate --hideBreadcrumbs --includes src/ src/extensions/extension-api.ts",
|
||||||
|
"version-checkout": "cat package.json | jq '.version' -r | xargs printf \"release/v%s\" | xargs git checkout -b",
|
||||||
|
"version-commit": "cat package.json | jq '.version' -r | xargs printf \"release v%s\" | git commit --no-edit -s -F -",
|
||||||
|
"version": "yarn run version-checkout && git add package.json && yarn run version-commit",
|
||||||
|
"postversion": "git push --set-upstream origin release/v$npm_package_version"
|
||||||
},
|
},
|
||||||
"config": {
|
"config": {
|
||||||
"bundledKubectlVersion": "1.18.15",
|
"bundledKubectlVersion": "1.18.15",
|
||||||
@ -233,6 +237,7 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@emeraldpay/hashicon-react": "^0.4.0",
|
"@emeraldpay/hashicon-react": "^0.4.0",
|
||||||
"@material-ui/core": "^4.10.1",
|
"@material-ui/core": "^4.10.1",
|
||||||
|
"@material-ui/icons": "^4.11.2",
|
||||||
"@material-ui/lab": "^4.0.0-alpha.57",
|
"@material-ui/lab": "^4.0.0-alpha.57",
|
||||||
"@pmmmwh/react-refresh-webpack-plugin": "^0.4.3",
|
"@pmmmwh/react-refresh-webpack-plugin": "^0.4.3",
|
||||||
"@testing-library/jest-dom": "^5.11.10",
|
"@testing-library/jest-dom": "^5.11.10",
|
||||||
@ -250,7 +255,7 @@
|
|||||||
"@types/hoist-non-react-statics": "^3.3.1",
|
"@types/hoist-non-react-statics": "^3.3.1",
|
||||||
"@types/html-webpack-plugin": "^3.2.3",
|
"@types/html-webpack-plugin": "^3.2.3",
|
||||||
"@types/http-proxy": "^1.17.5",
|
"@types/http-proxy": "^1.17.5",
|
||||||
"@types/jest": "^25.2.3",
|
"@types/jest": "^26.0.22",
|
||||||
"@types/js-yaml": "^3.12.4",
|
"@types/js-yaml": "^3.12.4",
|
||||||
"@types/jsdom": "^16.2.4",
|
"@types/jsdom": "^16.2.4",
|
||||||
"@types/jsonpath": "^0.2.0",
|
"@types/jsonpath": "^0.2.0",
|
||||||
@ -340,7 +345,7 @@
|
|||||||
"sharp": "^0.26.1",
|
"sharp": "^0.26.1",
|
||||||
"spectron": "11.0.0",
|
"spectron": "11.0.0",
|
||||||
"style-loader": "^1.2.1",
|
"style-loader": "^1.2.1",
|
||||||
"ts-jest": "^26.1.0",
|
"ts-jest": "26.3.0",
|
||||||
"ts-loader": "^7.0.5",
|
"ts-loader": "^7.0.5",
|
||||||
"ts-node": "^8.10.2",
|
"ts-node": "^8.10.2",
|
||||||
"type-fest": "^1.0.2",
|
"type-fest": "^1.0.2",
|
||||||
|
|||||||
@ -27,7 +27,7 @@ describe("CatalogEntityRegistry", () => {
|
|||||||
it ("allows to add an observable source", () => {
|
it ("allows to add an observable source", () => {
|
||||||
const source = observable.array([]);
|
const source = observable.array([]);
|
||||||
|
|
||||||
registry.addSource("test", source);
|
registry.addObservableSource("test", source);
|
||||||
expect(registry.items.length).toEqual(0);
|
expect(registry.items.length).toEqual(0);
|
||||||
|
|
||||||
source.push(entity);
|
source.push(entity);
|
||||||
@ -38,7 +38,7 @@ describe("CatalogEntityRegistry", () => {
|
|||||||
it ("added source change triggers reaction", (done) => {
|
it ("added source change triggers reaction", (done) => {
|
||||||
const source = observable.array([]);
|
const source = observable.array([]);
|
||||||
|
|
||||||
registry.addSource("test", source);
|
registry.addObservableSource("test", source);
|
||||||
reaction(() => registry.items, () => {
|
reaction(() => registry.items, () => {
|
||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
@ -51,7 +51,7 @@ describe("CatalogEntityRegistry", () => {
|
|||||||
it ("removes source", () => {
|
it ("removes source", () => {
|
||||||
const source = observable.array([]);
|
const source = observable.array([]);
|
||||||
|
|
||||||
registry.addSource("test", source);
|
registry.addObservableSource("test", source);
|
||||||
source.push(entity);
|
source.push(entity);
|
||||||
registry.removeSource("test");
|
registry.removeSource("test");
|
||||||
|
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import { KubeConfig } from "@kubernetes/client-node";
|
import { KubeConfig } from "@kubernetes/client-node";
|
||||||
import { validateKubeConfig, loadConfig } from "../kube-helpers";
|
import { validateKubeConfig, loadConfig, getNodeWarningConditions } from "../kube-helpers";
|
||||||
|
|
||||||
const kubeconfig = `
|
const kubeconfig = `
|
||||||
apiVersion: v1
|
apiVersion: v1
|
||||||
@ -72,55 +72,63 @@ describe("kube helpers", () => {
|
|||||||
});
|
});
|
||||||
describe("with default validation options", () => {
|
describe("with default validation options", () => {
|
||||||
describe("with valid kubeconfig", () => {
|
describe("with valid kubeconfig", () => {
|
||||||
it("does not raise exceptions", () => {
|
it("does not return an error", () => {
|
||||||
expect(() => { validateKubeConfig(kc, "valid");}).not.toThrow();
|
expect(validateKubeConfig(kc, "valid")).toBeUndefined();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
describe("with invalid context object", () => {
|
describe("with invalid context object", () => {
|
||||||
it("it raises exception", () => {
|
it("returns an error", () => {
|
||||||
expect(() => { validateKubeConfig(kc, "invalid");}).toThrow("No valid context object provided in kubeconfig for context 'invalid'");
|
expect(String(validateKubeConfig(kc, "invalid"))).toEqual(
|
||||||
|
expect.stringContaining("No valid context object provided in kubeconfig for context 'invalid'")
|
||||||
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("with invalid cluster object", () => {
|
describe("with invalid cluster object", () => {
|
||||||
it("it raises exception", () => {
|
it("returns an error", () => {
|
||||||
expect(() => { validateKubeConfig(kc, "invalidCluster");}).toThrow("No valid cluster object provided in kubeconfig for context 'invalidCluster'");
|
expect(String(validateKubeConfig(kc, "invalidCluster"))).toEqual(
|
||||||
|
expect.stringContaining("No valid cluster object provided in kubeconfig for context 'invalidCluster'")
|
||||||
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("with invalid user object", () => {
|
describe("with invalid user object", () => {
|
||||||
it("it raises exception", () => {
|
it("returns an error", () => {
|
||||||
expect(() => { validateKubeConfig(kc, "invalidUser");}).toThrow("No valid user object provided in kubeconfig for context 'invalidUser'");
|
expect(String(validateKubeConfig(kc, "invalidUser"))).toEqual(
|
||||||
|
expect.stringContaining("No valid user object provided in kubeconfig for context 'invalidUser'")
|
||||||
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("with invalid exec command", () => {
|
describe("with invalid exec command", () => {
|
||||||
it("it raises exception", () => {
|
it("returns an error", () => {
|
||||||
expect(() => { validateKubeConfig(kc, "invalidExec");}).toThrow("User Exec command \"foo\" not found on host. Please ensure binary is found in PATH or use absolute path to binary in Kubeconfig");
|
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 validateCluster as false", () => {
|
||||||
describe("with invalid cluster object", () => {
|
describe("with invalid cluster object", () => {
|
||||||
it("does not raise exception", () => {
|
it("does not return an error", () => {
|
||||||
expect(() => { validateKubeConfig(kc, "invalidCluster", { validateCluster: false });}).not.toThrow();
|
expect(validateKubeConfig(kc, "invalidCluster", { validateCluster: false })).toBeUndefined();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("with validateUser as false", () => {
|
describe("with validateUser as false", () => {
|
||||||
describe("with invalid user object", () => {
|
describe("with invalid user object", () => {
|
||||||
it("does not raise exceptions", () => {
|
it("does not return an error", () => {
|
||||||
expect(() => { validateKubeConfig(kc, "invalidUser", { validateUser: false });}).not.toThrow();
|
expect(validateKubeConfig(kc, "invalidUser", { validateUser: false })).toBeUndefined();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("with validateExec as false", () => {
|
describe("with validateExec as false", () => {
|
||||||
describe("with invalid exec object", () => {
|
describe("with invalid exec object", () => {
|
||||||
it("does not raise exceptions", () => {
|
it("does not return an error", () => {
|
||||||
expect(() => { validateKubeConfig(kc, "invalidExec", { validateExec: false });}).not.toThrow();
|
expect(validateKubeConfig(kc, "invalidExec", { validateExec: false })).toBeUndefined();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@ -251,4 +259,43 @@ 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");
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -19,12 +19,13 @@ import { UserStore } from "../user-store";
|
|||||||
import { SemVer } from "semver";
|
import { SemVer } from "semver";
|
||||||
import electron from "electron";
|
import electron from "electron";
|
||||||
import { stdout, stderr } from "process";
|
import { stdout, stderr } from "process";
|
||||||
|
import { beforeEachWrapped } from "../../../integration/helpers/utils";
|
||||||
|
|
||||||
console = new Console(stdout, stderr);
|
console = new Console(stdout, stderr);
|
||||||
|
|
||||||
describe("user store tests", () => {
|
describe("user store tests", () => {
|
||||||
describe("for an empty config", () => {
|
describe("for an empty config", () => {
|
||||||
beforeEach(() => {
|
beforeEachWrapped(() => {
|
||||||
UserStore.resetInstance();
|
UserStore.resetInstance();
|
||||||
mockFs({ tmp: { "config.json": "{}", "kube_config": "{}" } });
|
mockFs({ tmp: { "config.json": "{}", "kube_config": "{}" } });
|
||||||
|
|
||||||
@ -90,7 +91,7 @@ describe("user store tests", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe("migrations", () => {
|
describe("migrations", () => {
|
||||||
beforeEach(() => {
|
beforeEachWrapped(() => {
|
||||||
UserStore.resetInstance();
|
UserStore.resetInstance();
|
||||||
mockFs({
|
mockFs({
|
||||||
"tmp": {
|
"tmp": {
|
||||||
|
|||||||
@ -94,7 +94,7 @@ export class KubernetesClusterCategory extends CatalogCategory {
|
|||||||
ctx.menuItems.push({
|
ctx.menuItems.push({
|
||||||
icon: "text_snippet",
|
icon: "text_snippet",
|
||||||
title: "Add from kubeconfig",
|
title: "Add from kubeconfig",
|
||||||
onClick: async () => {
|
onClick: () => {
|
||||||
ctx.navigate("/add-cluster");
|
ctx.navigate("/add-cluster");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@ -1,15 +1,19 @@
|
|||||||
import { action, computed, IObservableArray, makeObservable, observable } from "mobx";
|
import { action, computed, observable, IComputedValue, IObservableArray, makeObservable } from "mobx";
|
||||||
import { CatalogEntity } from "./catalog-entity";
|
import { CatalogEntity } from "./catalog-entity";
|
||||||
import { toJS } from "../utils";
|
import { iter } from "../utils";
|
||||||
|
|
||||||
export class CatalogEntityRegistry {
|
export class CatalogEntityRegistry {
|
||||||
protected sources = observable.map<string, IObservableArray<CatalogEntity>>([], { deep: true });
|
protected sources = observable.map<string, IComputedValue<CatalogEntity[]>>([], { deep: true });
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
makeObservable(this);
|
makeObservable(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
@action addSource(id: string, source: IObservableArray<CatalogEntity>) {
|
@action addObservableSource(id: string, source: IObservableArray<CatalogEntity>) {
|
||||||
|
this.sources.set(id, computed(() => source));
|
||||||
|
}
|
||||||
|
|
||||||
|
@action addComputedSource(id: string, source: IComputedValue<CatalogEntity[]>) {
|
||||||
this.sources.set(id, source);
|
this.sources.set(id, source);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -18,7 +22,7 @@ export class CatalogEntityRegistry {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@computed get items(): CatalogEntity[] {
|
@computed get items(): CatalogEntity[] {
|
||||||
return toJS(Array.from(this.sources.values()).flat());
|
return Array.from(iter.flatMap(this.sources.values(), source => source.get()));
|
||||||
}
|
}
|
||||||
|
|
||||||
getItemsForApiKind<T extends CatalogEntity>(apiVersion: string, kind: string): T[] {
|
getItemsForApiKind<T extends CatalogEntity>(apiVersion: string, kind: string): T[] {
|
||||||
|
|||||||
@ -65,7 +65,7 @@ export interface CatalogEntityContextMenu {
|
|||||||
icon: string;
|
icon: string;
|
||||||
title: string;
|
title: string;
|
||||||
onlyVisibleForSource?: string; // show only if empty or if matches with entity source
|
onlyVisibleForSource?: string; // show only if empty or if matches with entity source
|
||||||
onClick: () => Promise<void>;
|
onClick: () => void | Promise<void>;
|
||||||
confirm?: {
|
confirm?: {
|
||||||
message: string;
|
message: string;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -37,6 +37,10 @@ export interface ClusterStoreModel {
|
|||||||
|
|
||||||
export type ClusterId = string;
|
export type ClusterId = string;
|
||||||
|
|
||||||
|
export interface UpdateClusterModel extends Omit<ClusterModel, "id"> {
|
||||||
|
id?: ClusterId;
|
||||||
|
}
|
||||||
|
|
||||||
export interface ClusterModel {
|
export interface ClusterModel {
|
||||||
/** Unique id for a cluster */
|
/** Unique id for a cluster */
|
||||||
id: ClusterId;
|
id: ClusterId;
|
||||||
@ -94,8 +98,12 @@ export interface ClusterPrometheusPreferences {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export class ClusterStore extends BaseStore<ClusterStoreModel> {
|
export class ClusterStore extends BaseStore<ClusterStoreModel> {
|
||||||
|
static get storedKubeConfigFolder(): string {
|
||||||
|
return path.resolve((app || remote.app).getPath("userData"), "kubeconfigs");
|
||||||
|
}
|
||||||
|
|
||||||
static getCustomKubeConfigPath(clusterId: ClusterId): string {
|
static getCustomKubeConfigPath(clusterId: ClusterId): string {
|
||||||
return path.resolve((app || remote.app).getPath("userData"), "kubeconfigs", clusterId);
|
return path.resolve(ClusterStore.storedKubeConfigFolder, clusterId);
|
||||||
}
|
}
|
||||||
|
|
||||||
static embedCustomKubeConfig(clusterId: ClusterId, kubeConfig: KubeConfig | string): string {
|
static embedCustomKubeConfig(clusterId: ClusterId, kubeConfig: KubeConfig | string): string {
|
||||||
@ -261,18 +269,18 @@ export class ClusterStore extends BaseStore<ClusterStoreModel> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@action
|
@action
|
||||||
addCluster(model: ClusterModel | Cluster): Cluster {
|
addCluster(clusterOrModel: ClusterModel | Cluster): Cluster {
|
||||||
appEventBus.emit({ name: "cluster", action: "add" });
|
appEventBus.emit({ name: "cluster", action: "add" });
|
||||||
let cluster = model as Cluster;
|
|
||||||
|
|
||||||
if (!(model instanceof Cluster)) {
|
const cluster = clusterOrModel instanceof Cluster
|
||||||
cluster = new Cluster(model);
|
? clusterOrModel
|
||||||
}
|
: new Cluster(clusterOrModel);
|
||||||
|
|
||||||
if (!cluster.isManaged) {
|
if (!cluster.isManaged) {
|
||||||
cluster.enabled = true;
|
cluster.enabled = true;
|
||||||
}
|
}
|
||||||
this.clusters.set(model.id, cluster);
|
|
||||||
|
this.clusters.set(cluster.id, cluster);
|
||||||
|
|
||||||
return cluster;
|
return cluster;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -161,18 +161,6 @@ export class HotbarStore extends BaseStore<HotbarStoreModel> {
|
|||||||
hotbar.items[index] = null;
|
hotbar.items[index] = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
addEmptyCell() {
|
|
||||||
const hotbar = this.getActive();
|
|
||||||
|
|
||||||
hotbar.items.push(null);
|
|
||||||
}
|
|
||||||
|
|
||||||
removeEmptyCell(index: number) {
|
|
||||||
const hotbar = this.getActive();
|
|
||||||
|
|
||||||
hotbar.items.splice(index, 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
switchToPrevious() {
|
switchToPrevious() {
|
||||||
const hotbarStore = HotbarStore.getInstance();
|
const hotbarStore = HotbarStore.getInstance();
|
||||||
let index = hotbarStore.activeHotbarIndex - 1;
|
let index = hotbarStore.activeHotbarIndex - 1;
|
||||||
|
|||||||
@ -6,7 +6,7 @@ import yaml from "js-yaml";
|
|||||||
import logger from "../main/logger";
|
import logger from "../main/logger";
|
||||||
import commandExists from "command-exists";
|
import commandExists from "command-exists";
|
||||||
import { ExecValidationNotFoundError } from "./custom-errors";
|
import { ExecValidationNotFoundError } from "./custom-errors";
|
||||||
import { newClusters, newContexts, newUsers } from "@kubernetes/client-node/dist/config_types";
|
import { Cluster, Context, newClusters, newContexts, newUsers, User } from "@kubernetes/client-node/dist/config_types";
|
||||||
|
|
||||||
export type KubeConfigValidationOpts = {
|
export type KubeConfigValidationOpts = {
|
||||||
validateCluster?: boolean;
|
validateCluster?: boolean;
|
||||||
@ -28,11 +28,26 @@ function readResolvedPathSync(filePath: string): string {
|
|||||||
return fse.readFileSync(path.resolve(resolveTilde(filePath)), "utf8");
|
return fse.readFileSync(path.resolve(resolveTilde(filePath)), "utf8");
|
||||||
}
|
}
|
||||||
|
|
||||||
function checkRawContext(rawContext: any): boolean {
|
function checkRawCluster(rawCluster: any): boolean {
|
||||||
return rawContext.name && rawContext.context?.cluster && rawContext.context?.user;
|
return Boolean(rawCluster?.name && rawCluster?.cluster?.server);
|
||||||
}
|
}
|
||||||
|
|
||||||
function loadToOptions(rawYaml: string): any {
|
function checkRawUser(rawUser: any): boolean {
|
||||||
|
return Boolean(rawUser?.name);
|
||||||
|
}
|
||||||
|
|
||||||
|
function checkRawContext(rawContext: any): boolean {
|
||||||
|
return Boolean(rawContext.name && rawContext.context?.cluster && rawContext.context?.user);
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface KubeConfigOptions {
|
||||||
|
clusters: Cluster[];
|
||||||
|
users: User[];
|
||||||
|
contexts: Context[];
|
||||||
|
currentContext: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadToOptions(rawYaml: string): KubeConfigOptions {
|
||||||
const obj = yaml.safeLoad(rawYaml);
|
const obj = yaml.safeLoad(rawYaml);
|
||||||
|
|
||||||
if (typeof obj !== "object" || !obj) {
|
if (typeof obj !== "object" || !obj) {
|
||||||
@ -40,16 +55,14 @@ function loadToOptions(rawYaml: string): any {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const { clusters: rawClusters, users: rawUsers, contexts: rawContexts, "current-context": currentContext } = obj;
|
const { clusters: rawClusters, users: rawUsers, contexts: rawContexts, "current-context": currentContext } = obj;
|
||||||
const clusters = newClusters(rawClusters);
|
const clusters = newClusters(rawClusters?.filter(checkRawCluster));
|
||||||
const users = newUsers(rawUsers);
|
const users = newUsers(rawUsers?.filter(checkRawUser));
|
||||||
const contexts = newContexts(rawContexts?.filter(checkRawContext));
|
const contexts = newContexts(rawContexts?.filter(checkRawContext));
|
||||||
|
|
||||||
return { clusters, users, contexts, currentContext };
|
return { clusters, users, contexts, currentContext };
|
||||||
}
|
}
|
||||||
|
|
||||||
export function loadConfig(pathOrContent?: string): KubeConfig {
|
export function loadFromOptions(options: KubeConfigOptions): KubeConfig {
|
||||||
const content = fse.pathExistsSync(pathOrContent) ? readResolvedPathSync(pathOrContent) : pathOrContent;
|
|
||||||
const options = loadToOptions(content);
|
|
||||||
const kc = new KubeConfig();
|
const kc = new KubeConfig();
|
||||||
|
|
||||||
// need to load using the kubernetes client to generate a kubeconfig object
|
// need to load using the kubernetes client to generate a kubeconfig object
|
||||||
@ -58,6 +71,18 @@ export function loadConfig(pathOrContent?: string): KubeConfig {
|
|||||||
return kc;
|
return kc;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function loadConfig(pathOrContent?: string): KubeConfig {
|
||||||
|
return loadConfigFromString(
|
||||||
|
fse.pathExistsSync(pathOrContent)
|
||||||
|
? readResolvedPathSync(pathOrContent)
|
||||||
|
: pathOrContent
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function loadConfigFromString(content: string): KubeConfig {
|
||||||
|
return loadFromOptions(loadToOptions(content));
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* KubeConfig is valid when there's at least one of each defined:
|
* KubeConfig is valid when there's at least one of each defined:
|
||||||
* - User
|
* - User
|
||||||
@ -174,49 +199,54 @@ export function podHasIssues(pod: V1Pod) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function getNodeWarningConditions(node: V1Node) {
|
export function getNodeWarningConditions(node: V1Node) {
|
||||||
return node.status.conditions.filter(c =>
|
return node.status?.conditions?.filter(c =>
|
||||||
c.status.toLowerCase() === "true" && c.type !== "Ready" && c.type !== "HostUpgrades"
|
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
|
||||||
*/
|
*/
|
||||||
export function validateKubeConfig (config: KubeConfig, contextName: string, validationOpts: KubeConfigValidationOpts = {}) {
|
export function validateKubeConfig(config: KubeConfig, contextName: string, validationOpts: KubeConfigValidationOpts = {}): Error | undefined {
|
||||||
// we only receive a single context, cluster & user object here so lets validate them as this
|
try {
|
||||||
// will be called when we add a new cluster to Lens
|
// 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 { validateUser = true, validateCluster = true, validateExec = true } = validationOpts;
|
||||||
|
|
||||||
const contextObject = config.getContextObject(contextName);
|
const contextObject = config.getContextObject(contextName);
|
||||||
|
|
||||||
// Validate the Context Object
|
// Validate the Context Object
|
||||||
if (!contextObject) {
|
if (!contextObject) {
|
||||||
throw 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 (validateCluster && !config.getCluster(contextObject.cluster)) {
|
|
||||||
throw 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) {
|
|
||||||
throw 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)) {
|
|
||||||
logger.debug(`validateKubeConfig: exec command ${String(execCommand)} in kubeconfig ${contextName} not found`);
|
|
||||||
throw new ExecValidationNotFoundError(execCommand, isAbsolute);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
return error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -11,6 +11,7 @@ import { kubeConfigDefaultPath, loadConfig } from "./kube-helpers";
|
|||||||
import { appEventBus } from "./event-bus";
|
import { appEventBus } from "./event-bus";
|
||||||
import logger from "../main/logger";
|
import logger from "../main/logger";
|
||||||
import path from "path";
|
import path from "path";
|
||||||
|
import os from "os";
|
||||||
import { fileNameMigration } from "../migrations/user-store";
|
import { fileNameMigration } from "../migrations/user-store";
|
||||||
import { ObservableToggleSet } from "../renderer/utils";
|
import { ObservableToggleSet } from "../renderer/utils";
|
||||||
|
|
||||||
@ -21,6 +22,12 @@ export interface UserStoreModel {
|
|||||||
preferences: UserPreferencesModel;
|
preferences: UserPreferencesModel;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface KubeconfigSyncEntry extends KubeconfigSyncValue {
|
||||||
|
filePath: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface KubeconfigSyncValue {}
|
||||||
|
|
||||||
export interface UserPreferencesModel {
|
export interface UserPreferencesModel {
|
||||||
httpsProxy?: string;
|
httpsProxy?: string;
|
||||||
shell?: string;
|
shell?: string;
|
||||||
@ -34,6 +41,7 @@ export interface UserPreferencesModel {
|
|||||||
kubectlBinariesPath?: string;
|
kubectlBinariesPath?: string;
|
||||||
openAtLogin?: boolean;
|
openAtLogin?: boolean;
|
||||||
hiddenTableColumns?: [string, string[]][];
|
hiddenTableColumns?: [string, string[]][];
|
||||||
|
syncKubeconfigEntries?: KubeconfigSyncEntry[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export class UserStore extends BaseStore<UserStoreModel> {
|
export class UserStore extends BaseStore<UserStoreModel> {
|
||||||
@ -44,9 +52,7 @@ export class UserStore extends BaseStore<UserStoreModel> {
|
|||||||
configName: "lens-user-store",
|
configName: "lens-user-store",
|
||||||
migrations,
|
migrations,
|
||||||
});
|
});
|
||||||
|
|
||||||
makeObservable(this);
|
makeObservable(this);
|
||||||
this.handleOnLoad();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@observable lastSeenAppVersion = "0.0.0";
|
@observable lastSeenAppVersion = "0.0.0";
|
||||||
@ -72,14 +78,31 @@ export class UserStore extends BaseStore<UserStoreModel> {
|
|||||||
*/
|
*/
|
||||||
@observable downloadKubectlBinaries = true;
|
@observable downloadKubectlBinaries = true;
|
||||||
@observable openAtLogin = false;
|
@observable openAtLogin = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The column IDs under each configurable table ID that have been configured
|
||||||
|
* to not be shown
|
||||||
|
*/
|
||||||
hiddenTableColumns = observable.map<string, ObservableToggleSet<string>>();
|
hiddenTableColumns = observable.map<string, ObservableToggleSet<string>>();
|
||||||
|
|
||||||
protected async handleOnLoad() {
|
/**
|
||||||
await this.whenLoaded;
|
* The set of file/folder paths to be synced
|
||||||
|
*/
|
||||||
|
syncKubeconfigEntries = observable.map<string, KubeconfigSyncValue>([
|
||||||
|
[path.join(os.homedir(), ".kube"), {}]
|
||||||
|
]);
|
||||||
|
|
||||||
|
async load(): Promise<void> {
|
||||||
|
/**
|
||||||
|
* This has to be here before the call to `new Config` in `super.load()`
|
||||||
|
* as we have to make sure that file is in the expected place for that call
|
||||||
|
*/
|
||||||
|
await fileNameMigration();
|
||||||
|
await super.load();
|
||||||
|
|
||||||
// refresh new contexts
|
// refresh new contexts
|
||||||
this.refreshNewContexts();
|
await this.refreshNewContexts();
|
||||||
reaction(() => this.kubeConfigPath, this.refreshNewContexts);
|
reaction(() => this.kubeConfigPath, () => this.refreshNewContexts());
|
||||||
|
|
||||||
if (app) {
|
if (app) {
|
||||||
// track telemetry availability
|
// track telemetry availability
|
||||||
@ -100,16 +123,6 @@ export class UserStore extends BaseStore<UserStoreModel> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async load(): Promise<void> {
|
|
||||||
/**
|
|
||||||
* This has to be here before the call to `new Config` in `super.load()`
|
|
||||||
* as we have to make sure that file is in the expected place for that call
|
|
||||||
*/
|
|
||||||
await fileNameMigration();
|
|
||||||
|
|
||||||
return super.load();
|
|
||||||
}
|
|
||||||
|
|
||||||
@computed get isNewVersion() {
|
@computed get isNewVersion() {
|
||||||
return semver.gt(getAppVersion(), this.lastSeenAppVersion);
|
return semver.gt(getAppVersion(), this.lastSeenAppVersion);
|
||||||
}
|
}
|
||||||
@ -172,7 +185,7 @@ export class UserStore extends BaseStore<UserStoreModel> {
|
|||||||
this.localeTimezone = tz;
|
this.localeTimezone = tz;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected refreshNewContexts = async () => {
|
protected async refreshNewContexts() {
|
||||||
try {
|
try {
|
||||||
const kubeConfig = await readFile(this.kubeConfigPath, "utf8");
|
const kubeConfig = await readFile(this.kubeConfigPath, "utf8");
|
||||||
|
|
||||||
@ -187,7 +200,7 @@ export class UserStore extends BaseStore<UserStoreModel> {
|
|||||||
logger.error(err);
|
logger.error(err);
|
||||||
this.resetKubeConfigPath();
|
this.resetKubeConfigPath();
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
@action
|
@action
|
||||||
markNewContextsAsSeen() {
|
markNewContextsAsSeen() {
|
||||||
@ -226,37 +239,50 @@ export class UserStore extends BaseStore<UserStoreModel> {
|
|||||||
this.kubectlBinariesPath = preferences.kubectlBinariesPath;
|
this.kubectlBinariesPath = preferences.kubectlBinariesPath;
|
||||||
this.openAtLogin = preferences.openAtLogin;
|
this.openAtLogin = preferences.openAtLogin;
|
||||||
|
|
||||||
this.hiddenTableColumns.clear();
|
if (preferences.hiddenTableColumns) {
|
||||||
|
this.hiddenTableColumns.replace(
|
||||||
|
preferences.hiddenTableColumns
|
||||||
|
.map(([tableId, columnIds]) => [tableId, new ObservableToggleSet(columnIds)])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
for (const [tableId, columnIds] of preferences.hiddenTableColumns ?? []) {
|
if (preferences.syncKubeconfigEntries) {
|
||||||
this.hiddenTableColumns.set(tableId, new ObservableToggleSet(columnIds));
|
this.syncKubeconfigEntries.replace(
|
||||||
|
preferences.syncKubeconfigEntries.map(({ filePath, ...rest }) => [filePath, rest])
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
toJSON(): UserStoreModel {
|
toJSON(): UserStoreModel {
|
||||||
const hiddenTableColumns: [string, string[]][] = [];
|
const hiddenTableColumns: [string, string[]][] = [];
|
||||||
|
const syncKubeconfigEntries: KubeconfigSyncEntry[] = [];
|
||||||
|
|
||||||
for (const [key, values] of this.hiddenTableColumns.entries()) {
|
for (const [key, values] of this.hiddenTableColumns.entries()) {
|
||||||
hiddenTableColumns.push([key, Array.from(values)]);
|
hiddenTableColumns.push([key, Array.from(values)]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for (const [filePath, rest] of this.syncKubeconfigEntries) {
|
||||||
|
syncKubeconfigEntries.push({ filePath, ...rest });
|
||||||
|
}
|
||||||
|
|
||||||
const model: UserStoreModel = {
|
const model: UserStoreModel = {
|
||||||
kubeConfigPath: this.kubeConfigPath,
|
kubeConfigPath: this.kubeConfigPath,
|
||||||
lastSeenAppVersion: this.lastSeenAppVersion,
|
lastSeenAppVersion: this.lastSeenAppVersion,
|
||||||
seenContexts: Array.from(this.seenContexts),
|
seenContexts: Array.from(this.seenContexts),
|
||||||
preferences: {
|
preferences: {
|
||||||
httpsProxy: this.httpsProxy,
|
httpsProxy: toJS(this.httpsProxy),
|
||||||
shell: this.shell,
|
shell: toJS(this.shell),
|
||||||
colorTheme: this.colorTheme,
|
colorTheme: toJS(this.colorTheme),
|
||||||
localeTimezone: this.localeTimezone,
|
localeTimezone: toJS(this.localeTimezone),
|
||||||
allowUntrustedCAs: this.allowUntrustedCAs,
|
allowUntrustedCAs: toJS(this.allowUntrustedCAs),
|
||||||
allowTelemetry: this.allowTelemetry,
|
allowTelemetry: toJS(this.allowTelemetry),
|
||||||
downloadMirror: this.downloadMirror,
|
downloadMirror: toJS(this.downloadMirror),
|
||||||
downloadKubectlBinaries: this.downloadKubectlBinaries,
|
downloadKubectlBinaries: toJS(this.downloadKubectlBinaries),
|
||||||
downloadBinariesPath: this.downloadBinariesPath,
|
downloadBinariesPath: toJS(this.downloadBinariesPath),
|
||||||
kubectlBinariesPath: this.kubectlBinariesPath,
|
kubectlBinariesPath: toJS(this.kubectlBinariesPath),
|
||||||
openAtLogin: this.openAtLogin,
|
openAtLogin: toJS(this.openAtLogin),
|
||||||
hiddenTableColumns,
|
hiddenTableColumns,
|
||||||
|
syncKubeconfigEntries,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
64
src/common/utils/extended-map.ts
Normal file
64
src/common/utils/extended-map.ts
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
import { action, IEnhancer, IObservableMapInitialValues, ObservableMap } from "mobx";
|
||||||
|
|
||||||
|
export class ExtendedMap<K, V> extends Map<K, V> {
|
||||||
|
constructor(protected getDefault: () => V, entries?: readonly (readonly [K, V])[] | null) {
|
||||||
|
super(entries);
|
||||||
|
}
|
||||||
|
|
||||||
|
getOrInsert(key: K, val: V): V {
|
||||||
|
if (this.has(key)) {
|
||||||
|
return this.get(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.set(key, val).get(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
getOrInsertWith(key: K, getVal: () => V): V {
|
||||||
|
if (this.has(key)) {
|
||||||
|
return this.get(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.set(key, getVal()).get(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
getOrDefault(key: K): V {
|
||||||
|
if (this.has(key)) {
|
||||||
|
return this.get(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.set(key, this.getDefault()).get(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ExtendedObservableMap<K, V> extends ObservableMap<K, V> {
|
||||||
|
constructor(protected getDefault: () => V, initialData?: IObservableMapInitialValues<K, V>, enhancer?: IEnhancer<V>, name?: string) {
|
||||||
|
super(initialData, enhancer, name);
|
||||||
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
getOrInsert(key: K, val: V): V {
|
||||||
|
if (this.has(key)) {
|
||||||
|
return this.get(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.set(key, val).get(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
getOrInsertWith(key: K, getVal: () => V): V {
|
||||||
|
if (this.has(key)) {
|
||||||
|
return this.get(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.set(key, getVal()).get(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
getOrDefault(key: K): V {
|
||||||
|
if (this.has(key)) {
|
||||||
|
return this.get(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.set(key, this.getDefault()).get(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -12,10 +12,13 @@ export * from "./debouncePromise";
|
|||||||
export * from "./defineGlobal";
|
export * from "./defineGlobal";
|
||||||
export * from "./delay";
|
export * from "./delay";
|
||||||
export * from "./disposer";
|
export * from "./disposer";
|
||||||
|
export * from "./disposer";
|
||||||
export * from "./downloadFile";
|
export * from "./downloadFile";
|
||||||
export * from "./escapeRegExp";
|
export * from "./escapeRegExp";
|
||||||
|
export * from "./extended-map";
|
||||||
export * from "./getRandId";
|
export * from "./getRandId";
|
||||||
export * from "./openExternal";
|
export * from "./openExternal";
|
||||||
|
export * from "./reject-promise";
|
||||||
export * from "./saveToAppFiles";
|
export * from "./saveToAppFiles";
|
||||||
export * from "./singleton";
|
export * from "./singleton";
|
||||||
export * from "./splitArray";
|
export * from "./splitArray";
|
||||||
|
|||||||
@ -23,3 +23,67 @@ export function* take<T>(src: Iterable<T>, n: number): Iterable<T> {
|
|||||||
break outer;
|
break outer;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new iterator that iterates (lazily) over its input and yields the
|
||||||
|
* result of `fn` for each item.
|
||||||
|
* @param src A type that can be iterated over
|
||||||
|
* @param fn The function that is called for each value
|
||||||
|
*/
|
||||||
|
export function* map<T, U>(src: Iterable<T>, fn: (from: T) => U): Iterable<U> {
|
||||||
|
for (const from of src) {
|
||||||
|
yield fn(from);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function* flatMap<T, U>(src: Iterable<T>, fn: (from: T) => Iterable<U>): Iterable<U> {
|
||||||
|
for (const from of src) {
|
||||||
|
yield* fn(from);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new iterator that iterates (lazily) over its input and yields the
|
||||||
|
* items that return a `truthy` value from `fn`.
|
||||||
|
* @param src A type that can be iterated over
|
||||||
|
* @param fn The function that is called for each value
|
||||||
|
*/
|
||||||
|
export function* filter<T>(src: Iterable<T>, fn: (from: T) => any): Iterable<T> {
|
||||||
|
for (const from of src) {
|
||||||
|
if (fn(from)) {
|
||||||
|
yield from;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new iterator that iterates (lazily) over its input and yields the
|
||||||
|
* result of `fn` when it is `truthy`
|
||||||
|
* @param src A type that can be iterated over
|
||||||
|
* @param fn The function that is called for each value
|
||||||
|
*/
|
||||||
|
export function* filterMap<T, U>(src: Iterable<T>, fn: (from: T) => U): Iterable<U> {
|
||||||
|
for (const from of src) {
|
||||||
|
const res = fn(from);
|
||||||
|
|
||||||
|
if (res) {
|
||||||
|
yield res;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new iterator that iterates (lazily) over its input and yields the
|
||||||
|
* result of `fn` when it is not null or undefined
|
||||||
|
* @param src A type that can be iterated over
|
||||||
|
* @param fn The function that is called for each value
|
||||||
|
*/
|
||||||
|
export function* filterMapStrict<T, U>(src: Iterable<T>, fn: (from: T) => U): Iterable<U> {
|
||||||
|
for (const from of src) {
|
||||||
|
const res = fn(from);
|
||||||
|
|
||||||
|
if (res != null) {
|
||||||
|
yield res;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
13
src/common/utils/reject-promise.ts
Normal file
13
src/common/utils/reject-promise.ts
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import "abort-controller/polyfill";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new promise that will be rejected when the signal rejects.
|
||||||
|
*
|
||||||
|
* Useful for `Promise.race()` applications.
|
||||||
|
* @param signal The AbortController's signal to reject with
|
||||||
|
*/
|
||||||
|
export function rejectPromiseBy(signal: AbortSignal): Promise<void> {
|
||||||
|
return new Promise((_, reject) => {
|
||||||
|
signal.addEventListener("abort", reject);
|
||||||
|
});
|
||||||
|
}
|
||||||
@ -42,6 +42,10 @@ interface ExtensionDiscoveryChannelMessage {
|
|||||||
*/
|
*/
|
||||||
const isDirectoryLike = (lstat: fse.Stats) => lstat.isDirectory() || lstat.isSymbolicLink();
|
const isDirectoryLike = (lstat: fse.Stats) => lstat.isDirectory() || lstat.isSymbolicLink();
|
||||||
|
|
||||||
|
interface LoadFromFolderOptions {
|
||||||
|
isBundled?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Discovers installed bundled and local extensions from the filesystem.
|
* Discovers installed bundled and local extensions from the filesystem.
|
||||||
* Also watches for added and removed local extensions by watching the directory.
|
* Also watches for added and removed local extensions by watching the directory.
|
||||||
@ -332,7 +336,12 @@ export class ExtensionDiscovery extends Singleton {
|
|||||||
isEnabled
|
isEnabled
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(`${logModule}: can't load extension manifest at ${manifestPath}: ${error}`);
|
if (error.code === "ENOTDIR") {
|
||||||
|
// ignore this error, probably from .DS_Store file
|
||||||
|
logger.debug(`${logModule}: failed to load extension manifest through a not-dir-like at ${manifestPath}`);
|
||||||
|
} else {
|
||||||
|
logger.error(`${logModule}: can't load extension manifest at ${manifestPath}: ${error}`);
|
||||||
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@ -429,12 +438,10 @@ export class ExtensionDiscovery extends Singleton {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Loads extension from absolute path, updates this.packagesJson to include it and returns the extension.
|
* Loads extension from absolute path, updates this.packagesJson to include it and returns the extension.
|
||||||
* @param absPath Folder path to extension
|
* @param folderPath Folder path to extension
|
||||||
*/
|
*/
|
||||||
async loadExtensionFromFolder(absPath: string, { isBundled = false }: {
|
async loadExtensionFromFolder(folderPath: string, { isBundled = false }: LoadFromFolderOptions = {}): Promise<InstalledExtension | null> {
|
||||||
isBundled?: boolean;
|
const manifestPath = path.resolve(folderPath, manifestFilename);
|
||||||
} = {}): Promise<InstalledExtension | null> {
|
|
||||||
const manifestPath = path.resolve(absPath, manifestFilename);
|
|
||||||
|
|
||||||
return this.getByManifest(manifestPath, { isBundled });
|
return this.getByManifest(manifestPath, { isBundled });
|
||||||
}
|
}
|
||||||
|
|||||||
@ -20,7 +20,7 @@ export class LensMainExtension extends LensExtension {
|
|||||||
}
|
}
|
||||||
|
|
||||||
addCatalogSource(id: string, source: IObservableArray<CatalogEntity>) {
|
addCatalogSource(id: string, source: IObservableArray<CatalogEntity>) {
|
||||||
catalogEntityRegistry.addSource(`${this.name}:${id}`, source);
|
catalogEntityRegistry.addObservableSource(`${this.name}:${id}`, source);
|
||||||
}
|
}
|
||||||
|
|
||||||
removeCatalogSource(id: string) {
|
removeCatalogSource(id: string) {
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import { autorun } from "mobx";
|
import { reaction } from "mobx";
|
||||||
import { toJS } from "../common/utils";
|
import { toJS, Disposer } from "../common/utils";
|
||||||
import { broadcastMessage, subscribeToBroadcast, unsubscribeFromBroadcast } from "../common/ipc";
|
import { broadcastMessage, subscribeToBroadcast, unsubscribeFromBroadcast } from "../common/ipc";
|
||||||
import { CatalogEntityRegistry } from "../common/catalog";
|
import { CatalogEntityRegistry} from "../common/catalog";
|
||||||
import "../common/catalog-entities/kubernetes-cluster";
|
import "../common/catalog-entities/kubernetes-cluster";
|
||||||
|
|
||||||
export class CatalogPusher {
|
export class CatalogPusher {
|
||||||
@ -9,26 +9,23 @@ export class CatalogPusher {
|
|||||||
new CatalogPusher(catalog).init();
|
new CatalogPusher(catalog).init();
|
||||||
}
|
}
|
||||||
|
|
||||||
private constructor(private catalog: CatalogEntityRegistry) {
|
private constructor(private catalog: CatalogEntityRegistry) {}
|
||||||
}
|
|
||||||
|
|
||||||
init() {
|
init() {
|
||||||
const disposers: { (): void; }[] = [];
|
const disposers: Disposer[] = [];
|
||||||
|
|
||||||
disposers.push(autorun(() => {
|
disposers.push(reaction(() => this.catalog.items, (items) => {
|
||||||
this.broadcast();
|
broadcastMessage("catalog:items", toJS(items, { recurseEverything: true }));
|
||||||
|
}, {
|
||||||
|
fireImmediately: true,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const listener = subscribeToBroadcast("catalog:broadcast", () => {
|
const listener = subscribeToBroadcast("catalog:broadcast", () => {
|
||||||
this.broadcast();
|
broadcastMessage("catalog:items", toJS(this.catalog.items, { recurseEverything: true }));
|
||||||
});
|
});
|
||||||
|
|
||||||
disposers.push(() => unsubscribeFromBroadcast("catalog:broadcast", listener));
|
disposers.push(() => unsubscribeFromBroadcast("catalog:broadcast", listener));
|
||||||
|
|
||||||
return disposers;
|
return disposers;
|
||||||
}
|
}
|
||||||
|
|
||||||
broadcast() {
|
|
||||||
broadcastMessage("catalog:items", toJS(this.catalog.items));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
252
src/main/catalog-sources/__test__/kubeconfig-sync.test.ts
Normal file
252
src/main/catalog-sources/__test__/kubeconfig-sync.test.ts
Normal file
@ -0,0 +1,252 @@
|
|||||||
|
import { ObservableMap } from "mobx";
|
||||||
|
import { CatalogEntity } from "../../../common/catalog";
|
||||||
|
import { loadFromOptions } from "../../../common/kube-helpers";
|
||||||
|
import { Cluster } from "../../cluster";
|
||||||
|
import { computeDiff, configToModels } from "../kubeconfig-sync";
|
||||||
|
import mockFs from "mock-fs";
|
||||||
|
import fs from "fs";
|
||||||
|
|
||||||
|
describe("kubeconfig-sync.source tests", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
mockFs();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
mockFs.restore();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("configsToModels", () => {
|
||||||
|
it("should filter out invalid split configs", () => {
|
||||||
|
const config = loadFromOptions({
|
||||||
|
clusters: [],
|
||||||
|
users: [],
|
||||||
|
contexts: [],
|
||||||
|
currentContext: "foobar"
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(configToModels(config, "").length).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should keep a single valid split config", () => {
|
||||||
|
const config = loadFromOptions({
|
||||||
|
clusters: [{
|
||||||
|
name: "cluster-name",
|
||||||
|
server: "1.2.3.4",
|
||||||
|
skipTLSVerify: false,
|
||||||
|
}],
|
||||||
|
users: [{
|
||||||
|
name: "user-name",
|
||||||
|
}],
|
||||||
|
contexts: [{
|
||||||
|
cluster: "cluster-name",
|
||||||
|
name: "context-name",
|
||||||
|
user: "user-name",
|
||||||
|
}],
|
||||||
|
currentContext: "foobar"
|
||||||
|
});
|
||||||
|
|
||||||
|
const models = configToModels(config, "/bar");
|
||||||
|
|
||||||
|
expect(models.length).toBe(1);
|
||||||
|
expect(models[0].contextName).toBe("context-name");
|
||||||
|
expect(models[0].kubeConfigPath).toBe("/bar");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("computeDiff", () => {
|
||||||
|
it("should leave an empty source empty if there are no entries", () => {
|
||||||
|
const contents = "";
|
||||||
|
const rootSource = new ObservableMap<string, [Cluster, CatalogEntity]>();
|
||||||
|
const port = 0;
|
||||||
|
const filePath = "/bar";
|
||||||
|
|
||||||
|
computeDiff(contents, rootSource, port, filePath);
|
||||||
|
|
||||||
|
expect(rootSource.size).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should add only the valid clusters to the source", () => {
|
||||||
|
const contents = JSON.stringify({
|
||||||
|
clusters: [{
|
||||||
|
name: "cluster-name",
|
||||||
|
cluster: {
|
||||||
|
server: "1.2.3.4",
|
||||||
|
},
|
||||||
|
skipTLSVerify: false,
|
||||||
|
}],
|
||||||
|
users: [{
|
||||||
|
name: "user-name",
|
||||||
|
}],
|
||||||
|
contexts: [{
|
||||||
|
name: "context-name",
|
||||||
|
context: {
|
||||||
|
cluster: "cluster-name",
|
||||||
|
user: "user-name",
|
||||||
|
},
|
||||||
|
}, {
|
||||||
|
name: "context-the-second",
|
||||||
|
context: {
|
||||||
|
cluster: "missing-cluster",
|
||||||
|
user: "user-name",
|
||||||
|
},
|
||||||
|
}],
|
||||||
|
currentContext: "foobar"
|
||||||
|
});
|
||||||
|
const rootSource = new ObservableMap<string, [Cluster, CatalogEntity]>();
|
||||||
|
const port = 0;
|
||||||
|
const filePath = "/bar";
|
||||||
|
|
||||||
|
fs.writeFileSync(filePath, contents);
|
||||||
|
|
||||||
|
computeDiff(contents, rootSource, port, filePath);
|
||||||
|
|
||||||
|
expect(rootSource.size).toBe(1);
|
||||||
|
|
||||||
|
const c = rootSource.values().next().value[0] as Cluster;
|
||||||
|
|
||||||
|
expect(c.kubeConfigPath).toBe("/bar");
|
||||||
|
expect(c.contextName).toBe("context-name");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should remove a cluster when it is removed from the contents", () => {
|
||||||
|
const contents = JSON.stringify({
|
||||||
|
clusters: [{
|
||||||
|
name: "cluster-name",
|
||||||
|
cluster: {
|
||||||
|
server: "1.2.3.4",
|
||||||
|
},
|
||||||
|
skipTLSVerify: false,
|
||||||
|
}],
|
||||||
|
users: [{
|
||||||
|
name: "user-name",
|
||||||
|
|
||||||
|
}],
|
||||||
|
contexts: [{
|
||||||
|
name: "context-name",
|
||||||
|
context: {
|
||||||
|
cluster: "cluster-name",
|
||||||
|
user: "user-name",
|
||||||
|
},
|
||||||
|
}, {
|
||||||
|
name: "context-the-second",
|
||||||
|
context: {
|
||||||
|
cluster: "missing-cluster",
|
||||||
|
user: "user-name",
|
||||||
|
},
|
||||||
|
}],
|
||||||
|
currentContext: "foobar"
|
||||||
|
});
|
||||||
|
const rootSource = new ObservableMap<string, [Cluster, CatalogEntity]>();
|
||||||
|
const port = 0;
|
||||||
|
const filePath = "/bar";
|
||||||
|
|
||||||
|
fs.writeFileSync(filePath, contents);
|
||||||
|
|
||||||
|
computeDiff(contents, rootSource, port, filePath);
|
||||||
|
|
||||||
|
expect(rootSource.size).toBe(1);
|
||||||
|
|
||||||
|
const c = rootSource.values().next().value[0] as Cluster;
|
||||||
|
|
||||||
|
expect(c.kubeConfigPath).toBe("/bar");
|
||||||
|
expect(c.contextName).toBe("context-name");
|
||||||
|
|
||||||
|
computeDiff("{}", rootSource, port, filePath);
|
||||||
|
|
||||||
|
expect(rootSource.size).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should remove only the cluster that it is removed from the contents", () => {
|
||||||
|
const contents = JSON.stringify({
|
||||||
|
clusters: [{
|
||||||
|
name: "cluster-name",
|
||||||
|
cluster: {
|
||||||
|
server: "1.2.3.4",
|
||||||
|
},
|
||||||
|
skipTLSVerify: false,
|
||||||
|
}],
|
||||||
|
users: [{
|
||||||
|
name: "user-name",
|
||||||
|
}, {
|
||||||
|
name: "user-name-2",
|
||||||
|
}],
|
||||||
|
contexts: [{
|
||||||
|
name: "context-name",
|
||||||
|
context: {
|
||||||
|
cluster: "cluster-name",
|
||||||
|
user: "user-name",
|
||||||
|
},
|
||||||
|
}, {
|
||||||
|
name: "context-name-2",
|
||||||
|
context: {
|
||||||
|
cluster: "cluster-name",
|
||||||
|
user: "user-name-2",
|
||||||
|
},
|
||||||
|
}, {
|
||||||
|
name: "context-the-second",
|
||||||
|
context: {
|
||||||
|
cluster: "missing-cluster",
|
||||||
|
user: "user-name",
|
||||||
|
},
|
||||||
|
}],
|
||||||
|
currentContext: "foobar"
|
||||||
|
});
|
||||||
|
const rootSource = new ObservableMap<string, [Cluster, CatalogEntity]>();
|
||||||
|
const port = 0;
|
||||||
|
const filePath = "/bar";
|
||||||
|
|
||||||
|
fs.writeFileSync(filePath, contents);
|
||||||
|
|
||||||
|
computeDiff(contents, rootSource, port, filePath);
|
||||||
|
|
||||||
|
expect(rootSource.size).toBe(2);
|
||||||
|
|
||||||
|
{
|
||||||
|
const c = rootSource.values().next().value[0] as Cluster;
|
||||||
|
|
||||||
|
expect(c.kubeConfigPath).toBe("/bar");
|
||||||
|
expect(["context-name", "context-name-2"].includes(c.contextName)).toBe(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
const newContents = JSON.stringify({
|
||||||
|
clusters: [{
|
||||||
|
name: "cluster-name",
|
||||||
|
cluster: {
|
||||||
|
server: "1.2.3.4",
|
||||||
|
},
|
||||||
|
skipTLSVerify: false,
|
||||||
|
}],
|
||||||
|
users: [{
|
||||||
|
name: "user-name",
|
||||||
|
}, {
|
||||||
|
name: "user-name-2",
|
||||||
|
}],
|
||||||
|
contexts: [{
|
||||||
|
name: "context-name",
|
||||||
|
context: {
|
||||||
|
cluster: "cluster-name",
|
||||||
|
user: "user-name",
|
||||||
|
},
|
||||||
|
}, {
|
||||||
|
name: "context-the-second",
|
||||||
|
context: {
|
||||||
|
cluster: "missing-cluster",
|
||||||
|
user: "user-name",
|
||||||
|
},
|
||||||
|
}],
|
||||||
|
currentContext: "foobar"
|
||||||
|
});
|
||||||
|
|
||||||
|
computeDiff(newContents, rootSource, port, filePath);
|
||||||
|
|
||||||
|
expect(rootSource.size).toBe(1);
|
||||||
|
|
||||||
|
{
|
||||||
|
const c = rootSource.values().next().value[0] as Cluster;
|
||||||
|
|
||||||
|
expect(c.kubeConfigPath).toBe("/bar");
|
||||||
|
expect(c.contextName).toBe("context-name");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
1
src/main/catalog-sources/index.ts
Normal file
1
src/main/catalog-sources/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export { KubeconfigSyncManager } from "./kubeconfig-sync";
|
||||||
251
src/main/catalog-sources/kubeconfig-sync.ts
Normal file
251
src/main/catalog-sources/kubeconfig-sync.ts
Normal file
@ -0,0 +1,251 @@
|
|||||||
|
import { action, observable, IComputedValue, computed, ObservableMap, runInAction } from "mobx";
|
||||||
|
import { CatalogEntity, catalogEntityRegistry } from "../../common/catalog";
|
||||||
|
import { watch } from "chokidar";
|
||||||
|
import fs from "fs";
|
||||||
|
import fse from "fs-extra";
|
||||||
|
import * as uuid from "uuid";
|
||||||
|
import stream from "stream";
|
||||||
|
import { Disposer, ExtendedObservableMap, iter, Singleton } from "../../common/utils";
|
||||||
|
import logger from "../logger";
|
||||||
|
import { KubeConfig } from "@kubernetes/client-node";
|
||||||
|
import { loadConfigFromString, splitConfig, validateKubeConfig } from "../../common/kube-helpers";
|
||||||
|
import { Cluster } from "../cluster";
|
||||||
|
import { catalogEntityFromCluster } from "../cluster-manager";
|
||||||
|
import { UserStore } from "../../common/user-store";
|
||||||
|
import { ClusterStore, UpdateClusterModel } from "../../common/cluster-store";
|
||||||
|
|
||||||
|
const logPrefix = "[KUBECONFIG-SYNC]:";
|
||||||
|
|
||||||
|
export class KubeconfigSyncManager extends Singleton {
|
||||||
|
protected sources = observable.map<string, [IComputedValue<CatalogEntity[]>, Disposer]>();
|
||||||
|
protected syncing = false;
|
||||||
|
protected syncListDisposer?: Disposer;
|
||||||
|
|
||||||
|
protected static readonly syncName = "lens:kube-sync";
|
||||||
|
|
||||||
|
@action
|
||||||
|
startSync(port: number): void {
|
||||||
|
if (this.syncing) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.syncing = true;
|
||||||
|
|
||||||
|
logger.info(`${logPrefix} starting requested syncs`);
|
||||||
|
|
||||||
|
catalogEntityRegistry.addComputedSource(KubeconfigSyncManager.syncName, computed(() => (
|
||||||
|
Array.from(iter.flatMap(
|
||||||
|
this.sources.values(),
|
||||||
|
([entities]) => entities.get()
|
||||||
|
))
|
||||||
|
)));
|
||||||
|
|
||||||
|
// This must be done so that c&p-ed clusters are visible
|
||||||
|
this.startNewSync(ClusterStore.storedKubeConfigFolder, port);
|
||||||
|
|
||||||
|
for (const filePath of UserStore.getInstance().syncKubeconfigEntries.keys()) {
|
||||||
|
this.startNewSync(filePath, port);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.syncListDisposer = UserStore.getInstance().syncKubeconfigEntries.observe(change => {
|
||||||
|
switch (change.type) {
|
||||||
|
case "add":
|
||||||
|
this.startNewSync(change.name, port);
|
||||||
|
break;
|
||||||
|
case "delete":
|
||||||
|
this.stopOldSync(change.name);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
stopSync() {
|
||||||
|
this.syncListDisposer?.();
|
||||||
|
|
||||||
|
for (const filePath of this.sources.keys()) {
|
||||||
|
this.stopOldSync(filePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
catalogEntityRegistry.removeSource(KubeconfigSyncManager.syncName);
|
||||||
|
this.syncing = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
protected async startNewSync(filePath: string, port: number): Promise<void> {
|
||||||
|
if (this.sources.has(filePath)) {
|
||||||
|
// don't start a new sync if we already have one
|
||||||
|
return void logger.debug(`${logPrefix} already syncing file/folder`, { filePath });
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
this.sources.set(filePath, await watchFileChanges(filePath, port));
|
||||||
|
|
||||||
|
logger.info(`${logPrefix} starting sync of file/folder`, { filePath });
|
||||||
|
logger.debug(`${logPrefix} ${this.sources.size} files/folders watched`, { files: Array.from(this.sources.keys()) });
|
||||||
|
} catch (error) {
|
||||||
|
logger.warn(`${logPrefix} failed to start watching changes: ${error}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
protected stopOldSync(filePath: string): void {
|
||||||
|
if (!this.sources.delete(filePath)) {
|
||||||
|
// already stopped
|
||||||
|
return void logger.debug(`${logPrefix} no syncing file/folder to stop`, { filePath });
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info(`${logPrefix} stopping sync of file/folder`, { filePath });
|
||||||
|
logger.debug(`${logPrefix} ${this.sources.size} files/folders watched`, { files: Array.from(this.sources.keys()) });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// exported for testing
|
||||||
|
export function configToModels(config: KubeConfig, filePath: string): UpdateClusterModel[] {
|
||||||
|
const validConfigs = [];
|
||||||
|
|
||||||
|
for (const contextConfig of splitConfig(config)) {
|
||||||
|
const error = validateKubeConfig(contextConfig, contextConfig.currentContext);
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
logger.debug(`${logPrefix} context failed validation: ${error}`, { context: contextConfig.currentContext, filePath });
|
||||||
|
} else {
|
||||||
|
validConfigs.push({
|
||||||
|
kubeConfigPath: filePath,
|
||||||
|
contextName: contextConfig.currentContext,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return validConfigs;
|
||||||
|
}
|
||||||
|
|
||||||
|
type RootSourceValue = [Cluster, CatalogEntity];
|
||||||
|
type RootSource = ObservableMap<string, RootSourceValue>;
|
||||||
|
|
||||||
|
// exported for testing
|
||||||
|
export function computeDiff(contents: string, source: RootSource, port: number, filePath: string): void {
|
||||||
|
runInAction(() => {
|
||||||
|
try {
|
||||||
|
const rawModels = configToModels(loadConfigFromString(contents), filePath);
|
||||||
|
const models = new Map(rawModels.map(m => [m.contextName, m]));
|
||||||
|
|
||||||
|
logger.debug(`${logPrefix} File now has ${models.size} entries`, { filePath });
|
||||||
|
|
||||||
|
for (const [contextName, value] of source) {
|
||||||
|
const model = models.get(contextName);
|
||||||
|
|
||||||
|
// remove and disconnect clusters that were removed from the config
|
||||||
|
if (!model) {
|
||||||
|
value[0].disconnect();
|
||||||
|
source.delete(contextName);
|
||||||
|
logger.debug(`${logPrefix} Removed old cluster from sync`, { filePath, contextName });
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: For the update check we need to make sure that the config itself hasn't changed.
|
||||||
|
// Probably should make it so that cluster keeps a copy of the config in its memory and
|
||||||
|
// diff against that
|
||||||
|
|
||||||
|
// or update the model and mark it as not needed to be added
|
||||||
|
value[0].updateModel(model);
|
||||||
|
models.delete(contextName);
|
||||||
|
logger.debug(`${logPrefix} Updated old cluster from sync`, { filePath, contextName });
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const [contextName, model] of models) {
|
||||||
|
// add new clusters to the source
|
||||||
|
try {
|
||||||
|
const cluster = new Cluster({ ...model, id: uuid.v4() });
|
||||||
|
|
||||||
|
if (!cluster.apiUrl) {
|
||||||
|
throw new Error("Cluster constructor failed, see above error");
|
||||||
|
}
|
||||||
|
|
||||||
|
cluster.init(port);
|
||||||
|
|
||||||
|
const entity = catalogEntityFromCluster(cluster);
|
||||||
|
|
||||||
|
entity.metadata.labels.file = filePath;
|
||||||
|
source.set(contextName, [cluster, entity]);
|
||||||
|
|
||||||
|
logger.debug(`${logPrefix} Added new cluster from sync`, { filePath, contextName });
|
||||||
|
} catch (error) {
|
||||||
|
logger.warn(`${logPrefix} Failed to create cluster from model: ${error}`, { filePath, contextName });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logger.warn(`${logPrefix} Failed to compute diff: ${error}`, { filePath });
|
||||||
|
source.clear(); // clear source if we have failed so as to not show outdated information
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function diffChangedConfig(filePath: string, source: RootSource, port: number): Disposer {
|
||||||
|
logger.debug(`${logPrefix} file changed`, { filePath });
|
||||||
|
|
||||||
|
// TODO: replace with an AbortController with fs.readFile when we upgrade to Node 16 (after it comes out)
|
||||||
|
const fileReader = fs.createReadStream(filePath, {
|
||||||
|
mode: fs.constants.O_RDONLY,
|
||||||
|
});
|
||||||
|
const readStream: stream.Readable = fileReader;
|
||||||
|
const bufs: Buffer[] = [];
|
||||||
|
let closed = false;
|
||||||
|
|
||||||
|
const cleanup = () => {
|
||||||
|
closed = true;
|
||||||
|
fileReader.close(); // This may not close the stream.
|
||||||
|
// Artificially marking end-of-stream, as if the underlying resource had
|
||||||
|
// indicated end-of-file by itself, allows the stream to close.
|
||||||
|
// This does not cancel pending read operations, and if there is such an
|
||||||
|
// operation, the process may still not be able to exit successfully
|
||||||
|
// until it finishes.
|
||||||
|
fileReader.push(null);
|
||||||
|
fileReader.read(0);
|
||||||
|
readStream.removeAllListeners();
|
||||||
|
};
|
||||||
|
|
||||||
|
readStream
|
||||||
|
.on("data", chunk => bufs.push(chunk))
|
||||||
|
.on("close", () => cleanup())
|
||||||
|
.on("error", error => {
|
||||||
|
cleanup();
|
||||||
|
logger.warn(`${logPrefix} failed to read file: ${error}`, { filePath });
|
||||||
|
})
|
||||||
|
.on("end", () => {
|
||||||
|
if (!closed) {
|
||||||
|
computeDiff(Buffer.concat(bufs).toString("utf-8"), source, port, filePath);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return cleanup;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function watchFileChanges(filePath: string, port: number): Promise<[IComputedValue<CatalogEntity[]>, Disposer]> {
|
||||||
|
const stat = await fse.stat(filePath); // traverses symlinks, is a race condition
|
||||||
|
const watcher = watch(filePath, {
|
||||||
|
followSymlinks: true,
|
||||||
|
depth: stat.isDirectory() ? 0 : 1, // DIRs works with 0 but files need 1 (bug: https://github.com/paulmillr/chokidar/issues/1095)
|
||||||
|
disableGlobbing: true,
|
||||||
|
});
|
||||||
|
const rootSource = new ExtendedObservableMap<string, ObservableMap<string, RootSourceValue>>(observable.map);
|
||||||
|
const derivedSource = computed(() => Array.from(iter.flatMap(rootSource.values(), from => iter.map(from.values(), child => child[1]))));
|
||||||
|
const stoppers = new Map<string, Disposer>();
|
||||||
|
|
||||||
|
watcher
|
||||||
|
.on("change", (childFilePath) => {
|
||||||
|
stoppers.get(childFilePath)();
|
||||||
|
stoppers.set(childFilePath, diffChangedConfig(childFilePath, rootSource.getOrDefault(childFilePath), port));
|
||||||
|
})
|
||||||
|
.on("add", (childFilePath) => {
|
||||||
|
stoppers.set(childFilePath, diffChangedConfig(childFilePath, rootSource.getOrDefault(childFilePath), port));
|
||||||
|
})
|
||||||
|
.on("unlink", (childFilePath) => {
|
||||||
|
stoppers.get(childFilePath)();
|
||||||
|
stoppers.delete(childFilePath);
|
||||||
|
rootSource.delete(childFilePath);
|
||||||
|
})
|
||||||
|
.on("error", error => logger.error(`${logPrefix} watching file/folder failed: ${error}`, { filePath }));
|
||||||
|
|
||||||
|
return [derivedSource, () => watcher.close()];
|
||||||
|
}
|
||||||
@ -1,13 +1,13 @@
|
|||||||
import "../common/cluster-ipc";
|
import "../common/cluster-ipc";
|
||||||
import type http from "http";
|
import type http from "http";
|
||||||
import { ipcMain } from "electron";
|
import { ipcMain } from "electron";
|
||||||
import { action, autorun, makeObservable, observable, reaction } from "mobx";
|
import { action, autorun, observable, reaction, makeObservable } from "mobx";
|
||||||
|
import { Singleton, toJS } from "../common/utils";
|
||||||
import { ClusterStore, getClusterIdFromHost } from "../common/cluster-store";
|
import { ClusterStore, getClusterIdFromHost } from "../common/cluster-store";
|
||||||
import { Cluster } from "./cluster";
|
import { Cluster } from "./cluster";
|
||||||
import logger from "./logger";
|
import logger from "./logger";
|
||||||
import { apiKubePrefix } from "../common/vars";
|
import { apiKubePrefix } from "../common/vars";
|
||||||
import { CatalogEntity, CatalogEntityData, catalogEntityRegistry } from "../common/catalog";
|
import { CatalogEntity, catalogEntityRegistry } from "../common/catalog";
|
||||||
import { Singleton, toJS } from "../common/utils";
|
|
||||||
import { KubernetesCluster } from "../common/catalog-entities/kubernetes-cluster";
|
import { KubernetesCluster } from "../common/catalog-entities/kubernetes-cluster";
|
||||||
|
|
||||||
const clusterOwnerRef = "ClusterManager";
|
const clusterOwnerRef = "ClusterManager";
|
||||||
@ -20,7 +20,7 @@ export class ClusterManager extends Singleton {
|
|||||||
|
|
||||||
makeObservable(this);
|
makeObservable(this);
|
||||||
|
|
||||||
catalogEntityRegistry.addSource("lens:kubernetes-clusters", this.catalogSource);
|
catalogEntityRegistry.addObservableSource("lens:kubernetes-clusters", this.catalogSource);
|
||||||
// auto-init clusters
|
// auto-init clusters
|
||||||
reaction(() => ClusterStore.getInstance().enabledClustersList, (clusters) => {
|
reaction(() => ClusterStore.getInstance().enabledClustersList, (clusters) => {
|
||||||
clusters.forEach((cluster) => {
|
clusters.forEach((cluster) => {
|
||||||
@ -32,7 +32,7 @@ export class ClusterManager extends Singleton {
|
|||||||
|
|
||||||
}, { fireImmediately: true });
|
}, { fireImmediately: true });
|
||||||
|
|
||||||
reaction(() => toJS(ClusterStore.getInstance().enabledClustersList), () => {
|
reaction(() => toJS(ClusterStore.getInstance().enabledClustersList, { recurseEverything: true }), () => {
|
||||||
this.updateCatalogSource(ClusterStore.getInstance().enabledClustersList);
|
this.updateCatalogSource(ClusterStore.getInstance().enabledClustersList);
|
||||||
}, { fireImmediately: true });
|
}, { fireImmediately: true });
|
||||||
|
|
||||||
@ -40,6 +40,7 @@ export class ClusterManager extends Singleton {
|
|||||||
this.syncClustersFromCatalog(entities);
|
this.syncClustersFromCatalog(entities);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
// auto-stop removed clusters
|
// auto-stop removed clusters
|
||||||
autorun(() => {
|
autorun(() => {
|
||||||
const removedClusters = Array.from(ClusterStore.getInstance().removedClusters.values());
|
const removedClusters = Array.from(ClusterStore.getInstance().removedClusters.values());
|
||||||
@ -55,27 +56,22 @@ export class ClusterManager extends Singleton {
|
|||||||
delay: 250
|
delay: 250
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.on("network:offline", () => {
|
ipcMain.on("network:offline", () => { this.onNetworkOffline(); });
|
||||||
this.onNetworkOffline();
|
ipcMain.on("network:online", () => { this.onNetworkOnline(); });
|
||||||
});
|
|
||||||
ipcMain.on("network:online", () => {
|
|
||||||
this.onNetworkOnline();
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@action
|
@action protected updateCatalogSource(clusters: Cluster[]) {
|
||||||
protected updateCatalogSource(clusters: Cluster[]) {
|
this.catalogSource.replace(this.catalogSource.filter(entity => (
|
||||||
this.catalogSource.forEach((entity, index) => {
|
clusters.find((cluster) => entity.metadata.uid === cluster.id)
|
||||||
const clusterIndex = clusters.findIndex((cluster) => entity.metadata.uid === cluster.id);
|
)));
|
||||||
|
|
||||||
if (clusterIndex === -1) {
|
for (const cluster of clusters) {
|
||||||
this.catalogSource.splice(index, 1);
|
if (cluster.ownerRef) {
|
||||||
|
continue;
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
|
||||||
clusters.filter((c) => !c.ownerRef).forEach((cluster) => {
|
|
||||||
const entityIndex = this.catalogSource.findIndex((entity) => entity.metadata.uid === cluster.id);
|
const entityIndex = this.catalogSource.findIndex((entity) => entity.metadata.uid === cluster.id);
|
||||||
const newEntity = this.catalogEntityFromCluster(cluster);
|
const newEntity = catalogEntityFromCluster(cluster);
|
||||||
|
|
||||||
if (entityIndex === -1) {
|
if (entityIndex === -1) {
|
||||||
this.catalogSource.push(newEntity);
|
this.catalogSource.push(newEntity);
|
||||||
@ -90,11 +86,15 @@ export class ClusterManager extends Singleton {
|
|||||||
};
|
};
|
||||||
this.catalogSource.splice(entityIndex, 1, newEntity);
|
this.catalogSource.splice(entityIndex, 1, newEntity);
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@action syncClustersFromCatalog(entities: KubernetesCluster[]) {
|
@action syncClustersFromCatalog(entities: KubernetesCluster[]) {
|
||||||
entities.filter((entity) => entity.metadata.source !== "local").forEach((entity: KubernetesCluster) => {
|
for (const entity of entities) {
|
||||||
|
if (entity.metadata.source !== "local") {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
const cluster = ClusterStore.getInstance().getById(entity.metadata.uid);
|
const cluster = ClusterStore.getInstance().getById(entity.metadata.uid);
|
||||||
|
|
||||||
if (!cluster) {
|
if (!cluster) {
|
||||||
@ -110,7 +110,7 @@ export class ClusterManager extends Singleton {
|
|||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
cluster.enabled = true;
|
cluster.enabled = true;
|
||||||
if (!cluster.ownerRef) cluster.ownerRef = clusterOwnerRef;
|
cluster.ownerRef ||= clusterOwnerRef;
|
||||||
cluster.preferences.clusterName = entity.metadata.name;
|
cluster.preferences.clusterName = entity.metadata.name;
|
||||||
cluster.kubeConfigPath = entity.spec.kubeconfigPath;
|
cluster.kubeConfigPath = entity.spec.kubeconfigPath;
|
||||||
cluster.contextName = entity.spec.kubeconfigContext;
|
cluster.contextName = entity.spec.kubeconfigContext;
|
||||||
@ -120,34 +120,7 @@ export class ClusterManager extends Singleton {
|
|||||||
active: !cluster.disconnected
|
active: !cluster.disconnected
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
}
|
|
||||||
|
|
||||||
protected catalogEntityFromCluster(cluster: Cluster) {
|
|
||||||
const data: CatalogEntityData = toJS({
|
|
||||||
apiVersion: "entity.k8slens.dev/v1alpha1",
|
|
||||||
kind: "KubernetesCluster",
|
|
||||||
metadata: {
|
|
||||||
uid: cluster.id,
|
|
||||||
name: cluster.name,
|
|
||||||
source: "local",
|
|
||||||
labels: {
|
|
||||||
"distro": (cluster.metadata["distribution"] || "unknown").toString()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
spec: {
|
|
||||||
kubeconfigPath: cluster.kubeConfigPath,
|
|
||||||
kubeconfigContext: cluster.contextName
|
|
||||||
},
|
|
||||||
status: {
|
|
||||||
phase: cluster.disconnected ? "disconnected" : "connected",
|
|
||||||
reason: "",
|
|
||||||
message: "",
|
|
||||||
active: !cluster.disconnected
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return new KubernetesCluster(data as any);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected onNetworkOffline() {
|
protected onNetworkOffline() {
|
||||||
@ -200,3 +173,28 @@ export class ClusterManager extends Singleton {
|
|||||||
return cluster;
|
return cluster;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function catalogEntityFromCluster(cluster: Cluster) {
|
||||||
|
return new KubernetesCluster(toJS({
|
||||||
|
apiVersion: "entity.k8slens.dev/v1alpha1",
|
||||||
|
kind: "KubernetesCluster",
|
||||||
|
metadata: {
|
||||||
|
uid: cluster.id,
|
||||||
|
name: cluster.name,
|
||||||
|
source: "local",
|
||||||
|
labels: {
|
||||||
|
distro: cluster.distribution,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
spec: {
|
||||||
|
kubeconfigPath: cluster.kubeConfigPath,
|
||||||
|
kubeconfigContext: cluster.contextName
|
||||||
|
},
|
||||||
|
status: {
|
||||||
|
phase: cluster.disconnected ? "disconnected" : "connected",
|
||||||
|
reason: "",
|
||||||
|
message: "",
|
||||||
|
active: !cluster.disconnected
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import { ipcMain } from "electron";
|
import { ipcMain } from "electron";
|
||||||
import type { ClusterId, ClusterMetadata, ClusterModel, ClusterPreferences, ClusterPrometheusPreferences } from "../common/cluster-store";
|
import type { ClusterId, ClusterMetadata, ClusterModel, ClusterPreferences, ClusterPrometheusPreferences, UpdateClusterModel } from "../common/cluster-store";
|
||||||
import type { IMetricsReqParams } from "../renderer/api/endpoints/metrics.api";
|
import type { IMetricsReqParams } from "../renderer/api/endpoints/metrics.api";
|
||||||
import { action, comparer, computed, makeObservable, observable, reaction, when } from "mobx";
|
import { action, comparer, computed, makeObservable, observable, reaction, when } from "mobx";
|
||||||
import { apiKubePrefix } from "../common/vars";
|
import { apiKubePrefix } from "../common/vars";
|
||||||
@ -58,7 +58,7 @@ export interface ClusterState {
|
|||||||
*/
|
*/
|
||||||
export class Cluster implements ClusterModel, ClusterState {
|
export class Cluster implements ClusterModel, ClusterState {
|
||||||
/** Unique id for a cluster */
|
/** Unique id for a cluster */
|
||||||
public id: ClusterId;
|
public readonly id: ClusterId;
|
||||||
/**
|
/**
|
||||||
* Kubectl
|
* Kubectl
|
||||||
*
|
*
|
||||||
@ -86,7 +86,7 @@ export class Cluster implements ClusterModel, ClusterState {
|
|||||||
whenReady = when(() => this.ready);
|
whenReady = when(() => this.ready);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Is cluster object initializinng on-going
|
* Is cluster object initializing on-going
|
||||||
*
|
*
|
||||||
* @observable
|
* @observable
|
||||||
*/
|
*/
|
||||||
@ -232,6 +232,10 @@ export class Cluster implements ClusterModel, ClusterState {
|
|||||||
return this.preferences.clusterName || this.contextName;
|
return this.preferences.clusterName || this.contextName;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@computed get distribution(): string {
|
||||||
|
return this.metadata.distribution?.toString() || "unknown";
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Prometheus preferences
|
* Prometheus preferences
|
||||||
*
|
*
|
||||||
@ -253,12 +257,17 @@ export class Cluster implements ClusterModel, ClusterState {
|
|||||||
|
|
||||||
constructor(model: ClusterModel) {
|
constructor(model: ClusterModel) {
|
||||||
makeObservable(this);
|
makeObservable(this);
|
||||||
|
|
||||||
|
this.id = model.id;
|
||||||
this.updateModel(model);
|
this.updateModel(model);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const kubeconfig = this.getKubeconfig();
|
const kubeconfig = this.getKubeconfig();
|
||||||
|
const error = validateKubeConfig(kubeconfig, this.contextName, { validateCluster: true, validateUser: false, validateExec: false});
|
||||||
|
|
||||||
validateKubeConfig(kubeconfig, this.contextName, { validateCluster: true, validateUser: false, validateExec: false });
|
if (error) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
this.apiUrl = kubeconfig.getCluster(kubeconfig.getContextObject(this.contextName).cluster).server;
|
this.apiUrl = kubeconfig.getCluster(kubeconfig.getContextObject(this.contextName).cluster).server;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logger.error(err);
|
logger.error(err);
|
||||||
@ -279,8 +288,34 @@ export class Cluster implements ClusterModel, ClusterState {
|
|||||||
*
|
*
|
||||||
* @param model
|
* @param model
|
||||||
*/
|
*/
|
||||||
@action updateModel(model: ClusterModel) {
|
@action updateModel(model: UpdateClusterModel) {
|
||||||
Object.assign(this, model);
|
// Note: do not assign ID as that should never be updated
|
||||||
|
|
||||||
|
this.kubeConfigPath = model.kubeConfigPath;
|
||||||
|
|
||||||
|
if (model.workspace) {
|
||||||
|
this.workspace = model.workspace;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (model.contextName) {
|
||||||
|
this.contextName = model.contextName;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (model.preferences) {
|
||||||
|
this.preferences = model.preferences;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (model.metadata) {
|
||||||
|
this.metadata = model.metadata;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (model.ownerRef) {
|
||||||
|
this.ownerRef = model.ownerRef;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (model.accessibleNamespaces) {
|
||||||
|
this.accessibleNamespaces = model.accessibleNamespaces;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -33,6 +33,7 @@ import { CatalogPusher } from "./catalog-pusher";
|
|||||||
import { catalogEntityRegistry } from "../common/catalog";
|
import { catalogEntityRegistry } from "../common/catalog";
|
||||||
import { HotbarStore } from "../common/hotbar-store";
|
import { HotbarStore } from "../common/hotbar-store";
|
||||||
import { HelmRepoManager } from "./helm/helm-repo-manager";
|
import { HelmRepoManager } from "./helm/helm-repo-manager";
|
||||||
|
import { KubeconfigSyncManager } from "./catalog-sources";
|
||||||
import configurePackages from "../common/configure-packages";
|
import configurePackages from "../common/configure-packages";
|
||||||
|
|
||||||
const workingDir = path.join(app.getPath("appData"), appName);
|
const workingDir = path.join(app.getPath("appData"), appName);
|
||||||
@ -133,6 +134,9 @@ app.on("ready", async () => {
|
|||||||
|
|
||||||
const clusterManager = ClusterManager.getInstance();
|
const clusterManager = ClusterManager.getInstance();
|
||||||
|
|
||||||
|
// create kubeconfig sync manager
|
||||||
|
KubeconfigSyncManager.createInstance().startSync(clusterManager.port);
|
||||||
|
|
||||||
// run proxy
|
// run proxy
|
||||||
try {
|
try {
|
||||||
logger.info("🔌 Starting LensProxy");
|
logger.info("🔌 Starting LensProxy");
|
||||||
@ -239,6 +243,7 @@ app.on("will-quit", (event) => {
|
|||||||
logger.info("APP:QUIT");
|
logger.info("APP:QUIT");
|
||||||
appEventBus.emit({ name: "app", action: "close" });
|
appEventBus.emit({ name: "app", action: "close" });
|
||||||
ClusterManager.getInstance(false)?.stop(); // close cluster connections
|
ClusterManager.getInstance(false)?.stop(); // close cluster connections
|
||||||
|
KubeconfigSyncManager.getInstance(false)?.stopSync();
|
||||||
|
|
||||||
if (blockQuit) {
|
if (blockQuit) {
|
||||||
event.preventDefault(); // prevent app's default shutdown (e.g. required for telemetry, etc.)
|
event.preventDefault(); // prevent app's default shutdown (e.g. required for telemetry, etc.)
|
||||||
|
|||||||
@ -10,6 +10,7 @@ import { Singleton } from "../common/utils";
|
|||||||
import { ClusterFrameInfo, clusterFrameMap } from "../common/cluster-frames";
|
import { ClusterFrameInfo, clusterFrameMap } from "../common/cluster-frames";
|
||||||
import { IpcRendererNavigationEvents } from "../renderer/navigation/events";
|
import { IpcRendererNavigationEvents } from "../renderer/navigation/events";
|
||||||
import logger from "./logger";
|
import logger from "./logger";
|
||||||
|
import { productName } from "../common/vars";
|
||||||
|
|
||||||
export class WindowManager extends Singleton {
|
export class WindowManager extends Singleton {
|
||||||
protected mainWindow: BrowserWindow;
|
protected mainWindow: BrowserWindow;
|
||||||
@ -48,6 +49,7 @@ export class WindowManager extends Singleton {
|
|||||||
|
|
||||||
this.mainWindow = new BrowserWindow({
|
this.mainWindow = new BrowserWindow({
|
||||||
x, y, width, height,
|
x, y, width, height,
|
||||||
|
title: productName,
|
||||||
show: false,
|
show: false,
|
||||||
minWidth: 700, // accommodate 800 x 600 display minimum
|
minWidth: 700, // accommodate 800 x 600 display minimum
|
||||||
minHeight: 500, // accommodate 800 x 600 display minimum
|
minHeight: 500, // accommodate 800 x 600 display minimum
|
||||||
|
|||||||
@ -43,27 +43,6 @@ describe("CatalogEntityRegistry", () => {
|
|||||||
expect(catalog.items.length).toEqual(2);
|
expect(catalog.items.length).toEqual(2);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("ignores unknown items", () => {
|
|
||||||
const catalog = new CatalogEntityRegistry(catalogCategoryRegistry);
|
|
||||||
const items = [{
|
|
||||||
apiVersion: "entity.k8slens.dev/v1alpha1",
|
|
||||||
kind: "FooBar",
|
|
||||||
metadata: {
|
|
||||||
uid: "123",
|
|
||||||
name: "foobar",
|
|
||||||
source: "test",
|
|
||||||
labels: {}
|
|
||||||
},
|
|
||||||
status: {
|
|
||||||
phase: "disconnected"
|
|
||||||
},
|
|
||||||
spec: {}
|
|
||||||
}];
|
|
||||||
|
|
||||||
catalog.updateItems(items);
|
|
||||||
expect(catalog.items.length).toEqual(0);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("updates existing items", () => {
|
it("updates existing items", () => {
|
||||||
const catalog = new CatalogEntityRegistry(catalogCategoryRegistry);
|
const catalog = new CatalogEntityRegistry(catalogCategoryRegistry);
|
||||||
const items = [{
|
const items = [{
|
||||||
|
|||||||
@ -5,6 +5,7 @@ import { CatalogCategory, CatalogEntity, CatalogEntityData, catalogCategoryRegis
|
|||||||
|
|
||||||
export class CatalogEntityRegistry {
|
export class CatalogEntityRegistry {
|
||||||
@observable protected _items: CatalogEntity[] = observable.array([], { deep: true });
|
@observable protected _items: CatalogEntity[] = observable.array([], { deep: true });
|
||||||
|
@observable protected _activeEntity: CatalogEntity;
|
||||||
|
|
||||||
constructor(private categoryRegistry: CatalogCategoryRegistry) {
|
constructor(private categoryRegistry: CatalogCategoryRegistry) {
|
||||||
makeObservable(this);
|
makeObservable(this);
|
||||||
@ -18,27 +19,15 @@ export class CatalogEntityRegistry {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@action updateItems(items: (CatalogEntityData & CatalogEntityKindData)[]) {
|
@action updateItems(items: (CatalogEntityData & CatalogEntityKindData)[]) {
|
||||||
this._items.forEach((item, index) => {
|
this._items = items.map(data => this.categoryRegistry.getEntityForData(data));
|
||||||
const foundIndex = items.findIndex((i) => i.apiVersion === item.apiVersion && i.kind === item.kind && i.metadata.uid === item.metadata.uid);
|
}
|
||||||
|
|
||||||
if (foundIndex === -1) {
|
set activeEntity(entity: CatalogEntity) {
|
||||||
this._items.splice(index, 1);
|
this._activeEntity = entity;
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
|
||||||
items.forEach((data) => {
|
get activeEntity() {
|
||||||
const item = this.categoryRegistry.getEntityForData(data);
|
return this._activeEntity;
|
||||||
|
|
||||||
if (!item) return; // invalid data
|
|
||||||
|
|
||||||
const index = this._items.findIndex((i) => i.apiVersion === item.apiVersion && i.kind === item.kind && i.metadata.uid === item.metadata.uid);
|
|
||||||
|
|
||||||
if (index === -1) {
|
|
||||||
this._items.push(item);
|
|
||||||
} else {
|
|
||||||
this._items.splice(index, 1, item);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
get items() {
|
get items() {
|
||||||
|
|||||||
@ -50,6 +50,11 @@ export interface IKubeApiQueryParams {
|
|||||||
fieldSelector?: string | string[]; // restrict list of objects by their fields, e.g. fieldSelector: "field=name"
|
fieldSelector?: string | string[]; // restrict list of objects by their fields, e.g. fieldSelector: "field=name"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface KubeApiListOptions {
|
||||||
|
namespace?: string;
|
||||||
|
reqInit?: RequestInit;
|
||||||
|
}
|
||||||
|
|
||||||
export interface IKubePreferredVersion {
|
export interface IKubePreferredVersion {
|
||||||
preferredVersion?: {
|
preferredVersion?: {
|
||||||
version: string;
|
version: string;
|
||||||
|
|||||||
@ -5,6 +5,7 @@ import * as MobxReact from "mobx-react";
|
|||||||
import * as ReactRouter from "react-router";
|
import * as ReactRouter from "react-router";
|
||||||
import * as ReactRouterDom from "react-router-dom";
|
import * as ReactRouterDom from "react-router-dom";
|
||||||
import * as LensExtensions from "../extensions/extension-api";
|
import * as LensExtensions from "../extensions/extension-api";
|
||||||
|
import configurePackages from "../common/configure-packages";
|
||||||
import { render, unmountComponentAtNode } from "react-dom";
|
import { render, unmountComponentAtNode } from "react-dom";
|
||||||
import { delay } from "../common/utils";
|
import { delay } from "../common/utils";
|
||||||
import { isDevelopment, isMac } from "../common/vars";
|
import { isDevelopment, isMac } from "../common/vars";
|
||||||
@ -20,7 +21,7 @@ import { LensApp } from "./lens-app";
|
|||||||
import { ThemeStore } from "./theme.store";
|
import { ThemeStore } from "./theme.store";
|
||||||
import { HelmRepoManager } from "../main/helm/helm-repo-manager";
|
import { HelmRepoManager } from "../main/helm/helm-repo-manager";
|
||||||
import { ExtensionInstallationStateStore } from "./components/+extensions/extension-install.store";
|
import { ExtensionInstallationStateStore } from "./components/+extensions/extension-install.store";
|
||||||
import configurePackages from "../common/configure-packages";
|
import { DefaultProps } from "./mui-base-theme";
|
||||||
|
|
||||||
configurePackages();
|
configurePackages();
|
||||||
|
|
||||||
@ -94,7 +95,7 @@ export async function bootstrap(App: AppComponent) {
|
|||||||
});
|
});
|
||||||
render(<>
|
render(<>
|
||||||
{isMac && <div id="draggable-top"/>}
|
{isMac && <div id="draggable-top"/>}
|
||||||
<App/>
|
{DefaultProps(App)}
|
||||||
</>, rootElem);
|
</>, rootElem);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -11,7 +11,7 @@ import { AceEditor } from "../ace-editor";
|
|||||||
import { Button } from "../button";
|
import { Button } from "../button";
|
||||||
import { Icon } from "../icon";
|
import { Icon } from "../icon";
|
||||||
import { kubeConfigDefaultPath, loadConfig, splitConfig, validateConfig, validateKubeConfig } from "../../../common/kube-helpers";
|
import { kubeConfigDefaultPath, loadConfig, splitConfig, validateConfig, validateKubeConfig } from "../../../common/kube-helpers";
|
||||||
import { ClusterModel, ClusterStore } from "../../../common/cluster-store";
|
import { ClusterStore } from "../../../common/cluster-store";
|
||||||
import { v4 as uuid } from "uuid";
|
import { v4 as uuid } from "uuid";
|
||||||
import { navigate } from "../../navigation";
|
import { navigate } from "../../navigation";
|
||||||
import { UserStore } from "../../../common/user-store";
|
import { UserStore } from "../../../common/user-store";
|
||||||
@ -137,36 +137,28 @@ export class AddCluster extends React.Component {
|
|||||||
};
|
};
|
||||||
|
|
||||||
@action
|
@action
|
||||||
addClusters = () => {
|
addClusters = (): void => {
|
||||||
let newClusters: ClusterModel[] = [];
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (!this.selectedContexts.length) {
|
if (!this.selectedContexts.length) {
|
||||||
this.error = "Please select at least one cluster context";
|
return void (this.error = "Please select at least one cluster context");
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this.error = "";
|
this.error = "";
|
||||||
this.isWaiting = true;
|
this.isWaiting = true;
|
||||||
appEventBus.emit({ name: "cluster-add", action: "click" });
|
appEventBus.emit({ name: "cluster-add", action: "click" });
|
||||||
newClusters = this.selectedContexts.filter(context => {
|
const newClusters = this.selectedContexts.filter(context => {
|
||||||
try {
|
const kubeConfig = this.kubeContexts.get(context);
|
||||||
const kubeConfig = this.kubeContexts.get(context);
|
const error = validateKubeConfig(kubeConfig, context);
|
||||||
|
|
||||||
validateKubeConfig(kubeConfig, context);
|
if (error) {
|
||||||
|
this.error = error.toString();
|
||||||
|
|
||||||
return true;
|
if (error instanceof ExecValidationNotFoundError) {
|
||||||
} catch (err) {
|
|
||||||
this.error = String(err.message);
|
|
||||||
|
|
||||||
if (err instanceof ExecValidationNotFoundError) {
|
|
||||||
Notifications.error(<>Error while adding cluster(s): {this.error}</>);
|
Notifications.error(<>Error while adding cluster(s): {this.error}</>);
|
||||||
|
|
||||||
return false;
|
|
||||||
} else {
|
|
||||||
throw new Error(err);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return Boolean(!error);
|
||||||
}).map(context => {
|
}).map(context => {
|
||||||
const clusterId = uuid();
|
const clusterId = uuid();
|
||||||
const kubeConfig = this.kubeContexts.get(context);
|
const kubeConfig = this.kubeContexts.get(context);
|
||||||
@ -209,7 +201,7 @@ export class AddCluster extends React.Component {
|
|||||||
Add clusters by clicking the <span className="text-primary">Add Cluster</span> button.
|
Add clusters by clicking the <span className="text-primary">Add Cluster</span> button.
|
||||||
You'll need to obtain a working kubeconfig for the cluster you want to add.
|
You'll need to obtain a working kubeconfig for the cluster you want to add.
|
||||||
You can either browse it from the file system or paste it as a text from the clipboard.
|
You can either browse it from the file system or paste it as a text from the clipboard.
|
||||||
Read more about adding clusters <a href={`${docsUrl}/latest/clusters/adding-clusters/`} rel="noreferrer" target="_blank">here</a>.
|
Read more about adding clusters <a href={`${docsUrl}/clusters/adding-clusters/`} rel="noreferrer" target="_blank">here</a>.
|
||||||
</p>
|
</p>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -12,7 +12,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.MuiTooltip-popper {
|
.catalogSpeedDialPopper {
|
||||||
.MuiTooltip-tooltip {
|
.MuiTooltip-tooltip {
|
||||||
background-color: #222;
|
background-color: #222;
|
||||||
font-size: 12px
|
font-size: 12px
|
||||||
|
|||||||
@ -79,6 +79,9 @@ export class CatalogAddButton extends React.Component<CatalogAddButtonProps> {
|
|||||||
icon={<Icon material={menuItem.icon}/>}
|
icon={<Icon material={menuItem.icon}/>}
|
||||||
tooltipTitle={menuItem.title}
|
tooltipTitle={menuItem.title}
|
||||||
onClick={() => menuItem.onClick()}
|
onClick={() => menuItem.onClick()}
|
||||||
|
TooltipClasses={{
|
||||||
|
popper: "catalogSpeedDialPopper"
|
||||||
|
}}
|
||||||
/>;
|
/>;
|
||||||
})}
|
})}
|
||||||
</SpeedDial>
|
</SpeedDial>
|
||||||
|
|||||||
@ -57,4 +57,23 @@
|
|||||||
color: var(--halfGray);
|
color: var(--halfGray);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.TableCell.labels {
|
||||||
|
overflow-x: scroll;
|
||||||
|
text-overflow: unset;
|
||||||
|
|
||||||
|
&::-webkit-scrollbar {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.Badge {
|
||||||
|
overflow: unset;
|
||||||
|
text-overflow: unset;
|
||||||
|
max-width: unset;
|
||||||
|
|
||||||
|
&:not(:first-child) {
|
||||||
|
margin-left: 0.5em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
176
src/renderer/components/+preferences/kubeconfig-syncs.tsx
Normal file
176
src/renderer/components/+preferences/kubeconfig-syncs.tsx
Normal file
@ -0,0 +1,176 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { remote } from "electron";
|
||||||
|
import { Avatar, IconButton, List, ListItem, ListItemAvatar, ListItemSecondaryAction, ListItemText, Paper } from "@material-ui/core";
|
||||||
|
import { Description, Folder, Delete, HelpOutline } from "@material-ui/icons";
|
||||||
|
import { action, computed, observable, reaction } from "mobx";
|
||||||
|
import { disposeOnUnmount, observer } from "mobx-react";
|
||||||
|
import fse from "fs-extra";
|
||||||
|
import { KubeconfigSyncEntry, KubeconfigSyncValue, UserStore } from "../../../common/user-store";
|
||||||
|
import { Button } from "../button";
|
||||||
|
import { SubTitle } from "../layout/sub-title";
|
||||||
|
import { Spinner } from "../spinner";
|
||||||
|
import logger from "../../../main/logger";
|
||||||
|
import { iter } from "../../utils";
|
||||||
|
|
||||||
|
interface SyncInfo {
|
||||||
|
type: "file" | "folder" | "unknown";
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Entry extends Value {
|
||||||
|
filePath: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Value {
|
||||||
|
data: KubeconfigSyncValue;
|
||||||
|
info: SyncInfo;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getMapEntry({ filePath, ...data}: KubeconfigSyncEntry): Promise<[string, Value]> {
|
||||||
|
try {
|
||||||
|
// stat follows the stat(2) linux syscall spec, namely it follows symlinks
|
||||||
|
const stats = await fse.stat(filePath);
|
||||||
|
|
||||||
|
if (stats.isFile()) {
|
||||||
|
return [filePath, { info: { type: "file" }, data }];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (stats.isDirectory()) {
|
||||||
|
return [filePath, { info: { type: "folder" }, data }];
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.warn("[KubeconfigSyncs]: unknown stat entry", { stats });
|
||||||
|
|
||||||
|
return [filePath, { info: { type: "unknown" }, data }];
|
||||||
|
} catch (error) {
|
||||||
|
logger.warn(`[KubeconfigSyncs]: failed to stat entry: ${error}`, { error });
|
||||||
|
|
||||||
|
return [filePath, { info: { type: "unknown" }, data }];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@observer
|
||||||
|
export class KubeconfigSyncs extends React.Component {
|
||||||
|
syncs = observable.map<string, Value>();
|
||||||
|
@observable loaded = false;
|
||||||
|
|
||||||
|
async componentDidMount() {
|
||||||
|
const mapEntries = await Promise.all(
|
||||||
|
iter.map(
|
||||||
|
UserStore.getInstance().syncKubeconfigEntries,
|
||||||
|
([filePath, ...value]) => getMapEntry({ filePath, ...value }),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
this.syncs.replace(mapEntries);
|
||||||
|
this.loaded = true;
|
||||||
|
|
||||||
|
disposeOnUnmount(this, [
|
||||||
|
reaction(() => Array.from(this.syncs.entries(), ([filePath, { data }]) => [filePath, data]), syncs => {
|
||||||
|
UserStore.getInstance().syncKubeconfigEntries.replace(syncs);
|
||||||
|
})
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
@computed get syncsList(): Entry[] | undefined {
|
||||||
|
if (!this.loaded) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Array.from(this.syncs.entries(), ([filePath, value]) => ({ filePath, ...value }));
|
||||||
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
openFileDialog = async () => {
|
||||||
|
const { dialog, BrowserWindow } = remote;
|
||||||
|
const { canceled, filePaths } = await dialog.showOpenDialog(BrowserWindow.getFocusedWindow(), {
|
||||||
|
properties: ["openFile", "showHiddenFiles", "multiSelections", "openDirectory"],
|
||||||
|
message: "Select kubeconfig file(s) and folder(s)",
|
||||||
|
buttonLabel: "Sync",
|
||||||
|
});
|
||||||
|
|
||||||
|
if (canceled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const newEntries = await Promise.all(filePaths.map(filePath => getMapEntry({ filePath })));
|
||||||
|
|
||||||
|
for (const [filePath, info] of newEntries) {
|
||||||
|
this.syncs.set(filePath, info);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
renderEntryIcon(entry: Entry) {
|
||||||
|
switch (entry.info.type) {
|
||||||
|
case "file":
|
||||||
|
return <Description />;
|
||||||
|
case "folder":
|
||||||
|
return <Folder />;
|
||||||
|
case "unknown":
|
||||||
|
return <HelpOutline />;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
renderEntry = (entry: Entry) => {
|
||||||
|
return (
|
||||||
|
<Paper className="entry" key={entry.filePath} elevation={3}>
|
||||||
|
<ListItem>
|
||||||
|
<ListItemAvatar>
|
||||||
|
<Avatar>
|
||||||
|
{this.renderEntryIcon(entry)}
|
||||||
|
</Avatar>
|
||||||
|
</ListItemAvatar>
|
||||||
|
<ListItemText
|
||||||
|
primary={entry.filePath}
|
||||||
|
className="description"
|
||||||
|
/>
|
||||||
|
<ListItemSecondaryAction className="action">
|
||||||
|
<IconButton
|
||||||
|
edge="end"
|
||||||
|
aria-label="delete"
|
||||||
|
onClick={() => this.syncs.delete(entry.filePath)}
|
||||||
|
>
|
||||||
|
<Delete />
|
||||||
|
</IconButton>
|
||||||
|
</ListItemSecondaryAction>
|
||||||
|
</ListItem>
|
||||||
|
</Paper>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
renderEntries() {
|
||||||
|
const entries = this.syncsList;
|
||||||
|
|
||||||
|
if (!entries) {
|
||||||
|
return (
|
||||||
|
<div className="loading-spinner">
|
||||||
|
<Spinner />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<List className="kubeconfig-sync-list">
|
||||||
|
{entries.map(this.renderEntry)}
|
||||||
|
</List>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<section className="small">
|
||||||
|
<SubTitle title="Files and Folders to sync" />
|
||||||
|
<Button
|
||||||
|
primary
|
||||||
|
label="Sync file or folder"
|
||||||
|
onClick={() => void this.openFileDialog()}
|
||||||
|
/>
|
||||||
|
<div className="hint">
|
||||||
|
Sync an individual file or all files in a folder (non-recursive).
|
||||||
|
</div>
|
||||||
|
{this.renderEntries()}
|
||||||
|
</section>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,2 +1,32 @@
|
|||||||
.Preferences {
|
.Preferences {
|
||||||
}
|
.loading-spinner {
|
||||||
|
margin: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.kubeconfig-sync-list {
|
||||||
|
.entry {
|
||||||
|
&.MuiPaper-root {
|
||||||
|
background-color: var(--inputControlBackground);
|
||||||
|
margin-bottom: var(--flex-gap, 1em);
|
||||||
|
color: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
.MuiAvatar-root {
|
||||||
|
color: var(--buttonPrimaryBackground);
|
||||||
|
font-size: calc(2.5 * var(--unit));
|
||||||
|
}
|
||||||
|
|
||||||
|
.description {
|
||||||
|
font-family: monospace;
|
||||||
|
|
||||||
|
.MuiTypography-body1 {
|
||||||
|
font-size: var(--font-size);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.action .MuiIconButton-root {
|
||||||
|
font-size: calc(2.5 * var(--unit));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -18,6 +18,7 @@ import { KubectlBinaries } from "./kubectl-binaries";
|
|||||||
import { navigation } from "../../navigation";
|
import { navigation } from "../../navigation";
|
||||||
import { Tab, Tabs } from "../tabs";
|
import { Tab, Tabs } from "../tabs";
|
||||||
import { FormSwitch, Switcher } from "../switch";
|
import { FormSwitch, Switcher } from "../switch";
|
||||||
|
import { KubeconfigSyncs } from "./kubeconfig-syncs";
|
||||||
|
|
||||||
enum Pages {
|
enum Pages {
|
||||||
Application = "application",
|
Application = "application",
|
||||||
@ -214,7 +215,6 @@ export class Preferences extends React.Component {
|
|||||||
</section>
|
</section>
|
||||||
</section>
|
</section>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{this.activeTab == Pages.Kubernetes && (
|
{this.activeTab == Pages.Kubernetes && (
|
||||||
<section id="kubernetes">
|
<section id="kubernetes">
|
||||||
<section id="kubectl">
|
<section id="kubectl">
|
||||||
@ -222,20 +222,23 @@ export class Preferences extends React.Component {
|
|||||||
<KubectlBinaries />
|
<KubectlBinaries />
|
||||||
</section>
|
</section>
|
||||||
<hr/>
|
<hr/>
|
||||||
|
<section id="kube-sync">
|
||||||
|
<h2 data-testid="kubernetes-sync-header">Kubeconfig Syncs</h2>
|
||||||
|
<KubeconfigSyncs />
|
||||||
|
</section>
|
||||||
|
<hr/>
|
||||||
<section id="helm">
|
<section id="helm">
|
||||||
<h2>Helm Charts</h2>
|
<h2>Helm Charts</h2>
|
||||||
<HelmCharts/>
|
<HelmCharts/>
|
||||||
</section>
|
</section>
|
||||||
</section>
|
</section>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{this.activeTab == Pages.Telemetry && (
|
{this.activeTab == Pages.Telemetry && (
|
||||||
<section id="telemetry">
|
<section id="telemetry">
|
||||||
<h2 data-testid="telemetry-header">Telemetry</h2>
|
<h2 data-testid="telemetry-header">Telemetry</h2>
|
||||||
{telemetryExtensions.map(this.renderExtension)}
|
{telemetryExtensions.map(this.renderExtension)}
|
||||||
</section>
|
</section>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{this.activeTab == Pages.Extensions && (
|
{this.activeTab == Pages.Extensions && (
|
||||||
<section id="extensions">
|
<section id="extensions">
|
||||||
<h2>Extensions</h2>
|
<h2>Extensions</h2>
|
||||||
|
|||||||
@ -2,49 +2,21 @@ import "./cluster-manager.scss";
|
|||||||
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { Redirect, Route, Switch } from "react-router";
|
import { Redirect, Route, Switch } from "react-router";
|
||||||
import { comparer, reaction } from "mobx";
|
import { observer } from "mobx-react";
|
||||||
import { disposeOnUnmount, observer } from "mobx-react";
|
|
||||||
import { BottomBar } from "./bottom-bar";
|
import { BottomBar } from "./bottom-bar";
|
||||||
import { Catalog, catalogRoute } from "../+catalog";
|
import { Catalog, catalogRoute } from "../+catalog";
|
||||||
import { Preferences, preferencesRoute } from "../+preferences";
|
import { Preferences, preferencesRoute } from "../+preferences";
|
||||||
import { AddCluster, addClusterRoute } from "../+add-cluster";
|
import { AddCluster, addClusterRoute } from "../+add-cluster";
|
||||||
import { ClusterView } from "./cluster-view";
|
import { ClusterView } from "./cluster-view";
|
||||||
import { clusterViewRoute } from "./cluster-view.route";
|
import { clusterViewRoute } from "./cluster-view.route";
|
||||||
import { ClusterStore } from "../../../common/cluster-store";
|
|
||||||
import { hasLoadedView, initView, lensViews, refreshViews } from "./lens-views";
|
|
||||||
import { globalPageRegistry } from "../../../extensions/registries/page-registry";
|
import { globalPageRegistry } from "../../../extensions/registries/page-registry";
|
||||||
import { Extensions, extensionsRoute } from "../+extensions";
|
import { Extensions, extensionsRoute } from "../+extensions";
|
||||||
import { getMatchedClusterId } from "../../navigation";
|
|
||||||
import { HotbarMenu } from "../hotbar/hotbar-menu";
|
import { HotbarMenu } from "../hotbar/hotbar-menu";
|
||||||
import { EntitySettings, entitySettingsRoute } from "../+entity-settings";
|
import { EntitySettings, entitySettingsRoute } from "../+entity-settings";
|
||||||
import { Welcome, welcomeRoute, welcomeURL } from "../+welcome";
|
import { Welcome, welcomeRoute, welcomeURL } from "../+welcome";
|
||||||
|
|
||||||
@observer
|
@observer
|
||||||
export class ClusterManager extends React.Component {
|
export class ClusterManager extends React.Component {
|
||||||
componentDidMount() {
|
|
||||||
const getMatchedCluster = () => ClusterStore.getInstance().getById(getMatchedClusterId());
|
|
||||||
|
|
||||||
disposeOnUnmount(this, [
|
|
||||||
reaction(getMatchedClusterId, initView, {
|
|
||||||
fireImmediately: true
|
|
||||||
}),
|
|
||||||
reaction(() => !getMatchedClusterId(), () => ClusterStore.getInstance().setActive(null)),
|
|
||||||
reaction(() => [
|
|
||||||
getMatchedClusterId(), // refresh when active cluster-view changed
|
|
||||||
hasLoadedView(getMatchedClusterId()), // refresh when cluster's webview loaded
|
|
||||||
getMatchedCluster()?.available, // refresh on disconnect active-cluster
|
|
||||||
getMatchedCluster()?.ready, // refresh when cluster ready-state change
|
|
||||||
], refreshViews, {
|
|
||||||
fireImmediately: true,
|
|
||||||
equals: comparer.shallow,
|
|
||||||
}),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
componentWillUnmount() {
|
|
||||||
lensViews.clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<div className="ClusterManager">
|
<div className="ClusterManager">
|
||||||
|
|||||||
@ -44,10 +44,6 @@ export class ClusterStatus extends React.Component<Props> {
|
|||||||
error: res.error,
|
error: res.error,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
if (this.cluster.disconnected) {
|
|
||||||
await this.activateCluster();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount() {
|
componentWillUnmount() {
|
||||||
|
|||||||
@ -5,11 +5,12 @@ import { disposeOnUnmount, observer } from "mobx-react";
|
|||||||
import { RouteComponentProps } from "react-router";
|
import { RouteComponentProps } from "react-router";
|
||||||
import { IClusterViewRouteParams } from "./cluster-view.route";
|
import { IClusterViewRouteParams } from "./cluster-view.route";
|
||||||
import { ClusterStatus } from "./cluster-status";
|
import { ClusterStatus } from "./cluster-status";
|
||||||
import { hasLoadedView } from "./lens-views";
|
import { hasLoadedView, initView, refreshViews } from "./lens-views";
|
||||||
import { Cluster } from "../../../main/cluster";
|
import { Cluster } from "../../../main/cluster";
|
||||||
import { navigate } from "../../navigation";
|
|
||||||
import { catalogURL } from "../+catalog";
|
|
||||||
import { ClusterStore } from "../../../common/cluster-store";
|
import { ClusterStore } from "../../../common/cluster-store";
|
||||||
|
import { requestMain } from "../../../common/ipc";
|
||||||
|
import { clusterActivateHandler } from "../../../common/cluster-ipc";
|
||||||
|
import { catalogEntityRegistry } from "../../api/catalog-entity-registry";
|
||||||
|
|
||||||
interface Props extends RouteComponentProps<IClusterViewRouteParams> {
|
interface Props extends RouteComponentProps<IClusterViewRouteParams> {
|
||||||
}
|
}
|
||||||
@ -26,19 +27,43 @@ export class ClusterView extends React.Component<Props> {
|
|||||||
|
|
||||||
async componentDidMount() {
|
async componentDidMount() {
|
||||||
disposeOnUnmount(this, [
|
disposeOnUnmount(this, [
|
||||||
reaction(() => this.clusterId, clusterId => ClusterStore.getInstance().setActive(clusterId), {
|
reaction(() => this.clusterId, (clusterId) => {
|
||||||
|
this.showCluster(clusterId);
|
||||||
|
}, {
|
||||||
fireImmediately: true,
|
fireImmediately: true,
|
||||||
}),
|
|
||||||
reaction(() => this.cluster.online, (online) => {
|
|
||||||
if (!online) navigate(catalogURL());
|
|
||||||
})
|
})
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
componentWillUnmount() {
|
||||||
|
this.hideCluster();
|
||||||
|
}
|
||||||
|
|
||||||
|
showCluster(clusterId: string) {
|
||||||
|
initView(clusterId);
|
||||||
|
requestMain(clusterActivateHandler, this.clusterId, false);
|
||||||
|
|
||||||
|
const entity = catalogEntityRegistry.getById(this.clusterId);
|
||||||
|
|
||||||
|
if (entity) {
|
||||||
|
catalogEntityRegistry.activeEntity = entity;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
hideCluster() {
|
||||||
|
refreshViews();
|
||||||
|
|
||||||
|
if (catalogEntityRegistry.activeEntity?.metadata?.uid === this.clusterId) {
|
||||||
|
catalogEntityRegistry.activeEntity = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { cluster } = this;
|
const { cluster } = this;
|
||||||
const showStatus = cluster && (!cluster.available || !hasLoadedView(cluster.id) || !cluster.ready);
|
const showStatus = cluster && (!cluster.available || !hasLoadedView(cluster.id) || !cluster.ready);
|
||||||
|
|
||||||
|
refreshViews(cluster.id);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="ClusterView flex align-center">
|
<div className="ClusterView flex align-center">
|
||||||
{showStatus && (
|
{showStatus && (
|
||||||
|
|||||||
@ -1,7 +1,8 @@
|
|||||||
import { observable, when } from "mobx";
|
import { observable, when } from "mobx";
|
||||||
import { ClusterId, ClusterStore, getClusterFrameUrl } from "../../../common/cluster-store";
|
import { ClusterId, ClusterStore, getClusterFrameUrl } from "../../../common/cluster-store";
|
||||||
import { getMatchedClusterId } from "../../navigation";
|
import { navigate } from "../../navigation";
|
||||||
import logger from "../../../main/logger";
|
import logger from "../../../main/logger";
|
||||||
|
import { catalogURL } from "../+catalog";
|
||||||
|
|
||||||
export interface LensView {
|
export interface LensView {
|
||||||
isLoaded?: boolean
|
isLoaded?: boolean
|
||||||
@ -16,9 +17,12 @@ export function hasLoadedView(clusterId: ClusterId): boolean {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function initView(clusterId: ClusterId) {
|
export async function initView(clusterId: ClusterId) {
|
||||||
|
refreshViews(clusterId);
|
||||||
|
|
||||||
if (!clusterId || lensViews.has(clusterId)) {
|
if (!clusterId || lensViews.has(clusterId)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const cluster = ClusterStore.getInstance().getById(clusterId);
|
const cluster = ClusterStore.getInstance().getById(clusterId);
|
||||||
|
|
||||||
if (!cluster) {
|
if (!cluster) {
|
||||||
@ -51,16 +55,23 @@ export async function autoCleanOnRemove(clusterId: ClusterId, iframe: HTMLIFrame
|
|||||||
logger.info(`[LENS-VIEW]: remove dashboard, clusterId=${clusterId}`);
|
logger.info(`[LENS-VIEW]: remove dashboard, clusterId=${clusterId}`);
|
||||||
lensViews.delete(clusterId);
|
lensViews.delete(clusterId);
|
||||||
|
|
||||||
|
const wasVisible = iframe.style.display !== "none";
|
||||||
|
|
||||||
// Keep frame in DOM to avoid possible bugs when same cluster re-created after being removed.
|
// Keep frame in DOM to avoid possible bugs when same cluster re-created after being removed.
|
||||||
// In that case for some reasons `webFrame.routingId` returns some previous frameId (usage in app.tsx)
|
// In that case for some reasons `webFrame.routingId` returns some previous frameId (usage in app.tsx)
|
||||||
// Issue: https://github.com/lensapp/lens/issues/811
|
// Issue: https://github.com/lensapp/lens/issues/811
|
||||||
|
iframe.style.display = "none";
|
||||||
iframe.dataset.meta = `${iframe.name} was removed at ${new Date().toLocaleString()}`;
|
iframe.dataset.meta = `${iframe.name} was removed at ${new Date().toLocaleString()}`;
|
||||||
iframe.removeAttribute("name");
|
iframe.removeAttribute("name");
|
||||||
iframe.contentWindow.postMessage("teardown", "*");
|
iframe.contentWindow.postMessage("teardown", "*");
|
||||||
|
|
||||||
|
if (wasVisible) {
|
||||||
|
navigate(catalogURL());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function refreshViews() {
|
export function refreshViews(visibleClusterId?: string) {
|
||||||
const cluster = ClusterStore.getInstance().getById(getMatchedClusterId());
|
const cluster = !visibleClusterId ? null : ClusterStore.getInstance().getById(visibleClusterId);
|
||||||
|
|
||||||
lensViews.forEach(({ clusterId, view, isLoaded }) => {
|
lensViews.forEach(({ clusterId, view, isLoaded }) => {
|
||||||
const isCurrent = clusterId === cluster?.id;
|
const isCurrent = clusterId === cluster?.id;
|
||||||
|
|||||||
@ -30,6 +30,7 @@
|
|||||||
|
|
||||||
span {
|
span {
|
||||||
-webkit-font-smoothing: auto; // Better readability on non-retina screens
|
-webkit-font-smoothing: auto; // Better readability on non-retina screens
|
||||||
|
white-space: pre;
|
||||||
}
|
}
|
||||||
|
|
||||||
span.overlay {
|
span.overlay {
|
||||||
|
|||||||
@ -48,7 +48,7 @@
|
|||||||
margin: -8px;
|
margin: -8px;
|
||||||
font-size: var(--font-size-small);
|
font-size: var(--font-size-small);
|
||||||
background: var(--clusterMenuBackground);
|
background: var(--clusterMenuBackground);
|
||||||
color: white;
|
color: var(--textColorAccent);
|
||||||
padding: 0px;
|
padding: 0px;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
border: 2px solid var(--clusterMenuBackground);
|
border: 2px solid var(--clusterMenuBackground);
|
||||||
|
|||||||
@ -123,7 +123,7 @@ export class HotbarIcon extends React.Component<Props> {
|
|||||||
|
|
||||||
generateAvatarStyle(entity: CatalogEntity): React.CSSProperties {
|
generateAvatarStyle(entity: CatalogEntity): React.CSSProperties {
|
||||||
return {
|
return {
|
||||||
"backgroundColor": randomColor({ seed: entity.metadata.name, luminosity: "dark" })
|
"backgroundColor": randomColor({ seed: `${entity.metadata.name}-${entity.metadata.source}`, luminosity: "dark" })
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -145,7 +145,7 @@ export class HotbarIcon extends React.Component<Props> {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={className}>
|
<div className={className}>
|
||||||
<Tooltip targetId={entityIconId}>{entity.metadata.name}</Tooltip>
|
<Tooltip targetId={entityIconId}>{entity.metadata.name} ({entity.metadata.source || "local"})</Tooltip>
|
||||||
<Avatar
|
<Avatar
|
||||||
{...elemProps}
|
{...elemProps}
|
||||||
id={entityIconId}
|
id={entityIconId}
|
||||||
|
|||||||
@ -12,12 +12,6 @@
|
|||||||
height: 4px; // extra spacing for mac-os "traffic-light" buttons
|
height: 4px; // extra spacing for mac-os "traffic-light" buttons
|
||||||
}
|
}
|
||||||
|
|
||||||
&:hover {
|
|
||||||
.AddCellButton {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.HotbarItems {
|
.HotbarItems {
|
||||||
--cellWidth: 40px;
|
--cellWidth: 40px;
|
||||||
--cellHeight: 40px;
|
--cellHeight: 40px;
|
||||||
@ -53,55 +47,23 @@
|
|||||||
transform: translateZ(0); // Remove flickering artifacts
|
transform: translateZ(0); // Remove flickering artifacts
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
.cellDeleteButton {
|
&:not(:empty) {
|
||||||
opacity: 1;
|
|
||||||
transition: opacity 0.1s 0.2s;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:not(.empty) {
|
|
||||||
box-shadow: 0 0 0px 3px #ffffff1a;
|
box-shadow: 0 0 0px 3px #ffffff1a;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&.animating {
|
&.animating {
|
||||||
&.empty {
|
&:empty {
|
||||||
animation: shake .6s cubic-bezier(.36,.07,.19,.97) both;
|
animation: shake .6s cubic-bezier(.36,.07,.19,.97) both;
|
||||||
transform: translate3d(0, 0, 0);
|
transform: translate3d(0, 0, 0);
|
||||||
backface-visibility: hidden;
|
backface-visibility: hidden;
|
||||||
perspective: 1000px;
|
perspective: 1000px;
|
||||||
}
|
}
|
||||||
|
|
||||||
&:not(.empty) {
|
&:not(:empty) {
|
||||||
animation: outline 0.8s cubic-bezier(0.19, 1, 0.22, 1);
|
animation: outline 0.8s cubic-bezier(0.19, 1, 0.22, 1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.cellDeleteButton {
|
|
||||||
width: 2rem;
|
|
||||||
height: 2rem;
|
|
||||||
border-radius: 50%;
|
|
||||||
background-color: var(--textColorDimmed);
|
|
||||||
position: absolute;
|
|
||||||
top: -7px;
|
|
||||||
right: -7px;
|
|
||||||
color: var(--secondaryBackground);
|
|
||||||
opacity: 0;
|
|
||||||
border: 3px solid var(--clusterMenuBackground);
|
|
||||||
box-sizing: border-box;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background-color: white;
|
|
||||||
transition: all 0.2s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.Icon {
|
|
||||||
--smallest-size: 12px;
|
|
||||||
font-weight: bold;
|
|
||||||
position: relative;
|
|
||||||
top: -2px;
|
|
||||||
left: .5px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -141,30 +103,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.AddCellButton {
|
|
||||||
width: 40px;
|
|
||||||
height: 40px;
|
|
||||||
min-height: 40px;
|
|
||||||
margin: 12px auto 8px;
|
|
||||||
background-color: transparent;
|
|
||||||
color: var(--textColorDimmed);
|
|
||||||
border-radius: 6px;
|
|
||||||
transition: all 0.2s;
|
|
||||||
cursor: pointer;
|
|
||||||
z-index: 1;
|
|
||||||
opacity: 0;
|
|
||||||
transition: all 0.2s;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background-color: var(--sidebarBackground);
|
|
||||||
}
|
|
||||||
|
|
||||||
.Icon {
|
|
||||||
--size: 24px;
|
|
||||||
margin-left: 2px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes shake {
|
@keyframes shake {
|
||||||
|
|||||||
@ -6,13 +6,12 @@ import { observer } from "mobx-react";
|
|||||||
import { HotbarIcon } from "./hotbar-icon";
|
import { HotbarIcon } from "./hotbar-icon";
|
||||||
import { cssNames, IClassName } from "../../utils";
|
import { cssNames, IClassName } from "../../utils";
|
||||||
import { catalogEntityRegistry } from "../../api/catalog-entity-registry";
|
import { catalogEntityRegistry } from "../../api/catalog-entity-registry";
|
||||||
import { defaultHotbarCells, HotbarItem, HotbarStore } from "../../../common/hotbar-store";
|
import { HotbarItem, HotbarStore } from "../../../common/hotbar-store";
|
||||||
import { CatalogEntity, catalogEntityRunContext } from "../../api/catalog-entity";
|
import { CatalogEntity, catalogEntityRunContext } from "../../api/catalog-entity";
|
||||||
import { Icon } from "../icon";
|
import { Icon } from "../icon";
|
||||||
import { Badge } from "../badge";
|
import { Badge } from "../badge";
|
||||||
import { CommandOverlay } from "../command-palette";
|
import { CommandOverlay } from "../command-palette";
|
||||||
import { HotbarSwitchCommand } from "./hotbar-switch-command";
|
import { HotbarSwitchCommand } from "./hotbar-switch-command";
|
||||||
import { ClusterStore } from "../../../common/cluster-store";
|
|
||||||
import { Tooltip, TooltipPosition } from "../tooltip";
|
import { Tooltip, TooltipPosition } from "../tooltip";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
@ -26,7 +25,7 @@ export class HotbarMenu extends React.Component<Props> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
isActive(item: CatalogEntity) {
|
isActive(item: CatalogEntity) {
|
||||||
return ClusterStore.getInstance().activeClusterId == item.getId();
|
return catalogEntityRegistry.activeEntity?.metadata?.uid == item.getId();
|
||||||
}
|
}
|
||||||
|
|
||||||
getEntity(item: HotbarItem) {
|
getEntity(item: HotbarItem) {
|
||||||
@ -73,14 +72,6 @@ export class HotbarMenu extends React.Component<Props> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
renderAddCellButton() {
|
|
||||||
return (
|
|
||||||
<button className="AddCellButton" onClick={() => HotbarStore.getInstance().addEmptyCell()}>
|
|
||||||
<Icon material="add"/>
|
|
||||||
</button>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { className } = this.props;
|
const { className } = this.props;
|
||||||
const hotbarStore = HotbarStore.getInstance();
|
const hotbarStore = HotbarStore.getInstance();
|
||||||
@ -91,7 +82,6 @@ export class HotbarMenu extends React.Component<Props> {
|
|||||||
<div className={cssNames("HotbarMenu flex column", className)}>
|
<div className={cssNames("HotbarMenu flex column", className)}>
|
||||||
<div className="HotbarItems flex column gaps">
|
<div className="HotbarItems flex column gaps">
|
||||||
{this.renderGrid()}
|
{this.renderGrid()}
|
||||||
{this.hotbar.items.length != defaultHotbarCells && this.renderAddCellButton()}
|
|
||||||
</div>
|
</div>
|
||||||
<div className="HotbarSelector flex align-center">
|
<div className="HotbarSelector flex align-center">
|
||||||
<Icon material="play_arrow" className="previous box" onClick={() => this.previous()} />
|
<Icon material="play_arrow" className="previous box" onClick={() => this.previous()} />
|
||||||
@ -120,23 +110,14 @@ function HotbarCell(props: HotbarCellProps) {
|
|||||||
const [animating, setAnimating] = useState(false);
|
const [animating, setAnimating] = useState(false);
|
||||||
const onAnimationEnd = () => { setAnimating(false); };
|
const onAnimationEnd = () => { setAnimating(false); };
|
||||||
const onClick = () => { setAnimating(true); };
|
const onClick = () => { setAnimating(true); };
|
||||||
const onDeleteClick = (evt: Event | React.SyntheticEvent) => {
|
|
||||||
evt.stopPropagation();
|
|
||||||
HotbarStore.getInstance().removeEmptyCell(props.index);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={cssNames("HotbarCell", { animating, empty: !props.children })}
|
className={cssNames("HotbarCell", { animating })}
|
||||||
onAnimationEnd={onAnimationEnd}
|
onAnimationEnd={onAnimationEnd}
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
>
|
>
|
||||||
{props.children}
|
{props.children}
|
||||||
{!props.children && (
|
|
||||||
<div className="cellDeleteButton" onClick={onDeleteClick}>
|
|
||||||
<Icon material="close" smallest/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -76,7 +76,7 @@ function ListNamespacesForbiddenHandler(event: IpcRendererEvent, ...[clusterId]:
|
|||||||
(
|
(
|
||||||
<div className="flex column gaps">
|
<div className="flex column gaps">
|
||||||
<b>Add Accessible Namespaces</b>
|
<b>Add Accessible Namespaces</b>
|
||||||
<p>Cluster <b>{ClusterStore.getInstance().active.name}</b> does not have permissions to list namespaces. Please add the namespaces you have access to.</p>
|
<p>Cluster <b>{ClusterStore.getInstance().getById(clusterId).name}</b> does not have permissions to list namespaces. Please add the namespaces you have access to.</p>
|
||||||
<div className="flex gaps row align-left box grow">
|
<div className="flex gaps row align-left box grow">
|
||||||
<Button active outlined label="Go to Accessible Namespaces Settings" onClick={()=> {
|
<Button active outlined label="Go to Accessible Namespaces Settings" onClick={()=> {
|
||||||
navigate(entitySettingsURL({ params: { entityId: clusterId }, fragment: "accessible-namespaces" }));
|
navigate(entitySettingsURL({ params: { entityId: clusterId }, fragment: "accessible-namespaces" }));
|
||||||
|
|||||||
@ -1,15 +1,19 @@
|
|||||||
import type { ClusterContext } from "./components/context";
|
import type { ClusterContext } from "./components/context";
|
||||||
import { action, computed, makeObservable, observable, reaction, when } from "mobx";
|
|
||||||
|
import { action, computed, observable, reaction, when, makeObservable } from "mobx";
|
||||||
|
import { noop, rejectPromiseBy } from "./utils";
|
||||||
import { KubeObject, KubeStatus } from "./api/kube-object";
|
import { KubeObject, KubeStatus } from "./api/kube-object";
|
||||||
import { IKubeWatchEvent } from "./api/kube-watch-api";
|
import { IKubeWatchEvent } from "./api/kube-watch-api";
|
||||||
import { ItemStore } from "./item.store";
|
import { ItemStore } from "./item.store";
|
||||||
import { apiManager } from "./api/api-manager";
|
import { apiManager } from "./api/api-manager";
|
||||||
import { IKubeApiQueryParams, KubeApi, parseKubeApi } from "./api/kube-api";
|
import { IKubeApiQueryParams, KubeApi, parseKubeApi } from "./api/kube-api";
|
||||||
import { KubeJsonApiData } from "./api/kube-json-api";
|
import { KubeJsonApiData } from "./api/kube-json-api";
|
||||||
|
import { Notifications } from "./components/notifications";
|
||||||
|
|
||||||
export interface KubeObjectStoreLoadingParams {
|
export interface KubeObjectStoreLoadingParams {
|
||||||
namespaces: string[];
|
namespaces: string[];
|
||||||
api?: KubeApi;
|
api?: KubeApi;
|
||||||
|
reqInit?: RequestInit;
|
||||||
}
|
}
|
||||||
|
|
||||||
export abstract class KubeObjectStore<T extends KubeObject = any> extends ItemStore<T> {
|
export abstract class KubeObjectStore<T extends KubeObject = any> extends ItemStore<T> {
|
||||||
@ -20,9 +24,10 @@ export abstract class KubeObjectStore<T extends KubeObject = any> extends ItemSt
|
|||||||
abstract api: KubeApi<T>;
|
abstract api: KubeApi<T>;
|
||||||
public readonly limit?: number;
|
public readonly limit?: number;
|
||||||
public readonly bufferSize: number = 50000;
|
public readonly bufferSize: number = 50000;
|
||||||
private loadedNamespaces: string[] = [];
|
@observable private loadedNamespaces?: string[];
|
||||||
|
|
||||||
contextReady = when(() => Boolean(this.context));
|
contextReady = when(() => Boolean(this.context));
|
||||||
|
namespacesReady = when(() => Boolean(this.loadedNamespaces));
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
@ -103,10 +108,10 @@ export abstract class KubeObjectStore<T extends KubeObject = any> extends ItemSt
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async loadItems({ namespaces, api }: KubeObjectStoreLoadingParams): Promise<T[]> {
|
protected async loadItems({ namespaces, api, reqInit }: KubeObjectStoreLoadingParams): Promise<T[]> {
|
||||||
if (this.context?.cluster.isAllowedResource(api.kind)) {
|
if (this.context?.cluster.isAllowedResource(api.kind)) {
|
||||||
if (!api.isNamespaced) {
|
if (!api.isNamespaced) {
|
||||||
return api.list({}, this.query);
|
return api.list({ reqInit }, this.query);
|
||||||
}
|
}
|
||||||
|
|
||||||
const isLoadingAll = this.context.allNamespaces?.length > 1
|
const isLoadingAll = this.context.allNamespaces?.length > 1
|
||||||
@ -116,13 +121,13 @@ export abstract class KubeObjectStore<T extends KubeObject = any> extends ItemSt
|
|||||||
if (isLoadingAll) {
|
if (isLoadingAll) {
|
||||||
this.loadedNamespaces = [];
|
this.loadedNamespaces = [];
|
||||||
|
|
||||||
return api.list({}, this.query);
|
return api.list({ reqInit }, this.query);
|
||||||
} else {
|
} else {
|
||||||
this.loadedNamespaces = namespaces;
|
this.loadedNamespaces = namespaces;
|
||||||
|
|
||||||
return Promise // load resources per namespace
|
return Promise // load resources per namespace
|
||||||
.all(namespaces.map(namespace => api.list({ namespace })))
|
.all(namespaces.map(namespace => api.list({ namespace, reqInit })))
|
||||||
.then(items => items.flat());
|
.then(items => items.flat().filter(Boolean));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -134,7 +139,7 @@ export abstract class KubeObjectStore<T extends KubeObject = any> extends ItemSt
|
|||||||
}
|
}
|
||||||
|
|
||||||
@action
|
@action
|
||||||
async loadAll(options: { namespaces?: string[], merge?: boolean } = {}): Promise<void | T[]> {
|
async loadAll(options: { namespaces?: string[], merge?: boolean, reqInit?: RequestInit } = {}): Promise<void | T[]> {
|
||||||
await this.contextReady;
|
await this.contextReady;
|
||||||
this.isLoading = true;
|
this.isLoading = true;
|
||||||
|
|
||||||
@ -142,9 +147,10 @@ export abstract class KubeObjectStore<T extends KubeObject = any> extends ItemSt
|
|||||||
const {
|
const {
|
||||||
namespaces = this.context.allNamespaces, // load all namespaces by default
|
namespaces = this.context.allNamespaces, // load all namespaces by default
|
||||||
merge = true, // merge loaded items or return as result
|
merge = true, // merge loaded items or return as result
|
||||||
|
reqInit,
|
||||||
} = options;
|
} = options;
|
||||||
|
|
||||||
const items = await this.loadItems({ namespaces, api: this.api });
|
const items = await this.loadItems({ namespaces, api: this.api, reqInit });
|
||||||
|
|
||||||
if (merge) {
|
if (merge) {
|
||||||
this.mergeItems(items, { replace: false });
|
this.mergeItems(items, { replace: false });
|
||||||
@ -157,7 +163,10 @@ export abstract class KubeObjectStore<T extends KubeObject = any> extends ItemSt
|
|||||||
|
|
||||||
return items;
|
return items;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Loading store items failed", { error, store: this });
|
if (error.message) {
|
||||||
|
Notifications.error(error.message);
|
||||||
|
}
|
||||||
|
console.error("Loading store items failed", { error });
|
||||||
this.resetOnError(error);
|
this.resetOnError(error);
|
||||||
this.failedLoading = true;
|
this.failedLoading = true;
|
||||||
} finally {
|
} finally {
|
||||||
@ -274,17 +283,21 @@ export abstract class KubeObjectStore<T extends KubeObject = any> extends ItemSt
|
|||||||
|
|
||||||
subscribe(apis = this.getSubscribeApis()) {
|
subscribe(apis = this.getSubscribeApis()) {
|
||||||
const abortController = new AbortController();
|
const abortController = new AbortController();
|
||||||
const namespaces = [...this.loadedNamespaces];
|
|
||||||
|
|
||||||
if (this.context.cluster?.isGlobalWatchEnabled && namespaces.length === 0) {
|
// This waits for the context and namespaces to be ready or fails fast if the disposer is called
|
||||||
apis.forEach(api => this.watchNamespace(api, "", abortController));
|
Promise.race([rejectPromiseBy(abortController.signal), Promise.all([this.contextReady, this.namespacesReady])])
|
||||||
} else {
|
.then(() => {
|
||||||
apis.forEach(api => {
|
if (this.context.cluster.isGlobalWatchEnabled && this.loadedNamespaces.length === 0) {
|
||||||
this.loadedNamespaces.forEach((namespace) => {
|
apis.forEach(api => this.watchNamespace(api, "", abortController));
|
||||||
this.watchNamespace(api, namespace, abortController);
|
} else {
|
||||||
});
|
apis.forEach(api => {
|
||||||
});
|
this.loadedNamespaces.forEach((namespace) => {
|
||||||
}
|
this.watchNamespace(api, namespace, abortController);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(noop); // ignore DOMExceptions
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
abortController.abort();
|
abortController.abort();
|
||||||
@ -293,48 +306,38 @@ export abstract class KubeObjectStore<T extends KubeObject = any> extends ItemSt
|
|||||||
|
|
||||||
private watchNamespace(api: KubeApi<T>, namespace: string, abortController: AbortController) {
|
private watchNamespace(api: KubeApi<T>, namespace: string, abortController: AbortController) {
|
||||||
let timedRetry: NodeJS.Timeout;
|
let timedRetry: NodeJS.Timeout;
|
||||||
|
const watch = () => api.watch({
|
||||||
|
namespace,
|
||||||
|
abortController,
|
||||||
|
callback
|
||||||
|
});
|
||||||
|
|
||||||
abortController.signal.addEventListener("abort", () => clearTimeout(timedRetry));
|
const { signal } = abortController;
|
||||||
|
|
||||||
const callback = (data: IKubeWatchEvent, error: any) => {
|
const callback = (data: IKubeWatchEvent, error: any) => {
|
||||||
if (!this.isLoaded || abortController.signal.aborted) return;
|
if (!this.isLoaded || error instanceof DOMException) return;
|
||||||
|
|
||||||
if (error instanceof Response) {
|
if (error instanceof Response) {
|
||||||
if (error.status === 404) {
|
if (error.status === 404) {
|
||||||
// api has gone, let's not retry
|
// api has gone, let's not retry
|
||||||
return;
|
return;
|
||||||
} else { // not sure what to do, best to retry
|
|
||||||
if (timedRetry) clearTimeout(timedRetry);
|
|
||||||
timedRetry = setTimeout(() => {
|
|
||||||
api.watch({
|
|
||||||
namespace,
|
|
||||||
abortController,
|
|
||||||
callback
|
|
||||||
});
|
|
||||||
}, 5000);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// not sure what to do, best to retry
|
||||||
|
clearTimeout(timedRetry);
|
||||||
|
timedRetry = setTimeout(watch, 5000);
|
||||||
} else if (error instanceof KubeStatus && error.code === 410) {
|
} else if (error instanceof KubeStatus && error.code === 410) {
|
||||||
if (timedRetry) clearTimeout(timedRetry);
|
clearTimeout(timedRetry);
|
||||||
// resourceVersion has gone, let's try to reload
|
// resourceVersion has gone, let's try to reload
|
||||||
timedRetry = setTimeout(() => {
|
timedRetry = setTimeout(() => {
|
||||||
(namespace === "" ? this.loadAll({ merge: false }) : this.loadAll({ namespaces: [namespace] })).then(() => {
|
(
|
||||||
api.watch({
|
namespace
|
||||||
namespace,
|
? this.loadAll({ namespaces: [namespace], reqInit: { signal } }) : this.loadAll({ merge: false, reqInit: { signal } })
|
||||||
abortController,
|
).then(watch);
|
||||||
callback
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}, 1000);
|
}, 1000);
|
||||||
} else if (error) { // not sure what to do, best to retry
|
} else if (error) { // not sure what to do, best to retry
|
||||||
if (timedRetry) clearTimeout(timedRetry);
|
clearTimeout(timedRetry);
|
||||||
|
timedRetry = setTimeout(watch, 5000);
|
||||||
timedRetry = setTimeout(() => {
|
|
||||||
api.watch({
|
|
||||||
namespace,
|
|
||||||
abortController,
|
|
||||||
callback
|
|
||||||
});
|
|
||||||
}, 5000);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (data) {
|
if (data) {
|
||||||
@ -342,11 +345,8 @@ export abstract class KubeObjectStore<T extends KubeObject = any> extends ItemSt
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
api.watch({
|
signal.addEventListener("abort", () => clearTimeout(timedRetry));
|
||||||
namespace,
|
watch();
|
||||||
abortController,
|
|
||||||
callback: (data, error) => callback(data, error)
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@action.bound
|
@action.bound
|
||||||
|
|||||||
34
src/renderer/mui-base-theme.tsx
Normal file
34
src/renderer/mui-base-theme.tsx
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { createMuiTheme, ThemeProvider } from "@material-ui/core";
|
||||||
|
|
||||||
|
const defaultTheme = createMuiTheme({
|
||||||
|
props: {
|
||||||
|
MuiIconButton: {
|
||||||
|
color: "inherit",
|
||||||
|
},
|
||||||
|
MuiSvgIcon: {
|
||||||
|
fontSize: "inherit",
|
||||||
|
},
|
||||||
|
MuiTooltip: {
|
||||||
|
placement: "top",
|
||||||
|
}
|
||||||
|
},
|
||||||
|
overrides: {
|
||||||
|
MuiIconButton: {
|
||||||
|
root: {
|
||||||
|
"&:hover": {
|
||||||
|
color: "var(--iconActiveColor)",
|
||||||
|
backgroundColor: "var(--iconActiveBackground)",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export function DefaultProps(App: React.ComponentType) {
|
||||||
|
return (
|
||||||
|
<ThemeProvider theme= { defaultTheme } >
|
||||||
|
<App />
|
||||||
|
</ThemeProvider>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -2,7 +2,6 @@
|
|||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<title>OpenLens - Open Source Kubernetes IDE</title>
|
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|
||||||
|
|||||||
90
yarn.lock
90
yarn.lock
@ -838,6 +838,13 @@
|
|||||||
react-is "^16.8.0"
|
react-is "^16.8.0"
|
||||||
react-transition-group "^4.4.0"
|
react-transition-group "^4.4.0"
|
||||||
|
|
||||||
|
"@material-ui/icons@^4.11.2":
|
||||||
|
version "4.11.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/@material-ui/icons/-/icons-4.11.2.tgz#b3a7353266519cd743b6461ae9fdfcb1b25eb4c5"
|
||||||
|
integrity sha512-fQNsKX2TxBmqIGJCSi3tGTO/gZ+eJgWmMJkgDiOfyNaunNaxcklJQFaFogYcFl0qFuaEz1qaXYXboa/bUXVSOQ==
|
||||||
|
dependencies:
|
||||||
|
"@babel/runtime" "^7.4.4"
|
||||||
|
|
||||||
"@material-ui/lab@^4.0.0-alpha.57":
|
"@material-ui/lab@^4.0.0-alpha.57":
|
||||||
version "4.0.0-alpha.57"
|
version "4.0.0-alpha.57"
|
||||||
resolved "https://registry.yarnpkg.com/@material-ui/lab/-/lab-4.0.0-alpha.57.tgz#e8961bcf6449e8a8dabe84f2700daacfcafbf83a"
|
resolved "https://registry.yarnpkg.com/@material-ui/lab/-/lab-4.0.0-alpha.57.tgz#e8961bcf6449e8a8dabe84f2700daacfcafbf83a"
|
||||||
@ -1364,13 +1371,13 @@
|
|||||||
jest-diff "^25.2.1"
|
jest-diff "^25.2.1"
|
||||||
pretty-format "^25.2.1"
|
pretty-format "^25.2.1"
|
||||||
|
|
||||||
"@types/jest@^25.2.3":
|
"@types/jest@^26.0.22":
|
||||||
version "25.2.3"
|
version "26.0.22"
|
||||||
resolved "https://registry.yarnpkg.com/@types/jest/-/jest-25.2.3.tgz#33d27e4c4716caae4eced355097a47ad363fdcaf"
|
resolved "https://registry.yarnpkg.com/@types/jest/-/jest-26.0.22.tgz#8308a1debdf1b807aa47be2838acdcd91e88fbe6"
|
||||||
integrity sha512-JXc1nK/tXHiDhV55dvfzqtmP4S3sy3T3ouV2tkViZgxY/zeUkcpQcQPGRlgF4KmWzWW5oiWYSZwtCB+2RsE4Fw==
|
integrity sha512-eeWwWjlqxvBxc4oQdkueW5OF/gtfSceKk4OnOAGlUSwS/liBRtZppbJuz1YkgbrbfGOoeBHun9fOvXnjNwrSOw==
|
||||||
dependencies:
|
dependencies:
|
||||||
jest-diff "^25.2.1"
|
jest-diff "^26.0.0"
|
||||||
pretty-format "^25.2.1"
|
pretty-format "^26.0.0"
|
||||||
|
|
||||||
"@types/js-yaml@^3.12.1", "@types/js-yaml@^3.12.4":
|
"@types/js-yaml@^3.12.1", "@types/js-yaml@^3.12.4":
|
||||||
version "3.12.4"
|
version "3.12.4"
|
||||||
@ -1465,9 +1472,9 @@
|
|||||||
integrity sha512-e3sW4oEH0qS1QxSfX7PT6xIi5qk/YSMsrB9Lq8EtkhQBZB+bKyfkP+jpLJRySanvBhAQPSv2PEBe81M8Iy/7yg==
|
integrity sha512-e3sW4oEH0qS1QxSfX7PT6xIi5qk/YSMsrB9Lq8EtkhQBZB+bKyfkP+jpLJRySanvBhAQPSv2PEBe81M8Iy/7yg==
|
||||||
|
|
||||||
"@types/node@*":
|
"@types/node@*":
|
||||||
version "14.0.11"
|
version "14.14.41"
|
||||||
resolved "https://registry.yarnpkg.com/@types/node/-/node-14.0.11.tgz#61d4886e2424da73b7b25547f59fdcb534c165a3"
|
resolved "https://registry.yarnpkg.com/@types/node/-/node-14.14.41.tgz#d0b939d94c1d7bd53d04824af45f1139b8c45615"
|
||||||
integrity sha512-lCvvI24L21ZVeIiyIUHZ5Oflv1hhHQ5E1S25IRlKIXaRkVgmXpJMI3wUJkmym2bTbCe+WoIibQnMVAU3FguaOg==
|
integrity sha512-dueRKfaJL4RTtSa7bWeTK1M+VH+Gns73oCgzvYfHZywRCoPSd8EkXBL0mZ9unPTveBn+D9phZBaxuzpwjWkW0g==
|
||||||
|
|
||||||
"@types/node@^10.12.0":
|
"@types/node@^10.12.0":
|
||||||
version "10.17.24"
|
version "10.17.24"
|
||||||
@ -1683,13 +1690,18 @@
|
|||||||
resolved "https://registry.yarnpkg.com/@types/retry/-/retry-0.12.0.tgz#2b35eccfcee7d38cd72ad99232fbd58bffb3c84d"
|
resolved "https://registry.yarnpkg.com/@types/retry/-/retry-0.12.0.tgz#2b35eccfcee7d38cd72ad99232fbd58bffb3c84d"
|
||||||
integrity sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==
|
integrity sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==
|
||||||
|
|
||||||
"@types/semver@^7.1.0", "@types/semver@^7.2.0":
|
"@types/semver@^7.2.0":
|
||||||
version "7.2.0"
|
version "7.2.0"
|
||||||
resolved "https://registry.yarnpkg.com/@types/semver/-/semver-7.2.0.tgz#0d72066965e910531e1db4621c15d0ca36b8d83b"
|
resolved "https://registry.yarnpkg.com/@types/semver/-/semver-7.2.0.tgz#0d72066965e910531e1db4621c15d0ca36b8d83b"
|
||||||
integrity sha512-TbB0A8ACUWZt3Y6bQPstW9QNbhNeebdgLX4T/ZfkrswAfUzRiXrgd9seol+X379Wa589Pu4UEx9Uok0D4RjRCQ==
|
integrity sha512-TbB0A8ACUWZt3Y6bQPstW9QNbhNeebdgLX4T/ZfkrswAfUzRiXrgd9seol+X379Wa589Pu4UEx9Uok0D4RjRCQ==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@types/node" "*"
|
"@types/node" "*"
|
||||||
|
|
||||||
|
"@types/semver@^7.3.4":
|
||||||
|
version "7.3.4"
|
||||||
|
resolved "https://registry.yarnpkg.com/@types/semver/-/semver-7.3.4.tgz#43d7168fec6fa0988bb1a513a697b29296721afb"
|
||||||
|
integrity sha512-+nVsLKlcUCeMzD2ufHEYuJ9a2ovstb6Dp52A5VsoKxDXgvE051XgHI/33I1EymwkRGQkwnA0LkhnUzituGs4EQ==
|
||||||
|
|
||||||
"@types/serve-static@*":
|
"@types/serve-static@*":
|
||||||
version "1.13.6"
|
version "1.13.6"
|
||||||
resolved "https://registry.yarnpkg.com/@types/serve-static/-/serve-static-1.13.6.tgz#866b1b8dec41c36e28c7be40ac725b88be43c5c1"
|
resolved "https://registry.yarnpkg.com/@types/serve-static/-/serve-static-1.13.6.tgz#866b1b8dec41c36e28c7be40ac725b88be43c5c1"
|
||||||
@ -3194,14 +3206,6 @@ buffer@^5.1.0, buffer@^5.5.0:
|
|||||||
base64-js "^1.0.2"
|
base64-js "^1.0.2"
|
||||||
ieee754 "^1.1.4"
|
ieee754 "^1.1.4"
|
||||||
|
|
||||||
builder-util-runtime@8.7.0:
|
|
||||||
version "8.7.0"
|
|
||||||
resolved "https://registry.yarnpkg.com/builder-util-runtime/-/builder-util-runtime-8.7.0.tgz#e48ad004835c8284662e8eaf47a53468c66e8e8d"
|
|
||||||
integrity sha512-G1AqqVM2vYTrSFR982c1NNzwXKrGLQjVjaZaWQdn4O6Z3YKjdMDofw88aD9jpyK9ZXkrCxR0tI3Qe9wNbyTlXg==
|
|
||||||
dependencies:
|
|
||||||
debug "^4.1.1"
|
|
||||||
sax "^1.2.4"
|
|
||||||
|
|
||||||
builder-util-runtime@8.7.3:
|
builder-util-runtime@8.7.3:
|
||||||
version "8.7.3"
|
version "8.7.3"
|
||||||
resolved "https://registry.yarnpkg.com/builder-util-runtime/-/builder-util-runtime-8.7.3.tgz#0aaafa52d25295c939496f62231ca9ff06c30e40"
|
resolved "https://registry.yarnpkg.com/builder-util-runtime/-/builder-util-runtime-8.7.3.tgz#0aaafa52d25295c939496f62231ca9ff06c30e40"
|
||||||
@ -4972,17 +4976,17 @@ electron-publish@22.10.5:
|
|||||||
mime "^2.5.0"
|
mime "^2.5.0"
|
||||||
|
|
||||||
electron-updater@^4.3.1:
|
electron-updater@^4.3.1:
|
||||||
version "4.3.1"
|
version "4.3.8"
|
||||||
resolved "https://registry.yarnpkg.com/electron-updater/-/electron-updater-4.3.1.tgz#9d485b6262bc56fcf7ee62b1dc1b3b105a3e96a7"
|
resolved "https://registry.yarnpkg.com/electron-updater/-/electron-updater-4.3.8.tgz#94f1731682a756385726183e2b04b959cb319456"
|
||||||
integrity sha512-UDC5AHCgeiHJYDYWZG/rsl1vdAFKqI/Lm7whN57LKAk8EfhTewhcEHzheRcncLgikMcQL8gFo1KeX51tf5a5Wg==
|
integrity sha512-/tB82Ogb2LqaXrUzAD8waJC+TZV52Pr0Znfj7w+i4D+jA2GgrKFI3Pxjp+36y9FcBMQz7kYsMHcB6c5zBJao+A==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@types/semver" "^7.1.0"
|
"@types/semver" "^7.3.4"
|
||||||
builder-util-runtime "8.7.0"
|
builder-util-runtime "8.7.3"
|
||||||
fs-extra "^9.0.0"
|
fs-extra "^9.1.0"
|
||||||
js-yaml "^3.13.1"
|
js-yaml "^4.0.0"
|
||||||
lazy-val "^1.0.4"
|
lazy-val "^1.0.4"
|
||||||
lodash.isequal "^4.5.0"
|
lodash.isequal "^4.5.0"
|
||||||
semver "^7.1.3"
|
semver "^7.3.4"
|
||||||
|
|
||||||
electron-window-state@^5.0.3:
|
electron-window-state@^5.0.3:
|
||||||
version "5.0.3"
|
version "5.0.3"
|
||||||
@ -6526,11 +6530,16 @@ got@^9.6.0:
|
|||||||
to-readable-stream "^1.0.0"
|
to-readable-stream "^1.0.0"
|
||||||
url-parse-lax "^3.0.0"
|
url-parse-lax "^3.0.0"
|
||||||
|
|
||||||
graceful-fs@^4.1.0, graceful-fs@^4.1.11, graceful-fs@^4.1.15, graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.2.0, graceful-fs@^4.2.2, graceful-fs@^4.2.4:
|
graceful-fs@^4.1.0, graceful-fs@^4.1.11, graceful-fs@^4.1.15, graceful-fs@^4.1.2, graceful-fs@^4.2.2, graceful-fs@^4.2.4:
|
||||||
version "4.2.4"
|
version "4.2.4"
|
||||||
resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.4.tgz#2256bde14d3632958c465ebc96dc467ca07a29fb"
|
resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.4.tgz#2256bde14d3632958c465ebc96dc467ca07a29fb"
|
||||||
integrity sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw==
|
integrity sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw==
|
||||||
|
|
||||||
|
graceful-fs@^4.1.6, graceful-fs@^4.2.0:
|
||||||
|
version "4.2.6"
|
||||||
|
resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.6.tgz#ff040b2b0853b23c3d31027523706f1885d76bee"
|
||||||
|
integrity sha512-nTnJ528pbqxYanhpDYsi4Rd8MAeaBA67+RZ10CM1m3bTAVFEDcd5AuA4a6W5YkGZ1iNXHzZz8T6TBKLeBuNriQ==
|
||||||
|
|
||||||
"graceful-readlink@>= 1.0.0":
|
"graceful-readlink@>= 1.0.0":
|
||||||
version "1.0.1"
|
version "1.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/graceful-readlink/-/graceful-readlink-1.0.1.tgz#4cafad76bc62f02fa039b2f94e9a3dd3a391a725"
|
resolved "https://registry.yarnpkg.com/graceful-readlink/-/graceful-readlink-1.0.1.tgz#4cafad76bc62f02fa039b2f94e9a3dd3a391a725"
|
||||||
@ -8215,7 +8224,15 @@ js-sha3@^0.8.0:
|
|||||||
resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499"
|
resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499"
|
||||||
integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==
|
integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==
|
||||||
|
|
||||||
js-yaml@^3.13.1, js-yaml@^3.14.0:
|
js-yaml@^3.13.1:
|
||||||
|
version "3.14.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.14.1.tgz#dae812fdb3825fa306609a8717383c50c36a0537"
|
||||||
|
integrity sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==
|
||||||
|
dependencies:
|
||||||
|
argparse "^1.0.7"
|
||||||
|
esprima "^4.0.0"
|
||||||
|
|
||||||
|
js-yaml@^3.14.0:
|
||||||
version "3.14.0"
|
version "3.14.0"
|
||||||
resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.14.0.tgz#a7a34170f26a21bb162424d8adacb4113a69e482"
|
resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.14.0.tgz#a7a34170f26a21bb162424d8adacb4113a69e482"
|
||||||
integrity sha512-/4IbIeHcD9VMHFqDR/gQ7EdZdLimOvW2DdcxFjdyyZ9NsbS+ccrXqVWDtab/lRl5AlUqmpBx8EhPaWR+OtY17A==
|
integrity sha512-/4IbIeHcD9VMHFqDR/gQ7EdZdLimOvW2DdcxFjdyyZ9NsbS+ccrXqVWDtab/lRl5AlUqmpBx8EhPaWR+OtY17A==
|
||||||
@ -8371,11 +8388,11 @@ jsonfile@^4.0.0:
|
|||||||
graceful-fs "^4.1.6"
|
graceful-fs "^4.1.6"
|
||||||
|
|
||||||
jsonfile@^6.0.1:
|
jsonfile@^6.0.1:
|
||||||
version "6.0.1"
|
version "6.1.0"
|
||||||
resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-6.0.1.tgz#98966cba214378c8c84b82e085907b40bf614179"
|
resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-6.1.0.tgz#bc55b2634793c679ec6403094eb13698a6ec0aae"
|
||||||
integrity sha512-jR2b5v7d2vIOust+w3wtFKZIfpC2pnRmFAhAC/BuweZFQR8qZzxH1OyrQ10HmdVYiXWkYUqPVsz91cG7EL2FBg==
|
integrity sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==
|
||||||
dependencies:
|
dependencies:
|
||||||
universalify "^1.0.0"
|
universalify "^2.0.0"
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
graceful-fs "^4.1.6"
|
graceful-fs "^4.1.6"
|
||||||
|
|
||||||
@ -12330,7 +12347,7 @@ semver-diff@^3.1.1:
|
|||||||
resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7"
|
resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7"
|
||||||
integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==
|
integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==
|
||||||
|
|
||||||
semver@7.x, semver@^7.1.3, semver@^7.2.1, semver@^7.3.2:
|
semver@7.x, semver@^7.2.1, semver@^7.3.2:
|
||||||
version "7.3.2"
|
version "7.3.2"
|
||||||
resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.2.tgz#604962b052b81ed0786aae84389ffba70ffd3938"
|
resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.2.tgz#604962b052b81ed0786aae84389ffba70ffd3938"
|
||||||
integrity sha512-OrOb32TeeambH6UrhtShmF7CRDqhL6/5XpPNp2DuRH6+9QLw/orhp72j87v8Qa1ScDkvrrBNpZcDejAirJmfXQ==
|
integrity sha512-OrOb32TeeambH6UrhtShmF7CRDqhL6/5XpPNp2DuRH6+9QLw/orhp72j87v8Qa1ScDkvrrBNpZcDejAirJmfXQ==
|
||||||
@ -13650,7 +13667,7 @@ ts-essentials@^4.0.0:
|
|||||||
resolved "https://registry.yarnpkg.com/ts-essentials/-/ts-essentials-4.0.0.tgz#506c42b270bbd0465574b90416533175b09205ab"
|
resolved "https://registry.yarnpkg.com/ts-essentials/-/ts-essentials-4.0.0.tgz#506c42b270bbd0465574b90416533175b09205ab"
|
||||||
integrity sha512-uQJX+SRY9mtbKU+g9kl5Fi7AEMofPCvHfJkQlaygpPmHPZrtgaBqbWFOYyiA47RhnSwwnXdepUJrgqUYxoUyhQ==
|
integrity sha512-uQJX+SRY9mtbKU+g9kl5Fi7AEMofPCvHfJkQlaygpPmHPZrtgaBqbWFOYyiA47RhnSwwnXdepUJrgqUYxoUyhQ==
|
||||||
|
|
||||||
ts-jest@^26.1.0:
|
ts-jest@26.3.0:
|
||||||
version "26.3.0"
|
version "26.3.0"
|
||||||
resolved "https://registry.yarnpkg.com/ts-jest/-/ts-jest-26.3.0.tgz#6b2845045347dce394f069bb59358253bc1338a9"
|
resolved "https://registry.yarnpkg.com/ts-jest/-/ts-jest-26.3.0.tgz#6b2845045347dce394f069bb59358253bc1338a9"
|
||||||
integrity sha512-Jq2uKfx6bPd9+JDpZNMBJMdMQUC3sJ08acISj8NXlVgR2d5OqslEHOR2KHMgwymu8h50+lKIm0m0xj/ioYdW2Q==
|
integrity sha512-Jq2uKfx6bPd9+JDpZNMBJMdMQUC3sJ08acISj8NXlVgR2d5OqslEHOR2KHMgwymu8h50+lKIm0m0xj/ioYdW2Q==
|
||||||
@ -13945,11 +13962,6 @@ universalify@^0.1.0:
|
|||||||
resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.1.2.tgz#b646f69be3942dabcecc9d6639c80dc105efaa66"
|
resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.1.2.tgz#b646f69be3942dabcecc9d6639c80dc105efaa66"
|
||||||
integrity sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==
|
integrity sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==
|
||||||
|
|
||||||
universalify@^1.0.0:
|
|
||||||
version "1.0.0"
|
|
||||||
resolved "https://registry.yarnpkg.com/universalify/-/universalify-1.0.0.tgz#b61a1da173e8435b2fe3c67d29b9adf8594bd16d"
|
|
||||||
integrity sha512-rb6X1W158d7pRQBg5gkR8uPaSfiids68LTJQYOtEUhoJUWBdaQHsuT/EUduxXYxcrt4r5PJ4fuHW1MHT6p0qug==
|
|
||||||
|
|
||||||
universalify@^2.0.0:
|
universalify@^2.0.0:
|
||||||
version "2.0.0"
|
version "2.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/universalify/-/universalify-2.0.0.tgz#75a4984efedc4b08975c5aeb73f530d02df25717"
|
resolved "https://registry.yarnpkg.com/universalify/-/universalify-2.0.0.tgz#75a4984efedc4b08975c5aeb73f530d02df25717"
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user