From 3547a29b60cab701978563fb58a2aec12f78a49f Mon Sep 17 00:00:00 2001 From: Sebastian Malton Date: Fri, 17 Jul 2020 10:11:48 -0400 Subject: [PATCH 01/35] configure big folders to ignore for vscode (#593) Signed-off-by: Sebastian Malton Co-authored-by: Sebastian Malton Signed-off-by: Sebastian Malton --- tsconfig.json | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/tsconfig.json b/tsconfig.json index 2a110d2c5a..0555cd40ee 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -32,5 +32,13 @@ "compilerOptions": { "module": "CommonJS" } - } + }, + "exclude": [ + "node_modules", + "out", + "dist", + "coverage", + "binaries", + "static" + ] } From a09caca157dae18f6372a6ef0317d0ccaccf74d2 Mon Sep 17 00:00:00 2001 From: Roman Date: Fri, 17 Jul 2020 17:48:16 +0300 Subject: [PATCH 02/35] Better log line when quitting Lens, close #480 Signed-off-by: Roman --- src/main/kube-auth-proxy.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/main/kube-auth-proxy.ts b/src/main/kube-auth-proxy.ts index b4b4a4bdd8..26349300e5 100644 --- a/src/main/kube-auth-proxy.ts +++ b/src/main/kube-auth-proxy.ts @@ -42,7 +42,11 @@ export class KubeAuthProxy { env: this.env }) this.proxyProcess.on("exit", (code) => { - logger.error(`proxy ${this.cluster.contextName} exited with code ${code}`) + if (code) { + logger.error(`proxy ${this.cluster.contextName} exited with code ${code}`) + } else { + logger.info(`proxy ${this.cluster.contextName} exited successfully`) + } this.sendIpcLogMessage(`proxy exited with code ${code}`, "stderr").catch((err: Error) => { logger.debug("failed to send IPC log message: " + err.message) }) @@ -73,7 +77,7 @@ export class KubeAuthProxy { try { const parsedError = JSON.parse(jsonError) errorMsg = parsedError.error_description || parsedError.error || jsonError - } catch(_) { + } catch (_) { errorMsg = jsonError.trim() } } From b8e054c0db6509af8032d10f23c5362a2b3250df Mon Sep 17 00:00:00 2001 From: Roman Date: Fri, 17 Jul 2020 20:00:47 +0300 Subject: [PATCH 03/35] improved singleton types inferring Signed-off-by: Roman --- src/common/cluster-store.ts | 2 +- src/common/tracker.ts | 2 +- src/common/user-store.ts | 2 +- src/common/utils/singleton.ts | 6 ++++-- src/common/workspace-store.ts | 2 +- src/main/index.ts | 2 +- src/main/lens-proxy.ts | 10 ++++------ src/main/window-manager.ts | 1 + yarn.lock | 6 +++--- 9 files changed, 17 insertions(+), 16 deletions(-) diff --git a/src/common/cluster-store.ts b/src/common/cluster-store.ts index 083031eba1..54c16f94d9 100644 --- a/src/common/cluster-store.ts +++ b/src/common/cluster-store.ts @@ -192,4 +192,4 @@ export class ClusterStore extends BaseStore { } } -export const clusterStore: ClusterStore = ClusterStore.getInstance(); +export const clusterStore = ClusterStore.getInstance(); diff --git a/src/common/tracker.ts b/src/common/tracker.ts index 97550d2ad6..c904d2c806 100644 --- a/src/common/tracker.ts +++ b/src/common/tracker.ts @@ -45,4 +45,4 @@ export class Tracker extends Singleton { } } -export const tracker: Tracker = Tracker.getInstance(app || remote.app); +export const tracker = Tracker.getInstance(app || remote.app); diff --git a/src/common/user-store.ts b/src/common/user-store.ts index 7e09d8e0bd..705cdae23f 100644 --- a/src/common/user-store.ts +++ b/src/common/user-store.ts @@ -75,4 +75,4 @@ export class UserStore extends BaseStore { } } -export const userStore: UserStore = UserStore.getInstance(); +export const userStore = UserStore.getInstance(); diff --git a/src/common/utils/singleton.ts b/src/common/utils/singleton.ts index b940f2bfbb..70347f9b42 100644 --- a/src/common/utils/singleton.ts +++ b/src/common/utils/singleton.ts @@ -6,11 +6,13 @@ * const usersStore: UsersStore = UsersStore.getInstance(); */ +type Constructor = new (...args: any[]) => T; + class Singleton { private static instances = new WeakMap(); - // todo: figure out how to infer child class + arguments types - static getInstance(...args: any[]): T { + // todo: improve types inferring + static getInstance(...args: ConstructorParameters>): T { if (!Singleton.instances.has(this)) { Singleton.instances.set(this, Reflect.construct(this, args)); } diff --git a/src/common/workspace-store.ts b/src/common/workspace-store.ts index 393f14c0df..6afee4878e 100644 --- a/src/common/workspace-store.ts +++ b/src/common/workspace-store.ts @@ -93,4 +93,4 @@ export class WorkspaceStore extends BaseStore { } } -export const workspaceStore: WorkspaceStore = WorkspaceStore.getInstance() +export const workspaceStore = WorkspaceStore.getInstance() diff --git a/src/main/index.ts b/src/main/index.ts index 74a0b4a963..fc5dcade85 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -66,7 +66,7 @@ async function main() { // run proxy try { - proxyServer = LensProxy.create(clusterManager); + proxyServer = LensProxy.create(proxyPort, clusterManager); } catch (error) { logger.error(`Could not start proxy (127.0.0:${proxyPort}): ${error.message}`) await dialog.showErrorBox("Lens Error", `Could not start proxy (127.0.0:${proxyPort}): ${error.message || "unknown error"}`) diff --git a/src/main/lens-proxy.ts b/src/main/lens-proxy.ts index b5c485f4fc..8179b9d8d9 100644 --- a/src/main/lens-proxy.ts +++ b/src/main/lens-proxy.ts @@ -11,22 +11,20 @@ import { apiKubePrefix } from "../common/vars"; import logger from "./logger" export class LensProxy { - protected clusterManager: ClusterManager protected proxyServer: http.Server protected router: Router protected closed = false protected retryCounters = new Map() - static create(clusterManager: ClusterManager) { - return new LensProxy(clusterManager).listen(); + static create(port: number, clusterManager: ClusterManager) { + return new LensProxy(port, clusterManager).listen(); } - private constructor(clusterManager: ClusterManager) { - this.clusterManager = clusterManager; + private constructor(protected port: number, protected clusterManager: ClusterManager) { this.router = new Router(); } - listen(port = this.clusterManager.port): this { + listen(port = this.port): this { this.proxyServer = this.buildCustomProxy().listen(port); logger.info(`LensProxy server has started http://localhost:${port}`); return this; diff --git a/src/main/window-manager.ts b/src/main/window-manager.ts index fde815dd7f..828fd66e8f 100644 --- a/src/main/window-manager.ts +++ b/src/main/window-manager.ts @@ -5,6 +5,7 @@ import type { ClusterId } from "../common/cluster-store"; import { clusterStore } from "../common/cluster-store"; import logger from "./logger"; +// fixme: error when removing active cluster (can't activate next view => empty window) // fixme: remove switching view delay on first load export class WindowManager { diff --git a/yarn.lock b/yarn.lock index 5eee77baf2..fc614075ec 100644 --- a/yarn.lock +++ b/yarn.lock @@ -11255,9 +11255,9 @@ typeface-roboto@^0.0.75: integrity sha512-VrR/IiH00Z1tFP4vDGfwZ1esNqTiDMchBEXYY9kilT6wRGgFoCAlgkEUMHb1E3mB0FsfZhv756IF0+R+SFPfdg== typescript@^3.9.5: - version "3.9.5" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.9.5.tgz#586f0dba300cde8be52dd1ac4f7e1009c1b13f36" - integrity sha512-hSAifV3k+i6lEoCJ2k6R2Z/rp/H3+8sdmcn5NrS3/3kE7+RyZXm9aqvxWqjEXHAd8b0pShatpcdMTvEdvAJltQ== + version "3.9.7" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.9.7.tgz#98d600a5ebdc38f40cb277522f12dc800e9e25fa" + integrity sha512-BLbiRkiBzAwsjut4x/dsibSTB6yWpwT5qWmC2OfuCg3GgVQCSgMs4vEctYPhsaGtd0AeuuHMkjZ2h2WG8MSzRw== uglify-js@^3.1.4: version "3.9.4" From e0d88be3e8d7a90a70908aa986eb6201afa29fa0 Mon Sep 17 00:00:00 2001 From: Roman Date: Fri, 17 Jul 2020 23:19:41 +0300 Subject: [PATCH 04/35] cluster-status -- part 1 (ex. ClusterPage.vue) Signed-off-by: Roman --- src/common/base-store.ts | 2 +- src/main/cluster-manager.ts | 8 +++ src/main/cluster.ts | 3 +- src/main/kube-auth-proxy.ts | 30 ++++++----- .../cluster-manager/cluster-status.scss | 3 ++ .../cluster-manager/cluster-status.tsx | 53 +++++++++++++++++++ .../components/dialog/logs-dialog.tsx | 2 + 7 files changed, 85 insertions(+), 16 deletions(-) create mode 100644 src/renderer/components/cluster-manager/cluster-status.scss create mode 100644 src/renderer/components/cluster-manager/cluster-status.tsx diff --git a/src/common/base-store.ts b/src/common/base-store.ts index 77969fbac8..8747388ffa 100644 --- a/src/common/base-store.ts +++ b/src/common/base-store.ts @@ -118,7 +118,7 @@ export class BaseStore extends Singleton { protected async onModelChange(model: T) { if (ipcMain) { this.save(model); // save config file - sendMessage({ channel: this.syncChannel, args: model }); // broadcast to renderer views + sendMessage({ channel: this.syncChannel, args: [model] }); // broadcast to renderer views } // send "update-request" to main-process if (ipcRenderer) { diff --git a/src/main/cluster-manager.ts b/src/main/cluster-manager.ts index c6254c6ae7..53b8dbc35a 100644 --- a/src/main/cluster-manager.ts +++ b/src/main/cluster-manager.ts @@ -48,6 +48,14 @@ export class ClusterManager { this.getCluster(clusterId)?.destroy(); } + // todo + protected reconnectCluster(clusterId: ClusterId) { + tracker.event("cluster", "reconnect"); + logger.info(`[CLUSTER-MANAGER]: reconnect cluster`, { + meta: this.getCluster(clusterId)?.getMeta() + }); + } + getClusterForRequest(req: http.IncomingMessage): Cluster { let cluster: Cluster = null diff --git a/src/main/cluster.ts b/src/main/cluster.ts index e03a307cf3..c5fa80fb36 100644 --- a/src/main/cluster.ts +++ b/src/main/cluster.ts @@ -15,6 +15,7 @@ import logger from "./logger" export enum ClusterIpcEvent { STOP = "cluster:stop", + RECONNECT = "cluster:reconnect", } export enum ClusterStatus { @@ -111,7 +112,7 @@ export class Cluster implements ClusterModel { sendMessage({ channel: "cluster:state", webContentId: viewId, - args: clusterState, + args: [clusterState], }) }, { fireImmediately: true diff --git a/src/main/kube-auth-proxy.ts b/src/main/kube-auth-proxy.ts index 26349300e5..d793128c34 100644 --- a/src/main/kube-auth-proxy.ts +++ b/src/main/kube-auth-proxy.ts @@ -5,6 +5,11 @@ import type { Cluster } from "./cluster" import { bundledKubectl, Kubectl } from "./kubectl" import logger from "./logger" +export interface KubeAuthProxyResponse { + data: string; + stream: "stderr" | "stdout"; +} + export class KubeAuthProxy { public lastError: string @@ -43,13 +48,9 @@ export class KubeAuthProxy { }) this.proxyProcess.on("exit", (code) => { if (code) { - logger.error(`proxy ${this.cluster.contextName} exited with code ${code}`) - } else { - logger.info(`proxy ${this.cluster.contextName} exited successfully`) + logger.error(`[KUBE-AUTH]: proxying ${this.cluster.contextName} exited with code ${code}`, this.cluster.getMeta()); } - this.sendIpcLogMessage(`proxy exited with code ${code}`, "stderr").catch((err: Error) => { - logger.debug("failed to send IPC log message: " + err.message) - }) + this.sendIpcLogMessage({ data: `proxy exited with code ${code}`, stream: "stderr" }) this.proxyProcess = null }) this.proxyProcess.stdout.on('data', (data) => { @@ -57,13 +58,11 @@ export class KubeAuthProxy { if (logItem.startsWith("Starting to serve on")) { logItem = "Authentication proxy started\n" } - logger.debug(`proxy ${this.cluster.contextName} stdout: ${logItem}`) - this.sendIpcLogMessage(logItem, "stdout") + this.sendIpcLogMessage({ data: logItem, stream: "stdout" }) }) this.proxyProcess.stderr.on('data', (data) => { this.lastError = this.parseError(data.toString()) - logger.debug(`proxy ${this.cluster.contextName} stderr: ${data}`) - this.sendIpcLogMessage(data.toString(), "stderr") + this.sendIpcLogMessage({ data: data.toString(), stream: "stderr" }) }) return waitUntilUsed(this.port, 500, 10000) @@ -84,11 +83,14 @@ export class KubeAuthProxy { return errorMsg } - protected async sendIpcLogMessage(data: string, stream: string) { + protected async sendIpcLogMessage(res: KubeAuthProxyResponse) { const channel = `kube-auth:${this.cluster.id}` - const message = { data, stream }; - logger.debug(channel, message); - sendMessage({ channel, args: message }); // todo: send message only to cluster's window + logger.debug(`[KUBE-AUTH]: output for ${channel}`, { ...res, meta: this.cluster.getMeta() }); + sendMessage({ + // webContentId: null, // todo: send a message only to single cluster's window + channel: channel, + args: [res], + }); } public exit() { diff --git a/src/renderer/components/cluster-manager/cluster-status.scss b/src/renderer/components/cluster-manager/cluster-status.scss new file mode 100644 index 0000000000..ad1fc1d11e --- /dev/null +++ b/src/renderer/components/cluster-manager/cluster-status.scss @@ -0,0 +1,3 @@ +.ClusterStatus { + +} \ No newline at end of file diff --git a/src/renderer/components/cluster-manager/cluster-status.tsx b/src/renderer/components/cluster-manager/cluster-status.tsx new file mode 100644 index 0000000000..6863d855ba --- /dev/null +++ b/src/renderer/components/cluster-manager/cluster-status.tsx @@ -0,0 +1,53 @@ +import "./cluster-manager.scss" +import type { KubeAuthProxyResponse } from "../../../main/kube-auth-proxy"; +import { Cluster, ClusterIpcEvent } from "../../../main/cluster"; +import React from "react"; +import { ipcRenderer } from "electron"; +import { computed, observable } from "mobx"; +import { observer } from "mobx-react"; +import { Icon } from "../icon"; +import { Button } from "../button"; +import { Trans } from "@lingui/macro"; + +interface Props { + cluster: Cluster; +} + +@observer +export class ClusterStatus extends React.Component { + @observable authProxyOutput = "Connecting ...\n" + + @computed get clusterId() { + return this.props.cluster.id; + } + + componentDidMount() { + ipcRenderer.on(`kube-auth:${this.clusterId}`, (evt, authResponse: KubeAuthProxyResponse) => { + this.authProxyOutput += authResponse.data; + }) + } + + componentWillUnmount() { + ipcRenderer.removeAllListeners(`kube-auth:${this.clusterId}`); + } + + reconnectCluster = () => { + ipcRenderer.send(ClusterIpcEvent.RECONNECT, this.clusterId); + } + + render() { + const { authProxyOutput } = this; + const { contextName, online } = this.props.cluster; + return ( +
+ +

{contextName}

+
{authProxyOutput}
+
+ ) + } +} diff --git a/src/renderer/components/dialog/logs-dialog.tsx b/src/renderer/components/dialog/logs-dialog.tsx index 3e24e7d712..4ce1b3b27f 100644 --- a/src/renderer/components/dialog/logs-dialog.tsx +++ b/src/renderer/components/dialog/logs-dialog.tsx @@ -10,6 +10,8 @@ import { Button } from "../button"; import { Icon } from "../icon"; import { _i18n } from "../../i18n"; +// todo: make as external BrowserWindow (?) + interface Props extends DialogProps { title: string; logs: string; From 6cc4e79481d794567c58b13d1d9d3cb479d0d0ff Mon Sep 17 00:00:00 2001 From: Sebastian Malton Date: Wed, 15 Jul 2020 09:45:52 -0400 Subject: [PATCH 05/35] fix format duration rounding days error (#582) * fix format duration rounding days error * added tests Signed-off-by: Sebastian Malton Co-authored-by: Sebastian Malton --- src/main/kubectl.ts | 2 +- src/renderer/utils/formatDuration.ts | 41 +++++++++++------ src/renderer/utils/formatDuration_spec.ts | 56 +++++++++++++++++++++++ tsconfig.json | 1 - 4 files changed, 83 insertions(+), 17 deletions(-) create mode 100644 src/renderer/utils/formatDuration_spec.ts diff --git a/src/main/kubectl.ts b/src/main/kubectl.ts index 9372c439a9..db54f2bd12 100644 --- a/src/main/kubectl.ts +++ b/src/main/kubectl.ts @@ -127,7 +127,7 @@ export class Kubectl { } try { - const { stdout, stderr } = await promiseExec(`"${this.path}" version --client=true -o json`) + const { stdout } = await promiseExec(`"${this.path}" version --client=true -o json`) const output = JSON.parse(stdout) let version: string = output.clientVersion.gitVersion if (version[0] === 'v') { diff --git a/src/renderer/utils/formatDuration.ts b/src/renderer/utils/formatDuration.ts index 48107d3c0b..84f90731b5 100644 --- a/src/renderer/utils/formatDuration.ts +++ b/src/renderer/utils/formatDuration.ts @@ -1,24 +1,35 @@ -// Formatting date duration in shorten format, e.g. "2d", or "25m" - import moment from "moment"; +const suffixes = ["w", "d", "h", "m", "s"]; + +/** + * This function formats durations in a more human readable form. + * @param timeValue the duration in milliseconds to format + * @param compact when true, only the largest non-zero time frame will be returned + */ export function formatDuration(timeValue: number, compact: boolean) { - let result = ""; - const duration = moment.duration(timeValue); - const suffixes = ["d", "h", "m"]; + const duration = moment.duration(timeValue, "milliseconds"); const durationValues = [ - Math.round(duration.asDays()), + Math.floor(duration.asWeeks()), + Math.floor(duration.asDays()) % 7, duration.hours(), duration.minutes(), + duration.seconds(), ]; - durationValues.forEach((value, index) => { - if (value) result += value + suffixes[index] + " "; - }); + + const meaningfulValues = durationValues + .map((a, i): [number, string] => [a, suffixes[i]]) + .filter(([dur, _suf]) => dur > 0) + .filter(([_dur, suf], i) => i === 0 || suf !== "s") // remove seconds, unless it is the only one + .map(([dur, suf]) => dur + suf); + + if (meaningfulValues.length === 0) { + return "0s"; + } + if (compact) { - result = result.split(" ")[0]; + return meaningfulValues[0]; } - if (!result) { - return "<1m"; - } - return result; -} \ No newline at end of file + + return meaningfulValues.join(" "); +} diff --git a/src/renderer/utils/formatDuration_spec.ts b/src/renderer/utils/formatDuration_spec.ts new file mode 100644 index 0000000000..53e2148286 --- /dev/null +++ b/src/renderer/utils/formatDuration_spec.ts @@ -0,0 +1,56 @@ +import { formatDuration } from "./formatDuration"; + +const second = 1000; +const minute = 60 * second; +const hour = 60 * minute; +const day = 24 * hour; +const week = 7 * day; + +describe("human format durations", () => { + test("long formatted durations less than 24 hours long shouldn't have a 'd' component", () => { + const res = formatDuration(19 * 60 * 60 * 1000, false); + + expect(res).not.toContain("d"); + expect(res).toBe("19h"); + }); + + test("long formatted durations more than a week have correct day count", () => { + const res = formatDuration(2 * week + 2 * day, false); + + expect(res).toBe("2w 2d"); + }); + + test("durations > 1/2 week shouldn't show 1w has passed", () => { + const res = formatDuration(5 * 24 * 60 * 60 * 1000, false); + + expect(res).not.toContain("w"); + expect(res).toBe("5d"); + }); + + test("durations shouldn't include zero magnitude parts", () => { + const res = formatDuration(6 * day + 2 * minute, false); + + expect(res).not.toContain("h"); + expect(res).toBe("6d 2m"); + }); + + test("seconds are ignored unless they are significant (< 1m)", () => { + const insignificant = formatDuration(1 * hour + 2 * minute + 31 * second, false); + + expect(insignificant).not.toContain("s"); + expect(insignificant).toBe("1h 2m"); + + const significant = formatDuration(31 * second, false); + expect(significant).toBe("31s"); + }); + + test("zero duration should output something", () => { + expect(formatDuration(0, false)).toBe("0s"); + expect(formatDuration(0, true)).toBe("0s"); + }); + + test("small duration should output something", () => { + expect(formatDuration(1, false)).toBe("0s"); + expect(formatDuration(3, true)).toBe("0s"); + }); +}); diff --git a/tsconfig.json b/tsconfig.json index 0555cd40ee..95f8861c59 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -6,7 +6,6 @@ "target": "ES2017", "module": "ESNext", "lib": ["ESNext", "DOM", "DOM.Iterable"], - "importsNotUsedAsValues": "preserve", "moduleResolution": "Node", "sourceMap": true, "strict": false, From eb180a66f1d1b12790c14b83e6c418754e66a64d Mon Sep 17 00:00:00 2001 From: Roman Date: Mon, 20 Jul 2020 13:17:39 +0300 Subject: [PATCH 06/35] cluster-status -- part 2 Signed-off-by: Roman --- locales/en/messages.po | 37 ++-- locales/fi/messages.po | 37 ++-- locales/ru/messages.po | 37 ++-- src/common/base-store.ts | 4 +- src/common/ipc.ts | 22 +-- src/main/cluster-manager.ts | 85 +++++++--- src/main/cluster.ts | 64 +++---- src/main/context-handler.ts | 32 ++-- src/main/kube-auth-proxy.ts | 8 +- src/main/lens-proxy.ts | 21 +-- src/main/window-manager.ts | 21 +-- src/renderer/_vue/components/ClusterPage.vue | 159 ------------------ src/renderer/components/app.tsx | 45 +++-- .../cluster-manager/cluster-manager.tsx | 34 +++- .../cluster-manager/cluster-status.scss | 16 ++ .../cluster-manager/cluster-status.tsx | 67 +++++--- .../cluster-manager/clusters-menu.tsx | 2 +- src/renderer/index.tsx | 7 +- 18 files changed, 319 insertions(+), 379 deletions(-) delete mode 100644 src/renderer/_vue/components/ClusterPage.vue diff --git a/locales/en/messages.po b/locales/en/messages.po index e879875424..c967c5901a 100644 --- a/locales/en/messages.po +++ b/locales/en/messages.po @@ -29,6 +29,10 @@ msgstr "(as a percentage of request)" msgid "(empty) (Allowing the specific traffic to all pods in this namespace)" msgstr "(empty) (Allowing the specific traffic to all pods in this namespace)" +#: src/renderer/components/+add-cluster/add-cluster.tsx:104 +msgid "(new)" +msgstr "(new)" + #: src/renderer/components/item-object-list/item-list-layout.tsx:224 msgid "<0>Filtered: {itemsCount} / {allItemsCount}" msgstr "<0>Filtered: {itemsCount} / {allItemsCount}" @@ -41,7 +45,7 @@ msgstr "<0>Your browser does not support all Lens features. Please consider msgid "<0>{0} successfully created" msgstr "<0>{0} successfully created" -#: src/renderer/components/+add-cluster/add-cluster.tsx:52 +#: src/renderer/components/+add-cluster/add-cluster.tsx:126 msgid "A HTTP proxy server URL (format: http://
:)" msgstr "A HTTP proxy server URL (format: http://
:)" @@ -67,7 +71,8 @@ msgstr "Account Name" msgid "Active" msgstr "Active" -#: src/renderer/components/+add-cluster/add-cluster.tsx:44 +#: src/renderer/components/+add-cluster/add-cluster.tsx:118 +#: src/renderer/components/cluster-manager/clusters-menu.tsx:97 msgid "Add Cluster" msgstr "Add Cluster" @@ -83,7 +88,7 @@ msgstr "Add RoleBinding" msgid "Add bindings to {name}" msgstr "Add bindings to {name}" -#: src/renderer/components/+add-cluster/add-cluster.tsx:62 +#: src/renderer/components/+add-cluster/add-cluster.tsx:137 msgid "Add cluster" msgstr "Add cluster" @@ -223,7 +228,7 @@ msgstr "Are you sure you want to drain <0>{nodeName}?" msgid "Arguments" msgstr "Arguments" -#: src/renderer/components/cluster-manager/clusters-menu.tsx:74 +#: src/renderer/components/cluster-manager/clusters-menu.tsx:84 msgid "Associate clusters and choose the ones you want to access via quick launch menu by clicking the + button." msgstr "Associate clusters and choose the ones you want to access via quick launch menu by clicking the + button." @@ -634,7 +639,7 @@ msgstr "Currently applied filters:" msgid "Custom Resources" msgstr "Custom Resources" -#: src/renderer/components/+add-cluster/add-cluster.tsx:36 +#: src/renderer/components/+add-cluster/add-cluster.tsx:110 msgid "Custom.." msgstr "Custom.." @@ -698,7 +703,7 @@ msgstr "Description" msgid "Desired number of replicas" msgstr "Desired number of replicas" -#: src/renderer/components/cluster-manager/clusters-menu.tsx:49 +#: src/renderer/components/cluster-manager/clusters-menu.tsx:51 msgid "Disconnect" msgstr "Disconnect" @@ -882,7 +887,7 @@ msgstr "Groups" msgid "HPA" msgstr "HPA" -#: src/renderer/components/+add-cluster/add-cluster.tsx:54 +#: src/renderer/components/+add-cluster/add-cluster.tsx:128 msgid "HTTP Proxy server. Used for communicating with Kubernetes API." msgstr "HTTP Proxy server. Used for communicating with Kubernetes API." @@ -1570,6 +1575,10 @@ msgstr "Persistent Volume Claims" msgid "Persistent Volumes" msgstr "Persistent Volumes" +#: src/renderer/components/+add-cluster/add-cluster.tsx:51 +msgid "Please select kubeconfig" +msgstr "Please select kubeconfig" + #: src/renderer/components/+workloads-pods/pod-menu.tsx:50 msgid "Pod" msgstr "Pod" @@ -1651,7 +1660,7 @@ msgstr "Privileged" msgid "Provisioner" msgstr "Provisioner" -#: src/renderer/components/+add-cluster/add-cluster.tsx:48 +#: src/renderer/components/+add-cluster/add-cluster.tsx:122 msgid "Proxy settings" msgstr "Proxy settings" @@ -1701,6 +1710,10 @@ msgstr "Receive" msgid "Reclaim Policy" msgstr "Reclaim Policy" +#: src/renderer/components/cluster-manager/cluster-status.tsx:52 +msgid "Reconnect" +msgstr "Reconnect" + #: src/renderer/components/+config-autoscalers/hpa-details.tsx:70 #: src/renderer/components/+user-management-roles-bindings/role-binding-details.tsx:75 msgid "Reference" @@ -1728,6 +1741,8 @@ msgid "Releases" msgstr "Releases" #: src/renderer/components/+user-management-roles-bindings/role-binding-details.tsx:60 +#: src/renderer/components/cluster-manager/clusters-menu.tsx:58 +#: src/renderer/components/cluster-manager/clusters-menu.tsx:62 #: src/renderer/components/item-object-list/item-list-layout.tsx:179 #: src/renderer/components/menu/menu-actions.tsx:49 #: src/renderer/components/menu/menu-actions.tsx:85 @@ -2015,7 +2030,7 @@ msgstr "Secrets" msgid "Select a quota.." msgstr "Select a quota.." -#: src/renderer/components/+add-cluster/add-cluster.tsx:45 +#: src/renderer/components/+add-cluster/add-cluster.tsx:119 msgid "Select kubeconfig" msgstr "Select kubeconfig" @@ -2070,7 +2085,7 @@ msgstr "Set" msgid "Set quota" msgstr "Set quota" -#: src/renderer/components/cluster-manager/clusters-menu.tsx:43 +#: src/renderer/components/cluster-manager/clusters-menu.tsx:46 msgid "Settings" msgstr "Settings" @@ -2244,7 +2259,7 @@ msgstr "This field is required" msgid "This field must contain only lowercase latin characters, numbers and dash." msgstr "This field must contain only lowercase latin characters, numbers and dash." -#: src/renderer/components/cluster-manager/clusters-menu.tsx:72 +#: src/renderer/components/cluster-manager/clusters-menu.tsx:82 msgid "This is the quick launch menu." msgstr "This is the quick launch menu." diff --git a/locales/fi/messages.po b/locales/fi/messages.po index cd0bb6feec..cd6b540f90 100644 --- a/locales/fi/messages.po +++ b/locales/fi/messages.po @@ -29,6 +29,10 @@ msgstr "" msgid "(empty) (Allowing the specific traffic to all pods in this namespace)" msgstr "" +#: src/renderer/components/+add-cluster/add-cluster.tsx:104 +msgid "(new)" +msgstr "" + #: src/renderer/components/item-object-list/item-list-layout.tsx:224 msgid "<0>Filtered: {itemsCount} / {allItemsCount}" msgstr "" @@ -41,7 +45,7 @@ msgstr "" msgid "<0>{0} successfully created" msgstr "" -#: src/renderer/components/+add-cluster/add-cluster.tsx:52 +#: src/renderer/components/+add-cluster/add-cluster.tsx:126 msgid "A HTTP proxy server URL (format: http://
:)" msgstr "" @@ -67,7 +71,8 @@ msgstr "" msgid "Active" msgstr "" -#: src/renderer/components/+add-cluster/add-cluster.tsx:44 +#: src/renderer/components/+add-cluster/add-cluster.tsx:118 +#: src/renderer/components/cluster-manager/clusters-menu.tsx:97 msgid "Add Cluster" msgstr "" @@ -83,7 +88,7 @@ msgstr "" msgid "Add bindings to {name}" msgstr "" -#: src/renderer/components/+add-cluster/add-cluster.tsx:62 +#: src/renderer/components/+add-cluster/add-cluster.tsx:137 msgid "Add cluster" msgstr "" @@ -223,7 +228,7 @@ msgstr "" msgid "Arguments" msgstr "" -#: src/renderer/components/cluster-manager/clusters-menu.tsx:74 +#: src/renderer/components/cluster-manager/clusters-menu.tsx:84 msgid "Associate clusters and choose the ones you want to access via quick launch menu by clicking the + button." msgstr "" @@ -630,7 +635,7 @@ msgstr "" msgid "Custom Resources" msgstr "" -#: src/renderer/components/+add-cluster/add-cluster.tsx:36 +#: src/renderer/components/+add-cluster/add-cluster.tsx:110 msgid "Custom.." msgstr "" @@ -694,7 +699,7 @@ msgstr "" msgid "Desired number of replicas" msgstr "" -#: src/renderer/components/cluster-manager/clusters-menu.tsx:49 +#: src/renderer/components/cluster-manager/clusters-menu.tsx:51 msgid "Disconnect" msgstr "" @@ -873,7 +878,7 @@ msgstr "" msgid "HPA" msgstr "" -#: src/renderer/components/+add-cluster/add-cluster.tsx:54 +#: src/renderer/components/+add-cluster/add-cluster.tsx:128 msgid "HTTP Proxy server. Used for communicating with Kubernetes API." msgstr "" @@ -1553,6 +1558,10 @@ msgstr "" msgid "Persistent Volumes" msgstr "" +#: src/renderer/components/+add-cluster/add-cluster.tsx:51 +msgid "Please select kubeconfig" +msgstr "" + #: src/renderer/components/+workloads-pods/pod-menu.tsx:50 msgid "Pod" msgstr "" @@ -1634,7 +1643,7 @@ msgstr "" msgid "Provisioner" msgstr "" -#: src/renderer/components/+add-cluster/add-cluster.tsx:48 +#: src/renderer/components/+add-cluster/add-cluster.tsx:122 msgid "Proxy settings" msgstr "" @@ -1684,6 +1693,10 @@ msgstr "" msgid "Reclaim Policy" msgstr "" +#: src/renderer/components/cluster-manager/cluster-status.tsx:52 +msgid "Reconnect" +msgstr "" + #: src/renderer/components/+config-autoscalers/hpa-details.tsx:70 #: src/renderer/components/+user-management-roles-bindings/role-binding-details.tsx:75 msgid "Reference" @@ -1711,6 +1724,8 @@ msgid "Releases" msgstr "" #: src/renderer/components/+user-management-roles-bindings/role-binding-details.tsx:60 +#: src/renderer/components/cluster-manager/clusters-menu.tsx:58 +#: src/renderer/components/cluster-manager/clusters-menu.tsx:62 #: src/renderer/components/item-object-list/item-list-layout.tsx:179 #: src/renderer/components/menu/menu-actions.tsx:49 #: src/renderer/components/menu/menu-actions.tsx:85 @@ -1998,7 +2013,7 @@ msgstr "" msgid "Select a quota.." msgstr "" -#: src/renderer/components/+add-cluster/add-cluster.tsx:45 +#: src/renderer/components/+add-cluster/add-cluster.tsx:119 msgid "Select kubeconfig" msgstr "" @@ -2053,7 +2068,7 @@ msgstr "" msgid "Set quota" msgstr "" -#: src/renderer/components/cluster-manager/clusters-menu.tsx:43 +#: src/renderer/components/cluster-manager/clusters-menu.tsx:46 msgid "Settings" msgstr "" @@ -2227,7 +2242,7 @@ msgstr "" msgid "This field must contain only lowercase latin characters, numbers and dash." msgstr "" -#: src/renderer/components/cluster-manager/clusters-menu.tsx:72 +#: src/renderer/components/cluster-manager/clusters-menu.tsx:82 msgid "This is the quick launch menu." msgstr "" diff --git a/locales/ru/messages.po b/locales/ru/messages.po index 1787dbc5c9..fc4b01d90c 100644 --- a/locales/ru/messages.po +++ b/locales/ru/messages.po @@ -30,6 +30,10 @@ msgstr "" msgid "(empty) (Allowing the specific traffic to all pods in this namespace)" msgstr "(Пусто) (Допускается трафик ко всем подам в данной области имен)" +#: src/renderer/components/+add-cluster/add-cluster.tsx:104 +msgid "(new)" +msgstr "" + #: src/renderer/components/item-object-list/item-list-layout.tsx:224 msgid "<0>Filtered: {itemsCount} / {allItemsCount}" msgstr "<0>Отфильтровано: {itemsCount} / {allItemsCount}" @@ -42,7 +46,7 @@ msgstr "<0>Ваш браузер не поддерживает все возмо msgid "<0>{0} successfully created" msgstr "" -#: src/renderer/components/+add-cluster/add-cluster.tsx:52 +#: src/renderer/components/+add-cluster/add-cluster.tsx:126 msgid "A HTTP proxy server URL (format: http://
:)" msgstr "" @@ -68,7 +72,8 @@ msgstr "Название аккаунта" msgid "Active" msgstr "Активный" -#: src/renderer/components/+add-cluster/add-cluster.tsx:44 +#: src/renderer/components/+add-cluster/add-cluster.tsx:118 +#: src/renderer/components/cluster-manager/clusters-menu.tsx:97 msgid "Add Cluster" msgstr "" @@ -84,7 +89,7 @@ msgstr "Добавить привязку ролей" msgid "Add bindings to {name}" msgstr "Добавить привязки к {name}" -#: src/renderer/components/+add-cluster/add-cluster.tsx:62 +#: src/renderer/components/+add-cluster/add-cluster.tsx:137 msgid "Add cluster" msgstr "" @@ -224,7 +229,7 @@ msgstr "Выполнить команду drain для ноды <0>{nodeName} extends ConfOptions { @@ -118,7 +118,7 @@ export class BaseStore extends Singleton { protected async onModelChange(model: T) { if (ipcMain) { this.save(model); // save config file - sendMessage({ channel: this.syncChannel, args: [model] }); // broadcast to renderer views + broadcastIpc({ channel: this.syncChannel, args: [model] }); // broadcast to renderer views } // send "update-request" to main-process if (ipcRenderer) { diff --git a/src/common/ipc.ts b/src/common/ipc.ts index 704ce9a9ba..e5e8101994 100644 --- a/src/common/ipc.ts +++ b/src/common/ipc.ts @@ -19,11 +19,11 @@ export interface IpcMessageOpts { channel: IpcChannel webContentId?: number; // sends to single webContents view filter?: (webContent: WebContents) => boolean - timeout?: number; // fixme: support + timeout?: number; // fixme: add support args?: A; } -export function sendMessage({ channel, webContentId, filter, args = [] }: IpcMessageOpts) { +export function broadcastIpc({ channel, webContentId, filter, args = [] }: IpcMessageOpts) { const singleView = webContentId ? webContents.fromId(webContentId) : null; let views = singleView ? [singleView] : webContents.getAllWebContents(); if (filter) { @@ -37,16 +37,16 @@ export function sendMessage({ channel, webContentId, filter, args = [] }: IpcMes } // todo: support timeout + merge with sendMessage? -export async function invokeMessage(channel: IpcChannel, ...args: T): Promise { - logger.debug(`[IPC]: invoke channel "${channel}"`, { args }); +export async function invokeIpc(channel: IpcChannel, ...args: any[]): Promise { + logger.info(`[IPC]: invoke channel "${channel}"`, { args }); return ipcRenderer.invoke(channel, ...args); } // todo: make isomorphic api -export function handleMessage(channel: IpcChannel, handler: IpcMessageHandler, options: IpcHandleOpts = {}) { +export function handleIpc(channel: IpcChannel, handler: IpcMessageHandler, options: IpcHandleOpts = {}) { const { timeout = 0 } = options; ipcMain.handle(channel, async (event, ...args: T) => { - logger.debug(`[IPC]: handle "${channel}"`, { args }); + logger.info(`[IPC]: handle "${channel}"`, { args }); return new Promise(async (resolve, reject) => { let timerId; if (timeout) { @@ -57,17 +57,11 @@ export function handleMessage(channel: IpcChannel, handler: Ipc } try { const result = await handler(...args); // todo: maybe exec in separate thread/worker + resolve(result); clearTimeout(timerId); - return result; } catch (err) { - logger.debug(`[IPC]: handling "${channel}" error`, { err }); + reject(err); } }) }) } - -export function handleMessages(messages: Record, options?: IpcHandleOpts) { - Object.entries(messages).forEach(([channel, handler]) => { - handleMessage(channel, handler, options); - }) -} diff --git a/src/main/cluster-manager.ts b/src/main/cluster-manager.ts index 53b8dbc35a..f569012c3a 100644 --- a/src/main/cluster-manager.ts +++ b/src/main/cluster-manager.ts @@ -1,61 +1,92 @@ import type http from "http" -import { autorun } from "mobx"; +import { autorun, reaction } from "mobx"; import { apiKubePrefix } from "../common/vars"; import { ClusterId, clusterStore } from "../common/cluster-store" -import { handleMessage } from "../common/ipc"; -import { tracker } from "../common/tracker"; -import { Cluster, ClusterIpcEvent } from "./cluster" +import { handleIpc } from "../common/ipc"; +import { Cluster, ClusterIpcChannel } from "./cluster" import logger from "./logger"; +import { tracker } from "../common/tracker"; export class ClusterManager { + protected activeClusterId: ClusterId; + constructor(public readonly port: number) { + this.activeClusterId = clusterStore.activeClusterId; + // auto-init clusters autorun(() => { clusterStore.clusters.forEach(cluster => { - if (cluster.initialized) return; - cluster.init(port); - logger.info(`[CLUSTER-MANAGER]: initializing cluster`, cluster.getMeta()); + if (!cluster.initialized) { + logger.info(`[CLUSTER-MANAGER]: initializing cluster`, cluster.getMeta()); + cluster.init(port); + } }); }); + // auto-bind events for active cluster + reaction(() => clusterStore.activeCluster, activeCluster => { + const prevCluster = clusterStore.getById(this.activeClusterId); + if (prevCluster) { + prevCluster.unbindEvents(); + } + if (activeCluster) { + this.activeClusterId = activeCluster.id; + activeCluster.bindEvents(); + activeCluster.refreshStatus(); + } + }, { + fireImmediately: true + }); + // auto-stop removed clusters autorun(() => { const { removedClusters } = clusterStore; - const meta = Array.from(removedClusters.values()).map(cluster => cluster.getMeta()); - logger.info(`[CLUSTER-MANAGER]: removing clusters`, meta); - removedClusters.forEach(cluster => cluster.destroy()); - removedClusters.clear(); + if (removedClusters.size > 0) { + const meta = Array.from(removedClusters.values()).map(cluster => cluster.getMeta()); + logger.info(`[CLUSTER-MANAGER]: removing clusters`, meta); + removedClusters.forEach(cluster => cluster.disconnect()); + removedClusters.clear(); + } }, { delay: 250 }); - // listen for ipc-events that must be handled *only* in main-process (nodeIntegration=true) - handleMessage(ClusterIpcEvent.STOP, this.stopCluster.bind(this)); + // listen for ipc-events that must/can be handled *only* in main-process (nodeIntegration=true) + handleIpc(ClusterIpcChannel.INIT, this.onClusterInit); + handleIpc(ClusterIpcChannel.DISCONNECT, this.onClusterDisconnect); + handleIpc(ClusterIpcChannel.RECONNECT, this.onClusterReconnect); } stop() { clusterStore.clusters.forEach((cluster: Cluster) => { - cluster.stop(); + cluster.disconnect(); }) } + protected onClusterInit = async (id = clusterStore.activeClusterId) => { + const cluster = this.getCluster(id); + if (cluster) { + logger.info(`[CLUSTER-MANAGER]: init cluster`, cluster.getMeta()); + tracker.event("cluster", "activate"); + await cluster.refreshStatus(); + cluster.pushState(); + } + } + + protected onClusterDisconnect = (id: ClusterId) => { + tracker.event("cluster", "stop"); + this.getCluster(id)?.disconnect(); + } + + protected onClusterReconnect = (id: ClusterId) => { + tracker.event("cluster", "reconnect"); + this.getCluster(id)?.reconnect(); + } + protected getCluster(id: ClusterId) { return clusterStore.getById(id); } - protected stopCluster(clusterId: ClusterId) { - tracker.event("cluster", "stop"); - this.getCluster(clusterId)?.destroy(); - } - - // todo - protected reconnectCluster(clusterId: ClusterId) { - tracker.event("cluster", "reconnect"); - logger.info(`[CLUSTER-MANAGER]: reconnect cluster`, { - meta: this.getCluster(clusterId)?.getMeta() - }); - } - getClusterForRequest(req: http.IncomingMessage): Cluster { let cluster: Cluster = null diff --git a/src/main/cluster.ts b/src/main/cluster.ts index c5fa80fb36..88b8e9ac93 100644 --- a/src/main/cluster.ts +++ b/src/main/cluster.ts @@ -1,9 +1,9 @@ import type { ClusterId, ClusterModel, ClusterPreferences } from "../common/cluster-store" import type { FeatureStatusMap } from "./feature" import type { WorkspaceId } from "../common/workspace-store"; -import { action, observable, reaction, toJS, when } from "mobx"; +import { action, computed, observable, reaction, toJS, when } from "mobx"; import { apiKubePrefix } from "../common/vars"; -import { sendMessage } from "../common/ipc"; +import { broadcastIpc } from "../common/ipc"; import { ContextHandler } from "./context-handler" import { AuthorizationV1Api, CoreV1Api, KubeConfig, V1ResourceAttributes } from "@kubernetes/client-node" import { Kubectl } from "./kubectl"; @@ -13,8 +13,9 @@ import { getFeatures, installFeature, uninstallFeature, upgradeFeature } from ". import request, { RequestPromiseOptions } from "request-promise-native" import logger from "./logger" -export enum ClusterIpcEvent { - STOP = "cluster:stop", +export enum ClusterIpcChannel { + INIT = "cluster:init", + DISCONNECT = "cluster:disconnect", RECONNECT = "cluster:reconnect", } @@ -43,8 +44,6 @@ export class Cluster implements ClusterModel { public kubeCtl: Kubectl public contextHandler: ContextHandler; protected kubeconfigManager: KubeconfigManager; - - public whenReady = when(() => this.initialized); protected disposers: Function[] = []; @observable initialized = false; @@ -69,6 +68,10 @@ export class Cluster implements ClusterModel { this.updateModel(model); } + @computed get isReady() { + return this.initialized && this.accessible === true; + } + @action updateModel(model: ClusterModel) { Object.assign(this, model); @@ -98,8 +101,7 @@ export class Cluster implements ClusterModel { } } - bindEvents(viewId: number) { - if (!this.initialized) return; + bindEvents() { logger.info(`[CLUSTER]: bind events`, this.getMeta()); const refreshStatusTimer = setInterval(() => this.refreshStatus(), 30000); // every 30s const refreshEventsTimer = setInterval(() => this.refreshEvents(), 3000); // every 3s @@ -107,43 +109,35 @@ export class Cluster implements ClusterModel { this.disposers.push( () => clearInterval(refreshStatusTimer), () => clearInterval(refreshEventsTimer), - - reaction(() => this.getState(), clusterState => { - sendMessage({ - channel: "cluster:state", - webContentId: viewId, - args: [clusterState], - }) - }, { + reaction(() => this.getState(), this.pushState, { fireImmediately: true }) ); } unbindEvents() { - if (!this.initialized) return; logger.info(`[CLUSTER]: unbind events`, this.getMeta()); this.disposers.forEach(dispose => dispose()); this.disposers.length = 0; } - stop() { - this.contextHandler.stopServer(); + // fixme: possibly doesn't work as expected + async reconnect() { + logger.info(`[CLUSTER]: reconnect`, this.getMeta()); + await this.contextHandler.stopServer(); + await this.contextHandler.ensureServer(); } - destroy() { - try { - this.stop(); - this.unbindEvents(); - this.kubeconfigManager.unlink(); - } catch (err) { - logger.error(`[CLUSTER]: destroy() throws: ${err}`, this.getMeta()); - } + disconnect() { + logger.info(`[CLUSTER]: disconnect`, this.getMeta()); + this.contextHandler.stopServer(); + this.unbindEvents(); } @action async refreshStatus() { - await this.whenReady; + await when(() => this.initialized); + logger.info(`[CLUSTER]: refreshing status`, this.getMeta()); const connectionStatus = await this.getConnectionStatus(); this.online = connectionStatus > ClusterStatus.Offline; this.accessible = connectionStatus == ClusterStatus.AccessGranted; @@ -191,9 +185,8 @@ export class Cluster implements ClusterModel { return this.preferences.prometheus?.prefix || "" } - protected k8sRequest(path: string, options: RequestPromiseOptions = {}) { + protected async k8sRequest(path: string, options: RequestPromiseOptions = {}) { const apiUrl = this.kubeProxyUrl + path; - logger.debug(`[CLUSTER]: getting request to: ${apiUrl}`); return request(apiUrl, { json: true, timeout: 10000, @@ -211,7 +204,7 @@ export class Cluster implements ClusterModel { this.failureReason = null return ClusterStatus.AccessGranted; } catch (error) { - logger.error(`Failed to connect cluster "${this.contextName}": ${error.stack}`) + logger.error(`Failed to connect cluster "${this.contextName}": ${error}`) if (error.statusCode) { if (error.statusCode >= 400 && error.statusCode < 500) { this.failureReason = "Invalid credentials"; @@ -347,6 +340,15 @@ export class Cluster implements ClusterModel { }) } + pushState = (clusterState = this.getState()) => { + logger.info(`[CLUSTER]: push-state`, clusterState); + broadcastIpc({ + // webContentId: viewId, // todo: send to cluster-view only + channel: "cluster:state", + args: [clusterState], + }) + } + // get cluster system meta, e.g. use in "logger" getMeta() { return { diff --git a/src/main/context-handler.ts b/src/main/context-handler.ts index 228721887c..32e933d8d4 100644 --- a/src/main/context-handler.ts +++ b/src/main/context-handler.ts @@ -12,7 +12,7 @@ import { KubeAuthProxy } from "./kube-auth-proxy" export class ContextHandler { public proxyPort: number; public clusterUrl: UrlWithStringQuery; - protected proxyServer: KubeAuthProxy + protected kubeAuthProxy: KubeAuthProxy protected apiTarget: httpProxy.ServerOptions protected prometheusProvider: string protected prometheusPath: string @@ -36,7 +36,7 @@ export class ContextHandler { return `${namespace}/services/${service}:${port}` } - public async getPrometheusProvider() { + async getPrometheusProvider() { if (!this.prometheusProvider) { const service = await this.getPrometheusService() logger.info(`using ${service.id} as prometheus provider`) @@ -45,7 +45,7 @@ export class ContextHandler { return prometheusProviders.find(p => p.id === this.prometheusProvider) } - public async getPrometheusService(): Promise { + async getPrometheusService(): Promise { const providers = this.prometheusProvider ? prometheusProviders.filter(provider => provider.id == this.prometheusProvider) : prometheusProviders; const prometheusPromises: Promise[] = providers.map(async (provider: PrometheusProvider): Promise => { const apiClient = this.cluster.getProxyKubeconfig().makeApiClient(CoreV1Api) @@ -61,19 +61,19 @@ export class ContextHandler { } } - public async getPrometheusPath(): Promise { + async getPrometheusPath(): Promise { if (!this.prometheusPath) { this.prometheusPath = await this.resolvePrometheusPath() } return this.prometheusPath; } - public async resolveAuthProxyUrl() { + async resolveAuthProxyUrl() { const proxyPort = await this.ensurePort(); return `http://127.0.0.1:${proxyPort}`; } - public async getApiTarget(isWatchRequest = false): Promise { + async getApiTarget(isWatchRequest = false): Promise { if (this.apiTarget && !isWatchRequest) { return this.apiTarget } @@ -104,26 +104,26 @@ export class ContextHandler { return this.proxyPort } - public async ensureServer() { - if (!this.proxyServer) { + async ensureServer() { + if (!this.kubeAuthProxy) { await this.ensurePort(); const proxyEnv = Object.assign({}, process.env) if (this.cluster.preferences.httpsProxy) { proxyEnv.HTTPS_PROXY = this.cluster.preferences.httpsProxy } - this.proxyServer = new KubeAuthProxy(this.cluster, this.proxyPort, proxyEnv) - await this.proxyServer.run() + this.kubeAuthProxy = new KubeAuthProxy(this.cluster, this.proxyPort, proxyEnv) + await this.kubeAuthProxy.run() } } - public stopServer() { - if (this.proxyServer) { - this.proxyServer.exit() - this.proxyServer = null + stopServer() { + if (this.kubeAuthProxy) { + this.kubeAuthProxy.exit() + this.kubeAuthProxy = null } } - public proxyServerError(): string { - return this.proxyServer?.lastError || "" + get proxyLastError(): string { + return this.kubeAuthProxy?.lastError || "" } } diff --git a/src/main/kube-auth-proxy.ts b/src/main/kube-auth-proxy.ts index d793128c34..21e9bd8a7d 100644 --- a/src/main/kube-auth-proxy.ts +++ b/src/main/kube-auth-proxy.ts @@ -1,6 +1,6 @@ import { ChildProcess, spawn } from "child_process" import { waitUntilUsed } from "tcp-port-used"; -import { sendMessage } from "../common/ipc"; +import { broadcastIpc } from "../common/ipc"; import type { Cluster } from "./cluster" import { bundledKubectl, Kubectl } from "./kubectl" import logger from "./logger" @@ -85,8 +85,8 @@ export class KubeAuthProxy { protected async sendIpcLogMessage(res: KubeAuthProxyResponse) { const channel = `kube-auth:${this.cluster.id}` - logger.debug(`[KUBE-AUTH]: output for ${channel}`, { ...res, meta: this.cluster.getMeta() }); - sendMessage({ + logger.info(`[KUBE-AUTH]: out-channel "${channel}"`, { ...res, meta: this.cluster.getMeta() }); + broadcastIpc({ // webContentId: null, // todo: send a message only to single cluster's window channel: channel, args: [res], @@ -95,7 +95,7 @@ export class KubeAuthProxy { public exit() { if (this.proxyProcess) { - logger.debug(`Stopping local proxy: ${this.cluster.contextName}`) + logger.debug("[KUBE-AUTH]: stopping local proxy", this.cluster.getMeta()) this.proxyProcess.kill() } } diff --git a/src/main/lens-proxy.ts b/src/main/lens-proxy.ts index 8179b9d8d9..9c6690f45c 100644 --- a/src/main/lens-proxy.ts +++ b/src/main/lens-proxy.ts @@ -52,21 +52,17 @@ export class LensProxy { protected createProxy(): httpProxy { const proxy = httpProxy.createProxyServer(); - proxy.on("proxyRes", (proxyRes, req, res) => { + if (req.method !== "GET") { + return; + } if (proxyRes.statusCode === 502) { const cluster = this.clusterManager.getClusterForRequest(req) - if (cluster && cluster.contextHandler.proxyServerError()) { - res.writeHead(proxyRes.statusCode, { - "Content-Type": "text/plain" - }) - res.end(cluster.contextHandler.proxyServerError()) - return + const proxyError = cluster?.contextHandler.proxyLastError; + if (proxyError) { + return res.writeHead(502).end(proxyError); } } - if (req.method !== "GET") { - return - } const reqId = this.getRequestId(req); if (this.retryCounters.has(reqId)) { logger.debug(`Resetting proxy retry cache for url: ${reqId}`); @@ -92,10 +88,7 @@ export class LensProxy { } } } - res.writeHead(500, { - 'Content-Type': 'text/plain' - }) - res.end('Oops, something went wrong.') + res.writeHead(500).end("Oops, something went wrong.") }) return proxy; diff --git a/src/main/window-manager.ts b/src/main/window-manager.ts index 828fd66e8f..a634f4f082 100644 --- a/src/main/window-manager.ts +++ b/src/main/window-manager.ts @@ -1,4 +1,4 @@ -import { autorun, reaction } from "mobx"; +import { autorun, reaction, when } from "mobx"; import { BrowserWindow, shell } from "electron" import windowStateKeeper from "electron-window-state" import type { ClusterId } from "../common/cluster-store"; @@ -9,7 +9,6 @@ import logger from "./logger"; // fixme: remove switching view delay on first load export class WindowManager { - protected activeClusterId: ClusterId; protected activeView: BrowserWindow; protected views = new Map(); protected disposers: CallableFunction[] = []; @@ -36,21 +35,7 @@ export class WindowManager { // Manage reactive state this.disposers.push( // auto-show active cluster window and subscribe for push-events - reaction(() => clusterStore.activeCluster, async activeCluster => { - if (this.activeClusterId) { - const prevCluster = clusterStore.getById(this.activeClusterId); - if (prevCluster) prevCluster.unbindEvents(); - this.activeClusterId = null; - } - if (activeCluster) { - this.activeClusterId = activeCluster.id; - const viewId = await this.activateView(activeCluster.id); - if (viewId) { - await activeCluster.refreshStatus(); - activeCluster.bindEvents(viewId); - } - } - }, { + reaction(() => clusterStore.activeClusterId, clusterId => this.activateView(clusterId), { fireImmediately: true, }), @@ -108,7 +93,7 @@ export class WindowManager { if (activeView !== view) { this.activeView = view; if (!isLoadedBefore) { - await cluster.whenReady; + await when(() => cluster.initialized); await view.loadURL(cluster.webContentUrl); this.hideSplash(); } diff --git a/src/renderer/_vue/components/ClusterPage.vue b/src/renderer/_vue/components/ClusterPage.vue deleted file mode 100644 index 29235b1095..0000000000 --- a/src/renderer/_vue/components/ClusterPage.vue +++ /dev/null @@ -1,159 +0,0 @@ - - - - diff --git a/src/renderer/components/app.tsx b/src/renderer/components/app.tsx index 9ef5a9ad35..8992ea6fe2 100755 --- a/src/renderer/components/app.tsx +++ b/src/renderer/components/app.tsx @@ -1,5 +1,5 @@ import "./app.scss"; -import React, { Fragment } from "react"; +import React from "react"; import { observer } from "mobx-react"; import { i18nStore } from "../i18n"; import { configStore } from "../config.store"; @@ -34,11 +34,10 @@ import { LandingPage, landingRoute, landingURL } from "./+landing-page"; import { clusterStore } from "../../common/cluster-store"; import { ClusterSettings, clusterSettingsRoute } from "./+cluster-settings"; import { Workspaces, workspacesRoute } from "./+workspaces"; +import { ErrorBoundary } from "./error-boundary"; @observer export class App extends React.Component { - static rootElem = document.getElementById('app'); - static async init() { await i18nStore.init(); await configStore.init(); @@ -57,27 +56,25 @@ export class App extends React.Component { render() { return ( - + - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + @@ -86,7 +83,7 @@ export class App extends React.Component { - + ) } } diff --git a/src/renderer/components/cluster-manager/cluster-manager.tsx b/src/renderer/components/cluster-manager/cluster-manager.tsx index 6b6ebc6200..50bff19daa 100644 --- a/src/renderer/components/cluster-manager/cluster-manager.tsx +++ b/src/renderer/components/cluster-manager/cluster-manager.tsx @@ -1,16 +1,40 @@ import "./cluster-manager.scss" import React from "react"; +import { observer } from "mobx-react"; +import { computed } from "mobx"; +import { App } from "../app"; +import { ClusterStatus } from "./cluster-status"; import { ClustersMenu } from "./clusters-menu"; import { BottomBar } from "./bottom-bar"; -import { App } from "../app"; +import { cssNames, IClassName } from "../../utils"; +import { invokeIpc } from "../../../common/ipc"; +import { ClusterIpcChannel } from "../../../main/cluster"; +import { clusterStore } from "../../../common/cluster-store"; + +interface Props { + className?: IClassName; + contentClass?: IClassName; +} + +@observer +export class ClusterManager extends React.Component { + @computed get isReady() { + return clusterStore.activeCluster?.isReady + } + + async componentDidMount() { + await invokeIpc(ClusterIpcChannel.INIT) + await App.init(); + } -export class ClusterManager extends React.Component { render() { + const { className, contentClass } = this.props; return ( -
+
-
- +
+ {this.isReady && } + {!this.isReady && }
diff --git a/src/renderer/components/cluster-manager/cluster-status.scss b/src/renderer/components/cluster-manager/cluster-status.scss index ad1fc1d11e..f60b79d8ac 100644 --- a/src/renderer/components/cluster-manager/cluster-status.scss +++ b/src/renderer/components/cluster-manager/cluster-status.scss @@ -1,3 +1,19 @@ .ClusterStatus { + --flex-gap: #{$padding * 2}; + min-width: 350px; + margin: auto; + text-align: center; + + pre { + @include custom-scrollbar; + max-width: 70vw; + max-height: 40vh; + //text-align: left; + } + + .Icon { + --size: 70px; + margin: auto; + } } \ No newline at end of file diff --git a/src/renderer/components/cluster-manager/cluster-status.tsx b/src/renderer/components/cluster-manager/cluster-status.tsx index 6863d855ba..d1d5239f4c 100644 --- a/src/renderer/components/cluster-manager/cluster-status.tsx +++ b/src/renderer/components/cluster-manager/cluster-status.tsx @@ -1,29 +1,32 @@ -import "./cluster-manager.scss" -import type { KubeAuthProxyResponse } from "../../../main/kube-auth-proxy"; -import { Cluster, ClusterIpcEvent } from "../../../main/cluster"; +import "./cluster-status.scss" import React from "react"; +import type { KubeAuthProxyResponse } from "../../../main/kube-auth-proxy"; +import { ClusterIpcChannel } from "../../../main/cluster"; +import { invokeIpc } from "../../../common/ipc"; +import { clusterStore } from "../../../common/cluster-store"; import { ipcRenderer } from "electron"; -import { computed, observable } from "mobx"; +import { observable } from "mobx"; import { observer } from "mobx-react"; import { Icon } from "../icon"; import { Button } from "../button"; -import { Trans } from "@lingui/macro"; - -interface Props { - cluster: Cluster; -} +import { cssNames } from "../../utils"; @observer -export class ClusterStatus extends React.Component { - @observable authProxyOutput = "Connecting ...\n" +export class ClusterStatus extends React.Component { + @observable authOutput: string[] = []; - @computed get clusterId() { - return this.props.cluster.id; + get cluster() { + return clusterStore.activeCluster; + } + + get clusterId() { + return clusterStore.activeClusterId; } componentDidMount() { - ipcRenderer.on(`kube-auth:${this.clusterId}`, (evt, authResponse: KubeAuthProxyResponse) => { - this.authProxyOutput += authResponse.data; + this.authOutput = ["Connecting ...\n"]; + ipcRenderer.on(`kube-auth:${this.clusterId}`, (evt, { data, stream }: KubeAuthProxyResponse) => { + this.authOutput.push(`[${stream}]: ${data}`); }) } @@ -31,22 +34,32 @@ export class ClusterStatus extends React.Component { ipcRenderer.removeAllListeners(`kube-auth:${this.clusterId}`); } - reconnectCluster = () => { - ipcRenderer.send(ClusterIpcEvent.RECONNECT, this.clusterId); + reconnect = () => { + this.authOutput = ["Reconnecting ...\n"]; + invokeIpc(ClusterIpcChannel.RECONNECT, this.clusterId); } render() { - const { authProxyOutput } = this; - const { contextName, online } = this.props.cluster; + const { authOutput, cluster } = this; + const isError = cluster?.accessible === false; return ( -
- -

{contextName}

-
{authProxyOutput}
-
) } diff --git a/src/renderer/components/cluster-manager/clusters-menu.tsx b/src/renderer/components/cluster-manager/clusters-menu.tsx index c3c66b697b..132f39e5cd 100644 --- a/src/renderer/components/cluster-manager/clusters-menu.tsx +++ b/src/renderer/components/cluster-manager/clusters-menu.tsx @@ -51,7 +51,7 @@ export class ClustersMenu extends React.Component { label: _i18n._(t`Settings`), click: () => navigate(clusterSettingsURL()) })); - if (cluster.initialized) { + if (cluster.online) { menu.append(new MenuItem({ label: _i18n._(t`Disconnect`), click: () => { diff --git a/src/renderer/index.tsx b/src/renderer/index.tsx index 86c24632d1..c68cc004ac 100644 --- a/src/renderer/index.tsx +++ b/src/renderer/index.tsx @@ -10,7 +10,6 @@ import { I18nProvider } from "@lingui/react"; import { browserHistory } from "./navigation"; import { isMac } from "../common/vars"; import { _i18n } from "./i18n"; -import { App } from "./components/app"; import { ClusterManager } from "./components/cluster-manager"; import { ErrorBoundary } from "./components/error-boundary"; import { WhatsNew, whatsNewRoute } from "./components/+whats-new"; @@ -19,14 +18,14 @@ import { Preferences, preferencesRoute } from "./components/+preferences"; @observer class LensApp extends React.Component { static async init() { - App.rootElem.classList.toggle("is-mac", isMac); await Promise.all([ userStore.load(), workspaceStore.load(), clusterStore.load(), ]); - await App.init(); - render(, App.rootElem); + const elem = document.getElementById("app"); + elem.classList.toggle("is-mac", isMac); + render(, elem); } render() { From a4424f7e9e214bab7b84abda4fb9e40d4b52d463 Mon Sep 17 00:00:00 2001 From: Roman Date: Mon, 20 Jul 2020 19:14:27 +0300 Subject: [PATCH 07/35] cluster-status -- part 3 Signed-off-by: Roman --- src/main/cluster-manager.ts | 1 + .../cluster-manager/cluster-manager.tsx | 16 ++++++++++------ .../cluster-manager/cluster-status.scss | 3 +-- src/renderer/index.tsx | 6 +++--- 4 files changed, 15 insertions(+), 11 deletions(-) diff --git a/src/main/cluster-manager.ts b/src/main/cluster-manager.ts index f569012c3a..a4e8057bef 100644 --- a/src/main/cluster-manager.ts +++ b/src/main/cluster-manager.ts @@ -33,6 +33,7 @@ export class ClusterManager { this.activeClusterId = activeCluster.id; activeCluster.bindEvents(); activeCluster.refreshStatus(); + activeCluster.pushState(); } }, { fireImmediately: true diff --git a/src/renderer/components/cluster-manager/cluster-manager.tsx b/src/renderer/components/cluster-manager/cluster-manager.tsx index 50bff19daa..09e89dc3a2 100644 --- a/src/renderer/components/cluster-manager/cluster-manager.tsx +++ b/src/renderer/components/cluster-manager/cluster-manager.tsx @@ -1,7 +1,7 @@ import "./cluster-manager.scss" import React from "react"; import { observer } from "mobx-react"; -import { computed } from "mobx"; +import { computed, observable } from "mobx"; import { App } from "../app"; import { ClusterStatus } from "./cluster-status"; import { ClustersMenu } from "./clusters-menu"; @@ -18,23 +18,27 @@ interface Props { @observer export class ClusterManager extends React.Component { - @computed get isReady() { + @observable appReady = false; + + @computed get clusterReady() { return clusterStore.activeCluster?.isReady } async componentDidMount() { - await invokeIpc(ClusterIpcChannel.INIT) + invokeIpc(ClusterIpcChannel.INIT); await App.init(); + this.appReady = true; } render() { const { className, contentClass } = this.props; + const isReady = this.appReady && this.clusterReady; return (
-
- {this.isReady && } - {!this.isReady && } +
+ {isReady && } + {!isReady && }
diff --git a/src/renderer/components/cluster-manager/cluster-status.scss b/src/renderer/components/cluster-manager/cluster-status.scss index f60b79d8ac..59a0103db8 100644 --- a/src/renderer/components/cluster-manager/cluster-status.scss +++ b/src/renderer/components/cluster-manager/cluster-status.scss @@ -6,10 +6,9 @@ text-align: center; pre { - @include custom-scrollbar; + @include hidden-scrollbar; max-width: 70vw; max-height: 40vh; - //text-align: left; } .Icon { diff --git a/src/renderer/index.tsx b/src/renderer/index.tsx index c68cc004ac..38486fd368 100644 --- a/src/renderer/index.tsx +++ b/src/renderer/index.tsx @@ -18,14 +18,14 @@ import { Preferences, preferencesRoute } from "./components/+preferences"; @observer class LensApp extends React.Component { static async init() { + const rootElem = document.getElementById("app"); + rootElem.classList.toggle("is-mac", isMac); await Promise.all([ userStore.load(), workspaceStore.load(), clusterStore.load(), ]); - const elem = document.getElementById("app"); - elem.classList.toggle("is-mac", isMac); - render(, elem); + render(, rootElem); } render() { From 32dfa1b936af99717adc9ceec49156043eb625c0 Mon Sep 17 00:00:00 2001 From: Roman Date: Mon, 20 Jul 2020 19:29:59 +0300 Subject: [PATCH 08/35] fix: cluster-icon badge margin Signed-off-by: Roman --- src/renderer/components/+cluster-settings/cluster-icon.scss | 1 + src/renderer/components/cluster-manager/clusters-menu.tsx | 1 + 2 files changed, 2 insertions(+) diff --git a/src/renderer/components/+cluster-settings/cluster-icon.scss b/src/renderer/components/+cluster-settings/cluster-icon.scss index 09ad4b9e3f..13efbffd29 100644 --- a/src/renderer/components/+cluster-settings/cluster-icon.scss +++ b/src/renderer/components/+cluster-settings/cluster-icon.scss @@ -21,5 +21,6 @@ position: absolute; right: 0; bottom: 0; + margin: -$padding * 1.5; } } \ No newline at end of file diff --git a/src/renderer/components/cluster-manager/clusters-menu.tsx b/src/renderer/components/cluster-manager/clusters-menu.tsx index 132f39e5cd..8ae269b683 100644 --- a/src/renderer/components/cluster-manager/clusters-menu.tsx +++ b/src/renderer/components/cluster-manager/clusters-menu.tsx @@ -20,6 +20,7 @@ import { landingURL } from "../+landing-page"; import { Tooltip, TooltipContent } from "../tooltip"; import { ConfirmDialog } from "../confirm-dialog"; +// fixme: refresh all cluster-icon badges in background (events-count per cluster) // fixme: allow to rearrange clusters with drag&drop // fixme: disconnect cluster from context-menu From 505d3ba1ce767e358ac820ce80ed8a42505797d4 Mon Sep 17 00:00:00 2001 From: Roman Date: Mon, 20 Jul 2020 21:24:41 +0300 Subject: [PATCH 09/35] fix: search-input warning, removed outdated `items-list.tsx` Signed-off-by: Roman --- .../components/input/search-input.tsx | 2 +- src/renderer/components/items-list/index.ts | 1 - .../components/items-list/items-list.scss | 45 ------- .../components/items-list/items-list.tsx | 121 ------------------ 4 files changed, 1 insertion(+), 168 deletions(-) delete mode 100644 src/renderer/components/items-list/index.ts delete mode 100644 src/renderer/components/items-list/items-list.scss delete mode 100644 src/renderer/components/items-list/items-list.tsx diff --git a/src/renderer/components/input/search-input.tsx b/src/renderer/components/input/search-input.tsx index 6d7eb11746..9bf2b7387d 100644 --- a/src/renderer/components/input/search-input.tsx +++ b/src/renderer/components/input/search-input.tsx @@ -26,7 +26,7 @@ const defaultProps: Partial = { export class SearchInput extends React.Component { static defaultProps = defaultProps as object; - @observable inputVal: string; + @observable inputVal = ""; // fix: use empty string to avoid react warnings @disposeOnUnmount updateInput = autorun(() => this.inputVal = getSearch()) diff --git a/src/renderer/components/items-list/index.ts b/src/renderer/components/items-list/index.ts deleted file mode 100644 index 0db21a797b..0000000000 --- a/src/renderer/components/items-list/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./items-list" \ No newline at end of file diff --git a/src/renderer/components/items-list/items-list.scss b/src/renderer/components/items-list/items-list.scss deleted file mode 100644 index fbb3495443..0000000000 --- a/src/renderer/components/items-list/items-list.scss +++ /dev/null @@ -1,45 +0,0 @@ -.ItemsList { - &.selectable { - .Item:not(.disabled) { - cursor: pointer; - - &:hover { - background-color: #f7f7f7; - border-radius: $radius; - } - } - } - - &.inline { - .Item { - margin-right: $margin; - } - } -} - -.Item { - padding: $padding / 2; - margin: 0 $margin / -2; - user-select: none; - - .Icon { - &.tick-icon { - color: $colorOk; - } - - &.action-icon { - border-radius: $radius; - background: white; - color: #36393e; - } - } - - &.selected { - } - - &.disabled { - opacity: .5; - pointer-events: none; - } -} - diff --git a/src/renderer/components/items-list/items-list.tsx b/src/renderer/components/items-list/items-list.tsx deleted file mode 100644 index 0b15ff1684..0000000000 --- a/src/renderer/components/items-list/items-list.tsx +++ /dev/null @@ -1,121 +0,0 @@ -import './items-list.scss' -import React from 'react' -import { observer } from "mobx-react"; -import { cssNames } from "../../utils"; -import { Icon } from "../icon"; - -interface ItemsListProps { - className?: string; - disabled?: boolean; - inline?: boolean; - selectable?: boolean; - multiSelect?: boolean; - showSelectedItems?: boolean; - showSelectedIcon?: boolean; - selectedValues?: any[]; - onSelectChange(currentItem: any, selectedItems: any[]): void; -} - -@observer -export class ItemsList extends React.Component { - static defaultProps: Partial = { - selectable: true, - multiSelect: true, - showSelectedIcon: false, - } - - onClickItem(itemValue: any) { - const { selectedValues, multiSelect, onSelectChange } = this.props; - if (multiSelect) { - const itemIndex = selectedValues.findIndex(value => value === itemValue); - if (itemIndex > -1) { - // remove - const newSelectedValues = [...selectedValues]; - newSelectedValues.splice(itemIndex, 1); - onSelectChange(itemValue, newSelectedValues); - } - else { - // add - const newSelectedValues = [].concat(selectedValues, itemValue); - onSelectChange(itemValue, newSelectedValues) - } - } - else { - onSelectChange(itemValue, [itemValue]); - } - } - - render() { - const { disabled, inline, selectable, selectedValues, showSelectedItems, showSelectedIcon } = this.props; - let { className, children } = this.props; - className = cssNames('ItemsList flex', className, { - selectable: selectable, - "inline wrap": inline, - column: !inline, - }); - if (selectable) { - children = React.Children.toArray(children).map((item: React.ReactElement) => { - const itemValue = item.props.value; - const isSelected = selectedValues.includes(itemValue); - const isDisabled = disabled !== undefined ? disabled : item.props.disabled; - - if (showSelectedItems === false && isSelected) { - return null; - } - - const onClick = (evt: React.MouseEvent) => { - if (item.props.onClick) item.props.onClick(evt); - this.onClickItem(itemValue); - }; - - return React.cloneElement(item, { - showSelectedIcon: showSelectedIcon, - selected: isSelected, - disabled: isDisabled, - onClick: onClick, - }) - }); - } - return ( -
    - {children} -
- ); - } -} - -interface ItemProps extends React.HTMLProps { - value: any; - className?: string; - disabled?: boolean; - selected?: boolean; - showSelectedIcon?: boolean; -} - -const defaultProps: Partial = { - showSelectedIcon: true, -} - -export class Item extends React.Component { - static defaultProps = defaultProps as object; - - render() { - const { disabled, selected, value, showSelectedIcon, children, ...itemProps } = this.props; - let { className } = this.props; - className = cssNames('Item flex gaps', className, { disabled, selected }); - const actionIcon = selected ? "remove" : "add"; - return ( -
  • -
    - {children} -
    - {showSelectedIcon && selected && ( - - )} - {!showSelectedIcon && ( - - )} -
  • - ); - } -} From b1e6f8da193df8fe2e8b6be6204aba1963e2ac27 Mon Sep 17 00:00:00 2001 From: Roman Date: Tue, 21 Jul 2020 11:28:36 +0300 Subject: [PATCH 10/35] fix: terminal was broken at some point during refactoring Signed-off-by: Roman --- src/main/cluster.ts | 6 +++--- src/main/lens-proxy.ts | 6 +++--- src/main/node-shell-session.ts | 10 +++++++--- src/main/shell-session.ts | 2 -- src/renderer/config.store.ts | 2 ++ 5 files changed, 15 insertions(+), 11 deletions(-) diff --git a/src/main/cluster.ts b/src/main/cluster.ts index 88b8e9ac93..be4417242e 100644 --- a/src/main/cluster.ts +++ b/src/main/cluster.ts @@ -109,7 +109,7 @@ export class Cluster implements ClusterModel { this.disposers.push( () => clearInterval(refreshStatusTimer), () => clearInterval(refreshEventsTimer), - reaction(() => this.getState(), this.pushState, { + reaction(this.getState, this.pushState, { fireImmediately: true }) ); @@ -319,8 +319,8 @@ export class Cluster implements ClusterModel { }) } - // serializable cluster-info for push-notifications - getState(): ClusterState { + // serializable cluster-state (mostly used for push-notifications) + getState = (): ClusterState => { const state: ClusterState = { ...this.toJSON(), initialized: this.initialized, diff --git a/src/main/lens-proxy.ts b/src/main/lens-proxy.ts index 9c6690f45c..7951ad2162 100644 --- a/src/main/lens-proxy.ts +++ b/src/main/lens-proxy.ts @@ -3,7 +3,7 @@ import http from "http"; import httpProxy from "http-proxy"; import url from "url"; import * as WebSocket from "ws" -import * as nodeShell from "./node-shell-session" +import { openShell } from "./node-shell-session"; import { Router } from "./router" import { ClusterManager } from "./cluster-manager" import { ContextHandler } from "./context-handler"; @@ -96,10 +96,10 @@ export class LensProxy { protected createWsListener(): WebSocket.Server { const ws = new WebSocket.Server({ noServer: true }) - return ws.on("connection", (async (socket: WebSocket, req: http.IncomingMessage) => { + return ws.on("connection", ((socket: WebSocket, req: http.IncomingMessage) => { const cluster = this.clusterManager.getClusterForRequest(req); const nodeParam = url.parse(req.url, true).query["node"]?.toString(); - await nodeShell.open(socket, cluster, nodeParam); + openShell(socket, cluster, nodeParam); })); } diff --git a/src/main/node-shell-session.ts b/src/main/node-shell-session.ts index bc89b5fb25..b669c262eb 100644 --- a/src/main/node-shell-session.ts +++ b/src/main/node-shell-session.ts @@ -133,9 +133,13 @@ export class NodeShellSession extends ShellSession { } } -export async function open(socket: WebSocket, cluster: Cluster, nodeName?: string): Promise { +export async function openShell(socket: WebSocket, cluster: Cluster, nodeName?: string): Promise { + let shell: ShellSession; if (nodeName) { - return new NodeShellSession(socket, cluster, nodeName) + shell = new NodeShellSession(socket, cluster, nodeName) + } else { + shell = new ShellSession(socket, cluster); } - return new ShellSession(socket, cluster); + shell.open() + return shell; } diff --git a/src/main/shell-session.ts b/src/main/shell-session.ts index 6071428b26..4b82b2d77d 100644 --- a/src/main/shell-session.ts +++ b/src/main/shell-session.ts @@ -11,8 +11,6 @@ import { helmCli } from "./helm/helm-cli" import { isWindows } from "../common/vars"; import { tracker } from "../common/tracker"; -// fixme: terminal doesn't work in ui - export class ShellSession extends EventEmitter { static shellEnvs: Map = new Map() diff --git a/src/renderer/config.store.ts b/src/renderer/config.store.ts index 05d881dbf3..54b45d2622 100755 --- a/src/renderer/config.store.ts +++ b/src/renderer/config.store.ts @@ -3,6 +3,8 @@ import { observable, when } from "mobx"; import { autobind, interval } from "./utils"; import { apiBase } from "./api"; +// todo: use user-store.ts as isomorphic-store with config/settings for ui + @autobind() export class ConfigStore { protected updater = interval(60, this.load); From 51901183cfe8602771153bdfc8b7e8974a7a9f75 Mon Sep 17 00:00:00 2001 From: Roman Date: Tue, 21 Jul 2020 11:43:00 +0300 Subject: [PATCH 11/35] clean-up/update "todo/fixme-comments" Signed-off-by: Roman --- src/common/base-store.ts | 5 ++--- src/common/ipc.ts | 2 +- src/renderer/api/endpoints/selfsubjectrulesreviews.api.ts | 2 +- src/renderer/api/workload-kube-object.ts | 6 ++---- src/renderer/components/+custom-resources/crd-list.tsx | 2 +- .../components/+custom-resources/crd-resource-details.tsx | 4 +--- src/renderer/components/dialog/dialog.tsx | 3 ++- src/renderer/components/tooltip/tooltip.tsx | 2 +- 8 files changed, 11 insertions(+), 15 deletions(-) diff --git a/src/common/base-store.ts b/src/common/base-store.ts index d212c88a7e..04862f9146 100644 --- a/src/common/base-store.ts +++ b/src/common/base-store.ts @@ -61,7 +61,7 @@ export class BaseStore extends Singleton { }, }); const storedModel = Object.assign({}, this.storeConfig.store); - Reflect.deleteProperty(storedModel, "__internal__"); // fixme: avoid "external-internals" + Reflect.deleteProperty(storedModel, "__internal__"); // todo: avoid "external-internals" logger.info(`[STORE]: LOADED from ${this.storeConfig.path}`); this.fromStore(storedModel); this.isLoaded = true; @@ -69,8 +69,7 @@ export class BaseStore extends Singleton { protected async save(model: T) { logger.info(`[STORE]: SAVING ${this.name}`); - // todo: avoid multiple file updates - // fixme: https://github.com/sindresorhus/conf/issues/114 + // todo: update when fixed https://github.com/sindresorhus/conf/issues/114 Object.entries(model).forEach(([key, value]) => { this.storeConfig.set(key, value); }); diff --git a/src/common/ipc.ts b/src/common/ipc.ts index e5e8101994..bdcce8eee3 100644 --- a/src/common/ipc.ts +++ b/src/common/ipc.ts @@ -19,7 +19,7 @@ export interface IpcMessageOpts
    { channel: IpcChannel webContentId?: number; // sends to single webContents view filter?: (webContent: WebContents) => boolean - timeout?: number; // fixme: add support + timeout?: number; // todo: add support args?: A; } diff --git a/src/renderer/api/endpoints/selfsubjectrulesreviews.api.ts b/src/renderer/api/endpoints/selfsubjectrulesreviews.api.ts index 755dcfa54b..e03ba0b6ae 100644 --- a/src/renderer/api/endpoints/selfsubjectrulesreviews.api.ts +++ b/src/renderer/api/endpoints/selfsubjectrulesreviews.api.ts @@ -24,7 +24,7 @@ export class SelfSubjectRulesReview extends KubeObject { static kind = "SelfSubjectRulesReview" spec: { - // fixme: add more types from api docs + // todo: add more types from api docs namespace?: string; } diff --git a/src/renderer/api/workload-kube-object.ts b/src/renderer/api/workload-kube-object.ts index a32ccd99e9..c18b8df6c4 100644 --- a/src/renderer/api/workload-kube-object.ts +++ b/src/renderer/api/workload-kube-object.ts @@ -1,5 +1,5 @@ import get from "lodash/get"; -import { IKubeObjectMetadata, KubeObject } from "./kube-object"; +import { KubeObject } from "./kube-object"; interface IToleration { key?: string; @@ -47,9 +47,7 @@ export interface IAffinity { } export class WorkloadKubeObject extends KubeObject { - - // fixme: add type - spec: any; + spec: any; // todo: add proper types getSelectors(): string[] { const selector = this.spec.selector; diff --git a/src/renderer/components/+custom-resources/crd-list.tsx b/src/renderer/components/+custom-resources/crd-list.tsx index 5707f27c9d..bf8b89101a 100644 --- a/src/renderer/components/+custom-resources/crd-list.tsx +++ b/src/renderer/components/+custom-resources/crd-list.tsx @@ -63,7 +63,7 @@ export class CrdList extends React.Component { if (selectedGroups.length == 1) placeholder = <>Group: {selectedGroups[0]} if (selectedGroups.length >= 2) placeholder = <>Groups: {selectedGroups.join(", ")} return { - // fixme: move to global filters + // todo: move to global filters filters: ( Select kubeconfig} - value={this.clusterConfig} - options={this.clusterOptions} - onChange={({ value }: SelectOption) => this.clusterConfig = value} - /> -
    - this.showSettings = !this.showSettings}> - Proxy settings - + +

    Add Cluster

    + :)`)} + value={this.proxyServer} + onChange={value => this.proxyServer = value} + /> + + HTTP Proxy server. Used for communicating with Kubernetes API. +
    - {this.showSettings && ( -
    - :)`)} - value={this.proxyServer} - onChange={value => this.proxyServer = value} - /> - - HTTP Proxy server. Used for communicating with Kubernetes API. - -
    - )} - {this.isCustom && ( -
    -

    Kubeconfig:

    - this.customConfig = value} - /> -
    - )} - {this.error && ( -
    {this.error}
    - )} -
    -
    -
    -

    Clusters associated with Lens

    -

    - Add clusters by clicking the Add Cluster button. - You'll need to obtain a working kubeconfig for the cluster you want to add. -

    -

    - Each cluster context is added as a separate item in the - left-side cluster menu - to allow you to operate easily on multiple clusters and/or contexts. -

    -

    - For more information on kubeconfig see Kubernetes docs -

    -

    - NOTE: Any manually added cluster is not merged into your kubeconfig file. -

    -

    - To see your currently enabled config with kubectl, use kubectl config view --minify --raw command in your terminal. -

    -

    - When connecting to a cluster, make sure you have a valid and working kubeconfig for the cluster. Following lists known "gotchas" in some authentication types used in kubeconfig with Lens - app. -

    - -

    OIDC (OpenID Connect)

    -
    -
    -

    - When connecting Lens to OIDC enabled cluster, there's few things you as a user need to take into account. -

    - Dedicated refresh token -

    - As Lens app utilized kubeconfig is "disconnected" from your main kubeconfig Lens needs to have it's own refresh token it utilizes. - If you share the refresh token with e.g. kubectl who ever uses the token first will invalidate it for the next user. - One way to achieve this is with kubelogin tool by removing the tokens - (both id_token and refresh_token) from - the config and issuing kubelogin command. That'll take you through the login process and will result you having "dedicated" refresh token. -

    -
    -

    Exec auth plugins

    -

    - When using exec auth plugins make sure the paths that are used to call - any binaries - are full paths as Lens app might not be able to call binaries with relative paths. Make also sure that you pass all needed information either as arguments or env variables in the config, - Lens app might not have all login shell env variables set automatically. -

    -
    -
    + ) } } diff --git a/src/renderer/components/+workspaces/workspace-menu.scss b/src/renderer/components/+workspaces/workspace-menu.scss new file mode 100644 index 0000000000..e7adf9ae6a --- /dev/null +++ b/src/renderer/components/+workspaces/workspace-menu.scss @@ -0,0 +1,7 @@ +.WorkspaceMenu { + border-radius: $radius; + + .workspaces-title { + padding: $padding; + } +} \ No newline at end of file diff --git a/src/renderer/components/+workspaces/workspace-menu.tsx b/src/renderer/components/+workspaces/workspace-menu.tsx new file mode 100644 index 0000000000..bcf9c3a74d --- /dev/null +++ b/src/renderer/components/+workspaces/workspace-menu.tsx @@ -0,0 +1,51 @@ +import "./workspace-menu.scss" +import React from "react"; +import { observer } from "mobx-react"; +import { Link } from "react-router-dom"; +import { workspacesURL } from "./workspaces.route"; +import { Trans } from "@lingui/macro"; +import { Menu, MenuItem, MenuProps } from "../menu"; +import { Icon } from "../icon"; +import { observable } from "mobx"; +import { workspaceStore } from "../../../common/workspace-store"; +import { cssNames } from "../../utils"; + +interface Props extends Partial { +} + +@observer +export class WorkspaceMenu extends React.Component { + @observable menuVisible = false; + + render() { + const { className, ...menuProps } = this.props; + const { workspacesList, currentWorkspace } = workspaceStore; + return ( + this.menuVisible = true} + close={() => this.menuVisible = false} + > + + Workspaces + + {workspacesList.map(({ id: workspaceId, name, description }) => { + return ( + workspaceStore.setActive(workspaceId)} + > + + {name} + + ) + })} + + ) + } +} diff --git a/src/renderer/components/+workspaces/workspaces.tsx b/src/renderer/components/+workspaces/workspaces.tsx index fa6cf28271..5e7dc3089d 100644 --- a/src/renderer/components/+workspaces/workspaces.tsx +++ b/src/renderer/components/+workspaces/workspaces.tsx @@ -1,14 +1,44 @@ import "./workspaces.scss" -import React from "react"; +import React, { Fragment } from "react"; import { observer } from "mobx-react"; +import { Trans } from "@lingui/macro"; +import { WizardLayout } from "../layout/wizard-layout"; +import { workspaceStore } from "../../../common/workspace-store"; @observer export class Workspaces extends React.Component { - render() { + renderInfo() { return ( -
    - Workspaces -
    + +

    What is a Workspace?

    +

    Workspaces are used to organize number of clusters into logical groups.

    +

    A single workspaces contains a list of clusters and their full configuration.

    +
    + ) + } + + addWorkspace = () => { + console.log('add workspace') + } + + render() { + const { workspacesList, currentWorkspace } = workspaceStore; + return ( + +

    + Workspaces +

    +
    + {workspacesList.map(({ id: workspaceId, name, description }) => { + return ( +
    + {name} + {description} +
    + ) + })} +
    +
    ); } } diff --git a/src/renderer/components/cluster-manager/bottom-bar.scss b/src/renderer/components/cluster-manager/bottom-bar.scss index 7193084180..974585723a 100644 --- a/src/renderer/components/cluster-manager/bottom-bar.scss +++ b/src/renderer/components/cluster-manager/bottom-bar.scss @@ -11,11 +11,3 @@ cursor: pointer; } } - -#workspace-menu { - border-radius: $radius; - - .workspaces-title { - padding: $padding; - } -} \ No newline at end of file diff --git a/src/renderer/components/cluster-manager/bottom-bar.tsx b/src/renderer/components/cluster-manager/bottom-bar.tsx index fe3153ecad..14809618f5 100644 --- a/src/renderer/components/cluster-manager/bottom-bar.tsx +++ b/src/renderer/components/cluster-manager/bottom-bar.tsx @@ -1,56 +1,21 @@ import "./bottom-bar.scss" import React from "react"; -import { observable } from "mobx"; import { observer } from "mobx-react"; -import { Link } from "react-router-dom"; -import { Trans } from "@lingui/macro"; import { Icon } from "../icon"; -import { Menu, MenuItem } from "../menu"; -import { WorkspaceId, workspaceStore } from "../../../common/workspace-store"; -import { workspacesURL } from "../+workspaces"; +import { WorkspaceMenu } from "../+workspaces/workspace-menu"; +import { workspaceStore } from "../../../common/workspace-store"; @observer export class BottomBar extends React.Component { - @observable menuVisible = false; - - selectWorkspace = (workspaceId: WorkspaceId) => { - workspaceStore.currentWorkspaceId = workspaceId; - } - render() { - const { currentWorkspace, workspacesList } = workspaceStore; - const menuId = "workspaces-menu" + const { currentWorkspace } = workspaceStore; return (
    {currentWorkspace.name}
    - this.menuVisible = true} - close={() => this.menuVisible = false} - > - - Workspaces - - {workspacesList.map(({ id, name, description }) => { - return ( - this.selectWorkspace(id)} - title={description} - > - - {name} - - ) - })} - +
    ) } diff --git a/src/renderer/components/layout/wizard-layout.scss b/src/renderer/components/layout/wizard-layout.scss new file mode 100644 index 0000000000..08f5e01580 --- /dev/null +++ b/src/renderer/components/layout/wizard-layout.scss @@ -0,0 +1,32 @@ +.WizardLayout { + --flex-gap: #{$padding * 2}; + + position: relative; + padding: $padding * 3; + height: 100%; + display: grid; + grid-template-columns: 1fr 40%; + + > .content-col { + padding: var(--flex-gap); + margin-right: var(--flex-gap); + background-color: var(--clusters-menu-bgc); + border-radius: $radius; + + > .error { + border-radius: $radius; + padding: $padding; + background-color: pink; + } + } + + > .info-col { + @include hidden-scrollbar; + padding: var(--flex-gap); + border-left: 1px solid #353a3e; + } + + a { + color: $colorInfo; + } +} \ No newline at end of file diff --git a/src/renderer/components/layout/wizard-layout.tsx b/src/renderer/components/layout/wizard-layout.tsx new file mode 100644 index 0000000000..107c8feffe --- /dev/null +++ b/src/renderer/components/layout/wizard-layout.tsx @@ -0,0 +1,28 @@ +import "./wizard-layout.scss" +import React from "react"; +import { observer } from "mobx-react"; +import { cssNames, IClassName } from "../../utils"; + +interface Props { + className?: IClassName; + contentClass?: IClassName; + infoPanelClass?: IClassName; + infoPanel?: React.ReactNode; +} + +@observer +export class WizardLayout extends React.Component { + render() { + const { className, contentClass, infoPanelClass, infoPanel, children: content } = this.props; + return ( +
    +
    + {content} +
    +
    + {infoPanel} +
    +
    + ) + } +} From 3622f8b99295b70220ab573c00b760c412e7194a Mon Sep 17 00:00:00 2001 From: Roman Date: Wed, 22 Jul 2020 14:26:17 +0300 Subject: [PATCH 19/35] workspaces -- part 2 Signed-off-by: Roman --- locales/en/messages.po | 83 ++++++++-- locales/fi/messages.po | 83 ++++++++-- locales/ru/messages.po | 83 ++++++++-- src/common/workspace-store.ts | 4 +- .../_vue/components/AddWorkspacePage.vue | 115 ------------- .../_vue/components/EditWorkspacePage.vue | 125 -------------- .../_vue/components/WorkspacesPage.vue | 156 ------------------ .../components/+workspaces/workspaces.scss | 11 ++ .../components/+workspaces/workspaces.tsx | 152 +++++++++++++++-- .../confirm-dialog/confirm-dialog.tsx | 21 ++- 10 files changed, 363 insertions(+), 470 deletions(-) delete mode 100644 src/renderer/_vue/components/AddWorkspacePage.vue delete mode 100644 src/renderer/_vue/components/EditWorkspacePage.vue delete mode 100644 src/renderer/_vue/components/WorkspacesPage.vue diff --git a/locales/en/messages.po b/locales/en/messages.po index ad8798db04..01ac5a96d5 100644 --- a/locales/en/messages.po +++ b/locales/en/messages.po @@ -25,15 +25,19 @@ msgstr "" msgid "(as a percentage of request)" msgstr "(as a percentage of request)" +#: src/renderer/components/+workspaces/workspaces.tsx:108 +msgid "(current)" +msgstr "(current)" + #: src/renderer/components/+network-policies/network-policy-details.tsx:88 msgid "(empty) (Allowing the specific traffic to all pods in this namespace)" msgstr "(empty) (Allowing the specific traffic to all pods in this namespace)" -#: src/renderer/components/+add-cluster/add-cluster.tsx:104 +#: src/renderer/components/+add-cluster/add-cluster.tsx:105 msgid "(new)" msgstr "(new)" -#: src/renderer/components/item-object-list/item-list-layout.tsx:224 +#: src/renderer/components/item-object-list/item-list-layout.tsx:226 msgid "<0>Filtered: {itemsCount} / {allItemsCount}" msgstr "<0>Filtered: {itemsCount} / {allItemsCount}" @@ -45,10 +49,14 @@ msgstr "<0>Your browser does not support all Lens features. Please consider msgid "<0>{0} successfully created" msgstr "<0>{0} successfully created" -#: src/renderer/components/+add-cluster/add-cluster.tsx:126 +#: src/renderer/components/+add-cluster/add-cluster.tsx:176 msgid "A HTTP proxy server URL (format: http://
    :)" msgstr "A HTTP proxy server URL (format: http://
    :)" +#: src/renderer/components/+workspaces/workspaces.tsx:84 +msgid "A single workspaces contains a list of clusters and their full configuration." +msgstr "A single workspaces contains a list of clusters and their full configuration." + #: src/renderer/components/+user-management-roles-bindings/role-binding-details.tsx:80 msgid "API Group" msgstr "API Group" @@ -71,7 +79,7 @@ msgstr "Account Name" msgid "Active" msgstr "Active" -#: src/renderer/components/+add-cluster/add-cluster.tsx:118 +#: src/renderer/components/+add-cluster/add-cluster.tsx:168 #: src/renderer/components/cluster-manager/clusters-menu.tsx:99 msgid "Add Cluster" msgstr "Add Cluster" @@ -84,11 +92,15 @@ msgstr "Add Namespace" msgid "Add RoleBinding" msgstr "Add RoleBinding" +#: src/renderer/components/+workspaces/workspaces.tsx:125 +msgid "Add Workspace" +msgstr "Add Workspace" + #: src/renderer/components/+user-management-roles-bindings/role-binding-details.tsx:111 msgid "Add bindings to {name}" msgstr "Add bindings to {name}" -#: src/renderer/components/+add-cluster/add-cluster.tsx:137 +#: src/renderer/components/+add-cluster/add-cluster.tsx:187 msgid "Add cluster" msgstr "Add cluster" @@ -146,6 +158,14 @@ msgstr "Affinities" msgid "Age" msgstr "Age" +#: src/renderer/components/+workspaces/workspaces.tsx:64 +msgid "All clusters within workspace will be cleared as well" +msgstr "All clusters within workspace will be cleared as well" + +#: src/renderer/components/+workspaces/workspaces.tsx:64 +#~ msgid "All clusters within workspace will be cleared as well." +#~ msgstr "All clusters within workspace will be cleared as well." + #: src/renderer/components/+custom-resources/crd-list.tsx:56 msgid "All groups" msgstr "All groups" @@ -220,6 +240,10 @@ msgstr "Applying.." msgid "Apps" msgstr "Apps" +#: src/renderer/components/+workspaces/workspaces.tsx:61 +msgid "Are you sure you want remove workspace <0>{0}?" +msgstr "Are you sure you want remove workspace <0>{0}?" + #: src/renderer/components/+nodes/node-menu.tsx:41 msgid "Are you sure you want to drain <0>{nodeName}?" msgstr "Are you sure you want to drain <0>{nodeName}?" @@ -333,6 +357,7 @@ msgstr "CPU requests" msgid "CPU:" msgstr "CPU:" +#: src/renderer/components/+workspaces/workspaces.tsx:119 #: src/renderer/components/confirm-dialog/confirm-dialog.tsx:44 #: src/renderer/components/dock/info-panel.tsx:97 #: src/renderer/components/wizard/wizard.tsx:130 @@ -639,7 +664,7 @@ msgstr "Currently applied filters:" msgid "Custom Resources" msgstr "Custom Resources" -#: src/renderer/components/+add-cluster/add-cluster.tsx:110 +#: src/renderer/components/+add-cluster/add-cluster.tsx:111 msgid "Custom.." msgstr "Custom.." @@ -681,6 +706,7 @@ msgstr "Default Runtime Class Name" msgid "Definitions" msgstr "Definitions" +#: src/renderer/components/+workspaces/workspaces.tsx:113 #: src/renderer/components/menu/menu-actions.tsx:84 msgid "Delete" msgstr "Delete" @@ -696,6 +722,7 @@ msgid "Deployments" msgstr "Deployments" #: src/renderer/components/+apps-helm-charts/helm-charts.tsx:65 +#: src/renderer/components/+workspaces/workspaces.tsx:118 msgid "Description" msgstr "Description" @@ -747,6 +774,7 @@ msgstr "Duration" msgid "E-mail" msgstr "E-mail" +#: src/renderer/components/+workspaces/workspaces.tsx:112 #: src/renderer/components/menu/menu-actions.tsx:80 #: src/renderer/components/menu/menu-actions.tsx:81 msgid "Edit" @@ -887,7 +915,7 @@ msgstr "Groups" msgid "HPA" msgstr "HPA" -#: src/renderer/components/+add-cluster/add-cluster.tsx:128 +#: src/renderer/components/+add-cluster/add-cluster.tsx:178 msgid "HTTP Proxy server. Used for communicating with Kubernetes API." msgstr "HTTP Proxy server. Used for communicating with Kubernetes API." @@ -1268,6 +1296,10 @@ msgstr "Mountable secrets" msgid "Mounts" msgstr "Mounts" +#: src/renderer/components/+workspaces/workspaces.tsx:36 +msgid "My Workspace" +msgstr "My Workspace" + #: src/renderer/components/+apps-helm-charts/helm-charts.tsx:64 #: src/renderer/components/+apps-releases/releases.tsx:87 #: src/renderer/components/+config-autoscalers/hpa-details.tsx:49 @@ -1310,6 +1342,7 @@ msgstr "Mounts" #: src/renderer/components/+workloads-pods/pods.tsx:73 #: src/renderer/components/+workloads-replicasets/replicasets.tsx:50 #: src/renderer/components/+workloads-statefulsets/statefulsets.tsx:40 +#: src/renderer/components/+workspaces/workspaces.tsx:117 #: src/renderer/components/dock/edit-resource.tsx:90 #: src/renderer/components/kube-object/kube-object-meta.tsx:20 msgid "Name" @@ -1436,7 +1469,7 @@ msgstr "No filters available." msgid "No issues found" msgstr "No issues found" -#: src/renderer/components/item-object-list/item-list-layout.tsx:196 +#: src/renderer/components/item-object-list/item-list-layout.tsx:198 msgid "No items found." msgstr "No items found." @@ -1575,7 +1608,7 @@ msgstr "Persistent Volume Claims" msgid "Persistent Volumes" msgstr "Persistent Volumes" -#: src/renderer/components/+add-cluster/add-cluster.tsx:51 +#: src/renderer/components/+add-cluster/add-cluster.tsx:52 msgid "Please select kubeconfig" msgstr "Please select kubeconfig" @@ -1660,7 +1693,7 @@ msgstr "Privileged" msgid "Provisioner" msgstr "Provisioner" -#: src/renderer/components/+add-cluster/add-cluster.tsx:122 +#: src/renderer/components/+add-cluster/add-cluster.tsx:172 msgid "Proxy settings" msgstr "Proxy settings" @@ -1743,7 +1776,7 @@ msgstr "Releases" #: src/renderer/components/+user-management-roles-bindings/role-binding-details.tsx:60 #: src/renderer/components/cluster-manager/clusters-menu.tsx:60 #: src/renderer/components/cluster-manager/clusters-menu.tsx:64 -#: src/renderer/components/item-object-list/item-list-layout.tsx:179 +#: src/renderer/components/item-object-list/item-list-layout.tsx:181 #: src/renderer/components/menu/menu-actions.tsx:49 #: src/renderer/components/menu/menu-actions.tsx:85 msgid "Remove" @@ -1753,6 +1786,10 @@ msgstr "Remove" msgid "Remove <0>{releaseNames}?" msgstr "Remove <0>{releaseNames}?" +#: src/renderer/components/+workspaces/workspaces.tsx:51 +msgid "Remove Workspace" +msgstr "Remove Workspace" + #: src/renderer/components/+config-secrets/add-secret-dialog.tsx:133 msgid "Remove field" msgstr "Remove field" @@ -1769,7 +1806,7 @@ msgstr "Remove selected bindings for <0>{0}?" msgid "Remove selected bindings from ${name}" msgstr "Remove selected bindings from ${name}" -#: src/renderer/components/item-object-list/item-list-layout.tsx:275 +#: src/renderer/components/item-object-list/item-list-layout.tsx:277 msgid "Remove selected items ({0})" msgstr "Remove selected items ({0})" @@ -1828,7 +1865,7 @@ msgstr "Required field" msgid "Reset" msgstr "Reset" -#: src/renderer/components/item-object-list/item-list-layout.tsx:199 +#: src/renderer/components/item-object-list/item-list-layout.tsx:201 msgid "Reset filters?" msgstr "Reset filters?" @@ -1954,6 +1991,7 @@ msgstr "Runtime Class" #: src/renderer/components/+config-maps/config-map-details.tsx:78 #: src/renderer/components/+config-secrets/secret-details.tsx:97 #: src/renderer/components/+workloads-pods/pod-logs-dialog.tsx:216 +#: src/renderer/components/+workspaces/workspaces.tsx:120 #: src/renderer/components/dock/edit-resource.tsx:88 msgid "Save" msgstr "Save" @@ -2030,7 +2068,7 @@ msgstr "Secrets" msgid "Select a quota.." msgstr "Select a quota.." -#: src/renderer/components/+add-cluster/add-cluster.tsx:119 +#: src/renderer/components/+add-cluster/add-cluster.tsx:169 msgid "Select kubeconfig" msgstr "Select kubeconfig" @@ -2433,6 +2471,10 @@ msgstr "Warnings: {0}" msgid "Welcome!" msgstr "Welcome!" +#: src/renderer/components/+workspaces/workspaces.tsx:79 +msgid "What is a Workspace?" +msgstr "What is a Workspace?" + #: src/renderer/components/+cluster/cluster-metric-switchers.tsx:19 msgid "Worker" msgstr "Worker" @@ -2441,10 +2483,15 @@ msgstr "Worker" msgid "Workloads" msgstr "Workloads" -#: src/renderer/components/cluster-manager/bottom-bar.tsx:35 +#: src/renderer/components/+workspaces/workspace-menu.tsx:39 +#: src/renderer/components/+workspaces/workspaces.tsx:91 msgid "Workspaces" msgstr "Workspaces" +#: src/renderer/components/+workspaces/workspaces.tsx:81 +msgid "Workspaces are used to organize number of clusters into logical groups." +msgstr "Workspaces are used to organize number of clusters into logical groups." + #: src/renderer/components/input/input.validators.ts:10 msgid "Wrong email format" msgstr "Wrong email format" @@ -2478,7 +2525,7 @@ msgstr "Zone" msgid "ago" msgstr "ago" -#: src/renderer/components/item-object-list/item-list-layout.tsx:178 +#: src/renderer/components/item-object-list/item-list-layout.tsx:180 msgid "and <0>{tailCount} more" msgstr "and <0>{tailCount} more" @@ -2555,7 +2602,7 @@ msgstr "{0} unavailable" msgid "{accountName} kubeconfig" msgstr "{accountName} kubeconfig" -#: src/renderer/components/item-object-list/item-list-layout.tsx:228 +#: src/renderer/components/item-object-list/item-list-layout.tsx:230 msgid "{allItemsCount, plural, one {# item} other {# items}}" msgstr "{allItemsCount, plural, one {# item} other {# items}}" @@ -2571,7 +2618,7 @@ msgstr "{podName} Logs" msgid "{resourceType} <0>{resourceName} updated." msgstr "{resourceType} <0>{resourceName} updated." -#: src/renderer/components/item-object-list/item-list-layout.tsx:179 +#: src/renderer/components/item-object-list/item-list-layout.tsx:181 msgid "{selectedCount, plural, one {<0>Remove item <1>{selectedNames}?} other {<2>Remove <3>{selectedCount} items <4>{selectedNames} {tail}?}}" msgstr "{selectedCount, plural, one {<0>Remove item <1>{selectedNames}?} other {<2>Remove <3>{selectedCount} items <4>{selectedNames} {tail}?}}" diff --git a/locales/fi/messages.po b/locales/fi/messages.po index 4fba45271a..6892f5bed8 100644 --- a/locales/fi/messages.po +++ b/locales/fi/messages.po @@ -25,15 +25,19 @@ msgstr "" msgid "(as a percentage of request)" msgstr "" +#: src/renderer/components/+workspaces/workspaces.tsx:108 +msgid "(current)" +msgstr "" + #: src/renderer/components/+network-policies/network-policy-details.tsx:88 msgid "(empty) (Allowing the specific traffic to all pods in this namespace)" msgstr "" -#: src/renderer/components/+add-cluster/add-cluster.tsx:104 +#: src/renderer/components/+add-cluster/add-cluster.tsx:105 msgid "(new)" msgstr "" -#: src/renderer/components/item-object-list/item-list-layout.tsx:224 +#: src/renderer/components/item-object-list/item-list-layout.tsx:226 msgid "<0>Filtered: {itemsCount} / {allItemsCount}" msgstr "" @@ -45,10 +49,14 @@ msgstr "" msgid "<0>{0} successfully created" msgstr "" -#: src/renderer/components/+add-cluster/add-cluster.tsx:126 +#: src/renderer/components/+add-cluster/add-cluster.tsx:176 msgid "A HTTP proxy server URL (format: http://
    :)" msgstr "" +#: src/renderer/components/+workspaces/workspaces.tsx:84 +msgid "A single workspaces contains a list of clusters and their full configuration." +msgstr "" + #: src/renderer/components/+user-management-roles-bindings/role-binding-details.tsx:80 msgid "API Group" msgstr "" @@ -71,7 +79,7 @@ msgstr "" msgid "Active" msgstr "" -#: src/renderer/components/+add-cluster/add-cluster.tsx:118 +#: src/renderer/components/+add-cluster/add-cluster.tsx:168 #: src/renderer/components/cluster-manager/clusters-menu.tsx:99 msgid "Add Cluster" msgstr "" @@ -84,11 +92,15 @@ msgstr "" msgid "Add RoleBinding" msgstr "" +#: src/renderer/components/+workspaces/workspaces.tsx:125 +msgid "Add Workspace" +msgstr "" + #: src/renderer/components/+user-management-roles-bindings/role-binding-details.tsx:111 msgid "Add bindings to {name}" msgstr "" -#: src/renderer/components/+add-cluster/add-cluster.tsx:137 +#: src/renderer/components/+add-cluster/add-cluster.tsx:187 msgid "Add cluster" msgstr "" @@ -146,6 +158,14 @@ msgstr "" msgid "Age" msgstr "" +#: src/renderer/components/+workspaces/workspaces.tsx:64 +msgid "All clusters within workspace will be cleared as well" +msgstr "" + +#: src/renderer/components/+workspaces/workspaces.tsx:64 +#~ msgid "All clusters within workspace will be cleared as well." +#~ msgstr "" + #: src/renderer/components/+custom-resources/crd-list.tsx:56 msgid "All groups" msgstr "" @@ -220,6 +240,10 @@ msgstr "" msgid "Apps" msgstr "" +#: src/renderer/components/+workspaces/workspaces.tsx:61 +msgid "Are you sure you want remove workspace <0>{0}?" +msgstr "" + #: src/renderer/components/+nodes/node-menu.tsx:41 msgid "Are you sure you want to drain <0>{nodeName}?" msgstr "" @@ -333,6 +357,7 @@ msgstr "" msgid "CPU:" msgstr "" +#: src/renderer/components/+workspaces/workspaces.tsx:119 #: src/renderer/components/confirm-dialog/confirm-dialog.tsx:44 #: src/renderer/components/dock/info-panel.tsx:97 #: src/renderer/components/wizard/wizard.tsx:130 @@ -635,7 +660,7 @@ msgstr "" msgid "Custom Resources" msgstr "" -#: src/renderer/components/+add-cluster/add-cluster.tsx:110 +#: src/renderer/components/+add-cluster/add-cluster.tsx:111 msgid "Custom.." msgstr "" @@ -677,6 +702,7 @@ msgstr "" msgid "Definitions" msgstr "" +#: src/renderer/components/+workspaces/workspaces.tsx:113 #: src/renderer/components/menu/menu-actions.tsx:84 msgid "Delete" msgstr "" @@ -692,6 +718,7 @@ msgid "Deployments" msgstr "" #: src/renderer/components/+apps-helm-charts/helm-charts.tsx:65 +#: src/renderer/components/+workspaces/workspaces.tsx:118 msgid "Description" msgstr "" @@ -743,6 +770,7 @@ msgstr "" msgid "E-mail" msgstr "" +#: src/renderer/components/+workspaces/workspaces.tsx:112 #: src/renderer/components/menu/menu-actions.tsx:80 #: src/renderer/components/menu/menu-actions.tsx:81 msgid "Edit" @@ -878,7 +906,7 @@ msgstr "" msgid "HPA" msgstr "" -#: src/renderer/components/+add-cluster/add-cluster.tsx:128 +#: src/renderer/components/+add-cluster/add-cluster.tsx:178 msgid "HTTP Proxy server. Used for communicating with Kubernetes API." msgstr "" @@ -1259,6 +1287,10 @@ msgstr "" msgid "Mounts" msgstr "" +#: src/renderer/components/+workspaces/workspaces.tsx:36 +msgid "My Workspace" +msgstr "" + #: src/renderer/components/+apps-helm-charts/helm-charts.tsx:64 #: src/renderer/components/+apps-releases/releases.tsx:87 #: src/renderer/components/+config-autoscalers/hpa-details.tsx:49 @@ -1301,6 +1333,7 @@ msgstr "" #: src/renderer/components/+workloads-pods/pods.tsx:73 #: src/renderer/components/+workloads-replicasets/replicasets.tsx:50 #: src/renderer/components/+workloads-statefulsets/statefulsets.tsx:40 +#: src/renderer/components/+workspaces/workspaces.tsx:117 #: src/renderer/components/dock/edit-resource.tsx:90 #: src/renderer/components/kube-object/kube-object-meta.tsx:20 msgid "Name" @@ -1419,7 +1452,7 @@ msgstr "" msgid "No issues found" msgstr "" -#: src/renderer/components/item-object-list/item-list-layout.tsx:196 +#: src/renderer/components/item-object-list/item-list-layout.tsx:198 msgid "No items found." msgstr "" @@ -1558,7 +1591,7 @@ msgstr "" msgid "Persistent Volumes" msgstr "" -#: src/renderer/components/+add-cluster/add-cluster.tsx:51 +#: src/renderer/components/+add-cluster/add-cluster.tsx:52 msgid "Please select kubeconfig" msgstr "" @@ -1643,7 +1676,7 @@ msgstr "" msgid "Provisioner" msgstr "" -#: src/renderer/components/+add-cluster/add-cluster.tsx:122 +#: src/renderer/components/+add-cluster/add-cluster.tsx:172 msgid "Proxy settings" msgstr "" @@ -1726,7 +1759,7 @@ msgstr "" #: src/renderer/components/+user-management-roles-bindings/role-binding-details.tsx:60 #: src/renderer/components/cluster-manager/clusters-menu.tsx:60 #: src/renderer/components/cluster-manager/clusters-menu.tsx:64 -#: src/renderer/components/item-object-list/item-list-layout.tsx:179 +#: src/renderer/components/item-object-list/item-list-layout.tsx:181 #: src/renderer/components/menu/menu-actions.tsx:49 #: src/renderer/components/menu/menu-actions.tsx:85 msgid "Remove" @@ -1736,6 +1769,10 @@ msgstr "" msgid "Remove <0>{releaseNames}?" msgstr "" +#: src/renderer/components/+workspaces/workspaces.tsx:51 +msgid "Remove Workspace" +msgstr "" + #: src/renderer/components/+config-secrets/add-secret-dialog.tsx:133 msgid "Remove field" msgstr "" @@ -1752,7 +1789,7 @@ msgstr "" msgid "Remove selected bindings from ${name}" msgstr "" -#: src/renderer/components/item-object-list/item-list-layout.tsx:275 +#: src/renderer/components/item-object-list/item-list-layout.tsx:277 msgid "Remove selected items ({0})" msgstr "" @@ -1811,7 +1848,7 @@ msgstr "" msgid "Reset" msgstr "" -#: src/renderer/components/item-object-list/item-list-layout.tsx:199 +#: src/renderer/components/item-object-list/item-list-layout.tsx:201 msgid "Reset filters?" msgstr "" @@ -1937,6 +1974,7 @@ msgstr "" #: src/renderer/components/+config-maps/config-map-details.tsx:78 #: src/renderer/components/+config-secrets/secret-details.tsx:97 #: src/renderer/components/+workloads-pods/pod-logs-dialog.tsx:216 +#: src/renderer/components/+workspaces/workspaces.tsx:120 #: src/renderer/components/dock/edit-resource.tsx:88 msgid "Save" msgstr "" @@ -2013,7 +2051,7 @@ msgstr "" msgid "Select a quota.." msgstr "" -#: src/renderer/components/+add-cluster/add-cluster.tsx:119 +#: src/renderer/components/+add-cluster/add-cluster.tsx:169 msgid "Select kubeconfig" msgstr "" @@ -2416,6 +2454,10 @@ msgstr "" msgid "Welcome!" msgstr "" +#: src/renderer/components/+workspaces/workspaces.tsx:79 +msgid "What is a Workspace?" +msgstr "" + #: src/renderer/components/+cluster/cluster-metric-switchers.tsx:19 msgid "Worker" msgstr "" @@ -2424,10 +2466,15 @@ msgstr "" msgid "Workloads" msgstr "" -#: src/renderer/components/cluster-manager/bottom-bar.tsx:35 +#: src/renderer/components/+workspaces/workspace-menu.tsx:39 +#: src/renderer/components/+workspaces/workspaces.tsx:91 msgid "Workspaces" msgstr "" +#: src/renderer/components/+workspaces/workspaces.tsx:81 +msgid "Workspaces are used to organize number of clusters into logical groups." +msgstr "" + #: src/renderer/components/input/input.validators.ts:10 msgid "Wrong email format" msgstr "" @@ -2461,7 +2508,7 @@ msgstr "" msgid "ago" msgstr "" -#: src/renderer/components/item-object-list/item-list-layout.tsx:178 +#: src/renderer/components/item-object-list/item-list-layout.tsx:180 msgid "and <0>{tailCount} more" msgstr "" @@ -2538,7 +2585,7 @@ msgstr "" msgid "{accountName} kubeconfig" msgstr "" -#: src/renderer/components/item-object-list/item-list-layout.tsx:228 +#: src/renderer/components/item-object-list/item-list-layout.tsx:230 msgid "{allItemsCount, plural, one {# item} other {# items}}" msgstr "" @@ -2554,7 +2601,7 @@ msgstr "" msgid "{resourceType} <0>{resourceName} updated." msgstr "" -#: src/renderer/components/item-object-list/item-list-layout.tsx:179 +#: src/renderer/components/item-object-list/item-list-layout.tsx:181 msgid "{selectedCount, plural, one {<0>Remove item <1>{selectedNames}?} other {<2>Remove <3>{selectedCount} items <4>{selectedNames} {tail}?}}" msgstr "" diff --git a/locales/ru/messages.po b/locales/ru/messages.po index d77d9d92d7..6048ee8c5a 100644 --- a/locales/ru/messages.po +++ b/locales/ru/messages.po @@ -26,15 +26,19 @@ msgstr "" msgid "(as a percentage of request)" msgstr "" +#: src/renderer/components/+workspaces/workspaces.tsx:108 +msgid "(current)" +msgstr "" + #: src/renderer/components/+network-policies/network-policy-details.tsx:88 msgid "(empty) (Allowing the specific traffic to all pods in this namespace)" msgstr "(Пусто) (Допускается трафик ко всем подам в данной области имен)" -#: src/renderer/components/+add-cluster/add-cluster.tsx:104 +#: src/renderer/components/+add-cluster/add-cluster.tsx:105 msgid "(new)" msgstr "" -#: src/renderer/components/item-object-list/item-list-layout.tsx:224 +#: src/renderer/components/item-object-list/item-list-layout.tsx:226 msgid "<0>Filtered: {itemsCount} / {allItemsCount}" msgstr "<0>Отфильтровано: {itemsCount} / {allItemsCount}" @@ -46,10 +50,14 @@ msgstr "<0>Ваш браузер не поддерживает все возмо msgid "<0>{0} successfully created" msgstr "" -#: src/renderer/components/+add-cluster/add-cluster.tsx:126 +#: src/renderer/components/+add-cluster/add-cluster.tsx:176 msgid "A HTTP proxy server URL (format: http://
    :)" msgstr "" +#: src/renderer/components/+workspaces/workspaces.tsx:84 +msgid "A single workspaces contains a list of clusters and their full configuration." +msgstr "" + #: src/renderer/components/+user-management-roles-bindings/role-binding-details.tsx:80 msgid "API Group" msgstr "" @@ -72,7 +80,7 @@ msgstr "Название аккаунта" msgid "Active" msgstr "Активный" -#: src/renderer/components/+add-cluster/add-cluster.tsx:118 +#: src/renderer/components/+add-cluster/add-cluster.tsx:168 #: src/renderer/components/cluster-manager/clusters-menu.tsx:99 msgid "Add Cluster" msgstr "" @@ -85,11 +93,15 @@ msgstr "Добавить Namespace" msgid "Add RoleBinding" msgstr "Добавить привязку ролей" +#: src/renderer/components/+workspaces/workspaces.tsx:125 +msgid "Add Workspace" +msgstr "" + #: src/renderer/components/+user-management-roles-bindings/role-binding-details.tsx:111 msgid "Add bindings to {name}" msgstr "Добавить привязки к {name}" -#: src/renderer/components/+add-cluster/add-cluster.tsx:137 +#: src/renderer/components/+add-cluster/add-cluster.tsx:187 msgid "Add cluster" msgstr "" @@ -147,6 +159,14 @@ msgstr "Аффинитеты" msgid "Age" msgstr "Возраст" +#: src/renderer/components/+workspaces/workspaces.tsx:64 +msgid "All clusters within workspace will be cleared as well" +msgstr "" + +#: src/renderer/components/+workspaces/workspaces.tsx:64 +#~ msgid "All clusters within workspace will be cleared as well." +#~ msgstr "" + #: src/renderer/components/+custom-resources/crd-list.tsx:56 msgid "All groups" msgstr "" @@ -221,6 +241,10 @@ msgstr "Применение.." msgid "Apps" msgstr "Приложения" +#: src/renderer/components/+workspaces/workspaces.tsx:61 +msgid "Are you sure you want remove workspace <0>{0}?" +msgstr "" + #: src/renderer/components/+nodes/node-menu.tsx:41 msgid "Are you sure you want to drain <0>{nodeName}?" msgstr "Выполнить команду drain для ноды <0>{nodeName}?" @@ -334,6 +358,7 @@ msgstr "Запросы к процессору" msgid "CPU:" msgstr "CPU:" +#: src/renderer/components/+workspaces/workspaces.tsx:119 #: src/renderer/components/confirm-dialog/confirm-dialog.tsx:44 #: src/renderer/components/dock/info-panel.tsx:97 #: src/renderer/components/wizard/wizard.tsx:130 @@ -640,7 +665,7 @@ msgstr "Текущие фильтры:" msgid "Custom Resources" msgstr "" -#: src/renderer/components/+add-cluster/add-cluster.tsx:110 +#: src/renderer/components/+add-cluster/add-cluster.tsx:111 msgid "Custom.." msgstr "" @@ -682,6 +707,7 @@ msgstr "" msgid "Definitions" msgstr "" +#: src/renderer/components/+workspaces/workspaces.tsx:113 #: src/renderer/components/menu/menu-actions.tsx:84 msgid "Delete" msgstr "Удалить" @@ -697,6 +723,7 @@ msgid "Deployments" msgstr "Deployments" #: src/renderer/components/+apps-helm-charts/helm-charts.tsx:65 +#: src/renderer/components/+workspaces/workspaces.tsx:118 msgid "Description" msgstr "Описание" @@ -748,6 +775,7 @@ msgstr "Продолжительность" msgid "E-mail" msgstr "Эл. почта" +#: src/renderer/components/+workspaces/workspaces.tsx:112 #: src/renderer/components/menu/menu-actions.tsx:80 #: src/renderer/components/menu/menu-actions.tsx:81 msgid "Edit" @@ -888,7 +916,7 @@ msgstr "Группы" msgid "HPA" msgstr "HPA" -#: src/renderer/components/+add-cluster/add-cluster.tsx:128 +#: src/renderer/components/+add-cluster/add-cluster.tsx:178 msgid "HTTP Proxy server. Used for communicating with Kubernetes API." msgstr "" @@ -1269,6 +1297,10 @@ msgstr "Монтируемые секреты" msgid "Mounts" msgstr "Установки" +#: src/renderer/components/+workspaces/workspaces.tsx:36 +msgid "My Workspace" +msgstr "" + #: src/renderer/components/+apps-helm-charts/helm-charts.tsx:64 #: src/renderer/components/+apps-releases/releases.tsx:87 #: src/renderer/components/+config-autoscalers/hpa-details.tsx:49 @@ -1311,6 +1343,7 @@ msgstr "Установки" #: src/renderer/components/+workloads-pods/pods.tsx:73 #: src/renderer/components/+workloads-replicasets/replicasets.tsx:50 #: src/renderer/components/+workloads-statefulsets/statefulsets.tsx:40 +#: src/renderer/components/+workspaces/workspaces.tsx:117 #: src/renderer/components/dock/edit-resource.tsx:90 #: src/renderer/components/kube-object/kube-object-meta.tsx:20 msgid "Name" @@ -1437,7 +1470,7 @@ msgstr "Нет доступных фильтров." msgid "No issues found" msgstr "Проблемы не обнаружены" -#: src/renderer/components/item-object-list/item-list-layout.tsx:196 +#: src/renderer/components/item-object-list/item-list-layout.tsx:198 msgid "No items found." msgstr "Ничего не найдено." @@ -1576,7 +1609,7 @@ msgstr "Persistent Volume Claims" msgid "Persistent Volumes" msgstr "Persistent Volumes" -#: src/renderer/components/+add-cluster/add-cluster.tsx:51 +#: src/renderer/components/+add-cluster/add-cluster.tsx:52 msgid "Please select kubeconfig" msgstr "" @@ -1661,7 +1694,7 @@ msgstr "" msgid "Provisioner" msgstr "Комиссия" -#: src/renderer/components/+add-cluster/add-cluster.tsx:122 +#: src/renderer/components/+add-cluster/add-cluster.tsx:172 msgid "Proxy settings" msgstr "" @@ -1744,7 +1777,7 @@ msgstr "Релизы" #: src/renderer/components/+user-management-roles-bindings/role-binding-details.tsx:60 #: src/renderer/components/cluster-manager/clusters-menu.tsx:60 #: src/renderer/components/cluster-manager/clusters-menu.tsx:64 -#: src/renderer/components/item-object-list/item-list-layout.tsx:179 +#: src/renderer/components/item-object-list/item-list-layout.tsx:181 #: src/renderer/components/menu/menu-actions.tsx:49 #: src/renderer/components/menu/menu-actions.tsx:85 msgid "Remove" @@ -1754,6 +1787,10 @@ msgstr "Удалить" msgid "Remove <0>{releaseNames}?" msgstr "Удалить <0>{releaseNames}?" +#: src/renderer/components/+workspaces/workspaces.tsx:51 +msgid "Remove Workspace" +msgstr "" + #: src/renderer/components/+config-secrets/add-secret-dialog.tsx:133 msgid "Remove field" msgstr "Удалить поле" @@ -1770,7 +1807,7 @@ msgstr "Удалить выбранные связки <0>{0}?" msgid "Remove selected bindings from ${name}" msgstr "Удалить выбранные связки из ${name}" -#: src/renderer/components/item-object-list/item-list-layout.tsx:275 +#: src/renderer/components/item-object-list/item-list-layout.tsx:277 msgid "Remove selected items ({0})" msgstr "Удалить выбранные элементы ({0})" @@ -1829,7 +1866,7 @@ msgstr "Обязательное поле" msgid "Reset" msgstr "Сбросить" -#: src/renderer/components/item-object-list/item-list-layout.tsx:199 +#: src/renderer/components/item-object-list/item-list-layout.tsx:201 msgid "Reset filters?" msgstr "Сбросить фильтры?" @@ -1955,6 +1992,7 @@ msgstr "" #: src/renderer/components/+config-maps/config-map-details.tsx:78 #: src/renderer/components/+config-secrets/secret-details.tsx:97 #: src/renderer/components/+workloads-pods/pod-logs-dialog.tsx:216 +#: src/renderer/components/+workspaces/workspaces.tsx:120 #: src/renderer/components/dock/edit-resource.tsx:88 msgid "Save" msgstr "Сохранить" @@ -2031,7 +2069,7 @@ msgstr "Secrets" msgid "Select a quota.." msgstr "Выберите квоту..." -#: src/renderer/components/+add-cluster/add-cluster.tsx:119 +#: src/renderer/components/+add-cluster/add-cluster.tsx:169 msgid "Select kubeconfig" msgstr "" @@ -2434,6 +2472,10 @@ msgstr "Предупреждения: {0}" msgid "Welcome!" msgstr "" +#: src/renderer/components/+workspaces/workspaces.tsx:79 +msgid "What is a Workspace?" +msgstr "" + #: src/renderer/components/+cluster/cluster-metric-switchers.tsx:19 msgid "Worker" msgstr "Рабочие" @@ -2442,10 +2484,15 @@ msgstr "Рабочие" msgid "Workloads" msgstr "Ресурсы" -#: src/renderer/components/cluster-manager/bottom-bar.tsx:35 +#: src/renderer/components/+workspaces/workspace-menu.tsx:39 +#: src/renderer/components/+workspaces/workspaces.tsx:91 msgid "Workspaces" msgstr "" +#: src/renderer/components/+workspaces/workspaces.tsx:81 +msgid "Workspaces are used to organize number of clusters into logical groups." +msgstr "" + #: src/renderer/components/input/input.validators.ts:10 msgid "Wrong email format" msgstr "Неверный формат электронной почты" @@ -2479,7 +2526,7 @@ msgstr "Зона" msgid "ago" msgstr "тому назад" -#: src/renderer/components/item-object-list/item-list-layout.tsx:178 +#: src/renderer/components/item-object-list/item-list-layout.tsx:180 msgid "and <0>{tailCount} more" msgstr "и <0>{tailCount} ещё" @@ -2556,7 +2603,7 @@ msgstr "{0} недоступно" msgid "{accountName} kubeconfig" msgstr "{accountName} конфигурация" -#: src/renderer/components/item-object-list/item-list-layout.tsx:228 +#: src/renderer/components/item-object-list/item-list-layout.tsx:230 msgid "{allItemsCount, plural, one {# item} other {# items}}" msgstr "{allItemsCount, plural, one {# элемент} few {# элемента} many {# элементов} other {# элементов}}" @@ -2572,7 +2619,7 @@ msgstr "{podName} логи" msgid "{resourceType} <0>{resourceName} updated." msgstr "{resourceType} <0>{resourceName} обновлен." -#: src/renderer/components/item-object-list/item-list-layout.tsx:179 +#: src/renderer/components/item-object-list/item-list-layout.tsx:181 msgid "{selectedCount, plural, one {<0>Remove item <1>{selectedNames}?} other {<2>Remove <3>{selectedCount} items <4>{selectedNames} {tail}?}}" msgstr "" "{\n" diff --git a/src/common/workspace-store.ts b/src/common/workspace-store.ts index 7d67bd4d35..3935dda02e 100644 --- a/src/common/workspace-store.ts +++ b/src/common/workspace-store.ts @@ -55,7 +55,7 @@ export class WorkspaceStore extends BaseStore { } @action - public saveWorkspace(workspace: Workspace) { + saveWorkspace(workspace: Workspace) { const id = workspace.id; const existingWorkspace = this.getById(id); if (existingWorkspace) { @@ -66,7 +66,7 @@ export class WorkspaceStore extends BaseStore { } @action - public removeWorkspace(id: WorkspaceId) { + removeWorkspace(id: WorkspaceId) { const workspace = this.getById(id); if (!workspace) return; if (this.isDefault(id)) { diff --git a/src/renderer/_vue/components/AddWorkspacePage.vue b/src/renderer/_vue/components/AddWorkspacePage.vue deleted file mode 100644 index c3794b934a..0000000000 --- a/src/renderer/_vue/components/AddWorkspacePage.vue +++ /dev/null @@ -1,115 +0,0 @@ - - - - - diff --git a/src/renderer/_vue/components/EditWorkspacePage.vue b/src/renderer/_vue/components/EditWorkspacePage.vue deleted file mode 100644 index 8fc40c93d6..0000000000 --- a/src/renderer/_vue/components/EditWorkspacePage.vue +++ /dev/null @@ -1,125 +0,0 @@ - - - - - diff --git a/src/renderer/_vue/components/WorkspacesPage.vue b/src/renderer/_vue/components/WorkspacesPage.vue deleted file mode 100644 index d8311c7020..0000000000 --- a/src/renderer/_vue/components/WorkspacesPage.vue +++ /dev/null @@ -1,156 +0,0 @@ - - - - - diff --git a/src/renderer/components/+workspaces/workspaces.scss b/src/renderer/components/+workspaces/workspaces.scss index 7b636036db..95c036c304 100644 --- a/src/renderer/components/+workspaces/workspaces.scss +++ b/src/renderer/components/+workspaces/workspaces.scss @@ -1,3 +1,14 @@ .Workspaces { + .workspace { + --flex-gap: #{$padding}; + padding: $padding / 2; + &.default { + font-style: italic; + } + + > .description { + flex: 1; + } + } } \ No newline at end of file diff --git a/src/renderer/components/+workspaces/workspaces.tsx b/src/renderer/components/+workspaces/workspaces.tsx index 5e7dc3089d..5e6b8cf72b 100644 --- a/src/renderer/components/+workspaces/workspaces.tsx +++ b/src/renderer/components/+workspaces/workspaces.tsx @@ -1,43 +1,173 @@ import "./workspaces.scss" import React, { Fragment } from "react"; import { observer } from "mobx-react"; -import { Trans } from "@lingui/macro"; +import { computed, observable, toJS } from "mobx"; +import { t, Trans } from "@lingui/macro"; import { WizardLayout } from "../layout/wizard-layout"; -import { workspaceStore } from "../../../common/workspace-store"; +import { Workspace, WorkspaceId, workspaceStore } from "../../../common/workspace-store"; +import { v4 as uuid } from "uuid" +import { _i18n } from "../../i18n"; +import { ConfirmDialog } from "../confirm-dialog"; +import { Icon } from "../icon"; +import { Input } from "../input"; +import { cssNames, prevDefault } from "../../utils"; +import { Button } from "../button"; @observer export class Workspaces extends React.Component { + @observable editingWorkspaces = observable.map(); + + @computed get workspaces(): Workspace[] { + const allWorkspaces = new Map([ + ...workspaceStore.workspaces, + ...this.editingWorkspaces, + ]); + return Array.from(allWorkspaces.values()); + } + renderInfo() { return (

    What is a Workspace?

    -

    Workspaces are used to organize number of clusters into logical groups.

    -

    A single workspaces contains a list of clusters and their full configuration.

    +

    + Workspaces are used to organize number of clusters into logical groups. +

    +

    + A single workspaces contains a list of clusters and their full configuration. +

    ) } + saveWorkspace = (id: WorkspaceId) => { + const draft = toJS(this.editingWorkspaces.get(id)); + if (draft) { + this.clearEditing(id); + workspaceStore.saveWorkspace(draft); + } + } + addWorkspace = () => { - console.log('add workspace') + const workspaceId = uuid(); + this.editingWorkspaces.set(workspaceId, { + id: workspaceId, + name: _i18n._(t`My Workspace`), + description: "", + }) + } + + editWorkspace = (id: WorkspaceId) => { + const workspace = workspaceStore.getById(id); + this.editingWorkspaces.set(id, toJS(workspace)); + } + + clearEditing = (id: WorkspaceId) => { + this.editingWorkspaces.delete(id); + } + + removeWorkspace = (id: WorkspaceId) => { + const workspace = workspaceStore.getById(id); + ConfirmDialog.open({ + okButtonProps: { + label: _i18n._(t`Remove Workspace`), + primary: false, + accent: true, + }, + ok: () => { + this.clearEditing(id); + workspaceStore.removeWorkspace(id); + }, + message: ( +
    +

    + Are you sure you want remove workspace {workspace.name}? +

    +

    + All clusters within workspace will be cleared as well +

    +
    + ), + }) } render() { - const { workspacesList, currentWorkspace } = workspaceStore; return (

    Workspaces

    -
    - {workspacesList.map(({ id: workspaceId, name, description }) => { +
    + {this.workspaces.map(({ id: workspaceId, name, description }) => { + const isActive = workspaceStore.currentWorkspaceId === workspaceId; + const isDefault = workspaceStore.isDefault(workspaceId); + const isEditing = this.editingWorkspaces.has(workspaceId); + const editingWorkspace = this.editingWorkspaces.get(workspaceId); + const className = cssNames("workspace flex gaps align-center", { + active: isActive, + editing: isEditing, + default: isDefault, + }); return ( -
    - {name} - {description} +
    + {!isEditing && ( + + + workspaceStore.setActive(workspaceId))}>{name} + {isActive && (current)} + + {description} + {!isDefault && ( + + Edit} + onClick={() => this.editWorkspace(workspaceId)} + /> + Delete} + onClick={() => this.removeWorkspace(workspaceId)} + /> + + )} + + )} + {isEditing && ( + + editingWorkspace.name = v} + /> + editingWorkspace.description = v} + /> + Cancel} + onClick={() => this.clearEditing(workspaceId)} + /> + Save} + onClick={() => this.saveWorkspace(workspaceId)} + /> + + )}
    ) })}
    +
    From 072895b371ed5f362a9321e452c2f66b0e3670e4 Mon Sep 17 00:00:00 2001 From: Roman Date: Wed, 22 Jul 2020 17:10:37 +0300 Subject: [PATCH 20/35] menu.ts: refactoring and fixes, reuse route paths from views (renderer) Signed-off-by: Roman --- src/common/ipc.ts | 22 +- src/main/index.ts | 4 +- src/main/menu.ts | 203 +++++++----------- .../components/+whats-new/whats-new.tsx | 2 +- src/renderer/components/drawer/drawer.tsx | 8 +- src/renderer/index.tsx | 4 +- src/renderer/navigation.ts | 14 +- 7 files changed, 118 insertions(+), 139 deletions(-) diff --git a/src/common/ipc.ts b/src/common/ipc.ts index 96a0157358..db65b00e6a 100644 --- a/src/common/ipc.ts +++ b/src/common/ipc.ts @@ -69,16 +69,24 @@ export function handleIpc(channel: IpcChannel, handler: IpcMessageHandler, optio export interface IpcPairOptions { channel: IpcChannel handle?: IpcMessageHandler - options?: IpcHandleOpts + autoBind?: boolean; + timeout?: number; } -export function createIpcChannel({ channel, ...initOpts }: IpcPairOptions) { +// 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 { - handleInMain: (opts: Partial> = {}) => { - const { handle = initOpts.handle, options = initOpts.options } = opts; - return handleIpc(channel, handle, options); - }, - invokeFromRenderer: (...args: any[]) => { + channel: channel, + handleInMain: bindHandler, + invokeFromRenderer(...args: any[]) { return invokeIpc(channel, ...args); }, } diff --git a/src/main/index.ts b/src/main/index.ts index fc5dcade85..a897b1ac9e 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -5,7 +5,7 @@ import "../common/prometheus-providers" import { app, dialog } from "electron" import { appName, appProto, staticDir, staticProto } from "../common/vars"; import path from "path" -import initMenu from "./menu" +import { initMenu } from "./menu" import { LensProxy } from "./lens-proxy" import { WindowManager } from "./window-manager"; import { ClusterManager } from "./cluster-manager"; @@ -41,7 +41,6 @@ async function main() { const updater = new AppUpdater() updater.start(); - initMenu(); registerFileProtocol(appProto, app.getPath("userData")); registerFileProtocol(staticProto, staticDir); @@ -76,6 +75,7 @@ async function main() { // create window manager and open app windowManager = new WindowManager(proxyPort); windowManager.showSplash(); + initMenu(windowManager); } app.on("ready", main); diff --git a/src/main/menu.ts b/src/main/menu.ts index f35abf5362..0bab5ed617 100644 --- a/src/main/menu.ts +++ b/src/main/menu.ts @@ -1,113 +1,71 @@ +import type { WindowManager } from "./window-manager"; import { app, BrowserWindow, dialog, Menu, MenuItem, MenuItemConstructorOptions, shell, webContents } from "electron" +import { broadcastIpc } from "../common/ipc"; import { appName, isMac, issuesTrackerUrl, isWindows, slackUrl } from "../common/vars"; +import { clusterStore } from "../common/cluster-store"; +import { addClusterURL } from "../renderer/components/+add-cluster/add-cluster.route"; +import { preferencesURL } from "../renderer/components/+preferences/preferences.route"; +import { whatsNewURL } from "../renderer/components/+whats-new/whats-new.route"; +import { clusterSettingsURL } from "../renderer/components/+cluster-settings/cluster-settings.route"; -// todo: refactor + split menu sections to separated files, e.g. menus/file.menu.ts +export function initMenu(windowManager: WindowManager) { + const menuItems: MenuItemConstructorOptions[] = []; -export interface MenuOptions { - logoutHook: any; - addClusterHook: any; - clusterSettingsHook: any; - showWhatsNewHook: any; - showPreferencesHook: any; - // all the above are really () => void type functions -} + function navigate(url: string) { + const activeClusterId = clusterStore.activeClusterId; + const view = windowManager.getView(activeClusterId); + if (view) { + broadcastIpc({ + channel: "menu:navigate", + webContentId: view.id, + args: [url], + }); + } + } -function setClusterSettingsEnabled(enabled: boolean) { - const menuIndex = isMac ? 1 : 0 - Menu.getApplicationMenu().items[menuIndex].submenu.items[1].enabled = enabled -} + function macOnly(menuItems: MenuItemConstructorOptions[]): MenuItemConstructorOptions[] { + if (!isMac) return []; + return menuItems; + } -export function showAbout(_menuitem: MenuItem, browserWindow: BrowserWindow) { - const appInfo = [ - `${appName}: ${app.getVersion()}`, - `Electron: ${process.versions.electron}`, - `Chrome: ${process.versions.chrome}`, - `Copyright 2020 Lakend Labs, Inc.`, - ] - dialog.showMessageBoxSync(browserWindow, { - title: `${isWindows ? " ".repeat(2) : ""}${appName}`, - type: "info", - buttons: ["Close"], - message: `Lens`, - detail: appInfo.join("\r\n") - }) -} - -/** - * Constructs the menu based on the example at: https://electronjs.org/docs/api/menu#main-process - * Menu items are constructed piece-by-piece to have slightly better control on individual sub-menus - */ -export default function initMenu(opts: Partial = {}) { - const mt: MenuItemConstructorOptions[] = []; - const macAppMenu: MenuItemConstructorOptions = { - label: app.getName(), + // "File" submenu + menuItems.push({ + label: isMac ? app.getName() : "File", submenu: [ { - label: "About Lens", - click: showAbout + label: 'Add Cluster', + click() { + navigate(addClusterURL()) + } + }, + { + label: 'Cluster Settings', + click() { + navigate(clusterSettingsURL()) + } }, { type: 'separator' }, { label: 'Preferences', - click: opts.showPreferencesHook, - enabled: true + click() { + navigate(preferencesURL()) + } }, - { type: 'separator' }, - { role: 'services' }, - { type: 'separator' }, - { role: 'hide' }, - { role: 'hideOthers' }, - { role: 'unhide' }, + ...macOnly([ + { type: 'separator' }, + { role: 'services' }, + { type: 'separator' }, + { role: 'hide' }, + { role: 'hideOthers' }, + { role: 'unhide' }, + ]), { type: 'separator' }, { role: 'quit' } ] - }; - if (isMac) { - mt.push(macAppMenu); - } + }); - let fileMenu: MenuItemConstructorOptions; - if (isMac) { - fileMenu = { - label: 'File', - submenu: [{ - label: 'Add Cluster...', - click: opts.addClusterHook, - }, - { - label: 'Cluster Settings', - click: opts.clusterSettingsHook, - enabled: false - } - ] - } - } else { - fileMenu = { - label: 'File', - submenu: [ - { - label: 'Add Cluster...', - click: opts.addClusterHook, - }, - { - label: 'Cluster Settings', - click: opts.clusterSettingsHook, - enabled: false - }, - { type: 'separator' }, - { - label: 'Preferences', - click: opts.showPreferencesHook, - enabled: true - }, - { type: 'separator' }, - { role: 'quit' } - ] - } - } - mt.push(fileMenu); - - const editMenu: MenuItemConstructorOptions = { + // "Edit" submenu + menuItems.push({ label: 'Edit', submenu: [ { role: 'undo' }, @@ -120,10 +78,10 @@ export default function initMenu(opts: Partial = {}) { { type: 'separator' }, { role: 'selectAll' }, ] - }; - mt.push(editMenu); + }); - const viewMenu: MenuItemConstructorOptions = { + // "View" submenu + menuItems.push({ label: 'View', submenu: [ { @@ -155,20 +113,26 @@ export default function initMenu(opts: Partial = {}) { { type: 'separator' }, { role: 'togglefullscreen' } ] - }; - mt.push(viewMenu); + }) - const helpMenu: MenuItemConstructorOptions = { + // "Help" submenu + menuItems.push({ role: 'help', submenu: [ { - label: 'License', + label: "What's new?", + click() { + navigate(whatsNewURL()) + }, + }, + { + label: "License", click: async () => { shell.openExternal('https://lakendlabs.com/licenses/lens-eula.md'); }, }, { - label: 'Community Slack', + label: "Community Slack", click: async () => { shell.openExternal(slackUrl); }, @@ -180,27 +144,26 @@ export default function initMenu(opts: Partial = {}) { }, }, { - label: "What's new?", - click: opts.showWhatsNewHook, - }, - ...(!isMac ? [{ label: "About Lens", - click: showAbout - } as MenuItemConstructorOptions] : []) + click(menuItem: MenuItem, browserWindow: BrowserWindow) { + const appInfo = [ + `${appName}: ${app.getVersion()}`, + `Electron: ${process.versions.electron}`, + `Chrome: ${process.versions.chrome}`, + `Copyright 2020 Lakend Labs, Inc.`, + ] + dialog.showMessageBoxSync(browserWindow, { + title: `${isWindows ? " ".repeat(2) : ""}${appName}`, + type: "info", + buttons: ["Close"], + message: `Lens`, + detail: appInfo.join("\r\n") + }) + } + } ] - }; - mt.push(helpMenu); + }); - const menu = Menu.buildFromTemplate(mt); + const menu = Menu.buildFromTemplate(menuItems); Menu.setApplicationMenu(menu); - - // const promiseIpc = new PromiseIpc({ timeout: 2000 }) - // - // promiseIpc.on("enableClusterSettingsMenuItem", (clusterId: string) => { - // setClusterSettingsEnabled(true) - // }); - // - // promiseIpc.on("disableClusterSettingsMenuItem", () => { - // setClusterSettingsEnabled(false) - // }); } diff --git a/src/renderer/components/+whats-new/whats-new.tsx b/src/renderer/components/+whats-new/whats-new.tsx index 1fcd0691d9..9bde7da141 100644 --- a/src/renderer/components/+whats-new/whats-new.tsx +++ b/src/renderer/components/+whats-new/whats-new.tsx @@ -33,7 +33,7 @@ export class WhatsNew extends React.Component {
    From ce54fc645df50be5f7b673a06312362b027374ef Mon Sep 17 00:00:00 2001 From: Roman Date: Sat, 25 Jul 2020 17:46:06 +0300 Subject: [PATCH 34/35] DCO-bot check fix Signed-off-by: Roman --- src/main/cluster.ts | 2 +- src/main/index.ts | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/main/cluster.ts b/src/main/cluster.ts index 38ddb79577..8e399375e3 100644 --- a/src/main/cluster.ts +++ b/src/main/cluster.ts @@ -417,7 +417,7 @@ export class Cluster implements ClusterModel { } protected async getAllowedResources() { - // TODO: auto-populate all resources dynamically + // todo: auto-populate all resources dynamically (e.g. kubectl api-resources -o=wide -v=7) const apiResources = [ { resource: "configmaps" }, { resource: "cronjobs", group: "batch" }, diff --git a/src/main/index.ts b/src/main/index.ts index 6ca9a78be0..3da443ae8b 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -81,7 +81,8 @@ app.on("ready", main); app.on("will-quit", async (event) => { event.preventDefault(); // To allow mixpanel sending to be executed - if (clusterManager) clusterManager.stop() if (proxyServer) proxyServer.close() + if (clusterManager) clusterManager.stop() + if (windowManager) windowManager.destroy() app.exit(); }) From 4eec23689bbe99784e790214acc0839e374f399f Mon Sep 17 00:00:00 2001 From: Roman Date: Mon, 27 Jul 2020 15:00:47 +0300 Subject: [PATCH 35/35] base-store clean up Signed-off-by: Roman Signed-off-by: Sebastian Malton --- src/common/base-store.ts | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/common/base-store.ts b/src/common/base-store.ts index 54a633e517..722698fa67 100644 --- a/src/common/base-store.ts +++ b/src/common/base-store.ts @@ -56,14 +56,10 @@ export class BaseStore extends Singleton { ...confOptions, projectName: "lens", projectVersion: getAppVersion(), - get cwd() { - return (app || remote.app).getPath("userData"); // todo: remove usage of remote.app (deprecated) - }, + cwd: (app || remote.app).getPath("userData"), }); - const storedModel = Object.assign({}, this.storeConfig.store); - Reflect.deleteProperty(storedModel, "__internal__"); // todo: avoid "external-internals" logger.info(`[STORE]: LOADED from ${this.storeConfig.path}`); - this.fromStore(storedModel); + this.fromStore(this.storeConfig.store); this.isLoaded = true; }