diff --git a/.azure-pipelines.yml b/.azure-pipelines.yml index 009418b1c0..56e5fbe57e 100644 --- a/.azure-pipelines.yml +++ b/.azure-pipelines.yml @@ -149,6 +149,11 @@ jobs: condition: "and(succeeded(), startsWith(variables['Build.SourceBranch'], 'refs/tags/'))" env: GH_TOKEN: $(GH_TOKEN) + - script: make publish-npm + displayName: Publish npm package + condition: "and(succeeded(), startsWith(variables['Build.SourceBranch'], 'refs/tags/'))" + env: + NPM_TOKEN: $(NPM_TOKEN) - bash: | mkdir -p "$AZURE_CACHE_FOLDER" tar -czf "$AZURE_CACHE_FOLDER/yarn-cache.tar.gz" "$YARN_CACHE_FOLDER" diff --git a/.eslintrc.js b/.eslintrc.js index 743c7e26fc..062675e75c 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -1,4 +1,5 @@ module.exports = { + ignorePatterns: ["src/extensions/npm/extensions/api.d.ts"], overrides: [ { files: [ diff --git a/LICENSE b/LICENSE index 841736290b..b7a6d74205 100644 --- a/LICENSE +++ b/LICENSE @@ -1,8 +1,13 @@ -MIT License - Copyright (c) 2020 Mirantis, Inc. -All rights reserved. +Portions of this software are licensed as follows: + +* All content residing under the "docs/" directory of this repository, if that +directory exists, is licensed under "Creative Commons: CC BY-SA 4.0 license". +* All third party components incorporated into the Lens Software are licensed +under the original license provided by the owner of the applicable component. +* Content outside of the above mentioned directories or restrictions above is +available under the "MIT" license as defined below. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/Makefile b/Makefile index 540a8ca0e4..514e64ae49 100644 --- a/Makefile +++ b/Makefile @@ -59,10 +59,12 @@ build-extensions: $(foreach dir, $(wildcard $(EXTENSIONS_DIR)/*), $(MAKE) -C $(dir) build;) build-npm: + yarn compile:extension-types yarn npm:fix-package-version publish-npm: build-npm - cd src/extensions/npm/extensions && npm publish + npm config set '//registry.npmjs.org/:_authToken' "${NPM_TOKEN}" + cd src/extensions/npm/extensions && npm publish --access=public 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/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/renderer.tsx b/extensions/example-extension/renderer.tsx index 4deb08558b..a1fb546df7 100644 --- a/extensions/example-extension/renderer.tsx +++ b/extensions/example-extension/renderer.tsx @@ -1,26 +1,16 @@ -import { LensRendererExtension, Registry } from "@k8slens/extensions"; -import { ExamplePage, ExampleIcon } from "./page" +import { LensRendererExtension } from "@k8slens/extensions"; +import { ExampleIcon, ExamplePage } from "./page" import React from "react" export default class ExampleExtension extends LensRendererExtension { - onActivate() { - console.log('EXAMPLE EXTENSION RENDERER: ACTIVATED', this.getMeta()); - } - - registerClusterPage(registry: Registry.ClusterPageRegistry) { - this.disposers.push( - registry.add({ - path: "/extension-example", - title: "Example Extension", - components: { - Page: () => , - MenuIcon: ExampleIcon, - } - }) - ) - } - - onDeactivate() { - console.log('EXAMPLE EXTENSION RENDERER: DEACTIVATED', this.getMeta()); - } + clusterPages = [ + { + path: "/extension-example", + title: "Example Extension", + components: { + Page: () => , + MenuIcon: ExampleIcon, + } + } + ] } diff --git a/extensions/metrics-cluster-feature/renderer.tsx b/extensions/metrics-cluster-feature/renderer.tsx index 2b4b4f07e9..58565c13f5 100644 --- a/extensions/metrics-cluster-feature/renderer.tsx +++ b/extensions/metrics-cluster-feature/renderer.tsx @@ -1,25 +1,23 @@ -import { Registry, LensRendererExtension } from "@k8slens/extensions" +import { LensRendererExtension } from "@k8slens/extensions" import { MetricsFeature } from "./src/metrics-feature" import React from "react" export default class ClusterMetricsFeatureExtension extends LensRendererExtension { - registerClusterFeatures(registry: Registry.ClusterFeatureRegistry) { - this.disposers.push( - registry.add({ - title: "Metrics Stack", - components: { - Description: () => { - return ( - + clusterFeatures = [ + { + title: "Metrics Stack", + components: { + Description: () => { + return ( + Enable timeseries data visualization (Prometheus stack) for your cluster. Install this only if you don't have existing Prometheus stack installed. You can see preview of manifests here. - ) - } - }, - feature: new MetricsFeature() - }) - ) - } + ) + } + }, + feature: new MetricsFeature() + } + ] } diff --git a/extensions/node-menu/renderer.tsx b/extensions/node-menu/renderer.tsx index b3e3ffd121..db8232a518 100644 --- a/extensions/node-menu/renderer.tsx +++ b/extensions/node-menu/renderer.tsx @@ -1,21 +1,15 @@ -import { Registry, LensRendererExtension } from "@k8slens/extensions"; +import { LensRendererExtension } from "@k8slens/extensions"; import React from "react" -import { NodeMenu } from "./src/node-menu" +import { NodeMenu, NodeMenuProps } from "./src/node-menu" export default class NodeMenuRendererExtension extends LensRendererExtension { - async onActivate() { - console.log("node-menu extension activated") - } - - registerKubeObjectMenus(registry: Registry.KubeObjectMenuRegistry) { - this.disposers.push( - registry.add({ - kind: "Node", - apiVersions: ["v1"], - components: { - MenuItem: (props) => - } - }) - ) - } + kubeObjectMenuItems = [ + { + kind: "Node", + apiVersions: ["v1"], + components: { + MenuItem: (props: NodeMenuProps) => + } + } + ] } diff --git a/extensions/node-menu/src/node-menu.tsx b/extensions/node-menu/src/node-menu.tsx index 09ef251d60..8c7466694e 100644 --- a/extensions/node-menu/src/node-menu.tsx +++ b/extensions/node-menu/src/node-menu.tsx @@ -1,7 +1,10 @@ import React from "react"; import { Component, K8sApi, Navigation} from "@k8slens/extensions" -export function NodeMenu(props: Component.KubeObjectMenuProps) { +export interface NodeMenuProps extends Component.KubeObjectMenuProps { +} + +export function NodeMenu(props: NodeMenuProps) { const { object: node, toolbar } = props; if (!node) return null; const nodeName = node.getName(); diff --git a/extensions/pod-menu/renderer.tsx b/extensions/pod-menu/renderer.tsx index 1340d835d0..30898da806 100644 --- a/extensions/pod-menu/renderer.tsx +++ b/extensions/pod-menu/renderer.tsx @@ -1,31 +1,23 @@ -import { Registry, LensRendererExtension } from "@k8slens/extensions"; -import { PodShellMenu } from "./src/shell-menu" -import { PodLogsMenu } from "./src/logs-menu" +import { LensRendererExtension } from "@k8slens/extensions"; +import { PodShellMenu, PodShellMenuProps } from "./src/shell-menu" +import { PodLogsMenu, PodLogsMenuProps } from "./src/logs-menu" import React from "react" export default class PodMenuRendererExtension extends LensRendererExtension { - async onActivate() { - console.log("pod-menu extension activated") - } - - registerKubeObjectMenus(registry: Registry.KubeObjectMenuRegistry) { - this.disposers.push( - registry.add({ - kind: "Pod", - apiVersions: ["v1"], - components: { - MenuItem: (props) => - } - }) - ) - this.disposers.push( - registry.add({ - kind: "Pod", - apiVersions: ["v1"], - components: { - MenuItem: (props) => - } - }) - ) - } + kubeObjectMenuItems = [ + { + kind: "Pod", + apiVersions: ["v1"], + components: { + MenuItem: (props: PodShellMenuProps) => + } + }, + { + kind: "Pod", + apiVersions: ["v1"], + components: { + MenuItem: (props: PodLogsMenuProps) => + } + } + ] } diff --git a/extensions/pod-menu/src/logs-menu.tsx b/extensions/pod-menu/src/logs-menu.tsx index e99e454fdf..f80c55cc01 100644 --- a/extensions/pod-menu/src/logs-menu.tsx +++ b/extensions/pod-menu/src/logs-menu.tsx @@ -1,10 +1,10 @@ import React from "react"; import { Component, K8sApi, Util, Navigation } from "@k8slens/extensions"; -interface Props extends Component.KubeObjectMenuProps { +export interface PodLogsMenuProps extends Component.KubeObjectMenuProps { } -export class PodLogsMenu extends React.Component { +export class PodLogsMenu extends React.Component { showLogs(container: K8sApi.IPodContainer) { Navigation.hideDetails(); const pod = this.props.object; diff --git a/extensions/pod-menu/src/shell-menu.tsx b/extensions/pod-menu/src/shell-menu.tsx index 5f19ceca01..772d17a894 100644 --- a/extensions/pod-menu/src/shell-menu.tsx +++ b/extensions/pod-menu/src/shell-menu.tsx @@ -3,10 +3,10 @@ import React from "react"; import { Component, K8sApi, Util, Navigation } from "@k8slens/extensions"; -interface Props extends Component.KubeObjectMenuProps { +export interface PodShellMenuProps extends Component.KubeObjectMenuProps { } -export class PodShellMenu extends React.Component { +export class PodShellMenu extends React.Component { async execShell(container?: string) { Navigation.hideDetails(); const { object: pod } = this.props diff --git a/extensions/support-page/main.ts b/extensions/support-page/main.ts index 133df0b890..70ec3bc026 100644 --- a/extensions/support-page/main.ts +++ b/extensions/support-page/main.ts @@ -1,23 +1,14 @@ -import { LensMainExtension, Registry, windowManager } from "@k8slens/extensions"; +import { LensMainExtension, windowManager } from "@k8slens/extensions"; import { supportPageURL } from "./src/support.route"; export default class SupportPageMainExtension extends LensMainExtension { - async onActivate() { - console.log("support page extension activated") - } - - async registerAppMenus(registry: Registry.MenuRegistry) { - this.disposers.push( - registry.add({ - parentId: "help", - label: "Support", - click() { - windowManager.navigate({ - channel: "menu:navigate", // fixme: use windowManager.ensureMainWindow from Tray's PR - url: supportPageURL(), - }); - } - }) - ) - } + appMenus = [ + { + parentId: "help", + label: "Support", + click() { + windowManager.navigate(supportPageURL()); + } + } + ] } diff --git a/extensions/support-page/renderer.tsx b/extensions/support-page/renderer.tsx index 84e7f87f65..9f52a972bf 100644 --- a/extensions/support-page/renderer.tsx +++ b/extensions/support-page/renderer.tsx @@ -1,39 +1,31 @@ import React from "react"; -import { Component, LensRendererExtension, Navigation, Registry } from "@k8slens/extensions"; +import { Component, LensRendererExtension, Navigation } from "@k8slens/extensions"; import { supportPageRoute, supportPageURL } from "./src/support.route"; import { Support } from "./src/support"; export default class SupportPageRendererExtension extends LensRendererExtension { - async onActivate() { - console.log("support page extension activated") - } + globalPages = [ + { + ...supportPageRoute, + url: supportPageURL(), + hideInMenu: true, + components: { + Page: Support, + } + } + ] - registerGlobalPage(registry: Registry.GlobalPageRegistry) { - this.disposers.push( - registry.add({ - ...supportPageRoute, - url: supportPageURL(), - hideInMenu: true, - components: { - Page: Support, - } - }) - ) - } - - registerStatusBarItem(registry: Registry.StatusBarRegistry) { - this.disposers.push( - registry.add({ - item: ( -
Navigation.navigate(supportPageURL())} - > - - Support -
- ) - }) - ) - } + statusBarItems = [ + { + item: ( +
Navigation.navigate(supportPageURL())} + > + + Support +
+ ) + } + ] } 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/telemetry/renderer.tsx b/extensions/telemetry/renderer.tsx index 3988e97097..04057386bf 100644 --- a/extensions/telemetry/renderer.tsx +++ b/extensions/telemetry/renderer.tsx @@ -1,29 +1,23 @@ -import { LensRendererExtension, Registry } from "@k8slens/extensions"; +import { LensRendererExtension } from "@k8slens/extensions"; import { telemetryPreferencesStore } from "./src/telemetry-preferences-store" import { TelemetryPreferenceHint, TelemetryPreferenceInput } from "./src/telemetry-preference" import { tracker } from "./src/tracker" import React from "react" export default class TelemetryRendererExtension extends LensRendererExtension { + appPreferences = [ + { + title: "Telemetry & Usage Tracking", + components: { + Hint: () => , + Input: () => + } + } + ]; + async onActivate() { console.log("telemetry extension activated") tracker.start() await telemetryPreferencesStore.loadExtension(this) } - - registerAppPreferences(registry: Registry.AppPreferenceRegistry) { - this.disposers.push( - registry.add({ - title: "Telemetry & Usage Tracking", - components: { - Hint: () => , - Input: () => - } - }) - ) - } - - onDeactivate() { - console.log("telemetry extension deactivated") - } } diff --git a/extensions/telemetry/src/tracker.ts b/extensions/telemetry/src/tracker.ts index d5f16334b2..f6ac8c19a4 100644 --- a/extensions/telemetry/src/tracker.ts +++ b/extensions/telemetry/src/tracker.ts @@ -38,8 +38,9 @@ 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 + this.reportInterval = setInterval(() => { + this.reportData() + }, 60 * 60 * 1000) // report every 1h } stop() { diff --git a/integration/__tests__/app.tests.ts b/integration/__tests__/app.tests.ts index eeef7a01f1..c9567a195f 100644 --- a/integration/__tests__/app.tests.ts +++ b/integration/__tests__/app.tests.ts @@ -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 51bf5cd7f7..f5251d1cba 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "kontena-lens", "productName": "Lens", "description": "Lens - The Kubernetes IDE", - "version": "3.6.6", + "version": "4.0.0-alpha.2", "main": "static/build/main.js", "copyright": "© 2020, Mirantis, Inc.", "license": "MIT", @@ -16,12 +16,12 @@ "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", @@ -36,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": { @@ -97,6 +98,11 @@ "to": "static/", "filter": "!**/main.js" }, + { + "from": "build/tray", + "to": "static/icons", + "filter": "*.png" + }, { "from": "extensions/", "to": "./extensions/", @@ -186,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", @@ -198,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", @@ -276,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", @@ -327,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", @@ -339,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/workspace-store.ts b/src/common/workspace-store.ts index 5cb01788f3..b7f2467013 100644 --- a/src/common/workspace-store.ts +++ b/src/common/workspace-store.ts @@ -1,8 +1,6 @@ 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; @@ -57,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 diff --git a/src/extensions/core-api/app.ts b/src/extensions/core-api/app.ts index 192a3222af..f3e44ed001 100644 --- a/src/extensions/core-api/app.ts +++ b/src/extensions/core-api/app.ts @@ -1,5 +1,4 @@ -import { app } from "electron"; import { getAppVersion } from "../../common/utils"; export const version = getAppVersion() -export { isSnap, isWindows, isMac, isLinux, appName } from "../../common/vars" \ No newline at end of file +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 16a989e7ac..3882763681 100644 --- a/src/extensions/core-api/index.ts +++ b/src/extensions/core-api/index.ts @@ -10,9 +10,9 @@ 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 { @@ -22,5 +22,4 @@ export { Store, Util, Registry, - CommonVars, } diff --git a/src/extensions/extension-loader.ts b/src/extensions/extension-loader.ts index 0e831af9f8..c0b2d5ada6 100644 --- a/src/extensions/extension-loader.ts +++ b/src/extensions/extension-loader.ts @@ -36,26 +36,26 @@ export class ExtensionLoader { loadOnMain() { logger.info('[EXTENSIONS-LOADER]: load on main') - this.autoloadExtensions((instance: LensMainExtension) => { - instance.registerAppMenus(menuRegistry); + this.autoloadExtensions((extension: LensMainExtension) => { + extension.registerTo(menuRegistry, extension.appMenus) }) } loadOnClusterManagerRenderer() { logger.info('[EXTENSIONS-LOADER]: load on main renderer (cluster manager)') - this.autoloadExtensions((instance: LensRendererExtension) => { - instance.registerGlobalPage(globalPageRegistry) - instance.registerAppPreferences(appPreferenceRegistry) - instance.registerClusterFeatures(clusterFeatureRegistry) - instance.registerStatusBarItem(statusBarRegistry) + this.autoloadExtensions((extension: LensRendererExtension) => { + extension.registerTo(globalPageRegistry, extension.globalPages) + extension.registerTo(appPreferenceRegistry, extension.appPreferences) + extension.registerTo(clusterFeatureRegistry, extension.clusterFeatures) + extension.registerTo(statusBarRegistry, extension.statusBarItems) }) } loadOnClusterRenderer() { logger.info('[EXTENSIONS-LOADER]: load on cluster renderer (dashboard)') - this.autoloadExtensions((instance: LensRendererExtension) => { - instance.registerClusterPage(clusterPageRegistry) - instance.registerKubeObjectMenus(kubeObjectMenuRegistry) + this.autoloadExtensions((extension: LensRendererExtension) => { + extension.registerTo(clusterPageRegistry, extension.clusterPages) + extension.registerTo(kubeObjectMenuRegistry, extension.kubeObjectMenuItems) }) } diff --git a/src/extensions/extension-manager.ts b/src/extensions/extension-manager.ts index 1e0ebf9387..eb4bad08fb 100644 --- a/src/extensions/extension-manager.ts +++ b/src/extensions/extension-manager.ts @@ -25,10 +25,18 @@ export class ExtensionManager { return extensionPackagesRoot() } + get inTreeTargetPath() { + return path.join(this.extensionPackagesRoot, "extensions") + } + get inTreeFolderPath(): string { return path.resolve(__static, "../extensions"); } + get nodeModulesPath(): string { + return path.join(this.extensionPackagesRoot, "node_modules") + } + get localFolderPath(): string { return path.join(os.homedir(), ".k8slens", "extensions"); } @@ -39,7 +47,13 @@ export class ExtensionManager { async load() { logger.info("[EXTENSION-MANAGER] loading extensions from " + this.extensionPackagesRoot) - await fs.ensureDir(path.join(this.extensionPackagesRoot, "node_modules")) + if (this.inTreeFolderPath !== this.inTreeTargetPath) { + // we need to copy in-tree extensions so that we can symlink them properly on "npm install" + await fs.remove(this.inTreeTargetPath) + await fs.ensureDir(this.inTreeTargetPath) + await fs.copy(this.inTreeFolderPath, this.inTreeTargetPath) + } + await fs.ensureDir(this.nodeModulesPath) await fs.ensureDir(this.localFolderPath) return await this.loadExtensions(); } @@ -55,7 +69,7 @@ export class ExtensionManager { id: manifestJson.name, version: manifestJson.version, name: manifestJson.name, - manifestPath: path.join(this.extensionPackagesRoot, "node_modules", manifestJson.name, "package.json"), + manifestPath: path.join(this.nodeModulesPath, manifestJson.name, "package.json"), manifest: manifestJson } } catch (err) { @@ -80,14 +94,14 @@ export class ExtensionManager { async loadExtensions() { const bundledExtensions = await this.loadBundledExtensions() - const localExtendions = await this.loadFromFolder(this.localFolderPath) - const extensions = bundledExtensions.concat(localExtendions) + const localExtensions = await this.loadFromFolder(this.localFolderPath) + const extensions = bundledExtensions.concat(localExtensions) return new Map(extensions.map(ext => [ext.id, ext])); } async loadBundledExtensions() { const extensions: InstalledExtension[] = [] - const folderPath = this.inTreeFolderPath + const folderPath = this.inTreeTargetPath const bundledExtensions = getBundledExtensions() const paths = await fs.readdir(folderPath); for (const fileName of paths) { diff --git a/src/extensions/lens-extension.ts b/src/extensions/lens-extension.ts index b348cd6868..0921dc066a 100644 --- a/src/extensions/lens-extension.ts +++ b/src/extensions/lens-extension.ts @@ -1,6 +1,7 @@ import { readJsonSync } from "fs-extra"; import { action, observable, toJS } from "mobx"; import logger from "../main/logger"; +import { BaseRegistry } from "./registries/base-registry"; export type ExtensionId = string | ExtensionPackageJsonPath; export type ExtensionPackageJsonPath = string; @@ -25,7 +26,7 @@ export interface ExtensionManifest extends ExtensionModel { export class LensExtension implements ExtensionModel { public id: ExtensionId; public updateUrl: string; - protected disposers: Function[] = []; + protected disposers: (() => void)[] = []; @observable name = ""; @observable description = ""; @@ -77,6 +78,14 @@ export class LensExtension implements ExtensionModel { // mock } + registerTo(registry: BaseRegistry, items: T[] = []) { + const disposers = items.map(item => registry.add(item)); + this.disposers.push(...disposers); + return () => { + this.disposers = this.disposers.filter(disposer => !disposers.includes(disposer)) + }; + } + getMeta() { return toJS({ id: this.id, diff --git a/src/extensions/lens-main-extension.ts b/src/extensions/lens-main-extension.ts index 9ad858d0c6..8e300bda77 100644 --- a/src/extensions/lens-main-extension.ts +++ b/src/extensions/lens-main-extension.ts @@ -1,12 +1,7 @@ +import type { MenuRegistration } from "./registries/menu-registry"; +import { observable } from "mobx"; import { LensExtension } from "./lens-extension" -import type { MenuRegistry } from "./registries/menu-registry"; export class LensMainExtension extends LensExtension { - registerAppMenus(registry: MenuRegistry) { - // - } - - registerPrometheusProviders(registry: any) { - // - } + @observable.shallow appMenus: MenuRegistration[] = [] } diff --git a/src/extensions/lens-renderer-extension.ts b/src/extensions/lens-renderer-extension.ts index 4c2e25ebdf..829079d0f0 100644 --- a/src/extensions/lens-renderer-extension.ts +++ b/src/extensions/lens-renderer-extension.ts @@ -1,28 +1,12 @@ +import type { AppPreferenceRegistration, ClusterFeatureRegistration, KubeObjectMenuRegistration, PageRegistration, StatusBarRegistration } from "./registries" +import { observable } from "mobx"; import { LensExtension } from "./lens-extension" -import type { GlobalPageRegistry, ClusterPageRegistry, AppPreferenceRegistry, StatusBarRegistry, KubeObjectMenuRegistry, ClusterFeatureRegistry } from "./registries" export class LensRendererExtension extends LensExtension { - registerGlobalPage(registry: GlobalPageRegistry) { - return - } - - registerClusterPage(registry: ClusterPageRegistry) { - return - } - - registerAppPreferences(registry: AppPreferenceRegistry) { - return - } - - registerClusterFeatures(registry: ClusterFeatureRegistry) { - return - } - - registerStatusBarItem(registry: StatusBarRegistry) { - return - } - - registerKubeObjectMenus(registry: KubeObjectMenuRegistry) { - return - } + @observable.shallow globalPages: PageRegistration[] = [] + @observable.shallow clusterPages: PageRegistration[] = [] + @observable.shallow appPreferences: AppPreferenceRegistration[] = [] + @observable.shallow clusterFeatures: ClusterFeatureRegistration[] = [] + @observable.shallow statusBarItems: StatusBarRegistration[] = [] + @observable.shallow kubeObjectMenuItems: KubeObjectMenuRegistration[] = [] } diff --git a/src/extensions/npm/extensions/package.json b/src/extensions/npm/extensions/package.json index 373e43007a..8c56c1f6eb 100644 --- a/src/extensions/npm/extensions/package.json +++ b/src/extensions/npm/extensions/package.json @@ -6,6 +6,9 @@ "copyright": "© 2020, Mirantis, Inc.", "license": "MIT", "types": "api.d.ts", + "files": [ + "api.d.ts" + ], "author": { "name": "Mirantis, Inc.", "email": "info@k8slens.dev" diff --git a/src/extensions/registries/menu-registry.ts b/src/extensions/registries/menu-registry.ts index d31d124f30..4b98201073 100644 --- a/src/extensions/registries/menu-registry.ts +++ b/src/extensions/registries/menu-registry.ts @@ -1,11 +1,10 @@ // Extensions API -> Global menu customizations -import type { MenuTopId } from "../../main/menu"; import type { MenuItemConstructorOptions } from "electron"; import { BaseRegistry } from "./base-registry"; export interface MenuRegistration extends MenuItemConstructorOptions { - parentId?: MenuTopId; + parentId: string; } export class MenuRegistry extends BaseRegistry { 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/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 7bb340b5ce..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' }, @@ -209,7 +214,7 @@ export function buildMenu(windowManager: WindowManager) { // Modify menu from extensions-api menuRegistry.getItems().forEach(({ parentId, ...menuItem }) => { try { - const topMenu = appMenu[parentId].submenu as MenuItemConstructorOptions[]; + const topMenu = appMenu[parentId as MenuTopId].submenu as MenuItemConstructorOptions[]; topMenu.push(menuItem); } catch (err) { logger.error(`[MENU]: can't register menu item, parentId=${parentId}`, { menuItem }) 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/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-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/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/icon/logo-full.svg b/src/renderer/components/icon/logo-lens.svg similarity index 100% rename from src/renderer/components/icon/logo-full.svg rename to src/renderer/components/icon/logo-lens.svg diff --git a/src/renderer/components/icon/logo.svg b/src/renderer/components/icon/logo.svg deleted file mode 100644 index 6b05f1ba17..0000000000 --- a/src/renderer/components/icon/logo.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - \ No newline at end of file diff --git a/src/renderer/components/layout/sidebar.tsx b/src/renderer/components/layout/sidebar.tsx index 9eb8268785..4619e48de7 100644 --- a/src/renderer/components/layout/sidebar.tsx +++ b/src/renderer/components/layout/sidebar.tsx @@ -80,7 +80,7 @@ export class Sidebar extends React.Component {
- +
Lens
cluster view interactions) -if (ipcRenderer) { - ipcRenderer.on("menu:navigate", (event, location: LocationDescriptor) => { - logger.info(`[IPC]: ${event.type} ${JSON.stringify(location)}`, event); - navigate(location); - }) -} - export function navigate(location: LocationDescriptor) { const currentLocation = navigation.getPath(); navigation.push(location); @@ -25,20 +19,6 @@ export function navigate(location: LocationDescriptor) { } } -export interface IURLParams

{ - params?: P; - query?: IQueryParams & Q; -} - -// todo: extract building urls to commons (also used in menu.ts) -// fixme: missing types validation for params & query -export function buildURL

(path: string | string[]) { - const pathBuilder = compile(path.toString()); - return function ({ params, query }: IURLParams = {}) { - return pathBuilder(params) + (query ? getQueryString(query, false) : "") - } -} - // common params for all pages export interface IQueryParams { namespaces?: string[]; // selected context namespaces @@ -100,3 +80,33 @@ export function setSearch(text: string) { export function getSearch() { return navigation.searchParams.get("search") || ""; } + +export function getMatchedClusterId(): string { + const matched = matchPath(navigation.location.pathname, { + exact: true, + path: clusterViewRoute.path + }); + return matched?.params.clusterId; +} + +//-- EVENTS + +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 + }) +} + +// Handle navigation via IPC (e.g. from top menu) +ipcRenderer.on("menu:navigate", (event, location: LocationDescriptor) => { + logger.info(`[IPC]: ${event.type} ${JSON.stringify(location)}`, event); + navigate(location); +}); + +// Reload dashboard window +ipcRenderer.on("menu:reload", () => { + location.reload(); +}); diff --git a/static/RELEASE_NOTES.md b/static/RELEASE_NOTES.md index 39d8b45aff..dd168092a4 100644 --- a/static/RELEASE_NOTES.md +++ b/static/RELEASE_NOTES.md @@ -2,7 +2,17 @@ Here you can find description of changes we've built into each release. While we try our best to make each upgrade automatic and as smooth as possible, there may be some cases where you might need to do something to ensure the application works smoothly. So please read through the release highlights! -## 3.6.6 (current version) +## 4.0.0-alpha.2 (current version) + +- Extension API +- Improved pod logs +- Tray icon +- Add last-status information for container +- Add LoadBalancer information to Ingress view +- Move tracker to an extension +- Add support page (as an extension) + +## 3.6.6 - Fix labels' word boundary to cover only drawer badges - Fix cluster dashboard opening not to start authentication proxy twice - Fix: Refresh cluster connection status also when connection is disconnected diff --git a/yarn.lock b/yarn.lock index fae2507115..23721721b1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1941,6 +1941,15 @@ resolved "https://registry.yarnpkg.com/@types/js-yaml/-/js-yaml-3.12.4.tgz#7d3b534ec35a0585128e2d332db1403ebe057e25" integrity sha512-fYMgzN+9e28R81weVN49inn/u798ruU91En1ZnGvSZzCRc5jXx9B2EDhlRaWmcO1RIxFHL8AajRXzxDuJu93+A== +"@types/jsdom@^16.2.4": + version "16.2.4" + resolved "https://registry.yarnpkg.com/@types/jsdom/-/jsdom-16.2.4.tgz#527ca99943e00561ca4056b1904fd5f4facebc3b" + integrity sha512-RssgLa5ptjVKRkHho/Ex0+DJWkVsYuV8oh2PSG3gKxFp8n/VNyB7kOrZGQkk2zgPlcBkIKOItUc/T5BXit9uhg== + dependencies: + "@types/node" "*" + "@types/parse5" "*" + "@types/tough-cookie" "*" + "@types/json-schema@^7.0.3": version "7.0.5" resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.5.tgz#dcce4430e64b443ba8945f0290fb564ad5bac6dd" @@ -2059,6 +2068,11 @@ resolved "https://registry.yarnpkg.com/@types/parse-json/-/parse-json-4.0.0.tgz#2f8bb441434d163b35fb8ffdccd7138927ffb8c0" integrity sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA== +"@types/parse5@*": + version "5.0.3" + resolved "https://registry.yarnpkg.com/@types/parse5/-/parse5-5.0.3.tgz#e7b5aebbac150f8b5fdd4a46e7f0bd8e65e19109" + integrity sha512-kUNnecmtkunAoQ3CnjmMkzNU/gtxG8guhi+Fk2U/kOpIKjIMKnXGp4IJCgQJrXSgMsWYimYG4TGjz/UzbGEBTw== + "@types/podium@*": version "1.0.0" resolved "https://registry.yarnpkg.com/@types/podium/-/podium-1.0.0.tgz#bfaa2151be2b1d6109cc69f7faa9dac2cba3bb20" @@ -2199,6 +2213,13 @@ dependencies: "@types/node" "*" +"@types/sharp@^0.26.0": + version "0.26.0" + resolved "https://registry.yarnpkg.com/@types/sharp/-/sharp-0.26.0.tgz#2fa8419dbdaca8dd38f73888b27b207f188a8669" + integrity sha512-oJrR8eiwpL7qykn2IeFRduXM4za7z+7yOUEbKVtuDQ/F6htDLHYO6IbzhaJQHV5n6O3adIh4tJvtgPyLyyydqg== + dependencies: + "@types/node" "*" + "@types/shelljs@^0.8.8": version "0.8.8" resolved "https://registry.yarnpkg.com/@types/shelljs/-/shelljs-0.8.8.tgz#e439c69929b88a2c8123c1a55e09eb708315addf" @@ -3247,6 +3268,15 @@ bl@^1.0.0: readable-stream "^2.3.5" safe-buffer "^5.1.1" +bl@^4.0.3: + version "4.0.3" + resolved "https://registry.yarnpkg.com/bl/-/bl-4.0.3.tgz#12d6287adc29080e22a705e5764b2a9522cdc489" + integrity sha512-fs4G6/Hu4/EE+F75J8DuN/0IpQqNjAdC7aEQv7Qt8MHGUH7Ckv2MwTEEeN9QehD0pfIDkMI1bkHYkKy7xHyKIg== + dependencies: + buffer "^5.5.0" + inherits "^2.0.4" + readable-stream "^3.4.0" + block-stream@*: version "0.0.9" resolved "https://registry.yarnpkg.com/block-stream/-/block-stream-0.0.9.tgz#13ebfe778a03205cfe03751481ebb4b3300c126a" @@ -4647,6 +4677,20 @@ decompress-response@^3.3.0: dependencies: mimic-response "^1.0.0" +decompress-response@^4.2.0: + version "4.2.1" + resolved "https://registry.yarnpkg.com/decompress-response/-/decompress-response-4.2.1.tgz#414023cc7a302da25ce2ec82d0d5238ccafd8986" + integrity sha512-jOSne2qbyE+/r8G1VU+G/82LBs2Fs4LAsTiLSHOCOMZQl2OKZ6i8i4IyHemTe+/yIXOtTcRQMzPcgyhoFlqPkw== + dependencies: + mimic-response "^2.0.0" + +decompress-response@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/decompress-response/-/decompress-response-6.0.0.tgz#ca387612ddb7e104bd16d85aab00d5ecf09c66fc" + integrity sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ== + dependencies: + mimic-response "^3.1.0" + deep-extend@^0.6.0: version "0.6.0" resolved "https://registry.yarnpkg.com/deep-extend/-/deep-extend-0.6.0.tgz#c4fa7c95404a17a9c3e8ca7e1537312b736330ac" @@ -4741,6 +4785,11 @@ detect-indent@~5.0.0: resolved "https://registry.yarnpkg.com/detect-indent/-/detect-indent-5.0.0.tgz#3871cc0a6a002e8c3e5b3cf7f336264675f06b9d" integrity sha1-OHHMCmoALow+Wzz38zYmRnXwa50= +detect-libc@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-1.0.3.tgz#fa137c4bd698edf55cd5cd02ac559f91a4c4ba9b" + integrity sha1-+hN8S9aY7fVc1c0CrFWfkaTEups= + detect-newline@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/detect-newline/-/detect-newline-2.1.0.tgz#f41f1c10be4b00e87b5f13da680759f2c5bfd3e2" @@ -5124,7 +5173,7 @@ encoding@^0.1.11: dependencies: iconv-lite "^0.6.2" -end-of-stream@^1.0.0, end-of-stream@^1.1.0: +end-of-stream@^1.0.0, end-of-stream@^1.1.0, end-of-stream@^1.4.1: version "1.4.4" resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.4.tgz#5ae64a5f45057baf3626ec14da0ca5e4b2431eb0" integrity sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q== @@ -5485,6 +5534,11 @@ expand-brackets@^2.1.4: snapdragon "^0.8.1" to-regex "^3.0.1" +expand-template@^2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/expand-template/-/expand-template-2.0.3.tgz#6e14b3fcee0f3a6340ecb57d2e8918692052a47c" + integrity sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg== + expand-tilde@^2.0.0, expand-tilde@^2.0.2: version "2.0.2" resolved "https://registry.yarnpkg.com/expand-tilde/-/expand-tilde-2.0.2.tgz#97e801aa052df02454de46b02bf621642cdc8502" @@ -6131,6 +6185,11 @@ getpass@^0.1.1: dependencies: assert-plus "^1.0.0" +github-from-package@0.0.0: + version "0.0.0" + resolved "https://registry.yarnpkg.com/github-from-package/-/github-from-package-0.0.0.tgz#97fb5d96bfde8973313f20e8288ef9a167fa64ce" + integrity sha1-l/tdlr/eiXMxPyDoKI75oWf6ZM4= + glob-parent@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-3.1.0.tgz#9e6af6299d8d3bd2bd40430832bd113df906c5ae" @@ -6739,11 +6798,6 @@ ignore@^5.1.4: resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.1.8.tgz#f150a8b50a34289b33e22f5889abd4d8016f0e57" integrity sha512-BMpfD7PpiETpBl/A6S498BaIJ6Y/ABT93ETbby2fP00v4EbvPBXWEoaR1UBPKs3iR53pJY7EtZk5KACI57i1Uw== -immer@^7.0.5: - version "7.0.5" - resolved "https://registry.yarnpkg.com/immer/-/immer-7.0.5.tgz#8af347db5b60b40af8ae7baf1784ea4d35b5208e" - integrity sha512-TtRAKZyuqld2eYjvWgXISLJ0ZlOl1OOTzRmrmiY8SlB0dnAhZ1OiykIDL5KDFNaPHDXiLfGQFNJGtet8z8AEmg== - import-fresh@^3.0.0, import-fresh@^3.1.0: version "3.2.1" resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.2.1.tgz#633ff618506e793af5ac91bf48b72677e15cbe66" @@ -7867,6 +7921,38 @@ jsdom@^16.2.2: ws "^7.2.3" xml-name-validator "^3.0.0" +jsdom@^16.4.0: + version "16.4.0" + resolved "https://registry.yarnpkg.com/jsdom/-/jsdom-16.4.0.tgz#36005bde2d136f73eee1a830c6d45e55408edddb" + integrity sha512-lYMm3wYdgPhrl7pDcRmvzPhhrGVBeVhPIqeHjzeiHN3DFmD1RBpbExbi8vU7BJdH8VAZYovR8DMt0PNNDM7k8w== + dependencies: + abab "^2.0.3" + acorn "^7.1.1" + acorn-globals "^6.0.0" + cssom "^0.4.4" + cssstyle "^2.2.0" + data-urls "^2.0.0" + decimal.js "^10.2.0" + domexception "^2.0.1" + escodegen "^1.14.1" + html-encoding-sniffer "^2.0.1" + is-potential-custom-element-name "^1.0.0" + nwsapi "^2.2.0" + parse5 "5.1.1" + request "^2.88.2" + request-promise-native "^1.0.8" + saxes "^5.0.0" + symbol-tree "^3.2.4" + tough-cookie "^3.0.1" + w3c-hr-time "^1.0.2" + w3c-xmlserializer "^2.0.0" + webidl-conversions "^6.1.0" + whatwg-encoding "^1.0.5" + whatwg-mimetype "^2.3.0" + whatwg-url "^8.0.0" + ws "^7.2.3" + xml-name-validator "^3.0.0" + jsesc@^2.5.1: version "2.5.2" resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-2.5.2.tgz#80564d2e483dacf6e8ef209650a67df3f0c283a4" @@ -8810,6 +8896,16 @@ mimic-response@^1.0.0, mimic-response@^1.0.1: resolved "https://registry.yarnpkg.com/mimic-response/-/mimic-response-1.0.1.tgz#4923538878eef42063cb8a3e3b0798781487ab1b" integrity sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ== +mimic-response@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/mimic-response/-/mimic-response-2.1.0.tgz#d13763d35f613d09ec37ebb30bac0469c0ee8f43" + integrity sha512-wXqjST+SLt7R009ySCglWBCFpjUygmCIfD790/kVbiGmUgfYGuB14PiTd5DwVxSV4NcYHjzMkoj5LjQZwTQLEA== + +mimic-response@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/mimic-response/-/mimic-response-3.1.0.tgz#2d1d59af9c1b129815accc2c46a022a5ce1fa3c9" + integrity sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ== + mini-create-react-context@^0.4.0: version "0.4.0" resolved "https://registry.yarnpkg.com/mini-create-react-context/-/mini-create-react-context-0.4.0.tgz#df60501c83151db69e28eac0ef08b4002efab040" @@ -8845,7 +8941,7 @@ minimatch@^3.0.4, minimatch@~3.0.2: dependencies: brace-expansion "^1.1.7" -minimist@^1.1.1, minimist@^1.1.3, minimist@^1.2.0, minimist@^1.2.5: +minimist@^1.1.1, minimist@^1.1.3, minimist@^1.2.0, minimist@^1.2.3, minimist@^1.2.5: version "1.2.5" resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602" integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw== @@ -8930,6 +9026,11 @@ mixin-deep@^1.2.0: for-in "^1.0.2" is-extendable "^1.0.1" +mkdirp-classic@^0.5.2: + version "0.5.3" + resolved "https://registry.yarnpkg.com/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz#fa10c9115cc6d8865be221ba47ee9bed78601113" + integrity sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A== + mkdirp@1.x, mkdirp@^1.0.3, mkdirp@^1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e" @@ -9037,6 +9138,11 @@ nanomatch@^1.2.9: snapdragon "^0.8.1" to-regex "^3.0.1" +napi-build-utils@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/napi-build-utils/-/napi-build-utils-1.0.2.tgz#b1fddc0b2c46e380a0b7a76f984dd47c41a13806" + integrity sha512-ONmRUqK7zj7DWX0D9ADe03wbwOBZxNAfF20PlGfCWQcD3+/MakShIHrMqx9YwPTfxDdF1zLeL+RGZiR9kGMLdg== + natural-compare@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7" @@ -9060,6 +9166,18 @@ no-case@^3.0.3: lower-case "^2.0.1" tslib "^1.10.0" +node-abi@^2.7.0: + version "2.19.1" + resolved "https://registry.yarnpkg.com/node-abi/-/node-abi-2.19.1.tgz#6aa32561d0a5e2fdb6810d8c25641b657a8cea85" + integrity sha512-HbtmIuByq44yhAzK7b9j/FelKlHYISKQn0mtvcBrU5QBkhoCMp5bu8Hv5AI34DcKfOAcJBcOEMwLlwO62FFu9A== + dependencies: + semver "^5.4.1" + +node-addon-api@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-3.0.2.tgz#04bc7b83fd845ba785bb6eae25bc857e1ef75681" + integrity sha512-+D4s2HCnxPd5PjjI0STKwncjXTUKKqm74MDMz9OPXavjsGmjkvwgLtA5yoxJUdmpj52+2u+RrXgPipahKczMKg== + node-fetch-npm@^2.0.2: version "2.0.4" resolved "https://registry.yarnpkg.com/node-fetch-npm/-/node-fetch-npm-2.0.4.tgz#6507d0e17a9ec0be3bec516958a497cec54bf5a4" @@ -9237,6 +9355,11 @@ nodemon@^2.0.4: undefsafe "^2.0.2" update-notifier "^4.0.0" +noop-logger@^0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/noop-logger/-/noop-logger-0.1.1.tgz#94a2b1633c4f1317553007d8966fd0e841b6a4c2" + integrity sha1-lKKxYzxPExdVMAfYlm/Q6EG2pMI= + "nopt@2 || 3": version "3.0.6" resolved "https://registry.yarnpkg.com/nopt/-/nopt-3.0.6.tgz#c6465dbf08abcd4db359317f79ac68a646b28ff9" @@ -9559,7 +9682,7 @@ npm@^6.14.8: worker-farm "^1.7.0" write-file-atomic "^2.4.3" -"npmlog@0 || 1 || 2 || 3 || 4", npmlog@^4.0.0, npmlog@^4.1.2, npmlog@~4.1.2: +"npmlog@0 || 1 || 2 || 3 || 4", npmlog@^4.0.0, npmlog@^4.0.1, npmlog@^4.1.2, npmlog@~4.1.2: version "4.1.2" resolved "https://registry.yarnpkg.com/npmlog/-/npmlog-4.1.2.tgz#08a7f2a8bf734604779a9efa4ad5cc717abb954b" integrity sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg== @@ -10371,6 +10494,27 @@ postinstall-postinstall@^2.1.0: resolved "https://registry.yarnpkg.com/postinstall-postinstall/-/postinstall-postinstall-2.1.0.tgz#4f7f77441ef539d1512c40bd04c71b06a4704ca3" integrity sha512-7hQX6ZlZXIoRiWNrbMQaLzUUfH+sSx39u8EJ9HYuDc1kLo9IXKWjM5RSquZN1ad5GnH8CGFM78fsAAQi3OKEEQ== +prebuild-install@^5.3.5: + version "5.3.5" + resolved "https://registry.yarnpkg.com/prebuild-install/-/prebuild-install-5.3.5.tgz#e7e71e425298785ea9d22d4f958dbaccf8bb0e1b" + integrity sha512-YmMO7dph9CYKi5IR/BzjOJlRzpxGGVo1EsLSUZ0mt/Mq0HWZIHOKHHcHdT69yG54C9m6i45GpItwRHpk0Py7Uw== + dependencies: + detect-libc "^1.0.3" + expand-template "^2.0.3" + github-from-package "0.0.0" + minimist "^1.2.3" + mkdirp "^0.5.1" + napi-build-utils "^1.0.1" + node-abi "^2.7.0" + noop-logger "^0.1.1" + npmlog "^4.0.1" + pump "^3.0.0" + rc "^1.2.7" + simple-get "^3.0.3" + tar-fs "^2.0.0" + tunnel-agent "^0.6.0" + which-pm-runs "^1.0.0" + prelude-ls@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396" @@ -10680,7 +10824,7 @@ raw-loader@^4.0.1: loader-utils "^2.0.0" schema-utils "^2.6.5" -rc@^1.0.1, rc@^1.1.6, rc@^1.2.8: +rc@^1.0.1, rc@^1.1.6, rc@^1.2.7, rc@^1.2.8: version "1.2.8" resolved "https://registry.yarnpkg.com/rc/-/rc-1.2.8.tgz#cd924bf5200a075b83c188cd6b9e211b7fc0d3ed" integrity sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw== @@ -10802,7 +10946,16 @@ react-zlib-js@^1.0.4: resolved "https://registry.yarnpkg.com/react-zlib-js/-/react-zlib-js-1.0.4.tgz#dd2b9fbf56d5ab224fa7a99affbbedeba9aa3dc7" integrity sha512-ynXD9DFxpE7vtGoa3ZwBtPmZrkZYw2plzHGbanUjBOSN4RtuXdektSfABykHtTiWEHMh7WdYj45LHtp228ZF1A== -react@^16.13.1, react@^16.8.0: +react@^16.14.0: + version "16.14.0" + resolved "https://registry.yarnpkg.com/react/-/react-16.14.0.tgz#94d776ddd0aaa37da3eda8fc5b6b18a4c9a3114d" + integrity sha512-0X2CImDkJGApiAlcf0ODKIneSwBPhqJawOa5wCtKbu7ZECrmS26NvtSILynQ66cgkT/RJ4LidJOc3bUESwmU8g== + dependencies: + loose-envify "^1.1.0" + object-assign "^4.1.1" + prop-types "^15.6.2" + +react@^16.8.0: version "16.13.1" resolved "https://registry.yarnpkg.com/react/-/react-16.13.1.tgz#2e818822f1a9743122c063d6410d85c1e3afe48e" integrity sha512-YMZQQq32xHLX0bz5Mnibv1/LHb3Sqzngu7xstSM+vrkE5Kzr9xE0yMByK5kMoTK30YVJE61WfbxIFFvfeDKT1w== @@ -10944,7 +11097,7 @@ read@1, read@~1.0.1, read@~1.0.7: string_decoder "~1.1.1" util-deprecate "~1.0.1" -readable-stream@^3.0.6, readable-stream@^3.1.1, readable-stream@^3.6.0: +readable-stream@^3.0.6, readable-stream@^3.1.1, readable-stream@^3.4.0, readable-stream@^3.6.0: version "3.6.0" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.0.tgz#337bbda3adc0706bd3e024426a286d4b4b2c9198" integrity sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA== @@ -11649,6 +11802,21 @@ shallow-clone@^3.0.0: dependencies: kind-of "^6.0.2" +sharp@^0.26.1: + version "0.26.1" + resolved "https://registry.yarnpkg.com/sharp/-/sharp-0.26.1.tgz#084e3447ba17f1baf3e3f2e08305ed7aec236ce9" + integrity sha512-9MhwS4ys8pnwBH7MtnBdLzUv+cb24QC4xbzzQL6A+1MQ4Se2V6oPHEX8TIGIZUPRKi6S1kJPVNzt/Xqqp6/H3Q== + dependencies: + color "^3.1.2" + detect-libc "^1.0.3" + node-addon-api "^3.0.2" + npmlog "^4.1.2" + prebuild-install "^5.3.5" + semver "^7.3.2" + simple-get "^4.0.0" + tar-fs "^2.1.0" + tunnel-agent "^0.6.0" + shebang-command@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-1.2.0.tgz#44aac65b695b03398968c39f363fee5deafdf1ea" @@ -11701,6 +11869,29 @@ signal-exit@^3.0.0, signal-exit@^3.0.2: resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.3.tgz#a1410c2edd8f077b08b4e253c8eacfcaf057461c" integrity sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA== +simple-concat@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/simple-concat/-/simple-concat-1.0.1.tgz#f46976082ba35c2263f1c8ab5edfe26c41c9552f" + integrity sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q== + +simple-get@^3.0.3: + version "3.1.0" + resolved "https://registry.yarnpkg.com/simple-get/-/simple-get-3.1.0.tgz#b45be062435e50d159540b576202ceec40b9c6b3" + integrity sha512-bCR6cP+aTdScaQCnQKbPKtJOKDp/hj9EDLJo3Nw4y1QksqaovlW/bnptB6/c1e+qmNIDHRK+oXFDdEqBT8WzUA== + dependencies: + decompress-response "^4.2.0" + once "^1.3.1" + simple-concat "^1.0.0" + +simple-get@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/simple-get/-/simple-get-4.0.0.tgz#73fa628278d21de83dadd5512d2cc1f4872bd675" + integrity sha512-ZalZGexYr3TA0SwySsr5HlgOOinS4Jsa8YB2GJ6lUNAazyAu4KG/VmzMTwAt2YVXzzVj8QmefmAonZIK2BSGcQ== + dependencies: + decompress-response "^6.0.0" + once "^1.3.1" + simple-concat "^1.0.0" + simple-swizzle@^0.2.2: version "0.2.2" resolved "https://registry.yarnpkg.com/simple-swizzle/-/simple-swizzle-0.2.2.tgz#a4da6b635ffcccca33f70d17cb92592de95e557a" @@ -12354,6 +12545,16 @@ tapable@^1.0.0, tapable@^1.1.3: resolved "https://registry.yarnpkg.com/tapable/-/tapable-1.1.3.tgz#a1fccc06b58db61fd7a45da2da44f5f3a3e67ba2" integrity sha512-4WK/bYZmj8xLr+HUCODHGF1ZFzsYffasLUgEiMBY4fgtltdO6B4WJtlSbPaDTLpYTcGVwM2qLnFTICEcNxs3kA== +tar-fs@^2.0.0, tar-fs@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/tar-fs/-/tar-fs-2.1.0.tgz#d1cdd121ab465ee0eb9ccde2d35049d3f3daf0d5" + integrity sha512-9uW5iDvrIMCVpvasdFHW0wJPez0K4JnMZtsuIeDI7HyMGJNxmDZDOCQROr7lXyS+iL/QMpj07qcjGYTSdRFXUg== + dependencies: + chownr "^1.1.1" + mkdirp-classic "^0.5.2" + pump "^3.0.0" + tar-stream "^2.0.0" + tar-stream@^1.5.0: version "1.6.2" resolved "https://registry.yarnpkg.com/tar-stream/-/tar-stream-1.6.2.tgz#8ea55dab37972253d9a9af90fdcd559ae435c555" @@ -12367,6 +12568,17 @@ tar-stream@^1.5.0: to-buffer "^1.1.1" xtend "^4.0.0" +tar-stream@^2.0.0: + version "2.1.4" + resolved "https://registry.yarnpkg.com/tar-stream/-/tar-stream-2.1.4.tgz#c4fb1a11eb0da29b893a5b25476397ba2d053bfa" + integrity sha512-o3pS2zlG4gxr67GmFYBLlq+dM8gyRGUOvsrHclSkvtVtQbjV0s/+ZE8OpICbaj8clrX3tjeHngYGP7rweaBnuw== + dependencies: + bl "^4.0.3" + end-of-stream "^1.4.1" + fs-constants "^1.0.0" + inherits "^2.0.3" + readable-stream "^3.1.1" + tar@^2.0.0: version "2.2.2" resolved "https://registry.yarnpkg.com/tar/-/tar-2.2.2.tgz#0ca8848562c7299b8b446ff6a4d60cdbb23edc40" @@ -13272,7 +13484,7 @@ webidl-conversions@^5.0.0: resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-5.0.0.tgz#ae59c8a00b121543a2acc65c0434f57b0fc11aff" integrity sha512-VlZwKPCkYKxQgeSbH5EyngOmRp7Ww7I9rQLERETtf5ofd9pGeswWiOtogpEO850jziPRarreGxn5QIiTqpb2wA== -webidl-conversions@^6.0.0: +webidl-conversions@^6.0.0, webidl-conversions@^6.1.0: version "6.1.0" resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-6.1.0.tgz#9111b4d7ea80acd40f5270d666621afa78b69514" integrity sha512-qBIvFLGiBpLjfwmYAaHPXsn+ho5xZnGvyGvsarywGNc8VyQJUMHJ8OBKGGrPER0okBeMDaan4mNBlgBROxuI8w== @@ -13367,6 +13579,11 @@ which-module@^2.0.0: resolved "https://registry.yarnpkg.com/which-module/-/which-module-2.0.0.tgz#d9ef07dce77b9902b8a3a8fa4b31c3e3f7e6e87a" integrity sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho= +which-pm-runs@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/which-pm-runs/-/which-pm-runs-1.0.0.tgz#670b3afbc552e0b55df6b7780ca74615f23ad1cb" + integrity sha1-Zws6+8VS4LVd9rd4DKdGFfI60cs= + which@1, which@^1.2.14, which@^1.2.9, which@^1.3.0, which@^1.3.1: version "1.3.1" resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a"