1
0
mirror of https://github.com/lensapp/lens.git synced 2025-05-20 05:10:56 +00:00

Merge branch 'master' into extensions_register_feature_clean_up

# Conflicts:
#	extensions/support-page/main.ts
This commit is contained in:
Roman 2020-10-27 16:25:44 +02:00
commit e2aaaa1e26
112 changed files with 1500 additions and 623 deletions

View File

@ -56,7 +56,13 @@ else
endif endif
build-extensions: build-extensions:
$(foreach file, $(wildcard $(EXTENSIONS_DIR)/*), $(MAKE) -C $(file) build;) $(foreach dir, $(wildcard $(EXTENSIONS_DIR)/*), $(MAKE) -C $(dir) build;)
build-npm:
yarn npm:fix-package-version
publish-npm: build-npm
cd src/extensions/npm/extensions && npm publish
clean: clean:
ifeq "$(DETECTED_OS)" "Windows" ifeq "$(DETECTED_OS)" "Windows"

View File

@ -40,9 +40,10 @@ brew cask install lens
Allows for faster separate re-runs of some of the more involved processes: 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:main` compiles electron's main process app part
1. `yarn dev:renderer` compiles electron's renderer part and start watching files 1. `yarn dev:renderer` compiles electron's renderer app part
1. `yarn dev-run` runs app in dev-mode and restarts when electron's main process file has changed 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: ## Developer's ~~RTFM~~ recommended list:

51
build/build_tray_icon.ts Normal file
View File

@ -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 += `<style>* {fill: ${trayIconColor} !important;}</style>`
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<string, number> = {
"1x": 16,
"2x": 32,
"3x": 48,
};
Object.entries(iconSizes).forEach(([dpiSuffix, pixelSize]) => {
generateTrayIcon({ dpiSuffix, pixelSize, shouldUseDarkColors: false });
generateTrayIcon({ dpiSuffix, pixelSize, shouldUseDarkColors: true });
});

9
build/set_npm_version.ts Normal file
View File

@ -0,0 +1,9 @@
import * as fs from "fs"
import * as path from "path"
import packageInfo from "../src/extensions/npm/extensions/package.json"
import appInfo from "../package.json"
const packagePath = path.join(__dirname, "../src/extensions/npm/extensions/package.json")
packageInfo.version = appInfo.version
fs.writeFileSync(packagePath, JSON.stringify(packageInfo, null, 2))

BIN
build/tray/tray_icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 448 B

BIN
build/tray/tray_icon@2x.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 973 B

BIN
build/tray/tray_icon@3x.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 478 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

@ -4,6 +4,10 @@
"lockfileVersion": 1, "lockfileVersion": 1,
"requires": true, "requires": true,
"dependencies": { "dependencies": {
"@k8slens/extensions": {
"version": "file:../../src/extensions/npm/extensions",
"dev": true
},
"@webassemblyjs/ast": { "@webassemblyjs/ast": {
"version": "1.9.0", "version": "1.9.0",
"resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.9.0.tgz", "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.9.0.tgz",

View File

@ -16,6 +16,7 @@
"react-open-doodles": "^1.0.5" "react-open-doodles": "^1.0.5"
}, },
"devDependencies": { "devDependencies": {
"@k8slens/extensions": "file:../../src/extensions/npm/extensions",
"ts-loader": "^8.0.4", "ts-loader": "^8.0.4",
"typescript": "^4.0.3", "typescript": "^4.0.3",
"webpack": "^4.44.2" "webpack": "^4.44.2"

View File

@ -16,7 +16,6 @@
"jsx": "react" "jsx": "react"
}, },
"include": [ "include": [
"../../src/extensions/npm/**/*.d.ts",
"./*.ts", "./*.ts",
"./*.tsx" "./*.tsx"
], ],

View File

@ -4,6 +4,10 @@
"lockfileVersion": 1, "lockfileVersion": 1,
"requires": true, "requires": true,
"dependencies": { "dependencies": {
"@k8slens/extensions": {
"version": "file:../../src/extensions/npm/extensions",
"dev": true
},
"@webassemblyjs/ast": { "@webassemblyjs/ast": {
"version": "1.9.0", "version": "1.9.0",
"resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.9.0.tgz", "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.9.0.tgz",

View File

@ -15,6 +15,7 @@
"semver": "^7.3.2" "semver": "^7.3.2"
}, },
"devDependencies": { "devDependencies": {
"@k8slens/extensions": "file:../../src/extensions/npm/extensions",
"ts-loader": "^8.0.4", "ts-loader": "^8.0.4",
"typescript": "^4.0.3", "typescript": "^4.0.3",
"webpack": "^4.44.2", "webpack": "^4.44.2",

View File

@ -16,7 +16,6 @@
"jsx": "react" "jsx": "react"
}, },
"include": [ "include": [
"../../src/extensions/npm/**/*.d.ts",
"./*.ts", "./*.ts",
"./*.tsx" "./*.tsx"
], ],

View File

@ -4,6 +4,10 @@
"lockfileVersion": 1, "lockfileVersion": 1,
"requires": true, "requires": true,
"dependencies": { "dependencies": {
"@k8slens/extensions": {
"version": "file:../../src/extensions/npm/extensions",
"dev": true
},
"@webassemblyjs/ast": { "@webassemblyjs/ast": {
"version": "1.9.0", "version": "1.9.0",
"resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.9.0.tgz", "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.9.0.tgz",

View File

@ -13,6 +13,7 @@
}, },
"dependencies": {}, "dependencies": {},
"devDependencies": { "devDependencies": {
"@k8slens/extensions": "file:../../src/extensions/npm/extensions",
"ts-loader": "^8.0.4", "ts-loader": "^8.0.4",
"typescript": "^4.0.3", "typescript": "^4.0.3",
"webpack": "^4.44.2", "webpack": "^4.44.2",

View File

@ -16,7 +16,6 @@
"jsx": "react" "jsx": "react"
}, },
"include": [ "include": [
"../../src/extensions/npm/**/*.d.ts",
"./*.ts", "./*.ts",
"./*.tsx" "./*.tsx"
], ],

View File

@ -4,6 +4,10 @@
"lockfileVersion": 1, "lockfileVersion": 1,
"requires": true, "requires": true,
"dependencies": { "dependencies": {
"@k8slens/extensions": {
"version": "file:../../src/extensions/npm/extensions",
"dev": true
},
"@webassemblyjs/ast": { "@webassemblyjs/ast": {
"version": "1.9.0", "version": "1.9.0",
"resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.9.0.tgz", "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.9.0.tgz",

View File

@ -17,6 +17,7 @@
"typescript": "^4.0.3", "typescript": "^4.0.3",
"webpack": "^4.44.2", "webpack": "^4.44.2",
"mobx": "^5.15.5", "mobx": "^5.15.5",
"react": "^16.13.1" "react": "^16.13.1",
"@k8slens/extensions": "file:../../src/extensions/npm/extensions"
} }
} }

View File

@ -15,7 +15,6 @@ export class PodLogsMenu extends React.Component<PodLogsMenuProps> {
selectedContainer: container, selectedContainer: container,
showTimestamps: false, showTimestamps: false,
previous: false, previous: false,
tailLines: 1000
}); });
} }

View File

@ -33,7 +33,6 @@ export class PodShellMenu extends React.Component<PodShellMenuProps> {
render() { render() {
const { object, toolbar } = this.props const { object, toolbar } = this.props
console.log(Object.keys(object))
const containers = object.getRunningContainers(); const containers = object.getRunningContainers();
if (!containers.length) return; if (!containers.length) return;
return ( return (

View File

@ -16,7 +16,6 @@
"jsx": "react" "jsx": "react"
}, },
"include": [ "include": [
"../../src/extensions/npm/**/*.d.ts",
"./*.ts", "./*.ts",
"./*.tsx" "./*.tsx"
], ],

View File

@ -7,10 +7,7 @@ export default class SupportPageMainExtension extends LensMainExtension {
parentId: "help", parentId: "help",
label: "Support", label: "Support",
click() { click() {
windowManager.navigate({ windowManager.navigate(supportPageURL());
channel: "menu:navigate", // fixme: use windowManager.ensureMainWindow from Tray's PR
url: supportPageURL(),
});
} }
} }
] ]

View File

