diff --git a/Makefile b/Makefile index 1df15b4857..540a8ca0e4 100644 --- a/Makefile +++ b/Makefile @@ -56,7 +56,13 @@ else endif build-extensions: - $(foreach file, $(wildcard $(EXTENSIONS_DIR)/*), $(MAKE) -C $(file) build;) + $(foreach dir, $(wildcard $(EXTENSIONS_DIR)/*), $(MAKE) -C $(dir) build;) + +build-npm: + yarn npm:fix-package-version + +publish-npm: build-npm + cd src/extensions/npm/extensions && npm publish clean: ifeq "$(DETECTED_OS)" "Windows" diff --git a/README.md b/README.md index 149706c99f..b403492e72 100644 --- a/README.md +++ b/README.md @@ -40,9 +40,10 @@ brew cask install lens Allows for faster separate re-runs of some of the more involved processes: -1. `yarn dev:main` compiles electron's main process part and start watching files -1. `yarn dev:renderer` compiles electron's renderer part and start watching files -1. `yarn dev-run` runs app in dev-mode and restarts when electron's main process file has changed +1. `yarn dev:main` compiles electron's main process app part +1. `yarn dev:renderer` compiles electron's renderer app part +1. `yarn dev:extension-types` compile declaration types for `@k8slens/extensions` +1. `yarn dev-run` runs app in dev-mode and auto-restart when main process file has changed ## Developer's ~~RTFM~~ recommended list: diff --git a/build/build_tray_icon.ts b/build/build_tray_icon.ts new file mode 100644 index 0000000000..00d09b69bc --- /dev/null +++ b/build/build_tray_icon.ts @@ -0,0 +1,51 @@ +// Generate tray icons from SVG to PNG + different sizes and colors (B&W) +// Command: `yarn build:tray-icons` +import path from "path" +import sharp from "sharp"; +import jsdom from "jsdom" +import fs from "fs-extra" + +export async function generateTrayIcon( + { + outputFilename = "tray_icon", // e.g. output tray_icon_dark@2x.png + svgIconPath = path.resolve(__dirname, "../src/renderer/components/icon/logo-lens.svg"), + outputFolder = path.resolve(__dirname, "./tray"), + dpiSuffix = "2x", + pixelSize = 32, + shouldUseDarkColors = false, // managed by electron.nativeTheme.shouldUseDarkColors + } = {}) { + outputFilename += shouldUseDarkColors ? "_dark" : "" + dpiSuffix = dpiSuffix !== "1x" ? `@${dpiSuffix}` : "" + const pngIconDestPath = path.resolve(outputFolder, `${outputFilename}${dpiSuffix}.png`) + try { + // Modify .SVG colors + const trayIconColor = shouldUseDarkColors ? "white" : "black"; + const svgDom = await jsdom.JSDOM.fromFile(svgIconPath); + const svgRoot = svgDom.window.document.body.getElementsByTagName("svg")[0]; + svgRoot.innerHTML += `` + const svgIconBuffer = Buffer.from(svgRoot.outerHTML); + + // Resize and convert to .PNG + const pngIconBuffer: Buffer = await sharp(svgIconBuffer) + .resize({ width: pixelSize, height: pixelSize }) + .png() + .toBuffer(); + + // Save icon + await fs.writeFile(pngIconDestPath, pngIconBuffer); + console.info(`[DONE]: Tray icon saved at "${pngIconDestPath}"`); + } catch (err) { + console.error(`[ERROR]: ${err}`); + } +} + +// Run +const iconSizes: Record = { + "1x": 16, + "2x": 32, + "3x": 48, +}; +Object.entries(iconSizes).forEach(([dpiSuffix, pixelSize]) => { + generateTrayIcon({ dpiSuffix, pixelSize, shouldUseDarkColors: false }); + generateTrayIcon({ dpiSuffix, pixelSize, shouldUseDarkColors: true }); +}); diff --git a/build/set_npm_version.ts b/build/set_npm_version.ts new file mode 100644 index 0000000000..34a11da6c9 --- /dev/null +++ b/build/set_npm_version.ts @@ -0,0 +1,9 @@ +import * as fs from "fs" +import * as path from "path" +import packageInfo from "../src/extensions/npm/extensions/package.json" +import appInfo from "../package.json" + +const packagePath = path.join(__dirname, "../src/extensions/npm/extensions/package.json") + +packageInfo.version = appInfo.version +fs.writeFileSync(packagePath, JSON.stringify(packageInfo, null, 2)) diff --git a/build/tray/tray_icon.png b/build/tray/tray_icon.png new file mode 100644 index 0000000000..73c7346d33 Binary files /dev/null and b/build/tray/tray_icon.png differ diff --git a/build/tray/tray_icon@2x.png b/build/tray/tray_icon@2x.png new file mode 100644 index 0000000000..71206802ac Binary files /dev/null and b/build/tray/tray_icon@2x.png differ diff --git a/build/tray/tray_icon@3x.png b/build/tray/tray_icon@3x.png new file mode 100644 index 0000000000..a293ba7d32 Binary files /dev/null and b/build/tray/tray_icon@3x.png differ diff --git a/build/tray/tray_icon_dark.png b/build/tray/tray_icon_dark.png new file mode 100644 index 0000000000..568d13e00b Binary files /dev/null and b/build/tray/tray_icon_dark.png differ diff --git a/build/tray/tray_icon_dark@2x.png b/build/tray/tray_icon_dark@2x.png new file mode 100644 index 0000000000..3f28605dbf Binary files /dev/null and b/build/tray/tray_icon_dark@2x.png differ diff --git a/build/tray/tray_icon_dark@3x.png b/build/tray/tray_icon_dark@3x.png new file mode 100644 index 0000000000..5e682a5d82 Binary files /dev/null and b/build/tray/tray_icon_dark@3x.png differ diff --git a/extensions/example-extension/package-lock.json b/extensions/example-extension/package-lock.json index 7c99096fb9..35be0e93e0 100644 --- a/extensions/example-extension/package-lock.json +++ b/extensions/example-extension/package-lock.json @@ -4,6 +4,10 @@ "lockfileVersion": 1, "requires": true, "dependencies": { + "@k8slens/extensions": { + "version": "file:../../src/extensions/npm/extensions", + "dev": true + }, "@webassemblyjs/ast": { "version": "1.9.0", "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.9.0.tgz", diff --git a/extensions/example-extension/package.json b/extensions/example-extension/package.json index 86d5397845..a91b332f38 100644 --- a/extensions/example-extension/package.json +++ b/extensions/example-extension/package.json @@ -16,6 +16,7 @@ "react-open-doodles": "^1.0.5" }, "devDependencies": { + "@k8slens/extensions": "file:../../src/extensions/npm/extensions", "ts-loader": "^8.0.4", "typescript": "^4.0.3", "webpack": "^4.44.2" diff --git a/extensions/example-extension/tsconfig.json b/extensions/example-extension/tsconfig.json index ac83e008db..a93ad6fe9f 100644 --- a/extensions/example-extension/tsconfig.json +++ b/extensions/example-extension/tsconfig.json @@ -16,7 +16,6 @@ "jsx": "react" }, "include": [ - "../../src/extensions/npm/**/*.d.ts", "./*.ts", "./*.tsx" ], diff --git a/extensions/metrics-cluster-feature/package-lock.json b/extensions/metrics-cluster-feature/package-lock.json index 253f75a79a..509c1bc6a5 100644 --- a/extensions/metrics-cluster-feature/package-lock.json +++ b/extensions/metrics-cluster-feature/package-lock.json @@ -4,6 +4,10 @@ "lockfileVersion": 1, "requires": true, "dependencies": { + "@k8slens/extensions": { + "version": "file:../../src/extensions/npm/extensions", + "dev": true + }, "@webassemblyjs/ast": { "version": "1.9.0", "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.9.0.tgz", diff --git a/extensions/metrics-cluster-feature/package.json b/extensions/metrics-cluster-feature/package.json index 3968e59527..0843156f0a 100644 --- a/extensions/metrics-cluster-feature/package.json +++ b/extensions/metrics-cluster-feature/package.json @@ -15,6 +15,7 @@ "semver": "^7.3.2" }, "devDependencies": { + "@k8slens/extensions": "file:../../src/extensions/npm/extensions", "ts-loader": "^8.0.4", "typescript": "^4.0.3", "webpack": "^4.44.2", diff --git a/extensions/metrics-cluster-feature/tsconfig.json b/extensions/metrics-cluster-feature/tsconfig.json index ac83e008db..a93ad6fe9f 100644 --- a/extensions/metrics-cluster-feature/tsconfig.json +++ b/extensions/metrics-cluster-feature/tsconfig.json @@ -16,7 +16,6 @@ "jsx": "react" }, "include": [ - "../../src/extensions/npm/**/*.d.ts", "./*.ts", "./*.tsx" ], diff --git a/extensions/node-menu/package-lock.json b/extensions/node-menu/package-lock.json index 10e12774c6..0165b993be 100644 --- a/extensions/node-menu/package-lock.json +++ b/extensions/node-menu/package-lock.json @@ -4,6 +4,10 @@ "lockfileVersion": 1, "requires": true, "dependencies": { + "@k8slens/extensions": { + "version": "file:../../src/extensions/npm/extensions", + "dev": true + }, "@webassemblyjs/ast": { "version": "1.9.0", "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.9.0.tgz", diff --git a/extensions/node-menu/package.json b/extensions/node-menu/package.json index 51bcccb1da..c183316a78 100644 --- a/extensions/node-menu/package.json +++ b/extensions/node-menu/package.json @@ -13,6 +13,7 @@ }, "dependencies": {}, "devDependencies": { + "@k8slens/extensions": "file:../../src/extensions/npm/extensions", "ts-loader": "^8.0.4", "typescript": "^4.0.3", "webpack": "^4.44.2", diff --git a/extensions/node-menu/tsconfig.json b/extensions/node-menu/tsconfig.json index ac83e008db..a93ad6fe9f 100644 --- a/extensions/node-menu/tsconfig.json +++ b/extensions/node-menu/tsconfig.json @@ -16,7 +16,6 @@ "jsx": "react" }, "include": [ - "../../src/extensions/npm/**/*.d.ts", "./*.ts", "./*.tsx" ], diff --git a/extensions/pod-menu/package-lock.json b/extensions/pod-menu/package-lock.json index 7d91be00e1..52cca65d73 100644 --- a/extensions/pod-menu/package-lock.json +++ b/extensions/pod-menu/package-lock.json @@ -4,6 +4,10 @@ "lockfileVersion": 1, "requires": true, "dependencies": { + "@k8slens/extensions": { + "version": "file:../../src/extensions/npm/extensions", + "dev": true + }, "@webassemblyjs/ast": { "version": "1.9.0", "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.9.0.tgz", diff --git a/extensions/pod-menu/package.json b/extensions/pod-menu/package.json index 3925c47cb6..21b4f3d65b 100644 --- a/extensions/pod-menu/package.json +++ b/extensions/pod-menu/package.json @@ -17,6 +17,7 @@ "typescript": "^4.0.3", "webpack": "^4.44.2", "mobx": "^5.15.5", - "react": "^16.13.1" + "react": "^16.13.1", + "@k8slens/extensions": "file:../../src/extensions/npm/extensions" } } diff --git a/extensions/pod-menu/src/logs-menu.tsx b/extensions/pod-menu/src/logs-menu.tsx index 7ea6056caa..f80c55cc01 100644 --- a/extensions/pod-menu/src/logs-menu.tsx +++ b/extensions/pod-menu/src/logs-menu.tsx @@ -15,7 +15,6 @@ export class PodLogsMenu extends React.Component { selectedContainer: container, showTimestamps: false, previous: false, - tailLines: 1000 }); } diff --git a/extensions/pod-menu/src/shell-menu.tsx b/extensions/pod-menu/src/shell-menu.tsx index d16188c6e7..772d17a894 100644 --- a/extensions/pod-menu/src/shell-menu.tsx +++ b/extensions/pod-menu/src/shell-menu.tsx @@ -33,7 +33,6 @@ export class PodShellMenu extends React.Component { render() { const { object, toolbar } = this.props - console.log(Object.keys(object)) const containers = object.getRunningContainers(); if (!containers.length) return; return ( diff --git a/extensions/pod-menu/tsconfig.json b/extensions/pod-menu/tsconfig.json index ac83e008db..a93ad6fe9f 100644 --- a/extensions/pod-menu/tsconfig.json +++ b/extensions/pod-menu/tsconfig.json @@ -16,7 +16,6 @@ "jsx": "react" }, "include": [ - "../../src/extensions/npm/**/*.d.ts", "./*.ts", "./*.tsx" ], diff --git a/extensions/support-page/main.ts b/extensions/support-page/main.ts index af30ebf0cd..70ec3bc026 100644 --- a/extensions/support-page/main.ts +++ b/extensions/support-page/main.ts @@ -7,10 +7,7 @@ export default class SupportPageMainExtension extends LensMainExtension { parentId: "help", label: "Support", click() { - windowManager.navigate({ - channel: "menu:navigate", // fixme: use windowManager.ensureMainWindow from Tray's PR - url: supportPageURL(), - }); + windowManager.navigate(supportPageURL()); } } ] diff --git a/extensions/support-page/package-lock.json b/extensions/support-page/package-lock.json index 7a6d1a3f77..029ee4a2eb 100644 --- a/extensions/support-page/package-lock.json +++ b/extensions/support-page/package-lock.json @@ -4,6 +4,10 @@ "lockfileVersion": 1, "requires": true, "dependencies": { + "@k8slens/extensions": { + "version": "file:../../src/extensions/npm/extensions", + "dev": true + }, "@types/anymatch": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/@types/anymatch/-/anymatch-1.3.1.tgz", diff --git a/extensions/support-page/package.json b/extensions/support-page/package.json index 6568ee3fda..2ed40c6fa8 100644 --- a/extensions/support-page/package.json +++ b/extensions/support-page/package.json @@ -14,6 +14,7 @@ "@types/react": "^16.9.53", "@types/react-router": "^5.1.8", "@types/webpack": "^4.41.17", + "@k8slens/extensions": "file:../../src/extensions/npm/extensions", "mobx": "^5.15.5", "react": "^16.13.1", "ts-loader": "^8.0.4", diff --git a/extensions/support-page/src/support.tsx b/extensions/support-page/src/support.tsx index 4f189286f1..37c42021b2 100644 --- a/extensions/support-page/src/support.tsx +++ b/extensions/support-page/src/support.tsx @@ -3,13 +3,13 @@ import React from "react" import { observer } from "mobx-react" -import { CommonVars, Component } from "@k8slens/extensions"; +import { App, Component } from "@k8slens/extensions"; @observer export class Support extends React.Component { render() { const { PageLayout } = Component; - const { slackUrl, issuesTrackerUrl } = CommonVars; + const { slackUrl, issuesTrackerUrl } = App; return ( Support}>

Community Slack Channel

diff --git a/extensions/support-page/tsconfig.json b/extensions/support-page/tsconfig.json index 966b29e183..18ebb2dceb 100644 --- a/extensions/support-page/tsconfig.json +++ b/extensions/support-page/tsconfig.json @@ -24,7 +24,6 @@ }, "include": [ "renderer.tsx", - "../../src/extensions/npm/**/*.d.ts", "src/**/*" ] } diff --git a/extensions/telemetry/package-lock.json b/extensions/telemetry/package-lock.json index 9c02993621..f0d291c1b3 100644 --- a/extensions/telemetry/package-lock.json +++ b/extensions/telemetry/package-lock.json @@ -4,6 +4,10 @@ "lockfileVersion": 1, "requires": true, "dependencies": { + "@k8slens/extensions": { + "version": "file:../../src/extensions/npm/extensions", + "dev": true + }, "@webassemblyjs/ast": { "version": "1.9.0", "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.9.0.tgz", diff --git a/extensions/telemetry/package.json b/extensions/telemetry/package.json index 238a69b454..4fa6abbca9 100644 --- a/extensions/telemetry/package.json +++ b/extensions/telemetry/package.json @@ -14,6 +14,7 @@ }, "dependencies": {}, "devDependencies": { + "@k8slens/extensions": "file:../../src/extensions/npm/extensions", "ts-loader": "^8.0.4", "typescript": "^4.0.3", "webpack": "^4.44.2", diff --git a/extensions/telemetry/src/tracker.ts b/extensions/telemetry/src/tracker.ts index ed23253bd6..d5f16334b2 100644 --- a/extensions/telemetry/src/tracker.ts +++ b/extensions/telemetry/src/tracker.ts @@ -1,4 +1,4 @@ -import { EventBus, Util } from "@k8slens/extensions" +import { EventBus, Util, Store, App } from "@k8slens/extensions" import ua from "universal-analytics" import { machineIdSync } from "node-machine-id" import { telemetryPreferencesStore } from "./telemetry-preferences-store" @@ -15,6 +15,8 @@ export class Tracker extends Util.Singleton { protected locale: string; protected electronUA: string; + protected reportInterval: NodeJS.Timeout + private constructor() { super(); try { @@ -23,7 +25,7 @@ export class Tracker extends Util.Singleton { this.visitor = ua(Tracker.GA_ID) } this.visitor.set("dl", "https://telemetry.k8slens.dev") - + this.visitor.set("ua", `Lens ${App.version} (${this.getOS()})`) } start() { @@ -36,6 +38,8 @@ export class Tracker extends Util.Singleton { } this.eventHandlers.push(handler) EventBus.appEventBus.addListener(handler) + + this.reportInterval = setInterval(this.reportData, 60 * 60 * 1000) // report every 1h } stop() { @@ -46,12 +50,59 @@ export class Tracker extends Util.Singleton { for (const handler of this.eventHandlers) { EventBus.appEventBus.removeListener(handler) } + if (this.reportInterval) { + clearInterval(this.reportInterval) + } } protected async isTelemetryAllowed(): Promise { return telemetryPreferencesStore.enabled } + protected reportData() { + const clustersList = Store.clusterStore.clustersList + + this.event("generic-data", "report", { + appVersion: App.version, + clustersCount: clustersList.length, + workspacesCount: Store.workspaceStore.workspacesList.length + }) + + clustersList.forEach((cluster) => { + if (!cluster?.metadata.lastSeen) { return } + this.reportClusterData(cluster) + }) + } + + protected reportClusterData(cluster: Store.ClusterModel) { + this.event("cluster-data", "report", { + id: cluster.metadata.id, + kubernetesVersion: cluster.metadata.version, + distribution: cluster.metadata.distribution, + nodesCount: cluster.metadata.nodes, + lastSeen: cluster.metadata.lastSeen + }) + } + + protected getOS() { + let os = "" + if (App.isMac) { + os = "MacOS" + } else if(App.isWindows) { + os = "Windows" + } else if (App.isLinux) { + os = "Linux" + if (App.isSnap) { + os += "; Snap" + } else { + os += "; AppImage" + } + } else { + os = "Unknown" + } + return os + } + protected async event(eventCategory: string, eventAction: string, otherParams = {}) { try { const allowed = await this.isTelemetryAllowed(); diff --git a/extensions/telemetry/tsconfig.json b/extensions/telemetry/tsconfig.json index c32d3f0b74..05c3fa6671 100644 --- a/extensions/telemetry/tsconfig.json +++ b/extensions/telemetry/tsconfig.json @@ -24,7 +24,6 @@ }, "include": [ "renderer.ts", - "../../src/extensions/npm/**/*.d.ts", "src/**/*" ] } diff --git a/integration/__tests__/app.tests.ts b/integration/__tests__/app.tests.ts index 492f02d1e6..c9567a195f 100644 --- a/integration/__tests__/app.tests.ts +++ b/integration/__tests__/app.tests.ts @@ -166,8 +166,8 @@ describe("Lens integration tests", () => { pages: [{ name: "Cluster", href: "cluster", - expectedSelector: "div.ClusterNoMetrics p", - expectedText: "Metrics are not available due" + expectedSelector: "div.Cluster div.label", + expectedText: "Master" }] }, { @@ -389,13 +389,13 @@ describe("Lens integration tests", () => { it(`shows ${drawer} drawer`, async () => { expect(clusterAdded).toBe(true) await app.client.click(`.sidebar-nav #${drawerId} span.link-text`) - await app.client.waitUntilTextExists(`a[href="/${pages[0].href}"]`, pages[0].name) + await app.client.waitUntilTextExists(`a[href^="/${pages[0].href}"]`, pages[0].name) }) } pages.forEach(({ name, href, expectedSelector, expectedText }) => { it(`shows ${drawer}->${name} page`, async () => { expect(clusterAdded).toBe(true) - await app.client.click(`a[href="/${href}"]`) + await app.client.click(`a[href^="/${href}"]`) await app.client.waitUntilTextExists(expectedSelector, expectedText) }) }) @@ -404,7 +404,7 @@ describe("Lens integration tests", () => { it(`hides ${drawer} drawer`, async () => { expect(clusterAdded).toBe(true) await app.client.click(`.sidebar-nav #${drawerId} span.link-text`) - await expect(app.client.waitUntilTextExists(`a[href="/${pages[0].href}"]`, pages[0].name, 100)).rejects.toThrow() + await expect(app.client.waitUntilTextExists(`a[href^="/${pages[0].href}"]`, pages[0].name, 100)).rejects.toThrow() }) } }) @@ -440,8 +440,8 @@ describe("Lens integration tests", () => { it(`creates a pod in ${TEST_NAMESPACE} namespace`, async () => { expect(clusterAdded).toBe(true) await app.client.click(".sidebar-nav #workloads span.link-text") - await app.client.waitUntilTextExists('a[href="/pods"]', "Pods") - await app.client.click('a[href="/pods"]') + await app.client.waitUntilTextExists('a[href^="/pods"]', "Pods") + await app.client.click('a[href^="/pods"]') await app.client.waitUntilTextExists("div.TableCell", "kube-apiserver") await app.client.click('.Icon.new-dock-tab') await app.client.waitUntilTextExists("li.MenuItem.create-resource-tab", "Create resource") diff --git a/integration/helpers/utils.ts b/integration/helpers/utils.ts index 92e9ade7af..65ea9a1a78 100644 --- a/integration/helpers/utils.ts +++ b/integration/helpers/utils.ts @@ -13,7 +13,6 @@ export function setup(): Application { path: AppPaths[process.platform], startTimeout: 30000, waitTimeout: 60000, - chromeDriverArgs: ['remote-debugging-port=9222'], env: { CICD: "true" } @@ -27,6 +26,6 @@ export async function tearDown(app: Application) { try { process.kill(pid, "SIGKILL"); } catch (e) { - return + console.error(e) } } diff --git a/locales/en/messages.po b/locales/en/messages.po index 2b3980ed5c..c062c63db1 100644 --- a/locales/en/messages.po +++ b/locales/en/messages.po @@ -88,7 +88,7 @@ msgid "Active" msgstr "Active" #: src/renderer/components/+add-cluster/add-cluster.tsx:310 -#: src/renderer/components/cluster-manager/clusters-menu.tsx:130 +#: src/renderer/components/cluster-manager/clusters-menu.tsx:127 msgid "Add Cluster" msgstr "Add Cluster" @@ -219,11 +219,11 @@ msgstr "Allocatable" msgid "Allow Privilege Escalation" msgstr "Allow Privilege Escalation" -#: src/renderer/components/+preferences/preferences.tsx:169 +#: src/renderer/components/+preferences/preferences.tsx:172 msgid "Allow telemetry & usage tracking" msgstr "Allow telemetry & usage tracking" -#: src/renderer/components/+preferences/preferences.tsx:161 +#: src/renderer/components/+preferences/preferences.tsx:164 msgid "Allow untrusted Certificate Authorities" msgstr "Allow untrusted Certificate Authorities" @@ -301,6 +301,14 @@ msgstr "Associate clusters and choose the ones you want to access via quick laun msgid "Auth App Role" msgstr "Auth App Role" +#: src/renderer/components/+preferences/preferences.tsx:160 +msgid "Auto start-up" +msgstr "Auto start-up" + +#: src/renderer/components/+preferences/preferences.tsx:161 +msgid "Automatically start Lens on login" +msgstr "Automatically start Lens on login" + #: src/renderer/components/error-boundary/error-boundary.tsx:53 #: src/renderer/components/wizard/wizard.tsx:130 msgid "Back" @@ -422,7 +430,7 @@ msgstr "Cancel" msgid "Capacity" msgstr "Capacity" -#: src/renderer/components/+preferences/preferences.tsx:160 +#: src/renderer/components/+preferences/preferences.tsx:163 msgid "Certificate Trust" msgstr "Certificate Trust" @@ -817,7 +825,7 @@ msgstr "Desired Healthy" msgid "Desired number of replicas" msgstr "Desired number of replicas" -#: src/renderer/components/cluster-manager/clusters-menu.tsx:65 +#: src/renderer/components/cluster-manager/clusters-menu.tsx:62 msgid "Disconnect" msgstr "Disconnect" @@ -831,7 +839,7 @@ msgstr "Disk" msgid "Disk:" msgstr "Disk:" -#: src/renderer/components/+preferences/preferences.tsx:165 +#: src/renderer/components/+preferences/preferences.tsx:168 msgid "Does not affect cluster communications!" msgstr "Does not affect cluster communications!" @@ -927,8 +935,8 @@ msgstr "Environment" msgid "Error stack" msgstr "Error stack" -#: src/renderer/components/+add-cluster/add-cluster.tsx:89 -#: src/renderer/components/+add-cluster/add-cluster.tsx:130 +#: src/renderer/components/+add-cluster/add-cluster.tsx:88 +#: src/renderer/components/+add-cluster/add-cluster.tsx:129 msgid "Error while adding cluster(s): {0}" msgstr "Error while adding cluster(s): {0}" @@ -1581,7 +1589,7 @@ msgstr "Namespaces" msgid "Namespaces: {0}" msgstr "Namespaces: {0}" -#: src/renderer/components/+preferences/preferences.tsx:164 +#: src/renderer/components/+preferences/preferences.tsx:167 msgid "Needed with some corporate proxies that do certificate re-writing." msgstr "Needed with some corporate proxies that do certificate re-writing." @@ -1798,7 +1806,7 @@ msgstr "Persistent Volume Claims" msgid "Persistent Volumes" msgstr "Persistent Volumes" -#: src/renderer/components/+add-cluster/add-cluster.tsx:75 +#: src/renderer/components/+add-cluster/add-cluster.tsx:74 msgid "Please select at least one cluster context" msgstr "Please select at least one cluster context" @@ -2025,8 +2033,8 @@ msgstr "Releases" #: src/renderer/components/+preferences/preferences.tsx:152 #: src/renderer/components/+user-management-roles-bindings/role-binding-details.tsx:60 -#: src/renderer/components/cluster-manager/clusters-menu.tsx:76 -#: src/renderer/components/cluster-manager/clusters-menu.tsx:82 +#: src/renderer/components/cluster-manager/clusters-menu.tsx:73 +#: src/renderer/components/cluster-manager/clusters-menu.tsx:79 #: 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 @@ -2470,7 +2478,7 @@ msgstr "Set" msgid "Set quota" msgstr "Set quota" -#: src/renderer/components/cluster-manager/clusters-menu.tsx:53 +#: src/renderer/components/cluster-manager/clusters-menu.tsx:51 msgid "Settings" msgstr "Settings" @@ -2613,7 +2621,7 @@ msgstr "Submitting.." msgid "Subsets" msgstr "Subsets" -#: src/renderer/components/+add-cluster/add-cluster.tsx:122 +#: src/renderer/components/+add-cluster/add-cluster.tsx:121 msgid "Successfully imported <0>{0} cluster(s)" msgstr "Successfully imported <0>{0} cluster(s)" @@ -2635,11 +2643,11 @@ msgstr "TLS" msgid "Taints" msgstr "Taints" -#: src/renderer/components/+preferences/preferences.tsx:168 +#: src/renderer/components/+preferences/preferences.tsx:171 msgid "Telemetry & Usage Tracking" msgstr "Telemetry & Usage Tracking" -#: src/renderer/components/+preferences/preferences.tsx:171 +#: src/renderer/components/+preferences/preferences.tsx:174 msgid "Telemetry & usage data is collected to continuously improve the Lens experience." msgstr "Telemetry & usage data is collected to continuously improve the Lens experience." @@ -2675,7 +2683,7 @@ msgstr "This field must be a valid path" msgid "This is the quick launch menu." msgstr "This is the quick launch menu." -#: src/renderer/components/+preferences/preferences.tsx:163 +#: src/renderer/components/+preferences/preferences.tsx:166 msgid "This will make Lens to trust ANY certificate authority without any validations." msgstr "This will make Lens to trust ANY certificate authority without any validations." @@ -2953,7 +2961,7 @@ msgstr "listKind" msgid "never" msgstr "never" -#: src/renderer/components/cluster-manager/clusters-menu.tsx:133 +#: src/renderer/components/cluster-manager/clusters-menu.tsx:130 msgid "new" msgstr "new" diff --git a/locales/fi/messages.po b/locales/fi/messages.po index 613e68b6fe..ee19bf5187 100644 --- a/locales/fi/messages.po +++ b/locales/fi/messages.po @@ -88,7 +88,7 @@ msgid "Active" msgstr "" #: src/renderer/components/+add-cluster/add-cluster.tsx:310 -#: src/renderer/components/cluster-manager/clusters-menu.tsx:130 +#: src/renderer/components/cluster-manager/clusters-menu.tsx:127 msgid "Add Cluster" msgstr "" @@ -219,11 +219,11 @@ msgstr "" msgid "Allow Privilege Escalation" msgstr "" -#: src/renderer/components/+preferences/preferences.tsx:169 +#: src/renderer/components/+preferences/preferences.tsx:172 msgid "Allow telemetry & usage tracking" msgstr "" -#: src/renderer/components/+preferences/preferences.tsx:161 +#: src/renderer/components/+preferences/preferences.tsx:164 msgid "Allow untrusted Certificate Authorities" msgstr "" @@ -301,6 +301,14 @@ msgstr "" msgid "Auth App Role" msgstr "" +#: src/renderer/components/+preferences/preferences.tsx:160 +msgid "Auto start-up" +msgstr "" + +#: src/renderer/components/+preferences/preferences.tsx:161 +msgid "Automatically start Lens on login" +msgstr "" + #: src/renderer/components/error-boundary/error-boundary.tsx:53 #: src/renderer/components/wizard/wizard.tsx:130 msgid "Back" @@ -422,7 +430,7 @@ msgstr "" msgid "Capacity" msgstr "" -#: src/renderer/components/+preferences/preferences.tsx:160 +#: src/renderer/components/+preferences/preferences.tsx:163 msgid "Certificate Trust" msgstr "" @@ -813,7 +821,7 @@ msgstr "" msgid "Desired number of replicas" msgstr "" -#: src/renderer/components/cluster-manager/clusters-menu.tsx:65 +#: src/renderer/components/cluster-manager/clusters-menu.tsx:62 msgid "Disconnect" msgstr "" @@ -827,7 +835,7 @@ msgstr "" msgid "Disk:" msgstr "" -#: src/renderer/components/+preferences/preferences.tsx:165 +#: src/renderer/components/+preferences/preferences.tsx:168 msgid "Does not affect cluster communications!" msgstr "" @@ -923,8 +931,8 @@ msgstr "" msgid "Error stack" msgstr "" -#: src/renderer/components/+add-cluster/add-cluster.tsx:89 -#: src/renderer/components/+add-cluster/add-cluster.tsx:130 +#: src/renderer/components/+add-cluster/add-cluster.tsx:88 +#: src/renderer/components/+add-cluster/add-cluster.tsx:129 msgid "Error while adding cluster(s): {0}" msgstr "" @@ -1572,7 +1580,7 @@ msgstr "" msgid "Namespaces: {0}" msgstr "" -#: src/renderer/components/+preferences/preferences.tsx:164 +#: src/renderer/components/+preferences/preferences.tsx:167 msgid "Needed with some corporate proxies that do certificate re-writing." msgstr "" @@ -1781,7 +1789,7 @@ msgstr "" msgid "Persistent Volumes" msgstr "" -#: src/renderer/components/+add-cluster/add-cluster.tsx:75 +#: src/renderer/components/+add-cluster/add-cluster.tsx:74 msgid "Please select at least one cluster context" msgstr "" @@ -2008,8 +2016,8 @@ msgstr "" #: src/renderer/components/+preferences/preferences.tsx:152 #: src/renderer/components/+user-management-roles-bindings/role-binding-details.tsx:60 -#: src/renderer/components/cluster-manager/clusters-menu.tsx:76 -#: src/renderer/components/cluster-manager/clusters-menu.tsx:82 +#: src/renderer/components/cluster-manager/clusters-menu.tsx:73 +#: src/renderer/components/cluster-manager/clusters-menu.tsx:79 #: 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 @@ -2453,7 +2461,7 @@ msgstr "" msgid "Set quota" msgstr "" -#: src/renderer/components/cluster-manager/clusters-menu.tsx:53 +#: src/renderer/components/cluster-manager/clusters-menu.tsx:51 msgid "Settings" msgstr "" @@ -2596,7 +2604,7 @@ msgstr "" msgid "Subsets" msgstr "" -#: src/renderer/components/+add-cluster/add-cluster.tsx:122 +#: src/renderer/components/+add-cluster/add-cluster.tsx:121 msgid "Successfully imported <0>{0} cluster(s)" msgstr "" @@ -2618,11 +2626,11 @@ msgstr "" msgid "Taints" msgstr "" -#: src/renderer/components/+preferences/preferences.tsx:168 +#: src/renderer/components/+preferences/preferences.tsx:171 msgid "Telemetry & Usage Tracking" msgstr "" -#: src/renderer/components/+preferences/preferences.tsx:171 +#: src/renderer/components/+preferences/preferences.tsx:174 msgid "Telemetry & usage data is collected to continuously improve the Lens experience." msgstr "" @@ -2658,7 +2666,7 @@ msgstr "" msgid "This is the quick launch menu." msgstr "" -#: src/renderer/components/+preferences/preferences.tsx:163 +#: src/renderer/components/+preferences/preferences.tsx:166 msgid "This will make Lens to trust ANY certificate authority without any validations." msgstr "" @@ -2936,7 +2944,7 @@ msgstr "" msgid "never" msgstr "" -#: src/renderer/components/cluster-manager/clusters-menu.tsx:133 +#: src/renderer/components/cluster-manager/clusters-menu.tsx:130 msgid "new" msgstr "" diff --git a/locales/ru/messages.po b/locales/ru/messages.po index 58d4a545f1..01b8d777a3 100644 --- a/locales/ru/messages.po +++ b/locales/ru/messages.po @@ -89,7 +89,7 @@ msgid "Active" msgstr "Активный" #: src/renderer/components/+add-cluster/add-cluster.tsx:310 -#: src/renderer/components/cluster-manager/clusters-menu.tsx:130 +#: src/renderer/components/cluster-manager/clusters-menu.tsx:127 msgid "Add Cluster" msgstr "" @@ -220,11 +220,11 @@ msgstr "" msgid "Allow Privilege Escalation" msgstr "" -#: src/renderer/components/+preferences/preferences.tsx:169 +#: src/renderer/components/+preferences/preferences.tsx:172 msgid "Allow telemetry & usage tracking" msgstr "" -#: src/renderer/components/+preferences/preferences.tsx:161 +#: src/renderer/components/+preferences/preferences.tsx:164 msgid "Allow untrusted Certificate Authorities" msgstr "" @@ -302,6 +302,14 @@ msgstr "" msgid "Auth App Role" msgstr "Auth App Role" +#: src/renderer/components/+preferences/preferences.tsx:160 +msgid "Auto start-up" +msgstr "" + +#: src/renderer/components/+preferences/preferences.tsx:161 +msgid "Automatically start Lens on login" +msgstr "" + #: src/renderer/components/error-boundary/error-boundary.tsx:53 #: src/renderer/components/wizard/wizard.tsx:130 msgid "Back" @@ -423,7 +431,7 @@ msgstr "Отмена" msgid "Capacity" msgstr "Емкость" -#: src/renderer/components/+preferences/preferences.tsx:160 +#: src/renderer/components/+preferences/preferences.tsx:163 msgid "Certificate Trust" msgstr "" @@ -818,7 +826,7 @@ msgstr "" msgid "Desired number of replicas" msgstr "Нужный уровень реплик" -#: src/renderer/components/cluster-manager/clusters-menu.tsx:65 +#: src/renderer/components/cluster-manager/clusters-menu.tsx:62 msgid "Disconnect" msgstr "" @@ -832,7 +840,7 @@ msgstr "Диск" msgid "Disk:" msgstr "Диск:" -#: src/renderer/components/+preferences/preferences.tsx:165 +#: src/renderer/components/+preferences/preferences.tsx:168 msgid "Does not affect cluster communications!" msgstr "" @@ -928,8 +936,8 @@ msgstr "Среда" msgid "Error stack" msgstr "Стэк ошибки" -#: src/renderer/components/+add-cluster/add-cluster.tsx:89 -#: src/renderer/components/+add-cluster/add-cluster.tsx:130 +#: src/renderer/components/+add-cluster/add-cluster.tsx:88 +#: src/renderer/components/+add-cluster/add-cluster.tsx:129 msgid "Error while adding cluster(s): {0}" msgstr "" @@ -1582,7 +1590,7 @@ msgstr "Namespaces" msgid "Namespaces: {0}" msgstr "Namespaces: {0}" -#: src/renderer/components/+preferences/preferences.tsx:164 +#: src/renderer/components/+preferences/preferences.tsx:167 msgid "Needed with some corporate proxies that do certificate re-writing." msgstr "" @@ -1799,7 +1807,7 @@ msgstr "Persistent Volume Claims" msgid "Persistent Volumes" msgstr "Persistent Volumes" -#: src/renderer/components/+add-cluster/add-cluster.tsx:75 +#: src/renderer/components/+add-cluster/add-cluster.tsx:74 msgid "Please select at least one cluster context" msgstr "" @@ -2026,8 +2034,8 @@ msgstr "Релизы" #: src/renderer/components/+preferences/preferences.tsx:152 #: src/renderer/components/+user-management-roles-bindings/role-binding-details.tsx:60 -#: src/renderer/components/cluster-manager/clusters-menu.tsx:76 -#: src/renderer/components/cluster-manager/clusters-menu.tsx:82 +#: src/renderer/components/cluster-manager/clusters-menu.tsx:73 +#: src/renderer/components/cluster-manager/clusters-menu.tsx:79 #: 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 @@ -2471,7 +2479,7 @@ msgstr "Установлено" msgid "Set quota" msgstr "Установить квоту" -#: src/renderer/components/cluster-manager/clusters-menu.tsx:53 +#: src/renderer/components/cluster-manager/clusters-menu.tsx:51 msgid "Settings" msgstr "" @@ -2614,7 +2622,7 @@ msgstr "Применение.." msgid "Subsets" msgstr "" -#: src/renderer/components/+add-cluster/add-cluster.tsx:122 +#: src/renderer/components/+add-cluster/add-cluster.tsx:121 msgid "Successfully imported <0>{0} cluster(s)" msgstr "" @@ -2636,11 +2644,11 @@ msgstr "TLS" msgid "Taints" msgstr "Метки блокировки" -#: src/renderer/components/+preferences/preferences.tsx:168 +#: src/renderer/components/+preferences/preferences.tsx:171 msgid "Telemetry & Usage Tracking" msgstr "" -#: src/renderer/components/+preferences/preferences.tsx:171 +#: src/renderer/components/+preferences/preferences.tsx:174 msgid "Telemetry & usage data is collected to continuously improve the Lens experience." msgstr "" @@ -2676,7 +2684,7 @@ msgstr "" msgid "This is the quick launch menu." msgstr "" -#: src/renderer/components/+preferences/preferences.tsx:163 +#: src/renderer/components/+preferences/preferences.tsx:166 msgid "This will make Lens to trust ANY certificate authority without any validations." msgstr "" @@ -2954,7 +2962,7 @@ msgstr "" msgid "never" msgstr "" -#: src/renderer/components/cluster-manager/clusters-menu.tsx:133 +#: src/renderer/components/cluster-manager/clusters-menu.tsx:130 msgid "new" msgstr "" diff --git a/package.json b/package.json index e330613b82..8b850b213e 100644 --- a/package.json +++ b/package.json @@ -16,12 +16,13 @@ "dev-run": "nodemon --watch static/build/main.js --exec \"electron --inspect .\"", "dev:main": "yarn compile:main --watch", "dev:renderer": "yarn compile:renderer --watch", - "dev:extension-rollup": "yarn compile:extension-rollup --watch", + "dev:extension-types": "yarn compile:extension-types --watch", "compile": "env NODE_ENV=production concurrently yarn:compile:*", "compile:main": "webpack --config webpack.main.ts", "compile:renderer": "webpack --config webpack.renderer.ts", "compile:i18n": "lingui compile", - "compile:extension-rollup": "rollup --config src/extensions/rollup.config.js", + "compile:extension-types": "rollup --config src/extensions/rollup.config.js", + "npm:fix-package-version": "ts-node build/set_npm_version.ts", "build:linux": "yarn compile && electron-builder --linux --dir -c.productName=Lens", "build:mac": "yarn compile && electron-builder --mac --dir -c.productName=Lens", "build:win": "yarn compile && electron-builder --win --dir -c.productName=Lens", @@ -35,6 +36,7 @@ "download-bins": "concurrently yarn:download:*", "download:kubectl": "yarn run ts-node build/download_kubectl.ts", "download:helm": "yarn run ts-node build/download_helm.ts", + "build:tray-icons": "yarn run ts-node build/build_tray_icon.ts", "lint": "eslint $@ --ext js,ts,tsx --max-warnings=0 src/" }, "config": { @@ -96,6 +98,11 @@ "to": "static/", "filter": "!**/main.js" }, + { + "from": "build/tray", + "to": "static/icons", + "filter": "*.png" + }, { "from": "extensions/", "to": "./extensions/", @@ -185,6 +192,20 @@ "@hapi/call": "^8.0.0", "@hapi/subtext": "^7.0.3", "@kubernetes/client-node": "^0.12.0", + "@types/crypto-js": "^3.1.47", + "@types/electron-window-state": "^2.0.34", + "@types/fs-extra": "^9.0.1", + "@types/http-proxy": "^1.17.4", + "@types/js-yaml": "^3.12.4", + "@types/jsdom": "^16.2.4", + "@types/jsonpath": "^0.2.0", + "@types/lodash": "^4.14.155", + "@types/marked": "^0.7.4", + "@types/mock-fs": "^4.10.0", + "@types/node": "^12.12.45", + "@types/proper-lockfile": "^4.1.1", + "@types/react-beautiful-dnd": "^13.0.0", + "@types/tar": "^4.0.3", "array-move": "^3.0.0", "chalk": "^4.1.0", "command-exists": "1.2.9", @@ -197,8 +218,8 @@ "fs-extra": "^9.0.1", "handlebars": "^4.7.6", "http-proxy": "^1.18.1", - "immer": "^7.0.5", "js-yaml": "^3.14.0", + "jsdom": "^16.4.0", "jsonpath": "^1.0.2", "lodash": "^4.17.15", "mac-ca": "^1.0.4", @@ -275,6 +296,7 @@ "@types/request": "^2.48.5", "@types/request-promise-native": "^1.0.17", "@types/semver": "^7.2.0", + "@types/sharp": "^0.26.0", "@types/shelljs": "^0.8.8", "@types/spdy": "^3.4.4", "@types/tar": "^4.0.3", @@ -326,7 +348,7 @@ "postinstall-postinstall": "^2.1.0", "progress-bar-webpack-plugin": "^2.1.0", "raw-loader": "^4.0.1", - "react": "^16.13.1", + "react": "^16.14.0", "react-beautiful-dnd": "^13.0.0", "react-dom": "^16.13.1", "react-router": "^5.2.0", @@ -338,6 +360,7 @@ "rollup-plugin-ignore-import": "^1.3.2", "rollup-pluginutils": "^2.8.2", "sass-loader": "^8.0.2", + "sharp": "^0.26.1", "spectron": "11.0.0", "style-loader": "^1.2.1", "terser-webpack-plugin": "^3.0.3", diff --git a/src/common/cluster-store.ts b/src/common/cluster-store.ts index 9ce9196217..fddd0f3be6 100644 --- a/src/common/cluster-store.ts +++ b/src/common/cluster-store.ts @@ -1,6 +1,6 @@ import type { WorkspaceId } from "./workspace-store"; import path from "path"; -import { app, ipcRenderer, remote, webFrame, webContents } from "electron"; +import { app, ipcRenderer, remote, webFrame } from "electron"; import { unlink } from "fs-extra"; import { action, computed, observable, toJS } from "mobx"; import { BaseStore } from "./base-store"; @@ -113,7 +113,7 @@ export class ClusterStore extends BaseStore { @action setActive(id: ClusterId) { - this.activeClusterId = id; + this.activeClusterId = this.clusters.has(id) ? id : null; } @action @@ -160,7 +160,7 @@ export class ClusterStore extends BaseStore { if (cluster) { this.clusters.delete(clusterId); if (this.activeClusterId === clusterId) { - this.activeClusterId = null; + this.setActive(null); } // remove only custom kubeconfigs (pasted as text) if (cluster.kubeConfigPath == ClusterStore.getCustomKubeConfigPath(clusterId)) { diff --git a/src/common/user-store.ts b/src/common/user-store.ts index 00abe8dd2c..0bb953b153 100644 --- a/src/common/user-store.ts +++ b/src/common/user-store.ts @@ -27,6 +27,7 @@ export interface UserPreferences { downloadKubectlBinaries?: boolean; downloadBinariesPath?: string; kubectlBinariesPath?: string; + openAtLogin?: boolean; } export class UserStore extends BaseStore { @@ -38,14 +39,7 @@ export class UserStore extends BaseStore { migrations: migrations, }); - // track telemetry availability - reaction(() => this.preferences.allowTelemetry, allowed => { - appEventBus.emit({name: "telemetry", action: allowed ? "enabled" : "disabled"}) - }); - - // refresh new contexts - this.whenLoaded.then(this.refreshNewContexts); - reaction(() => this.kubeConfigPath, this.refreshNewContexts); + this.handleOnLoad(); } @observable lastSeenAppVersion = "0.0.0" @@ -59,8 +53,31 @@ export class UserStore extends BaseStore { colorTheme: UserStore.defaultTheme, downloadMirror: "default", downloadKubectlBinaries: true, // Download kubectl binaries matching cluster version + openAtLogin: true, }; + protected async handleOnLoad() { + await this.whenLoaded; + + // refresh new contexts + this.refreshNewContexts(); + reaction(() => this.kubeConfigPath, this.refreshNewContexts); + + if (app) { + // track telemetry availability + reaction(() => this.preferences.allowTelemetry, allowed => { + appEventBus.emit({name: "telemetry", action: allowed ? "enabled" : "disabled"}) + }); + + // open at system start-up + reaction(() => this.preferences.openAtLogin, open => { + app.setLoginItemSettings({ openAtLogin: open }); + }, { + fireImmediately: true, + }); + } + } + get isNewVersion() { return semver.gt(getAppVersion(), this.lastSeenAppVersion); } diff --git a/src/common/utils/buildUrl.ts b/src/common/utils/buildUrl.ts new file mode 100644 index 0000000000..cdaf36720c --- /dev/null +++ b/src/common/utils/buildUrl.ts @@ -0,0 +1,14 @@ +import { compile } from "path-to-regexp" + +export interface IURLParams

{ + params?: P; + query?: Q; +} + +export function buildURL

(path: string | any) { + const pathBuilder = compile(String(path)); + return function ({ params, query }: IURLParams = {}) { + const queryParams = query ? new URLSearchParams(Object.entries(query)).toString() : "" + return pathBuilder(params) + (queryParams ? `?${queryParams}` : "") + } +} diff --git a/src/common/vars.ts b/src/common/vars.ts index ca28a2f99a..206aa59ce2 100644 --- a/src/common/vars.ts +++ b/src/common/vars.ts @@ -5,7 +5,9 @@ import { defineGlobal } from "./utils/defineGlobal"; export const isMac = process.platform === "darwin" export const isWindows = process.platform === "win32" +export const isLinux = process.platform === "linux" export const isDebugging = process.env.DEBUG === "true"; +export const isSnap = !!process.env["SNAP"] export const isProduction = process.env.NODE_ENV === "production" export const isTestEnv = !!process.env.JEST_WORKER_ID; export const isDevelopment = !isTestEnv && !isProduction; diff --git a/src/common/workspace-store.ts b/src/common/workspace-store.ts index 752a53f41b..b7f2467013 100644 --- a/src/common/workspace-store.ts +++ b/src/common/workspace-store.ts @@ -1,8 +1,7 @@ import { action, computed, observable, toJS } from "mobx"; import { BaseStore } from "./base-store"; import { clusterStore } from "./cluster-store" -import { landingURL } from "../renderer/components/+landing-page/landing-page.route"; -import { navigate } from "../renderer/navigation"; +import { appEventBus } from "./event-bus"; export type WorkspaceId = string; @@ -56,18 +55,13 @@ export class WorkspaceStore extends BaseStore { } @action - setActive(id = WorkspaceStore.defaultId, { redirectToLanding = true, resetActiveCluster = true } = {}) { + setActive(id = WorkspaceStore.defaultId, reset = true) { if (id === this.currentWorkspaceId) return; if (!this.getById(id)) { throw new Error(`workspace ${id} doesn't exist`); } this.currentWorkspaceId = id; - if (resetActiveCluster) { - clusterStore.setActive(null) - } - if (redirectToLanding) { - navigate(landingURL()) - } + clusterStore.activeClusterId = null; // fixme: handle previously selected cluster from current workspace } @action @@ -79,6 +73,9 @@ export class WorkspaceStore extends BaseStore { } if (existingWorkspace) { Object.assign(existingWorkspace, workspace); + appEventBus.emit({name: "workspace", action: "update"}) + } else { + appEventBus.emit({name: "workspace", action: "add"}) } this.workspaces.set(id, workspace); return workspace; @@ -95,6 +92,7 @@ export class WorkspaceStore extends BaseStore { this.currentWorkspaceId = WorkspaceStore.defaultId; // reset to default } this.workspaces.delete(id); + appEventBus.emit({name: "workspace", action: "remove"}) clusterStore.removeByWorkspaceId(id) } diff --git a/src/extensions/core-api/app.ts b/src/extensions/core-api/app.ts new file mode 100644 index 0000000000..f3e44ed001 --- /dev/null +++ b/src/extensions/core-api/app.ts @@ -0,0 +1,4 @@ +import { getAppVersion } from "../../common/utils"; + +export const version = getAppVersion() +export { isSnap, isWindows, isMac, isLinux, appName, slackUrl, issuesTrackerUrl } from "../../common/vars" \ No newline at end of file diff --git a/src/extensions/core-api/index.ts b/src/extensions/core-api/index.ts index c98266a2c5..3882763681 100644 --- a/src/extensions/core-api/index.ts +++ b/src/extensions/core-api/index.ts @@ -5,20 +5,21 @@ export * from "../lens-renderer-extension" import type { WindowManager } from "../../main/window-manager"; // APIs +import * as App from "./app" import * as EventBus from "./event-bus" import * as Store from "./stores" import * as Util from "./utils" import * as Registry from "../registries" -import * as CommonVars from "../../common/vars"; import * as ClusterFeature from "./cluster-feature" +// TODO: allow to expose windowManager.navigate() as Navigation.navigate() in runtime export let windowManager: WindowManager; export { + App, EventBus, ClusterFeature, Store, Util, Registry, - CommonVars, } diff --git a/src/extensions/core-api/stores.ts b/src/extensions/core-api/stores.ts index 9ff2c66763..dfaae325af 100644 --- a/src/extensions/core-api/stores.ts +++ b/src/extensions/core-api/stores.ts @@ -1,2 +1,4 @@ export { ExtensionStore } from "../extension-store" +export { clusterStore, ClusterModel } from "../../common/cluster-store" +export { workspaceStore} from "../../common/workspace-store" export type { Cluster } from "../../main/cluster" diff --git a/src/extensions/extension-manager.ts b/src/extensions/extension-manager.ts index a0ec3ad897..1e0ebf9387 100644 --- a/src/extensions/extension-manager.ts +++ b/src/extensions/extension-manager.ts @@ -1,5 +1,6 @@ import type { ExtensionManifest } from "./lens-extension" import path from "path" +import os from "os" import fs from "fs-extra" import logger from "../main/logger" import { extensionPackagesRoot, InstalledExtension } from "./extension-loader" @@ -24,10 +25,14 @@ export class ExtensionManager { return extensionPackagesRoot() } - get folderPath(): string { + get inTreeFolderPath(): string { return path.resolve(__static, "../extensions"); } + get localFolderPath(): string { + return path.join(os.homedir(), ".k8slens", "extensions"); + } + get npmPath() { return __non_webpack_require__.resolve('npm/bin/npm-cli') } @@ -35,7 +40,7 @@ export class ExtensionManager { async load() { logger.info("[EXTENSION-MANAGER] loading extensions from " + this.extensionPackagesRoot) await fs.ensureDir(path.join(this.extensionPackagesRoot, "node_modules")) - + await fs.ensureDir(this.localFolderPath) return await this.loadExtensions(); } @@ -74,14 +79,17 @@ export class ExtensionManager { } async loadExtensions() { - const extensions = await this.loadFromFolder(this.folderPath); + const bundledExtensions = await this.loadBundledExtensions() + const localExtendions = await this.loadFromFolder(this.localFolderPath) + const extensions = bundledExtensions.concat(localExtendions) return new Map(extensions.map(ext => [ext.id, ext])); } - async loadFromFolder(folderPath: string): Promise { - const paths = await fs.readdir(folderPath); + async loadBundledExtensions() { const extensions: InstalledExtension[] = [] + const folderPath = this.inTreeFolderPath const bundledExtensions = getBundledExtensions() + const paths = await fs.readdir(folderPath); for (const fileName of paths) { if (!bundledExtensions.includes(fileName)) { continue @@ -94,10 +102,33 @@ export class ExtensionManager { extensions.push(ext) } } + logger.debug(`[EXTENSION-MANAGER]: ${extensions.length} extensions loaded`, { folderPath, extensions }); + await fs.writeFile(path.join(this.extensionPackagesRoot, "package.json"), JSON.stringify(this.packagesJson), {mode: 0o600}) + await this.installPackages() + return extensions + } + + async loadFromFolder(folderPath: string): Promise { + const bundledExtensions = getBundledExtensions() + const extensions: InstalledExtension[] = [] + const paths = await fs.readdir(folderPath); + for (const fileName of paths) { + if (bundledExtensions.includes(fileName)) { // do no allow to override bundled extensions + continue + } + const absPath = path.resolve(folderPath, fileName); + const manifestPath = path.resolve(absPath, "package.json"); + await fs.access(manifestPath, fs.constants.F_OK) + const ext = await this.getExtensionByManifest(manifestPath).catch(() => null) + if (ext) { + extensions.push(ext) + } + } + + logger.debug(`[EXTENSION-MANAGER]: ${extensions.length} extensions loaded`, { folderPath, extensions }); await fs.writeFile(path.join(this.extensionPackagesRoot, "package.json"), JSON.stringify(this.packagesJson), {mode: 0o600}) await this.installPackages() - logger.debug(`[EXTENSION-MANAGER]: ${extensions.length} extensions loaded`, { folderPath, extensions }); return extensions; } } diff --git a/src/extensions/npm/extensions/package.json b/src/extensions/npm/extensions/package.json index 7097629539..373e43007a 100644 --- a/src/extensions/npm/extensions/package.json +++ b/src/extensions/npm/extensions/package.json @@ -5,9 +5,7 @@ "version": "0.0.0", "copyright": "© 2020, Mirantis, Inc.", "license": "MIT", - "files": [ - "api.d.ts" - ], + "types": "api.d.ts", "author": { "name": "Mirantis, Inc.", "email": "info@k8slens.dev" diff --git a/src/main/app-updater.ts b/src/main/app-updater.ts index 7b27a7c3f6..3613c3ef18 100644 --- a/src/main/app-updater.ts +++ b/src/main/app-updater.ts @@ -1,19 +1,19 @@ import { autoUpdater } from "electron-updater" import logger from "./logger" -export default class AppUpdater { +export class AppUpdater { + static readonly defaultUpdateIntervalMs = 1000 * 60 * 60 * 24 // once a day - protected updateInterval: number = (1000 * 60 * 60 * 24) // once a day + static checkForUpdates() { + return autoUpdater.checkForUpdatesAndNotify() + } - constructor() { + constructor(protected updateInterval = AppUpdater.defaultUpdateIntervalMs) { autoUpdater.logger = logger } public start() { - setInterval(() => { - autoUpdater.checkForUpdatesAndNotify() - }, this.updateInterval) - - return autoUpdater.checkForUpdatesAndNotify() + setInterval(AppUpdater.checkForUpdates, this.updateInterval) + return AppUpdater.checkForUpdates(); } } diff --git a/src/main/helm/helm-repo-manager.ts b/src/main/helm/helm-repo-manager.ts index 3bbec7841f..c2af9ea7ba 100644 --- a/src/main/helm/helm-repo-manager.ts +++ b/src/main/helm/helm-repo-manager.ts @@ -37,12 +37,12 @@ export class HelmRepoManager extends Singleton { async loadAvailableRepos(): Promise { const res = await customRequestPromise({ - uri: "https://hub.helm.sh/assets/js/repos.json", + uri: "https://github.com/lensapp/artifact-hub-repositories/releases/download/latest/repositories.json", json: true, resolveWithFullResponse: true, timeout: 10000, }); - return orderBy(res.body.data, repo => repo.name); + return orderBy(res.body, repo => repo.name); } async init() { diff --git a/src/main/index.ts b/src/main/index.ts index 6444e6b5f0..ff7e050dd2 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -10,7 +10,7 @@ import path from "path" import { LensProxy } from "./lens-proxy" import { WindowManager } from "./window-manager"; import { ClusterManager } from "./cluster-manager"; -import AppUpdater from "./app-updater" +import { AppUpdater } from "./app-updater" import { shellSync } from "./shell-sync" import { getFreePort } from "./port" import { mangleProxyEnv } from "./proxy-env" @@ -24,45 +24,46 @@ import { extensionLoader } from "../extensions/extension-loader"; import logger from "./logger" const workingDir = path.join(app.getPath("appData"), appName); +let proxyPort: number; +let proxyServer: LensProxy; +let clusterManager: ClusterManager; +let windowManager: WindowManager; + app.setName(appName); if (!process.env.CICD) { app.setPath("userData", workingDir); } -let clusterManager: ClusterManager; -let proxyServer: LensProxy; - mangleProxyEnv() if (app.commandLine.getSwitchValue("proxy-server") !== "") { process.env.HTTPS_PROXY = app.commandLine.getSwitchValue("proxy-server") } -async function main() { - await shellSync(); +app.on("ready", async () => { logger.info(`🚀 Starting Lens from "${workingDir}"`) + await shellSync(); const updater = new AppUpdater() updater.start(); registerFileProtocol("static", __static); - // find free port - let proxyPort: number - try { - proxyPort = await getFreePort() - } catch (error) { - logger.error(error) - dialog.showErrorBox("Lens Error", "Could not find a free port for the cluster proxy") - app.quit(); - } - - // preload configuration from stores + // preload isomorphic stores await Promise.all([ userStore.load(), clusterStore.load(), workspaceStore.load(), ]); + // find free port + try { + proxyPort = await getFreePort() + } catch (error) { + logger.error(error) + dialog.showErrorBox("Lens Error", "Could not find a free port for the cluster proxy") + app.exit(); + } + // create cluster manager clusterManager = new ClusterManager(proxyPort); @@ -72,28 +73,34 @@ async function main() { } catch (error) { logger.error(`Could not start proxy (127.0.0:${proxyPort}): ${error.message}`) dialog.showErrorBox("Lens Error", `Could not start proxy (127.0.0:${proxyPort}): ${error.message || "unknown error"}`) - app.quit(); + app.exit(); } - // create window manager and open app - LensExtensionsApi.windowManager = new WindowManager(proxyPort); + windowManager = new WindowManager(proxyPort); + LensExtensionsApi.windowManager = windowManager; // expose to extensions extensionLoader.loadOnMain() extensionLoader.extensions.replace(await extensionManager.load()) extensionLoader.broadcastExtensions() setTimeout(() => { - appEventBus.emit({name: "app", action: "start"}) + appEventBus.emit({ name: "app", action: "start" }) }, 1000) -} +}); -app.on("ready", main); +app.on("activate", (event, hasVisibleWindows) => { + logger.info('APP:ACTIVATE', { hasVisibleWindows }) + if (!hasVisibleWindows) { + windowManager.initMainWindow(); + } +}); -app.on("will-quit", async (event) => { - event.preventDefault(); // To allow mixpanel sending to be executed - if (proxyServer) proxyServer.close() - if (clusterManager) clusterManager.stop() - app.exit(); +// Quit app on Cmd+Q (MacOS) +app.on("will-quit", (event) => { + logger.info('APP:QUIT'); + event.preventDefault(); // prevent app's default shutdown (e.g. required for telemetry, etc.) + clusterManager?.stop(); // close cluster connections + return; // skip exit to make tray work, to quit go to app's global menu or tray's menu }) // Extensions-api runtime exports diff --git a/src/main/menu.ts b/src/main/menu.ts index 935d71b519..32a78bd9d3 100644 --- a/src/main/menu.ts +++ b/src/main/menu.ts @@ -12,11 +12,27 @@ import logger from "./logger"; export type MenuTopId = "mac" | "file" | "edit" | "view" | "help" export function initMenu(windowManager: WindowManager) { - autorun(() => buildMenu(windowManager), { + return autorun(() => buildMenu(windowManager), { delay: 100 }); } +export function showAbout(browserWindow: BrowserWindow) { + const appInfo = [ + `${appName}: ${app.getVersion()}`, + `Electron: ${process.versions.electron}`, + `Chrome: ${process.versions.chrome}`, + `Copyright 2020 Mirantis, Inc.`, + ] + dialog.showMessageBoxSync(browserWindow, { + title: `${isWindows ? " ".repeat(2) : ""}${appName}`, + type: "info", + buttons: ["Close"], + message: `Lens`, + detail: appInfo.join("\r\n") + }) +} + export function buildMenu(windowManager: WindowManager) { function ignoreOnMac(menuItems: MenuItemConstructorOptions[]) { if (isMac) return []; @@ -32,28 +48,9 @@ export function buildMenu(windowManager: WindowManager) { return menuItems; } - function navigate(url: string) { + async function navigate(url: string) { logger.info(`[MENU]: navigating to ${url}`); - windowManager.navigate({ - channel: "menu:navigate", - url: url, - }) - } - - function showAbout(browserWindow: BrowserWindow) { - const appInfo = [ - `${appName}: ${app.getVersion()}`, - `Electron: ${process.versions.electron}`, - `Chrome: ${process.versions.chrome}`, - `Copyright 2020 Mirantis, Inc.`, - ] - dialog.showMessageBoxSync(browserWindow, { - title: `${isWindows ? " ".repeat(2) : ""}${appName}`, - type: "info", - buttons: ["Close"], - message: `Lens`, - detail: appInfo.join("\r\n") - }) + await windowManager.navigate(url); } const macAppMenu: MenuItemConstructorOptions = { @@ -80,7 +77,13 @@ export function buildMenu(windowManager: WindowManager) { { role: 'hideOthers' }, { role: 'unhide' }, { type: 'separator' }, - { role: 'quit' } + { + label: 'Quit', + accelerator: 'Cmd+Q', + click() { + app.exit(); // force quit since might be blocked within app.on("will-quit") + } + } ] }; @@ -118,7 +121,9 @@ export function buildMenu(windowManager: WindowManager) { }, { type: 'separator' }, { role: 'quit' } - ]) + ]), + { type: 'separator' }, + { role: 'close' } // close current window ] }; @@ -158,7 +163,7 @@ export function buildMenu(windowManager: WindowManager) { label: 'Reload', accelerator: 'CmdOrCtrl+R', click() { - windowManager.reload({ channel: "menu:reload" }); + windowManager.reload(); } }, { role: 'toggleDevTools' }, diff --git a/src/main/tray.ts b/src/main/tray.ts new file mode 100644 index 0000000000..31cc99b314 --- /dev/null +++ b/src/main/tray.ts @@ -0,0 +1,126 @@ +import path from "path" +import packageInfo from "../../package.json" +import { app, dialog, Menu, NativeImage, nativeTheme, Tray } from "electron" +import { autorun } from "mobx"; +import { showAbout } from "./menu"; +import { AppUpdater } from "./app-updater"; +import { WindowManager } from "./window-manager"; +import { clusterStore } from "../common/cluster-store"; +import { workspaceStore } from "../common/workspace-store"; +import { preferencesURL } from "../renderer/components/+preferences/preferences.route"; +import { clusterViewURL } from "../renderer/components/cluster-manager/cluster-view.route"; +import logger from "./logger"; +import { isDevelopment } from "../common/vars"; + +// note: instance of Tray should be saved somewhere, otherwise it disappears +export let tray: Tray; + +// refresh icon when MacOS dark/light theme has changed +nativeTheme.on("updated", () => tray?.setImage(getTrayIcon())); + +export function getTrayIcon(isDark = nativeTheme.shouldUseDarkColors): string { + return path.resolve( + __static, + isDevelopment ? "../build/tray" : "icons", // copied within electron-builder extras + `tray_icon${isDark ? "_dark" : ""}.png` + ) +} + +export function initTray(windowManager: WindowManager) { + const dispose = autorun(() => { + try { + const menu = createTrayMenu(windowManager); + buildTray(getTrayIcon(), menu); + } catch (err) { + logger.error(`[TRAY]: building failed: ${err}`); + } + }) + return () => { + dispose(); + tray?.destroy(); + tray = null; + } +} + +export function buildTray(icon: string | NativeImage, menu: Menu) { + if (!tray) { + tray = new Tray(icon) + tray.setToolTip(packageInfo.description) + tray.setIgnoreDoubleClickEvents(true); + } + + tray.setImage(icon); + tray.setContextMenu(menu); + + return tray; +} + +export function createTrayMenu(windowManager: WindowManager): Menu { + return Menu.buildFromTemplate([ + { + label: "About Lens", + async click() { + // note: argument[1] (browserWindow) not available when app is not focused / hidden + const browserWindow = await windowManager.ensureMainWindow(); + showAbout(browserWindow); + }, + }, + { type: 'separator' }, + { + label: "Open Lens", + async click() { + await windowManager.ensureMainWindow() + }, + }, + { + label: "Preferences", + click() { + windowManager.navigate(preferencesURL()); + }, + }, + { + label: "Clusters", + submenu: workspaceStore.workspacesList + .filter(workspace => clusterStore.getByWorkspaceId(workspace.id).length > 0) // hide empty workspaces + .map(workspace => { + const clusters = clusterStore.getByWorkspaceId(workspace.id); + return { + label: workspace.name, + toolTip: workspace.description, + submenu: clusters.map(cluster => { + const { id: clusterId, preferences: { clusterName: label }, online, workspace } = cluster; + return { + label: `${online ? '✓' : '\x20'.repeat(3)/*offset*/}${label}`, + toolTip: clusterId, + async click() { + workspaceStore.setActive(workspace); + clusterStore.setActive(clusterId); + windowManager.navigate(clusterViewURL({ params: { clusterId } })); + } + } + }) + } + }), + }, + { + label: "Check for updates", + async click() { + const result = await AppUpdater.checkForUpdates(); + if (!result) { + const browserWindow = await windowManager.ensureMainWindow(); + dialog.showMessageBoxSync(browserWindow, { + message: "No updates available", + type: "info", + }) + } + }, + }, + { type: 'separator' }, + { + label: 'Quit App', + click() { + app.exit(); + } + } + ]); +} diff --git a/src/main/window-manager.ts b/src/main/window-manager.ts index cbe0c3803d..46b008a0d4 100644 --- a/src/main/window-manager.ts +++ b/src/main/window-manager.ts @@ -1,95 +1,140 @@ import type { ClusterId } from "../common/cluster-store"; import { clusterStore } from "../common/cluster-store"; -import { BrowserWindow, dialog, ipcMain, shell, webContents } from "electron" -import windowStateKeeper from "electron-window-state" import { observable } from "mobx"; -import { initMenu } from "./menu"; +import { app, BrowserWindow, dialog, ipcMain, shell, webContents } from "electron" +import windowStateKeeper from "electron-window-state" import { extensionLoader } from "../extensions/extension-loader"; import { appEventBus } from "../common/event-bus" +import { initMenu } from "./menu"; +import { initTray } from "./tray"; export class WindowManager { - protected mainView: BrowserWindow; + protected mainWindow: BrowserWindow; protected splashWindow: BrowserWindow; protected windowState: windowStateKeeper.State; + protected disposers: Record = {}; @observable activeClusterId: ClusterId; constructor(protected proxyPort: number) { + this.bindEvents(); + this.initMenu(); + this.initTray(); + this.initMainWindow(); + } + + get mainUrl() { + return `http://localhost:${this.proxyPort}` + } + + async initMainWindow(showSplash = true) { // Manage main window size and position with state persistence - this.windowState = windowStateKeeper({ - defaultHeight: 900, - defaultWidth: 1440, - }); + if (!this.windowState) { + this.windowState = windowStateKeeper({ + defaultHeight: 900, + defaultWidth: 1440, + }); + } + if (!this.mainWindow) { + // show icon in dock (mac-os only) + app.dock?.show(); - const { width, height, x, y } = this.windowState; - this.mainView = new BrowserWindow({ - x, y, width, height, - show: false, - minWidth: 700, // accommodate 800 x 600 display minimum - minHeight: 500, // accommodate 800 x 600 display minimum - titleBarStyle: "hidden", - backgroundColor: "#1e2124", - webPreferences: { - nodeIntegration: true, - nodeIntegrationInSubFrames: true, - enableRemoteModule: true, - }, - }); - this.windowState.manage(this.mainView); + const { width, height, x, y } = this.windowState; + this.mainWindow = new BrowserWindow({ + x, y, width, height, + show: false, + minWidth: 700, // accommodate 800 x 600 display minimum + minHeight: 500, // accommodate 800 x 600 display minimum + titleBarStyle: "hidden", + backgroundColor: "#1e2124", + webPreferences: { + nodeIntegration: true, + nodeIntegrationInSubFrames: true, + enableRemoteModule: true, + }, + }); + this.windowState.manage(this.mainWindow); - // open external links in default browser (target=_blank, window.open) - this.mainView.webContents.on("new-window", (event, url) => { - event.preventDefault(); - shell.openExternal(url); - }); - this.mainView.webContents.on("dom-ready", () => { - extensionLoader.broadcastExtensions() - }) - this.mainView.on("focus", () => { - appEventBus.emit({name: "app", action: "focus"}) - }) - this.mainView.on("blur", () => { - appEventBus.emit({name: "app", action: "blur"}) - }) + // open external links in default browser (target=_blank, window.open) + this.mainWindow.webContents.on("new-window", (event, url) => { + event.preventDefault(); + shell.openExternal(url); + }); + this.mainWindow.webContents.on("dom-ready", () => { + extensionLoader.broadcastExtensions() + }) + this.mainWindow.on("focus", () => { + appEventBus.emit({name: "app", action: "focus"}) + }) + this.mainWindow.on("blur", () => { + appEventBus.emit({name: "app", action: "blur"}) + }) + // clean up + this.mainWindow.on("closed", () => { + this.windowState.unmanage(); + this.mainWindow = null; + this.splashWindow = null; + app.dock?.hide(); // hide icon in dock (mac-os) + }) + } + try { + if (showSplash) await this.showSplash(); + await this.mainWindow.loadURL(this.mainUrl); + this.mainWindow.show(); + this.splashWindow?.close(); + } catch (err) { + dialog.showErrorBox("ERROR!", err.toString()) + } + } + + protected async initMenu() { + this.disposers.menuAutoUpdater = initMenu(this); + } + + protected initTray() { + this.disposers.trayAutoUpdater = initTray(this); + } + + protected bindEvents() { // track visible cluster from ui ipcMain.on("cluster-view:current-id", (event, clusterId: ClusterId) => { this.activeClusterId = clusterId; }); - - // load & show app - this.showMain(); - initMenu(this); } - navigate({ url, channel, frameId }: { url: string, channel: string, frameId?: number }) { + async ensureMainWindow(): Promise { + if (!this.mainWindow) await this.initMainWindow(); + this.mainWindow.show(); + return this.mainWindow; + } + + sendToView({ channel, frameId, data = [] }: { channel: string, frameId?: number, data?: any[] }) { if (frameId) { - this.mainView.webContents.sendToFrame(frameId, channel, url); + this.mainWindow.webContents.sendToFrame(frameId, channel, ...data); } else { - this.mainView.webContents.send(channel, url); + this.mainWindow.webContents.send(channel, ...data); } } - reload({ channel }: { channel: string }) { + async navigate(url: string, frameId?: number) { + await this.ensureMainWindow(); + this.sendToView({ + channel: "menu:navigate", + frameId: frameId, + data: [url], + }) + } + + reload() { const frameId = clusterStore.getById(this.activeClusterId)?.frameId; if (frameId) { - this.mainView.webContents.sendToFrame(frameId, channel); + this.sendToView({ channel: "menu:reload", frameId }); } else { webContents.getFocusedWebContents()?.reload(); } } - async showMain() { - try { - await this.showSplash(); - await this.mainView.loadURL(`http://localhost:${this.proxyPort}`) - this.mainView.show() - this.splashWindow.close() - } catch (err) { - dialog.showErrorBox("ERROR!", err.toString()) - } - } - async showSplash() { if (!this.splashWindow) { this.splashWindow = new BrowserWindow({ @@ -110,8 +155,13 @@ export class WindowManager { } destroy() { - this.windowState.unmanage(); + this.mainWindow.destroy(); this.splashWindow.destroy(); - this.mainView.destroy(); + this.mainWindow = null; + this.splashWindow = null; + Object.entries(this.disposers).forEach(([name, dispose]) => { + dispose(); + delete this.disposers[name] + }); } } diff --git a/src/renderer/api/endpoints/helm-releases.api.ts b/src/renderer/api/endpoints/helm-releases.api.ts index a3c1a62045..9051936ac8 100644 --- a/src/renderer/api/endpoints/helm-releases.api.ts +++ b/src/renderer/api/endpoints/helm-releases.api.ts @@ -141,7 +141,7 @@ export class HelmRelease implements ItemObject { chart: string status: string updated: string - revision: number + revision: string getId() { return this.namespace + this.name; @@ -165,7 +165,7 @@ export class HelmRelease implements ItemObject { } getRevision() { - return this.revision; + return parseInt(this.revision, 10); } getStatus() { diff --git a/src/renderer/api/endpoints/ingress.api.ts b/src/renderer/api/endpoints/ingress.api.ts index 9e6840c1f7..1f3e1659f0 100644 --- a/src/renderer/api/endpoints/ingress.api.ts +++ b/src/renderer/api/endpoints/ingress.api.ts @@ -109,6 +109,14 @@ export class Ingress extends KubeObject { } return ports.join(", ") } + + getLoadBalancers() { + const { status: { loadBalancer = { ingress: [] } } } = this; + + return (loadBalancer.ingress ?? []).map(address => ( + address.hostname || address.ip + )) + } } export const ingressApi = new IngressApi({ diff --git a/src/renderer/api/endpoints/pods.api.ts b/src/renderer/api/endpoints/pods.api.ts index ab58660eab..92b7059272 100644 --- a/src/renderer/api/endpoints/pods.api.ts +++ b/src/renderer/api/endpoints/pods.api.ts @@ -152,7 +152,16 @@ export interface IPodContainerStatus { reason: string; }; }; - lastState: {}; + lastState: { + [index: string]: object; + terminated?: { + startedAt: string; + finishedAt: string; + exitCode: number; + reason: string; + containerID: string; + }; + }; ready: boolean; restartCount: number; image: string; diff --git a/src/renderer/components/+add-cluster/add-cluster.route.ts b/src/renderer/components/+add-cluster/add-cluster.route.ts index 21f7522f0f..ba3ffcd104 100644 --- a/src/renderer/components/+add-cluster/add-cluster.route.ts +++ b/src/renderer/components/+add-cluster/add-cluster.route.ts @@ -1,5 +1,5 @@ -import { RouteProps } from "react-router"; -import { buildURL } from "../../navigation"; +import type { RouteProps } from "react-router"; +import { buildURL } from "../../../common/utils/buildUrl"; export const addClusterRoute: RouteProps = { path: "/add-cluster" diff --git a/src/renderer/components/+add-cluster/add-cluster.tsx b/src/renderer/components/+add-cluster/add-cluster.tsx index 60d609d4f0..0751dd2d8c 100644 --- a/src/renderer/components/+add-cluster/add-cluster.tsx +++ b/src/renderer/components/+add-cluster/add-cluster.tsx @@ -46,6 +46,7 @@ export class AddCluster extends React.Component { @observable dropAreaActive = false; componentDidMount() { + clusterStore.setActive(null); this.setKubeConfig(userStore.kubeConfigPath); } @@ -118,17 +119,16 @@ export class AddCluster extends React.Component { } } + @action addClusters = () => { - const configValidationErrors:string[] = []; let newClusters: ClusterModel[] = []; - try { if (!this.selectedContexts.length) { this.error = Please select at least one cluster context return; } this.error = "" - this.isWaiting = true + this.isWaiting = true newClusters = this.selectedContexts.filter(context => { try { @@ -138,8 +138,8 @@ export class AddCluster extends React.Component { } catch (err) { this.error = String(err.message) if (err instanceof ExecValidationNotFoundError ) { - Notifications.error(Error while adding cluster(s): {this.error}); - return false; + Notifications.error(Error while adding cluster(s): {this.error}); + return false; } else { throw new Error(err); } @@ -169,7 +169,7 @@ export class AddCluster extends React.Component { clusterStore.setActive(clusterId); navigate(clusterViewURL({ params: { clusterId } })); } else { - if (newClusters.length > 1) { + if (newClusters.length > 1) { Notifications.ok( Successfully imported {newClusters.length} cluster(s) ); diff --git a/src/renderer/components/+apps-helm-charts/helm-charts.route.ts b/src/renderer/components/+apps-helm-charts/helm-charts.route.ts index 181f0c47f1..047ff656b6 100644 --- a/src/renderer/components/+apps-helm-charts/helm-charts.route.ts +++ b/src/renderer/components/+apps-helm-charts/helm-charts.route.ts @@ -1,6 +1,6 @@ -import { RouteProps } from "react-router" +import type { RouteProps } from "react-router"; +import { buildURL } from "../../../common/utils/buildUrl"; import { appsRoute } from "../+apps/apps.route"; -import { buildURL } from "../../navigation"; export const helmChartsRoute: RouteProps = { path: appsRoute.path + "/charts/:repo?/:chartName?" diff --git a/src/renderer/components/+apps-releases/release.route.ts b/src/renderer/components/+apps-releases/release.route.ts index 673a875b08..f874fefe67 100644 --- a/src/renderer/components/+apps-releases/release.route.ts +++ b/src/renderer/components/+apps-releases/release.route.ts @@ -1,6 +1,6 @@ -import { RouteProps } from "react-router" +import type { RouteProps } from "react-router"; +import { buildURL } from "../../../common/utils/buildUrl"; import { appsRoute } from "../+apps/apps.route"; -import { buildURL } from "../../navigation"; export const releaseRoute: RouteProps = { path: appsRoute.path + "/releases/:namespace?/:name?" diff --git a/src/renderer/components/+apps/apps.route.ts b/src/renderer/components/+apps/apps.route.ts index 065fbe05d6..5ed0671ace 100644 --- a/src/renderer/components/+apps/apps.route.ts +++ b/src/renderer/components/+apps/apps.route.ts @@ -1,5 +1,5 @@ -import { RouteProps } from "react-router"; -import { buildURL } from "../../navigation"; +import type { RouteProps } from "react-router"; +import { buildURL } from "../../../common/utils/buildUrl"; export const appsRoute: RouteProps = { path: "/apps", diff --git a/src/renderer/components/+cluster-settings/cluster-settings.route.ts b/src/renderer/components/+cluster-settings/cluster-settings.route.ts index a2c7a45fd8..3d1b1b2737 100644 --- a/src/renderer/components/+cluster-settings/cluster-settings.route.ts +++ b/src/renderer/components/+cluster-settings/cluster-settings.route.ts @@ -1,6 +1,6 @@ import type { IClusterViewRouteParams } from "../cluster-manager/cluster-view.route"; -import { RouteProps } from "react-router"; -import { buildURL } from "../../navigation"; +import type { RouteProps } from "react-router"; +import { buildURL } from "../../../common/utils/buildUrl"; export interface IClusterSettingsRouteParams extends IClusterViewRouteParams { } diff --git a/src/renderer/components/+cluster-settings/cluster-settings.tsx b/src/renderer/components/+cluster-settings/cluster-settings.tsx index 5404c46952..fc6cca3cce 100644 --- a/src/renderer/components/+cluster-settings/cluster-settings.tsx +++ b/src/renderer/components/+cluster-settings/cluster-settings.tsx @@ -1,8 +1,9 @@ import "./cluster-settings.scss"; import React from "react"; -import { autorun } from "mobx"; -import { disposeOnUnmount, observer } from "mobx-react"; +import { reaction } from "mobx"; +import { RouteComponentProps } from "react-router"; +import { observer, disposeOnUnmount } from "mobx-react"; import { Features } from "./features"; import { Removal } from "./removal"; import { Status } from "./status"; @@ -11,7 +12,6 @@ import { Cluster } from "../../../main/cluster"; import { ClusterIcon } from "../cluster-icon"; import { IClusterSettingsRouteParams } from "./cluster-settings.route"; import { clusterStore } from "../../../common/cluster-store"; -import { RouteComponentProps } from "react-router"; import { clusterIpc } from "../../../common/cluster-ipc"; import { PageLayout } from "../layout/page-layout"; @@ -20,16 +20,23 @@ interface Props extends RouteComponentProps { @observer export class ClusterSettings extends React.Component { - get cluster(): Cluster { - return clusterStore.getById(this.props.match.params.clusterId); + get clusterId() { + return this.props.match.params.clusterId } - async componentDidMount() { - disposeOnUnmount(this, - autorun(() => { - this.refreshCluster(); + get cluster(): Cluster { + return clusterStore.getById(this.clusterId); + } + + componentDidMount() { + disposeOnUnmount(this, [ + reaction(() => this.cluster, this.refreshCluster, { + fireImmediately: true, + }), + reaction(() => this.clusterId, clusterId => clusterStore.setActive(clusterId), { + fireImmediately: true, }) - ) + ]) } refreshCluster = async () => { diff --git a/src/renderer/components/+cluster/cluster.route.ts b/src/renderer/components/+cluster/cluster.route.ts index f541d83945..fbe1c47b86 100644 --- a/src/renderer/components/+cluster/cluster.route.ts +++ b/src/renderer/components/+cluster/cluster.route.ts @@ -1,5 +1,5 @@ -import { RouteProps } from "react-router"; -import { buildURL } from "../../navigation"; +import type { RouteProps } from "react-router"; +import { buildURL } from "../../../common/utils/buildUrl"; export const clusterRoute: RouteProps = { path: "/cluster" diff --git a/src/renderer/components/+config-autoscalers/hpa.route.ts b/src/renderer/components/+config-autoscalers/hpa.route.ts index 500b260062..0828db146b 100644 --- a/src/renderer/components/+config-autoscalers/hpa.route.ts +++ b/src/renderer/components/+config-autoscalers/hpa.route.ts @@ -1,5 +1,5 @@ -import { RouteProps } from "react-router"; -import { buildURL } from "../../navigation"; +import type { RouteProps } from "react-router"; +import { buildURL } from "../../../common/utils/buildUrl"; export const hpaRoute: RouteProps = { path: "/hpa" diff --git a/src/renderer/components/+config-maps/config-maps.route.ts b/src/renderer/components/+config-maps/config-maps.route.ts index 9f68afcfeb..3f42cf7e76 100644 --- a/src/renderer/components/+config-maps/config-maps.route.ts +++ b/src/renderer/components/+config-maps/config-maps.route.ts @@ -1,5 +1,5 @@ -import { RouteProps } from "react-router"; -import { buildURL } from "../../navigation"; +import type { RouteProps } from "react-router"; +import { buildURL } from "../../../common/utils/buildUrl"; export const configMapsRoute: RouteProps = { path: "/configmaps" diff --git a/src/renderer/components/+config-pod-disruption-budgets/pod-disruption-budgets.route.ts b/src/renderer/components/+config-pod-disruption-budgets/pod-disruption-budgets.route.ts index f7a3206f0f..b35f30bb8b 100644 --- a/src/renderer/components/+config-pod-disruption-budgets/pod-disruption-budgets.route.ts +++ b/src/renderer/components/+config-pod-disruption-budgets/pod-disruption-budgets.route.ts @@ -1,5 +1,5 @@ -import { RouteProps } from "react-router"; -import { buildURL } from "../../navigation"; +import type { RouteProps } from "react-router"; +import { buildURL } from "../../../common/utils/buildUrl"; export const pdbRoute: RouteProps = { path: "/poddisruptionbudgets" diff --git a/src/renderer/components/+config-resource-quotas/resource-quotas.route.ts b/src/renderer/components/+config-resource-quotas/resource-quotas.route.ts index 1040f2dc09..ee6966da97 100644 --- a/src/renderer/components/+config-resource-quotas/resource-quotas.route.ts +++ b/src/renderer/components/+config-resource-quotas/resource-quotas.route.ts @@ -1,5 +1,5 @@ -import { RouteProps } from "react-router"; -import { buildURL } from "../../navigation"; +import type { RouteProps } from "react-router"; +import { buildURL } from "../../../common/utils/buildUrl"; export const resourceQuotaRoute: RouteProps = { path: "/resourcequotas" diff --git a/src/renderer/components/+config-secrets/secrets.route.ts b/src/renderer/components/+config-secrets/secrets.route.ts index 1dcce30e2f..bd5f9a442d 100644 --- a/src/renderer/components/+config-secrets/secrets.route.ts +++ b/src/renderer/components/+config-secrets/secrets.route.ts @@ -1,5 +1,5 @@ -import { RouteProps } from "react-router"; -import { buildURL } from "../../navigation"; +import type { RouteProps } from "react-router"; +import { buildURL } from "../../../common/utils/buildUrl"; export const secretsRoute: RouteProps = { path: "/secrets" diff --git a/src/renderer/components/+config/config.route.ts b/src/renderer/components/+config/config.route.ts index 9b114c510b..f480e84952 100644 --- a/src/renderer/components/+config/config.route.ts +++ b/src/renderer/components/+config/config.route.ts @@ -1,7 +1,7 @@ import { RouteProps } from "react-router"; -import { configMapsURL } from "../+config-maps"; import { Config } from "./config"; -import { IURLParams } from "../../navigation"; +import { IURLParams } from "../../../common/utils/buildUrl"; +import { configMapsURL } from "../+config-maps/config-maps.route"; export const configRoute: RouteProps = { get path() { diff --git a/src/renderer/components/+config/config.tsx b/src/renderer/components/+config/config.tsx index 17c0c5c575..7e42c7fcc8 100644 --- a/src/renderer/components/+config/config.tsx +++ b/src/renderer/components/+config/config.tsx @@ -10,8 +10,8 @@ import { resourceQuotaRoute, ResourceQuotas, resourceQuotaURL } from "../+config import { PodDisruptionBudgets, pdbRoute, pdbURL } from "../+config-pod-disruption-budgets"; import { configURL } from "./config.route"; import { HorizontalPodAutoscalers, hpaRoute, hpaURL } from "../+config-autoscalers"; -import { buildURL } from "../../navigation"; import { isAllowedResource } from "../../../common/rbac" +import { buildURL } from "../../../common/utils/buildUrl"; export const certificatesURL = buildURL("/certificates"); export const issuersURL = buildURL("/issuers"); diff --git a/src/renderer/components/+custom-resources/crd.route.ts b/src/renderer/components/+custom-resources/crd.route.ts index a169149eff..f9affeb903 100644 --- a/src/renderer/components/+custom-resources/crd.route.ts +++ b/src/renderer/components/+custom-resources/crd.route.ts @@ -1,5 +1,5 @@ -import { RouteProps } from "react-router"; -import { buildURL } from "../../navigation"; +import type { RouteProps } from "react-router"; +import { buildURL } from "../../../common/utils/buildUrl"; export const crdRoute: RouteProps = { path: "/crd" diff --git a/src/renderer/components/+events/events.route.ts b/src/renderer/components/+events/events.route.ts index 862e54f731..b7d96824f6 100644 --- a/src/renderer/components/+events/events.route.ts +++ b/src/renderer/components/+events/events.route.ts @@ -1,5 +1,5 @@ -import { RouteProps } from "react-router"; -import { buildURL } from "../../navigation"; +import type { RouteProps } from "react-router"; +import { buildURL } from "../../../common/utils/buildUrl"; export const eventRoute: RouteProps = { path: "/events" diff --git a/src/renderer/components/+landing-page/landing-page.route.ts b/src/renderer/components/+landing-page/landing-page.route.ts index 3b3e17a6c2..344cc48033 100644 --- a/src/renderer/components/+landing-page/landing-page.route.ts +++ b/src/renderer/components/+landing-page/landing-page.route.ts @@ -1,5 +1,5 @@ -import { RouteProps } from "react-router"; -import { buildURL } from "../../navigation"; +import type { RouteProps } from "react-router"; +import { buildURL } from "../../../common/utils/buildUrl"; export const landingRoute: RouteProps = { path: "/landing" diff --git a/src/renderer/components/+namespaces/namespaces.route.ts b/src/renderer/components/+namespaces/namespaces.route.ts index 3573968101..9d901b6ada 100644 --- a/src/renderer/components/+namespaces/namespaces.route.ts +++ b/src/renderer/components/+namespaces/namespaces.route.ts @@ -1,5 +1,5 @@ -import { RouteProps } from "react-router" -import { buildURL } from "../../navigation"; +import type { RouteProps } from "react-router"; +import { buildURL } from "../../../common/utils/buildUrl"; export const namespacesRoute: RouteProps = { path: "/namespaces" diff --git a/src/renderer/components/+network-endpoints/endpoints.route.ts b/src/renderer/components/+network-endpoints/endpoints.route.ts index 932ff5eed5..98ecb67bdb 100644 --- a/src/renderer/components/+network-endpoints/endpoints.route.ts +++ b/src/renderer/components/+network-endpoints/endpoints.route.ts @@ -1,5 +1,5 @@ -import { RouteProps } from "react-router" -import { buildURL } from "../../navigation"; +import type { RouteProps } from "react-router"; +import { buildURL } from "../../../common/utils/buildUrl"; export const endpointRoute: RouteProps = { path: "/endpoints" diff --git a/src/renderer/components/+network-ingresses/ingresses.route.ts b/src/renderer/components/+network-ingresses/ingresses.route.ts index 9a21ad56c5..1c1f2941f4 100644 --- a/src/renderer/components/+network-ingresses/ingresses.route.ts +++ b/src/renderer/components/+network-ingresses/ingresses.route.ts @@ -1,5 +1,5 @@ -import { RouteProps } from "react-router" -import { buildURL } from "../../navigation"; +import type { RouteProps } from "react-router"; +import { buildURL } from "../../../common/utils/buildUrl"; export const ingressRoute: RouteProps = { path: "/ingresses" diff --git a/src/renderer/components/+network-ingresses/ingresses.tsx b/src/renderer/components/+network-ingresses/ingresses.tsx index 47ac9b3b47..7167e0ba7f 100644 --- a/src/renderer/components/+network-ingresses/ingresses.tsx +++ b/src/renderer/components/+network-ingresses/ingresses.tsx @@ -39,12 +39,14 @@ export class Ingresses extends React.Component { renderTableHeader={[ { title: Name, className: "name", sortBy: sortBy.name }, { title: Namespace, className: "namespace", sortBy: sortBy.namespace }, + { title: LoadBalancers, className: "loadbalancers" }, { title: Rules, className: "rules" }, { title: Age, className: "age", sortBy: sortBy.age }, ]} renderTableContents={(ingress: Ingress) => [ ingress.getName(), ingress.getNs(), + ingress.getLoadBalancers().map(lb =>

{lb}

), ingress.getRoutes().map(route =>

{route}

), ingress.getAge(), ]} diff --git a/src/renderer/components/+network-policies/network-policies.route.ts b/src/renderer/components/+network-policies/network-policies.route.ts index 60203ed79c..ab609200ce 100644 --- a/src/renderer/components/+network-policies/network-policies.route.ts +++ b/src/renderer/components/+network-policies/network-policies.route.ts @@ -1,5 +1,5 @@ -import { RouteProps } from "react-router" -import { buildURL } from "../../navigation"; +import type { RouteProps } from "react-router"; +import { buildURL } from "../../../common/utils/buildUrl"; export const networkPoliciesRoute: RouteProps = { path: "/network-policies" diff --git a/src/renderer/components/+network-services/services.route.ts b/src/renderer/components/+network-services/services.route.ts index 8bc6adf3f5..8cc42090a7 100644 --- a/src/renderer/components/+network-services/services.route.ts +++ b/src/renderer/components/+network-services/services.route.ts @@ -1,5 +1,5 @@ -import { RouteProps } from "react-router" -import { buildURL } from "../../navigation"; +import type { RouteProps } from "react-router"; +import { buildURL } from "../../../common/utils/buildUrl"; export const servicesRoute: RouteProps = { path: "/services" diff --git a/src/renderer/components/+network/network.route.ts b/src/renderer/components/+network/network.route.ts index adbf909a27..3029b85fa2 100644 --- a/src/renderer/components/+network/network.route.ts +++ b/src/renderer/components/+network/network.route.ts @@ -1,7 +1,7 @@ import { RouteProps } from "react-router" import { Network } from "./network"; import { servicesURL } from "../+network-services"; -import { IURLParams } from "../../navigation"; +import { IURLParams } from "../../../common/utils/buildUrl"; export const networkRoute: RouteProps = { get path() { diff --git a/src/renderer/components/+nodes/nodes.route.ts b/src/renderer/components/+nodes/nodes.route.ts index 3faf749acd..cf03a8733d 100644 --- a/src/renderer/components/+nodes/nodes.route.ts +++ b/src/renderer/components/+nodes/nodes.route.ts @@ -1,5 +1,5 @@ -import { RouteProps } from "react-router" -import { buildURL } from "../../navigation"; +import type { RouteProps } from "react-router"; +import { buildURL } from "../../../common/utils/buildUrl"; export const nodesRoute: RouteProps = { path: "/nodes" diff --git a/src/renderer/components/+pod-security-policies/pod-security-policies.route.ts b/src/renderer/components/+pod-security-policies/pod-security-policies.route.ts index f1a82f4c10..e69f711bea 100644 --- a/src/renderer/components/+pod-security-policies/pod-security-policies.route.ts +++ b/src/renderer/components/+pod-security-policies/pod-security-policies.route.ts @@ -1,5 +1,5 @@ -import { RouteProps } from "react-router" -import { buildURL } from "../../navigation"; +import type { RouteProps } from "react-router"; +import { buildURL } from "../../../common/utils/buildUrl"; export const podSecurityPoliciesRoute: RouteProps = { path: "/pod-security-policies" diff --git a/src/renderer/components/+preferences/preferences.route.ts b/src/renderer/components/+preferences/preferences.route.ts index 6e71a88963..8fc7e70b0e 100644 --- a/src/renderer/components/+preferences/preferences.route.ts +++ b/src/renderer/components/+preferences/preferences.route.ts @@ -1,5 +1,5 @@ -import { RouteProps } from "react-router"; -import { buildURL } from "../../navigation"; +import type { RouteProps } from "react-router"; +import { buildURL } from "../../../common/utils/buildUrl"; export const preferencesRoute: RouteProps = { path: "/preferences" diff --git a/src/renderer/components/+preferences/preferences.scss b/src/renderer/components/+preferences/preferences.scss index 7fd3a44445..1fd6469494 100644 --- a/src/renderer/components/+preferences/preferences.scss +++ b/src/renderer/components/+preferences/preferences.scss @@ -21,4 +21,8 @@ display: none; } } + + .Checkbox { + align-self: start; // limit clickable area to checkbox + text + } } \ No newline at end of file diff --git a/src/renderer/components/+preferences/preferences.tsx b/src/renderer/components/+preferences/preferences.tsx index 613cc25f61..22cbd4a7b1 100644 --- a/src/renderer/components/+preferences/preferences.tsx +++ b/src/renderer/components/+preferences/preferences.tsx @@ -156,6 +156,13 @@ export class Preferences extends React.Component { })} +

Auto start-up

+ Automatically start Lens on login} + value={preferences.openAtLogin} + onChange={v => preferences.openAtLogin = v} + /> +

Certificate Trust

Allow untrusted Certificate Authorities} diff --git a/src/renderer/components/+storage-classes/storage-classes.route.ts b/src/renderer/components/+storage-classes/storage-classes.route.ts index d47d8be7eb..2f809ef9f3 100644 --- a/src/renderer/components/+storage-classes/storage-classes.route.ts +++ b/src/renderer/components/+storage-classes/storage-classes.route.ts @@ -1,5 +1,5 @@ -import { RouteProps } from "react-router" -import { buildURL } from "../../navigation"; +import type { RouteProps } from "react-router"; +import { buildURL } from "../../../common/utils/buildUrl"; export const storageClassesRoute: RouteProps = { path: "/storage-classes" diff --git a/src/renderer/components/+storage-volume-claims/volume-claims.route.ts b/src/renderer/components/+storage-volume-claims/volume-claims.route.ts index 7b2496366e..b579623369 100644 --- a/src/renderer/components/+storage-volume-claims/volume-claims.route.ts +++ b/src/renderer/components/+storage-volume-claims/volume-claims.route.ts @@ -1,5 +1,5 @@ -import { RouteProps } from "react-router" -import { buildURL } from "../../navigation"; +import type { RouteProps } from "react-router"; +import { buildURL } from "../../../common/utils/buildUrl"; export const volumeClaimsRoute: RouteProps = { path: "/persistent-volume-claims" diff --git a/src/renderer/components/+storage-volumes/volumes.route.ts b/src/renderer/components/+storage-volumes/volumes.route.ts index 693c4e5ea3..42d4824c99 100644 --- a/src/renderer/components/+storage-volumes/volumes.route.ts +++ b/src/renderer/components/+storage-volumes/volumes.route.ts @@ -1,5 +1,5 @@ -import { RouteProps } from "react-router" -import { buildURL } from "../../navigation"; +import type { RouteProps } from "react-router"; +import { buildURL } from "../../../common/utils/buildUrl"; export const volumesRoute: RouteProps = { path: "/persistent-volumes" diff --git a/src/renderer/components/+storage/storage.route.ts b/src/renderer/components/+storage/storage.route.ts index c5cc36bb24..6a7ab065f9 100644 --- a/src/renderer/components/+storage/storage.route.ts +++ b/src/renderer/components/+storage/storage.route.ts @@ -1,7 +1,7 @@ import { RouteProps } from "react-router" import { volumeClaimsURL } from "../+storage-volume-claims"; import { Storage } from "./storage"; -import { IURLParams } from "../../navigation"; +import { IURLParams } from "../../../common/utils/buildUrl"; export const storageRoute: RouteProps = { get path() { diff --git a/src/renderer/components/+user-management/user-management.route.ts b/src/renderer/components/+user-management/user-management.route.ts index 77493e46d8..04de465e3f 100644 --- a/src/renderer/components/+user-management/user-management.route.ts +++ b/src/renderer/components/+user-management/user-management.route.ts @@ -1,6 +1,6 @@ -import { RouteProps } from "react-router"; +import type { RouteProps } from "react-router"; +import { buildURL, IURLParams } from "../../../common/utils/buildUrl"; import { UserManagement } from "./user-management" -import { buildURL, IURLParams } from "../../navigation"; export const usersManagementRoute: RouteProps = { get path() { @@ -30,9 +30,7 @@ export interface IRolesRouteParams { } // URL-builders +export const usersManagementURL = (params?: IURLParams) => serviceAccountsURL(params); export const serviceAccountsURL = buildURL(serviceAccountsRoute.path) export const roleBindingsURL = buildURL(roleBindingsRoute.path) export const rolesURL = buildURL(rolesRoute.path) -export const usersManagementURL = (params?: IURLParams) => { - return serviceAccountsURL(params); -}; diff --git a/src/renderer/components/+whats-new/whats-new.route.ts b/src/renderer/components/+whats-new/whats-new.route.ts index ee251ff81e..f989d676b7 100644 --- a/src/renderer/components/+whats-new/whats-new.route.ts +++ b/src/renderer/components/+whats-new/whats-new.route.ts @@ -1,5 +1,5 @@ -import { RouteProps } from "react-router"; -import { buildURL } from "../../navigation"; +import type { RouteProps } from "react-router"; +import { buildURL } from "../../../common/utils/buildUrl"; export const whatsNewRoute: RouteProps = { path: "/what-s-new" diff --git a/src/renderer/components/+workloads-pods/pod-container-env.tsx b/src/renderer/components/+workloads-pods/pod-container-env.tsx index fb779a9eaa..2071c959d1 100644 --- a/src/renderer/components/+workloads-pods/pod-container-env.tsx +++ b/src/renderer/components/+workloads-pods/pod-container-env.tsx @@ -1,7 +1,6 @@ import "./pod-container-env.scss"; import React, { useEffect, useState } from "react"; -import flatten from "lodash/flatten"; import { observer } from "mobx-react"; import { Trans } from "@lingui/macro"; import { IPodContainer, Secret } from "../../api/endpoints"; @@ -11,6 +10,7 @@ import { secretsStore } from "../+config-secrets/secrets.store"; import { configMapsStore } from "../+config-maps/config-maps.store"; import { Icon } from "../icon"; import { base64, cssNames } from "../../utils"; +import _ from "lodash"; interface Props { container: IPodContainer; @@ -40,7 +40,9 @@ export const ContainerEnvironment = observer((props: Props) => { ) const renderEnv = () => { - return env.map(variable => { + const orderedEnv = _.sortBy(env, 'name'); + + return orderedEnv.map(variable => { const { name, value, valueFrom } = variable let secretValue = null @@ -89,7 +91,7 @@ export const ContainerEnvironment = observer((props: Props) => { )) }) - return flatten(envVars) + return _.flatten(envVars) } return ( diff --git a/src/renderer/components/+workloads-pods/pod-details-container.tsx b/src/renderer/components/+workloads-pods/pod-details-container.tsx index 79180b1130..df00721afd 100644 --- a/src/renderer/components/+workloads-pods/pod-details-container.tsx +++ b/src/renderer/components/+workloads-pods/pod-details-container.tsx @@ -2,7 +2,7 @@ import "./pod-details-container.scss" import React from "react"; import { t, Trans } from "@lingui/macro"; -import { IPodContainer, Pod } from "../../api/endpoints"; +import { IPodContainer, IPodContainerStatus, Pod } from "../../api/endpoints"; import { DrawerItem } from "../drawer"; import { cssNames } from "../../utils"; import { StatusBrick } from "../status-brick"; @@ -21,12 +21,37 @@ interface Props { } export class PodDetailsContainer extends React.Component { + + renderStatus(state: string, status: IPodContainerStatus) { + const ready = status ? status.ready : "" + return ( + + {state}{ready ? `, ${_i18n._(t`ready`)}` : ""} + {state === 'terminated' ? ` - ${status.state.terminated.reason} (${_i18n._(t`exit code`)}: ${status.state.terminated.exitCode})` : ''} + + ); + } + + renderLastState(lastState: string, status: IPodContainerStatus) { + if (lastState === 'terminated') { + return ( + + {lastState}
+ {_i18n._(t`Reason`)}: {status.lastState.terminated.reason} - {_i18n._(t`exit code`)}: {status.lastState.terminated.exitCode}
+ {_i18n._(t`Started at`)}: {status.lastState.terminated.startedAt}
+ {_i18n._(t`Finished at`)}: {status.lastState.terminated.finishedAt}
+
+ ) + } + } + render() { const { pod, container, metrics } = this.props if (!pod || !container) return null const { name, image, imagePullPolicy, ports, volumeMounts, command, args } = container const status = pod.getContainerStatuses().find(status => status.name === container.name) const state = status ? Object.keys(status.state)[0] : "" + const lastState = status ? Object.keys(status.lastState)[0] : "" const ready = status ? status.ready : "" const liveness = pod.getLivenessProbe(container) const readiness = pod.getReadinessProbe(container) @@ -48,10 +73,12 @@ export class PodDetailsContainer extends React.Component { } {status && Status}> - - {state}{ready ? `, ${_i18n._(t`ready`)}` : ""} - {state === 'terminated' ? ` - ${status.state.terminated.reason} (${_i18n._(t`exit code`)}: ${status.state.terminated.exitCode})` : ''} - + {this.renderStatus(state, status)} + + } + {lastState && + Last Status}> + {this.renderLastState(lastState, status)} } Image}> diff --git a/src/renderer/components/+workloads/workloads.route.ts b/src/renderer/components/+workloads/workloads.route.ts index 5586102faf..47044b0d18 100644 --- a/src/renderer/components/+workloads/workloads.route.ts +++ b/src/renderer/components/+workloads/workloads.route.ts @@ -1,7 +1,7 @@ -import { RouteProps } from "react-router" -import { Workloads } from "./workloads"; -import { buildURL, IURLParams } from "../../navigation"; +import type { RouteProps } from "react-router"; +import { buildURL, IURLParams } from "../../../common/utils/buildUrl"; import { KubeResource } from "../../../common/rbac"; +import { Workloads } from "./workloads"; export const workloadsRoute: RouteProps = { get path() { diff --git a/src/renderer/components/+workspaces/workspaces.route.ts b/src/renderer/components/+workspaces/workspaces.route.ts index bfdbe012a6..37c1239e6c 100644 --- a/src/renderer/components/+workspaces/workspaces.route.ts +++ b/src/renderer/components/+workspaces/workspaces.route.ts @@ -1,5 +1,5 @@ -import { RouteProps } from "react-router"; -import { buildURL } from "../../navigation"; +import type { RouteProps } from "react-router"; +import { buildURL } from "../../../common/utils/buildUrl"; export const workspacesRoute: RouteProps = { path: "/workspaces" diff --git a/src/renderer/components/cluster-manager/cluster-manager.tsx b/src/renderer/components/cluster-manager/cluster-manager.tsx index aca2163402..785fe56e27 100644 --- a/src/renderer/components/cluster-manager/cluster-manager.tsx +++ b/src/renderer/components/cluster-manager/cluster-manager.tsx @@ -1,4 +1,5 @@ import "./cluster-manager.scss" + import React from "react"; import { Redirect, Route, Switch } from "react-router"; import { comparer, reaction } from "mobx"; @@ -11,14 +12,17 @@ import { Workspaces, workspacesRoute } from "../+workspaces"; import { AddCluster, addClusterRoute } from "../+add-cluster"; import { ClusterView } from "./cluster-view"; import { ClusterSettings, clusterSettingsRoute } from "../+cluster-settings"; -import { clusterViewRoute, clusterViewURL, getMatchedCluster, getMatchedClusterId } from "./cluster-view.route"; +import { clusterViewRoute, clusterViewURL } from "./cluster-view.route"; import { clusterStore } from "../../../common/cluster-store"; import { hasLoadedView, initView, lensViews, refreshViews } from "./lens-views"; import { globalPageRegistry } from "../../../extensions/registries/page-registry"; +import { getMatchedClusterId } from "../../navigation"; @observer export class ClusterManager extends React.Component { componentDidMount() { + const getMatchedCluster = () => clusterStore.getById(getMatchedClusterId()); + disposeOnUnmount(this, [ reaction(getMatchedClusterId, initView, { fireImmediately: true @@ -55,7 +59,7 @@ export class ClusterManager extends React.Component { return (
-
+
@@ -66,11 +70,11 @@ export class ClusterManager extends React.Component { {globalPageRegistry.getItems().map(({ path, url = String(path), components: { Page } }) => { return })} - +
- - + +
) } diff --git a/src/renderer/components/cluster-manager/cluster-view.route.ts b/src/renderer/components/cluster-manager/cluster-view.route.ts index 82e99497d5..bb66f56005 100644 --- a/src/renderer/components/cluster-manager/cluster-view.route.ts +++ b/src/renderer/components/cluster-manager/cluster-view.route.ts @@ -1,8 +1,5 @@ -import { reaction } from "mobx"; -import { ipcRenderer } from "electron"; -import { matchPath, RouteProps } from "react-router"; -import { buildURL, navigation } from "../../navigation"; -import { clusterStore } from "../../../common/cluster-store"; +import type { RouteProps } from "react-router"; +import { buildURL } from "../../../common/utils/buildUrl"; export interface IClusterViewRouteParams { clusterId: string; @@ -14,33 +11,3 @@ export const clusterViewRoute: RouteProps = { } export const clusterViewURL = buildURL(clusterViewRoute.path) - -export function getMatchedClusterId(): string { - const matched = matchPath(navigation.location.pathname, { - exact: true, - path: clusterViewRoute.path - }) - if (matched) { - return matched.params.clusterId; - } -} - -export function getMatchedCluster() { - return clusterStore.getById(getMatchedClusterId()) -} - -if (ipcRenderer) { - if (process.isMainFrame) { - // Keep track of active cluster-id for handling IPC/menus/etc. - reaction(() => getMatchedClusterId(), clusterId => { - ipcRenderer.send("cluster-view:current-id", clusterId); - }, { - fireImmediately: true - }) - } - - // Reload dashboard - ipcRenderer.on("menu:reload", () => { - location.reload(); - }); -} diff --git a/src/renderer/components/cluster-manager/cluster-view.tsx b/src/renderer/components/cluster-manager/cluster-view.tsx index 7a7c96a856..be3a955558 100644 --- a/src/renderer/components/cluster-manager/cluster-view.tsx +++ b/src/renderer/components/cluster-manager/cluster-view.tsx @@ -1,14 +1,37 @@ import "./cluster-view.scss" import React from "react"; -import { observer } from "mobx-react"; -import { getMatchedCluster } from "./cluster-view.route"; +import { reaction } from "mobx"; +import { disposeOnUnmount, observer } from "mobx-react"; +import { RouteComponentProps } from "react-router"; +import { IClusterViewRouteParams } from "./cluster-view.route"; import { ClusterStatus } from "./cluster-status"; import { hasLoadedView } from "./lens-views"; +import { Cluster } from "../../../main/cluster"; +import { clusterStore } from "../../../common/cluster-store"; + +interface Props extends RouteComponentProps { +} @observer -export class ClusterView extends React.Component { +export class ClusterView extends React.Component { + get clusterId() { + return this.props.match.params.clusterId; + } + + get cluster(): Cluster { + return clusterStore.getById(this.clusterId); + } + + async componentDidMount() { + disposeOnUnmount(this, [ + reaction(() => this.clusterId, clusterId => clusterStore.setActive(clusterId), { + fireImmediately: true, + }) + ]) + } + render() { - const cluster = getMatchedCluster(); + const { cluster } = this; const showStatus = cluster && (!cluster.available || !hasLoadedView(cluster.id) || !cluster.ready) return (
diff --git a/src/renderer/components/cluster-manager/clusters-menu.tsx b/src/renderer/components/cluster-manager/clusters-menu.tsx index 46d10a37ac..8e2db6ceaa 100644 --- a/src/renderer/components/cluster-manager/clusters-menu.tsx +++ b/src/renderer/components/cluster-manager/clusters-menu.tsx @@ -1,8 +1,9 @@ -import type { Cluster } from "../../../main/cluster"; import "./clusters-menu.scss" -import { remote } from "electron" import React from "react"; +import { remote } from "electron" +import type { Cluster } from "../../../main/cluster"; +import { DragDropContext, Draggable, DraggableProvided, Droppable, DroppableProvided, DropResult } from "react-beautiful-dnd"; import { observer } from "mobx-react"; import { _i18n } from "../../i18n"; import { t, Trans } from "@lingui/macro"; @@ -21,7 +22,6 @@ import { Tooltip } from "../tooltip"; import { ConfirmDialog } from "../confirm-dialog"; import { clusterIpc } from "../../../common/cluster-ipc"; import { clusterViewURL } from "./cluster-view.route"; -import { DragDropContext, Draggable, DraggableProvided, Droppable, DroppableProvided, DropResult } from "react-beautiful-dnd"; import { globalPageRegistry } from "../../../extensions/registries/page-registry"; interface Props { @@ -31,13 +31,11 @@ interface Props { @observer export class ClustersMenu extends React.Component { showCluster = (clusterId: ClusterId) => { - clusterStore.setActive(clusterId); navigate(clusterViewURL({ params: { clusterId } })); } addCluster = () => { navigate(addClusterURL()); - clusterStore.setActive(null); } showContextMenu = (cluster: Cluster) => { @@ -47,7 +45,6 @@ export class ClustersMenu extends React.Component { menu.append(new MenuItem({ label: _i18n._(t`Settings`), click: () => { - clusterStore.setActive(cluster.id); navigate(clusterSettingsURL({ params: { clusterId: cluster.id @@ -112,21 +109,14 @@ export class ClustersMenu extends React.Component {
- {(provided: DroppableProvided) => ( -
+ {({ innerRef, droppableProps, placeholder }: DroppableProvided) => ( +
{clusters.map((cluster, index) => { const isActive = cluster.id === clusterStore.activeClusterId; return ( - {(provided: DraggableProvided) => ( -
+ {({ draggableProps, dragHandleProps, innerRef }: DraggableProvided) => ( +
{
)} - )} - )} - {provided.placeholder} + ) + })} + {placeholder}
)} @@ -150,16 +140,14 @@ export class ClustersMenu extends React.Component { Add Cluster - + {newContexts.size > 0 && ( - new} /> + new}/> )}
{globalPageRegistry.getItems().map(({ path, url = String(path), hideInMenu, components: { MenuIcon } }) => { - if (!MenuIcon || hideInMenu) { - return; - } + if (!MenuIcon || hideInMenu) return; return navigate(url)}/> })}
diff --git a/src/renderer/components/cluster-manager/lens-views.ts b/src/renderer/components/cluster-manager/lens-views.ts index d23bdd6072..48c3578a0e 100644 --- a/src/renderer/components/cluster-manager/lens-views.ts +++ b/src/renderer/components/cluster-manager/lens-views.ts @@ -1,6 +1,6 @@ import { observable, when } from "mobx"; import { ClusterId, clusterStore, getClusterFrameUrl } from "../../../common/cluster-store"; -import { getMatchedCluster } from "./cluster-view.route" +import { getMatchedClusterId } from "../../navigation"; import logger from "../../../main/logger"; export interface LensView { @@ -51,7 +51,7 @@ export async function autoCleanOnRemove(clusterId: ClusterId, iframe: HTMLIFrame } export function refreshViews() { - const cluster = getMatchedCluster(); + const cluster = clusterStore.getById(getMatchedClusterId()); lensViews.forEach(({ clusterId, view, isLoaded }) => { const isCurrent = clusterId === cluster?.id; const isReady = cluster?.available && cluster?.ready; diff --git a/src/renderer/components/dock/pod-log-controls.tsx b/src/renderer/components/dock/pod-log-controls.tsx new file mode 100644 index 0000000000..d3ba81e7ab --- /dev/null +++ b/src/renderer/components/dock/pod-log-controls.tsx @@ -0,0 +1,116 @@ +import React from "react"; +import { observer } from "mobx-react"; +import { IPodLogsData, podLogsStore } from "./pod-logs.store"; +import { t, Trans } from "@lingui/macro"; +import { Select, SelectOption } from "../select"; +import { Badge } from "../badge"; +import { Icon } from "../icon"; +import { _i18n } from "../../i18n"; +import { cssNames, downloadFile } from "../../utils"; +import { Pod } from "../../api/endpoints"; + +interface Props { + ready: boolean + tabId: string + tabData: IPodLogsData + logs: string[][] + save: (data: Partial) => void + reload: () => void +} + +export const PodLogControls = observer((props: Props) => { + if (!props.ready) return null; + const { tabData, tabId, save, reload, logs } = props; + const { selectedContainer, showTimestamps, previous } = tabData; + const since = podLogsStore.getTimestamps(podLogsStore.logs.get(tabId)[0]); + const pod = new Pod(tabData.pod); + const toggleTimestamps = () => { + save({ showTimestamps: !showTimestamps }); + } + + const togglePrevious = () => { + save({ previous: !previous }); + reload(); + } + + const downloadLogs = () => { + const fileName = selectedContainer ? selectedContainer.name : pod.getName(); + const [oldLogs, newLogs] = logs; + downloadFile(fileName + ".log", [...oldLogs, ...newLogs].join("\n"), "text/plain"); + } + + const onContainerChange = (option: SelectOption) => { + const { containers, initContainers } = tabData; + save({ + selectedContainer: containers + .concat(initContainers) + .find(container => container.name === option.value) + }) + reload(); + } + + const containerSelectOptions = () => { + const { containers, initContainers } = tabData; + return [ + { + label: _i18n._(t`Containers`), + options: containers.map(container => { + return { value: container.name } + }), + }, + { + label: _i18n._(t`Init Containers`), + options: initContainers.map(container => { + return { value: container.name } + }), + } + ]; + } + + const formatOptionLabel = (option: SelectOption) => { + const { value, label } = option; + return label || <> {value}; + } + + return ( +
+ Pod: + Namespace: + Container + - Lines -