mirror of
https://github.com/lensapp/lens.git
synced 2025-05-20 05:10:56 +00:00
Views management refactoring (#669)
* Views management refactoring Signed-off-by: Roman <ixrock@gmail.com> Signed-off-by: Lauri Nevala <lauri.nevala@gmail.com>
This commit is contained in:
parent
592c8920b2
commit
47bcb63c95
2
.gitignore
vendored
2
.gitignore
vendored
@ -5,7 +5,7 @@ node_modules/
|
||||
yarn-error.log
|
||||
coverage/
|
||||
tmp/
|
||||
static/build/client/
|
||||
static/build/**
|
||||
binaries/client/
|
||||
binaries/server/
|
||||
locales/**/**.js
|
||||
|
||||
@ -3,13 +3,13 @@ import { Application } from "spectron";
|
||||
let appPath = ""
|
||||
switch(process.platform) {
|
||||
case "win32":
|
||||
appPath = "./dist/win-unpacked/LensDev.exe"
|
||||
appPath = "./dist/win-unpacked/Lens.exe"
|
||||
break
|
||||
case "linux":
|
||||
appPath = "./dist/linux-unpacked/kontena-lens"
|
||||
break
|
||||
case "darwin":
|
||||
appPath = "./dist/mac/LensDev.app/Contents/MacOS/LensDev"
|
||||
appPath = "./dist/mac/Lens.app/Contents/MacOS/Lens"
|
||||
break
|
||||
}
|
||||
|
||||
@ -20,6 +20,10 @@ export function setup(): Application {
|
||||
path: appPath,
|
||||
startTimeout: 30000,
|
||||
waitTimeout: 30000,
|
||||
chromeDriverArgs: ['remote-debugging-port=9222'],
|
||||
env: {
|
||||
CICD: "true"
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@ -1,7 +1,6 @@
|
||||
import { Application } from "spectron"
|
||||
import * as util from "../helpers/utils"
|
||||
import { spawnSync } from "child_process"
|
||||
import { stat } from "fs"
|
||||
|
||||
jest.setTimeout(20000)
|
||||
|
||||
@ -11,19 +10,21 @@ describe("app start", () => {
|
||||
let app: Application
|
||||
const clickWhatsNew = async (app: Application) => {
|
||||
await app.client.waitUntilTextExists("h1", "What's new")
|
||||
await app.client.click("button.btn-primary")
|
||||
await app.client.click("button.primary")
|
||||
await app.client.waitUntilTextExists("h1", "Welcome")
|
||||
}
|
||||
|
||||
const addMinikubeCluster = async (app: Application) => {
|
||||
await app.client.click("a#add-cluster")
|
||||
await app.client.waitUntilTextExists("legend", "Choose config:")
|
||||
await app.client.selectByVisibleText("select#kubecontext-select", "minikube (new)")
|
||||
await app.client.click("button.btn-primary")
|
||||
await app.client.click("div.add-cluster")
|
||||
await app.client.waitUntilTextExists("p", "Choose config")
|
||||
await app.client.click("div#kubecontext-select")
|
||||
await app.client.waitUntilTextExists("div", "minikube")
|
||||
await app.client.click("div.minikube")
|
||||
await app.client.click("button.primary")
|
||||
}
|
||||
|
||||
const waitForMinikubeDashboard = async (app: Application) => {
|
||||
await app.client.waitUntilTextExists("pre.auth-output", "Authentication proxy started")
|
||||
await app.client.waitUntilTextExists("pre.kube-auth-out", "Authentication proxy started")
|
||||
let windowCount = await app.client.getWindowCount()
|
||||
// wait for webview to appear on window count
|
||||
while (windowCount == 1) {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
39
package.json
39
package.json
@ -3,7 +3,7 @@
|
||||
"productName": "Lens",
|
||||
"description": "Lens - The Kubernetes IDE",
|
||||
"version": "3.6.0-dev",
|
||||
"main": "out/main.js",
|
||||
"main": "static/build/main.js",
|
||||
"copyright": "© 2020, Lakend Labs, Inc.",
|
||||
"license": "MIT",
|
||||
"author": {
|
||||
@ -12,29 +12,28 @@
|
||||
},
|
||||
"scripts": {
|
||||
"dev": "concurrently -k \"yarn dev-run -C\" \"yarn dev:main\" \"yarn dev:renderer\"",
|
||||
"dev-run": "nodemon --watch out/main.* --exec \"electron --inspect .\" $@",
|
||||
"dev-run": "nodemon --watch static/build/main.js --exec \"electron --inspect .\" $@",
|
||||
"dev:main": "env DEBUG=true yarn compile:main --watch $@",
|
||||
"dev:renderer": "env DEBUG=true yarn compile:renderer --watch $@",
|
||||
"compile": "concurrently \"yarn i18n:compile\" \"yarn compile:main -p\" \"yarn compile:renderer -p\"",
|
||||
"compile:main": "webpack --progress --config webpack.main.ts",
|
||||
"compile:renderer": "webpack --progress --config webpack.renderer.ts",
|
||||
"compile:dll": "webpack --config webpack.dll.ts",
|
||||
"build:linux": "yarn compile && electron-builder --linux --dir -c.productName=LensDev",
|
||||
"build:mac": "yarn compile && electron-builder --mac --dir -c.productName=LensDev",
|
||||
"build:win": "yarn compile && electron-builder --win --dir -c.productName=LensDev",
|
||||
"compile": "env NODE_ENV=production concurrently yarn:compile:*",
|
||||
"compile:main": "webpack --config webpack.main.ts",
|
||||
"compile:renderer": "webpack --config webpack.renderer.ts",
|
||||
"compile:i18n": "lingui compile",
|
||||
"build:linux": "yarn compile && electron-builder --linux --dir -c.productName=Lens",
|
||||
"build:mac": "yarn compile && electron-builder --mac --dir -c.productName=Lens",
|
||||
"build:win": "yarn compile && electron-builder --win --dir -c.productName=Lens",
|
||||
"test": "jest --env=jsdom src $@",
|
||||
"integration": "jest --coverage integration $@",
|
||||
"dist": "yarn compile && electron-builder -p onTag",
|
||||
"dist:win": "yarn compile && electron-builder -p onTag --x64 --ia32",
|
||||
"dist": "yarn compile && electron-builder --publish onTag",
|
||||
"dist:win": "yarn compile && electron-builder --publish onTag --x64 --ia32",
|
||||
"dist:dir": "yarn dist --dir -c.compression=store -c.mac.identity=null",
|
||||
"postinstall": "patch-package",
|
||||
"i18n:extract": "lingui extract",
|
||||
"i18n:compile": "lingui compile",
|
||||
"download-bins": "concurrently yarn:download:*",
|
||||
"download:kubectl": "yarn run ts-node build/download_kubectl.ts",
|
||||
"download:helm": "yarn run ts-node build/download_helm.ts",
|
||||
"lint": "eslint $@ --ext js,ts,tsx --max-warnings=0 src/",
|
||||
"rebuild-pty": "electron-rebuild -f -w node-pty"
|
||||
"rebuild-pty": "yarn run electron-rebuild -f -w node-pty"
|
||||
},
|
||||
"config": {
|
||||
"bundledKubectlVersion": "1.17.4",
|
||||
@ -88,7 +87,7 @@
|
||||
{
|
||||
"from": "static/",
|
||||
"to": "static/",
|
||||
"filter": "**/*"
|
||||
"filter": "!**/main.js"
|
||||
},
|
||||
"LICENSE"
|
||||
],
|
||||
@ -187,11 +186,15 @@
|
||||
"mac-ca": "^1.0.4",
|
||||
"marked": "^1.1.0",
|
||||
"md5-file": "^5.0.0",
|
||||
"mobx": "^5.15.5",
|
||||
"mobx-observable-history": "^1.0.3",
|
||||
"mock-fs": "^4.12.0",
|
||||
"node-machine-id": "^1.1.12",
|
||||
"node-pty": "^0.9.0",
|
||||
"openid-client": "^3.15.2",
|
||||
"path-to-regexp": "^6.1.0",
|
||||
"proper-lockfile": "^4.1.1",
|
||||
"react-router": "^5.2.0",
|
||||
"request": "^2.88.2",
|
||||
"request-promise-native": "^1.0.8",
|
||||
"semver": "^7.3.2",
|
||||
@ -233,7 +236,6 @@
|
||||
"@types/md5-file": "^4.0.2",
|
||||
"@types/mini-css-extract-plugin": "^0.9.1",
|
||||
"@types/react": "^16.9.35",
|
||||
"@types/react-dom": "^16.9.8",
|
||||
"@types/react-router-dom": "^5.1.5",
|
||||
"@types/react-select": "^3.0.13",
|
||||
"@types/react-window": "^1.8.2",
|
||||
@ -265,7 +267,7 @@
|
||||
"css-element-queries": "^1.2.3",
|
||||
"css-loader": "^3.5.3",
|
||||
"dompurify": "^2.0.11",
|
||||
"electron": "^9.1.0",
|
||||
"electron": "^9.1.2",
|
||||
"electron-builder": "^22.7.0",
|
||||
"electron-notarize": "^0.3.0",
|
||||
"electron-rebuild": "^1.11.0",
|
||||
@ -281,15 +283,12 @@
|
||||
"make-plural": "^6.2.1",
|
||||
"material-design-icons": "^3.0.1",
|
||||
"mini-css-extract-plugin": "^0.9.0",
|
||||
"mobx": "^5.15.4",
|
||||
"mobx-observable-history": "^1.0.3",
|
||||
"mobx-react": "^6.2.2",
|
||||
"moment": "^2.26.0",
|
||||
"node-loader": "^0.6.0",
|
||||
"node-sass": "^4.14.1",
|
||||
"nodemon": "^2.0.4",
|
||||
"patch-package": "^6.2.2",
|
||||
"path-to-regexp": "^6.1.0",
|
||||
"postinstall-postinstall": "^2.1.0",
|
||||
"raw-loader": "^4.0.1",
|
||||
"react": "^16.13.1",
|
||||
@ -298,7 +297,7 @@
|
||||
"react-select": "^3.1.0",
|
||||
"react-window": "^1.8.5",
|
||||
"sass-loader": "^8.0.2",
|
||||
"spectron": "^8.0.0",
|
||||
"spectron": "11.0.0",
|
||||
"style-loader": "^1.2.1",
|
||||
"terser-webpack-plugin": "^3.0.3",
|
||||
"ts-jest": "^26.1.0",
|
||||
|
||||
@ -3,16 +3,26 @@ import { ClusterId, clusterStore } from "./cluster-store";
|
||||
import { tracker } from "./tracker";
|
||||
|
||||
export const clusterIpc = {
|
||||
init: createIpcChannel({
|
||||
channel: "cluster:init",
|
||||
handle: async (clusterId: ClusterId, frameId: number) => {
|
||||
const cluster = clusterStore.getById(clusterId);
|
||||
if (cluster) {
|
||||
cluster.frameId = frameId; // save cluster's webFrame.routingId to be able to send push-updates
|
||||
return cluster.pushState();
|
||||
}
|
||||
},
|
||||
}),
|
||||
activate: createIpcChannel({
|
||||
channel: "cluster:activate",
|
||||
handle: async (clusterId: ClusterId = clusterStore.activeClusterId) => {
|
||||
handle: (clusterId: ClusterId) => {
|
||||
return clusterStore.getById(clusterId)?.activate();
|
||||
},
|
||||
}),
|
||||
|
||||
disconnect: createIpcChannel({
|
||||
channel: "cluster:disconnect",
|
||||
handle: (clusterId: ClusterId = clusterStore.activeClusterId) => {
|
||||
handle: (clusterId: ClusterId) => {
|
||||
tracker.event("cluster", "stop");
|
||||
return clusterStore.getById(clusterId)?.disconnect();
|
||||
},
|
||||
@ -23,7 +33,6 @@ export const clusterIpc = {
|
||||
handle: async (clusterId: ClusterId, feature: string, config?: any) => {
|
||||
tracker.event("cluster", "install", feature);
|
||||
const cluster = clusterStore.getById(clusterId);
|
||||
|
||||
if (cluster) {
|
||||
await cluster.installFeature(feature, config)
|
||||
} else {
|
||||
|
||||
@ -1,10 +1,8 @@
|
||||
import type { WorkspaceId } from "./workspace-store";
|
||||
import path from "path";
|
||||
import filenamify from "filenamify";
|
||||
import { app, ipcRenderer, remote } from "electron";
|
||||
import { copyFile, ensureDir, unlink } from "fs-extra";
|
||||
import { unlink } from "fs-extra";
|
||||
import { action, computed, observable, toJS } from "mobx";
|
||||
import { appProto, noClustersHost } from "./vars";
|
||||
import { BaseStore } from "./base-store";
|
||||
import { Cluster, ClusterState } from "../main/cluster";
|
||||
import migrations from "../migrations/cluster-store"
|
||||
@ -64,11 +62,10 @@ export class ClusterStore extends BaseStore<ClusterStoreModel> {
|
||||
migrations: migrations,
|
||||
});
|
||||
if (ipcRenderer) {
|
||||
ipcRenderer.on("cluster:state", (event, clusterState: ClusterState) => {
|
||||
ipcRenderer.on("cluster:state", (event, model: ClusterState) => {
|
||||
this.applyWithoutSync(() => {
|
||||
logger.debug(`[CLUSTER-STORE]: received state update for cluster=${clusterState.id}`, clusterState);
|
||||
const cluster = this.getById(clusterState.id);
|
||||
if (cluster) cluster.updateModel(clusterState)
|
||||
logger.debug(`[CLUSTER-STORE]: received push-state at ${location.host}`, model);
|
||||
this.getById(model.id)?.updateModel(model);
|
||||
})
|
||||
})
|
||||
}
|
||||
@ -86,6 +83,10 @@ export class ClusterStore extends BaseStore<ClusterStoreModel> {
|
||||
return Array.from(this.clusters.values());
|
||||
}
|
||||
|
||||
isActive(id: ClusterId) {
|
||||
return this.activeClusterId === id;
|
||||
}
|
||||
|
||||
setActive(id: ClusterId) {
|
||||
this.activeClusterId = id;
|
||||
}
|
||||
@ -162,11 +163,6 @@ export class ClusterStore extends BaseStore<ClusterStoreModel> {
|
||||
this.activeClusterId = newClusters.has(activeCluster) ? activeCluster : null;
|
||||
this.clusters.replace(newClusters);
|
||||
this.removedClusters.replace(removedClusters);
|
||||
|
||||
// "auto-select" first cluster if available
|
||||
if (!this.activeClusterId && newClusters.size) {
|
||||
this.activeClusterId = Array.from(newClusters.values())[0].id;
|
||||
}
|
||||
}
|
||||
|
||||
toJSON(): ClusterStoreModel {
|
||||
@ -181,12 +177,11 @@ export class ClusterStore extends BaseStore<ClusterStoreModel> {
|
||||
|
||||
export const clusterStore = ClusterStore.getInstance<ClusterStore>();
|
||||
|
||||
export function isNoClustersView() {
|
||||
return location.hostname === noClustersHost
|
||||
}
|
||||
|
||||
export function getHostedClusterId() {
|
||||
return location.hostname.split(".")[0];
|
||||
export function getHostedClusterId(): ClusterId {
|
||||
const clusterHost = location.hostname.match(/^(.*?)\.localhost/);
|
||||
if (clusterHost) {
|
||||
return clusterHost[1]
|
||||
}
|
||||
}
|
||||
|
||||
export function getHostedCluster(): Cluster {
|
||||
|
||||
333
src/common/cluster-store_test.ts
Normal file
333
src/common/cluster-store_test.ts
Normal file
@ -0,0 +1,333 @@
|
||||
import fs from "fs";
|
||||
import mockFs from "mock-fs";
|
||||
import yaml from "js-yaml";
|
||||
import { Cluster } from "../main/cluster";
|
||||
import { ClusterStore } from "./cluster-store";
|
||||
import { workspaceStore } from "./workspace-store";
|
||||
import { saveConfigToAppFiles } from "./kube-helpers";
|
||||
|
||||
let clusterStore: ClusterStore;
|
||||
|
||||
describe("empty config", () => {
|
||||
beforeAll(() => {
|
||||
ClusterStore.resetInstance();
|
||||
const mockOpts = {
|
||||
'tmp': {
|
||||
'lens-cluster-store.json': JSON.stringify({})
|
||||
}
|
||||
}
|
||||
mockFs(mockOpts);
|
||||
clusterStore = ClusterStore.getInstance<ClusterStore>();
|
||||
return clusterStore.load();
|
||||
})
|
||||
|
||||
afterAll(() => {
|
||||
mockFs.restore();
|
||||
})
|
||||
|
||||
it("adds new cluster to store", async () => {
|
||||
const cluster = new Cluster({
|
||||
id: "foo",
|
||||
preferences: {
|
||||
terminalCWD: "/tmp",
|
||||
icon: "data:image/jpeg;base64, iVBORw0KGgoAAAANSUhEUgAAA1wAAAKoCAYAAABjkf5",
|
||||
clusterName: "minikube"
|
||||
},
|
||||
kubeConfigPath: saveConfigToAppFiles("foo", "fancy foo config"),
|
||||
workspace: workspaceStore.currentWorkspaceId
|
||||
});
|
||||
clusterStore.addCluster(cluster);
|
||||
const storedCluster = clusterStore.getById(cluster.id);
|
||||
expect(storedCluster.id).toBe(cluster.id);
|
||||
expect(storedCluster.preferences.terminalCWD).toBe(cluster.preferences.terminalCWD);
|
||||
expect(storedCluster.preferences.icon).toBe(cluster.preferences.icon);
|
||||
})
|
||||
|
||||
it("adds cluster to default workspace", () => {
|
||||
const storedCluster = clusterStore.getById("foo");
|
||||
expect(storedCluster.workspace).toBe("default");
|
||||
})
|
||||
|
||||
it("check if store can contain multiple clusters", () => {
|
||||
const prodCluster = new Cluster({
|
||||
id: "prod",
|
||||
preferences: {
|
||||
clusterName: "prod"
|
||||
},
|
||||
kubeConfigPath: saveConfigToAppFiles("prod", "fancy config"),
|
||||
workspace: "workstation"
|
||||
});
|
||||
const devCluster = new Cluster({
|
||||
id: "dev",
|
||||
preferences: {
|
||||
clusterName: "dev"
|
||||
},
|
||||
kubeConfigPath: saveConfigToAppFiles("dev", "fancy config"),
|
||||
workspace: "workstation"
|
||||
});
|
||||
clusterStore.addCluster(prodCluster);
|
||||
clusterStore.addCluster(devCluster);
|
||||
expect(clusterStore.hasClusters()).toBeTruthy();
|
||||
expect(clusterStore.clusters.size).toBe(3);
|
||||
});
|
||||
|
||||
it("gets clusters by workspaces", () => {
|
||||
const wsClusters = clusterStore.getByWorkspaceId("workstation");
|
||||
const defaultClusters = clusterStore.getByWorkspaceId("default");
|
||||
expect(defaultClusters.length).toBe(1);
|
||||
expect(wsClusters.length).toBe(2);
|
||||
expect(wsClusters[0].id).toBe("prod");
|
||||
expect(wsClusters[1].id).toBe("dev");
|
||||
})
|
||||
|
||||
it("checks if last added cluster becomes active", () => {
|
||||
expect(clusterStore.activeCluster.id).toBe("dev");
|
||||
})
|
||||
|
||||
it("sets active cluster", () => {
|
||||
clusterStore.setActive("foo");
|
||||
expect(clusterStore.activeCluster.id).toBe("foo");
|
||||
})
|
||||
|
||||
it("check if cluster's kubeconfig file saved", () => {
|
||||
const file = saveConfigToAppFiles("boo", "kubeconfig");
|
||||
expect(fs.readFileSync(file, "utf8")).toBe("kubeconfig");
|
||||
})
|
||||
|
||||
it("removes cluster from store", async () => {
|
||||
await clusterStore.removeById("foo");
|
||||
expect(clusterStore.getById("foo")).toBeUndefined();
|
||||
})
|
||||
})
|
||||
|
||||
describe("config with existing clusters", () => {
|
||||
beforeEach(() => {
|
||||
ClusterStore.resetInstance();
|
||||
const mockOpts = {
|
||||
'tmp': {
|
||||
'lens-cluster-store.json': JSON.stringify({
|
||||
__internal__: {
|
||||
migrations: {
|
||||
version: "99.99.99"
|
||||
}
|
||||
},
|
||||
clusters: [
|
||||
{
|
||||
id: 'cluster1',
|
||||
kubeConfig: 'foo',
|
||||
preferences: { terminalCWD: '/foo' }
|
||||
},
|
||||
{
|
||||
id: 'cluster2',
|
||||
kubeConfig: 'foo2',
|
||||
preferences: { terminalCWD: '/foo2' }
|
||||
}
|
||||
]
|
||||
})
|
||||
}
|
||||
}
|
||||
mockFs(mockOpts);
|
||||
clusterStore = ClusterStore.getInstance<ClusterStore>();
|
||||
return clusterStore.load();
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
mockFs.restore();
|
||||
})
|
||||
|
||||
it("allows to retrieve a cluster", () => {
|
||||
const storedCluster = clusterStore.getById('cluster1');
|
||||
expect(storedCluster.id).toBe('cluster1');
|
||||
expect(storedCluster.preferences.terminalCWD).toBe('/foo');
|
||||
})
|
||||
|
||||
it("allows to delete a cluster", () => {
|
||||
clusterStore.removeById('cluster2');
|
||||
const storedCluster = clusterStore.getById('cluster1');
|
||||
expect(storedCluster).toBeTruthy();
|
||||
const storedCluster2 = clusterStore.getById('cluster2');
|
||||
expect(storedCluster2).toBeUndefined();
|
||||
})
|
||||
|
||||
it("allows getting all of the clusters", async () => {
|
||||
const storedClusters = clusterStore.clustersList;
|
||||
expect(storedClusters[0].id).toBe('cluster1')
|
||||
expect(storedClusters[0].preferences.terminalCWD).toBe('/foo')
|
||||
expect(storedClusters[1].id).toBe('cluster2')
|
||||
expect(storedClusters[1].preferences.terminalCWD).toBe('/foo2')
|
||||
})
|
||||
})
|
||||
|
||||
describe("pre 2.0 config with an existing cluster", () => {
|
||||
beforeEach(() => {
|
||||
ClusterStore.resetInstance();
|
||||
const mockOpts = {
|
||||
'tmp': {
|
||||
'lens-cluster-store.json': JSON.stringify({
|
||||
__internal__: {
|
||||
migrations: {
|
||||
version: "1.0.0"
|
||||
}
|
||||
},
|
||||
cluster1: 'kubeconfig content'
|
||||
})
|
||||
}
|
||||
};
|
||||
mockFs(mockOpts);
|
||||
clusterStore = ClusterStore.getInstance<ClusterStore>();
|
||||
return clusterStore.load();
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
mockFs.restore();
|
||||
})
|
||||
|
||||
it("migrates to modern format with kubeconfig in a file", async () => {
|
||||
const config = clusterStore.clustersList[0].kubeConfigPath;
|
||||
expect(fs.readFileSync(config, "utf8")).toBe("kubeconfig content");
|
||||
})
|
||||
})
|
||||
|
||||
describe("pre 2.6.0 config with a cluster that has arrays in auth config", () => {
|
||||
beforeEach(() => {
|
||||
ClusterStore.resetInstance();
|
||||
const mockOpts = {
|
||||
'tmp': {
|
||||
'lens-cluster-store.json': JSON.stringify({
|
||||
__internal__: {
|
||||
migrations: {
|
||||
version: "2.4.1"
|
||||
}
|
||||
},
|
||||
cluster1: {
|
||||
kubeConfig: "apiVersion: v1\nclusters:\n- cluster:\n server: https://10.211.55.6:8443\n name: minikube\ncontexts:\n- context:\n cluster: minikube\n user: minikube\n name: minikube\ncurrent-context: minikube\nkind: Config\npreferences: {}\nusers:\n- name: minikube\n user:\n client-certificate: /Users/kimmo/.minikube/client.crt\n client-key: /Users/kimmo/.minikube/client.key\n auth-provider:\n config:\n access-token:\n - should be string\n expiry:\n - should be string\n"
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
mockFs(mockOpts);
|
||||
clusterStore = ClusterStore.getInstance<ClusterStore>();
|
||||
return clusterStore.load();
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
mockFs.restore();
|
||||
})
|
||||
|
||||
it("replaces array format access token and expiry into string", async () => {
|
||||
const file = clusterStore.clustersList[0].kubeConfigPath;
|
||||
const config = fs.readFileSync(file, "utf8");
|
||||
const kc = yaml.safeLoad(config);
|
||||
expect(kc.users[0].user['auth-provider'].config['access-token']).toBe("should be string");
|
||||
expect(kc.users[0].user['auth-provider'].config['expiry']).toBe("should be string");
|
||||
})
|
||||
})
|
||||
|
||||
describe("pre 2.6.0 config with a cluster icon", () => {
|
||||
beforeEach(() => {
|
||||
ClusterStore.resetInstance();
|
||||
const mockOpts = {
|
||||
'tmp': {
|
||||
'lens-cluster-store.json': JSON.stringify({
|
||||
__internal__: {
|
||||
migrations: {
|
||||
version: "2.4.1"
|
||||
}
|
||||
},
|
||||
cluster1: {
|
||||
kubeConfig: "foo",
|
||||
icon: "icon path",
|
||||
preferences: {
|
||||
terminalCWD: "/tmp"
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
mockFs(mockOpts);
|
||||
clusterStore = ClusterStore.getInstance<ClusterStore>();
|
||||
return clusterStore.load();
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
mockFs.restore();
|
||||
})
|
||||
|
||||
it("moves the icon into preferences", async () => {
|
||||
const storedClusterData = clusterStore.clustersList[0];
|
||||
expect(storedClusterData.hasOwnProperty('icon')).toBe(false);
|
||||
expect(storedClusterData.preferences.hasOwnProperty('icon')).toBe(true);
|
||||
expect(storedClusterData.preferences.icon).toBe("icon path");
|
||||
})
|
||||
})
|
||||
|
||||
describe("for a pre 2.7.0-beta.0 config without a workspace", () => {
|
||||
beforeEach(() => {
|
||||
ClusterStore.resetInstance();
|
||||
const mockOpts = {
|
||||
'tmp': {
|
||||
'lens-cluster-store.json': JSON.stringify({
|
||||
__internal__: {
|
||||
migrations: {
|
||||
version: "2.6.6"
|
||||
}
|
||||
},
|
||||
cluster1: {
|
||||
kubeConfig: "foo",
|
||||
icon: "icon path",
|
||||
preferences: {
|
||||
terminalCWD: "/tmp"
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
mockFs(mockOpts);
|
||||
clusterStore = ClusterStore.getInstance<ClusterStore>();
|
||||
return clusterStore.load();
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
mockFs.restore();
|
||||
})
|
||||
|
||||
it("adds cluster to default workspace", async () => {
|
||||
const storedClusterData = clusterStore.clustersList[0];
|
||||
expect(storedClusterData.workspace).toBe('default');
|
||||
})
|
||||
})
|
||||
|
||||
describe("pre 3.6.0-beta.1 config with an existing cluster", () => {
|
||||
beforeEach(() => {
|
||||
ClusterStore.resetInstance();
|
||||
const mockOpts = {
|
||||
'tmp': {
|
||||
'lens-cluster-store.json': JSON.stringify({
|
||||
__internal__: {
|
||||
migrations: {
|
||||
version: "2.7.0"
|
||||
}
|
||||
},
|
||||
clusters: [
|
||||
{
|
||||
id: 'cluster1',
|
||||
kubeConfig: 'kubeconfig content'
|
||||
}
|
||||
]
|
||||
})
|
||||
}
|
||||
};
|
||||
mockFs(mockOpts);
|
||||
clusterStore = ClusterStore.getInstance<ClusterStore>();
|
||||
return clusterStore.load();
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
mockFs.restore();
|
||||
})
|
||||
|
||||
it("migrates to modern format with kubeconfig in a file", async () => {
|
||||
const config = clusterStore.clustersList[0].kubeConfigPath;
|
||||
expect(fs.readFileSync(config, "utf8")).toBe("kubeconfig content");
|
||||
})
|
||||
})
|
||||
@ -7,23 +7,59 @@ import logger from "../main/logger";
|
||||
|
||||
export type IpcChannel = string;
|
||||
|
||||
export interface IpcHandleOpts {
|
||||
timeout?: number;
|
||||
export interface IpcChannelOptions {
|
||||
channel: IpcChannel; // main <-> renderer communication channel name
|
||||
handle?: (...args: any[]) => Promise<any> | any; // message handler
|
||||
autoBind?: boolean; // auto-bind message handler in main-process, default: true
|
||||
timeout?: number; // timeout for waiting response from the sender
|
||||
once?: boolean; // one-time event
|
||||
}
|
||||
|
||||
export interface IpcMessageHandler<T extends any[] = any> {
|
||||
(...args: T): any;
|
||||
export function createIpcChannel({ autoBind = true, once, timeout = 0, handle, channel }: IpcChannelOptions) {
|
||||
const ipcChannel = {
|
||||
channel: channel,
|
||||
handleInMain: () => {
|
||||
logger.info(`[IPC]: setup channel "${channel}"`);
|
||||
const ipcHandler = once ? ipcMain.handleOnce : ipcMain.handle;
|
||||
ipcHandler(channel, async (event, ...args) => {
|
||||
let timerId: any;
|
||||
try {
|
||||
if (timeout > 0) {
|
||||
timerId = setTimeout(() => {
|
||||
throw new Error(`[IPC]: response timeout in ${timeout}ms`)
|
||||
}, timeout);
|
||||
}
|
||||
return await handle(...args); // todo: maybe exec in separate thread/worker
|
||||
} catch (error) {
|
||||
throw error
|
||||
} finally {
|
||||
clearTimeout(timerId);
|
||||
}
|
||||
})
|
||||
},
|
||||
removeHandler() {
|
||||
ipcMain.removeHandler(channel);
|
||||
},
|
||||
invokeFromRenderer: async <T>(...args: any[]): Promise<T> => {
|
||||
return ipcRenderer.invoke(channel, ...args);
|
||||
},
|
||||
}
|
||||
if (autoBind && ipcMain) {
|
||||
ipcChannel.handleInMain();
|
||||
}
|
||||
return ipcChannel;
|
||||
}
|
||||
|
||||
export interface IpcMessageOpts<A extends any[] = any> {
|
||||
export interface IpcBroadcastParams<A extends any[] = any> {
|
||||
channel: IpcChannel
|
||||
webContentId?: number; // sends to single webContents view
|
||||
webContentId?: number; // send to single webContents view
|
||||
frameId?: number; // send to inner frame of webContents
|
||||
filter?: (webContent: WebContents) => boolean
|
||||
timeout?: number; // todo: add support
|
||||
args?: A;
|
||||
}
|
||||
|
||||
export function broadcastIpc({ channel, webContentId, filter, args = [] }: IpcMessageOpts) {
|
||||
export function broadcastIpc({ channel, frameId, webContentId, filter, args = [] }: IpcBroadcastParams) {
|
||||
const singleView = webContentId ? webContents.fromId(webContentId) : null;
|
||||
let views = singleView ? [singleView] : webContents.getAllWebContents();
|
||||
if (filter) {
|
||||
@ -31,65 +67,10 @@ export function broadcastIpc({ channel, webContentId, filter, args = [] }: IpcMe
|
||||
}
|
||||
views.forEach(webContent => {
|
||||
const type = webContent.getType();
|
||||
logger.debug(`[IPC]: sending message "${channel}" to ${type}=${webContent.id}`, { args });
|
||||
webContent.send(channel, ...[args].flat());
|
||||
logger.debug(`[IPC]: broadcasting "${channel}" to ${type}=${webContent.id}`, { args });
|
||||
webContent.send(channel, ...args);
|
||||
if (frameId) {
|
||||
webContent.sendToFrame(frameId, channel, ...args)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// todo: support timeout + merge with sendMessage?
|
||||
export async function invokeIpc<R = any>(channel: IpcChannel, ...args: any[]): Promise<R> {
|
||||
logger.info(`[IPC]: invoke channel "${channel}"`, { args });
|
||||
return ipcRenderer.invoke(channel, ...args);
|
||||
}
|
||||
|
||||
// todo: make isomorphic api
|
||||
export function handleIpc(channel: IpcChannel, handler: IpcMessageHandler, options: IpcHandleOpts = {}) {
|
||||
const { timeout = 0 } = options;
|
||||
logger.info(`[IPC]: setup to handle "${channel}"`);
|
||||
|
||||
ipcMain.handle(channel, async (event, ...args) => {
|
||||
logger.info(`[IPC]: handle "${channel}"`, { args });
|
||||
return new Promise(async (resolve, reject) => {
|
||||
let timerId;
|
||||
if (timeout) {
|
||||
timerId = setTimeout(() => {
|
||||
const timeoutError = new Error("[IPC]: response timeout");
|
||||
reject(timeoutError);
|
||||
}, timeout);
|
||||
}
|
||||
try {
|
||||
const result = await handler(...args); // todo: maybe exec in separate thread/worker
|
||||
resolve(result);
|
||||
clearTimeout(timerId);
|
||||
} catch (err) {
|
||||
reject(err);
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
export interface IpcPairOptions {
|
||||
channel: IpcChannel
|
||||
handle?: IpcMessageHandler
|
||||
autoBind?: boolean;
|
||||
timeout?: number;
|
||||
}
|
||||
|
||||
// todo: improve api
|
||||
export function createIpcChannel({ channel, autoBind, ...initOpts }: IpcPairOptions) {
|
||||
const bindHandler = (opts: { handler?: IpcMessageHandler, options?: IpcHandleOpts } = {}) => {
|
||||
const handler = opts.handler || initOpts.handle || Function;
|
||||
const options = opts.options || { timeout: initOpts.timeout };
|
||||
handleIpc(channel, handler, options);
|
||||
};
|
||||
if (autoBind) {
|
||||
bindHandler();
|
||||
}
|
||||
return {
|
||||
channel: channel,
|
||||
handleInMain: bindHandler,
|
||||
invokeFromRenderer(...args: any[]) {
|
||||
return invokeIpc(channel, ...args);
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@ -153,7 +153,7 @@ export function saveConfigToAppFiles(clusterId: string, kubeConfig: KubeConfig |
|
||||
|
||||
export async function getKubeConfigLocal(): Promise<string> {
|
||||
try {
|
||||
const configFile = path.join(process.env.HOME, '.kube', 'config');
|
||||
const configFile = path.join(os.homedir(), '.kube', 'config');
|
||||
const file = await readFile(configFile, "utf8");
|
||||
const obj = yaml.safeLoad(file);
|
||||
if (obj.contexts) {
|
||||
|
||||
@ -71,7 +71,6 @@ export class UserStore extends BaseStore<UserStoreModel> {
|
||||
if (kubeConfig) {
|
||||
this.newContexts.clear();
|
||||
const localContexts = loadConfig(kubeConfig).getContexts();
|
||||
console.log(localContexts)
|
||||
localContexts
|
||||
.filter(ctx => ctx.cluster)
|
||||
.filter(ctx => !this.seenContexts.has(ctx.name))
|
||||
|
||||
12
src/common/utils/defineGlobal.ts
Executable file
12
src/common/utils/defineGlobal.ts
Executable file
@ -0,0 +1,12 @@
|
||||
// Setup variable in global scope (top-level object)
|
||||
// Global type definition must be added separately to `mocks.d.ts` in form:
|
||||
// declare const __globalName: any;
|
||||
|
||||
export function defineGlobal(propName: string, descriptor: PropertyDescriptor) {
|
||||
const scope = typeof global !== "undefined" ? global : window;
|
||||
if (scope.hasOwnProperty(propName)) {
|
||||
console.info(`Global variable "${propName}" already exists. Skipping.`)
|
||||
return;
|
||||
}
|
||||
Object.defineProperty(scope, propName, descriptor);
|
||||
}
|
||||
@ -1,6 +1,7 @@
|
||||
// App's common configuration for any process (main, renderer, build pipeline, etc.)
|
||||
import packageInfo from "../../package.json"
|
||||
import path from "path";
|
||||
import packageInfo from "../../package.json"
|
||||
import { defineGlobal } from "./utils/defineGlobal";
|
||||
|
||||
export const isMac = process.platform === "darwin"
|
||||
export const isWindows = process.platform === "win32"
|
||||
@ -10,20 +11,25 @@ export const isDevelopment = isDebugging || !isProduction;
|
||||
export const isTestEnv = !!process.env.JEST_WORKER_ID;
|
||||
|
||||
export const appName = `${packageInfo.productName}${isDevelopment ? "Dev" : ""}`
|
||||
export const appProto = "lens" // app.getPath("userData") folder
|
||||
export const staticProto = "static" // static folder (e.g. "static://RELEASE_NOTES.md")
|
||||
export const publicPath = "/build/"
|
||||
|
||||
// System paths
|
||||
// Webpack build paths
|
||||
export const contextDir = process.cwd();
|
||||
export const staticDir = path.join(contextDir, "static");
|
||||
export const outDir = path.join(contextDir, "out");
|
||||
export const buildDir = path.join(contextDir, "static", publicPath);
|
||||
export const mainDir = path.join(contextDir, "src/main");
|
||||
export const rendererDir = path.join(contextDir, "src/renderer");
|
||||
export const htmlTemplate = path.resolve(rendererDir, "template.html");
|
||||
export const sassCommonVars = path.resolve(rendererDir, "components/vars.scss");
|
||||
|
||||
// System pages
|
||||
export const noClustersHost = "no-clusters.localhost"
|
||||
// Special runtime paths
|
||||
defineGlobal("__static", {
|
||||
get() {
|
||||
if (isDevelopment) {
|
||||
return path.resolve(contextDir, "static");
|
||||
}
|
||||
return path.resolve(process.resourcesPath, "static")
|
||||
}
|
||||
})
|
||||
|
||||
// Apis
|
||||
export const apiPrefix = "/api" // local router apis
|
||||
|
||||
@ -1,10 +1,10 @@
|
||||
import "../common/cluster-ipc";
|
||||
import type http from "http"
|
||||
import { autorun } from "mobx";
|
||||
import { apiKubePrefix } from "../common/vars";
|
||||
import { ClusterId, clusterStore } from "../common/cluster-store"
|
||||
import { Cluster } from "./cluster"
|
||||
import { clusterIpc } from "../common/cluster-ipc";
|
||||
import logger from "./logger";
|
||||
import { apiKubePrefix } from "../common/vars";
|
||||
|
||||
export class ClusterManager {
|
||||
constructor(public readonly port: number) {
|
||||
@ -30,13 +30,6 @@ export class ClusterManager {
|
||||
}, {
|
||||
delay: 250
|
||||
});
|
||||
|
||||
// listen for ipc-events that must/can be handled *only* in main-process (nodeIntegration=true)
|
||||
clusterIpc.activate.handleInMain();
|
||||
clusterIpc.disconnect.handleInMain();
|
||||
clusterIpc.installFeature.handleInMain();
|
||||
clusterIpc.uninstallFeature.handleInMain();
|
||||
clusterIpc.upgradeFeature.handleInMain();
|
||||
}
|
||||
|
||||
stop() {
|
||||
|
||||
@ -1,7 +1,8 @@
|
||||
import type { ClusterId, ClusterModel, ClusterPreferences } from "../common/cluster-store"
|
||||
import type { FeatureStatusMap } from "./feature"
|
||||
import type { IMetricsReqParams } from "../renderer/api/endpoints/metrics.api";
|
||||
import type { WorkspaceId } from "../common/workspace-store";
|
||||
import { action, observable, reaction, toJS, when } from "mobx";
|
||||
import type { FeatureStatusMap } from "./feature"
|
||||
import { action, computed, observable, reaction, toJS, when } from "mobx";
|
||||
import { apiKubePrefix } from "../common/vars";
|
||||
import { broadcastIpc } from "../common/ipc";
|
||||
import { ContextHandler } from "./context-handler"
|
||||
@ -39,6 +40,7 @@ export interface ClusterState extends ClusterModel {
|
||||
|
||||
export class Cluster implements ClusterModel {
|
||||
public id: ClusterId;
|
||||
public frameId: number;
|
||||
public kubeCtl: Kubectl
|
||||
public contextHandler: ContextHandler;
|
||||
protected kubeconfigManager: KubeconfigManager;
|
||||
@ -52,7 +54,6 @@ export class Cluster implements ClusterModel {
|
||||
@observable kubeConfigPath: string;
|
||||
@observable apiUrl: string; // cluster server url
|
||||
@observable kubeProxyUrl: string; // lens-proxy to kube-api url
|
||||
@observable webContentUrl: string; // page content url for loading in renderer
|
||||
@observable online: boolean;
|
||||
@observable accessible: boolean;
|
||||
@observable disconnected: boolean;
|
||||
@ -67,6 +68,10 @@ export class Cluster implements ClusterModel {
|
||||
@observable allowedNamespaces: string[] = [];
|
||||
@observable allowedResources: string[] = [];
|
||||
|
||||
@computed get available() {
|
||||
return this.accessible && !this.disconnected;
|
||||
}
|
||||
|
||||
constructor(model: ClusterModel) {
|
||||
this.updateModel(model);
|
||||
}
|
||||
@ -74,26 +79,21 @@ export class Cluster implements ClusterModel {
|
||||
@action
|
||||
updateModel(model: ClusterModel) {
|
||||
Object.assign(this, model);
|
||||
this.apiUrl = this.getKubeconfig().getCurrentCluster().server;
|
||||
this.apiUrl = this.getKubeconfig().getCurrentCluster()?.server;
|
||||
this.contextName = this.contextName || this.preferences.clusterName;
|
||||
}
|
||||
|
||||
@action
|
||||
async init(port: number) {
|
||||
if (this.initialized) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
this.contextHandler = new ContextHandler(this);
|
||||
this.kubeconfigManager = new KubeconfigManager(this, this.contextHandler);
|
||||
this.kubeProxyUrl = `http://localhost:${port}${apiKubePrefix}`;
|
||||
this.webContentUrl = `http://${this.id}.localhost:${port}`;
|
||||
this.initialized = true;
|
||||
logger.info(`[CLUSTER]: init success`, {
|
||||
logger.info(`[CLUSTER]: "${this.contextName}" init success`, {
|
||||
id: this.id,
|
||||
serverUrl: this.apiUrl,
|
||||
webContentUrl: this.webContentUrl,
|
||||
kubeProxyUrl: this.kubeProxyUrl,
|
||||
context: this.contextName,
|
||||
apiUrl: this.apiUrl
|
||||
});
|
||||
} catch (err) {
|
||||
logger.error(`[CLUSTER]: init failed: ${err}`, {
|
||||
@ -155,7 +155,7 @@ export class Cluster implements ClusterModel {
|
||||
@action
|
||||
async refresh() {
|
||||
logger.info(`[CLUSTER]: refresh`, this.getMeta());
|
||||
await this.refreshConnectionStatus();
|
||||
await this.refreshConnectionStatus(); // refresh "version", "online", etc.
|
||||
if (this.accessible) {
|
||||
this.kubeCtl = new Kubectl(this.version)
|
||||
this.distribution = this.detectKubernetesDistribution(this.version)
|
||||
@ -217,22 +217,30 @@ export class Cluster implements ClusterModel {
|
||||
return uninstallFeature(name, this)
|
||||
}
|
||||
|
||||
getPrometheusApiPrefix() {
|
||||
return this.preferences.prometheus?.prefix || ""
|
||||
}
|
||||
|
||||
protected async k8sRequest(path: string, options: RequestPromiseOptions = {}) {
|
||||
protected async k8sRequest<T = any>(path: string, options: RequestPromiseOptions = {}): Promise<T> {
|
||||
const apiUrl = this.kubeProxyUrl + path;
|
||||
return request(apiUrl, {
|
||||
json: true,
|
||||
timeout: 5000,
|
||||
...options,
|
||||
headers: {
|
||||
Host: `${this.id}.${new URL(this.kubeProxyUrl).host}`, // required in ClusterManager.getClusterForRequest()
|
||||
...(options.headers || {}),
|
||||
Host: new URL(this.webContentUrl).host,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
getMetrics(prometheusPath: string, queryParams: IMetricsReqParams & { query: string }) {
|
||||
const prometheusPrefix = this.preferences.prometheus?.prefix || "";
|
||||
const metricsPath = `/api/v1/namespaces/${prometheusPath}/proxy${prometheusPrefix}/api/v1/query_range`;
|
||||
return this.k8sRequest(metricsPath, {
|
||||
timeout: 0,
|
||||
resolveWithFullResponse: false,
|
||||
json: true,
|
||||
qs: queryParams,
|
||||
})
|
||||
}
|
||||
|
||||
protected async getConnectionStatus(): Promise<ClusterStatus> {
|
||||
try {
|
||||
const response = await this.k8sRequest("/version")
|
||||
@ -382,8 +390,8 @@ export class Cluster implements ClusterModel {
|
||||
pushState = (state = this.getState()): ClusterState => {
|
||||
logger.debug(`[CLUSTER]: push-state`, state);
|
||||
broadcastIpc({
|
||||
// webContentId: viewId, // todo: send to cluster-view only
|
||||
channel: "cluster:state",
|
||||
frameId: this.frameId,
|
||||
args: [state],
|
||||
});
|
||||
return state;
|
||||
|
||||
@ -3,9 +3,8 @@
|
||||
import "../common/system-ca"
|
||||
import "../common/prometheus-providers"
|
||||
import { app, dialog } from "electron"
|
||||
import { appName, appProto, staticDir, staticProto } from "../common/vars";
|
||||
import { appName } from "../common/vars";
|
||||
import path from "path"
|
||||
import { initMenu } from "./menu"
|
||||
import { LensProxy } from "./lens-proxy"
|
||||
import { WindowManager } from "./window-manager";
|
||||
import { ClusterManager } from "./cluster-manager";
|
||||
@ -20,6 +19,12 @@ import { workspaceStore } from "../common/workspace-store";
|
||||
import { tracker } from "../common/tracker";
|
||||
import logger from "./logger"
|
||||
|
||||
const workingDir = path.join(app.getPath("appData"), appName);
|
||||
app.setName(appName);
|
||||
if(!process.env.CICD) {
|
||||
app.setPath("userData", workingDir);
|
||||
}
|
||||
|
||||
let windowManager: WindowManager;
|
||||
let clusterManager: ClusterManager;
|
||||
let proxyServer: LensProxy;
|
||||
@ -31,18 +36,13 @@ if (app.commandLine.getSwitchValue("proxy-server") !== "") {
|
||||
|
||||
async function main() {
|
||||
await shellSync();
|
||||
|
||||
const workingDir = path.join(app.getPath("appData"), appName);
|
||||
app.setName(appName);
|
||||
app.setPath("userData", workingDir);
|
||||
logger.info(`🚀 Starting Lens from "${workingDir}"`)
|
||||
|
||||
tracker.event("app", "start");
|
||||
const updater = new AppUpdater()
|
||||
updater.start();
|
||||
|
||||
registerFileProtocol(appProto, app.getPath("userData"));
|
||||
registerFileProtocol(staticProto, staticDir);
|
||||
registerFileProtocol("static", __static);
|
||||
|
||||
// find free port
|
||||
let proxyPort: number
|
||||
@ -74,7 +74,6 @@ async function main() {
|
||||
|
||||
// create window manager and open app
|
||||
windowManager = new WindowManager(proxyPort);
|
||||
initMenu(windowManager);
|
||||
}
|
||||
|
||||
app.on("ready", main);
|
||||
|
||||
@ -7,10 +7,11 @@ import { openShell } from "./node-shell-session";
|
||||
import { Router } from "./router"
|
||||
import { ClusterManager } from "./cluster-manager"
|
||||
import { ContextHandler } from "./context-handler";
|
||||
import { apiKubePrefix, noClustersHost } from "../common/vars";
|
||||
import { apiKubePrefix } from "../common/vars";
|
||||
import logger from "./logger"
|
||||
|
||||
export class LensProxy {
|
||||
protected origin: string
|
||||
protected proxyServer: http.Server
|
||||
protected router: Router
|
||||
protected closed = false
|
||||
@ -21,12 +22,13 @@ export class LensProxy {
|
||||
}
|
||||
|
||||
private constructor(protected port: number, protected clusterManager: ClusterManager) {
|
||||
this.origin = `http://localhost:${port}`
|
||||
this.router = new Router();
|
||||
}
|
||||
|
||||
listen(port = this.port): this {
|
||||
this.proxyServer = this.buildCustomProxy().listen(port);
|
||||
logger.info(`LensProxy server has started http://localhost:${port}`);
|
||||
logger.info(`LensProxy server has started at ${this.origin}`);
|
||||
return this;
|
||||
}
|
||||
|
||||
@ -117,26 +119,17 @@ export class LensProxy {
|
||||
}
|
||||
|
||||
protected async handleRequest(proxy: httpProxy, req: http.IncomingMessage, res: http.ServerResponse) {
|
||||
if (req.headers.host.split(":")[0] === noClustersHost) {
|
||||
this.router.handleStaticFile(req.url, res);
|
||||
return;
|
||||
}
|
||||
const cluster = this.clusterManager.getClusterForRequest(req)
|
||||
if (!cluster) {
|
||||
const reqId = this.getRequestId(req);
|
||||
logger.error("Got request to unknown cluster", { reqId })
|
||||
res.statusCode = 503
|
||||
res.end()
|
||||
return
|
||||
}
|
||||
const contextHandler = cluster.contextHandler
|
||||
await contextHandler.ensureServer();
|
||||
const proxyTarget = await this.getProxyTarget(req, contextHandler)
|
||||
if (proxyTarget) {
|
||||
proxy.web(req, res, proxyTarget)
|
||||
} else {
|
||||
this.router.route(cluster, req, res);
|
||||
if (cluster) {
|
||||
await cluster.contextHandler.ensureServer();
|
||||
const proxyTarget = await this.getProxyTarget(req, cluster.contextHandler)
|
||||
if (proxyTarget) {
|
||||
// allow to fetch apis in "clusterId.localhost:port" from "localhost:port"
|
||||
res.setHeader("Access-Control-Allow-Origin", this.origin);
|
||||
return proxy.web(req, res, proxyTarget);
|
||||
}
|
||||
}
|
||||
this.router.route(cluster, req, res);
|
||||
}
|
||||
|
||||
protected async handleWsUpgrade(req: http.IncomingMessage, socket: net.Socket, head: Buffer) {
|
||||
|
||||
181
src/main/menu.ts
181
src/main/menu.ts
@ -1,9 +1,7 @@
|
||||
import type { WindowManager } from "./window-manager";
|
||||
import { app, BrowserWindow, dialog, Menu, MenuItem, MenuItemConstructorOptions, shell, webContents } from "electron"
|
||||
import { autorun } from "mobx";
|
||||
import { broadcastIpc } from "../common/ipc";
|
||||
import { WindowManager } from "./window-manager";
|
||||
import { appName, isMac, issuesTrackerUrl, isWindows, slackUrl } from "../common/vars";
|
||||
import { clusterStore } from "../common/cluster-store";
|
||||
import { addClusterURL } from "../renderer/components/+add-cluster/add-cluster.route";
|
||||
import { preferencesURL } from "../renderer/components/+preferences/preferences.route";
|
||||
import { whatsNewURL } from "../renderer/components/+whats-new/whats-new.route";
|
||||
@ -11,45 +9,61 @@ import { clusterSettingsURL } from "../renderer/components/+cluster-settings/clu
|
||||
import logger from "./logger";
|
||||
|
||||
export function initMenu(windowManager: WindowManager) {
|
||||
autorun(() => {
|
||||
logger.debug(`[MENU]: building menu, cluster=${clusterStore.activeClusterId}`);
|
||||
buildMenu(windowManager);
|
||||
autorun(() => buildMenu(windowManager), {
|
||||
delay: 100
|
||||
});
|
||||
}
|
||||
|
||||
function buildMenu(windowManager: WindowManager) {
|
||||
const hasClusters = clusterStore.hasClusters();
|
||||
const activeClusterId = clusterStore.activeClusterId;
|
||||
|
||||
function navigate(url: string) {
|
||||
const clusterView = windowManager.getClusterView(activeClusterId);
|
||||
broadcastIpc({
|
||||
channel: "menu:navigate",
|
||||
webContentId: clusterView ? clusterView.id : undefined /*no-clusters*/,
|
||||
args: [url],
|
||||
});
|
||||
}
|
||||
|
||||
function macOnly(menuItems: MenuItemConstructorOptions[]): MenuItemConstructorOptions[] {
|
||||
if (!isMac) return [];
|
||||
export function buildMenu(windowManager: WindowManager) {
|
||||
function ignoreOnMac(menuItems: MenuItemConstructorOptions[]) {
|
||||
if (isMac) return [];
|
||||
return menuItems;
|
||||
}
|
||||
|
||||
const fileMenu: MenuItemConstructorOptions = {
|
||||
label: isMac ? app.getName() : "File",
|
||||
function activeClusterOnly(menuItems: MenuItemConstructorOptions[]) {
|
||||
if (!windowManager.activeClusterId) {
|
||||
menuItems.forEach(item => {
|
||||
item.enabled = false
|
||||
});
|
||||
}
|
||||
return menuItems;
|
||||
}
|
||||
|
||||
function navigate(url: string) {
|
||||
logger.info(`[MENU]: navigating to ${url}`);
|
||||
windowManager.navigate({
|
||||
channel: "menu:navigate",
|
||||
url: url,
|
||||
})
|
||||
}
|
||||
|
||||
function showAbout(browserWindow: BrowserWindow) {
|
||||
const appInfo = [
|
||||
`${appName}: ${app.getVersion()}`,
|
||||
`Electron: ${process.versions.electron}`,
|
||||
`Chrome: ${process.versions.chrome}`,
|
||||
`Copyright 2020 Lakend Labs, Inc.`,
|
||||
]
|
||||
dialog.showMessageBoxSync(browserWindow, {
|
||||
title: `${isWindows ? " ".repeat(2) : ""}${appName}`,
|
||||
type: "info",
|
||||
buttons: ["Close"],
|
||||
message: `Lens`,
|
||||
detail: appInfo.join("\r\n")
|
||||
})
|
||||
}
|
||||
|
||||
const mt: MenuItemConstructorOptions[] = [];
|
||||
|
||||
const macAppMenu: MenuItemConstructorOptions = {
|
||||
label: app.getName(),
|
||||
submenu: [
|
||||
{
|
||||
label: 'Add Cluster',
|
||||
click() {
|
||||
navigate(addClusterURL())
|
||||
label: "About Lens",
|
||||
click(menuItem: MenuItem, browserWindow: BrowserWindow) {
|
||||
showAbout(browserWindow)
|
||||
}
|
||||
},
|
||||
...(hasClusters ? [{
|
||||
label: 'Cluster Settings',
|
||||
click() {
|
||||
navigate(clusterSettingsURL())
|
||||
}
|
||||
}] : []),
|
||||
{ type: 'separator' },
|
||||
{
|
||||
label: 'Preferences',
|
||||
@ -57,19 +71,57 @@ function buildMenu(windowManager: WindowManager) {
|
||||
navigate(preferencesURL())
|
||||
}
|
||||
},
|
||||
...macOnly([
|
||||
{ type: 'separator' },
|
||||
{ role: 'services' },
|
||||
{ type: 'separator' },
|
||||
{ role: 'hide' },
|
||||
{ role: 'hideOthers' },
|
||||
{ role: 'unhide' },
|
||||
]),
|
||||
{ type: 'separator' },
|
||||
{ role: 'services' },
|
||||
{ type: 'separator' },
|
||||
{ role: 'hide' },
|
||||
{ role: 'hideOthers' },
|
||||
{ role: 'unhide' },
|
||||
{ type: 'separator' },
|
||||
{ role: 'quit' }
|
||||
]
|
||||
};
|
||||
|
||||
if (isMac) {
|
||||
mt.push(macAppMenu);
|
||||
}
|
||||
|
||||
const fileMenu: MenuItemConstructorOptions = {
|
||||
label: "File",
|
||||
submenu: [
|
||||
{
|
||||
label: 'Add Cluster',
|
||||
click() {
|
||||
navigate(addClusterURL())
|
||||
}
|
||||
},
|
||||
...activeClusterOnly([
|
||||
{
|
||||
label: 'Cluster Settings',
|
||||
click() {
|
||||
navigate(clusterSettingsURL({
|
||||
params: {
|
||||
clusterId: windowManager.activeClusterId
|
||||
}
|
||||
}))
|
||||
}
|
||||
}
|
||||
]),
|
||||
...ignoreOnMac([
|
||||
{ type: 'separator' },
|
||||
{
|
||||
label: 'Preferences',
|
||||
click() {
|
||||
navigate(preferencesURL())
|
||||
}
|
||||
},
|
||||
{ type: 'separator' },
|
||||
{ role: 'quit' }
|
||||
])
|
||||
]
|
||||
};
|
||||
mt.push(fileMenu)
|
||||
|
||||
const editMenu: MenuItemConstructorOptions = {
|
||||
label: 'Edit',
|
||||
submenu: [
|
||||
@ -84,7 +136,7 @@ function buildMenu(windowManager: WindowManager) {
|
||||
{ role: 'selectAll' },
|
||||
]
|
||||
};
|
||||
|
||||
mt.push(editMenu)
|
||||
const viewMenu: MenuItemConstructorOptions = {
|
||||
label: 'View',
|
||||
submenu: [
|
||||
@ -92,21 +144,21 @@ function buildMenu(windowManager: WindowManager) {
|
||||
label: 'Back',
|
||||
accelerator: 'CmdOrCtrl+[',
|
||||
click() {
|
||||
webContents.getFocusedWebContents().executeJavaScript('window.history.back()')
|
||||
webContents.getFocusedWebContents()?.goBack();
|
||||
}
|
||||
},
|
||||
{
|
||||
label: 'Forward',
|
||||
accelerator: 'CmdOrCtrl+]',
|
||||
click() {
|
||||
webContents.getFocusedWebContents().executeJavaScript('window.history.forward()')
|
||||
webContents.getFocusedWebContents()?.goForward();
|
||||
}
|
||||
},
|
||||
{
|
||||
label: 'Reload',
|
||||
accelerator: 'CmdOrCtrl+R',
|
||||
click() {
|
||||
webContents.getFocusedWebContents().reload()
|
||||
webContents.getFocusedWebContents()?.reload();
|
||||
}
|
||||
},
|
||||
{ role: 'toggleDevTools' },
|
||||
@ -118,16 +170,11 @@ function buildMenu(windowManager: WindowManager) {
|
||||
{ role: 'togglefullscreen' }
|
||||
]
|
||||
};
|
||||
mt.push(viewMenu)
|
||||
|
||||
const helpMenu: MenuItemConstructorOptions = {
|
||||
role: 'help',
|
||||
submenu: [
|
||||
{
|
||||
label: "What's new?",
|
||||
click() {
|
||||
navigate(whatsNewURL())
|
||||
},
|
||||
},
|
||||
{
|
||||
label: "License",
|
||||
click: async () => {
|
||||
@ -147,27 +194,23 @@ function buildMenu(windowManager: WindowManager) {
|
||||
},
|
||||
},
|
||||
{
|
||||
label: "About Lens",
|
||||
click(menuItem: MenuItem, browserWindow: BrowserWindow) {
|
||||
const appInfo = [
|
||||
`${appName}: ${app.getVersion()}`,
|
||||
`Electron: ${process.versions.electron}`,
|
||||
`Chrome: ${process.versions.chrome}`,
|
||||
`Copyright 2020 Lakend Labs, Inc.`,
|
||||
]
|
||||
dialog.showMessageBoxSync(browserWindow, {
|
||||
title: `${isWindows ? " ".repeat(2) : ""}${appName}`,
|
||||
type: "info",
|
||||
buttons: ["Close"],
|
||||
message: `Lens`,
|
||||
detail: appInfo.join("\r\n")
|
||||
})
|
||||
label: "What's new?",
|
||||
click() {
|
||||
navigate(whatsNewURL())
|
||||
},
|
||||
},
|
||||
...ignoreOnMac([
|
||||
{
|
||||
label: "About Lens",
|
||||
click(menuItem: MenuItem, browserWindow: BrowserWindow) {
|
||||
showAbout(browserWindow)
|
||||
}
|
||||
}
|
||||
}
|
||||
])
|
||||
]
|
||||
};
|
||||
|
||||
Menu.setApplicationMenu(Menu.buildFromTemplate([
|
||||
fileMenu, editMenu, viewMenu, helpMenu
|
||||
]));
|
||||
mt.push(helpMenu)
|
||||
|
||||
Menu.setApplicationMenu(Menu.buildFromTemplate(mt));
|
||||
}
|
||||
|
||||
@ -4,7 +4,7 @@ import http from "http"
|
||||
import path from "path"
|
||||
import { readFile } from "fs-extra"
|
||||
import { Cluster } from "./cluster"
|
||||
import { apiPrefix, appName, outDir } from "../common/vars";
|
||||
import { apiPrefix, appName, publicPath } from "../common/vars";
|
||||
import { helmRoute, kubeconfigRoute, metricsRoute, portForwardRoute, resourceApplierRoute, watchRoute } from "./routes";
|
||||
|
||||
export interface RouterRequestOpts {
|
||||
@ -95,14 +95,14 @@ export class Router {
|
||||
}
|
||||
|
||||
async handleStaticFile(filePath: string, res: http.ServerResponse) {
|
||||
const asset = path.join(outDir, filePath);
|
||||
const asset = path.join(__static, filePath);
|
||||
try {
|
||||
const data = await readFile(asset);
|
||||
res.setHeader("Content-Type", this.getMimeType(asset));
|
||||
res.write(data)
|
||||
res.end()
|
||||
} catch (err) {
|
||||
this.handleStaticFile(`${appName}.html`, res);
|
||||
this.handleStaticFile(`${publicPath}/${appName}.html`, res);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -1,7 +1,5 @@
|
||||
import url from "url"
|
||||
import { LensApiRequest } from "../router"
|
||||
import { LensApi } from "../lens-api"
|
||||
import requestPromise from "request-promise-native"
|
||||
import { PrometheusClusterQuery, PrometheusIngressQuery, PrometheusNodeQuery, PrometheusPodQuery, PrometheusProvider, PrometheusPvcQuery, PrometheusQueryOpts } from "../prometheus/provider-registry"
|
||||
|
||||
export type IMetricsQuery = string | string[] | {
|
||||
@ -9,25 +7,19 @@ export type IMetricsQuery = string | string[] | {
|
||||
}
|
||||
|
||||
class MetricsRoute extends LensApi {
|
||||
|
||||
public async routeMetrics(request: LensApiRequest) {
|
||||
async routeMetrics(request: LensApiRequest) {
|
||||
const { response, cluster, payload } = request
|
||||
const { contextHandler, kubeProxyUrl } = cluster;
|
||||
const headers: Record<string, string> = {
|
||||
"Host": url.parse(cluster.webContentUrl).host,
|
||||
"Content-type": "application/json",
|
||||
}
|
||||
const queryParams: IMetricsQuery = {}
|
||||
request.query.forEach((value: string, key: string) => {
|
||||
queryParams[key] = value
|
||||
})
|
||||
|
||||
let metricsUrl: string
|
||||
let prometheusPath: string
|
||||
let prometheusProvider: PrometheusProvider
|
||||
try {
|
||||
const prometheusPath = await contextHandler.getPrometheusPath()
|
||||
metricsUrl = `${kubeProxyUrl}/api/v1/namespaces/${prometheusPath}/proxy${cluster.getPrometheusApiPrefix()}/api/v1/query_range`
|
||||
prometheusProvider = await contextHandler.getPrometheusProvider()
|
||||
[prometheusPath, prometheusProvider] = await Promise.all([
|
||||
cluster.contextHandler.getPrometheusPath(),
|
||||
cluster.contextHandler.getPrometheusProvider()
|
||||
])
|
||||
} catch {
|
||||
this.respondJson(response, {})
|
||||
return
|
||||
@ -35,18 +27,10 @@ class MetricsRoute extends LensApi {
|
||||
// prometheus metrics loader
|
||||
const attempts: { [query: string]: number } = {};
|
||||
const maxAttempts = 5;
|
||||
const loadMetrics = (orgQuery: string): Promise<any> => {
|
||||
const query = orgQuery.trim()
|
||||
const loadMetrics = (promQuery: string): Promise<any> => {
|
||||
const query = promQuery.trim()
|
||||
const attempt = attempts[query] = (attempts[query] || 0) + 1;
|
||||
return requestPromise(metricsUrl, {
|
||||
resolveWithFullResponse: false,
|
||||
headers: headers,
|
||||
json: true,
|
||||
qs: {
|
||||
query: query,
|
||||
...queryParams
|
||||
}
|
||||
}).catch(async (error) => {
|
||||
return cluster.getMetrics(prometheusPath, { query, ...queryParams }).catch(async error => {
|
||||
if (attempt < maxAttempts && (error.statusCode && error.statusCode != 404)) {
|
||||
await new Promise(resolve => setTimeout(resolve, attempt * 1000)); // add delay before repeating request
|
||||
return loadMetrics(query);
|
||||
|
||||
@ -1,65 +1,71 @@
|
||||
import { reaction } from "mobx";
|
||||
import { BrowserWindow, shell } from "electron"
|
||||
import windowStateKeeper from "electron-window-state"
|
||||
import type { ClusterId } from "../common/cluster-store";
|
||||
import { clusterStore } from "../common/cluster-store";
|
||||
import { noClustersHost } from "../common/vars";
|
||||
import logger from "./logger";
|
||||
import { BrowserWindow, dialog, ipcMain, shell, WebContents, webContents } from "electron"
|
||||
import windowStateKeeper from "electron-window-state"
|
||||
import { observable } from "mobx";
|
||||
import { initMenu } from "./menu";
|
||||
|
||||
export class WindowManager {
|
||||
protected activeView: BrowserWindow;
|
||||
protected mainView: BrowserWindow;
|
||||
protected splashWindow: BrowserWindow;
|
||||
protected noClustersWindow: BrowserWindow;
|
||||
protected views = new Map<ClusterId, BrowserWindow>();
|
||||
protected disposers: CallableFunction[] = [];
|
||||
protected windowState: windowStateKeeper.State;
|
||||
|
||||
constructor(protected proxyPort: number, showSplash = true) {
|
||||
@observable activeClusterId: ClusterId;
|
||||
|
||||
constructor(protected proxyPort: number) {
|
||||
// Manage main window size and position with state persistence
|
||||
this.windowState = windowStateKeeper({
|
||||
defaultHeight: 900,
|
||||
defaultWidth: 1440,
|
||||
});
|
||||
|
||||
// Show while app not ready
|
||||
if (showSplash) {
|
||||
this.showSplash();
|
||||
}
|
||||
const { width, height, x, y } = this.windowState;
|
||||
this.mainView = new BrowserWindow({
|
||||
x, y, width, height,
|
||||
show: false,
|
||||
minWidth: 900,
|
||||
minHeight: 760,
|
||||
titleBarStyle: "hidden",
|
||||
backgroundColor: "#1e2124",
|
||||
webPreferences: {
|
||||
nodeIntegration: true,
|
||||
nodeIntegrationInSubFrames: true,
|
||||
enableRemoteModule: true,
|
||||
},
|
||||
});
|
||||
this.windowState.manage(this.mainView);
|
||||
|
||||
// Manage reactive state
|
||||
this.disposers.push(
|
||||
// auto-show/hide "no-clusters" window when necessary
|
||||
reaction(() => clusterStore.hasClusters(), hasClusters => {
|
||||
this.handleNoClustersView({ activate: !hasClusters });
|
||||
}, {
|
||||
fireImmediately: true
|
||||
}),
|
||||
// open external links in default browser (target=_blank, window.open)
|
||||
this.mainView.webContents.on("new-window", (event, url) => {
|
||||
event.preventDefault();
|
||||
shell.openExternal(url);
|
||||
});
|
||||
|
||||
// auto-show active cluster window
|
||||
reaction(() => clusterStore.activeClusterId, this.activateView, {
|
||||
fireImmediately: true,
|
||||
}),
|
||||
// track visible cluster from ui
|
||||
ipcMain.on("cluster-view:change", (event, clusterId: ClusterId) => {
|
||||
this.activeClusterId = clusterId;
|
||||
});
|
||||
|
||||
// auto-destroy views for removed clusters
|
||||
reaction(() => clusterStore.removedClusters.toJS(), removedClusters => {
|
||||
removedClusters.forEach(cluster => {
|
||||
this.destroyClusterView(cluster.id);
|
||||
});
|
||||
}, {
|
||||
delay: 25, // fix: destroy later and allow to use view's state in next activateView()
|
||||
}),
|
||||
);
|
||||
// load & show app
|
||||
this.showMain();
|
||||
initMenu(this);
|
||||
}
|
||||
|
||||
protected handleNoClustersView = async ({ activate = false } = {}) => {
|
||||
if (!this.noClustersWindow) {
|
||||
this.noClustersWindow = this.initClusterView(null);
|
||||
await this.noClustersWindow.loadURL(`http://${noClustersHost}:${this.proxyPort}`);
|
||||
navigate({ url, channel, frameId }: { url: string, channel: string, frameId?: number }) {
|
||||
if (frameId) {
|
||||
this.mainView.webContents.sendToFrame(frameId, channel, url);
|
||||
} else {
|
||||
this.mainView.webContents.send(channel, url);
|
||||
}
|
||||
if (activate) {
|
||||
this.activeView = this.noClustersWindow;
|
||||
this.noClustersWindow.show();
|
||||
this.hideSplash();
|
||||
}
|
||||
|
||||
async showMain() {
|
||||
try {
|
||||
await this.showSplash();
|
||||
await this.mainView.loadURL(`http://localhost:${this.proxyPort}`)
|
||||
this.mainView.show();
|
||||
this.splashWindow.close();
|
||||
} catch (err) {
|
||||
dialog.showErrorBox("ERROR!", err.toString())
|
||||
}
|
||||
}
|
||||
|
||||
@ -73,101 +79,18 @@ export class WindowManager {
|
||||
frame: false,
|
||||
resizable: false,
|
||||
show: false,
|
||||
webPreferences: {
|
||||
nodeIntegration: true
|
||||
}
|
||||
});
|
||||
await this.splashWindow.loadURL("static://splash.html");
|
||||
}
|
||||
this.splashWindow.show();
|
||||
}
|
||||
|
||||
hideSplash() {
|
||||
this.splashWindow.hide();
|
||||
}
|
||||
|
||||
getClusterView(clusterId: ClusterId): BrowserWindow {
|
||||
return this.views.get(clusterId);
|
||||
}
|
||||
|
||||
activateView = async (clusterId: ClusterId): Promise<number> => {
|
||||
const cluster = clusterStore.getById(clusterId);
|
||||
if (!cluster) return;
|
||||
try {
|
||||
const prevActiveView = this.activeView;
|
||||
const isLoadedBefore = !!this.getClusterView(clusterId);
|
||||
const view = this.initClusterView(clusterId);
|
||||
logger.info(`[WINDOW-MANAGER]: activating cluster view`, {
|
||||
id: view.id,
|
||||
clusterId: cluster.id,
|
||||
contextName: cluster.contextName,
|
||||
isLoadedBefore: isLoadedBefore,
|
||||
});
|
||||
if (prevActiveView !== view) {
|
||||
this.activeView = view;
|
||||
if (!isLoadedBefore) {
|
||||
await cluster.whenInitialized; // wait for url
|
||||
await view.loadURL(cluster.webContentUrl);
|
||||
this.hideSplash();
|
||||
}
|
||||
// refresh position and hide previous active window
|
||||
if (prevActiveView) {
|
||||
view.setBounds(prevActiveView.getBounds());
|
||||
prevActiveView.hide();
|
||||
}
|
||||
view.show();
|
||||
return view.id;
|
||||
}
|
||||
} catch (err) {
|
||||
logger.error(`[WINDOW-MANAGER]: can't activate cluster view`, {
|
||||
clusterId: cluster.id,
|
||||
err: String(err),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
protected initClusterView(clusterId: ClusterId): BrowserWindow {
|
||||
let view = this.getClusterView(clusterId);
|
||||
if (!view) {
|
||||
const { width, height, x, y } = this.windowState;
|
||||
view = new BrowserWindow({
|
||||
show: false,
|
||||
x: x, y: y,
|
||||
width: width,
|
||||
height: height,
|
||||
minWidth: 900,
|
||||
minHeight: 760,
|
||||
titleBarStyle: "hidden",
|
||||
backgroundColor: "#1e2124",
|
||||
webPreferences: {
|
||||
nodeIntegration: true,
|
||||
enableRemoteModule: true,
|
||||
},
|
||||
});
|
||||
// open external links in default browser (target=_blank, window.open)
|
||||
view.webContents.on("new-window", (event, url) => {
|
||||
event.preventDefault();
|
||||
shell.openExternal(url);
|
||||
});
|
||||
this.views.set(clusterId, view);
|
||||
this.windowState.manage(view);
|
||||
}
|
||||
return view;
|
||||
}
|
||||
|
||||
protected destroyClusterView(clusterId: ClusterId) {
|
||||
const view = this.views.get(clusterId);
|
||||
if (view) {
|
||||
view.destroy();
|
||||
this.views.delete(clusterId);
|
||||
}
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this.windowState.unmanage();
|
||||
this.disposers.forEach(dispose => dispose());
|
||||
this.disposers.length = 0;
|
||||
this.views.forEach(view => view.destroy());
|
||||
this.views.clear();
|
||||
this.splashWindow.destroy();
|
||||
this.splashWindow = null;
|
||||
this.activeView = null;
|
||||
this.mainView.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
@ -4,16 +4,16 @@ import { Notifications } from "../components/notifications";
|
||||
import { apiKubePrefix, apiPrefix, isDevelopment } from "../../common/vars";
|
||||
|
||||
export const apiBase = new JsonApi({
|
||||
apiBase: apiPrefix,
|
||||
debug: isDevelopment,
|
||||
apiPrefix: apiPrefix,
|
||||
});
|
||||
export const apiKube = new KubeJsonApi({
|
||||
apiBase: apiKubePrefix,
|
||||
debug: isDevelopment,
|
||||
apiPrefix: apiKubePrefix,
|
||||
});
|
||||
|
||||
// Common handler for HTTP api errors
|
||||
function onApiError(error: JsonApiErrorParsed, res: Response) {
|
||||
export function onApiError(error: JsonApiErrorParsed, res: Response) {
|
||||
switch (res.status) {
|
||||
case 403:
|
||||
error.isUsedForNotification = true;
|
||||
|
||||
@ -27,7 +27,7 @@ export interface JsonApiLog {
|
||||
}
|
||||
|
||||
export interface JsonApiConfig {
|
||||
apiPrefix: string;
|
||||
apiBase: string;
|
||||
debug?: boolean;
|
||||
}
|
||||
|
||||
@ -72,7 +72,7 @@ export class JsonApi<D = JsonApiData, P extends JsonApiParams = JsonApiParams> {
|
||||
}
|
||||
|
||||
protected request<D>(path: string, params?: P, init: RequestInit = {}) {
|
||||
let reqUrl = this.config.apiPrefix + path;
|
||||
let reqUrl = this.config.apiBase + path;
|
||||
const reqInit: RequestInit = { ...this.reqInit, ...init };
|
||||
const { data, query } = params || {} as P;
|
||||
if (data && !reqInit.body) {
|
||||
|
||||
@ -61,7 +61,7 @@ export class KubeWatchApi {
|
||||
}
|
||||
|
||||
protected getQuery(): Partial<IKubeWatchRouteQuery> {
|
||||
const { isAdmin, allowedNamespaces } = getHostedCluster();
|
||||
const { isAdmin, allowedNamespaces } = getHostedCluster()
|
||||
return {
|
||||
api: this.activeApis.map(api => {
|
||||
if (isAdmin) return api.getWatchUrl();
|
||||
|
||||
38
src/renderer/bootstrap.tsx
Normal file
38
src/renderer/bootstrap.tsx
Normal file
@ -0,0 +1,38 @@
|
||||
import "./components/app.scss"
|
||||
import React from "react";
|
||||
import { render } from "react-dom";
|
||||
import { isMac } from "../common/vars";
|
||||
import { userStore } from "../common/user-store";
|
||||
import { workspaceStore } from "../common/workspace-store";
|
||||
import { clusterStore, getHostedClusterId } from "../common/cluster-store";
|
||||
import { i18nStore } from "./i18n";
|
||||
import { themeStore } from "./theme.store";
|
||||
import { App } from "./components/app";
|
||||
import { LensApp } from "./lens-app";
|
||||
|
||||
type AppComponent = React.ComponentType & {
|
||||
init?(): void;
|
||||
}
|
||||
|
||||
export async function bootstrap(App: AppComponent) {
|
||||
const rootElem = document.getElementById("app")
|
||||
rootElem.classList.toggle("is-mac", isMac);
|
||||
|
||||
// preload common stores
|
||||
await Promise.all([
|
||||
userStore.load(),
|
||||
workspaceStore.load(),
|
||||
clusterStore.load(),
|
||||
i18nStore.init(),
|
||||
themeStore.init(),
|
||||
]);
|
||||
|
||||
// init app's dependencies if any
|
||||
if (App.init) {
|
||||
await App.init();
|
||||
}
|
||||
render(<App/>, rootElem);
|
||||
}
|
||||
|
||||
// run
|
||||
bootstrap(getHostedClusterId() ? App : LensApp);
|
||||
@ -1,2 +1,11 @@
|
||||
.AddCluster {
|
||||
.Select {
|
||||
&__control {
|
||||
box-shadow: 0 0 0 1px $borderFaintColor;
|
||||
}
|
||||
}
|
||||
|
||||
code {
|
||||
color: $pink-400;
|
||||
}
|
||||
}
|
||||
@ -15,8 +15,9 @@ import { getKubeConfigLocal, loadConfig, saveConfigToAppFiles, splitConfig, vali
|
||||
import { clusterStore } from "../../../common/cluster-store";
|
||||
import { workspaceStore } from "../../../common/workspace-store";
|
||||
import { v4 as uuid } from "uuid"
|
||||
import { navigation } from "../../navigation";
|
||||
import { navigate } from "../../navigation";
|
||||
import { userStore } from "../../../common/user-store";
|
||||
import { clusterViewURL } from "../cluster-manager/cluster-view.route";
|
||||
|
||||
@observer
|
||||
export class AddCluster extends React.Component {
|
||||
@ -70,8 +71,9 @@ export class AddCluster extends React.Component {
|
||||
if (value instanceof KubeConfig) {
|
||||
const context = value.currentContext;
|
||||
const isNew = userStore.newContexts.has(context);
|
||||
const className = `${context} kube-context flex gaps align-center`
|
||||
return (
|
||||
<div className="kube-context flex gaps align-center">
|
||||
<div className={className}>
|
||||
<span>{context}</span>
|
||||
{isNew && <Icon material="fiber_new"/>}
|
||||
</div>
|
||||
@ -102,7 +104,7 @@ export class AddCluster extends React.Component {
|
||||
httpsProxy: proxyServer || undefined,
|
||||
},
|
||||
});
|
||||
navigation.goBack(); // return to previous opened page for the cluster view
|
||||
navigate(clusterViewURL({ params: { clusterId } }))
|
||||
} catch (err) {
|
||||
this.error = String(err);
|
||||
} finally {
|
||||
@ -165,12 +167,14 @@ export class AddCluster extends React.Component {
|
||||
return (
|
||||
<WizardLayout className="AddCluster" infoPanel={this.renderInfo()}>
|
||||
<h2><Trans>Add Cluster</Trans></h2>
|
||||
<p>Choose config:</p>
|
||||
<Select
|
||||
placeholder={<Trans>Select kubeconfig</Trans>}
|
||||
value={this.clusterConfig}
|
||||
options={this.clusterOptions}
|
||||
onChange={({ value }: SelectOption) => this.clusterConfig = value}
|
||||
formatOptionLabel={this.formatClusterContextLabel}
|
||||
id="kubecontext-select"
|
||||
/>
|
||||
<div className="cluster-settings">
|
||||
<a href="#" onClick={() => this.showSettings = !this.showSettings}>
|
||||
@ -179,14 +183,15 @@ export class AddCluster extends React.Component {
|
||||
</div>
|
||||
{this.showSettings && (
|
||||
<div className="proxy-settings">
|
||||
<p>HTTP Proxy server. Used for communicating with Kubernetes API.</p>
|
||||
<Input
|
||||
autoFocus
|
||||
placeholder={_i18n._(t`A HTTP proxy server URL (format: http://<address>:<port>)`)}
|
||||
value={this.proxyServer}
|
||||
onChange={value => this.proxyServer = value}
|
||||
theme="round-black"
|
||||
/>
|
||||
<small className="hint">
|
||||
<Trans>HTTP Proxy server. Used for communicating with Kubernetes API.</Trans>
|
||||
{'A HTTP proxy server URL (format: http://<address>:<port>).'}
|
||||
</small>
|
||||
</div>
|
||||
)}
|
||||
@ -195,6 +200,7 @@ export class AddCluster extends React.Component {
|
||||
<p>Kubeconfig:</p>
|
||||
<AceEditor
|
||||
autoFocus
|
||||
showGutter={false}
|
||||
mode="yaml"
|
||||
value={this.customConfig}
|
||||
onChange={value => this.customConfig = value}
|
||||
@ -207,7 +213,7 @@ export class AddCluster extends React.Component {
|
||||
<div className="actions-panel">
|
||||
<Button
|
||||
primary
|
||||
label={<Trans>Add cluster</Trans>}
|
||||
label={<Trans>Add cluster(s)</Trans>}
|
||||
onClick={this.addCluster}
|
||||
waiting={this.isWaiting}
|
||||
/>
|
||||
|
||||
@ -58,7 +58,7 @@ export class ReleaseStore extends ItemStore<HelmRelease> {
|
||||
this.isLoading = true;
|
||||
let items;
|
||||
try {
|
||||
const { isAdmin, allowedNamespaces } = getHostedCluster();
|
||||
const { isAdmin, allowedNamespaces } = getHostedCluster()
|
||||
items = await this.loadItems(!isAdmin ? allowedNamespaces : null);
|
||||
} finally {
|
||||
if (items) {
|
||||
|
||||
@ -1,8 +1,12 @@
|
||||
import type { IClusterViewRouteParams } from "../cluster-manager/cluster-view.route";
|
||||
import { RouteProps } from "react-router";
|
||||
import { buildURL } from "../../navigation";
|
||||
|
||||
export const clusterSettingsRoute: RouteProps = {
|
||||
path: "/cluster-settings"
|
||||
export interface IClusterSettingsRouteParams extends IClusterViewRouteParams {
|
||||
}
|
||||
|
||||
export const clusterSettingsURL = buildURL(clusterSettingsRoute.path)
|
||||
export const clusterSettingsRoute: RouteProps = {
|
||||
path: `/cluster/:clusterId/settings`,
|
||||
}
|
||||
|
||||
export const clusterSettingsURL = buildURL<IClusterSettingsRouteParams>(clusterSettingsRoute.path)
|
||||
|
||||
@ -1,38 +1,25 @@
|
||||
.ClusterSettings {
|
||||
grid-template-columns: unset;
|
||||
padding: 0;
|
||||
.WizardLayout {
|
||||
grid-template-columns: unset;
|
||||
grid-template-rows: 76px 1fr;
|
||||
padding: 0;
|
||||
|
||||
.head-col {
|
||||
justify-content: space-between;
|
||||
.head-col {
|
||||
justify-content: space-between;
|
||||
|
||||
:nth-child(2) {
|
||||
flex: 1 0 0;
|
||||
:nth-child(2) {
|
||||
flex: 1 0 0;
|
||||
}
|
||||
}
|
||||
|
||||
a {
|
||||
text-decoration: none;
|
||||
color: $grey-600;
|
||||
}
|
||||
}
|
||||
.content-col {
|
||||
margin: 0;
|
||||
padding-top: $padding * 3;
|
||||
background-color: transparent;
|
||||
|
||||
.info-col {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.content-col {
|
||||
margin: 0;
|
||||
padding-top: $padding * 3;
|
||||
background-color: transparent;
|
||||
|
||||
.SubTitle {
|
||||
text-transform: none;
|
||||
}
|
||||
|
||||
.settings-wrapper {
|
||||
margin: 0 auto;
|
||||
width: 60%;
|
||||
min-width: 570px;
|
||||
max-width: 1000px;
|
||||
.SubTitle {
|
||||
text-transform: none;
|
||||
}
|
||||
|
||||
> div {
|
||||
margin-top: $margin * 5;
|
||||
@ -47,49 +34,50 @@
|
||||
.button-area {
|
||||
margin-top: $margin * 2;
|
||||
}
|
||||
|
||||
.file-loader {
|
||||
margin-top: $margin * 2;
|
||||
}
|
||||
|
||||
.hint {
|
||||
font-size: smaller;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
p + p, .hint + p {
|
||||
padding-top: $padding;
|
||||
}
|
||||
}
|
||||
|
||||
.file-loader {
|
||||
margin-top: $margin * 2;
|
||||
}
|
||||
.status-table {
|
||||
margin: $margin * 3 0;
|
||||
|
||||
.hint {
|
||||
font-size: smaller;
|
||||
opacity: 0.8;
|
||||
}
|
||||
.Table {
|
||||
border: 1px solid var(--drawerSubtitleBackground);
|
||||
border-radius: $radius;
|
||||
|
||||
p + p, .hint + p {
|
||||
padding-top: $padding;
|
||||
}
|
||||
}
|
||||
.TableRow {
|
||||
&:not(:last-of-type) {
|
||||
border-bottom: 1px solid var(--drawerSubtitleBackground);
|
||||
}
|
||||
|
||||
.status-table {
|
||||
margin: $margin * 3 0;
|
||||
|
||||
.Table {
|
||||
border: 1px solid var(--drawerSubtitleBackground);
|
||||
border-radius: $radius;
|
||||
|
||||
.TableRow {
|
||||
&:not(:last-of-type) {
|
||||
border-bottom: 1px solid var(--drawerSubtitleBackground);
|
||||
}
|
||||
|
||||
.value {
|
||||
flex-grow: 2;
|
||||
color: var(--textColorSecondary);
|
||||
.value {
|
||||
flex-grow: 2;
|
||||
word-break: break-word;
|
||||
color: var(--textColorSecondary);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.Input, .Select {
|
||||
margin-top: 10px;
|
||||
}
|
||||
.Input, .Select {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.Select {
|
||||
&__control {
|
||||
box-shadow: 0 0 0 1px $borderFaintColor;
|
||||
.Select {
|
||||
&__control {
|
||||
box-shadow: 0 0 0 1px $borderFaintColor;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -7,15 +7,17 @@ import { Features } from "./features";
|
||||
import { Removal } from "./removal";
|
||||
import { Status } from "./status";
|
||||
import { General } from "./general";
|
||||
import { getHostedCluster } from "../../../common/cluster-store";
|
||||
import { WizardLayout } from "../layout/wizard-layout";
|
||||
import { ClusterIcon } from "../cluster-icon";
|
||||
import { Icon } from "../icon";
|
||||
import { getMatchedCluster } from "../cluster-manager/cluster-view.route";
|
||||
import { navigate } from "../../navigation";
|
||||
|
||||
@observer
|
||||
export class ClusterSettings extends React.Component {
|
||||
render() {
|
||||
const cluster = getHostedCluster();
|
||||
const cluster = getMatchedCluster();
|
||||
if (!cluster) return null;
|
||||
const header = (
|
||||
<>
|
||||
<ClusterIcon
|
||||
@ -24,20 +26,18 @@ export class ClusterSettings extends React.Component {
|
||||
showTooltip={false}
|
||||
/>
|
||||
<h2>{cluster.preferences.clusterName}</h2>
|
||||
<Link to="/">
|
||||
<Icon material="close" big />
|
||||
</Link>
|
||||
<Icon material="close" onClick={() => navigate("/")} big/>
|
||||
</>
|
||||
);
|
||||
return (
|
||||
<WizardLayout header={header} className="ClusterSettings">
|
||||
<div className="settings-wrapper">
|
||||
<div className="ClusterSettings">
|
||||
<WizardLayout header={header} centered>
|
||||
<Status cluster={cluster}></Status>
|
||||
<General cluster={cluster}></General>
|
||||
<Features cluster={cluster}></Features>
|
||||
<Removal cluster={cluster}></Removal>
|
||||
</div>
|
||||
</WizardLayout>
|
||||
</WizardLayout>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -11,7 +11,7 @@ export class Status extends React.Component<Props> {
|
||||
renderStatusRows() {
|
||||
const { cluster } = this.props;
|
||||
const rows = [
|
||||
["Online Status", cluster.online ? "online" : `offline (${cluster.failureReason || "unknown reason"}`],
|
||||
["Online Status", cluster.online ? "online" : `offline (${cluster.failureReason || "unknown reason"})`],
|
||||
["Distribution", cluster.distribution],
|
||||
["Kerbel Version", cluster.version],
|
||||
["API Address", cluster.apiUrl],
|
||||
|
||||
@ -1,2 +1,2 @@
|
||||
export * from "./cluster.routes"
|
||||
export * from "./cluster.route"
|
||||
|
||||
|
||||
@ -1,16 +1,18 @@
|
||||
import "./landing-page.scss"
|
||||
import React from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { clusterStore } from "../../../common/cluster-store";
|
||||
import { Trans } from "@lingui/macro";
|
||||
import { clusterStore } from "../../../common/cluster-store";
|
||||
import { workspaceStore } from "../../../common/workspace-store";
|
||||
|
||||
@observer
|
||||
export class LandingPage extends React.Component {
|
||||
render() {
|
||||
const noClusters = !clusterStore.hasClusters();
|
||||
const clusters = clusterStore.getByWorkspaceId(workspaceStore.currentWorkspaceId);
|
||||
const noClustersInScope = !clusters.length;
|
||||
return (
|
||||
<div className="LandingPage flex">
|
||||
{noClusters && (
|
||||
{noClustersInScope && (
|
||||
<div className="no-clusters flex column gaps box center">
|
||||
<h1>
|
||||
<Trans>Welcome!</Trans>
|
||||
|
||||
@ -1,23 +1,51 @@
|
||||
.Preferences {
|
||||
h2 {
|
||||
&:not(:first-child) {
|
||||
margin-top: $padding * 3;
|
||||
position: fixed!important; // Allows to cover ClustersMenu
|
||||
z-index: 1;
|
||||
|
||||
.WizardLayout {
|
||||
grid-template-columns: unset;
|
||||
grid-template-rows: 76px 1fr;
|
||||
padding: 0;
|
||||
|
||||
.content-col {
|
||||
background-color: transparent;
|
||||
padding: $padding * 8 0;
|
||||
|
||||
h2 {
|
||||
margin-bottom: $margin * 2;
|
||||
|
||||
&:not(:first-child) {
|
||||
margin-top: $margin * 3;
|
||||
}
|
||||
}
|
||||
|
||||
.repos {
|
||||
position: relative;
|
||||
|
||||
.Badge {
|
||||
display: flex;
|
||||
margin: 0;
|
||||
margin-bottom: 1px;
|
||||
padding: $padding $padding * 2;
|
||||
}
|
||||
}
|
||||
|
||||
.hint {
|
||||
margin-top: -$margin;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.info-block {
|
||||
--flex-gap: #{$padding};
|
||||
.is-mac & {
|
||||
.WizardLayout .head-col {
|
||||
padding-top: 32px;
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
.repos {
|
||||
--flex-gap: #{$padding};
|
||||
|
||||
> .title {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.Badge {
|
||||
margin: $padding / 2;
|
||||
.Select {
|
||||
&__control {
|
||||
box-shadow: 0 0 0 1px $borderFaintColor;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,5 +1,5 @@
|
||||
import "./preferences.scss"
|
||||
import React, { Fragment } from "react";
|
||||
import React from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { action, computed, observable } from "mobx";
|
||||
import { t, Trans } from "@lingui/macro";
|
||||
@ -15,11 +15,12 @@ import { Notifications } from "../notifications";
|
||||
import { Badge } from "../badge";
|
||||
import { Spinner } from "../spinner";
|
||||
import { themeStore } from "../../theme.store";
|
||||
import { history } from "../../navigation";
|
||||
import { Tooltip } from "../tooltip";
|
||||
|
||||
@observer
|
||||
export class Preferences extends React.Component {
|
||||
@observable helmLoading = false;
|
||||
@observable helmUpdating = false;
|
||||
@observable helmRepos: HelmRepo[] = [];
|
||||
@observable helmAddedRepos = observable.map<string, HelmRepo>();
|
||||
|
||||
@ -88,9 +89,9 @@ export class Preferences extends React.Component {
|
||||
Notifications.ok(<Trans>Helm branch <b>{repo.name}</b> already in use</Trans>)
|
||||
return;
|
||||
}
|
||||
this.helmUpdating = false;
|
||||
this.helmLoading = true;
|
||||
await this.addRepo(repo);
|
||||
this.helmUpdating = false;
|
||||
this.helmLoading = false;
|
||||
}
|
||||
|
||||
formatHelmOptionLabel = ({ value: repo }: SelectOption<HelmRepo>) => {
|
||||
@ -103,104 +104,95 @@ export class Preferences extends React.Component {
|
||||
)
|
||||
}
|
||||
|
||||
renderInfo() {
|
||||
return (
|
||||
<Fragment>
|
||||
<h2>
|
||||
<Trans>Preferences</Trans>
|
||||
</h2>
|
||||
<div className="info-block flex gaps align-flex-start">
|
||||
<Icon small material="info"/>
|
||||
<p>
|
||||
<Trans>Lens Global Settings</Trans> (<Trans>applicable to all clusters</Trans>)
|
||||
</p>
|
||||
</div>
|
||||
</Fragment>
|
||||
)
|
||||
}
|
||||
|
||||
render() {
|
||||
const { preferences } = userStore;
|
||||
const header = (
|
||||
<>
|
||||
<h2>Preferences</h2>
|
||||
<Icon material="close" big onClick={history.goBack}/>
|
||||
</>
|
||||
);
|
||||
return (
|
||||
<WizardLayout className="Preferences" infoPanel={this.renderInfo()}>
|
||||
<h2><Trans>Color Theme</Trans></h2>
|
||||
<Select
|
||||
options={this.themeOptions}
|
||||
value={preferences.colorTheme}
|
||||
onChange={({ value }: SelectOption) => preferences.colorTheme = value}
|
||||
/>
|
||||
<div className="Preferences">
|
||||
<WizardLayout header={header} centered>
|
||||
<h2><Trans>Color Theme</Trans></h2>
|
||||
<Select
|
||||
options={this.themeOptions}
|
||||
value={preferences.colorTheme}
|
||||
onChange={({ value }: SelectOption) => preferences.colorTheme = value}
|
||||
/>
|
||||
|
||||
<h2><Trans>Download Mirror</Trans></h2>
|
||||
<Select
|
||||
placeholder={<Trans>Download mirror for kubectl</Trans>}
|
||||
options={this.downloadMirrorOptions}
|
||||
value={preferences.downloadMirror}
|
||||
onChange={({ value }: SelectOption) => preferences.downloadMirror = value}
|
||||
/>
|
||||
<h2><Trans>Download Mirror</Trans></h2>
|
||||
<Select
|
||||
placeholder={<Trans>Download mirror for kubectl</Trans>}
|
||||
options={this.downloadMirrorOptions}
|
||||
value={preferences.downloadMirror}
|
||||
onChange={({ value }: SelectOption) => preferences.downloadMirror = value}
|
||||
/>
|
||||
|
||||
<h2><Trans>Helm</Trans></h2>
|
||||
<Select
|
||||
placeholder={<Trans>Repositories</Trans>}
|
||||
isLoading={this.helmLoading}
|
||||
isDisabled={this.helmUpdating}
|
||||
options={this.helmOptions}
|
||||
onChange={this.onRepoSelect}
|
||||
formatOptionLabel={this.formatHelmOptionLabel}
|
||||
controlShouldRenderValue={false}
|
||||
/>
|
||||
<div className="repos flex gaps align-center">
|
||||
<div className="title">
|
||||
<Trans>Added repos:</Trans>
|
||||
</div>
|
||||
<div className="repos-list">
|
||||
{this.helmLoading && <Spinner/>}
|
||||
<h2><Trans>Helm</Trans></h2>
|
||||
<Select
|
||||
placeholder={<Trans>Repositories</Trans>}
|
||||
isLoading={this.helmLoading}
|
||||
isDisabled={this.helmLoading}
|
||||
options={this.helmOptions}
|
||||
onChange={this.onRepoSelect}
|
||||
formatOptionLabel={this.formatHelmOptionLabel}
|
||||
controlShouldRenderValue={false}
|
||||
/>
|
||||
<div className="repos flex gaps column">
|
||||
{Array.from(this.helmAddedRepos).map(([name, repo]) => {
|
||||
const tooltipId = `message-${name}`;
|
||||
return (
|
||||
<Badge key={name} className="added-repo flex gaps align-center" title={repo.url}>
|
||||
<span className="repo">{name}</span>
|
||||
<Badge key={name} className="added-repo flex gaps align-center justify-space-between">
|
||||
<span id={tooltipId} className="repo">{name}</span>
|
||||
<Icon
|
||||
material="remove_circle_outline"
|
||||
material="delete"
|
||||
onClick={() => this.removeRepo(repo)}
|
||||
tooltip={<Trans>Remove</Trans>}
|
||||
/>
|
||||
<Tooltip targetId={tooltipId} formatters={{ narrow: true }}>
|
||||
{repo.url}
|
||||
</Tooltip>
|
||||
</Badge>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h2><Trans>HTTP Proxy</Trans></h2>
|
||||
<Input
|
||||
placeholder={_i18n._(t`Type HTTP proxy url (example: http://proxy.acme.org:8080)`)}
|
||||
value={preferences.httpsProxy || ""}
|
||||
onChange={v => preferences.httpsProxy = v}
|
||||
/>
|
||||
<small className="hint">
|
||||
<Trans>Proxy is used only for non-cluster communication.</Trans>
|
||||
</small>
|
||||
<h2><Trans>HTTP Proxy</Trans></h2>
|
||||
<Input
|
||||
theme="round-black"
|
||||
placeholder={_i18n._(t`Type HTTP proxy url (example: http://proxy.acme.org:8080)`)}
|
||||
value={preferences.httpsProxy || ""}
|
||||
onChange={v => preferences.httpsProxy = v}
|
||||
/>
|
||||
<small className="hint">
|
||||
<Trans>Proxy is used only for non-cluster communication.</Trans>
|
||||
</small>
|
||||
|
||||
<h2><Trans>Certificate Trust</Trans></h2>
|
||||
<Checkbox
|
||||
label={<Trans>Allow untrusted Certificate Authorities</Trans>}
|
||||
value={preferences.allowUntrustedCAs}
|
||||
onChange={v => preferences.allowUntrustedCAs = v}
|
||||
/>
|
||||
<small className="hint">
|
||||
<Trans>This will make Lens to trust ANY certificate authority without any validations.</Trans>{" "}
|
||||
<Trans>Needed with some corporate proxies that do certificate re-writing.</Trans>{" "}
|
||||
<Trans>Does not affect cluster communications!</Trans>
|
||||
</small>
|
||||
<h2><Trans>Certificate Trust</Trans></h2>
|
||||
<Checkbox
|
||||
label={<Trans>Allow untrusted Certificate Authorities</Trans>}
|
||||
value={preferences.allowUntrustedCAs}
|
||||
onChange={v => preferences.allowUntrustedCAs = v}
|
||||
/>
|
||||
<small className="hint">
|
||||
<Trans>This will make Lens to trust ANY certificate authority without any validations.</Trans>{" "}
|
||||
<Trans>Needed with some corporate proxies that do certificate re-writing.</Trans>{" "}
|
||||
<Trans>Does not affect cluster communications!</Trans>
|
||||
</small>
|
||||
|
||||
<h2><Trans>Telemetry & Usage Tracking</Trans></h2>
|
||||
<Checkbox
|
||||
label={<Trans>Allow telemetry & usage tracking</Trans>}
|
||||
value={preferences.allowTelemetry}
|
||||
onChange={v => preferences.allowTelemetry = v}
|
||||
/>
|
||||
<small className="hint">
|
||||
<Trans>Telemetry & usage data is collected to continuously improve the Lens experience.</Trans>
|
||||
</small>
|
||||
</WizardLayout>
|
||||
)
|
||||
<h2><Trans>Telemetry & Usage Tracking</Trans></h2>
|
||||
<Checkbox
|
||||
label={<Trans>Allow telemetry & usage tracking</Trans>}
|
||||
value={preferences.allowTelemetry}
|
||||
onChange={v => preferences.allowTelemetry = v}
|
||||
/>
|
||||
<small className="hint">
|
||||
<Trans>Telemetry & usage data is collected to continuously improve the Lens experience.</Trans>
|
||||
</small>
|
||||
</WizardLayout>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -5,7 +5,7 @@ import { observer } from "mobx-react";
|
||||
import { Trans } from "@lingui/macro";
|
||||
import { RouteComponentProps } from "react-router";
|
||||
import { Icon } from "../icon";
|
||||
import { IRoleBindingsRouteParams } from "../+user-management/user-management.routes";
|
||||
import { IRoleBindingsRouteParams } from "../+user-management/user-management.route";
|
||||
import { KubeObjectMenu, KubeObjectMenuProps } from "../kube-object/kube-object-menu";
|
||||
import { clusterRoleBindingApi, RoleBinding, roleBindingApi } from "../../api/endpoints";
|
||||
import { roleBindingsStore } from "./role-bindings.store";
|
||||
|
||||
@ -4,7 +4,7 @@ import React from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { Trans } from "@lingui/macro";
|
||||
import { RouteComponentProps } from "react-router";
|
||||
import { IRolesRouteParams } from "../+user-management/user-management.routes";
|
||||
import { IRolesRouteParams } from "../+user-management/user-management.route";
|
||||
import { KubeObjectMenu, KubeObjectMenuProps } from "../kube-object/kube-object-menu";
|
||||
import { rolesStore } from "./roles.store";
|
||||
import { clusterRoleApi, Role, roleApi } from "../../api/endpoints";
|
||||
|
||||
@ -1,2 +1,2 @@
|
||||
export * from "./user-management"
|
||||
export * from "./user-management.routes"
|
||||
export * from "./user-management.route"
|
||||
@ -8,7 +8,7 @@ import { MainLayout, TabRoute } from "../layout/main-layout";
|
||||
import { Roles } from "../+user-management-roles";
|
||||
import { RoleBindings } from "../+user-management-roles-bindings";
|
||||
import { ServiceAccounts } from "../+user-management-service-accounts";
|
||||
import { roleBindingsRoute, roleBindingsURL, rolesRoute, rolesURL, serviceAccountsRoute, serviceAccountsURL, usersManagementURL } from "./user-management.routes";
|
||||
import { roleBindingsRoute, roleBindingsURL, rolesRoute, rolesURL, serviceAccountsRoute, serviceAccountsURL, usersManagementURL } from "./user-management.route";
|
||||
import { namespaceStore } from "../+namespaces/namespace.store";
|
||||
import { PodSecurityPolicies, podSecurityPoliciesRoute, podSecurityPoliciesURL } from "../+pod-security-policies";
|
||||
import { isAllowedResource } from "../../../common/rbac";
|
||||
|
||||
@ -7,12 +7,11 @@ import { userStore } from "../../../common/user-store"
|
||||
import { navigate } from "../../navigation";
|
||||
import { Button } from "../button";
|
||||
import { Trans } from "@lingui/macro";
|
||||
import { staticDir } from "../../../common/vars";
|
||||
import marked from "marked"
|
||||
|
||||
@observer
|
||||
export class WhatsNew extends React.Component {
|
||||
releaseNotes = fs.readFileSync(path.join(staticDir, "RELEASE_NOTES.md")).toString();
|
||||
releaseNotes = fs.readFileSync(path.join(__static, "RELEASE_NOTES.md")).toString();
|
||||
|
||||
ok = () => {
|
||||
navigate("/");
|
||||
|
||||
@ -13,7 +13,6 @@ interface Props {
|
||||
|
||||
export class AppInit extends React.Component<Props> {
|
||||
static async start(rootElem: HTMLElement) {
|
||||
|
||||
render(<AppInit/>, rootElem); // show loading indicator asap
|
||||
await AppInit.readyStateCheck(rootElem); // wait while all good to run
|
||||
}
|
||||
|
||||
@ -1,13 +1,14 @@
|
||||
import "./app.scss";
|
||||
import React from "react";
|
||||
import { disposeOnUnmount, observer } from "mobx-react";
|
||||
import { observable, reaction } from "mobx";
|
||||
import { Redirect, Route, Switch } from "react-router";
|
||||
import { observer } from "mobx-react";
|
||||
import { Redirect, Route, Router, Switch } from "react-router";
|
||||
import { I18nProvider } from "@lingui/react";
|
||||
import { _i18n } from "../i18n";
|
||||
import { history } from "../navigation";
|
||||
import { Notifications } from "./notifications";
|
||||
import { NotFound } from "./+404";
|
||||
import { UserManagement } from "./+user-management/user-management";
|
||||
import { ConfirmDialog } from "./confirm-dialog";
|
||||
import { usersManagementRoute } from "./+user-management/user-management.routes";
|
||||
import { usersManagementRoute } from "./+user-management/user-management.route";
|
||||
import { clusterRoute, clusterURL } from "./+cluster";
|
||||
import { KubeConfigDialog } from "./kubeconfig-dialog/kubeconfig-dialog";
|
||||
import { Nodes, nodesRoute } from "./+nodes";
|
||||
@ -27,94 +28,60 @@ import { DeploymentScaleDialog } from "./+workloads-deployments/deployment-scale
|
||||
import { CustomResources } from "./+custom-resources/custom-resources";
|
||||
import { crdRoute } from "./+custom-resources";
|
||||
import { isAllowedResource } from "../../common/rbac";
|
||||
import { AddCluster, addClusterRoute } from "./+add-cluster";
|
||||
import { LandingPage, landingRoute, landingURL } from "./+landing-page";
|
||||
import { ClusterSettings, clusterSettingsRoute } from "./+cluster-settings";
|
||||
import { Workspaces, workspacesRoute } from "./+workspaces";
|
||||
import { ErrorBoundary } from "./error-boundary";
|
||||
import { Terminal } from "./dock/terminal";
|
||||
import { getHostedCluster, getHostedClusterId } from "../../common/cluster-store";
|
||||
import logger from "../../main/logger";
|
||||
import { clusterIpc } from "../../common/cluster-ipc";
|
||||
import { getHostedCluster } from "../../common/cluster-store";
|
||||
import { clusterStatusRoute, clusterStatusURL } from "./cluster-manager/cluster-status.route";
|
||||
import { Preferences, preferencesRoute } from "./+preferences";
|
||||
import { ClusterStatus } from "./cluster-manager/cluster-status";
|
||||
import { CubeSpinner } from "./spinner";
|
||||
import { navigate, navigation } from "../navigation";
|
||||
import { webFrame } from "electron";
|
||||
|
||||
@observer
|
||||
export class App extends React.Component {
|
||||
@observable isReady = false;
|
||||
|
||||
get cluster() {
|
||||
return getHostedCluster()
|
||||
}
|
||||
|
||||
async componentDidMount() {
|
||||
if (this.cluster) {
|
||||
await clusterIpc.activate.invokeFromRenderer(); // refresh state, reconnect, etc.
|
||||
disposeOnUnmount(this, [
|
||||
reaction(() => this.cluster.accessible, this.onClusterAccessChange, {
|
||||
fireImmediately: true
|
||||
})
|
||||
])
|
||||
}
|
||||
this.isReady = true;
|
||||
}
|
||||
|
||||
protected onClusterAccessChange = (accessible: boolean) => {
|
||||
const path = navigation.getPath();
|
||||
if (!accessible || path === "/") {
|
||||
navigate(this.startURL);
|
||||
}
|
||||
static async init() {
|
||||
const clusterId = getHostedClusterId();
|
||||
logger.info(`[APP]: Init dashboard, clusterId=${clusterId}`)
|
||||
await Terminal.preloadFonts()
|
||||
await clusterIpc.init.invokeFromRenderer(clusterId, webFrame.routingId);
|
||||
await getHostedCluster().whenInitialized;
|
||||
}
|
||||
|
||||
get startURL() {
|
||||
if (this.cluster) {
|
||||
if (!this.cluster.accessible) {
|
||||
return clusterStatusURL();
|
||||
}
|
||||
if (isAllowedResource(["events", "nodes", "pods"])) {
|
||||
return clusterURL();
|
||||
}
|
||||
return workloadsURL();
|
||||
if (isAllowedResource(["events", "nodes", "pods"])) {
|
||||
return clusterURL();
|
||||
}
|
||||
return landingURL();
|
||||
return workloadsURL();
|
||||
}
|
||||
|
||||
render() {
|
||||
if (!this.isReady) {
|
||||
return <CubeSpinner className="box center"/>
|
||||
}
|
||||
return (
|
||||
<ErrorBoundary>
|
||||
<Switch>
|
||||
<Route component={LandingPage} {...landingRoute}/>
|
||||
<Route component={Preferences} {...preferencesRoute}/>
|
||||
<Route component={Workspaces} {...workspacesRoute}/>
|
||||
<Route component={AddCluster} {...addClusterRoute}/>
|
||||
<Route component={Cluster} {...clusterRoute}/>
|
||||
<Route component={ClusterStatus} {...clusterStatusRoute}/>
|
||||
<Route component={ClusterSettings} {...clusterSettingsRoute}/>
|
||||
<Route component={Nodes} {...nodesRoute}/>
|
||||
<Route component={Workloads} {...workloadsRoute}/>
|
||||
<Route component={Config} {...configRoute}/>
|
||||
<Route component={Network} {...networkRoute}/>
|
||||
<Route component={Storage} {...storageRoute}/>
|
||||
<Route component={Namespaces} {...namespacesRoute}/>
|
||||
<Route component={Events} {...eventRoute}/>
|
||||
<Route component={CustomResources} {...crdRoute}/>
|
||||
<Route component={UserManagement} {...usersManagementRoute}/>
|
||||
<Route component={Apps} {...appsRoute}/>
|
||||
<Redirect exact from="/" to={this.startURL}/>
|
||||
<Route component={NotFound}/>
|
||||
</Switch>
|
||||
<KubeObjectDetails/>
|
||||
<Notifications/>
|
||||
<ConfirmDialog/>
|
||||
<KubeConfigDialog/>
|
||||
<AddRoleBindingDialog/>
|
||||
<PodLogsDialog/>
|
||||
<DeploymentScaleDialog/>
|
||||
</ErrorBoundary>
|
||||
<I18nProvider i18n={_i18n}>
|
||||
<Router history={history}>
|
||||
<ErrorBoundary>
|
||||
<Switch>
|
||||
<Route component={Cluster} {...clusterRoute}/>
|
||||
<Route component={Nodes} {...nodesRoute}/>
|
||||
<Route component={Workloads} {...workloadsRoute}/>
|
||||
<Route component={Config} {...configRoute}/>
|
||||
<Route component={Network} {...networkRoute}/>
|
||||
<Route component={Storage} {...storageRoute}/>
|
||||
<Route component={Namespaces} {...namespacesRoute}/>
|
||||
<Route component={Events} {...eventRoute}/>
|
||||
<Route component={CustomResources} {...crdRoute}/>
|
||||
<Route component={UserManagement} {...usersManagementRoute}/>
|
||||
<Route component={Apps} {...appsRoute}/>
|
||||
<Redirect exact from="/" to={this.startURL}/>
|
||||
<Route component={NotFound}/>
|
||||
</Switch>
|
||||
<Notifications/>
|
||||
<ConfirmDialog/>
|
||||
<KubeObjectDetails/>
|
||||
<KubeConfigDialog/>
|
||||
<AddRoleBindingDialog/>
|
||||
<PodLogsDialog/>
|
||||
<DeploymentScaleDialog/>
|
||||
</ErrorBoundary>
|
||||
</Router>
|
||||
</I18nProvider>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,20 +1,27 @@
|
||||
.ClusterManager {
|
||||
display: grid;
|
||||
grid-template-areas: "menu lens-view" "menu lens-view" "bottom-bar bottom-bar";
|
||||
grid-template-areas: "menu main" "menu main" "bottom-bar bottom-bar";
|
||||
grid-template-rows: auto 1fr min-content;
|
||||
grid-template-columns: min-content 1fr;
|
||||
height: 100%;
|
||||
|
||||
#lens-view {
|
||||
main {
|
||||
grid-area: main;
|
||||
position: relative;
|
||||
grid-area: lens-view;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
|
||||
&.inactive {
|
||||
opacity: .85;
|
||||
filter: grayscale(1);
|
||||
user-select: none;
|
||||
pointer-events: none;
|
||||
> * {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
display: flex;
|
||||
background-color: $mainBackground;
|
||||
|
||||
> * {
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -1,52 +1,70 @@
|
||||
import "./cluster-manager.scss"
|
||||
import React from "react";
|
||||
import { computed } from "mobx";
|
||||
import { observer } from "mobx-react";
|
||||
import { App } from "../app";
|
||||
import { Redirect, Route, Switch } from "react-router";
|
||||
import { reaction } from "mobx";
|
||||
import { disposeOnUnmount, observer } from "mobx-react";
|
||||
import { ClustersMenu } from "./clusters-menu";
|
||||
import { BottomBar } from "./bottom-bar";
|
||||
import { cssNames, IClassName } from "../../utils";
|
||||
import { Terminal } from "../dock/terminal";
|
||||
import { i18nStore } from "../../i18n";
|
||||
import { themeStore } from "../../theme.store";
|
||||
import { clusterStore, getHostedClusterId, isNoClustersView } from "../../../common/cluster-store";
|
||||
import { CubeSpinner } from "../spinner";
|
||||
|
||||
interface Props {
|
||||
className?: IClassName;
|
||||
contentClass?: IClassName;
|
||||
}
|
||||
import { LandingPage, landingRoute, landingURL } from "../+landing-page";
|
||||
import { Preferences, preferencesRoute } from "../+preferences";
|
||||
import { Workspaces, workspacesRoute } from "../+workspaces";
|
||||
import { AddCluster, addClusterRoute } from "../+add-cluster";
|
||||
import { ClusterView } from "./cluster-view";
|
||||
import { ClusterSettings, clusterSettingsRoute } from "../+cluster-settings";
|
||||
import { clusterViewRoute, clusterViewURL, getMatchedCluster, getMatchedClusterId } from "./cluster-view.route";
|
||||
import { clusterStore } from "../../../common/cluster-store";
|
||||
import { hasLoadedView, initView, lensViews, refreshViews } from "./lens-views";
|
||||
|
||||
@observer
|
||||
export class ClusterManager extends React.Component<Props> {
|
||||
static async init() {
|
||||
await Promise.all([
|
||||
i18nStore.init(),
|
||||
themeStore.init(),
|
||||
Terminal.preloadFonts(),
|
||||
export class ClusterManager extends React.Component {
|
||||
componentDidMount() {
|
||||
disposeOnUnmount(this, [
|
||||
reaction(getMatchedClusterId, initView, {
|
||||
fireImmediately: true
|
||||
}),
|
||||
reaction(() => [
|
||||
hasLoadedView(getMatchedClusterId()), // refresh when cluster's webview loaded
|
||||
getMatchedCluster()?.available, // refresh on disconnect active-cluster
|
||||
], refreshViews, {
|
||||
fireImmediately: true
|
||||
})
|
||||
])
|
||||
}
|
||||
|
||||
@computed get isInactive() {
|
||||
const { activeCluster, activeClusterId, clusters } = clusterStore;
|
||||
const isActivatedBefore = activeCluster?.initialized;
|
||||
return clusters.size > 0 && !isActivatedBefore && activeClusterId !== getHostedClusterId();
|
||||
componentWillUnmount() {
|
||||
lensViews.clear();
|
||||
}
|
||||
|
||||
get startUrl() {
|
||||
const { activeClusterId } = clusterStore;
|
||||
if (activeClusterId) {
|
||||
return clusterViewURL({
|
||||
params: {
|
||||
clusterId: activeClusterId
|
||||
}
|
||||
})
|
||||
}
|
||||
return landingURL()
|
||||
}
|
||||
|
||||
render() {
|
||||
const { className, contentClass } = this.props;
|
||||
const lensViewClass = cssNames("flex column", contentClass, {
|
||||
inactive: this.isInactive,
|
||||
});
|
||||
return (
|
||||
<div className={cssNames("ClusterManager", className)}>
|
||||
<div className="ClusterManager">
|
||||
<div id="draggable-top"/>
|
||||
<div id="lens-view" className={lensViewClass}>
|
||||
<App/>
|
||||
</div>
|
||||
<main>
|
||||
<div id="lens-views"/>
|
||||
<Switch>
|
||||
<Route component={LandingPage} {...landingRoute}/>
|
||||
<Route component={Preferences} {...preferencesRoute}/>
|
||||
<Route component={Workspaces} {...workspacesRoute}/>
|
||||
<Route component={AddCluster} {...addClusterRoute}/>
|
||||
<Route component={ClusterView} {...clusterViewRoute}/>
|
||||
<Route component={ClusterSettings} {...clusterSettingsRoute}/>
|
||||
<Redirect exact to={this.startUrl}/>
|
||||
</Switch>
|
||||
</main>
|
||||
<ClustersMenu/>
|
||||
<BottomBar/>
|
||||
{this.isInactive && <CubeSpinner center/>}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@ -1,8 +0,0 @@
|
||||
import { RouteProps } from "react-router";
|
||||
import { buildURL } from "../../navigation";
|
||||
|
||||
export const clusterStatusRoute: RouteProps = {
|
||||
path: "/cluster-status"
|
||||
}
|
||||
|
||||
export const clusterStatusURL = buildURL(clusterStatusRoute.path)
|
||||
@ -1,14 +1,17 @@
|
||||
.ClusterStatus {
|
||||
--flex-gap: #{$padding * 2};
|
||||
|
||||
position: relative;
|
||||
min-width: 350px;
|
||||
margin: auto;
|
||||
text-align: center;
|
||||
z-index: 1;
|
||||
|
||||
pre {
|
||||
@include hidden-scrollbar;
|
||||
max-width: 70vw;
|
||||
max-height: 40vh;
|
||||
white-space: pre-line;
|
||||
}
|
||||
|
||||
.Icon {
|
||||
|
||||
@ -2,93 +2,107 @@ import type { KubeAuthProxyLog } from "../../../main/kube-auth-proxy";
|
||||
|
||||
import "./cluster-status.scss"
|
||||
import React from "react";
|
||||
import { disposeOnUnmount, observer } from "mobx-react";
|
||||
import { observer } from "mobx-react";
|
||||
import { ipcRenderer } from "electron";
|
||||
import { autorun, computed, observable } from "mobx";
|
||||
import { computed, observable } from "mobx";
|
||||
import { clusterIpc } from "../../../common/cluster-ipc";
|
||||
import { getHostedCluster } from "../../../common/cluster-store";
|
||||
import { Icon } from "../icon";
|
||||
import { Button } from "../button";
|
||||
import { cssNames } from "../../utils";
|
||||
import { navigate } from "../../navigation";
|
||||
import { cssNames, IClassName } from "../../utils";
|
||||
import { Cluster } from "../../../main/cluster";
|
||||
import { ClusterId, clusterStore } from "../../../common/cluster-store";
|
||||
import { CubeSpinner } from "../spinner";
|
||||
|
||||
interface Props {
|
||||
className?: IClassName;
|
||||
clusterId: ClusterId;
|
||||
}
|
||||
|
||||
@observer
|
||||
export class ClusterStatus extends React.Component {
|
||||
export class ClusterStatus extends React.Component<Props> {
|
||||
@observable authOutput: KubeAuthProxyLog[] = [];
|
||||
@observable isReconnecting = false;
|
||||
|
||||
@computed get cluster() {
|
||||
return getHostedCluster();
|
||||
get cluster(): Cluster {
|
||||
return clusterStore.getById(this.props.clusterId);
|
||||
}
|
||||
|
||||
@computed get hasErrors(): boolean {
|
||||
return this.authOutput.some(({ error }) => error) || !!this.cluster.failureReason;
|
||||
}
|
||||
|
||||
@disposeOnUnmount
|
||||
autoRedirectToMain = autorun(() => {
|
||||
if (this.cluster.accessible && !this.hasErrors) {
|
||||
navigate("/");
|
||||
}
|
||||
})
|
||||
|
||||
async componentDidMount() {
|
||||
this.authOutput = [{ data: "Connecting..." }];
|
||||
ipcRenderer.on(`kube-auth:${this.cluster.id}`, (evt, res: KubeAuthProxyLog) => {
|
||||
this.authOutput.push({
|
||||
data: res.data.trimRight(),
|
||||
error: res.error,
|
||||
});
|
||||
})
|
||||
if (!this.cluster.initialized || this.cluster.disconnected) {
|
||||
await this.refreshCluster();
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
ipcRenderer.removeAllListeners(`kube-auth:${this.cluster.id}`);
|
||||
ipcRenderer.removeAllListeners(`kube-auth:${this.props.clusterId}`);
|
||||
}
|
||||
|
||||
refreshCluster = async () => {
|
||||
await clusterIpc.activate.invokeFromRenderer(this.props.clusterId);
|
||||
}
|
||||
|
||||
reconnect = async () => {
|
||||
this.authOutput = [{ data: "Reconnecting..." }];
|
||||
this.isReconnecting = true;
|
||||
await clusterIpc.activate.invokeFromRenderer();
|
||||
await this.refreshCluster();
|
||||
this.isReconnecting = false;
|
||||
}
|
||||
|
||||
render() {
|
||||
renderContent() {
|
||||
const { authOutput, cluster, hasErrors } = this;
|
||||
const isDisconnected = !!cluster.disconnected;
|
||||
const failureReason = cluster.failureReason;
|
||||
const isError = hasErrors || isDisconnected;
|
||||
return (
|
||||
<div className="ClusterStatus flex column gaps">
|
||||
{isError && (
|
||||
<Icon
|
||||
material="cloud_off"
|
||||
className={cssNames({ error: hasErrors })}
|
||||
/>
|
||||
)}
|
||||
<h2>
|
||||
{cluster.contextName}
|
||||
</h2>
|
||||
{!isDisconnected && (
|
||||
if (!hasErrors || this.isReconnecting) {
|
||||
return (
|
||||
<>
|
||||
<CubeSpinner />
|
||||
<pre className="kube-auth-out">
|
||||
<p>{this.isReconnecting ? "Reconnecting..." : "Connecting..."}</p>
|
||||
{authOutput.map(({ data, error }, index) => {
|
||||
return <p key={index} className={cssNames({ error })}>{data}</p>
|
||||
})}
|
||||
</pre>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<Icon material="cloud_off" className="error" />
|
||||
<h2>
|
||||
{cluster.preferences.clusterName}
|
||||
</h2>
|
||||
<pre className="kube-auth-out">
|
||||
{authOutput.map(({ data, error }, index) => {
|
||||
return <p key={index} className={cssNames({ error })}>{data}</p>
|
||||
})}
|
||||
</pre>
|
||||
{failureReason && (
|
||||
<div className="failure-reason error">{failureReason}</div>
|
||||
)}
|
||||
{isError && (
|
||||
<Button
|
||||
primary
|
||||
label="Reconnect"
|
||||
className="box center"
|
||||
onClick={this.reconnect}
|
||||
waiting={this.isReconnecting}
|
||||
/>
|
||||
)}
|
||||
<Button
|
||||
primary
|
||||
label="Reconnect"
|
||||
className="box center"
|
||||
onClick={this.reconnect}
|
||||
waiting={this.isReconnecting}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div className={cssNames("ClusterStatus flex column gaps box center align-center justify-center", this.props.className)}>
|
||||
{this.renderContent()}
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,46 @@
|
||||
import { reaction } from "mobx";
|
||||
import { ipcRenderer } from "electron";
|
||||
import { matchPath, RouteProps } from "react-router";
|
||||
import { buildURL, navigation } from "../../navigation";
|
||||
import { clusterStore, getHostedClusterId } from "../../../common/cluster-store";
|
||||
import { clusterSettingsRoute } from "../+cluster-settings/cluster-settings.route";
|
||||
|
||||
export interface IClusterViewRouteParams {
|
||||
clusterId: string;
|
||||
}
|
||||
|
||||
export const clusterViewRoute: RouteProps = {
|
||||
exact: true,
|
||||
path: "/cluster/:clusterId",
|
||||
}
|
||||
|
||||
export const clusterViewURL = buildURL<IClusterViewRouteParams>(clusterViewRoute.path)
|
||||
|
||||
export function getMatchedClusterId(): string {
|
||||
const matched = matchPath<IClusterViewRouteParams>(navigation.location.pathname, {
|
||||
exact: true,
|
||||
path: [
|
||||
clusterViewRoute.path,
|
||||
clusterSettingsRoute.path,
|
||||
].flat(),
|
||||
})
|
||||
if (matched) {
|
||||
return matched.params.clusterId;
|
||||
}
|
||||
}
|
||||
|
||||
export function getMatchedCluster() {
|
||||
return clusterStore.getById(getMatchedClusterId())
|
||||
}
|
||||
|
||||
// Refresh global menu depending on active route's type (common/cluster view)
|
||||
if (ipcRenderer) {
|
||||
const isMainView = !getHostedClusterId();
|
||||
if (isMainView) {
|
||||
reaction(() => getMatchedClusterId(), clusterId => {
|
||||
ipcRenderer.send("cluster-view:change", clusterId);
|
||||
}, {
|
||||
fireImmediately: true
|
||||
})
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,5 @@
|
||||
.ClusterView {
|
||||
&:empty {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
21
src/renderer/components/cluster-manager/cluster-view.tsx
Normal file
21
src/renderer/components/cluster-manager/cluster-view.tsx
Normal file
@ -0,0 +1,21 @@
|
||||
import "./cluster-view.scss"
|
||||
import React from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { getMatchedCluster } from "./cluster-view.route";
|
||||
import { ClusterStatus } from "./cluster-status";
|
||||
import { hasLoadedView } from "./lens-views";
|
||||
|
||||
@observer
|
||||
export class ClusterView extends React.Component {
|
||||
render() {
|
||||
const cluster = getMatchedCluster();
|
||||
const showStatus = cluster && (!cluster.available || !hasLoadedView(cluster.id))
|
||||
return (
|
||||
<div className="ClusterView">
|
||||
{showStatus && (
|
||||
<ClusterStatus key={cluster.id} clusterId={cluster.id} className="box center"/>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -42,6 +42,7 @@
|
||||
> .add-cluster {
|
||||
position: relative;
|
||||
margin-top: $padding;
|
||||
min-width: 43px;
|
||||
|
||||
.Icon {
|
||||
border-radius: $radius;
|
||||
|
||||
@ -20,7 +20,7 @@ import { landingURL } from "../+landing-page";
|
||||
import { Tooltip } from "../tooltip";
|
||||
import { ConfirmDialog } from "../confirm-dialog";
|
||||
import { clusterIpc } from "../../../common/cluster-ipc";
|
||||
import { clusterStatusURL } from "./cluster-status.route";
|
||||
import { clusterViewURL, getMatchedClusterId } from "./cluster-view.route";
|
||||
|
||||
// fixme: allow to rearrange clusters with drag&drop
|
||||
|
||||
@ -34,7 +34,7 @@ export class ClustersMenu extends React.Component<Props> {
|
||||
|
||||
showCluster = (clusterId: ClusterId) => {
|
||||
clusterStore.setActive(clusterId);
|
||||
navigate("/"); // redirect to index
|
||||
navigate(clusterViewURL({ params: { clusterId } }));
|
||||
}
|
||||
|
||||
addCluster = () => {
|
||||
@ -48,18 +48,21 @@ export class ClustersMenu extends React.Component<Props> {
|
||||
menu.append(new MenuItem({
|
||||
label: _i18n._(t`Settings`),
|
||||
click: () => {
|
||||
clusterStore.setActive(cluster.id);
|
||||
navigate(clusterSettingsURL())
|
||||
navigate(clusterSettingsURL({
|
||||
params: {
|
||||
clusterId: cluster.id
|
||||
}
|
||||
}))
|
||||
}
|
||||
}));
|
||||
if (cluster.online) {
|
||||
menu.append(new MenuItem({
|
||||
label: _i18n._(t`Disconnect`),
|
||||
click: async () => {
|
||||
await clusterIpc.disconnect.invokeFromRenderer(cluster.id);
|
||||
if (cluster.id === clusterStore.activeClusterId) {
|
||||
navigate(clusterStatusURL());
|
||||
if (clusterStore.isActive(cluster.id)) {
|
||||
navigate(landingURL());
|
||||
}
|
||||
await clusterIpc.disconnect.invokeFromRenderer(cluster.id);
|
||||
}
|
||||
}))
|
||||
}
|
||||
@ -72,7 +75,10 @@ export class ClustersMenu extends React.Component<Props> {
|
||||
accent: true,
|
||||
label: _i18n._(t`Remove`),
|
||||
},
|
||||
ok: () => clusterStore.removeById(cluster.id),
|
||||
ok: () => {
|
||||
clusterStore.removeById(cluster.id);
|
||||
navigate(landingURL());
|
||||
},
|
||||
message: <p>Are you sure want to remove cluster <b title={cluster.id}>{cluster.contextName}</b>?</p>,
|
||||
})
|
||||
}
|
||||
@ -85,11 +91,10 @@ export class ClustersMenu extends React.Component<Props> {
|
||||
render() {
|
||||
const { className } = this.props;
|
||||
const { newContexts } = userStore;
|
||||
const { currentWorkspaceId } = workspaceStore;
|
||||
const clusters = clusterStore.getByWorkspaceId(currentWorkspaceId);
|
||||
const noClusters = !clusterStore.clusters.size;
|
||||
const clusters = clusterStore.getByWorkspaceId(workspaceStore.currentWorkspaceId);
|
||||
const noClustersInScope = clusters.length === 0;
|
||||
const isLanding = navigation.getPath() === landingURL();
|
||||
const showStartupHint = this.showHint && isLanding && noClusters;
|
||||
const showStartupHint = this.showHint && isLanding && noClustersInScope;
|
||||
return (
|
||||
<div
|
||||
className={cssNames("ClustersMenu flex column gaps", className)}
|
||||
@ -111,7 +116,7 @@ export class ClustersMenu extends React.Component<Props> {
|
||||
key={cluster.id}
|
||||
showErrors={true}
|
||||
cluster={cluster}
|
||||
isActive={cluster.id === clusterStore.activeClusterId}
|
||||
isActive={cluster.id === getMatchedClusterId()}
|
||||
onClick={() => this.showCluster(cluster.id)}
|
||||
onContextMenu={() => this.showContextMenu(cluster)}
|
||||
/>
|
||||
|
||||
43
src/renderer/components/cluster-manager/lens-views.ts
Normal file
43
src/renderer/components/cluster-manager/lens-views.ts
Normal file
@ -0,0 +1,43 @@
|
||||
import { observable } from "mobx";
|
||||
import { ClusterId, clusterStore } from "../../../common/cluster-store";
|
||||
import { getMatchedCluster } from "./cluster-view.route"
|
||||
import logger from "../../../main/logger";
|
||||
|
||||
export interface LensView {
|
||||
isLoaded?: boolean
|
||||
clusterId: ClusterId;
|
||||
view: HTMLIFrameElement
|
||||
}
|
||||
|
||||
export const lensViews = observable.map<ClusterId, LensView>();
|
||||
|
||||
export function hasLoadedView(clusterId: ClusterId): boolean {
|
||||
return !!lensViews.get(clusterId)?.isLoaded;
|
||||
}
|
||||
|
||||
export async function initView(clusterId: ClusterId) {
|
||||
if (!clusterId || lensViews.has(clusterId)) {
|
||||
return;
|
||||
}
|
||||
logger.info(`[LENS-VIEW]: init dashboard, clusterId=${clusterId}`)
|
||||
const cluster = clusterStore.getById(clusterId);
|
||||
await cluster.whenInitialized;
|
||||
const parentElem = document.getElementById("lens-views");
|
||||
const iframe = document.createElement("iframe");
|
||||
iframe.name = cluster.preferences.clusterName;
|
||||
iframe.setAttribute("src", `//${clusterId}.${location.host}`)
|
||||
iframe.addEventListener("load", async () => {
|
||||
logger.info(`[LENS-VIEW]: loaded from ${iframe.src}`)
|
||||
lensViews.get(clusterId).isLoaded = true;
|
||||
})
|
||||
lensViews.set(clusterId, { clusterId, view: iframe });
|
||||
parentElem.appendChild(iframe);
|
||||
}
|
||||
|
||||
export function refreshViews() {
|
||||
const cluster = getMatchedCluster();
|
||||
lensViews.forEach(({ clusterId, view, isLoaded }) => {
|
||||
const isVisible = cluster && cluster.available && cluster.id === clusterId;
|
||||
view.style.display = isLoaded && isVisible ? "flex" : "none"
|
||||
})
|
||||
}
|
||||
@ -38,7 +38,7 @@ export const isNumber: Validator = {
|
||||
export const isUrl: Validator = {
|
||||
condition: ({ type }) => type === "url",
|
||||
message: () => _i18n._(t`Wrong url format`),
|
||||
validate: value => !!value.match(/^(?:http(s)?:\/\/)?[\w.-]+(?:\.[\w\.-]+)+[\w\-\._~:/?#[\]@!\$&'\(\)\*\+,;=.]+$/),
|
||||
validate: value => !!value.match(/^http(s)?:\/\/\w+(\.\w+)*(:[0-9]+)?\/?(\/[\w\-\._~:/?#[\]@!\$&'\(\)\*\+,;=.]*)*$/),
|
||||
};
|
||||
|
||||
export const minLength: Validator = {
|
||||
|
||||
@ -10,8 +10,8 @@ import { Sidebar } from "./sidebar";
|
||||
import { ErrorBoundary } from "../error-boundary";
|
||||
import { Dock } from "../dock";
|
||||
import { navigate, navigation } from "../../navigation";
|
||||
import { themeStore } from "../../theme.store";
|
||||
import { getHostedCluster } from "../../../common/cluster-store";
|
||||
import { themeStore } from "../../theme.store";
|
||||
|
||||
export interface TabRoute extends RouteProps {
|
||||
title: React.ReactNode;
|
||||
@ -47,12 +47,14 @@ export class MainLayout extends React.Component<Props> {
|
||||
|
||||
render() {
|
||||
const { className, contentClass, headerClass, tabs, footer, footerClass, children } = this.props;
|
||||
const { contextName: clusterName } = getHostedCluster();
|
||||
const routePath = navigation.location.pathname;
|
||||
const cluster = getHostedCluster();
|
||||
return (
|
||||
<div className={cssNames("MainLayout", className, themeStore.activeTheme.type)}>
|
||||
<header className={cssNames("flex gaps align-center", headerClass)}>
|
||||
<span className="cluster">{clusterName}</span>
|
||||
<span className="cluster">
|
||||
{cluster.preferences?.clusterName || cluster.contextName}
|
||||
</span>
|
||||
</header>
|
||||
|
||||
<aside className={cssNames("flex column", { pinned: this.isPinned, accessible: this.isAccessible })}>
|
||||
|
||||
@ -11,7 +11,7 @@ import { Icon } from "../icon";
|
||||
import { workloadsRoute, workloadsURL } from "../+workloads/workloads.route";
|
||||
import { namespacesURL } from "../+namespaces/namespaces.route";
|
||||
import { nodesURL } from "../+nodes/nodes.route";
|
||||
import { usersManagementRoute, usersManagementURL } from "../+user-management/user-management.routes";
|
||||
import { usersManagementRoute, usersManagementURL } from "../+user-management/user-management.route";
|
||||
import { networkRoute, networkURL } from "../+network/network.route";
|
||||
import { storageRoute, storageURL } from "../+storage/storage.route";
|
||||
import { clusterURL } from "../+cluster";
|
||||
@ -43,7 +43,9 @@ interface Props {
|
||||
@observer
|
||||
export class Sidebar extends React.Component<Props> {
|
||||
async componentDidMount() {
|
||||
if (!crdStore.isLoaded && isAllowedResource('customresourcedefinitions')) crdStore.loadAll()
|
||||
if (!crdStore.isLoaded && isAllowedResource('customresourcedefinitions')) {
|
||||
crdStore.loadAll()
|
||||
}
|
||||
}
|
||||
|
||||
renderCustomResources() {
|
||||
|
||||
@ -16,6 +16,7 @@
|
||||
> .head-col {
|
||||
position: sticky;
|
||||
border-bottom: 1px solid $grey-800;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
> .content-col {
|
||||
@ -23,6 +24,10 @@
|
||||
background-color: var(--clusters-menu-bgc);
|
||||
border-radius: $radius;
|
||||
|
||||
> div {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
> .error {
|
||||
border-radius: $radius;
|
||||
padding: $padding;
|
||||
@ -41,4 +46,17 @@
|
||||
a {
|
||||
color: $colorInfo;
|
||||
}
|
||||
|
||||
&.centered {
|
||||
.content-col {
|
||||
margin: 0;
|
||||
|
||||
> div {
|
||||
margin: 0 auto;
|
||||
width: 60%;
|
||||
min-width: 570px;
|
||||
max-width: 1000px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -10,25 +10,30 @@ interface Props {
|
||||
contentClass?: IClassName;
|
||||
infoPanelClass?: IClassName;
|
||||
infoPanel?: React.ReactNode;
|
||||
centered?: boolean; // Centering content horizontally
|
||||
}
|
||||
|
||||
@observer
|
||||
export class WizardLayout extends React.Component<Props> {
|
||||
render() {
|
||||
const { className, contentClass, infoPanelClass, infoPanel, header, headerClass, children: content } = this.props;
|
||||
const { className, contentClass, infoPanelClass, infoPanel, header, headerClass, centered, children: content } = this.props;
|
||||
return (
|
||||
<div className={cssNames("WizardLayout", className)}>
|
||||
<div className={cssNames("WizardLayout", { centered }, className)}>
|
||||
{header && (
|
||||
<div className={cssNames("head-col flex gaps align-center", headerClass)}>
|
||||
{header}
|
||||
</div>
|
||||
)}
|
||||
<div className={cssNames("content-col flex column gaps", contentClass)}>
|
||||
{content}
|
||||
</div>
|
||||
<div className={cssNames("info-col flex column gaps", infoPanelClass)}>
|
||||
{infoPanel}
|
||||
<div className="flex column gaps">
|
||||
{content}
|
||||
</div>
|
||||
</div>
|
||||
{infoPanel && (
|
||||
<div className={cssNames("info-col flex column gaps", infoPanelClass)}>
|
||||
{infoPanel}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@ -23,7 +23,7 @@ html {
|
||||
&--is-disabled {
|
||||
opacity: .75;
|
||||
cursor: not-allowed;
|
||||
pointer-events: auto;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
&__control {
|
||||
|
||||
@ -1,33 +1,19 @@
|
||||
import "../common/system-ca"
|
||||
import React from "react";
|
||||
import { render } from "react-dom";
|
||||
import { Route, Router, Switch } from "react-router";
|
||||
import { observer } from "mobx-react";
|
||||
import { userStore } from "../common/user-store";
|
||||
import { workspaceStore } from "../common/workspace-store";
|
||||
import { clusterStore } from "../common/cluster-store";
|
||||
import { I18nProvider } from "@lingui/react";
|
||||
import { history } from "./navigation";
|
||||
import { isMac } from "../common/vars";
|
||||
import { _i18n } from "./i18n";
|
||||
import { ClusterManager } from "./components/cluster-manager";
|
||||
import { ErrorBoundary } from "./components/error-boundary";
|
||||
import { WhatsNew, whatsNewRoute } from "./components/+whats-new";
|
||||
import { Notifications } from "./components/notifications";
|
||||
import { ConfirmDialog } from "./components/confirm-dialog";
|
||||
|
||||
@observer
|
||||
class LensApp extends React.Component {
|
||||
static async init() {
|
||||
const rootElem = document.getElementById("app");
|
||||
rootElem.classList.toggle("is-mac", isMac);
|
||||
await Promise.all([
|
||||
userStore.load(),
|
||||
workspaceStore.load(),
|
||||
clusterStore.load(),
|
||||
]);
|
||||
await ClusterManager.init();
|
||||
render(<LensApp/>, rootElem);
|
||||
}
|
||||
|
||||
export class LensApp extends React.Component {
|
||||
render() {
|
||||
return (
|
||||
<I18nProvider i18n={_i18n}>
|
||||
@ -39,11 +25,10 @@ class LensApp extends React.Component {
|
||||
<Route component={ClusterManager}/>
|
||||
</Switch>
|
||||
</ErrorBoundary>
|
||||
<Notifications/>
|
||||
<ConfirmDialog/>
|
||||
</Router>
|
||||
</I18nProvider>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// run
|
||||
LensApp.init();
|
||||
@ -2,21 +2,23 @@
|
||||
|
||||
import { ipcRenderer } from "electron";
|
||||
import { compile } from "path-to-regexp"
|
||||
import { createBrowserHistory, createMemoryHistory, Location, LocationDescriptor } from "history";
|
||||
import { createBrowserHistory, createMemoryHistory, LocationDescriptor } from "history";
|
||||
import { createObservableHistory } from "mobx-observable-history";
|
||||
import logger from "../main/logger";
|
||||
|
||||
export const history = typeof window !== "undefined" ? createBrowserHistory() : createMemoryHistory();
|
||||
export const navigation = createObservableHistory(history);
|
||||
|
||||
// handle navigation from other process (e.g. system menus in main, common->cluster view interactions)
|
||||
if (ipcRenderer) {
|
||||
// subscribe for navigation via menu.ts
|
||||
ipcRenderer.on("menu:navigate", (event, path: string) => {
|
||||
navigate(path);
|
||||
});
|
||||
ipcRenderer.on("menu:navigate", (event, location: LocationDescriptor) => {
|
||||
logger.info(`[IPC]: ${event.type} ${JSON.stringify(location)}`, event);
|
||||
navigate(location);
|
||||
})
|
||||
}
|
||||
|
||||
export function navigate(location: LocationDescriptor) {
|
||||
navigation.location = location as Location;
|
||||
navigation.push(location);
|
||||
}
|
||||
|
||||
export interface IURLParams<P = {}, Q = {}> {
|
||||
@ -24,6 +26,8 @@ export interface IURLParams<P = {}, Q = {}> {
|
||||
query?: IQueryParams & Q;
|
||||
}
|
||||
|
||||
// todo: extract building urls to commons (also used in menu.ts)
|
||||
// fixme: missing types validation for params & query
|
||||
export function buildURL<P extends object, Q = object>(path: string | string[]) {
|
||||
const pathBuilder = compile(path.toString());
|
||||
return function ({ params, query }: IURLParams<P, Q> = {}) {
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { computed, observable, reaction } from "mobx";
|
||||
import { autobind } from "./utils";
|
||||
import { autobind } from "./utils/autobind";
|
||||
import { userStore } from "../common/user-store";
|
||||
import logger from "../main/logger";
|
||||
|
||||
|
||||
3
types/mocks.d.ts
vendored
3
types/mocks.d.ts
vendored
@ -4,6 +4,9 @@ declare module "win-ca"
|
||||
declare module "@hapi/call"
|
||||
declare module "@hapi/subtext"
|
||||
|
||||
// Global path to static assets
|
||||
declare const __static: string;
|
||||
|
||||
// Support import for custom module extensions
|
||||
// https://www.typescriptlang.org/docs/handbook/modules.html#wildcard-module-declarations
|
||||
declare module "*.scss" {
|
||||
|
||||
@ -1,10 +1,10 @@
|
||||
import path from "path";
|
||||
import webpack, { LibraryTarget } from "webpack";
|
||||
import { isDevelopment, outDir } from "./src/common/vars";
|
||||
import { isDevelopment, buildDir } from "./src/common/vars";
|
||||
|
||||
export const library = "dll"
|
||||
export const libraryTarget: LibraryTarget = "commonjs2"
|
||||
export const manifestPath = path.resolve(outDir, `${library}.manifest.json`);
|
||||
export const manifestPath = path.resolve(buildDir, `${library}.manifest.json`);
|
||||
|
||||
export const packages = [
|
||||
"react", "react-dom",
|
||||
|
||||
@ -1,10 +1,11 @@
|
||||
import path from "path";
|
||||
import webpack from "webpack";
|
||||
import ForkTsCheckerPlugin from "fork-ts-checker-webpack-plugin"
|
||||
import { isDevelopment, isProduction, mainDir, outDir } from "./src/common/vars";
|
||||
import { isDevelopment, isProduction, mainDir, buildDir } from "./src/common/vars";
|
||||
import nodeExternals from "webpack-node-externals";
|
||||
|
||||
export default function (): webpack.Configuration {
|
||||
console.info('WEBPACK:main', require("./src/common/vars"))
|
||||
return {
|
||||
context: __dirname,
|
||||
target: "electron-main",
|
||||
@ -15,7 +16,7 @@ export default function (): webpack.Configuration {
|
||||
main: path.resolve(mainDir, "index.ts"),
|
||||
},
|
||||
output: {
|
||||
path: outDir,
|
||||
path: buildDir,
|
||||
},
|
||||
resolve: {
|
||||
extensions: ['.json', '.js', '.ts']
|
||||
|
||||
@ -1,13 +1,13 @@
|
||||
import { htmlTemplate, isDevelopment, isProduction, outDir, appName, rendererDir, sassCommonVars } from "./src/common/vars";
|
||||
import { appName, htmlTemplate, isDevelopment, isProduction, buildDir, rendererDir, sassCommonVars, publicPath } from "./src/common/vars";
|
||||
import path from "path";
|
||||
import webpack from "webpack";
|
||||
import HtmlWebpackPlugin from "html-webpack-plugin";
|
||||
import MiniCssExtractPlugin from "mini-css-extract-plugin";
|
||||
import TerserPlugin from "terser-webpack-plugin";
|
||||
import ForkTsCheckerPlugin from "fork-ts-checker-webpack-plugin"
|
||||
import CircularDependencyPlugin from "circular-dependency-plugin"
|
||||
|
||||
export default function (): webpack.Configuration {
|
||||
console.info('WEBPACK:renderer', require("./src/common/vars"))
|
||||
return {
|
||||
context: __dirname,
|
||||
target: "electron-renderer",
|
||||
@ -15,11 +15,11 @@ export default function (): webpack.Configuration {
|
||||
mode: isProduction ? "production" : "development",
|
||||
cache: isDevelopment,
|
||||
entry: {
|
||||
[appName]: path.resolve(rendererDir, "index.tsx"),
|
||||
[appName]: path.resolve(rendererDir, "bootstrap.tsx"),
|
||||
},
|
||||
output: {
|
||||
publicPath: "/",
|
||||
path: outDir,
|
||||
publicPath: publicPath,
|
||||
path: buildDir,
|
||||
filename: '[name].js',
|
||||
chunkFilename: 'chunks/[name].js',
|
||||
},
|
||||
|
||||
184
yarn.lock
184
yarn.lock
@ -950,7 +950,7 @@
|
||||
ajv "^6.12.0"
|
||||
ajv-keywords "^3.4.1"
|
||||
|
||||
"@electron/get@^1.0.1":
|
||||
"@electron/get@^1.0.1", "@electron/get@^1.12.2":
|
||||
version "1.12.2"
|
||||
resolved "https://registry.yarnpkg.com/@electron/get/-/get-1.12.2.tgz#6442066afb99be08cefb9a281e4b4692b33764f3"
|
||||
integrity sha512-vAuHUbfvBQpYTJ5wB7uVIDq5c/Ry0fiTBMs7lnEYAo/qXXppIVcWdfBr57u6eRnKdVso7KSiH6p/LbQAG6Izrg==
|
||||
@ -1978,7 +1978,7 @@
|
||||
dependencies:
|
||||
"@types/react" "*"
|
||||
|
||||
"@types/react-dom@*", "@types/react-dom@^16.9.8":
|
||||
"@types/react-dom@*":
|
||||
version "16.9.8"
|
||||
resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-16.9.8.tgz#fe4c1e11dfc67155733dfa6aa65108b4971cb423"
|
||||
integrity sha512-ykkPQ+5nFknnlU6lDd947WbQ6TE3NNzbQAkInC2EKY1qeYdTKp7onFusmYZb+ityzx2YviqT6BXSu+LyWWJwcA==
|
||||
@ -2157,7 +2157,7 @@
|
||||
resolved "https://registry.yarnpkg.com/@types/uuid/-/uuid-8.0.0.tgz#165aae4819ad2174a17476dbe66feebd549556c0"
|
||||
integrity sha512-xSQfNcvOiE5f9dyd4Kzxbof1aTrLobL278pGLKOZI6esGfZ7ts9Ka16CzIN6Y8hFHE1C7jIBZokULhK1bOgjRw==
|
||||
|
||||
"@types/webdriverio@^4.13.0":
|
||||
"@types/webdriverio@^4.13.0", "@types/webdriverio@^4.8.0":
|
||||
version "4.13.3"
|
||||
resolved "https://registry.yarnpkg.com/@types/webdriverio/-/webdriverio-4.13.3.tgz#c1571c4e62724135c0b11e7d7e36b07af5168856"
|
||||
integrity sha512-AfSQM1xTO9Ax+u9uSQPDuw69DQ0qA2RMoKHn86jCgWNcwKVUjGMSP4sfSl3JOfcZN8X/gWvn7znVPp2/g9zcJA==
|
||||
@ -2216,6 +2216,13 @@
|
||||
dependencies:
|
||||
"@types/yargs-parser" "*"
|
||||
|
||||
"@types/yauzl@^2.9.1":
|
||||
version "2.9.1"
|
||||
resolved "https://registry.yarnpkg.com/@types/yauzl/-/yauzl-2.9.1.tgz#d10f69f9f522eef3cf98e30afb684a1e1ec923af"
|
||||
integrity sha512-A1b8SU4D10uoPjwb0lnHmmu8wZhR9d+9o2PKBQT2jU5YPTKsxac6M2qGAdY7VcL+dHHhARVUDmeg0rOrcd9EjA==
|
||||
dependencies:
|
||||
"@types/node" "*"
|
||||
|
||||
"@typescript-eslint/eslint-plugin@^3.4.0":
|
||||
version "3.4.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-3.4.0.tgz#8378062e6be8a1d049259bdbcf27ce5dfbeee62b"
|
||||
@ -4188,7 +4195,7 @@ debug@4.1.0:
|
||||
dependencies:
|
||||
ms "^2.1.1"
|
||||
|
||||
debug@^2.1.3, debug@^2.2.0, debug@^2.3.3, debug@^2.5.1, debug@^2.6.9:
|
||||
debug@^2.2.0, debug@^2.3.3, debug@^2.5.1, debug@^2.6.9:
|
||||
version "2.6.9"
|
||||
resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f"
|
||||
integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==
|
||||
@ -4552,28 +4559,13 @@ electron-builder@^22.7.0:
|
||||
update-notifier "^4.1.0"
|
||||
yargs "^15.3.1"
|
||||
|
||||
electron-chromedriver@^6.0.0:
|
||||
version "6.0.0"
|
||||
resolved "https://registry.yarnpkg.com/electron-chromedriver/-/electron-chromedriver-6.0.0.tgz#a91b940c83f1c42ced52c9ef0605d8721613a8a2"
|
||||
integrity sha512-UIhRl0sN5flfUjqActXsFrZQU1NmBObvlxzPnyeud8vhR67TllXCoqfvhQJmIrJAJJK+5M1DFhJ5iTGT++dvkg==
|
||||
electron-chromedriver@^9.0.0:
|
||||
version "9.0.0"
|
||||
resolved "https://registry.yarnpkg.com/electron-chromedriver/-/electron-chromedriver-9.0.0.tgz#c7629fe6b9721140f3a380144f99960c2bc3b5c1"
|
||||
integrity sha512-+MuukzicyfduXO/4yQv9ygLKaScttJNbWtg77A9fs2YhbkISjObWaCF3eJNZL+edZXRfaF/6D4XuXvklQCmwQg==
|
||||
dependencies:
|
||||
electron-download "^4.1.1"
|
||||
extract-zip "^1.6.7"
|
||||
|
||||
electron-download@^4.1.1:
|
||||
version "4.1.1"
|
||||
resolved "https://registry.yarnpkg.com/electron-download/-/electron-download-4.1.1.tgz#02e69556705cc456e520f9e035556ed5a015ebe8"
|
||||
integrity sha512-FjEWG9Jb/ppK/2zToP+U5dds114fM1ZOJqMAR4aXXL5CvyPE9fiqBK/9YcwC9poIFQTEJk/EM/zyRwziziRZrg==
|
||||
dependencies:
|
||||
debug "^3.0.0"
|
||||
env-paths "^1.0.0"
|
||||
fs-extra "^4.0.1"
|
||||
minimist "^1.2.0"
|
||||
nugget "^2.0.1"
|
||||
path-exists "^3.0.0"
|
||||
rc "^1.2.1"
|
||||
semver "^5.4.1"
|
||||
sumchecker "^2.0.2"
|
||||
"@electron/get" "^1.12.2"
|
||||
extract-zip "^2.0.0"
|
||||
|
||||
electron-notarize@^0.3.0:
|
||||
version "0.3.0"
|
||||
@ -4647,10 +4639,10 @@ electron@*:
|
||||
"@types/node" "^12.0.12"
|
||||
extract-zip "^1.0.3"
|
||||
|
||||
electron@^9.1.0:
|
||||
version "9.1.0"
|
||||
resolved "https://registry.yarnpkg.com/electron/-/electron-9.1.0.tgz#ca77600c9e4cd591298c340e013384114d3d8d05"
|
||||
integrity sha512-VRAF8KX1m0py9I9sf0kw1kWfeC87mlscfFcbcRdLBsNJ44/GrJhi3+E8rKbpHUeZNQxsPaVA5Zu5Lxb6dV/scQ==
|
||||
electron@^9.1.2:
|
||||
version "9.1.2"
|
||||
resolved "https://registry.yarnpkg.com/electron/-/electron-9.1.2.tgz#bfa26d6b192ea13abb6f1461371fd731a8358988"
|
||||
integrity sha512-xEYadr3XqIqJ4ktBPo0lhzPdovv4jLCpiUUGc2M1frUhFhwqXokwhPaTUcE+zfu5+uf/ONDnQApwjzznBsRrgQ==
|
||||
dependencies:
|
||||
"@electron/get" "^1.0.1"
|
||||
"@types/node" "^12.0.12"
|
||||
@ -4743,11 +4735,6 @@ entities@^2.0.0:
|
||||
resolved "https://registry.yarnpkg.com/entities/-/entities-2.0.3.tgz#5c487e5742ab93c15abb5da22759b8590ec03b7f"
|
||||
integrity sha512-MyoZ0jgnLvB2X3Lg5HqpFmn1kybDiIfEQmKzTb5apr51Rb+T3KdmMiqa70T+bhGnyv7bQ6WMj2QMHpGMmlrUYQ==
|
||||
|
||||
env-paths@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/env-paths/-/env-paths-1.0.0.tgz#4168133b42bb05c38a35b1ae4397c8298ab369e0"
|
||||
integrity sha1-QWgTO0K7BcOKNbGuQ5fIKYqzaeA=
|
||||
|
||||
env-paths@^2.2.0:
|
||||
version "2.2.0"
|
||||
resolved "https://registry.yarnpkg.com/env-paths/-/env-paths-2.2.0.tgz#cdca557dc009152917d6166e2febe1f039685e43"
|
||||
@ -5098,7 +5085,7 @@ extglob@^2.0.4:
|
||||
snapdragon "^0.8.1"
|
||||
to-regex "^3.0.1"
|
||||
|
||||
extract-zip@^1.0.3, extract-zip@^1.6.7:
|
||||
extract-zip@^1.0.3:
|
||||
version "1.7.0"
|
||||
resolved "https://registry.yarnpkg.com/extract-zip/-/extract-zip-1.7.0.tgz#556cc3ae9df7f452c493a0cfb51cc30277940927"
|
||||
integrity sha512-xoh5G1W/PB0/27lXgMQyIhP5DSY/LhoCsOyZgb+6iMmRtCwVBo55uKaMoEYrDCKQhWvqEip5ZPKAc6eFNyf/MA==
|
||||
@ -5108,6 +5095,17 @@ extract-zip@^1.0.3, extract-zip@^1.6.7:
|
||||
mkdirp "^0.5.4"
|
||||
yauzl "^2.10.0"
|
||||
|
||||
extract-zip@^2.0.0:
|
||||
version "2.0.1"
|
||||
resolved "https://registry.yarnpkg.com/extract-zip/-/extract-zip-2.0.1.tgz#663dca56fe46df890d5f131ef4a06d22bb8ba13a"
|
||||
integrity sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==
|
||||
dependencies:
|
||||
debug "^4.1.1"
|
||||
get-stream "^5.1.0"
|
||||
yauzl "^2.10.0"
|
||||
optionalDependencies:
|
||||
"@types/yauzl" "^2.9.1"
|
||||
|
||||
extsprintf@1.3.0:
|
||||
version "1.3.0"
|
||||
resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.3.0.tgz#96918440e3041a7a414f8c52e3c574eb3c3e1e05"
|
||||
@ -5403,7 +5401,7 @@ fs-constants@^1.0.0:
|
||||
resolved "https://registry.yarnpkg.com/fs-constants/-/fs-constants-1.0.0.tgz#6be0de9be998ce16af8afc24497b9ee9b7ccd9ad"
|
||||
integrity sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==
|
||||
|
||||
fs-extra@^4.0.1, fs-extra@^4.0.3:
|
||||
fs-extra@^4.0.3:
|
||||
version "4.0.3"
|
||||
resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-4.0.3.tgz#0d852122e5bc5beb453fb028e9c0c9bf36340c94"
|
||||
integrity sha512-q6rbdDd1o2mAnQreO7YADIxf/Whx4AHBiRf6d+/cVT8h44ss+lHgxf1FemcqDnQt9X3ct4McHr+JMGlYSsK7Cg==
|
||||
@ -7726,7 +7724,7 @@ memory-fs@^0.5.0:
|
||||
errno "^0.1.3"
|
||||
readable-stream "^2.0.1"
|
||||
|
||||
meow@^3.1.0, meow@^3.7.0:
|
||||
meow@^3.7.0:
|
||||
version "3.7.0"
|
||||
resolved "https://registry.yarnpkg.com/meow/-/meow-3.7.0.tgz#72cb668b425228290abbfa856892587308a801fb"
|
||||
integrity sha1-cstmi0JSKCkKu/qFaJJYcwioAfs=
|
||||
@ -7859,7 +7857,7 @@ minimatch@^3.0.4, minimatch@~3.0.2:
|
||||
dependencies:
|
||||
brace-expansion "^1.1.7"
|
||||
|
||||
minimist@^1.1.0, minimist@^1.1.1, minimist@^1.1.3, minimist@^1.2.0, minimist@^1.2.5:
|
||||
minimist@^1.1.1, minimist@^1.1.3, minimist@^1.2.0, minimist@^1.2.5:
|
||||
version "1.2.5"
|
||||
resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602"
|
||||
integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==
|
||||
@ -7982,6 +7980,11 @@ mobx@^5.15.4:
|
||||
resolved "https://registry.yarnpkg.com/mobx/-/mobx-5.15.4.tgz#9da1a84e97ba624622f4e55a0bf3300fb931c2ab"
|
||||
integrity sha512-xRFJxSU2Im3nrGCdjSuOTFmxVDGeqOHL+TyADCGbT0k4HHqGmx5u2yaHNryvoORpI4DfbzjJ5jPmuv+d7sioFw==
|
||||
|
||||
mobx@^5.15.5:
|
||||
version "5.15.5"
|
||||
resolved "https://registry.yarnpkg.com/mobx/-/mobx-5.15.5.tgz#69715dc8662f64d153309bfe95169b8df4b4be4b"
|
||||
integrity sha512-hzk17T+/IIYLPWClRcfoA6Q5aZhFpDCr1oh8RZzu+esWP77IX/lS0V/Ee1Np+aOPKFfbSInF0reHH0L/aFfSrw==
|
||||
|
||||
mock-fs@^4.12.0:
|
||||
version "4.12.0"
|
||||
resolved "https://registry.yarnpkg.com/mock-fs/-/mock-fs-4.12.0.tgz#a5d50b12d2d75e5bec9dac3b67ffe3c41d31ade4"
|
||||
@ -8361,19 +8364,6 @@ nth-check@~1.0.1:
|
||||
dependencies:
|
||||
boolbase "~1.0.0"
|
||||
|
||||
nugget@^2.0.1:
|
||||
version "2.0.1"
|
||||
resolved "https://registry.yarnpkg.com/nugget/-/nugget-2.0.1.tgz#201095a487e1ad36081b3432fa3cada4f8d071b0"
|
||||
integrity sha1-IBCVpIfhrTYIGzQy+jytpPjQcbA=
|
||||
dependencies:
|
||||
debug "^2.1.3"
|
||||
minimist "^1.1.0"
|
||||
pretty-bytes "^1.0.2"
|
||||
progress-stream "^1.1.0"
|
||||
request "^2.45.0"
|
||||
single-line-log "^1.1.2"
|
||||
throttleit "0.0.2"
|
||||
|
||||
number-is-nan@^1.0.0:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/number-is-nan/-/number-is-nan-1.0.1.tgz#097b602b53422a522c1afb8790318336941a011d"
|
||||
@ -8423,11 +8413,6 @@ object-keys@^1.0.11, object-keys@^1.0.12, object-keys@^1.1.1:
|
||||
resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.1.1.tgz#1c47f272df277f3b1daf061677d9c82e2322c60e"
|
||||
integrity sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==
|
||||
|
||||
object-keys@~0.4.0:
|
||||
version "0.4.0"
|
||||
resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-0.4.0.tgz#28a6aae7428dd2c3a92f3d95f21335dd204e0336"
|
||||
integrity sha1-KKaq50KN0sOpLz2V8hM13SBOAzY=
|
||||
|
||||
object-visit@^1.0.0:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/object-visit/-/object-visit-1.0.1.tgz#f79c4493af0c5377b59fe39d395e41042dd045bb"
|
||||
@ -9124,14 +9109,6 @@ prepend-http@^2.0.0:
|
||||
resolved "https://registry.yarnpkg.com/prepend-http/-/prepend-http-2.0.0.tgz#e92434bfa5ea8c19f41cdfd401d741a3c819d897"
|
||||
integrity sha1-6SQ0v6XqjBn0HN/UAddBo8gZ2Jc=
|
||||
|
||||
pretty-bytes@^1.0.2:
|
||||
version "1.0.4"
|
||||
resolved "https://registry.yarnpkg.com/pretty-bytes/-/pretty-bytes-1.0.4.tgz#0a22e8210609ad35542f8c8d5d2159aff0751c84"
|
||||
integrity sha1-CiLoIQYJrTVUL4yNXSFZr/B1HIQ=
|
||||
dependencies:
|
||||
get-stdin "^4.0.1"
|
||||
meow "^3.1.0"
|
||||
|
||||
pretty-error@^2.1.1:
|
||||
version "2.1.1"
|
||||
resolved "https://registry.yarnpkg.com/pretty-error/-/pretty-error-2.1.1.tgz#5f4f87c8f91e5ae3f3ba87ab4cf5e03b1a17f1a3"
|
||||
@ -9175,14 +9152,6 @@ process@^0.11.10:
|
||||
resolved "https://registry.yarnpkg.com/process/-/process-0.11.10.tgz#7332300e840161bda3e69a1d1d91a7d4bc16f182"
|
||||
integrity sha1-czIwDoQBYb2j5podHZGn1LwW8YI=
|
||||
|
||||
progress-stream@^1.1.0:
|
||||
version "1.2.0"
|
||||
resolved "https://registry.yarnpkg.com/progress-stream/-/progress-stream-1.2.0.tgz#2cd3cfea33ba3a89c9c121ec3347abe9ab125f77"
|
||||
integrity sha1-LNPP6jO6OonJwSHsM0er6asSX3c=
|
||||
dependencies:
|
||||
speedometer "~0.1.2"
|
||||
through2 "~0.2.3"
|
||||
|
||||
progress@^2.0.0, progress@^2.0.3:
|
||||
version "2.0.3"
|
||||
resolved "https://registry.yarnpkg.com/progress/-/progress-2.0.3.tgz#7e8cf8d8f5b8f239c1bc68beb4eb78567d572ef8"
|
||||
@ -9375,7 +9344,7 @@ raw-loader@^4.0.1:
|
||||
loader-utils "^2.0.0"
|
||||
schema-utils "^2.6.5"
|
||||
|
||||
rc@^1.2.1, rc@^1.2.8:
|
||||
rc@^1.2.8:
|
||||
version "1.2.8"
|
||||
resolved "https://registry.yarnpkg.com/rc/-/rc-1.2.8.tgz#cd924bf5200a075b83c188cd6b9e211b7fc0d3ed"
|
||||
integrity sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==
|
||||
@ -9420,7 +9389,7 @@ react-router-dom@^5.2.0:
|
||||
tiny-invariant "^1.0.2"
|
||||
tiny-warning "^1.0.0"
|
||||
|
||||
react-router@5.2.0:
|
||||
react-router@5.2.0, react-router@^5.2.0:
|
||||
version "5.2.0"
|
||||
resolved "https://registry.yarnpkg.com/react-router/-/react-router-5.2.0.tgz#424e75641ca8747fbf76e5ecca69781aa37ea293"
|
||||
integrity sha512-smz1DUuFHRKdcJC0jobGo8cVbhO3x50tCL4icacOlcwDOEQPq4TMqwx3sY1TP+DvtTgz4nm3thuo7A+BK2U0Dw==
|
||||
@ -9560,16 +9529,6 @@ readable-stream@^3.1.1, readable-stream@^3.6.0:
|
||||
string_decoder "^1.1.1"
|
||||
util-deprecate "^1.0.1"
|
||||
|
||||
readable-stream@~1.1.9:
|
||||
version "1.1.14"
|
||||
resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-1.1.14.tgz#7cf4c54ef648e3813084c636dd2079e166c081d9"
|
||||
integrity sha1-fPTFTvZI44EwhMY23SB54WbAgdk=
|
||||
dependencies:
|
||||
core-util-is "~1.0.0"
|
||||
inherits "~2.0.1"
|
||||
isarray "0.0.1"
|
||||
string_decoder "~0.10.x"
|
||||
|
||||
readdirp@^2.2.1:
|
||||
version "2.2.1"
|
||||
resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-2.2.1.tgz#0e87622a3325aa33e892285caf8b4e846529a525"
|
||||
@ -9736,7 +9695,7 @@ request-promise-native@^1.0.8:
|
||||
stealthy-require "^1.1.1"
|
||||
tough-cookie "^2.3.3"
|
||||
|
||||
request@^2.45.0, request@^2.83.0, request@^2.87.0, request@^2.88.0, request@^2.88.2:
|
||||
request@^2.83.0, request@^2.87.0, request@^2.88.0, request@^2.88.2:
|
||||
version "2.88.2"
|
||||
resolved "https://registry.yarnpkg.com/request/-/request-2.88.2.tgz#d73c918731cb5a87da047e207234146f664d12b3"
|
||||
integrity sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw==
|
||||
@ -10219,13 +10178,6 @@ simple-swizzle@^0.2.2:
|
||||
dependencies:
|
||||
is-arrayish "^0.3.1"
|
||||
|
||||
single-line-log@^1.1.2:
|
||||
version "1.1.2"
|
||||
resolved "https://registry.yarnpkg.com/single-line-log/-/single-line-log-1.1.2.tgz#c2f83f273a3e1a16edb0995661da0ed5ef033364"
|
||||
integrity sha1-wvg/Jzo+GhbtsJlWYdoO1e8DM2Q=
|
||||
dependencies:
|
||||
string-width "^1.0.1"
|
||||
|
||||
sisteransi@^1.0.4:
|
||||
version "1.0.5"
|
||||
resolved "https://registry.yarnpkg.com/sisteransi/-/sisteransi-1.0.5.tgz#134d681297756437cc05ca01370d3a7a571075ed"
|
||||
@ -10385,22 +10337,18 @@ spdx-license-ids@^3.0.0:
|
||||
resolved "https://registry.yarnpkg.com/spdx-license-ids/-/spdx-license-ids-3.0.5.tgz#3694b5804567a458d3c8045842a6358632f62654"
|
||||
integrity sha512-J+FWzZoynJEXGphVIS+XEh3kFSjZX/1i9gFBaWQcB+/tmpe2qUsSBABpcxqxnAxFdiUFEgAX1bjYGQvIZmoz9Q==
|
||||
|
||||
spectron@^8.0.0:
|
||||
version "8.0.0"
|
||||
resolved "https://registry.yarnpkg.com/spectron/-/spectron-8.0.0.tgz#86e83c5dccb174850c052e2e718d5b1158764a52"
|
||||
integrity sha512-MI9+lAamDnw7S0vKaxXjU3g5qaW5KANaFLc+Hgq+QmMCkQbZLt6ukFFGfalmwIuYrmq+yWQPCD4CXgt3VSHrLA==
|
||||
spectron@11.0.0:
|
||||
version "11.0.0"
|
||||
resolved "https://registry.yarnpkg.com/spectron/-/spectron-11.0.0.tgz#79d785e6b8898638e77b5186711e3910ed4ca09b"
|
||||
integrity sha512-YRiB0TTpJa8ofNML/k1fJShe+m7U/E2HnFZsdZK57ekWIzlTHF+Lq7ZvuKGxMbpooU/OZkLObZfitemxhBVH4w==
|
||||
dependencies:
|
||||
"@types/webdriverio" "^4.8.0"
|
||||
dev-null "^0.1.1"
|
||||
electron-chromedriver "^6.0.0"
|
||||
electron-chromedriver "^9.0.0"
|
||||
request "^2.87.0"
|
||||
split "^1.0.0"
|
||||
webdriverio "^4.13.0"
|
||||
|
||||
speedometer@~0.1.2:
|
||||
version "0.1.4"
|
||||
resolved "https://registry.yarnpkg.com/speedometer/-/speedometer-0.1.4.tgz#9876dbd2a169d3115402d48e6ea6329c8816a50d"
|
||||
integrity sha1-mHbb0qFp0xFUAtSObqYynIgWpQ0=
|
||||
|
||||
split-string@^3.0.1, split-string@^3.0.2:
|
||||
version "3.1.0"
|
||||
resolved "https://registry.yarnpkg.com/split-string/-/split-string-3.1.0.tgz#7cb09dda3a86585705c64b39a6466038682e8fe2"
|
||||
@ -10619,11 +10567,6 @@ string_decoder@^1.0.0, string_decoder@^1.1.1:
|
||||
dependencies:
|
||||
safe-buffer "~5.2.0"
|
||||
|
||||
string_decoder@~0.10.x:
|
||||
version "0.10.31"
|
||||
resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-0.10.31.tgz#62e203bc41766c6c28c9fc84301dab1c5310fa94"
|
||||
integrity sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=
|
||||
|
||||
string_decoder@~1.1.1:
|
||||
version "1.1.1"
|
||||
resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.1.1.tgz#9cf1611ba62685d7030ae9e4ba34149c3af03fc8"
|
||||
@ -10713,13 +10656,6 @@ style-loader@^1.2.1:
|
||||
loader-utils "^2.0.0"
|
||||
schema-utils "^2.6.6"
|
||||
|
||||
sumchecker@^2.0.2:
|
||||
version "2.0.2"
|
||||
resolved "https://registry.yarnpkg.com/sumchecker/-/sumchecker-2.0.2.tgz#0f42c10e5d05da5d42eea3e56c3399a37d6c5b3e"
|
||||
integrity sha1-D0LBDl0F2l1C7qPlbDOZo31sWz4=
|
||||
dependencies:
|
||||
debug "^2.2.0"
|
||||
|
||||
sumchecker@^3.0.1:
|
||||
version "3.0.1"
|
||||
resolved "https://registry.yarnpkg.com/sumchecker/-/sumchecker-3.0.1.tgz#6377e996795abb0b6d348e9b3e1dfb24345a8e42"
|
||||
@ -10942,11 +10878,6 @@ throat@^5.0.0:
|
||||
resolved "https://registry.yarnpkg.com/throat/-/throat-5.0.0.tgz#c5199235803aad18754a667d659b5e72ce16764b"
|
||||
integrity sha512-fcwX4mndzpLQKBS1DVYhGAcYaYt7vsHNIvQV+WXMvnow5cgjPphq5CaayLaGsjRdSCKZFNGt7/GYAuXaNOiYCA==
|
||||
|
||||
throttleit@0.0.2:
|
||||
version "0.0.2"
|
||||
resolved "https://registry.yarnpkg.com/throttleit/-/throttleit-0.0.2.tgz#cfedf88e60c00dd9697b61fdd2a8343a9b680eaf"
|
||||
integrity sha1-z+34jmDADdlpe2H90qg0OptoDq8=
|
||||
|
||||
through2@^2.0.0:
|
||||
version "2.0.5"
|
||||
resolved "https://registry.yarnpkg.com/through2/-/through2-2.0.5.tgz#01c1e39eb31d07cb7d03a96a70823260b23132cd"
|
||||
@ -10955,14 +10886,6 @@ through2@^2.0.0:
|
||||
readable-stream "~2.3.6"
|
||||
xtend "~4.0.1"
|
||||
|
||||
through2@~0.2.3:
|
||||
version "0.2.3"
|
||||
resolved "https://registry.yarnpkg.com/through2/-/through2-0.2.3.tgz#eb3284da4ea311b6cc8ace3653748a52abf25a3f"
|
||||
integrity sha1-6zKE2k6jEbbMis42U3SKUqvyWj8=
|
||||
dependencies:
|
||||
readable-stream "~1.1.9"
|
||||
xtend "~2.1.1"
|
||||
|
||||
through@2, through@^2.3.6:
|
||||
version "2.3.8"
|
||||
resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5"
|
||||
@ -11879,13 +11802,6 @@ xtend@^4.0.0, xtend@~4.0.1:
|
||||
resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54"
|
||||
integrity sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==
|
||||
|
||||
xtend@~2.1.1:
|
||||
version "2.1.2"
|
||||
resolved "https://registry.yarnpkg.com/xtend/-/xtend-2.1.2.tgz#6efecc2a4dad8e6962c4901b337ce7ba87b5d28b"
|
||||
integrity sha1-bv7MKk2tjmlixJAbM3znuoe10os=
|
||||
dependencies:
|
||||
object-keys "~0.4.0"
|
||||
|
||||
xterm-addon-fit@^0.4.0:
|
||||
version "0.4.0"
|
||||
resolved "https://registry.yarnpkg.com/xterm-addon-fit/-/xterm-addon-fit-0.4.0.tgz#06e0c5d0a6aaacfb009ef565efa1c81e93d90193"
|
||||
|
||||
Loading…
Reference in New Issue
Block a user