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