@ -4,6 +4,10 @@
"lockfileVersion": 1, "lockfileVersion": 1,
"requires": true, "requires": true,
"dependencies": { "dependencies": {
"@k8slens/extensions": {
"version": "file:../../src/extensions/npm/extensions",
"dev": true
},
"@types/anymatch": { "@types/anymatch": {
"version": "1.3.1", "version": "1.3.1",
"resolved": "https://registry.npmjs.org/@types/anymatch/-/anymatch-1.3.1.tgz", "resolved": "https://registry.npmjs.org/@types/anymatch/-/anymatch-1.3.1.tgz",

View File

@ -14,6 +14,7 @@
"@types/react": "^16.9.53", "@types/react": "^16.9.53",
"@types/react-router": "^5.1.8", "@types/react-router": "^5.1.8",
"@types/webpack": "^4.41.17", "@types/webpack": "^4.41.17",
"@k8slens/extensions": "file:../../src/extensions/npm/extensions",
"mobx": "^5.15.5", "mobx": "^5.15.5",
"react": "^16.13.1", "react": "^16.13.1",
"ts-loader": "^8.0.4", "ts-loader": "^8.0.4",

View File

@ -3,13 +3,13 @@
import React from "react" import React from "react"
import { observer } from "mobx-react" import { observer } from "mobx-react"
import { CommonVars, Component } from "@k8slens/extensions"; import { App, Component } from "@k8slens/extensions";
@observer @observer
export class Support extends React.Component { export class Support extends React.Component {
render() { render() {
const { PageLayout } = Component; const { PageLayout } = Component;
const { slackUrl, issuesTrackerUrl } = CommonVars; const { slackUrl, issuesTrackerUrl } = App;
return ( return (
<PageLayout showOnTop className="Support" header={<h2>Support</h2>}> <PageLayout showOnTop className="Support" header={<h2>Support</h2>}>
<h2>Community Slack Channel</h2> <h2>Community Slack Channel</h2>

View File

@ -24,7 +24,6 @@
}, },
"include": [ "include": [
"renderer.tsx", "renderer.tsx",
"../../src/extensions/npm/**/*.d.ts",
"src/**/*" "src/**/*"
] ]
} }

View File

@ -4,6 +4,10 @@
"lockfileVersion": 1, "lockfileVersion": 1,
"requires": true, "requires": true,
"dependencies": { "dependencies": {
"@k8slens/extensions": {
"version": "file:../../src/extensions/npm/extensions",
"dev": true
},
"@webassemblyjs/ast": { "@webassemblyjs/ast": {
"version": "1.9.0", "version": "1.9.0",
"resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.9.0.tgz", "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.9.0.tgz",

View File

@ -14,6 +14,7 @@
}, },
"dependencies": {}, "dependencies": {},
"devDependencies": { "devDependencies": {
"@k8slens/extensions": "file:../../src/extensions/npm/extensions",
"ts-loader": "^8.0.4", "ts-loader": "^8.0.4",
"typescript": "^4.0.3", "typescript": "^4.0.3",
"webpack": "^4.44.2", "webpack": "^4.44.2",

View File

@ -1,4 +1,4 @@
import { EventBus, Util } from "@k8slens/extensions" import { EventBus, Util, Store, App } from "@k8slens/extensions"
import ua from "universal-analytics" import ua from "universal-analytics"
import { machineIdSync } from "node-machine-id" import { machineIdSync } from "node-machine-id"
import { telemetryPreferencesStore } from "./telemetry-preferences-store" import { telemetryPreferencesStore } from "./telemetry-preferences-store"
@ -15,6 +15,8 @@ export class Tracker extends Util.Singleton {
protected locale: string; protected locale: string;
protected electronUA: string; protected electronUA: string;
protected reportInterval: NodeJS.Timeout
private constructor() { private constructor() {
super(); super();
try { try {
@ -23,7 +25,7 @@ export class Tracker extends Util.Singleton {
this.visitor = ua(Tracker.GA_ID) this.visitor = ua(Tracker.GA_ID)
} }
this.visitor.set("dl", "https://telemetry.k8slens.dev") this.visitor.set("dl", "https://telemetry.k8slens.dev")
this.visitor.set("ua", `Lens ${App.version} (${this.getOS()})`)
} }
start() { start() {
@ -36,6 +38,8 @@ export class Tracker extends Util.Singleton {
} }
this.eventHandlers.push(handler) this.eventHandlers.push(handler)
EventBus.appEventBus.addListener(handler) EventBus.appEventBus.addListener(handler)
this.reportInterval = setInterval(this.reportData, 60 * 60 * 1000) // report every 1h
} }
stop() { stop() {
@ -46,12 +50,59 @@ export class Tracker extends Util.Singleton {
for (const handler of this.eventHandlers) { for (const handler of this.eventHandlers) {
EventBus.appEventBus.removeListener(handler) EventBus.appEventBus.removeListener(handler)
} }
if (this.reportInterval) {
clearInterval(this.reportInterval)
}
} }
protected async isTelemetryAllowed(): Promise<boolean> { protected async isTelemetryAllowed(): Promise<boolean> {
return telemetryPreferencesStore.enabled return telemetryPreferencesStore.enabled
} }
protected reportData() {
const clustersList = Store.clusterStore.clustersList
this.event("generic-data", "report", {
appVersion: App.version,
clustersCount: clustersList.length,
workspacesCount: Store.workspaceStore.workspacesList.length
})
clustersList.forEach((cluster) => {
if (!cluster?.metadata.lastSeen) { return }
this.reportClusterData(cluster)
})
}
protected reportClusterData(cluster: Store.ClusterModel) {
this.event("cluster-data", "report", {
id: cluster.metadata.id,
kubernetesVersion: cluster.metadata.version,
distribution: cluster.metadata.distribution,
nodesCount: cluster.metadata.nodes,
lastSeen: cluster.metadata.lastSeen
})
}
protected getOS() {
let os = ""
if (App.isMac) {
os = "MacOS"
} else if(App.isWindows) {
os = "Windows"
} else if (App.isLinux) {
os = "Linux"
if (App.isSnap) {
os += "; Snap"
} else {
os += "; AppImage"
}
} else {
os = "Unknown"
}
return os
}
protected async event(eventCategory: string, eventAction: string, otherParams = {}) { protected async event(eventCategory: string, eventAction: string, otherParams = {}) {
try { try {
const allowed = await this.isTelemetryAllowed(); const allowed = await this.isTelemetryAllowed();

View File

@ -24,7 +24,6 @@
}, },
"include": [ "include": [
"renderer.ts", "renderer.ts",
"../../src/extensions/npm/**/*.d.ts",
"src/**/*" "src/**/*"
] ]
} }

View File

@ -166,8 +166,8 @@ describe("Lens integration tests", () => {
pages: [{ pages: [{
name: "Cluster", name: "Cluster",
href: "cluster", href: "cluster",
expectedSelector: "div.ClusterNoMetrics p", expectedSelector: "div.Cluster div.label",
expectedText: "Metrics are not available due" expectedText: "Master"
}] }]
}, },
{ {
@ -389,13 +389,13 @@ describe("Lens integration tests", () => {
it(`shows ${drawer} drawer`, async () => { it(`shows ${drawer} drawer`, async () => {
expect(clusterAdded).toBe(true) expect(clusterAdded).toBe(true)
await app.client.click(`.sidebar-nav #${drawerId} span.link-text`) 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 }) => { pages.forEach(({ name, href, expectedSelector, expectedText }) => {
it(`shows ${drawer}->${name} page`, async () => { it(`shows ${drawer}->${name} page`, async () => {
expect(clusterAdded).toBe(true) expect(clusterAdded).toBe(true)
await app.client.click(`a[href="/${href}"]`) await app.client.click(`a[href^="/${href}"]`)
await app.client.waitUntilTextExists(expectedSelector, expectedText) await app.client.waitUntilTextExists(expectedSelector, expectedText)
}) })
}) })
@ -404,7 +404,7 @@ describe("Lens integration tests", () => {
it(`hides ${drawer} drawer`, async () => { it(`hides ${drawer} drawer`, async () => {
expect(clusterAdded).toBe(true) expect(clusterAdded).toBe(true)
await app.client.click(`.sidebar-nav #${drawerId} span.link-text`) 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 () => { it(`creates a pod in ${TEST_NAMESPACE} namespace`, async () => {
expect(clusterAdded).toBe(true) expect(clusterAdded).toBe(true)
await app.client.click(".sidebar-nav #workloads span.link-text") await app.client.click(".sidebar-nav #workloads span.link-text")
await app.client.waitUntilTextExists('a[href="/pods"]', "Pods") await app.client.waitUntilTextExists('a[href^="/pods"]', "Pods")
await app.client.click('a[href="/pods"]') await app.client.click('a[href^="/pods"]')
await app.client.waitUntilTextExists("div.TableCell", "kube-apiserver") await app.client.waitUntilTextExists("div.TableCell", "kube-apiserver")
await app.client.click('.Icon.new-dock-tab') await app.client.click('.Icon.new-dock-tab')
await app.client.waitUntilTextExists("li.MenuItem.create-resource-tab", "Create resource") await app.client.waitUntilTextExists("li.MenuItem.create-resource-tab", "Create resource")

View File

@ -13,7 +13,6 @@ export function setup(): Application {
path: AppPaths[process.platform], path: AppPaths[process.platform],
startTimeout: 30000, startTimeout: 30000,
waitTimeout: 60000, waitTimeout: 60000,
chromeDriverArgs: ['remote-debugging-port=9222'],
env: { env: {
CICD: "true" CICD: "true"
} }
@ -27,6 +26,6 @@ export async function tearDown(app: Application) {
try { try {
process.kill(pid, "SIGKILL"); process.kill(pid, "SIGKILL");
} catch (e) { } catch (e) {
return console.error(e)
} }
} }

View File

@ -88,7 +88,7 @@ msgid "Active"
msgstr "Active" msgstr "Active"
#: src/renderer/components/+add-cluster/add-cluster.tsx:310 #: 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" msgid "Add Cluster"
msgstr "Add Cluster" msgstr "Add Cluster"
@ -219,11 +219,11 @@ msgstr "Allocatable"
msgid "Allow Privilege Escalation" msgid "Allow Privilege Escalation"
msgstr "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" msgid "Allow telemetry & usage tracking"
msgstr "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" msgid "Allow untrusted Certificate Authorities"
msgstr "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" msgid "Auth App Role"
msgstr "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/error-boundary/error-boundary.tsx:53
#: src/renderer/components/wizard/wizard.tsx:130 #: src/renderer/components/wizard/wizard.tsx:130
msgid "Back" msgid "Back"
@ -422,7 +430,7 @@ msgstr "Cancel"
msgid "Capacity" msgid "Capacity"
msgstr "Capacity" msgstr "Capacity"
#: src/renderer/components/+preferences/preferences.tsx:160 #: src/renderer/components/+preferences/preferences.tsx:163
msgid "Certificate Trust" msgid "Certificate Trust"
msgstr "Certificate Trust" msgstr "Certificate Trust"
@ -817,7 +825,7 @@ msgstr "Desired Healthy"
msgid "Desired number of replicas" msgid "Desired number of replicas"
msgstr "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" msgid "Disconnect"
msgstr "Disconnect" msgstr "Disconnect"
@ -831,7 +839,7 @@ msgstr "Disk"
msgid "Disk:" msgid "Disk:"
msgstr "Disk:" msgstr "Disk:"
#: src/renderer/components/+preferences/preferences.tsx:165 #: src/renderer/components/+preferences/preferences.tsx:168
msgid "Does not affect cluster communications!" msgid "Does not affect cluster communications!"
msgstr "Does not affect cluster communications!" msgstr "Does not affect cluster communications!"
@ -927,8 +935,8 @@ msgstr "Environment"
msgid "Error stack" msgid "Error stack"
msgstr "Error stack" msgstr "Error stack"
#: src/renderer/components/+add-cluster/add-cluster.tsx:89 #: src/renderer/components/+add-cluster/add-cluster.tsx:88
#: src/renderer/components/+add-cluster/add-cluster.tsx:130 #: src/renderer/components/+add-cluster/add-cluster.tsx:129
msgid "Error while adding cluster(s): {0}" msgid "Error while adding cluster(s): {0}"
msgstr "Error while adding cluster(s): {0}" msgstr "Error while adding cluster(s): {0}"
@ -1581,7 +1589,7 @@ msgstr "Namespaces"
msgid "Namespaces: {0}" msgid "Namespaces: {0}"
msgstr "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." msgid "Needed with some corporate proxies that do certificate re-writing."
msgstr "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" msgid "Persistent Volumes"
msgstr "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" msgid "Please select at least one cluster context"
msgstr "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/+preferences/preferences.tsx:152
#: src/renderer/components/+user-management-roles-bindings/role-binding-details.tsx:60 #: 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:73
#: src/renderer/components/cluster-manager/clusters-menu.tsx:82 #: src/renderer/components/cluster-manager/clusters-menu.tsx:79
#: src/renderer/components/item-object-list/item-list-layout.tsx:179 #: 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:49
#: src/renderer/components/menu/menu-actions.tsx:85 #: src/renderer/components/menu/menu-actions.tsx:85
@ -2470,7 +2478,7 @@ msgstr "Set"
msgid "Set quota" msgid "Set quota"
msgstr "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" msgid "Settings"
msgstr "Settings" msgstr "Settings"
@ -2613,7 +2621,7 @@ msgstr "Submitting.."
msgid "Subsets" msgid "Subsets"
msgstr "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}</0> cluster(s)" msgid "Successfully imported <0>{0}</0> cluster(s)"
msgstr "Successfully imported <0>{0}</0> cluster(s)" msgstr "Successfully imported <0>{0}</0> cluster(s)"
@ -2635,11 +2643,11 @@ msgstr "TLS"
msgid "Taints" msgid "Taints"
msgstr "Taints" msgstr "Taints"
#: src/renderer/components/+preferences/preferences.tsx:168 #: src/renderer/components/+preferences/preferences.tsx:171
msgid "Telemetry & Usage Tracking" msgid "Telemetry & Usage Tracking"
msgstr "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." msgid "Telemetry & usage data is collected to continuously improve the Lens experience."
msgstr "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." msgid "This is the quick launch menu."
msgstr "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." 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." msgstr "This will make Lens to trust ANY certificate authority without any validations."
@ -2953,7 +2961,7 @@ msgstr "listKind"
msgid "never" msgid "never"
msgstr "never" msgstr "never"
#: src/renderer/components/cluster-manager/clusters-menu.tsx:133 #: src/renderer/components/cluster-manager/clusters-menu.tsx:130
msgid "new" msgid "new"
msgstr "new" msgstr "new"

View File

@ -88,7 +88,7 @@ msgid "Active"
msgstr "" msgstr ""
#: src/renderer/components/+add-cluster/add-cluster.tsx:310 #: 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" msgid "Add Cluster"
msgstr "" msgstr ""
@ -219,11 +219,11 @@ msgstr ""
msgid "Allow Privilege Escalation" msgid "Allow Privilege Escalation"
msgstr "" msgstr ""
#: src/renderer/components/+preferences/preferences.tsx:169 #: src/renderer/components/+preferences/preferences.tsx:172
msgid "Allow telemetry & usage tracking" msgid "Allow telemetry & usage tracking"
msgstr "" msgstr ""
#: src/renderer/components/+preferences/preferences.tsx:161 #: src/renderer/components/+preferences/preferences.tsx:164
msgid "Allow untrusted Certificate Authorities" msgid "Allow untrusted Certificate Authorities"
msgstr "" msgstr ""
@ -301,6 +301,14 @@ msgstr ""
msgid "Auth App Role" msgid "Auth App Role"
msgstr "" 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/error-boundary/error-boundary.tsx:53
#: src/renderer/components/wizard/wizard.tsx:130 #: src/renderer/components/wizard/wizard.tsx:130
msgid "Back" msgid "Back"
@ -422,7 +430,7 @@ msgstr ""
msgid "Capacity" msgid "Capacity"
msgstr "" msgstr ""
#: src/renderer/components/+preferences/preferences.tsx:160 #: src/renderer/components/+preferences/preferences.tsx:163
msgid "Certificate Trust" msgid "Certificate Trust"
msgstr "" msgstr ""
@ -813,7 +821,7 @@ msgstr ""
msgid "Desired number of replicas" msgid "Desired number of replicas"
msgstr "" msgstr ""
#: src/renderer/components/cluster-manager/clusters-menu.tsx:65 #: src/renderer/components/cluster-manager/clusters-menu.tsx:62
msgid "Disconnect" msgid "Disconnect"
msgstr "" msgstr ""
@ -827,7 +835,7 @@ msgstr ""
msgid "Disk:" msgid "Disk:"
msgstr "" msgstr ""
#: src/renderer/components/+preferences/preferences.tsx:165 #: src/renderer/components/+preferences/preferences.tsx:168
msgid "Does not affect cluster communications!" msgid "Does not affect cluster communications!"
msgstr "" msgstr ""
@ -923,8 +931,8 @@ msgstr ""
msgid "Error stack" msgid "Error stack"
msgstr "" msgstr ""
#: src/renderer/components/+add-cluster/add-cluster.tsx:89 #: src/renderer/components/+add-cluster/add-cluster.tsx:88
#: src/renderer/components/+add-cluster/add-cluster.tsx:130 #: src/renderer/components/+add-cluster/add-cluster.tsx:129
msgid "Error while adding cluster(s): {0}" msgid "Error while adding cluster(s): {0}"
msgstr "" msgstr ""
@ -1572,7 +1580,7 @@ msgstr ""
msgid "Namespaces: {0}" msgid "Namespaces: {0}"
msgstr "" 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." msgid "Needed with some corporate proxies that do certificate re-writing."
msgstr "" msgstr ""
@ -1781,7 +1789,7 @@ msgstr ""
msgid "Persistent Volumes" msgid "Persistent Volumes"
msgstr "" 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" msgid "Please select at least one cluster context"
msgstr "" msgstr ""
@ -2008,8 +2016,8 @@ msgstr ""
#: src/renderer/components/+preferences/preferences.tsx:152 #: src/renderer/components/+preferences/preferences.tsx:152
#: src/renderer/components/+user-management-roles-bindings/role-binding-details.tsx:60 #: 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:73
#: src/renderer/components/cluster-manager/clusters-menu.tsx:82 #: src/renderer/components/cluster-manager/clusters-menu.tsx:79
#: src/renderer/components/item-object-list/item-list-layout.tsx:179 #: 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:49
#: src/renderer/components/menu/menu-actions.tsx:85 #: src/renderer/components/menu/menu-actions.tsx:85
@ -2453,7 +2461,7 @@ msgstr ""
msgid "Set quota" msgid "Set quota"
msgstr "" msgstr ""
#: src/renderer/components/cluster-manager/clusters-menu.tsx:53 #: src/renderer/components/cluster-manager/clusters-menu.tsx:51
msgid "Settings" msgid "Settings"
msgstr "" msgstr ""
@ -2596,7 +2604,7 @@ msgstr ""
msgid "Subsets" msgid "Subsets"
msgstr "" msgstr ""
#: src/renderer/components/+add-cluster/add-cluster.tsx:122 #: src/renderer/components/+add-cluster/add-cluster.tsx:121
msgid "Successfully imported <0>{0}</0> cluster(s)" msgid "Successfully imported <0>{0}</0> cluster(s)"
msgstr "" msgstr ""
@ -2618,11 +2626,11 @@ msgstr ""
msgid "Taints" msgid "Taints"
msgstr "" msgstr ""
#: src/renderer/components/+preferences/preferences.tsx:168 #: src/renderer/components/+preferences/preferences.tsx:171
msgid "Telemetry & Usage Tracking" msgid "Telemetry & Usage Tracking"
msgstr "" 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." msgid "Telemetry & usage data is collected to continuously improve the Lens experience."
msgstr "" msgstr ""
@ -2658,7 +2666,7 @@ msgstr ""
msgid "This is the quick launch menu." msgid "This is the quick launch menu."
msgstr "" 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." msgid "This will make Lens to trust ANY certificate authority without any validations."
msgstr "" msgstr ""
@ -2936,7 +2944,7 @@ msgstr ""
msgid "never" msgid "never"
msgstr "" msgstr ""
#: src/renderer/components/cluster-manager/clusters-menu.tsx:133 #: src/renderer/components/cluster-manager/clusters-menu.tsx:130
msgid "new" msgid "new"
msgstr "" msgstr ""

View File

@ -89,7 +89,7 @@ msgid "Active"
msgstr "Активный" msgstr "Активный"
#: src/renderer/components/+add-cluster/add-cluster.tsx:310 #: 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" msgid "Add Cluster"
msgstr "" msgstr ""
@ -220,11 +220,11 @@ msgstr ""
msgid "Allow Privilege Escalation" msgid "Allow Privilege Escalation"
msgstr "" msgstr ""
#: src/renderer/components/+preferences/preferences.tsx:169 #: src/renderer/components/+preferences/preferences.tsx:172
msgid "Allow telemetry & usage tracking" msgid "Allow telemetry & usage tracking"
msgstr "" msgstr ""
#: src/renderer/components/+preferences/preferences.tsx:161 #: src/renderer/components/+preferences/preferences.tsx:164
msgid "Allow untrusted Certificate Authorities" msgid "Allow untrusted Certificate Authorities"
msgstr "" msgstr ""
@ -302,6 +302,14 @@ msgstr ""
msgid "Auth App Role" msgid "Auth App Role"
msgstr "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/error-boundary/error-boundary.tsx:53
#: src/renderer/components/wizard/wizard.tsx:130 #: src/renderer/components/wizard/wizard.tsx:130
msgid "Back" msgid "Back"
@ -423,7 +431,7 @@ msgstr "Отмена"
msgid "Capacity" msgid "Capacity"
msgstr "Емкость" msgstr "Емкость"
#: src/renderer/components/+preferences/preferences.tsx:160 #: src/renderer/components/+preferences/preferences.tsx:163
msgid "Certificate Trust" msgid "Certificate Trust"
msgstr "" msgstr ""
@ -818,7 +826,7 @@ msgstr ""
msgid "Desired number of replicas" msgid "Desired number of replicas"
msgstr "Нужный уровень реплик" msgstr "Нужный уровень реплик"
#: src/renderer/components/cluster-manager/clusters-menu.tsx:65 #: src/renderer/components/cluster-manager/clusters-menu.tsx:62
msgid "Disconnect" msgid "Disconnect"
msgstr "" msgstr ""
@ -832,7 +840,7 @@ msgstr "Диск"
msgid "Disk:" msgid "Disk:"
msgstr "Диск:" msgstr "Диск:"
#: src/renderer/components/+preferences/preferences.tsx:165 #: src/renderer/components/+preferences/preferences.tsx:168
msgid "Does not affect cluster communications!" msgid "Does not affect cluster communications!"
msgstr "" msgstr ""
@ -928,8 +936,8 @@ msgstr "Среда"
msgid "Error stack" msgid "Error stack"
msgstr "Стэк ошибки" msgstr "Стэк ошибки"
#: src/renderer/components/+add-cluster/add-cluster.tsx:89 #: src/renderer/components/+add-cluster/add-cluster.tsx:88
#: src/renderer/components/+add-cluster/add-cluster.tsx:130 #: src/renderer/components/+add-cluster/add-cluster.tsx:129
msgid "Error while adding cluster(s): {0}" msgid "Error while adding cluster(s): {0}"
msgstr "" msgstr ""
@ -1582,7 +1590,7 @@ msgstr "Namespaces"
msgid "Namespaces: {0}" msgid "Namespaces: {0}"
msgstr "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." msgid "Needed with some corporate proxies that do certificate re-writing."
msgstr "" msgstr ""
@ -1799,7 +1807,7 @@ msgstr "Persistent Volume Claims"
msgid "Persistent Volumes" msgid "Persistent Volumes"
msgstr "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" msgid "Please select at least one cluster context"
msgstr "" msgstr ""
@ -2026,8 +2034,8 @@ msgstr "Релизы"
#: src/renderer/components/+preferences/preferences.tsx:152 #: src/renderer/components/+preferences/preferences.tsx:152
#: src/renderer/components/+user-management-roles-bindings/role-binding-details.tsx:60 #: 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:73
#: src/renderer/components/cluster-manager/clusters-menu.tsx:82 #: src/renderer/components/cluster-manager/clusters-menu.tsx:79
#: src/renderer/components/item-object-list/item-list-layout.tsx:179 #: 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:49
#: src/renderer/components/menu/menu-actions.tsx:85 #: src/renderer/components/menu/menu-actions.tsx:85
@ -2471,7 +2479,7 @@ msgstr "Установлено"
msgid "Set quota" msgid "Set quota"
msgstr "Установить квоту" msgstr "Установить квоту"
#: src/renderer/components/cluster-manager/clusters-menu.tsx:53 #: src/renderer/components/cluster-manager/clusters-menu.tsx:51
msgid "Settings" msgid "Settings"
msgstr "" msgstr ""
@ -2614,7 +2622,7 @@ msgstr "Применение.."
msgid "Subsets" msgid "Subsets"
msgstr "" msgstr ""
#: src/renderer/components/+add-cluster/add-cluster.tsx:122 #: src/renderer/components/+add-cluster/add-cluster.tsx:121
msgid "Successfully imported <0>{0}</0> cluster(s)" msgid "Successfully imported <0>{0}</0> cluster(s)"
msgstr "" msgstr ""
@ -2636,11 +2644,11 @@ msgstr "TLS"
msgid "Taints" msgid "Taints"
msgstr "Метки блокировки" msgstr "Метки блокировки"
#: src/renderer/components/+preferences/preferences.tsx:168 #: src/renderer/components/+preferences/preferences.tsx:171
msgid "Telemetry & Usage Tracking" msgid "Telemetry & Usage Tracking"
msgstr "" 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." msgid "Telemetry & usage data is collected to continuously improve the Lens experience."
msgstr "" msgstr ""
@ -2676,7 +2684,7 @@ msgstr ""
msgid "This is the quick launch menu." msgid "This is the quick launch menu."
msgstr "" 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." msgid "This will make Lens to trust ANY certificate authority without any validations."
msgstr "" msgstr ""
@ -2954,7 +2962,7 @@ msgstr ""
msgid "never" msgid "never"
msgstr "" msgstr ""
#: src/renderer/components/cluster-manager/clusters-menu.tsx:133 #: src/renderer/components/cluster-manager/clusters-menu.tsx:130
msgid "new" msgid "new"
msgstr "" msgstr ""

View File

@ -16,12 +16,13 @@
"dev-run": "nodemon --watch static/build/main.js --exec \"electron --inspect .\"", "dev-run": "nodemon --watch static/build/main.js --exec \"electron --inspect .\"",
"dev:main": "yarn compile:main --watch", "dev:main": "yarn compile:main --watch",
"dev:renderer": "yarn compile:renderer --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": "env NODE_ENV=production concurrently yarn:compile:*",
"compile:main": "webpack --config webpack.main.ts", "compile:main": "webpack --config webpack.main.ts",
"compile:renderer": "webpack --config webpack.renderer.ts", "compile:renderer": "webpack --config webpack.renderer.ts",
"compile:i18n": "lingui compile", "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:linux": "yarn compile && electron-builder --linux --dir -c.productName=Lens",
"build:mac": "yarn compile && electron-builder --mac --dir -c.productName=Lens", "build:mac": "yarn compile && electron-builder --mac --dir -c.productName=Lens",
"build:win": "yarn compile && electron-builder --win --dir -c.productName=Lens", "build:win": "yarn compile && electron-builder --win --dir -c.productName=Lens",
@ -35,6 +36,7 @@
"download-bins": "concurrently yarn:download:*", "download-bins": "concurrently yarn:download:*",
"download:kubectl": "yarn run ts-node build/download_kubectl.ts", "download:kubectl": "yarn run ts-node build/download_kubectl.ts",
"download:helm": "yarn run ts-node build/download_helm.ts", "download:helm": "yarn run ts-node build/download_helm.ts",
"build:tray-icons": "yarn run ts-node build/build_tray_icon.ts",
"lint": "eslint $@ --ext js,ts,tsx --max-warnings=0 src/" "lint": "eslint $@ --ext js,ts,tsx --max-warnings=0 src/"
}, },
"config": { "config": {
@ -96,6 +98,11 @@
"to": "static/", "to": "static/",
"filter": "!**/main.js" "filter": "!**/main.js"
}, },
{
"from": "build/tray",
"to": "static/icons",
"filter": "*.png"
},
{ {
"from": "extensions/", "from": "extensions/",
"to": "./extensions/", "to": "./extensions/",
@ -185,6 +192,20 @@
"@hapi/call": "^8.0.0", "@hapi/call": "^8.0.0",
"@hapi/subtext": "^7.0.3", "@hapi/subtext": "^7.0.3",
"@kubernetes/client-node": "^0.12.0", "@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", "array-move": "^3.0.0",
"chalk": "^4.1.0", "chalk": "^4.1.0",
"command-exists": "1.2.9", "command-exists": "1.2.9",
@ -197,8 +218,8 @@
"fs-extra": "^9.0.1", "fs-extra": "^9.0.1",
"handlebars": "^4.7.6", "handlebars": "^4.7.6",
"http-proxy": "^1.18.1", "http-proxy": "^1.18.1",
"immer": "^7.0.5",
"js-yaml": "^3.14.0", "js-yaml": "^3.14.0",
"jsdom": "^16.4.0",
"jsonpath": "^1.0.2", "jsonpath": "^1.0.2",
"lodash": "^4.17.15", "lodash": "^4.17.15",
"mac-ca": "^1.0.4", "mac-ca": "^1.0.4",
@ -275,6 +296,7 @@
"@types/request": "^2.48.5", "@types/request": "^2.48.5",
"@types/request-promise-native": "^1.0.17", "@types/request-promise-native": "^1.0.17",
"@types/semver": "^7.2.0", "@types/semver": "^7.2.0",
"@types/sharp": "^0.26.0",
"@types/shelljs": "^0.8.8", "@types/shelljs": "^0.8.8",
"@types/spdy": "^3.4.4", "@types/spdy": "^3.4.4",
"@types/tar": "^4.0.3", "@types/tar": "^4.0.3",
@ -326,7 +348,7 @@
"postinstall-postinstall": "^2.1.0", "postinstall-postinstall": "^2.1.0",
"progress-bar-webpack-plugin": "^2.1.0", "progress-bar-webpack-plugin": "^2.1.0",
"raw-loader": "^4.0.1", "raw-loader": "^4.0.1",
"react": "^16.13.1", "react": "^16.14.0",
"react-beautiful-dnd": "^13.0.0", "react-beautiful-dnd": "^13.0.0",
"react-dom": "^16.13.1", "react-dom": "^16.13.1",
"react-router": "^5.2.0", "react-router": "^5.2.0",
@ -338,6 +360,7 @@
"rollup-plugin-ignore-import": "^1.3.2", "rollup-plugin-ignore-import": "^1.3.2",
"rollup-pluginutils": "^2.8.2", "rollup-pluginutils": "^2.8.2",
"sass-loader": "^8.0.2", "sass-loader": "^8.0.2",
"sharp": "^0.26.1",
"spectron": "11.0.0", "spectron": "11.0.0",
"style-loader": "^1.2.1", "style-loader": "^1.2.1",
"terser-webpack-plugin": "^3.0.3", "terser-webpack-plugin": "^3.0.3",

View File

@ -1,6 +1,6 @@
import type { WorkspaceId } from "./workspace-store"; import type { WorkspaceId } from "./workspace-store";
import path from "path"; 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 { unlink } from "fs-extra";
import { action, computed, observable, toJS } from "mobx"; import { action, computed, observable, toJS } from "mobx";
import { BaseStore } from "./base-store"; import { BaseStore } from "./base-store";
@ -113,7 +113,7 @@ export class ClusterStore extends BaseStore<ClusterStoreModel> {
@action @action
setActive(id: ClusterId) { setActive(id: ClusterId) {
this.activeClusterId = id; this.activeClusterId = this.clusters.has(id) ? id : null;
} }
@action @action
@ -160,7 +160,7 @@ export class ClusterStore extends BaseStore<ClusterStoreModel> {
if (cluster) { if (cluster) {
this.clusters.delete(clusterId); this.clusters.delete(clusterId);
if (this.activeClusterId === clusterId) { if (this.activeClusterId === clusterId) {
this.activeClusterId = null; this.setActive(null);
} }
// remove only custom kubeconfigs (pasted as text) // remove only custom kubeconfigs (pasted as text)
if (cluster.kubeConfigPath == ClusterStore.getCustomKubeConfigPath(clusterId)) { if (cluster.kubeConfigPath == ClusterStore.getCustomKubeConfigPath(clusterId)) {

View File

@ -27,6 +27,7 @@ export interface UserPreferences {
downloadKubectlBinaries?: boolean; downloadKubectlBinaries?: boolean;
downloadBinariesPath?: string; downloadBinariesPath?: string;
kubectlBinariesPath?: string; kubectlBinariesPath?: string;
openAtLogin?: boolean;
} }
export class UserStore extends BaseStore<UserStoreModel> { export class UserStore extends BaseStore<UserStoreModel> {
@ -38,14 +39,7 @@ export class UserStore extends BaseStore<UserStoreModel> {
migrations: migrations, migrations: migrations,
}); });
// track telemetry availability this.handleOnLoad();
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);
} }
@observable lastSeenAppVersion = "0.0.0" @observable lastSeenAppVersion = "0.0.0"
@ -59,8 +53,31 @@ export class UserStore extends BaseStore<UserStoreModel> {
colorTheme: UserStore.defaultTheme, colorTheme: UserStore.defaultTheme,
downloadMirror: "default", downloadMirror: "default",
downloadKubectlBinaries: true, // Download kubectl binaries matching cluster version 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() { get isNewVersion() {
return semver.gt(getAppVersion(), this.lastSeenAppVersion); return semver.gt(getAppVersion(), this.lastSeenAppVersion);
} }

View File

@ -0,0 +1,14 @@
import { compile } from "path-to-regexp"
export interface IURLParams<P extends object = {}, Q extends object = {}> {
params?: P;
query?: Q;
}
export function buildURL<P extends object = {}, Q extends object = {}>(path: string | any) {
const pathBuilder = compile(String(path));
return function ({ params, query }: IURLParams<P, Q> = {}) {
const queryParams = query ? new URLSearchParams(Object.entries(query)).toString() : ""
return pathBuilder(params) + (queryParams ? `?${queryParams}` : "")
}
}

View File

@ -5,7 +5,9 @@ import { defineGlobal } from "./utils/defineGlobal";
export const isMac = process.platform === "darwin" export const isMac = process.platform === "darwin"
export const isWindows = process.platform === "win32" export const isWindows = process.platform === "win32"
export const isLinux = process.platform === "linux"
export const isDebugging = process.env.DEBUG === "true"; export const isDebugging = process.env.DEBUG === "true";
export const isSnap = !!process.env["SNAP"]
export const isProduction = process.env.NODE_ENV === "production" export const isProduction = process.env.NODE_ENV === "production"
export const isTestEnv = !!process.env.JEST_WORKER_ID; export const isTestEnv = !!process.env.JEST_WORKER_ID;
export const isDevelopment = !isTestEnv && !isProduction; export const isDevelopment = !isTestEnv && !isProduction;

View File

@ -1,8 +1,7 @@
import { action, computed, observable, toJS } from "mobx"; import { action, computed, observable, toJS } from "mobx";
import { BaseStore } from "./base-store"; import { BaseStore } from "./base-store";
import { clusterStore } from "./cluster-store" import { clusterStore } from "./cluster-store"
import { landingURL } from "../renderer/components/+landing-page/landing-page.route"; import { appEventBus } from "./event-bus";
import { navigate } from "../renderer/navigation";
export type WorkspaceId = string; export type WorkspaceId = string;
@ -56,18 +55,13 @@ export class WorkspaceStore extends BaseStore<WorkspaceStoreModel> {
} }
@action @action
setActive(id = WorkspaceStore.defaultId, { redirectToLanding = true, resetActiveCluster = true } = {}) { setActive(id = WorkspaceStore.defaultId, reset = true) {
if (id === this.currentWorkspaceId) return; if (id === this.currentWorkspaceId) return;
if (!this.getById(id)) { if (!this.getById(id)) {
throw new Error(`workspace ${id} doesn't exist`); throw new Error(`workspace ${id} doesn't exist`);
} }
this.currentWorkspaceId = id; this.currentWorkspaceId = id;
if (resetActiveCluster) { clusterStore.activeClusterId = null; // fixme: handle previously selected cluster from current workspace
clusterStore.setActive(null)
}
if (redirectToLanding) {
navigate(landingURL())
}
} }
@action @action
@ -79,6 +73,9 @@ export class WorkspaceStore extends BaseStore<WorkspaceStoreModel> {
} }
if (existingWorkspace) { if (existingWorkspace) {
Object.assign(existingWorkspace, workspace); Object.assign(existingWorkspace, workspace);
appEventBus.emit({name: "workspace", action: "update"})
} else {
appEventBus.emit({name: "workspace", action: "add"})
} }
this.workspaces.set(id, workspace); this.workspaces.set(id, workspace);
return workspace; return workspace;
@ -95,6 +92,7 @@ export class WorkspaceStore extends BaseStore<WorkspaceStoreModel> {
this.currentWorkspaceId = WorkspaceStore.defaultId; // reset to default this.currentWorkspaceId = WorkspaceStore.defaultId; // reset to default
} }
this.workspaces.delete(id); this.workspaces.delete(id);
appEventBus.emit({name: "workspace", action: "remove"})
clusterStore.removeByWorkspaceId(id) clusterStore.removeByWorkspaceId(id)
} }

View File

@ -0,0 +1,4 @@
import { getAppVersion } from "../../common/utils";
export const version = getAppVersion()
export { isSnap, isWindows, isMac, isLinux, appName, slackUrl, issuesTrackerUrl } from "../../common/vars"

View File

@ -5,20 +5,21 @@ export * from "../lens-renderer-extension"
import type { WindowManager } from "../../main/window-manager"; import type { WindowManager } from "../../main/window-manager";
// APIs // APIs
import * as App from "./app"
import * as EventBus from "./event-bus" import * as EventBus from "./event-bus"
import * as Store from "./stores" import * as Store from "./stores"
import * as Util from "./utils" import * as Util from "./utils"
import * as Registry from "../registries" import * as Registry from "../registries"
import * as CommonVars from "../../common/vars";
import * as ClusterFeature from "./cluster-feature" import * as ClusterFeature from "./cluster-feature"
// TODO: allow to expose windowManager.navigate() as Navigation.navigate() in runtime
export let windowManager: WindowManager; export let windowManager: WindowManager;
export { export {
App,
EventBus, EventBus,
ClusterFeature, ClusterFeature,
Store, Store,
Util, Util,
Registry, Registry,
CommonVars,
} }

View File

@ -1,2 +1,4 @@
export { ExtensionStore } from "../extension-store" export { ExtensionStore } from "../extension-store"
export { clusterStore, ClusterModel } from "../../common/cluster-store"
export { workspaceStore} from "../../common/workspace-store"
export type { Cluster } from "../../main/cluster" export type { Cluster } from "../../main/cluster"

View File

@ -1,5 +1,6 @@
import type { ExtensionManifest } from "./lens-extension" import type { ExtensionManifest } from "./lens-extension"
import path from "path" import path from "path"
import os from "os"
import fs from "fs-extra" import fs from "fs-extra"
import logger from "../main/logger" import logger from "../main/logger"
import { extensionPackagesRoot, InstalledExtension } from "./extension-loader" import { extensionPackagesRoot, InstalledExtension } from "./extension-loader"
@ -24,10 +25,14 @@ export class ExtensionManager {
return extensionPackagesRoot() return extensionPackagesRoot()
} }
get folderPath(): string { get inTreeFolderPath(): string {
return path.resolve(__static, "../extensions"); return path.resolve(__static, "../extensions");
} }
get localFolderPath(): string {
return path.join(os.homedir(), ".k8slens", "extensions");
}
get npmPath() { get npmPath() {
return __non_webpack_require__.resolve('npm/bin/npm-cli') return __non_webpack_require__.resolve('npm/bin/npm-cli')
} }
@ -35,7 +40,7 @@ export class ExtensionManager {
async load() { async load() {
logger.info("[EXTENSION-MANAGER] loading extensions from " + this.extensionPackagesRoot) logger.info("[EXTENSION-MANAGER] loading extensions from " + this.extensionPackagesRoot)
await fs.ensureDir(path.join(this.extensionPackagesRoot, "node_modules")) await fs.ensureDir(path.join(this.extensionPackagesRoot, "node_modules"))
await fs.ensureDir(this.localFolderPath)
return await this.loadExtensions(); return await this.loadExtensions();
} }
@ -74,14 +79,17 @@ export class ExtensionManager {
} }
async loadExtensions() { async loadExtensions() {
const extensions = await this.loadFromFolder(this.folderPath); const bundledExtensions = await this.loadBundledExtensions()
const localExtendions = await this.loadFromFolder(this.localFolderPath)
const extensions = bundledExtensions.concat(localExtendions)
return new Map(extensions.map(ext => [ext.id, ext])); return new Map(extensions.map(ext => [ext.id, ext]));
} }
async loadFromFolder(folderPath: string): Promise<InstalledExtension[]> { async loadBundledExtensions() {
const paths = await fs.readdir(folderPath);
const extensions: InstalledExtension[] = [] const extensions: InstalledExtension[] = []
const folderPath = this.inTreeFolderPath
const bundledExtensions = getBundledExtensions() const bundledExtensions = getBundledExtensions()
const paths = await fs.readdir(folderPath);
for (const fileName of paths) { for (const fileName of paths) {
if (!bundledExtensions.includes(fileName)) { if (!bundledExtensions.includes(fileName)) {
continue continue
@ -94,10 +102,33 @@ export class ExtensionManager {
extensions.push(ext) extensions.push(ext)
} }
} }
logger.debug(`[EXTENSION-MANAGER]: ${extensions.length} extensions loaded`, { folderPath, extensions });
await fs.writeFile(path.join(this.extensionPackagesRoot, "package.json"), JSON.stringify(this.packagesJson), {mode: 0o600})
await this.installPackages()
return extensions
}
async loadFromFolder(folderPath: string): Promise<InstalledExtension[]> {
const bundledExtensions = getBundledExtensions()
const extensions: InstalledExtension[] = []
const paths = await fs.readdir(folderPath);
for (const fileName of paths) {
if (bundledExtensions.includes(fileName)) { // do no allow to override bundled extensions
continue
}
const absPath = path.resolve(folderPath, fileName);
const manifestPath = path.resolve(absPath, "package.json");
await fs.access(manifestPath, fs.constants.F_OK)
const ext = await this.getExtensionByManifest(manifestPath).catch(() => null)
if (ext) {
extensions.push(ext)
}
}
logger.debug(`[EXTENSION-MANAGER]: ${extensions.length} extensions loaded`, { folderPath, extensions });
await fs.writeFile(path.join(this.extensionPackagesRoot, "package.json"), JSON.stringify(this.packagesJson), {mode: 0o600}) await fs.writeFile(path.join(this.extensionPackagesRoot, "package.json"), JSON.stringify(this.packagesJson), {mode: 0o600})
await this.installPackages() await this.installPackages()
logger.debug(`[EXTENSION-MANAGER]: ${extensions.length} extensions loaded`, { folderPath, extensions });
return extensions; return extensions;
} }
} }

View File

@ -5,9 +5,7 @@
"version": "0.0.0", "version": "0.0.0",
"copyright": "© 2020, Mirantis, Inc.", "copyright": "© 2020, Mirantis, Inc.",
"license": "MIT", "license": "MIT",
"files": [ "types": "api.d.ts",
"api.d.ts"
],
"author": { "author": {
"name": "Mirantis, Inc.", "name": "Mirantis, Inc.",
"email": "info@k8slens.dev" "email": "info@k8slens.dev"

View File

@ -1,19 +1,19 @@
import { autoUpdater } from "electron-updater" import { autoUpdater } from "electron-updater"
import logger from "./logger" 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 autoUpdater.logger = logger
} }
public start() { public start() {
setInterval(() => { setInterval(AppUpdater.checkForUpdates, this.updateInterval)
autoUpdater.checkForUpdatesAndNotify() return AppUpdater.checkForUpdates();
}, this.updateInterval)
return autoUpdater.checkForUpdatesAndNotify()
} }
} }

View File

@ -37,12 +37,12 @@ export class HelmRepoManager extends Singleton {
async loadAvailableRepos(): Promise<HelmRepo[]> { async loadAvailableRepos(): Promise<HelmRepo[]> {
const res = await customRequestPromise({ const res = await customRequestPromise({
uri: "https://hub.helm.sh/assets/js/repos.json", uri: "https://github.com/lensapp/artifact-hub-repositories/releases/download/latest/repositories.json",
json: true, json: true,
resolveWithFullResponse: true, resolveWithFullResponse: true,
timeout: 10000, timeout: 10000,
}); });
return orderBy<HelmRepo>(res.body.data, repo => repo.name); return orderBy<HelmRepo>(res.body, repo => repo.name);
} }
async init() { async init() {

View File

@ -10,7 +10,7 @@ import path from "path"
import { LensProxy } from "./lens-proxy" import { LensProxy } from "./lens-proxy"
import { WindowManager } from "./window-manager"; import { WindowManager } from "./window-manager";
import { ClusterManager } from "./cluster-manager"; import { ClusterManager } from "./cluster-manager";
import AppUpdater from "./app-updater" import { AppUpdater } from "./app-updater"
import { shellSync } from "./shell-sync" import { shellSync } from "./shell-sync"
import { getFreePort } from "./port" import { getFreePort } from "./port"
import { mangleProxyEnv } from "./proxy-env" import { mangleProxyEnv } from "./proxy-env"
@ -24,45 +24,46 @@ import { extensionLoader } from "../extensions/extension-loader";
import logger from "./logger" import logger from "./logger"
const workingDir = path.join(app.getPath("appData"), appName); const workingDir = path.join(app.getPath("appData"), appName);
let proxyPort: number;
let proxyServer: LensProxy;
let clusterManager: ClusterManager;
let windowManager: WindowManager;
app.setName(appName); app.setName(appName);
if (!process.env.CICD) { if (!process.env.CICD) {
app.setPath("userData", workingDir); app.setPath("userData", workingDir);
} }
let clusterManager: ClusterManager;
let proxyServer: LensProxy;
mangleProxyEnv() mangleProxyEnv()
if (app.commandLine.getSwitchValue("proxy-server") !== "") { if (app.commandLine.getSwitchValue("proxy-server") !== "") {
process.env.HTTPS_PROXY = app.commandLine.getSwitchValue("proxy-server") process.env.HTTPS_PROXY = app.commandLine.getSwitchValue("proxy-server")
} }
async function main() { app.on("ready", async () => {
await shellSync();
logger.info(`🚀 Starting Lens from "${workingDir}"`) logger.info(`🚀 Starting Lens from "${workingDir}"`)
await shellSync();
const updater = new AppUpdater() const updater = new AppUpdater()
updater.start(); updater.start();
registerFileProtocol("static", __static); registerFileProtocol("static", __static);
// find free port // preload isomorphic stores
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
await Promise.all([ await Promise.all([
userStore.load(), userStore.load(),
clusterStore.load(), clusterStore.load(),
workspaceStore.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 // create cluster manager
clusterManager = new ClusterManager(proxyPort); clusterManager = new ClusterManager(proxyPort);
@ -72,28 +73,34 @@ async function main() {
} catch (error) { } catch (error) {
logger.error(`Could not start proxy (127.0.0:${proxyPort}): ${error.message}`) 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"}`) 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 windowManager = new WindowManager(proxyPort);
LensExtensionsApi.windowManager = new WindowManager(proxyPort);
LensExtensionsApi.windowManager = windowManager; // expose to extensions
extensionLoader.loadOnMain() extensionLoader.loadOnMain()
extensionLoader.extensions.replace(await extensionManager.load()) extensionLoader.extensions.replace(await extensionManager.load())
extensionLoader.broadcastExtensions() extensionLoader.broadcastExtensions()
setTimeout(() => { setTimeout(() => {
appEventBus.emit({name: "app", action: "start"}) appEventBus.emit({ name: "app", action: "start" })
}, 1000) }, 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) => { // Quit app on Cmd+Q (MacOS)
event.preventDefault(); // To allow mixpanel sending to be executed app.on("will-quit", (event) => {
if (proxyServer) proxyServer.close() logger.info('APP:QUIT');
if (clusterManager) clusterManager.stop() event.preventDefault(); // prevent app's default shutdown (e.g. required for telemetry, etc.)
app.exit(); 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 // Extensions-api runtime exports

View File

@ -12,11 +12,27 @@ import logger from "./logger";
export type MenuTopId = "mac" | "file" | "edit" | "view" | "help" export type MenuTopId = "mac" | "file" | "edit" | "view" | "help"
export function initMenu(windowManager: WindowManager) { export function initMenu(windowManager: WindowManager) {
autorun(() => buildMenu(windowManager), { return autorun(() => buildMenu(windowManager), {
delay: 100 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) { export function buildMenu(windowManager: WindowManager) {
function ignoreOnMac(menuItems: MenuItemConstructorOptions[]) { function ignoreOnMac(menuItems: MenuItemConstructorOptions[]) {
if (isMac) return []; if (isMac) return [];
@ -32,28 +48,9 @@ export function buildMenu(windowManager: WindowManager) {
return menuItems; return menuItems;
} }
function navigate(url: string) { async function navigate(url: string) {
logger.info(`[MENU]: navigating to ${url}`); logger.info(`[MENU]: navigating to ${url}`);
windowManager.navigate({ await windowManager.navigate(url);
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")
})
} }
const macAppMenu: MenuItemConstructorOptions = { const macAppMenu: MenuItemConstructorOptions = {
@ -80,7 +77,13 @@ export function buildMenu(windowManager: WindowManager) {
{ role: 'hideOthers' }, { role: 'hideOthers' },
{ role: 'unhide' }, { role: 'unhide' },
{ type: 'separator' }, { 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' }, { type: 'separator' },
{ role: 'quit' } { role: 'quit' }
]) ]),
{ type: 'separator' },
{ role: 'close' } // close current window
] ]
}; };
@ -158,7 +163,7 @@ export function buildMenu(windowManager: WindowManager) {
label: 'Reload', label: 'Reload',
accelerator: 'CmdOrCtrl+R', accelerator: 'CmdOrCtrl+R',
click() { click() {
windowManager.reload({ channel: "menu:reload" }); windowManager.reload();
} }
}, },
{ role: 'toggleDevTools' }, { role: 'toggleDevTools' },

126
src/main/tray.ts Normal file
View File

@ -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();
}
}
]);
}

View File

@ -1,95 +1,140 @@
import type { ClusterId } from "../common/cluster-store"; import type { ClusterId } from "../common/cluster-store";
import { clusterStore } 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 { 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 { extensionLoader } from "../extensions/extension-loader";
import { appEventBus } from "../common/event-bus" import { appEventBus } from "../common/event-bus"
import { initMenu } from "./menu";
import { initTray } from "./tray";
export class WindowManager { export class WindowManager {
protected mainView: BrowserWindow; protected mainWindow: BrowserWindow;
protected splashWindow: BrowserWindow; protected splashWindow: BrowserWindow;
protected windowState: windowStateKeeper.State; protected windowState: windowStateKeeper.State;
protected disposers: Record<string, Function> = {};
@observable activeClusterId: ClusterId; @observable activeClusterId: ClusterId;
constructor(protected proxyPort: number) { 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 // Manage main window size and position with state persistence
this.windowState = windowStateKeeper({ if (!this.windowState) {
defaultHeight: 900, this.windowState = windowStateKeeper({
defaultWidth: 1440, defaultHeight: 900,
}); defaultWidth: 1440,
});
}
if (!this.mainWindow) {
// show icon in dock (mac-os only)
app.dock?.show();
const { width, height, x, y } = this.windowState; const { width, height, x, y } = this.windowState;
this.mainView = new BrowserWindow({ this.mainWindow = new BrowserWindow({
x, y, width, height, x, y, width, height,
show: false, show: false,
minWidth: 700, // accommodate 800 x 600 display minimum minWidth: 700, // accommodate 800 x 600 display minimum
minHeight: 500, // accommodate 800 x 600 display minimum minHeight: 500, // accommodate 800 x 600 display minimum
titleBarStyle: "hidden", titleBarStyle: "hidden",
backgroundColor: "#1e2124", backgroundColor: "#1e2124",
webPreferences: { webPreferences: {
nodeIntegration: true, nodeIntegration: true,
nodeIntegrationInSubFrames: true, nodeIntegrationInSubFrames: true,
enableRemoteModule: true, enableRemoteModule: true,
}, },
}); });
this.windowState.manage(this.mainView); this.windowState.manage(this.mainWindow);
// open external links in default browser (target=_blank, window.open) // open external links in default browser (target=_blank, window.open)
this.mainView.webContents.on("new-window", (event, url) => { this.mainWindow.webContents.on("new-window", (event, url) => {
event.preventDefault(); event.preventDefault();
shell.openExternal(url); shell.openExternal(url);
}); });
this.mainView.webContents.on("dom-ready", () => { this.mainWindow.webContents.on("dom-ready", () => {
extensionLoader.broadcastExtensions() extensionLoader.broadcastExtensions()
}) })
this.mainView.on("focus", () => { this.mainWindow.on("focus", () => {
appEventBus.emit({name: "app", action: "focus"}) appEventBus.emit({name: "app", action: "focus"})
}) })
this.mainView.on("blur", () => { this.mainWindow.on("blur", () => {
appEventBus.emit({name: "app", action: "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 // track visible cluster from ui
ipcMain.on("cluster-view:current-id", (event, clusterId: ClusterId) => { ipcMain.on("cluster-view:current-id", (event, clusterId: ClusterId) => {
this.activeClusterId = clusterId; this.activeClusterId = clusterId;
}); });
// load & show app
this.showMain();
initMenu(this);
} }
navigate({ url, channel, frameId }: { url: string, channel: string, frameId?: number }) { async ensureMainWindow(): Promise<BrowserWindow> {
if (!this.mainWindow) await this.initMainWindow();
this.mainWindow.show();
return this.mainWindow;
}
sendToView({ channel, frameId, data = [] }: { channel: string, frameId?: number, data?: any[] }) {
if (frameId) { if (frameId) {
this.mainView.webContents.sendToFrame(frameId, channel, url); this.mainWindow.webContents.sendToFrame(frameId, channel, ...data);
} else { } 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; const frameId = clusterStore.getById(this.activeClusterId)?.frameId;
if (frameId) { if (frameId) {
this.mainView.webContents.sendToFrame(frameId, channel); this.sendToView({ channel: "menu:reload", frameId });
} else { } else {
webContents.getFocusedWebContents()?.reload(); 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() { async showSplash() {
if (!this.splashWindow) { if (!this.splashWindow) {
this.splashWindow = new BrowserWindow({ this.splashWindow = new BrowserWindow({
@ -110,8 +155,13 @@ export class WindowManager {
} }
destroy() { destroy() {
this.windowState.unmanage(); this.mainWindow.destroy();
this.splashWindow.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]
});
} }
} }

View File

@ -141,7 +141,7 @@ export class HelmRelease implements ItemObject {
chart: string chart: string
status: string status: string
updated: string updated: string
revision: number revision: string
getId() { getId() {
return this.namespace + this.name; return this.namespace + this.name;
@ -165,7 +165,7 @@ export class HelmRelease implements ItemObject {
} }
getRevision() { getRevision() {
return this.revision; return parseInt(this.revision, 10);
} }
getStatus() { getStatus() {

View File

@ -109,6 +109,14 @@ export class Ingress extends KubeObject {
} }
return ports.join(", ") return ports.join(", ")
} }
getLoadBalancers() {
const { status: { loadBalancer = { ingress: [] } } } = this;
return (loadBalancer.ingress ?? []).map(address => (
address.hostname || address.ip
))
}
} }
export const ingressApi = new IngressApi({ export const ingressApi = new IngressApi({

View File

@ -152,7 +152,16 @@ export interface IPodContainerStatus {
reason: string; reason: string;
}; };
}; };
lastState: {}; lastState: {
[index: string]: object;
terminated?: {
startedAt: string;
finishedAt: string;
exitCode: number;
reason: string;
containerID: string;
};
};
ready: boolean; ready: boolean;
restartCount: number; restartCount: number;
image: string; image: string;

View File

@ -1,5 +1,5 @@
import { RouteProps } from "react-router"; import type { RouteProps } from "react-router";
import { buildURL } from "../../navigation"; import { buildURL } from "../../../common/utils/buildUrl";
export const addClusterRoute: RouteProps = { export const addClusterRoute: RouteProps = {
path: "/add-cluster" path: "/add-cluster"

View File

@ -46,6 +46,7 @@ export class AddCluster extends React.Component {
@observable dropAreaActive = false; @observable dropAreaActive = false;
componentDidMount() { componentDidMount() {
clusterStore.setActive(null);
this.setKubeConfig(userStore.kubeConfigPath); this.setKubeConfig(userStore.kubeConfigPath);
} }
@ -118,17 +119,16 @@ export class AddCluster extends React.Component {
} }
} }
@action
addClusters = () => { addClusters = () => {
const configValidationErrors:string[] = [];
let newClusters: ClusterModel[] = []; let newClusters: ClusterModel[] = [];
try { try {
if (!this.selectedContexts.length) { if (!this.selectedContexts.length) {
this.error = <Trans>Please select at least one cluster context</Trans> this.error = <Trans>Please select at least one cluster context</Trans>
return; return;
} }
this.error = "" this.error = ""
this.isWaiting = true this.isWaiting = true
newClusters = this.selectedContexts.filter(context => { newClusters = this.selectedContexts.filter(context => {
try { try {
@ -138,8 +138,8 @@ export class AddCluster extends React.Component {
} catch (err) { } catch (err) {
this.error = String(err.message) this.error = String(err.message)
if (err instanceof ExecValidationNotFoundError ) { if (err instanceof ExecValidationNotFoundError ) {
Notifications.error(<Trans>Error while adding cluster(s): {this.error}</Trans>); Notifications.error(<Trans>Error while adding cluster(s): {this.error}</Trans>);
return false; return false;
} else { } else {
throw new Error(err); throw new Error(err);
} }
@ -169,7 +169,7 @@ export class AddCluster extends React.Component {
clusterStore.setActive(clusterId); clusterStore.setActive(clusterId);
navigate(clusterViewURL({ params: { clusterId } })); navigate(clusterViewURL({ params: { clusterId } }));
} else { } else {
if (newClusters.length > 1) { if (newClusters.length > 1) {
Notifications.ok( Notifications.ok(
<Trans>Successfully imported <b>{newClusters.length}</b> cluster(s)</Trans> <Trans>Successfully imported <b>{newClusters.length}</b> cluster(s)</Trans>
); );

View File

@ -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 { appsRoute } from "../+apps/apps.route";
import { buildURL } from "../../navigation";
export const helmChartsRoute: RouteProps = { export const helmChartsRoute: RouteProps = {
path: appsRoute.path + "/charts/:repo?/:chartName?" path: appsRoute.path + "/charts/:repo?/:chartName?"

View File

@ -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 { appsRoute } from "../+apps/apps.route";
import { buildURL } from "../../navigation";
export const releaseRoute: RouteProps = { export const releaseRoute: RouteProps = {
path: appsRoute.path + "/releases/:namespace?/:name?" path: appsRoute.path + "/releases/:namespace?/:name?"

View File

@ -1,5 +1,5 @@
import { RouteProps } from "react-router"; import type { RouteProps } from "react-router";
import { buildURL } from "../../navigation"; import { buildURL } from "../../../common/utils/buildUrl";
export const appsRoute: RouteProps = { export const appsRoute: RouteProps = {
path: "/apps", path: "/apps",

View File

@ -1,6 +1,6 @@
import type { IClusterViewRouteParams } from "../cluster-manager/cluster-view.route"; import type { IClusterViewRouteParams } from "../cluster-manager/cluster-view.route";
import { RouteProps } from "react-router"; import type { RouteProps } from "react-router";
import { buildURL } from "../../navigation"; import { buildURL } from "../../../common/utils/buildUrl";
export interface IClusterSettingsRouteParams extends IClusterViewRouteParams { export interface IClusterSettingsRouteParams extends IClusterViewRouteParams {
} }

View File

@ -1,8 +1,9 @@
import "./cluster-settings.scss"; import "./cluster-settings.scss";
import React from "react"; import React from "react";
import { autorun } from "mobx"; import { reaction } from "mobx";
import { disposeOnUnmount, observer } from "mobx-react"; import { RouteComponentProps } from "react-router";
import { observer, disposeOnUnmount } from "mobx-react";
import { Features } from "./features"; import { Features } from "./features";
import { Removal } from "./removal"; import { Removal } from "./removal";
import { Status } from "./status"; import { Status } from "./status";
@ -11,7 +12,6 @@ import { Cluster } from "../../../main/cluster";
import { ClusterIcon } from "../cluster-icon"; import { ClusterIcon } from "../cluster-icon";
import { IClusterSettingsRouteParams } from "./cluster-settings.route"; import { IClusterSettingsRouteParams } from "./cluster-settings.route";
import { clusterStore } from "../../../common/cluster-store"; import { clusterStore } from "../../../common/cluster-store";
import { RouteComponentProps } from "react-router";
import { clusterIpc } from "../../../common/cluster-ipc"; import { clusterIpc } from "../../../common/cluster-ipc";
import { PageLayout } from "../layout/page-layout"; import { PageLayout } from "../layout/page-layout";
@ -20,16 +20,23 @@ interface Props extends RouteComponentProps<IClusterSettingsRouteParams> {
@observer @observer
export class ClusterSettings extends React.Component<Props> { export class ClusterSettings extends React.Component<Props> {
get cluster(): Cluster { get clusterId() {
return clusterStore.getById(this.props.match.params.clusterId); return this.props.match.params.clusterId
} }
async componentDidMount() { get cluster(): Cluster {
disposeOnUnmount(this, return clusterStore.getById(this.clusterId);
autorun(() => { }
this.refreshCluster();
componentDidMount() {
disposeOnUnmount(this, [
reaction(() => this.cluster, this.refreshCluster, {
fireImmediately: true,
}),
reaction(() => this.clusterId, clusterId => clusterStore.setActive(clusterId), {
fireImmediately: true,
}) })
) ])
} }
refreshCluster = async () => { refreshCluster = async () => {

View File

@ -1,5 +1,5 @@
import { RouteProps } from "react-router"; import type { RouteProps } from "react-router";
import { buildURL } from "../../navigation"; import { buildURL } from "../../../common/utils/buildUrl";
export const clusterRoute: RouteProps = { export const clusterRoute: RouteProps = {
path: "/cluster" path: "/cluster"

View File

@ -1,5 +1,5 @@
import { RouteProps } from "react-router"; import type { RouteProps } from "react-router";
import { buildURL } from "../../navigation"; import { buildURL } from "../../../common/utils/buildUrl";
export const hpaRoute: RouteProps = { export const hpaRoute: RouteProps = {
path: "/hpa" path: "/hpa"

View File

@ -1,5 +1,5 @@
import { RouteProps } from "react-router"; import type { RouteProps } from "react-router";
import { buildURL } from "../../navigation"; import { buildURL } from "../../../common/utils/buildUrl";
export const configMapsRoute: RouteProps = { export const configMapsRoute: RouteProps = {
path: "/configmaps" path: "/configmaps"

View File

@ -1,5 +1,5 @@
import { RouteProps } from "react-router"; import type { RouteProps } from "react-router";
import { buildURL } from "../../navigation"; import { buildURL } from "../../../common/utils/buildUrl";
export const pdbRoute: RouteProps = { export const pdbRoute: RouteProps = {
path: "/poddisruptionbudgets" path: "/poddisruptionbudgets"

View File

@ -1,5 +1,5 @@
import { RouteProps } from "react-router"; import type { RouteProps } from "react-router";
import { buildURL } from "../../navigation"; import { buildURL } from "../../../common/utils/buildUrl";
export const resourceQuotaRoute: RouteProps = { export const resourceQuotaRoute: RouteProps = {
path: "/resourcequotas" path: "/resourcequotas"

View File

@ -1,5 +1,5 @@
import { RouteProps } from "react-router"; import type { RouteProps } from "react-router";
import { buildURL } from "../../navigation"; import { buildURL } from "../../../common/utils/buildUrl";
export const secretsRoute: RouteProps = { export const secretsRoute: RouteProps = {
path: "/secrets" path: "/secrets"

View File

@ -1,7 +1,7 @@
import { RouteProps } from "react-router"; import { RouteProps } from "react-router";
import { configMapsURL } from "../+config-maps";
import { Config } from "./config"; 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 = { export const configRoute: RouteProps = {
get path() { get path() {

View File

@ -10,8 +10,8 @@ import { resourceQuotaRoute, ResourceQuotas, resourceQuotaURL } from "../+config
import { PodDisruptionBudgets, pdbRoute, pdbURL } from "../+config-pod-disruption-budgets"; import { PodDisruptionBudgets, pdbRoute, pdbURL } from "../+config-pod-disruption-budgets";
import { configURL } from "./config.route"; import { configURL } from "./config.route";
import { HorizontalPodAutoscalers, hpaRoute, hpaURL } from "../+config-autoscalers"; import { HorizontalPodAutoscalers, hpaRoute, hpaURL } from "../+config-autoscalers";
import { buildURL } from "../../navigation";
import { isAllowedResource } from "../../../common/rbac" import { isAllowedResource } from "../../../common/rbac"
import { buildURL } from "../../../common/utils/buildUrl";
export const certificatesURL = buildURL("/certificates"); export const certificatesURL = buildURL("/certificates");
export const issuersURL = buildURL("/issuers"); export const issuersURL = buildURL("/issuers");

View File

@ -1,5 +1,5 @@
import { RouteProps } from "react-router"; import type { RouteProps } from "react-router";
import { buildURL } from "../../navigation"; import { buildURL } from "../../../common/utils/buildUrl";
export const crdRoute: RouteProps = { export const crdRoute: RouteProps = {
path: "/crd" path: "/crd"

View File

@ -1,5 +1,5 @@
import { RouteProps } from "react-router"; import type { RouteProps } from "react-router";
import { buildURL } from "../../navigation"; import { buildURL } from "../../../common/utils/buildUrl";
export const eventRoute: RouteProps = { export const eventRoute: RouteProps = {
path: "/events" path: "/events"

View File

@ -1,5 +1,5 @@
import { RouteProps } from "react-router"; import type { RouteProps } from "react-router";
import { buildURL } from "../../navigation"; import { buildURL } from "../../../common/utils/buildUrl";
export const landingRoute: RouteProps = { export const landingRoute: RouteProps = {
path: "/landing" path: "/landing"

View File

@ -1,5 +1,5 @@
import { RouteProps } from "react-router" import type { RouteProps } from "react-router";
import { buildURL } from "../../navigation"; import { buildURL } from "../../../common/utils/buildUrl";
export const namespacesRoute: RouteProps = { export const namespacesRoute: RouteProps = {
path: "/namespaces" path: "/namespaces"

View File

@ -1,5 +1,5 @@
import { RouteProps } from "react-router" import type { RouteProps } from "react-router";
import { buildURL } from "../../navigation"; import { buildURL } from "../../../common/utils/buildUrl";
export const endpointRoute: RouteProps = { export const endpointRoute: RouteProps = {
path: "/endpoints" path: "/endpoints"

View File

@ -1,5 +1,5 @@
import { RouteProps } from "react-router" import type { RouteProps } from "react-router";
import { buildURL } from "../../navigation"; import { buildURL } from "../../../common/utils/buildUrl";
export const ingressRoute: RouteProps = { export const ingressRoute: RouteProps = {
path: "/ingresses" path: "/ingresses"

View File

@ -39,12 +39,14 @@ export class Ingresses extends React.Component<Props> {
renderTableHeader={[ renderTableHeader={[
{ title: <Trans>Name</Trans>, className: "name", sortBy: sortBy.name }, { title: <Trans>Name</Trans>, className: "name", sortBy: sortBy.name },
{ title: <Trans>Namespace</Trans>, className: "namespace", sortBy: sortBy.namespace }, { title: <Trans>Namespace</Trans>, className: "namespace", sortBy: sortBy.namespace },
{ title: <Trans>LoadBalancers</Trans>, className: "loadbalancers" },
{ title: <Trans>Rules</Trans>, className: "rules" }, { title: <Trans>Rules</Trans>, className: "rules" },
{ title: <Trans>Age</Trans>, className: "age", sortBy: sortBy.age }, { title: <Trans>Age</Trans>, className: "age", sortBy: sortBy.age },
]} ]}
renderTableContents={(ingress: Ingress) => [ renderTableContents={(ingress: Ingress) => [
ingress.getName(), ingress.getName(),
ingress.getNs(), ingress.getNs(),
ingress.getLoadBalancers().map(lb => <p key={lb}>{lb}</p>),
ingress.getRoutes().map(route => <p key={route}>{route}</p>), ingress.getRoutes().map(route => <p key={route}>{route}</p>),
ingress.getAge(), ingress.getAge(),
]} ]}

View File

@ -1,5 +1,5 @@
import { RouteProps } from "react-router" import type { RouteProps } from "react-router";
import { buildURL } from "../../navigation"; import { buildURL } from "../../../common/utils/buildUrl";
export const networkPoliciesRoute: RouteProps = { export const networkPoliciesRoute: RouteProps = {
path: "/network-policies" path: "/network-policies"

View File

@ -1,5 +1,5 @@
import { RouteProps } from "react-router" import type { RouteProps } from "react-router";
import { buildURL } from "../../navigation"; import { buildURL } from "../../../common/utils/buildUrl";
export const servicesRoute: RouteProps = { export const servicesRoute: RouteProps = {
path: "/services" path: "/services"

View File

@ -1,7 +1,7 @@
import { RouteProps } from "react-router" import { RouteProps } from "react-router"
import { Network } from "./network"; import { Network } from "./network";
import { servicesURL } from "../+network-services"; import { servicesURL } from "../+network-services";
import { IURLParams } from "../../navigation"; import { IURLParams } from "../../../common/utils/buildUrl";
export const networkRoute: RouteProps = { export const networkRoute: RouteProps = {
get path() { get path() {

View File

@ -1,5 +1,5 @@
import { RouteProps } from "react-router" import type { RouteProps } from "react-router";
import { buildURL } from "../../navigation"; import { buildURL } from "../../../common/utils/buildUrl";
export const nodesRoute: RouteProps = { export const nodesRoute: RouteProps = {
path: "/nodes" path: "/nodes"

View File

@ -1,5 +1,5 @@
import { RouteProps } from "react-router" import type { RouteProps } from "react-router";
import { buildURL } from "../../navigation"; import { buildURL } from "../../../common/utils/buildUrl";
export const podSecurityPoliciesRoute: RouteProps = { export const podSecurityPoliciesRoute: RouteProps = {
path: "/pod-security-policies" path: "/pod-security-policies"

View File

@ -1,5 +1,5 @@
import { RouteProps } from "react-router"; import type { RouteProps } from "react-router";
import { buildURL } from "../../navigation"; import { buildURL } from "../../../common/utils/buildUrl";
export const preferencesRoute: RouteProps = { export const preferencesRoute: RouteProps = {
path: "/preferences" path: "/preferences"

View File

@ -21,4 +21,8 @@
display: none; display: none;
} }
} }
.Checkbox {
align-self: start; // limit clickable area to checkbox + text
}
} }

View File

@ -156,6 +156,13 @@ export class Preferences extends React.Component {
})} })}
</div> </div>
<h2><Trans>Auto start-up</Trans></h2>
<Checkbox
label={<Trans>Automatically start Lens on login</Trans>}
value={preferences.openAtLogin}
onChange={v => preferences.openAtLogin = v}
/>
<h2><Trans>Certificate Trust</Trans></h2> <h2><Trans>Certificate Trust</Trans></h2>
<Checkbox <Checkbox
label={<Trans>Allow untrusted Certificate Authorities</Trans>} label={<Trans>Allow untrusted Certificate Authorities</Trans>}

View File

@ -1,5 +1,5 @@
import { RouteProps } from "react-router" import type { RouteProps } from "react-router";
import { buildURL } from "../../navigation"; import { buildURL } from "../../../common/utils/buildUrl";
export const storageClassesRoute: RouteProps = { export const storageClassesRoute: RouteProps = {
path: "/storage-classes" path: "/storage-classes"

View File

@ -1,5 +1,5 @@
import { RouteProps } from "react-router" import type { RouteProps } from "react-router";
import { buildURL } from "../../navigation"; import { buildURL } from "../../../common/utils/buildUrl";
export const volumeClaimsRoute: RouteProps = { export const volumeClaimsRoute: RouteProps = {
path: "/persistent-volume-claims" path: "/persistent-volume-claims"

View File

@ -1,5 +1,5 @@
import { RouteProps } from "react-router" import type { RouteProps } from "react-router";
import { buildURL } from "../../navigation"; import { buildURL } from "../../../common/utils/buildUrl";
export const volumesRoute: RouteProps = { export const volumesRoute: RouteProps = {
path: "/persistent-volumes" path: "/persistent-volumes"

View File

@ -1,7 +1,7 @@
import { RouteProps } from "react-router" import { RouteProps } from "react-router"
import { volumeClaimsURL } from "../+storage-volume-claims"; import { volumeClaimsURL } from "../+storage-volume-claims";
import { Storage } from "./storage"; import { Storage } from "./storage";
import { IURLParams } from "../../navigation"; import { IURLParams } from "../../../common/utils/buildUrl";
export const storageRoute: RouteProps = { export const storageRoute: RouteProps = {
get path() { get path() {

View File

@ -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 { UserManagement } from "./user-management"
import { buildURL, IURLParams } from "../../navigation";
export const usersManagementRoute: RouteProps = { export const usersManagementRoute: RouteProps = {
get path() { get path() {
@ -30,9 +30,7 @@ export interface IRolesRouteParams {
} }
// URL-builders // URL-builders
export const usersManagementURL = (params?: IURLParams) => serviceAccountsURL(params);
export const serviceAccountsURL = buildURL<IServiceAccountsRouteParams>(serviceAccountsRoute.path) export const serviceAccountsURL = buildURL<IServiceAccountsRouteParams>(serviceAccountsRoute.path)
export const roleBindingsURL = buildURL<IRoleBindingsRouteParams>(roleBindingsRoute.path) export const roleBindingsURL = buildURL<IRoleBindingsRouteParams>(roleBindingsRoute.path)
export const rolesURL = buildURL<IRoleBindingsRouteParams>(rolesRoute.path) export const rolesURL = buildURL<IRoleBindingsRouteParams>(rolesRoute.path)
export const usersManagementURL = (params?: IURLParams) => {
return serviceAccountsURL(params);
};

View File

@ -1,5 +1,5 @@
import { RouteProps } from "react-router"; import type { RouteProps } from "react-router";
import { buildURL } from "../../navigation"; import { buildURL } from "../../../common/utils/buildUrl";
export const whatsNewRoute: RouteProps = { export const whatsNewRoute: RouteProps = {
path: "/what-s-new" path: "/what-s-new"

View File

@ -1,7 +1,6 @@
import "./pod-container-env.scss"; import "./pod-container-env.scss";
import React, { useEffect, useState } from "react"; import React, { useEffect, useState } from "react";
import flatten from "lodash/flatten";
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import { Trans } from "@lingui/macro"; import { Trans } from "@lingui/macro";
import { IPodContainer, Secret } from "../../api/endpoints"; import { IPodContainer, Secret } from "../../api/endpoints";
@ -11,6 +10,7 @@ import { secretsStore } from "../+config-secrets/secrets.store";
import { configMapsStore } from "../+config-maps/config-maps.store"; import { configMapsStore } from "../+config-maps/config-maps.store";
import { Icon } from "../icon"; import { Icon } from "../icon";
import { base64, cssNames } from "../../utils"; import { base64, cssNames } from "../../utils";
import _ from "lodash";
interface Props { interface Props {
container: IPodContainer; container: IPodContainer;
@ -40,7 +40,9 @@ export const ContainerEnvironment = observer((props: Props) => {
) )
const renderEnv = () => { const renderEnv = () => {
return env.map(variable => { const orderedEnv = _.sortBy(env, 'name');
return orderedEnv.map(variable => {
const { name, value, valueFrom } = variable const { name, value, valueFrom } = variable
let secretValue = null let secretValue = null
@ -89,7 +91,7 @@ export const ContainerEnvironment = observer((props: Props) => {
</div> </div>
)) ))
}) })
return flatten(envVars) return _.flatten(envVars)
} }
return ( return (

View File

@ -2,7 +2,7 @@ import "./pod-details-container.scss"
import React from "react"; import React from "react";
import { t, Trans } from "@lingui/macro"; import { t, Trans } from "@lingui/macro";
import { IPodContainer, Pod } from "../../api/endpoints"; import { IPodContainer, IPodContainerStatus, Pod } from "../../api/endpoints";
import { DrawerItem } from "../drawer"; import { DrawerItem } from "../drawer";
import { cssNames } from "../../utils"; import { cssNames } from "../../utils";
import { StatusBrick } from "../status-brick"; import { StatusBrick } from "../status-brick";
@ -21,12 +21,37 @@ interface Props {
} }
export class PodDetailsContainer extends React.Component<Props> { export class PodDetailsContainer extends React.Component<Props> {
renderStatus(state: string, status: IPodContainerStatus) {
const ready = status ? status.ready : ""
return (
<span className={cssNames("status", state)}>
{state}{ready ? `, ${_i18n._(t`ready`)}` : ""}
{state === 'terminated' ? ` - ${status.state.terminated.reason} (${_i18n._(t`exit code`)}: ${status.state.terminated.exitCode})` : ''}
</span>
);
}
renderLastState(lastState: string, status: IPodContainerStatus) {
if (lastState === 'terminated') {
return (
<span>
{lastState}<br/>
{_i18n._(t`Reason`)}: {status.lastState.terminated.reason} - {_i18n._(t`exit code`)}: {status.lastState.terminated.exitCode}<br/>
{_i18n._(t`Started at`)}: {status.lastState.terminated.startedAt}<br/>
{_i18n._(t`Finished at`)}: {status.lastState.terminated.finishedAt}<br/>
</span>
)
}
}
render() { render() {
const { pod, container, metrics } = this.props const { pod, container, metrics } = this.props
if (!pod || !container) return null if (!pod || !container) return null
const { name, image, imagePullPolicy, ports, volumeMounts, command, args } = container const { name, image, imagePullPolicy, ports, volumeMounts, command, args } = container
const status = pod.getContainerStatuses().find(status => status.name === container.name) const status = pod.getContainerStatuses().find(status => status.name === container.name)
const state = status ? Object.keys(status.state)[0] : "" const state = status ? Object.keys(status.state)[0] : ""
const lastState = status ? Object.keys(status.lastState)[0] : ""
const ready = status ? status.ready : "" const ready = status ? status.ready : ""
const liveness = pod.getLivenessProbe(container) const liveness = pod.getLivenessProbe(container)
const readiness = pod.getReadinessProbe(container) const readiness = pod.getReadinessProbe(container)
@ -48,10 +73,12 @@ export class PodDetailsContainer extends React.Component<Props> {
} }
{status && {status &&
<DrawerItem name={<Trans>Status</Trans>}> <DrawerItem name={<Trans>Status</Trans>}>
<span className={cssNames("status", state)}> {this.renderStatus(state, status)}
{state}{ready ? `, ${_i18n._(t`ready`)}` : ""} </DrawerItem>
{state === 'terminated' ? ` - ${status.state.terminated.reason} (${_i18n._(t`exit code`)}: ${status.state.terminated.exitCode})` : ''} }
</span> {lastState &&
<DrawerItem name={<Trans>Last Status</Trans>}>
{this.renderLastState(lastState, status)}
</DrawerItem> </DrawerItem>
} }
<DrawerItem name={<Trans>Image</Trans>}> <DrawerItem name={<Trans>Image</Trans>}>

View File

@ -1,7 +1,7 @@
import { RouteProps } from "react-router" import type { RouteProps } from "react-router";
import { Workloads } from "./workloads"; import { buildURL, IURLParams } from "../../../common/utils/buildUrl";
import { buildURL, IURLParams } from "../../navigation";
import { KubeResource } from "../../../common/rbac"; import { KubeResource } from "../../../common/rbac";
import { Workloads } from "./workloads";
export const workloadsRoute: RouteProps = { export const workloadsRoute: RouteProps = {
get path() { get path() {

View File

@ -1,5 +1,5 @@
import { RouteProps } from "react-router"; import type { RouteProps } from "react-router";
import { buildURL } from "../../navigation"; import { buildURL } from "../../../common/utils/buildUrl";
export const workspacesRoute: RouteProps = { export const workspacesRoute: RouteProps = {
path: "/workspaces" path: "/workspaces"

View File

@ -1,4 +1,5 @@
import "./cluster-manager.scss" import "./cluster-manager.scss"
import React from "react"; import React from "react";
import { Redirect, Route, Switch } from "react-router"; import { Redirect, Route, Switch } from "react-router";
import { comparer, reaction } from "mobx"; import { comparer, reaction } from "mobx";
@ -11,14 +12,17 @@ import { Workspaces, workspacesRoute } from "../+workspaces";
import { AddCluster, addClusterRoute } from "../+add-cluster"; import { AddCluster, addClusterRoute } from "../+add-cluster";
import { ClusterView } from "./cluster-view"; import { ClusterView } from "./cluster-view";
import { ClusterSettings, clusterSettingsRoute } from "../+cluster-settings"; 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 { clusterStore } from "../../../common/cluster-store";
import { hasLoadedView, initView, lensViews, refreshViews } from "./lens-views"; import { hasLoadedView, initView, lensViews, refreshViews } from "./lens-views";
import { globalPageRegistry } from "../../../extensions/registries/page-registry"; import { globalPageRegistry } from "../../../extensions/registries/page-registry";
import { getMatchedClusterId } from "../../navigation";
@observer @observer
export class ClusterManager extends React.Component { export class ClusterManager extends React.Component {
componentDidMount() { componentDidMount() {
const getMatchedCluster = () => clusterStore.getById(getMatchedClusterId());
disposeOnUnmount(this, [ disposeOnUnmount(this, [
reaction(getMatchedClusterId, initView, { reaction(getMatchedClusterId, initView, {
fireImmediately: true fireImmediately: true
@ -55,7 +59,7 @@ export class ClusterManager extends React.Component {
return ( return (
<div className="ClusterManager"> <div className="ClusterManager">
<main> <main>
<div id="lens-views" /> <div id="lens-views"/>
<Switch> <Switch>
<Route component={LandingPage} {...landingRoute} /> <Route component={LandingPage} {...landingRoute} />
<Route component={Preferences} {...preferencesRoute} /> <Route component={Preferences} {...preferencesRoute} />
@ -66,11 +70,11 @@ export class ClusterManager extends React.Component {
{globalPageRegistry.getItems().map(({ path, url = String(path), components: { Page } }) => { {globalPageRegistry.getItems().map(({ path, url = String(path), components: { Page } }) => {
return <Route key={url} path={path} component={Page}/> return <Route key={url} path={path} component={Page}/>
})} })}
<Redirect exact to={this.startUrl} /> <Redirect exact to={this.startUrl}/>
</Switch> </Switch>
</main> </main>
<ClustersMenu /> <ClustersMenu/>
<BottomBar /> <BottomBar/>
</div> </div>
) )
} }

View File

@ -1,8 +1,5 @@
import { reaction } from "mobx"; import type { RouteProps } from "react-router";
import { ipcRenderer } from "electron"; import { buildURL } from "../../../common/utils/buildUrl";
import { matchPath, RouteProps } from "react-router";
import { buildURL, navigation } from "../../navigation";
import { clusterStore } from "../../../common/cluster-store";
export interface IClusterViewRouteParams { export interface IClusterViewRouteParams {
clusterId: string; clusterId: string;
@ -14,33 +11,3 @@ export const clusterViewRoute: RouteProps = {
} }
export const clusterViewURL = buildURL<IClusterViewRouteParams>(clusterViewRoute.path) export const clusterViewURL = buildURL<IClusterViewRouteParams>(clusterViewRoute.path)
export function getMatchedClusterId(): string {
const matched = matchPath<IClusterViewRouteParams>(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();
});
}

Some files were not shown because too many files have changed in this diff Show More