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/support-page/main.ts b/extensions/support-page/main.ts index 133df0b890..20f17df4cf 100644 --- a/extensions/support-page/main.ts +++ b/extensions/support-page/main.ts @@ -12,10 +12,7 @@ export default class SupportPageMainExtension extends LensMainExtension { parentId: "help", label: "Support", click() { - windowManager.navigate({ - channel: "menu:navigate", // fixme: use windowManager.ensureMainWindow from Tray's PR - url: supportPageURL(), - }); + windowManager.navigate(supportPageURL()); } }) ) diff --git a/extensions/support-page/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/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..8b850b213e 100644 --- a/package.json +++ b/package.json @@ -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/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/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"