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

Merge branch 'master' into extensions-list-page

Signed-off-by: Alex Andreev <alex.andreev.email@gmail.com>
This commit is contained in:
Alex Andreev 2020-10-28 10:21:03 +03:00
commit e84984eb99
114 changed files with 1193 additions and 635 deletions

View File

@ -149,6 +149,11 @@ jobs:
condition: "and(succeeded(), startsWith(variables['Build.SourceBranch'], 'refs/tags/'))"
env:
GH_TOKEN: $(GH_TOKEN)
- script: make publish-npm
displayName: Publish npm package
condition: "and(succeeded(), startsWith(variables['Build.SourceBranch'], 'refs/tags/'))"
env:
NPM_TOKEN: $(NPM_TOKEN)
- bash: |
mkdir -p "$AZURE_CACHE_FOLDER"
tar -czf "$AZURE_CACHE_FOLDER/yarn-cache.tar.gz" "$YARN_CACHE_FOLDER"

View File

@ -1,4 +1,5 @@
module.exports = {
ignorePatterns: ["src/extensions/npm/extensions/api.d.ts"],
overrides: [
{
files: [

11
LICENSE
View File

@ -1,8 +1,13 @@
MIT License
Copyright (c) 2020 Mirantis, Inc.
All rights reserved.
Portions of this software are licensed as follows:
* All content residing under the "docs/" directory of this repository, if that
directory exists, is licensed under "Creative Commons: CC BY-SA 4.0 license".
* All third party components incorporated into the Lens Software are licensed
under the original license provided by the owner of the applicable component.
* Content outside of the above mentioned directories or restrictions above is
available under the "MIT" license as defined below.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal

View File

@ -56,7 +56,15 @@ else
endif
build-extensions:
$(foreach file, $(wildcard $(EXTENSIONS_DIR)/*), $(MAKE) -C $(file) build;)
$(foreach dir, $(wildcard $(EXTENSIONS_DIR)/*), $(MAKE) -C $(dir) build;)
build-npm:
yarn compile:extension-types
yarn npm:fix-package-version
publish-npm: build-npm
npm config set '//registry.npmjs.org/:_authToken' "${NPM_TOKEN}"
cd src/extensions/npm/extensions && npm publish --access=public
clean:
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:
1. `yarn dev:main` compiles electron's main process part and start watching files
1. `yarn dev:renderer` compiles electron's renderer part and start watching files
1. `yarn dev-run` runs app in dev-mode and restarts when electron's main process file has changed
1. `yarn dev:main` compiles electron's main process app part
1. `yarn dev:renderer` compiles electron's renderer app part
1. `yarn dev:extension-types` compile declaration types for `@k8slens/extensions`
1. `yarn dev-run` runs app in dev-mode and auto-restart when main process file has changed
## Developer's ~~RTFM~~ recommended list:

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,
"requires": true,
"dependencies": {
"@k8slens/extensions": {
"version": "file:../../src/extensions/npm/extensions",
"dev": true
},
"@webassemblyjs/ast": {
"version": "1.9.0",
"resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.9.0.tgz",

View File

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

View File

@ -1,26 +1,16 @@
import { LensRendererExtension, Registry } from "@k8slens/extensions";
import { ExamplePage, ExampleIcon } from "./page"
import { LensRendererExtension } from "@k8slens/extensions";
import { ExampleIcon, ExamplePage } from "./page"
import React from "react"
export default class ExampleExtension extends LensRendererExtension {
onActivate() {
console.log('EXAMPLE EXTENSION RENDERER: ACTIVATED', this.getMeta());
}
registerClusterPage(registry: Registry.ClusterPageRegistry) {
this.disposers.push(
registry.add({
path: "/extension-example",
title: "Example Extension",
components: {
Page: () => <ExamplePage extension={this} />,
MenuIcon: ExampleIcon,
}
})
)
}
onDeactivate() {
console.log('EXAMPLE EXTENSION RENDERER: DEACTIVATED', this.getMeta());
}
clusterPages = [
{
path: "/extension-example",
title: "Example Extension",
components: {
Page: () => <ExamplePage extension={this}/>,
MenuIcon: ExampleIcon,
}
}
]
}

View File

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

View File

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

View File

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

View File

@ -1,25 +1,23 @@
import { Registry, LensRendererExtension } from "@k8slens/extensions"
import { LensRendererExtension } from "@k8slens/extensions"
import { MetricsFeature } from "./src/metrics-feature"
import React from "react"
export default class ClusterMetricsFeatureExtension extends LensRendererExtension {
registerClusterFeatures(registry: Registry.ClusterFeatureRegistry) {
this.disposers.push(
registry.add({
title: "Metrics Stack",
components: {
Description: () => {
return (
<span>
clusterFeatures = [
{
title: "Metrics Stack",
components: {
Description: () => {
return (
<span>
Enable timeseries data visualization (Prometheus stack) for your cluster.
Install this only if you don't have existing Prometheus stack installed.
You can see preview of manifests <a href="https://github.com/lensapp/lens/tree/master/extensions/lens-metrics/resources" target="_blank">here</a>.
</span>
)
}
},
feature: new MetricsFeature()
})
)
}
)
}
},
feature: new MetricsFeature()
}
]
}

View File

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

View File

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

View File

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

View File

@ -1,21 +1,15 @@
import { Registry, LensRendererExtension } from "@k8slens/extensions";
import { LensRendererExtension } from "@k8slens/extensions";
import React from "react"
import { NodeMenu } from "./src/node-menu"
import { NodeMenu, NodeMenuProps } from "./src/node-menu"
export default class NodeMenuRendererExtension extends LensRendererExtension {
async onActivate() {
console.log("node-menu extension activated")
}
registerKubeObjectMenus(registry: Registry.KubeObjectMenuRegistry) {
this.disposers.push(
registry.add({
kind: "Node",
apiVersions: ["v1"],
components: {
MenuItem: (props) => <NodeMenu {...props} />
}
})
)
}
kubeObjectMenuItems = [
{
kind: "Node",
apiVersions: ["v1"],
components: {
MenuItem: (props: NodeMenuProps) => <NodeMenu {...props} />
}
}
]
}

View File

@ -1,7 +1,10 @@
import React from "react";
import { Component, K8sApi, Navigation} from "@k8slens/extensions"
export function NodeMenu(props: Component.KubeObjectMenuProps<K8sApi.Node>) {
export interface NodeMenuProps extends Component.KubeObjectMenuProps<K8sApi.Node> {
}
export function NodeMenu(props: NodeMenuProps) {
const { object: node, toolbar } = props;
if (!node) return null;
const nodeName = node.getName();

View File

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

View File

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

View File

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

View File

@ -1,31 +1,23 @@
import { Registry, LensRendererExtension } from "@k8slens/extensions";
import { PodShellMenu } from "./src/shell-menu"
import { PodLogsMenu } from "./src/logs-menu"
import { LensRendererExtension } from "@k8slens/extensions";
import { PodShellMenu, PodShellMenuProps } from "./src/shell-menu"
import { PodLogsMenu, PodLogsMenuProps } from "./src/logs-menu"
import React from "react"
export default class PodMenuRendererExtension extends LensRendererExtension {
async onActivate() {
console.log("pod-menu extension activated")
}
registerKubeObjectMenus(registry: Registry.KubeObjectMenuRegistry) {
this.disposers.push(
registry.add({
kind: "Pod",
apiVersions: ["v1"],
components: {
MenuItem: (props) => <PodShellMenu {...props} />
}
})
)
this.disposers.push(
registry.add({
kind: "Pod",
apiVersions: ["v1"],
components: {
MenuItem: (props) => <PodLogsMenu {...props} />
}
})
)
}
kubeObjectMenuItems = [
{
kind: "Pod",
apiVersions: ["v1"],
components: {
MenuItem: (props: PodShellMenuProps) => <PodShellMenu {...props} />
}
},
{
kind: "Pod",
apiVersions: ["v1"],
components: {
MenuItem: (props: PodLogsMenuProps) => <PodLogsMenu {...props} />
}
}
]
}

View File

@ -1,10 +1,10 @@
import React from "react";
import { Component, K8sApi, Util, Navigation } from "@k8slens/extensions";
interface Props extends Component.KubeObjectMenuProps<K8sApi.Pod> {
export interface PodLogsMenuProps extends Component.KubeObjectMenuProps<K8sApi.Pod> {
}
export class PodLogsMenu extends React.Component<Props> {
export class PodLogsMenu extends React.Component<PodLogsMenuProps> {
showLogs(container: K8sApi.IPodContainer) {
Navigation.hideDetails();
const pod = this.props.object;

View File

@ -3,10 +3,10 @@
import React from "react";
import { Component, K8sApi, Util, Navigation } from "@k8slens/extensions";
interface Props extends Component.KubeObjectMenuProps<K8sApi.Pod> {
export interface PodShellMenuProps extends Component.KubeObjectMenuProps<K8sApi.Pod> {
}
export class PodShellMenu extends React.Component<Props> {
export class PodShellMenu extends React.Component<PodShellMenuProps> {
async execShell(container?: string) {
Navigation.hideDetails();
const { object: pod } = this.props

View File

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

View File

@ -1,23 +1,14 @@
import { LensMainExtension, Registry, windowManager } from "@k8slens/extensions";
import { LensMainExtension, windowManager } from "@k8slens/extensions";
import { supportPageURL } from "./src/support.route";
export default class SupportPageMainExtension extends LensMainExtension {
async onActivate() {
console.log("support page extension activated")
}
async registerAppMenus(registry: Registry.MenuRegistry) {
this.disposers.push(
registry.add({
parentId: "help",
label: "Support",
click() {
windowManager.navigate({
channel: "menu:navigate", // fixme: use windowManager.ensureMainWindow from Tray's PR
url: supportPageURL(),
});
}
})
)
}
appMenus = [
{
parentId: "help",
label: "Support",
click() {
windowManager.navigate(supportPageURL());
}
}
]
}

View File

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

View File

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

View File

@ -1,39 +1,31 @@
import React from "react";
import { Component, LensRendererExtension, Navigation, Registry } from "@k8slens/extensions";
import { Component, LensRendererExtension, Navigation } from "@k8slens/extensions";
import { supportPageRoute, supportPageURL } from "./src/support.route";
import { Support } from "./src/support";
export default class SupportPageRendererExtension extends LensRendererExtension {
async onActivate() {
console.log("support page extension activated")
}
globalPages = [
{
...supportPageRoute,
url: supportPageURL(),
hideInMenu: true,
components: {
Page: Support,
}
}
]
registerGlobalPage(registry: Registry.GlobalPageRegistry) {
this.disposers.push(
registry.add({
...supportPageRoute,
url: supportPageURL(),
hideInMenu: true,
components: {
Page: Support,
}
})
)
}
registerStatusBarItem(registry: Registry.StatusBarRegistry) {
this.disposers.push(
registry.add({
item: (
<div
className="flex align-center gaps hover-highlight"
onClick={() => Navigation.navigate(supportPageURL())}
>
<Component.Icon material="help_outline" small />
<span>Support</span>
</div>
)
})
)
}
statusBarItems = [
{
item: (
<div
className="flex align-center gaps hover-highlight"
onClick={() => Navigation.navigate(supportPageURL())}
>
<Component.Icon material="help_outline" small/>
<span>Support</span>
</div>
)
}
]
}

View File

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

View File

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

View File

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

View File

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

View File

@ -1,29 +1,23 @@
import { LensRendererExtension, Registry } from "@k8slens/extensions";
import { LensRendererExtension } from "@k8slens/extensions";
import { telemetryPreferencesStore } from "./src/telemetry-preferences-store"
import { TelemetryPreferenceHint, TelemetryPreferenceInput } from "./src/telemetry-preference"
import { tracker } from "./src/tracker"
import React from "react"
export default class TelemetryRendererExtension extends LensRendererExtension {
appPreferences = [
{
title: "Telemetry & Usage Tracking",
components: {
Hint: () => <TelemetryPreferenceHint/>,
Input: () => <TelemetryPreferenceInput telemetry={telemetryPreferencesStore}/>
}
}
];
async onActivate() {
console.log("telemetry extension activated")
tracker.start()
await telemetryPreferencesStore.loadExtension(this)
}
registerAppPreferences(registry: Registry.AppPreferenceRegistry) {
this.disposers.push(
registry.add({
title: "Telemetry & Usage Tracking",
components: {
Hint: () => <TelemetryPreferenceHint />,
Input: () => <TelemetryPreferenceInput telemetry={telemetryPreferencesStore} />
}
})
)
}
onDeactivate() {
console.log("telemetry extension deactivated")
}
}

View File

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

View File

@ -389,13 +389,13 @@ describe("Lens integration tests", () => {
it(`shows ${drawer} drawer`, async () => {
expect(clusterAdded).toBe(true)
await app.client.click(`.sidebar-nav #${drawerId} span.link-text`)
await app.client.waitUntilTextExists(`a[href="/${pages[0].href}"]`, pages[0].name)
await app.client.waitUntilTextExists(`a[href^="/${pages[0].href}"]`, pages[0].name)
})
}
pages.forEach(({ name, href, expectedSelector, expectedText }) => {
it(`shows ${drawer}->${name} page`, async () => {
expect(clusterAdded).toBe(true)
await app.client.click(`a[href="/${href}"]`)
await app.client.click(`a[href^="/${href}"]`)
await app.client.waitUntilTextExists(expectedSelector, expectedText)
})
})
@ -404,7 +404,7 @@ describe("Lens integration tests", () => {
it(`hides ${drawer} drawer`, async () => {
expect(clusterAdded).toBe(true)
await app.client.click(`.sidebar-nav #${drawerId} span.link-text`)
await expect(app.client.waitUntilTextExists(`a[href="/${pages[0].href}"]`, pages[0].name, 100)).rejects.toThrow()
await expect(app.client.waitUntilTextExists(`a[href^="/${pages[0].href}"]`, pages[0].name, 100)).rejects.toThrow()
})
}
})
@ -440,8 +440,8 @@ describe("Lens integration tests", () => {
it(`creates a pod in ${TEST_NAMESPACE} namespace`, async () => {
expect(clusterAdded).toBe(true)
await app.client.click(".sidebar-nav #workloads span.link-text")
await app.client.waitUntilTextExists('a[href="/pods"]', "Pods")
await app.client.click('a[href="/pods"]')
await app.client.waitUntilTextExists('a[href^="/pods"]', "Pods")
await app.client.click('a[href^="/pods"]')
await app.client.waitUntilTextExists("div.TableCell", "kube-apiserver")
await app.client.click('.Icon.new-dock-tab')
await app.client.waitUntilTextExists("li.MenuItem.create-resource-tab", "Create resource")

View File

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

View File

@ -88,7 +88,7 @@ msgid "Active"
msgstr "Active"
#: src/renderer/components/+add-cluster/add-cluster.tsx:310
#: src/renderer/components/cluster-manager/clusters-menu.tsx:130
#: src/renderer/components/cluster-manager/clusters-menu.tsx:127
msgid "Add Cluster"
msgstr "Add Cluster"
@ -219,11 +219,11 @@ msgstr "Allocatable"
msgid "Allow Privilege Escalation"
msgstr "Allow Privilege Escalation"
#: src/renderer/components/+preferences/preferences.tsx:169
#: src/renderer/components/+preferences/preferences.tsx:172
msgid "Allow telemetry & usage tracking"
msgstr "Allow telemetry & usage tracking"
#: src/renderer/components/+preferences/preferences.tsx:161
#: src/renderer/components/+preferences/preferences.tsx:164
msgid "Allow untrusted Certificate Authorities"
msgstr "Allow untrusted Certificate Authorities"
@ -301,6 +301,14 @@ msgstr "Associate clusters and choose the ones you want to access via quick laun
msgid "Auth App Role"
msgstr "Auth App Role"
#: src/renderer/components/+preferences/preferences.tsx:160
msgid "Auto start-up"
msgstr "Auto start-up"
#: src/renderer/components/+preferences/preferences.tsx:161
msgid "Automatically start Lens on login"
msgstr "Automatically start Lens on login"
#: src/renderer/components/error-boundary/error-boundary.tsx:53
#: src/renderer/components/wizard/wizard.tsx:130
msgid "Back"
@ -422,7 +430,7 @@ msgstr "Cancel"
msgid "Capacity"
msgstr "Capacity"
#: src/renderer/components/+preferences/preferences.tsx:160
#: src/renderer/components/+preferences/preferences.tsx:163
msgid "Certificate Trust"
msgstr "Certificate Trust"
@ -817,7 +825,7 @@ msgstr "Desired Healthy"
msgid "Desired number of replicas"
msgstr "Desired number of replicas"
#: src/renderer/components/cluster-manager/clusters-menu.tsx:65
#: src/renderer/components/cluster-manager/clusters-menu.tsx:62
msgid "Disconnect"
msgstr "Disconnect"
@ -831,7 +839,7 @@ msgstr "Disk"
msgid "Disk:"
msgstr "Disk:"
#: src/renderer/components/+preferences/preferences.tsx:165
#: src/renderer/components/+preferences/preferences.tsx:168
msgid "Does not affect cluster communications!"
msgstr "Does not affect cluster communications!"
@ -927,8 +935,8 @@ msgstr "Environment"
msgid "Error stack"
msgstr "Error stack"
#: src/renderer/components/+add-cluster/add-cluster.tsx:89
#: src/renderer/components/+add-cluster/add-cluster.tsx:130
#: src/renderer/components/+add-cluster/add-cluster.tsx:88
#: src/renderer/components/+add-cluster/add-cluster.tsx:129
msgid "Error while adding cluster(s): {0}"
msgstr "Error while adding cluster(s): {0}"
@ -1581,7 +1589,7 @@ msgstr "Namespaces"
msgid "Namespaces: {0}"
msgstr "Namespaces: {0}"
#: src/renderer/components/+preferences/preferences.tsx:164
#: src/renderer/components/+preferences/preferences.tsx:167
msgid "Needed with some corporate proxies that do certificate re-writing."
msgstr "Needed with some corporate proxies that do certificate re-writing."
@ -1798,7 +1806,7 @@ msgstr "Persistent Volume Claims"
msgid "Persistent Volumes"
msgstr "Persistent Volumes"
#: src/renderer/components/+add-cluster/add-cluster.tsx:75
#: src/renderer/components/+add-cluster/add-cluster.tsx:74
msgid "Please select at least one cluster context"
msgstr "Please select at least one cluster context"
@ -2025,8 +2033,8 @@ msgstr "Releases"
#: src/renderer/components/+preferences/preferences.tsx:152
#: src/renderer/components/+user-management-roles-bindings/role-binding-details.tsx:60
#: src/renderer/components/cluster-manager/clusters-menu.tsx:76
#: src/renderer/components/cluster-manager/clusters-menu.tsx:82
#: src/renderer/components/cluster-manager/clusters-menu.tsx:73
#: src/renderer/components/cluster-manager/clusters-menu.tsx:79
#: src/renderer/components/item-object-list/item-list-layout.tsx:179
#: src/renderer/components/menu/menu-actions.tsx:49
#: src/renderer/components/menu/menu-actions.tsx:85
@ -2470,7 +2478,7 @@ msgstr "Set"
msgid "Set quota"
msgstr "Set quota"
#: src/renderer/components/cluster-manager/clusters-menu.tsx:53
#: src/renderer/components/cluster-manager/clusters-menu.tsx:51
msgid "Settings"
msgstr "Settings"
@ -2613,7 +2621,7 @@ msgstr "Submitting.."
msgid "Subsets"
msgstr "Subsets"
#: src/renderer/components/+add-cluster/add-cluster.tsx:122
#: src/renderer/components/+add-cluster/add-cluster.tsx:121
msgid "Successfully imported <0>{0}</0> cluster(s)"
msgstr "Successfully imported <0>{0}</0> cluster(s)"
@ -2635,11 +2643,11 @@ msgstr "TLS"
msgid "Taints"
msgstr "Taints"
#: src/renderer/components/+preferences/preferences.tsx:168
#: src/renderer/components/+preferences/preferences.tsx:171
msgid "Telemetry & Usage Tracking"
msgstr "Telemetry & Usage Tracking"
#: src/renderer/components/+preferences/preferences.tsx:171
#: src/renderer/components/+preferences/preferences.tsx:174
msgid "Telemetry & usage data is collected to continuously improve the Lens experience."
msgstr "Telemetry & usage data is collected to continuously improve the Lens experience."
@ -2675,7 +2683,7 @@ msgstr "This field must be a valid path"
msgid "This is the quick launch menu."
msgstr "This is the quick launch menu."
#: src/renderer/components/+preferences/preferences.tsx:163
#: src/renderer/components/+preferences/preferences.tsx:166
msgid "This will make Lens to trust ANY certificate authority without any validations."
msgstr "This will make Lens to trust ANY certificate authority without any validations."
@ -2953,7 +2961,7 @@ msgstr "listKind"
msgid "never"
msgstr "never"
#: src/renderer/components/cluster-manager/clusters-menu.tsx:133
#: src/renderer/components/cluster-manager/clusters-menu.tsx:130
msgid "new"
msgstr "new"

View File

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

View File

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

View File

@ -2,7 +2,7 @@
"name": "kontena-lens",
"productName": "Lens",
"description": "Lens - The Kubernetes IDE",
"version": "3.6.6",
"version": "4.0.0-alpha.1",
"main": "static/build/main.js",
"copyright": "© 2020, Mirantis, Inc.",
"license": "MIT",
@ -16,12 +16,13 @@
"dev-run": "nodemon --watch static/build/main.js --exec \"electron --inspect .\"",
"dev:main": "yarn compile:main --watch",
"dev:renderer": "yarn compile:renderer --watch",
"dev:extension-rollup": "yarn compile:extension-rollup --watch",
"dev:extension-types": "yarn compile:extension-types --watch",
"compile": "env NODE_ENV=production concurrently yarn:compile:*",
"compile:main": "webpack --config webpack.main.ts",
"compile:renderer": "webpack --config webpack.renderer.ts",
"compile:i18n": "lingui compile",
"compile:extension-rollup": "rollup --config src/extensions/rollup.config.js",
"compile:extension-types": "rollup --config src/extensions/rollup.config.js",
"npm:fix-package-version": "ts-node build/set_npm_version.ts",
"build:linux": "yarn compile && electron-builder --linux --dir -c.productName=Lens",
"build:mac": "yarn compile && electron-builder --mac --dir -c.productName=Lens",
"build:win": "yarn compile && electron-builder --win --dir -c.productName=Lens",
@ -35,6 +36,7 @@
"download-bins": "concurrently yarn:download:*",
"download:kubectl": "yarn run ts-node build/download_kubectl.ts",
"download:helm": "yarn run ts-node build/download_helm.ts",
"build:tray-icons": "yarn run ts-node build/build_tray_icon.ts",
"lint": "eslint $@ --ext js,ts,tsx --max-warnings=0 src/"
},
"config": {
@ -96,6 +98,11 @@
"to": "static/",
"filter": "!**/main.js"
},
{
"from": "build/tray",
"to": "static/icons",
"filter": "*.png"
},
{
"from": "extensions/",
"to": "./extensions/",
@ -185,6 +192,20 @@
"@hapi/call": "^8.0.0",
"@hapi/subtext": "^7.0.3",
"@kubernetes/client-node": "^0.12.0",
"@types/crypto-js": "^3.1.47",
"@types/electron-window-state": "^2.0.34",
"@types/fs-extra": "^9.0.1",
"@types/http-proxy": "^1.17.4",
"@types/js-yaml": "^3.12.4",
"@types/jsdom": "^16.2.4",
"@types/jsonpath": "^0.2.0",
"@types/lodash": "^4.14.155",
"@types/marked": "^0.7.4",
"@types/mock-fs": "^4.10.0",
"@types/node": "^12.12.45",
"@types/proper-lockfile": "^4.1.1",
"@types/react-beautiful-dnd": "^13.0.0",
"@types/tar": "^4.0.3",
"array-move": "^3.0.0",
"chalk": "^4.1.0",
"command-exists": "1.2.9",
@ -197,8 +218,8 @@
"fs-extra": "^9.0.1",
"handlebars": "^4.7.6",
"http-proxy": "^1.18.1",
"immer": "^7.0.5",
"js-yaml": "^3.14.0",
"jsdom": "^16.4.0",
"jsonpath": "^1.0.2",
"lodash": "^4.17.15",
"mac-ca": "^1.0.4",
@ -275,6 +296,7 @@
"@types/request": "^2.48.5",
"@types/request-promise-native": "^1.0.17",
"@types/semver": "^7.2.0",
"@types/sharp": "^0.26.0",
"@types/shelljs": "^0.8.8",
"@types/spdy": "^3.4.4",
"@types/tar": "^4.0.3",
@ -326,7 +348,7 @@
"postinstall-postinstall": "^2.1.0",
"progress-bar-webpack-plugin": "^2.1.0",
"raw-loader": "^4.0.1",
"react": "^16.13.1",
"react": "^16.14.0",
"react-beautiful-dnd": "^13.0.0",
"react-dom": "^16.13.1",
"react-router": "^5.2.0",
@ -338,6 +360,7 @@
"rollup-plugin-ignore-import": "^1.3.2",
"rollup-pluginutils": "^2.8.2",
"sass-loader": "^8.0.2",
"sharp": "^0.26.1",
"spectron": "11.0.0",
"style-loader": "^1.2.1",
"terser-webpack-plugin": "^3.0.3",

View File

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

View File

@ -27,6 +27,7 @@ export interface UserPreferences {
downloadKubectlBinaries?: boolean;
downloadBinariesPath?: string;
kubectlBinariesPath?: string;
openAtLogin?: boolean;
}
export class UserStore extends BaseStore<UserStoreModel> {
@ -38,14 +39,7 @@ export class UserStore extends BaseStore<UserStoreModel> {
migrations: migrations,
});
// track telemetry availability
reaction(() => this.preferences.allowTelemetry, allowed => {
appEventBus.emit({name: "telemetry", action: allowed ? "enabled" : "disabled"})
});
// refresh new contexts
this.whenLoaded.then(this.refreshNewContexts);
reaction(() => this.kubeConfigPath, this.refreshNewContexts);
this.handleOnLoad();
}
@observable lastSeenAppVersion = "0.0.0"
@ -59,8 +53,31 @@ export class UserStore extends BaseStore<UserStoreModel> {
colorTheme: UserStore.defaultTheme,
downloadMirror: "default",
downloadKubectlBinaries: true, // Download kubectl binaries matching cluster version
openAtLogin: true,
};
protected async handleOnLoad() {
await this.whenLoaded;
// refresh new contexts
this.refreshNewContexts();
reaction(() => this.kubeConfigPath, this.refreshNewContexts);
if (app) {
// track telemetry availability
reaction(() => this.preferences.allowTelemetry, allowed => {
appEventBus.emit({name: "telemetry", action: allowed ? "enabled" : "disabled"})
});
// open at system start-up
reaction(() => this.preferences.openAtLogin, open => {
app.setLoginItemSettings({ openAtLogin: open });
}, {
fireImmediately: true,
});
}
}
get isNewVersion() {
return semver.gt(getAppVersion(), this.lastSeenAppVersion);
}

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

@ -1,8 +1,6 @@
import { action, computed, observable, toJS } from "mobx";
import { BaseStore } from "./base-store";
import { clusterStore } from "./cluster-store"
import { landingURL } from "../renderer/components/+landing-page/landing-page.route";
import { navigate } from "../renderer/navigation";
import { appEventBus } from "./event-bus";
export type WorkspaceId = string;
@ -57,18 +55,13 @@ export class WorkspaceStore extends BaseStore<WorkspaceStoreModel> {
}
@action
setActive(id = WorkspaceStore.defaultId, { redirectToLanding = true, resetActiveCluster = true } = {}) {
setActive(id = WorkspaceStore.defaultId, reset = true) {
if (id === this.currentWorkspaceId) return;
if (!this.getById(id)) {
throw new Error(`workspace ${id} doesn't exist`);
}
this.currentWorkspaceId = id;
if (resetActiveCluster) {
clusterStore.setActive(null)
}
if (redirectToLanding) {
navigate(landingURL())
}
clusterStore.activeClusterId = null; // fixme: handle previously selected cluster from current workspace
}
@action

View File

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

View File

@ -10,9 +10,9 @@ import * as EventBus from "./event-bus"
import * as Store from "./stores"
import * as Util from "./utils"
import * as Registry from "../registries"
import * as CommonVars from "../../common/vars";
import * as ClusterFeature from "./cluster-feature"
// TODO: allow to expose windowManager.navigate() as Navigation.navigate() in runtime
export let windowManager: WindowManager;
export {
@ -22,5 +22,4 @@ export {
Store,
Util,
Registry,
CommonVars,
}

View File

@ -36,26 +36,26 @@ export class ExtensionLoader {
loadOnMain() {
logger.info('[EXTENSIONS-LOADER]: load on main')
this.autoloadExtensions((instance: LensMainExtension) => {
instance.registerAppMenus(menuRegistry);
this.autoloadExtensions((extension: LensMainExtension) => {
extension.registerTo(menuRegistry, extension.appMenus)
})
}
loadOnClusterManagerRenderer() {
logger.info('[EXTENSIONS-LOADER]: load on main renderer (cluster manager)')
this.autoloadExtensions((instance: LensRendererExtension) => {
instance.registerGlobalPage(globalPageRegistry)
instance.registerAppPreferences(appPreferenceRegistry)
instance.registerClusterFeatures(clusterFeatureRegistry)
instance.registerStatusBarItem(statusBarRegistry)
this.autoloadExtensions((extension: LensRendererExtension) => {
extension.registerTo(globalPageRegistry, extension.globalPages)
extension.registerTo(appPreferenceRegistry, extension.appPreferences)
extension.registerTo(clusterFeatureRegistry, extension.clusterFeatures)
extension.registerTo(statusBarRegistry, extension.statusBarItems)
})
}
loadOnClusterRenderer() {
logger.info('[EXTENSIONS-LOADER]: load on cluster renderer (dashboard)')
this.autoloadExtensions((instance: LensRendererExtension) => {
instance.registerClusterPage(clusterPageRegistry)
instance.registerKubeObjectMenus(kubeObjectMenuRegistry)
this.autoloadExtensions((extension: LensRendererExtension) => {
extension.registerTo(clusterPageRegistry, extension.clusterPages)
extension.registerTo(kubeObjectMenuRegistry, extension.kubeObjectMenuItems)
})
}

View File

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

View File

@ -1,6 +1,7 @@
import { readJsonSync } from "fs-extra";
import { action, observable, toJS } from "mobx";
import logger from "../main/logger";
import { BaseRegistry } from "./registries/base-registry";
export type ExtensionId = string | ExtensionPackageJsonPath;
export type ExtensionPackageJsonPath = string;
@ -25,7 +26,7 @@ export interface ExtensionManifest extends ExtensionModel {
export class LensExtension implements ExtensionModel {
public id: ExtensionId;
public updateUrl: string;
protected disposers: Function[] = [];
protected disposers: (() => void)[] = [];
@observable name = "";
@observable description = "";
@ -77,6 +78,14 @@ export class LensExtension implements ExtensionModel {
// mock
}
registerTo<T = any>(registry: BaseRegistry<T>, items: T[] = []) {
const disposers = items.map(item => registry.add(item));
this.disposers.push(...disposers);
return () => {
this.disposers = this.disposers.filter(disposer => !disposers.includes(disposer))
};
}
getMeta() {
return toJS({
id: this.id,

View File

@ -1,12 +1,7 @@
import type { MenuRegistration } from "./registries/menu-registry";
import { observable } from "mobx";
import { LensExtension } from "./lens-extension"
import type { MenuRegistry } from "./registries/menu-registry";
export class LensMainExtension extends LensExtension {
registerAppMenus(registry: MenuRegistry) {
//
}
registerPrometheusProviders(registry: any) {
//
}
@observable.shallow appMenus: MenuRegistration[] = []
}

View File

@ -1,28 +1,12 @@
import type { AppPreferenceRegistration, ClusterFeatureRegistration, KubeObjectMenuRegistration, PageRegistration, StatusBarRegistration } from "./registries"
import { observable } from "mobx";
import { LensExtension } from "./lens-extension"
import type { GlobalPageRegistry, ClusterPageRegistry, AppPreferenceRegistry, StatusBarRegistry, KubeObjectMenuRegistry, ClusterFeatureRegistry } from "./registries"
export class LensRendererExtension extends LensExtension {
registerGlobalPage(registry: GlobalPageRegistry) {
return
}
registerClusterPage(registry: ClusterPageRegistry) {
return
}
registerAppPreferences(registry: AppPreferenceRegistry) {
return
}
registerClusterFeatures(registry: ClusterFeatureRegistry) {
return
}
registerStatusBarItem(registry: StatusBarRegistry) {
return
}
registerKubeObjectMenus(registry: KubeObjectMenuRegistry) {
return
}
@observable.shallow globalPages: PageRegistration[] = []
@observable.shallow clusterPages: PageRegistration[] = []
@observable.shallow appPreferences: AppPreferenceRegistration[] = []
@observable.shallow clusterFeatures: ClusterFeatureRegistration[] = []
@observable.shallow statusBarItems: StatusBarRegistration[] = []
@observable.shallow kubeObjectMenuItems: KubeObjectMenuRegistration[] = []
}

View File

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

View File

@ -1,11 +1,10 @@
// Extensions API -> Global menu customizations
import type { MenuTopId } from "../../main/menu";
import type { MenuItemConstructorOptions } from "electron";
import { BaseRegistry } from "./base-registry";
export interface MenuRegistration extends MenuItemConstructorOptions {
parentId?: MenuTopId;
parentId: string;
}
export class MenuRegistry extends BaseRegistry<MenuRegistration> {

View File

@ -1,19 +1,19 @@
import { autoUpdater } from "electron-updater"
import logger from "./logger"
export default class AppUpdater {
export class AppUpdater {
static readonly defaultUpdateIntervalMs = 1000 * 60 * 60 * 24 // once a day
protected updateInterval: number = (1000 * 60 * 60 * 24) // once a day
static checkForUpdates() {
return autoUpdater.checkForUpdatesAndNotify()
}
constructor() {
constructor(protected updateInterval = AppUpdater.defaultUpdateIntervalMs) {
autoUpdater.logger = logger
}
public start() {
setInterval(() => {
autoUpdater.checkForUpdatesAndNotify()
}, this.updateInterval)
return autoUpdater.checkForUpdatesAndNotify()
setInterval(AppUpdater.checkForUpdates, this.updateInterval)
return AppUpdater.checkForUpdates();
}
}

View File

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

View File

@ -13,11 +13,27 @@ import logger from "./logger";
export type MenuTopId = "mac" | "file" | "edit" | "view" | "help"
export function initMenu(windowManager: WindowManager) {
autorun(() => buildMenu(windowManager), {
return autorun(() => buildMenu(windowManager), {
delay: 100
});
}
export function showAbout(browserWindow: BrowserWindow) {
const appInfo = [
`${appName}: ${app.getVersion()}`,
`Electron: ${process.versions.electron}`,
`Chrome: ${process.versions.chrome}`,
`Copyright 2020 Mirantis, Inc.`,
]
dialog.showMessageBoxSync(browserWindow, {
title: `${isWindows ? " ".repeat(2) : ""}${appName}`,
type: "info",
buttons: ["Close"],
message: `Lens`,
detail: appInfo.join("\r\n")
})
}
export function buildMenu(windowManager: WindowManager) {
function ignoreOnMac(menuItems: MenuItemConstructorOptions[]) {
if (isMac) return [];
@ -33,28 +49,9 @@ export function buildMenu(windowManager: WindowManager) {
return menuItems;
}
function navigate(url: string) {
async function navigate(url: string) {
logger.info(`[MENU]: navigating to ${url}`);
windowManager.navigate({
channel: "menu:navigate",
url: url,
})
}
function showAbout(browserWindow: BrowserWindow) {
const appInfo = [
`${appName}: ${app.getVersion()}`,
`Electron: ${process.versions.electron}`,
`Chrome: ${process.versions.chrome}`,
`Copyright 2020 Mirantis, Inc.`,
]
dialog.showMessageBoxSync(browserWindow, {
title: `${isWindows ? " ".repeat(2) : ""}${appName}`,
type: "info",
buttons: ["Close"],
message: `Lens`,
detail: appInfo.join("\r\n")
})
await windowManager.navigate(url);
}
const macAppMenu: MenuItemConstructorOptions = {
@ -87,7 +84,13 @@ export function buildMenu(windowManager: WindowManager) {
{ role: 'hideOthers' },
{ role: 'unhide' },
{ type: 'separator' },
{ role: 'quit' }
{
label: 'Quit',
accelerator: 'Cmd+Q',
click() {
app.exit(); // force quit since might be blocked within app.on("will-quit")
}
}
]
};
@ -125,7 +128,9 @@ export function buildMenu(windowManager: WindowManager) {
},
{ type: 'separator' },
{ role: 'quit' }
])
]),
{ type: 'separator' },
{ role: 'close' } // close current window
]
};
@ -165,7 +170,7 @@ export function buildMenu(windowManager: WindowManager) {
label: 'Reload',
accelerator: 'CmdOrCtrl+R',
click() {
windowManager.reload({ channel: "menu:reload" });
windowManager.reload();
}
},
{ role: 'toggleDevTools' },
@ -216,7 +221,7 @@ export function buildMenu(windowManager: WindowManager) {
// Modify menu from extensions-api
menuRegistry.getItems().forEach(({ parentId, ...menuItem }) => {
try {
const topMenu = appMenu[parentId].submenu as MenuItemConstructorOptions[];
const topMenu = appMenu[parentId as MenuTopId].submenu as MenuItemConstructorOptions[];
topMenu.push(menuItem);
} catch (err) {
logger.error(`[MENU]: can't register menu item, parentId=${parentId}`, { menuItem })

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,7 +1,7 @@
import { RouteProps } from "react-router";
import { configMapsURL } from "../+config-maps";
import { Config } from "./config";
import { IURLParams } from "../../navigation";
import { IURLParams } from "../../../common/utils/buildUrl";
import { configMapsURL } from "../+config-maps/config-maps.route";
export const configRoute: RouteProps = {
get path() {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -21,4 +21,8 @@
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>
<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>
<Checkbox
label={<Trans>Allow untrusted Certificate Authorities</Trans>}

View File

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

View File

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

View File

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

View File

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

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

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