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

Merge branch 'master' into logs-search

This commit is contained in:
Alex Andreev 2020-10-30 15:14:22 +03:00
commit b0c0794200
352 changed files with 28437 additions and 3271 deletions

View File

@ -149,6 +149,11 @@ jobs:
condition: "and(succeeded(), startsWith(variables['Build.SourceBranch'], 'refs/tags/'))" condition: "and(succeeded(), startsWith(variables['Build.SourceBranch'], 'refs/tags/'))"
env: env:
GH_TOKEN: $(GH_TOKEN) 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: | - bash: |
mkdir -p "$AZURE_CACHE_FOLDER" mkdir -p "$AZURE_CACHE_FOLDER"
tar -czf "$AZURE_CACHE_FOLDER/yarn-cache.tar.gz" "$YARN_CACHE_FOLDER" tar -czf "$AZURE_CACHE_FOLDER/yarn-cache.tar.gz" "$YARN_CACHE_FOLDER"

View File

@ -1,9 +1,11 @@
module.exports = { module.exports = {
ignorePatterns: ["src/extensions/npm/extensions/api.d.ts"],
overrides: [ overrides: [
{ {
files: [ files: [
"src/renderer/**/*.js", "src/renderer/**/*.js",
"build/**/*.js", "build/**/*.js",
"extensions/**/*.js"
], ],
extends: [ extends: [
'eslint:recommended', 'eslint:recommended',
@ -24,7 +26,9 @@ module.exports = {
files: [ files: [
"build/*.ts", "build/*.ts",
"src/**/*.ts", "src/**/*.ts",
"integration/**/*.ts" "integration/**/*.ts",
"src/extensions/**/*.ts*",
"extensions/**/*.ts*"
], ],
parser: "@typescript-eslint/parser", parser: "@typescript-eslint/parser",
extends: [ extends: [

13
.gitignore vendored
View File

@ -1,12 +1,17 @@
dist/ dist/
out/
node_modules/ node_modules/
.DS_Store .DS_Store
yarn-error.log yarn-error.log
coverage/ coverage/
tmp/ tmp/
static/build/**
binaries/client/
binaries/server/
locales/**/**.js locales/**/**.js
lens.log lens.log
static/build
static/types
binaries/client/
binaries/server/
src/extensions/*/*.js
src/extensions/*/*.d.ts
types/extension-api.d.ts
types/extension-renderer-api.d.ts
extensions/*/dist

11
LICENSE
View File

@ -1,8 +1,13 @@
MIT License
Copyright (c) 2020 Mirantis, Inc. 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 Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal of this software and associated documentation files (the "Software"), to deal

View File

@ -1,3 +1,5 @@
EXTENSIONS_DIR = ./extensions
ifeq ($(OS),Windows_NT) ifeq ($(OS),Windows_NT)
DETECTED_OS := Windows DETECTED_OS := Windows
else else
@ -46,13 +48,24 @@ integration-win:
test-app: test-app:
yarn test yarn test
build: install-deps download-bins build: install-deps download-bins build-extensions
ifeq "$(DETECTED_OS)" "Windows" ifeq "$(DETECTED_OS)" "Windows"
yarn dist:win yarn dist:win
else else
yarn dist yarn dist
endif endif
build-extensions:
$(foreach dir, $(wildcard $(EXTENSIONS_DIR)/*), $(MAKE) -C $(dir) build;)
build-npm:
yarn compile:extension-types
yarn npm:fix-package-version
publish-npm: build-npm
npm config set '//registry.npmjs.org/:_authToken' "${NPM_TOKEN}"
cd src/extensions/npm/extensions && npm publish --access=public
clean: clean:
ifeq "$(DETECTED_OS)" "Windows" ifeq "$(DETECTED_OS)" "Windows"
if exist binaries\client del /s /q binaries\client\*.* if exist binaries\client del /s /q binaries\client\*.*
@ -62,4 +75,4 @@ else
rm -rf binaries/client/* rm -rf binaries/client/*
rm -rf dist/* rm -rf dist/*
rm -rf static/build/* rm -rf static/build/*
endif endif

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

@ -0,0 +1,2 @@
node_modules/
dist/

View File

@ -0,0 +1,5 @@
install-deps:
npm install
build: install-deps
npm run build

View File

@ -0,0 +1,11 @@
# Lens Example Extension
*TODO*: add more info
## Build
`npm run build`
## Dev
`npm run dev`

View File

@ -0,0 +1,11 @@
import { LensMainExtension } from "@k8slens/extensions";
export default class ExampleExtensionMain extends LensMainExtension {
onActivate() {
console.log('EXAMPLE EXTENSION MAIN: ACTIVATED', this.getMeta());
}
onDeactivate() {
console.log('EXAMPLE EXTENSION MAIN: DEACTIVATED', this.getMeta());
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,24 @@
{
"name": "extension-example",
"version": "1.0.0",
"description": "Example extension",
"main": "dist/main.js",
"renderer": "dist/renderer.js",
"lens": {
"metadata": {},
"styles": []
},
"scripts": {
"build": "webpack --config webpack.config.js",
"dev": "npm run build --watch"
},
"dependencies": {
"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

@ -0,0 +1,29 @@
import { LensRendererExtension, Component } from "@k8slens/extensions";
import { CoffeeDoodle } from "react-open-doodles";
import path from "path";
import React from "react"
export function ExampleIcon(props: Component.IconProps) {
return <Component.Icon {...props} material="pages" tooltip={path.basename(__filename)}/>
}
export class ExamplePage extends React.Component<{ extension: LensRendererExtension }> {
deactivate = () => {
const { extension } = this.props;
extension.disable();
}
render() {
const doodleStyle = {
width: "200px"
}
return (
<div className="flex column gaps align-flex-start">
<div style={doodleStyle}><CoffeeDoodle accent="#3d90ce" /></div>
<p>Hello from Example extension!</p>
<p>File: <i>{__filename}</i></p>
<Component.Button accent label="Deactivate" onClick={this.deactivate}/>
</div>
)
}
}

View File

@ -0,0 +1,16 @@
import { LensRendererExtension } from "@k8slens/extensions";
import { ExampleIcon, ExamplePage } from "./page"
import React from "react"
export default class ExampleExtension extends LensRendererExtension {
clusterPages = [
{
path: "/extension-example",
title: "Example Extension",
components: {
Page: () => <ExamplePage extension={this}/>,
MenuIcon: ExampleIcon,
}
}
]
}

View File

@ -0,0 +1,26 @@
{
"compilerOptions": {
"outDir": "dist",
"module": "CommonJS",
"target": "ES2017",
"lib": ["ESNext", "DOM", "DOM.Iterable"],
"moduleResolution": "Node",
"sourceMap": false,
"declaration": false,
"strict": false,
"noImplicitAny": true,
"skipLibCheck": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"experimentalDecorators": true,
"jsx": "react"
},
"include": [
"./*.ts",
"./*.tsx"
],
"exclude": [
"node_modules",
"*.js"
]
}

View File

@ -0,0 +1,65 @@
const path = require('path');
module.exports = [
{
entry: './main.ts',
context: __dirname,
target: "electron-main",
mode: "production",
module: {
rules: [
{
test: /\.tsx?$/,
use: 'ts-loader',
exclude: /node_modules/,
},
],
},
externals: [
{
"@k8slens/extensions": "var global.LensExtensions",
"mobx": "var global.Mobx",
"react": "var global.React"
}
],
resolve: {
extensions: [ '.tsx', '.ts', '.js' ],
},
output: {
libraryTarget: "commonjs2",
filename: 'main.js',
path: path.resolve(__dirname, 'dist'),
},
},
{
entry: './renderer.tsx',
context: __dirname,
target: "electron-renderer",
mode: "production",
module: {
rules: [
{
test: /\.tsx?$/,
use: 'ts-loader',
exclude: /node_modules/,
},
],
},
externals: [
{
"@k8slens/extensions": "var global.LensExtensions",
"react": "var global.React",
"mobx": "var global.Mobx"
}
],
resolve: {
extensions: [ '.tsx', '.ts', '.js' ],
},
output: {
libraryTarget: "commonjs2",
globalObject: "this",
filename: 'renderer.js',
path: path.resolve(__dirname, 'dist'),
},
},
];

View File

@ -0,0 +1,5 @@
install-deps:
npm install
build: install-deps
npm run build

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,25 @@
{
"name": "lens-metrics-cluster-feature",
"version": "0.1.0",
"description": "Lens metrics cluster feature",
"renderer": "dist/renderer.js",
"lens": {
"metadata": {},
"styles": []
},
"scripts": {
"build": "webpack --config webpack.config.js",
"dev": "npm run build --watch"
},
"dependencies": {
"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",
"mobx": "^5.15.5",
"react": "^16.13.1"
}
}

View File

@ -0,0 +1,23 @@
import { LensRendererExtension } from "@k8slens/extensions"
import { MetricsFeature } from "./src/metrics-feature"
import React from "react"
export default class ClusterMetricsFeatureExtension extends LensRendererExtension {
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()
}
]
}

View File

@ -0,0 +1,96 @@
import { ClusterFeature, Store, K8sApi } from "@k8slens/extensions"
import semver from "semver"
import * as path from "path"
export interface MetricsConfiguration {
// Placeholder for Metrics config structure
persistence: {
enabled: boolean;
storageClass: string;
size: string;
};
nodeExporter: {
enabled: boolean;
};
kubeStateMetrics: {
enabled: boolean;
};
retention: {
time: string;
size: string;
};
alertManagers: string[];
replicas: number;
storageClass: string;
}
export class MetricsFeature extends ClusterFeature.Feature {
name = "metrics"
latestVersion = "v2.17.2-lens1"
config: MetricsConfiguration = {
persistence: {
enabled: false,
storageClass: null,
size: "20G",
},
nodeExporter: {
enabled: true,
},
retention: {
time: "2d",
size: "5GB",
},
kubeStateMetrics: {
enabled: true,
},
alertManagers: null,
replicas: 1,
storageClass: null,
};
async install(cluster: Store.Cluster): Promise<void> {
// Check if there are storageclasses
const storageClassApi = K8sApi.forCluster(cluster, K8sApi.StorageClass)
const scs = await storageClassApi.list()
this.config.persistence.enabled = scs.some(sc => (
sc.metadata?.annotations?.['storageclass.kubernetes.io/is-default-class'] === 'true' ||
sc.metadata?.annotations?.['storageclass.beta.kubernetes.io/is-default-class'] === 'true'
));
super.applyResources(cluster, super.renderTemplates(path.join(__dirname, "../resources/")))
}
async upgrade(cluster: Store.Cluster): Promise<void> {
return this.install(cluster)
}
async updateStatus(cluster: Store.Cluster): Promise<ClusterFeature.FeatureStatus> {
try {
const statefulSet = K8sApi.forCluster(cluster, K8sApi.StatefulSet)
const prometheus = await statefulSet.get({name: "prometheus", namespace: "lens-metrics"})
if (prometheus?.kind) {
this.status.installed = true;
this.status.currentVersion = prometheus.spec.template.spec.containers[0].image.split(":")[1];
this.status.canUpgrade = semver.lt(this.status.currentVersion, this.latestVersion, true);
} else {
this.status.installed = false
}
} catch(e) {
if (e?.error?.code === 404) {
this.status.installed = false
}
}
return this.status
}
async uninstall(cluster: Store.Cluster): Promise<void> {
const namespaceApi = K8sApi.forCluster(cluster, K8sApi.Namespace)
const clusterRoleBindingApi = K8sApi.forCluster(cluster, K8sApi.ClusterRoleBinding)
const clusterRoleApi = K8sApi.forCluster(cluster, K8sApi.ClusterRole)
await namespaceApi.delete({name: "lens-metrics"})
await clusterRoleBindingApi.delete({name: "lens-prometheus"})
await clusterRoleApi.delete({name: "lens-prometheus"}) }
}

View File

@ -0,0 +1,26 @@
{
"compilerOptions": {
"outDir": "dist",
"module": "CommonJS",
"target": "ES2017",
"lib": ["ESNext", "DOM", "DOM.Iterable"],
"moduleResolution": "Node",
"sourceMap": false,
"declaration": false,
"strict": false,
"noImplicitAny": true,
"skipLibCheck": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"experimentalDecorators": true,
"jsx": "react"
},
"include": [
"./*.ts",
"./*.tsx"
],
"exclude": [
"node_modules",
"*.js"
]
}

View File

@ -0,0 +1,38 @@
const path = require('path');
module.exports = [
{
entry: './renderer.tsx',
context: __dirname,
target: "electron-renderer",
mode: "production",
module: {
rules: [
{
test: /\.tsx?$/,
use: 'ts-loader',
exclude: /node_modules/,
},
],
},
externals: [
{
"@k8slens/extensions": "var global.LensExtensions",
"react": "var global.React",
"mobx": "var global.Mobx"
}
],
resolve: {
extensions: [ '.tsx', '.ts', '.js' ],
},
output: {
libraryTarget: "commonjs2",
globalObject: "this",
filename: 'renderer.js',
path: path.resolve(__dirname, 'dist'),
},
node: {
__dirname: false
}
},
];

View File

@ -0,0 +1,5 @@
install-deps:
npm install
build: install-deps
npm run build

3512
extensions/node-menu/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,23 @@
{
"name": "lens-node-menu",
"version": "0.1.0",
"description": "Lens node menu",
"renderer": "dist/renderer.js",
"lens": {
"metadata": {},
"styles": []
},
"scripts": {
"build": "webpack --config webpack.config.js",
"dev": "npm run build --watch"
},
"dependencies": {},
"devDependencies": {
"@k8slens/extensions": "file:../../src/extensions/npm/extensions",
"ts-loader": "^8.0.4",
"typescript": "^4.0.3",
"webpack": "^4.44.2",
"mobx": "^5.15.5",
"react": "^16.13.1"
}
}

View File

@ -0,0 +1,15 @@
import { LensRendererExtension } from "@k8slens/extensions";
import React from "react"
import { NodeMenu, NodeMenuProps } from "./src/node-menu"
export default class NodeMenuRendererExtension extends LensRendererExtension {
kubeObjectMenuItems = [
{
kind: "Node",
apiVersions: ["v1"],
components: {
MenuItem: (props: NodeMenuProps) => <NodeMenu {...props} />
}
}
]
}

View File

@ -0,0 +1,73 @@
import React from "react";
import { Component, K8sApi, Navigation} from "@k8slens/extensions"
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();
const sendToTerminal = (command: string) => {
Component.terminalStore.sendCommand(command, {
enter: true,
newTab: true,
});
Navigation.hideDetails();
}
const shell = () => {
Component.createTerminalTab({
title: `Node: ${nodeName}`,
node: nodeName,
});
Navigation.hideDetails();
}
const cordon = () => {
sendToTerminal(`kubectl cordon ${nodeName}`);
}
const unCordon = () => {
sendToTerminal(`kubectl uncordon ${nodeName}`)
}
const drain = () => {
const command = `kubectl drain ${nodeName} --delete-local-data --ignore-daemonsets --force`;
Component.ConfirmDialog.open({
ok: () => sendToTerminal(command),
labelOk: `Drain Node`,
message: (
<p>
Are you sure you want to drain <b>{nodeName}</b>?
</p>
),
})
}
return (
<>
<Component.MenuItem onClick={shell}>
<Component.Icon svg="ssh" interactive={toolbar} title="Node shell"/>
<span className="title">Shell</span>
</Component.MenuItem>
{!node.isUnschedulable() && (
<Component.MenuItem onClick={cordon}>
<Component.Icon material="pause_circle_filled" title="Cordon" interactive={toolbar}/>
<span className="title">Cordon</span>
</Component.MenuItem>
)}
{node.isUnschedulable() && (
<Component.MenuItem onClick={unCordon}>
<Component.Icon material="play_circle_filled" title="Uncordon" interactive={toolbar}/>
<span className="title">Uncordon</span>
</Component.MenuItem>
)}
<Component.MenuItem onClick={drain}>
<Component.Icon material="delete_sweep" title="Drain" interactive={toolbar}/>
<span className="title">Drain</span>
</Component.MenuItem>
</>
);
}

View File

@ -0,0 +1,26 @@
{
"compilerOptions": {
"outDir": "dist",
"module": "CommonJS",
"target": "ES2017",
"lib": ["ESNext", "DOM", "DOM.Iterable"],
"moduleResolution": "Node",
"sourceMap": false,
"declaration": false,
"strict": false,
"noImplicitAny": true,
"skipLibCheck": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"experimentalDecorators": true,
"jsx": "react"
},
"include": [
"./*.ts",
"./*.tsx"
],
"exclude": [
"node_modules",
"*.js"
]
}

View File

@ -0,0 +1,35 @@
const path = require('path');
module.exports = [
{
entry: './renderer.tsx',
context: __dirname,
target: "electron-renderer",
mode: "production",
module: {
rules: [
{
test: /\.tsx?$/,
use: 'ts-loader',
exclude: /node_modules/,
},
],
},
externals: [
{
"@k8slens/extensions": "var global.LensExtensions",
"react": "var global.React",
"mobx": "var global.Mobx"
}
],
resolve: {
extensions: [ '.tsx', '.ts', '.js' ],
},
output: {
libraryTarget: "commonjs2",
globalObject: "this",
filename: 'renderer.js',
path: path.resolve(__dirname, 'dist'),
},
},
];

View File

@ -0,0 +1,5 @@
install-deps:
npm install
build: install-deps
npm run build

3512
extensions/pod-menu/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,23 @@
{
"name": "lens-pod-menu",
"version": "0.1.0",
"description": "Lens pod menu",
"renderer": "dist/renderer.js",
"lens": {
"metadata": {},
"styles": []
},
"scripts": {
"build": "webpack --config webpack.config.js",
"dev": "npm run build --watch"
},
"dependencies": {},
"devDependencies": {
"ts-loader": "^8.0.4",
"typescript": "^4.0.3",
"webpack": "^4.44.2",
"mobx": "^5.15.5",
"react": "^16.13.1",
"@k8slens/extensions": "file:../../src/extensions/npm/extensions"
}
}

View File

@ -0,0 +1,23 @@
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 {
kubeObjectMenuItems = [
{
kind: "Pod",
apiVersions: ["v1"],
components: {
MenuItem: (props: PodShellMenuProps) => <PodShellMenu {...props} />
}
},
{
kind: "Pod",
apiVersions: ["v1"],
components: {
MenuItem: (props: PodLogsMenuProps) => <PodLogsMenu {...props} />
}
}
]
}

View File

@ -0,0 +1,57 @@
import React from "react";
import { Component, K8sApi, Util, Navigation } from "@k8slens/extensions";
export interface PodLogsMenuProps extends Component.KubeObjectMenuProps<K8sApi.Pod> {
}
export class PodLogsMenu extends React.Component<PodLogsMenuProps> {
showLogs(container: K8sApi.IPodContainer) {
Navigation.hideDetails();
const pod = this.props.object;
Component.createPodLogsTab({
pod,
containers: pod.getContainers(),
initContainers: pod.getInitContainers(),
selectedContainer: container,
showTimestamps: false,
previous: false,
});
}
render() {
const { object: pod, toolbar } = this.props
const containers = pod.getAllContainers();
const statuses = pod.getContainerStatuses();
if (!containers.length) return;
return (
<Component.MenuItem onClick={Util.prevDefault(() => this.showLogs(containers[0]))}>
<Component.Icon material="subject" title="Logs" interactive={toolbar}/>
<span className="title">Logs</span>
{containers.length > 1 && (
<>
<Component.Icon className="arrow" material="keyboard_arrow_right"/>
<Component.SubMenu>
{
containers.map(container => {
const { name } = container
const status = statuses.find(status => status.name === name);
const brick = status ? (
<Component.StatusBrick
className={Util.cssNames(Object.keys(status.state)[0], { ready: status.ready })}
/>
) : null
return (
<Component.MenuItem key={name} onClick={Util.prevDefault(() => this.showLogs(container))} className="flex align-center">
{brick}
{name}
</Component.MenuItem>
)
})
}
</Component.SubMenu>
</>
)}
</Component.MenuItem>
)
}
}

View File

@ -0,0 +1,63 @@
import React from "react";
import { Component, K8sApi, Util, Navigation } from "@k8slens/extensions";
export interface PodShellMenuProps extends Component.KubeObjectMenuProps<K8sApi.Pod> {
}
export class PodShellMenu extends React.Component<PodShellMenuProps> {
async execShell(container?: string) {
Navigation.hideDetails();
const { object: pod } = this.props
const containerParam = container ? `-c ${container}` : ""
let command = `kubectl exec -i -t -n ${pod.getNs()} ${pod.getName()} ${containerParam} "--"`
if (window.navigator.platform !== "Win32") {
command = `exec ${command}`
}
if (pod.getSelectedNodeOs() === "windows") {
command = `${command} powershell`
} else {
command = `${command} sh -c "clear; (bash || ash || sh)"`
}
const shell = Component.createTerminalTab({
title: `Pod: ${pod.getName()} (namespace: ${pod.getNs()})`
});
Component.terminalStore.sendCommand(command, {
enter: true,
tabId: shell.id
});
}
render() {
const { object, toolbar } = this.props
const containers = object.getRunningContainers();
if (!containers.length) return;
return (
<Component.MenuItem onClick={Util.prevDefault(() => this.execShell(containers[0].name))}>
<Component.Icon svg="ssh" interactive={toolbar} title="Pod shell"/>
<span className="title">Shell</span>
{containers.length > 1 && (
<>
<Component.Icon className="arrow" material="keyboard_arrow_right"/>
<Component.SubMenu>
{
containers.map(container => {
const { name } = container;
return (
<Component.MenuItem key={name} onClick={Util.prevDefault(() => this.execShell(name))} className="flex align-center">
<Component.StatusBrick/>
{name}
</Component.MenuItem>
)
})
}
</Component.SubMenu>
</>
)}
</Component.MenuItem>
)
}
}

View File

@ -0,0 +1,26 @@
{
"compilerOptions": {
"outDir": "dist",
"module": "CommonJS",
"target": "ES2017",
"lib": ["ESNext", "DOM", "DOM.Iterable"],
"moduleResolution": "Node",
"sourceMap": false,
"declaration": false,
"strict": false,
"noImplicitAny": true,
"skipLibCheck": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"experimentalDecorators": true,
"jsx": "react"
},
"include": [
"./*.ts",
"./*.tsx"
],
"exclude": [
"node_modules",
"*.js"
]
}

View File

@ -0,0 +1,35 @@
const path = require('path');
module.exports = [
{
entry: './renderer.tsx',
context: __dirname,
target: "electron-renderer",
mode: "production",
module: {
rules: [
{
test: /\.tsx?$/,
use: 'ts-loader',
exclude: /node_modules/,
},
],
},
externals: [
{
"@k8slens/extensions": "var global.LensExtensions",
"react": "var global.React",
"mobx": "var global.Mobx"
}
],
resolve: {
extensions: [ '.tsx', '.ts', '.js' ],
},
output: {
libraryTarget: "commonjs2",
globalObject: "this",
filename: 'renderer.js',
path: path.resolve(__dirname, 'dist'),
},
},
];

View File

@ -0,0 +1,5 @@
install-deps:
npm install
build: install-deps
npm run build

View File

@ -0,0 +1,14 @@
import { LensMainExtension, windowManager } from "@k8slens/extensions";
import { supportPageURL } from "./src/support.route";
export default class SupportPageMainExtension extends LensMainExtension {
appMenus = [
{
parentId: "help",
label: "Support",
click() {
windowManager.navigate(supportPageURL());
}
}
]
}

3921
extensions/support-page/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,28 @@
{
"name": "lens-support-page",
"version": "0.1.0",
"description": "Lens support page",
"main": "dist/main.js",
"renderer": "dist/renderer.js",
"scripts": {
"build": "webpack -p",
"dev": "webpack --watch"
},
"dependencies": {},
"devDependencies": {
"@k8slens/extensions": "file:../../src/extensions/npm/extensions",
"@types/node": "^14.11.11",
"@types/react": "^16.9.53",
"@types/react-router": "^5.1.8",
"@types/webpack": "^4.41.17",
"css-loader": "^5.0.0",
"mobx": "^5.15.5",
"react": "^16.13.1",
"sass-loader": "^10.0.4",
"style-loader": "^2.0.0",
"ts-loader": "^8.0.4",
"ts-node": "^9.0.0",
"typescript": "^4.0.3",
"webpack": "^4.44.2"
}
}

View File

@ -0,0 +1,31 @@
import React from "react";
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 {
globalPages = [
{
...supportPageRoute,
url: supportPageURL(),
hideInMenu: true,
components: {
Page: Support,
}
}
]
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

@ -0,0 +1,7 @@
import type { RouteProps } from "react-router";
export const supportPageRoute: RouteProps = {
path: "/support"
}
export const supportPageURL = () => supportPageRoute.path.toString();

View File

@ -0,0 +1,13 @@
.PageLayout.Support {
a[target=_blank] {
text-decoration: none;
border-bottom: 1px solid;
&:after {
content: "launch";
font: small "Material Icons";
vertical-align: middle;
margin-left: 2px;
}
}
}

View File

@ -0,0 +1,29 @@
// TODO: support localization / figure out how to extract / consume i18n strings
import "./support.scss"
import React from "react"
import { observer } from "mobx-react"
import { App, Component } from "@k8slens/extensions";
@observer
export class Support extends React.Component {
render() {
const { PageLayout } = Component;
const { slackUrl, issuesTrackerUrl } = App;
return (
<PageLayout showOnTop className="Support" header={<h2>Support</h2>}>
<h2>Community Slack Channel</h2>
<p>
Ask a question, see what's being discussed, join the conversation <a href={slackUrl} target="_blank">here</a>
</p>
<h2>Report an Issue</h2>
<p>
Review existing issues or open a new one <a href={issuesTrackerUrl} target="_blank">here</a>
</p>
{/*<h2><Trans>Commercial Support</Trans></h2>*/}
</PageLayout>
);
}
}

View File

@ -0,0 +1,29 @@
{
"compilerOptions": {
"outDir": "dist",
"baseUrl": ".",
"module": "CommonJS",
"target": "ES2017",
"lib": ["ESNext", "DOM", "DOM.Iterable"],
"moduleResolution": "Node",
"sourceMap": false,
"declaration": false,
"strict": false,
"noImplicitAny": true,
"skipLibCheck": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"experimentalDecorators": true,
"jsx": "react",
"paths": {
"*": [
"node_modules/*",
"../../types/*"
]
}
},
"include": [
"renderer.tsx",
"src/**/*"
]
}

View File

@ -0,0 +1,75 @@
import path from "path"
const outputPath = path.resolve(__dirname, 'dist');
const lensExternals = {
"@k8slens/extensions": "var global.LensExtensions",
"react": "var global.React",
"mobx": "var global.Mobx",
"mobx-react": "var global.MobxReact",
};
export default [
{
entry: './main.ts',
context: __dirname,
target: "electron-main",
mode: "production",
module: {
rules: [
{
test: /\.tsx?$/,
use: 'ts-loader',
exclude: /node_modules/,
},
],
},
externals: [
lensExternals,
],
resolve: {
extensions: ['.tsx', '.ts', '.js'],
},
output: {
libraryTarget: "commonjs2",
globalObject: "this",
filename: 'main.js',
path: outputPath,
},
},
{
entry: './renderer.tsx',
context: __dirname,
target: "electron-renderer",
mode: "production",
module: {
rules: [
{
test: /\.tsx?$/,
use: 'ts-loader',
exclude: /node_modules/,
},
{
test: /\.s?css$/,
use: [
"style-loader",
"css-loader",
"sass-loader",
]
}
],
},
externals: [
lensExternals,
],
resolve: {
extensions: ['.tsx', '.ts', '.js'],
},
output: {
libraryTarget: "commonjs2",
globalObject: "this",
filename: 'renderer.js',
path: outputPath,
},
},
];

View File

@ -0,0 +1,5 @@
install-deps:
npm install
build: install-deps
npm run build

View File

@ -0,0 +1,17 @@
import { LensMainExtension } from "@k8slens/extensions";
import { telemetryPreferencesStore } from "./src/telemetry-preferences-store"
import { tracker } from "./src/tracker";
export default class TelemetryMainExtension extends LensMainExtension {
async onActivate() {
console.log("telemetry main extension activated")
tracker.start()
await telemetryPreferencesStore.loadExtension(this)
}
onDeactivate() {
tracker.stop()
console.log("telemetry main extension deactivated")
}
}

3861
extensions/telemetry/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,26 @@
{
"name": "lens-telemetry",
"version": "0.1.0",
"description": "Lens telemetry",
"main": "dist/main.js",
"renderer": "dist/renderer.js",
"lens": {
"metadata": {},
"styles": []
},
"scripts": {
"build": "webpack -p",
"dev": "webpack --watch"
},
"dependencies": {},
"devDependencies": {
"@k8slens/extensions": "file:../../src/extensions/npm/extensions",
"ts-loader": "^8.0.4",
"typescript": "^4.0.3",
"webpack": "^4.44.2",
"mobx": "^5.15.5",
"react": "^16.13.1",
"node-machine-id": "^1.1.12",
"universal-analytics": "^0.4.23"
}
}

View File

@ -0,0 +1,23 @@
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)
}
}

View File

@ -0,0 +1,26 @@
import { Component } from "@k8slens/extensions"
import React from "react"
import { observer } from "mobx-react";
import { TelemetryPreferencesStore } from "./telemetry-preferences-store"
@observer
export class TelemetryPreferenceInput extends React.Component<{telemetry: TelemetryPreferencesStore}, {}> {
render() {
const { telemetry } = this.props
return (
<Component.Checkbox
label="Allow telemetry & usage tracking"
value={telemetry.enabled}
onChange={v => { telemetry.enabled = v; }}
/>
)
}
}
export class TelemetryPreferenceHint extends React.Component {
render() {
return (
<span>Telemetry & usage data is collected to continuously improve the Lens experience.</span>
)
}
}

View File

@ -0,0 +1,35 @@
import { Store } from "@k8slens/extensions";
import { toJS } from "mobx"
export type TelemetryPreferencesModel = {
enabled: boolean;
}
export class TelemetryPreferencesStore extends Store.ExtensionStore<TelemetryPreferencesModel> {
private constructor() {
super({
configName: "preferences-store",
defaults: {
enabled: true
}
})
}
get enabled() {
return this.data.enabled
}
set enabled(v: boolean) {
this.data.enabled = v
}
toJSON(): TelemetryPreferencesModel {
return toJS({
enabled: this.data.enabled
}, {
recurseEverything: true
})
}
}
export const telemetryPreferencesStore = TelemetryPreferencesStore.getInstance<TelemetryPreferencesStore>()

View File

@ -0,0 +1,124 @@
import { EventBus, Util, Store, App } from "@k8slens/extensions"
import ua from "universal-analytics"
import { machineIdSync } from "node-machine-id"
import { telemetryPreferencesStore } from "./telemetry-preferences-store"
export class Tracker extends Util.Singleton {
static readonly GA_ID = "UA-159377374-1"
protected eventHandlers: Array<(ev: EventBus.AppEvent ) => void> = []
protected started = false
protected visitor: ua.Visitor
protected machineId: string = null;
protected ip: string = null;
protected appVersion: string;
protected locale: string;
protected electronUA: string;
protected reportInterval: NodeJS.Timeout
private constructor() {
super();
try {
this.visitor = ua(Tracker.GA_ID, machineIdSync(), { strictCidFormat: false })
} catch (error) {
this.visitor = ua(Tracker.GA_ID)
}
this.visitor.set("dl", "https://telemetry.k8slens.dev")
this.visitor.set("ua", `Lens ${App.version} (${this.getOS()})`)
}
start() {
if (this.started === true) { return }
this.started = true
const handler = (ev: EventBus.AppEvent) => {
this.event(ev.name, ev.action, ev.params)
}
this.eventHandlers.push(handler)
EventBus.appEventBus.addListener(handler)
this.reportInterval = setInterval(() => {
this.reportData()
}, 60 * 60 * 1000) // report every 1h
}
stop() {
if (!this.started) { return }
this.started = false
for (const handler of this.eventHandlers) {
EventBus.appEventBus.removeListener(handler)
}
if (this.reportInterval) {
clearInterval(this.reportInterval)
}
}
protected async isTelemetryAllowed(): Promise<boolean> {
return telemetryPreferencesStore.enabled
}
protected reportData() {
const clustersList = Store.clusterStore.clustersList
this.event("generic-data", "report", {
appVersion: App.version,
clustersCount: clustersList.length,
workspacesCount: Store.workspaceStore.workspacesList.length
})
clustersList.forEach((cluster) => {
if (!cluster?.metadata.lastSeen) { return }
this.reportClusterData(cluster)
})
}
protected reportClusterData(cluster: Store.ClusterModel) {
this.event("cluster-data", "report", {
id: cluster.metadata.id,
kubernetesVersion: cluster.metadata.version,
distribution: cluster.metadata.distribution,
nodesCount: cluster.metadata.nodes,
lastSeen: cluster.metadata.lastSeen
})
}
protected getOS() {
let os = ""
if (App.isMac) {
os = "MacOS"
} else if(App.isWindows) {
os = "Windows"
} else if (App.isLinux) {
os = "Linux"
if (App.isSnap) {
os += "; Snap"
} else {
os += "; AppImage"
}
} else {
os = "Unknown"
}
return os
}
protected async event(eventCategory: string, eventAction: string, otherParams = {}) {
try {
const allowed = await this.isTelemetryAllowed();
if (!allowed) {
return;
}
this.visitor.event({
ec: eventCategory,
ea: eventAction,
...otherParams,
}).send()
} catch (err) {
console.error(`Failed to track "${eventCategory}:${eventAction}"`, err)
}
}
}
export const tracker = Tracker.getInstance<Tracker>();

View File

@ -0,0 +1,29 @@
{
"compilerOptions": {
"outDir": "dist",
"baseUrl": ".",
"module": "CommonJS",
"target": "ES2017",
"lib": ["ESNext", "DOM", "DOM.Iterable"],
"moduleResolution": "Node",
"sourceMap": false,
"declaration": false,
"strict": false,
"noImplicitAny": true,
"skipLibCheck": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"experimentalDecorators": true,
"jsx": "react",
"paths": {
"*": [
"node_modules/*",
"../../types/*"
]
}
},
"include": [
"renderer.ts",
"src/**/*"
]
}

View File

@ -0,0 +1,67 @@
const path = require('path');
module.exports = [
{
entry: './main.ts',
context: __dirname,
target: "electron-main",
mode: "production",
module: {
rules: [
{
test: /\.tsx?$/,
use: 'ts-loader',
exclude: /node_modules/,
},
],
},
externals: [
{
"@k8slens/extensions": "var global.LensExtensions",
"react": "var global.React",
"mobx": "var global.Mobx"
}
],
resolve: {
extensions: [ '.tsx', '.ts', '.js' ],
},
output: {
libraryTarget: "commonjs2",
globalObject: "this",
filename: 'main.js',
path: path.resolve(__dirname, 'dist'),
},
},
{
entry: './renderer.tsx',
context: __dirname,
target: "electron-renderer",
mode: "production",
module: {
rules: [
{
test: /\.tsx?$/,
use: 'ts-loader',
exclude: /node_modules/,
},
],
},
externals: [
{
"@k8slens/extensions": "var global.LensExtensions",
"react": "var global.React",
"mobx": "var global.Mobx",
"mobx-react": "var global.MobxReact"
}
],
resolve: {
extensions: [ '.tsx', '.ts', '.js' ],
},
output: {
libraryTarget: "commonjs2",
globalObject: "this",
filename: 'renderer.js',
path: path.resolve(__dirname, 'dist'),
},
},
];

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

@ -2,7 +2,7 @@
"name": "kontena-lens", "name": "kontena-lens",
"productName": "Lens", "productName": "Lens",
"description": "Lens - The Kubernetes IDE", "description": "Lens - The Kubernetes IDE",
"version": "3.6.6", "version": "4.0.0-alpha.2",
"main": "static/build/main.js", "main": "static/build/main.js",
"copyright": "© 2020, Mirantis, Inc.", "copyright": "© 2020, Mirantis, Inc.",
"license": "MIT", "license": "MIT",
@ -11,14 +11,18 @@
"email": "info@k8slens.dev" "email": "info@k8slens.dev"
}, },
"scripts": { "scripts": {
"dev": "concurrently -k \"yarn dev-run -C\" \"yarn dev:main\" \"yarn dev:renderer\"", "dev": "concurrently -k \"yarn dev-run -C\" yarn:dev:*",
"dev-build": "concurrently yarn:compile:*",
"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-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-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",
@ -32,8 +36,8 @@
"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",
"lint": "eslint $@ --ext js,ts,tsx --max-warnings=0 src/", "build:tray-icons": "yarn run ts-node build/build_tray_icon.ts",
"rebuild-pty": "yarn run electron-rebuild -f -w node-pty" "lint": "eslint $@ --ext js,ts,tsx --max-warnings=0 src/"
}, },
"config": { "config": {
"bundledKubectlVersion": "1.17.11", "bundledKubectlVersion": "1.17.11",
@ -68,9 +72,15 @@
}, },
"moduleNameMapper": { "moduleNameMapper": {
"\\.(css|scss)$": "<rootDir>/__mocks__/styleMock.ts" "\\.(css|scss)$": "<rootDir>/__mocks__/styleMock.ts"
} },
"modulePathIgnorePatterns": [
"<rootDir>/dist"
]
}, },
"build": { "build": {
"files": [
"static/build/main.js"
],
"afterSign": "build/notarize.js", "afterSign": "build/notarize.js",
"extraResources": [ "extraResources": [
{ {
@ -88,6 +98,19 @@
"to": "static/", "to": "static/",
"filter": "!**/main.js" "filter": "!**/main.js"
}, },
{
"from": "build/tray",
"to": "static/icons",
"filter": "*.png"
},
{
"from": "extensions/",
"to": "./extensions/",
"filter": [
"**/*.js*",
"!**/node_modules"
]
},
"LICENSE" "LICENSE"
], ],
"linux": { "linux": {
@ -156,6 +179,15 @@
"confinement": "classic" "confinement": "classic"
} }
}, },
"lens": {
"extensions": [
"telemetry",
"pod-menu",
"node-menu",
"metrics-cluster-feature",
"support-page"
]
},
"dependencies": { "dependencies": {
"@hapi/call": "^8.0.0", "@hapi/call": "^8.0.0",
"@hapi/subtext": "^7.0.3", "@hapi/subtext": "^7.0.3",
@ -165,6 +197,7 @@
"@types/fs-extra": "^9.0.1", "@types/fs-extra": "^9.0.1",
"@types/http-proxy": "^1.17.4", "@types/http-proxy": "^1.17.4",
"@types/js-yaml": "^3.12.4", "@types/js-yaml": "^3.12.4",
"@types/jsdom": "^16.2.4",
"@types/jsonpath": "^0.2.0", "@types/jsonpath": "^0.2.0",
"@types/lodash": "^4.14.155", "@types/lodash": "^4.14.155",
"@types/marked": "^0.7.4", "@types/marked": "^0.7.4",
@ -185,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",
@ -195,13 +228,11 @@
"mobx": "^5.15.5", "mobx": "^5.15.5",
"mobx-observable-history": "^1.0.3", "mobx-observable-history": "^1.0.3",
"mock-fs": "^4.12.0", "mock-fs": "^4.12.0",
"node-machine-id": "^1.1.12",
"node-pty": "^0.9.0", "node-pty": "^0.9.0",
"npm": "^6.14.8",
"openid-client": "^3.15.2", "openid-client": "^3.15.2",
"path-to-regexp": "^6.1.0", "path-to-regexp": "^6.1.0",
"proper-lockfile": "^4.1.1", "proper-lockfile": "^4.1.1",
"react-beautiful-dnd": "^13.0.0",
"react-router": "^5.2.0",
"request": "^2.88.2", "request": "^2.88.2",
"request-promise-native": "^1.0.8", "request-promise-native": "^1.0.8",
"semver": "^7.3.2", "semver": "^7.3.2",
@ -211,7 +242,6 @@
"tar": "^6.0.2", "tar": "^6.0.2",
"tcp-port-used": "^1.0.1", "tcp-port-used": "^1.0.1",
"tempy": "^0.5.0", "tempy": "^0.5.0",
"universal-analytics": "^0.4.20",
"uuid": "^8.1.0", "uuid": "^8.1.0",
"win-ca": "^3.2.0", "win-ca": "^3.2.0",
"winston": "^3.2.1", "winston": "^3.2.1",
@ -232,27 +262,44 @@
"@lingui/macro": "^3.0.0-13", "@lingui/macro": "^3.0.0-13",
"@lingui/react": "^3.0.0-13", "@lingui/react": "^3.0.0-13",
"@material-ui/core": "^4.10.1", "@material-ui/core": "^4.10.1",
"@rollup/plugin-json": "^4.1.0",
"@types/chart.js": "^2.9.21", "@types/chart.js": "^2.9.21",
"@types/circular-dependency-plugin": "^5.0.1", "@types/circular-dependency-plugin": "^5.0.1",
"@types/color": "^3.0.1", "@types/color": "^3.0.1",
"@types/crypto-js": "^3.1.47",
"@types/dompurify": "^2.0.2", "@types/dompurify": "^2.0.2",
"@types/electron-window-state": "^2.0.34",
"@types/fs-extra": "^9.0.1",
"@types/hapi": "^18.0.3", "@types/hapi": "^18.0.3",
"@types/hoist-non-react-statics": "^3.3.1", "@types/hoist-non-react-statics": "^3.3.1",
"@types/html-webpack-plugin": "^3.2.3", "@types/html-webpack-plugin": "^3.2.3",
"@types/http-proxy": "^1.17.4",
"@types/jest": "^25.2.3", "@types/jest": "^25.2.3",
"@types/js-yaml": "^3.12.4",
"@types/jsonpath": "^0.2.0",
"@types/lodash": "^4.14.155",
"@types/marked": "^0.7.4",
"@types/material-ui": "^0.21.7", "@types/material-ui": "^0.21.7",
"@types/md5-file": "^4.0.2", "@types/md5-file": "^4.0.2",
"@types/mini-css-extract-plugin": "^0.9.1", "@types/mini-css-extract-plugin": "^0.9.1",
"@types/mock-fs": "^4.10.0",
"@types/module-alias": "^2.0.0",
"@types/node": "^12.12.45",
"@types/npm": "^2.0.31",
"@types/progress-bar-webpack-plugin": "^2.1.0", "@types/progress-bar-webpack-plugin": "^2.1.0",
"@types/proper-lockfile": "^4.1.1",
"@types/react": "^16.9.35", "@types/react": "^16.9.35",
"@types/react-beautiful-dnd": "^13.0.0",
"@types/react-router-dom": "^5.1.5", "@types/react-router-dom": "^5.1.5",
"@types/react-select": "^3.0.13", "@types/react-select": "^3.0.13",
"@types/react-window": "^1.8.2", "@types/react-window": "^1.8.2",
"@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/tcp-port-used": "^1.0.0", "@types/tcp-port-used": "^1.0.0",
"@types/tempy": "^0.3.0", "@types/tempy": "^0.3.0",
"@types/terser-webpack-plugin": "^3.0.0", "@types/terser-webpack-plugin": "^3.0.0",
@ -280,7 +327,6 @@
"electron": "^9.1.2", "electron": "^9.1.2",
"electron-builder": "^22.7.0", "electron-builder": "^22.7.0",
"electron-notarize": "^0.3.0", "electron-notarize": "^0.3.0",
"electron-rebuild": "^1.11.0",
"eslint": "^7.7.0", "eslint": "^7.7.0",
"file-loader": "^6.0.0", "file-loader": "^6.0.0",
"flex.box": "^3.4.4", "flex.box": "^3.4.4",
@ -292,7 +338,6 @@
"jest": "^26.0.1", "jest": "^26.0.1",
"jest-mock-extended": "^1.0.10", "jest-mock-extended": "^1.0.10",
"make-plural": "^6.2.1", "make-plural": "^6.2.1",
"material-design-icons": "^3.0.1",
"mini-css-extract-plugin": "^0.9.0", "mini-css-extract-plugin": "^0.9.0",
"mobx-react": "^6.2.2", "mobx-react": "^6.2.2",
"moment": "^2.26.0", "moment": "^2.26.0",
@ -303,12 +348,19 @@
"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-dom": "^16.13.1", "react-dom": "^16.13.1",
"react-router": "^5.2.0",
"react-router-dom": "^5.2.0", "react-router-dom": "^5.2.0",
"react-select": "^3.1.0", "react-select": "^3.1.0",
"react-window": "^1.8.5", "react-window": "^1.8.5",
"rollup": "^2.28.2",
"rollup-plugin-dts": "^1.4.13",
"rollup-plugin-ignore-import": "^1.3.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

@ -64,13 +64,13 @@ describe("empty config", () => {
it("sets active cluster", () => { it("sets active cluster", () => {
clusterStore.setActive("foo"); clusterStore.setActive("foo");
expect(clusterStore.activeCluster.id).toBe("foo"); expect(clusterStore.active.id).toBe("foo");
}) })
}) })
describe("with prod and dev clusters added", () => { describe("with prod and dev clusters added", () => {
beforeEach(() => { beforeEach(() => {
clusterStore.addCluster( clusterStore.addClusters(
new Cluster({ new Cluster({
id: "prod", id: "prod",
contextName: "prod", contextName: "prod",

View File

@ -0,0 +1,15 @@
import { appEventBus, AppEvent } from "../event-bus"
describe("event bus tests", () => {
describe("emit", () => {
it("emits an event", () => {
let event: AppEvent = null
appEventBus.addListener((data) => {
event = data
})
appEventBus.emit({name: "foo", action: "bar"})
expect(event.name).toBe("foo")
})
})
})

View File

@ -10,7 +10,7 @@ jest.mock("electron", () => {
} }
}) })
import { WorkspaceStore } from "../workspace-store" import { Workspace, WorkspaceStore } from "../workspace-store"
describe("workspace store tests", () => { describe("workspace store tests", () => {
describe("for an empty config", () => { describe("for an empty config", () => {
@ -35,16 +35,16 @@ describe("workspace store tests", () => {
it("cannot remove the default workspace", () => { it("cannot remove the default workspace", () => {
const ws = WorkspaceStore.getInstance<WorkspaceStore>(); const ws = WorkspaceStore.getInstance<WorkspaceStore>();
expect(() => ws.removeWorkspace(WorkspaceStore.defaultId)).toThrowError("Cannot remove"); expect(() => ws.removeWorkspaceById(WorkspaceStore.defaultId)).toThrowError("Cannot remove");
}) })
it("can update default workspace name", () => { it("can update default workspace name", () => {
const ws = WorkspaceStore.getInstance<WorkspaceStore>(); const ws = WorkspaceStore.getInstance<WorkspaceStore>();
ws.saveWorkspace({ ws.addWorkspace(new Workspace({
id: WorkspaceStore.defaultId, id: WorkspaceStore.defaultId,
name: "foobar", name: "foobar",
}); }));
expect(ws.currentWorkspace.name).toBe("foobar"); expect(ws.currentWorkspace.name).toBe("foobar");
}) })
@ -52,10 +52,10 @@ describe("workspace store tests", () => {
it("can add workspaces", () => { it("can add workspaces", () => {
const ws = WorkspaceStore.getInstance<WorkspaceStore>(); const ws = WorkspaceStore.getInstance<WorkspaceStore>();
ws.saveWorkspace({ ws.addWorkspace(new Workspace({
id: "123", id: "123",
name: "foobar", name: "foobar",
}); }));
expect(ws.getById("123").name).toBe("foobar"); expect(ws.getById("123").name).toBe("foobar");
}) })
@ -69,10 +69,10 @@ describe("workspace store tests", () => {
it("can set a existent workspace to be active", () => { it("can set a existent workspace to be active", () => {
const ws = WorkspaceStore.getInstance<WorkspaceStore>(); const ws = WorkspaceStore.getInstance<WorkspaceStore>();
ws.saveWorkspace({ ws.addWorkspace(new Workspace({
id: "abc", id: "abc",
name: "foobar", name: "foobar",
}); }));
expect(() => ws.setActive("abc")).not.toThrowError(); expect(() => ws.setActive("abc")).not.toThrowError();
}) })
@ -80,15 +80,15 @@ describe("workspace store tests", () => {
it("can remove a workspace", () => { it("can remove a workspace", () => {
const ws = WorkspaceStore.getInstance<WorkspaceStore>(); const ws = WorkspaceStore.getInstance<WorkspaceStore>();
ws.saveWorkspace({ ws.addWorkspace(new Workspace({
id: "123", id: "123",
name: "foobar", name: "foobar",
}); }));
ws.saveWorkspace({ ws.addWorkspace(new Workspace({
id: "1234", id: "1234",
name: "foobar 1", name: "foobar 1",
}); }));
ws.removeWorkspace("123"); ws.removeWorkspaceById("123");
expect(ws.workspaces.size).toBe(2); expect(ws.workspaces.size).toBe(2);
}) })
@ -96,10 +96,10 @@ describe("workspace store tests", () => {
it("cannot create workspace with existent name", () => { it("cannot create workspace with existent name", () => {
const ws = WorkspaceStore.getInstance<WorkspaceStore>(); const ws = WorkspaceStore.getInstance<WorkspaceStore>();
ws.saveWorkspace({ ws.addWorkspace(new Workspace({
id: "someid", id: "someid",
name: "default", name: "default",
}); }));
expect(ws.workspacesList.length).toBe(1); // default workspace only expect(ws.workspacesList.length).toBe(1); // default workspace only
}) })
@ -107,10 +107,10 @@ describe("workspace store tests", () => {
it("cannot create workspace with empty name", () => { it("cannot create workspace with empty name", () => {
const ws = WorkspaceStore.getInstance<WorkspaceStore>(); const ws = WorkspaceStore.getInstance<WorkspaceStore>();
ws.saveWorkspace({ ws.addWorkspace(new Workspace({
id: "random", id: "random",
name: "", name: "",
}); }));
expect(ws.workspacesList.length).toBe(1); // default workspace only expect(ws.workspacesList.length).toBe(1); // default workspace only
}) })
@ -118,10 +118,10 @@ describe("workspace store tests", () => {
it("cannot create workspace with ' ' name", () => { it("cannot create workspace with ' ' name", () => {
const ws = WorkspaceStore.getInstance<WorkspaceStore>(); const ws = WorkspaceStore.getInstance<WorkspaceStore>();
ws.saveWorkspace({ ws.addWorkspace(new Workspace({
id: "random", id: "random",
name: " ", name: " ",
}); }));
expect(ws.workspacesList.length).toBe(1); // default workspace only expect(ws.workspacesList.length).toBe(1); // default workspace only
}) })
@ -129,10 +129,10 @@ describe("workspace store tests", () => {
it("trim workspace name", () => { it("trim workspace name", () => {
const ws = WorkspaceStore.getInstance<WorkspaceStore>(); const ws = WorkspaceStore.getInstance<WorkspaceStore>();
ws.saveWorkspace({ ws.addWorkspace(new Workspace({
id: "random", id: "random",
name: "default ", name: "default ",
}); }));
expect(ws.workspacesList.length).toBe(1); // default workspace only expect(ws.workspacesList.length).toBe(1); // default workspace only
}) })
@ -169,4 +169,4 @@ describe("workspace store tests", () => {
expect(ws.currentWorkspaceId).toBe("abc"); expect(ws.currentWorkspaceId).toBe("abc");
}) })
}) })
}) })

View File

@ -56,13 +56,17 @@ export class BaseStore<T = any> extends Singleton {
...confOptions, ...confOptions,
projectName: "lens", projectName: "lens",
projectVersion: getAppVersion(), projectVersion: getAppVersion(),
cwd: (app || remote.app).getPath("userData"), cwd: this.storePath(),
}); });
logger.info(`[STORE]: LOADED from ${this.storeConfig.path}`); logger.info(`[STORE]: LOADED from ${this.storeConfig.path}`);
this.fromStore(this.storeConfig.store); this.fromStore(this.storeConfig.store);
this.isLoaded = true; this.isLoaded = true;
} }
protected storePath() {
return (app || remote.app).getPath("userData")
}
protected async saveToFile(model: T) { protected async saveToFile(model: T) {
logger.info(`[STORE]: SAVING ${this.name}`); logger.info(`[STORE]: SAVING ${this.name}`);
// todo: update when fixed https://github.com/sindresorhus/conf/issues/114 // todo: update when fixed https://github.com/sindresorhus/conf/issues/114
@ -86,13 +90,19 @@ export class BaseStore<T = any> extends Singleton {
if (ipcRenderer) { if (ipcRenderer) {
const callback = (event: IpcRendererEvent, model: T) => { const callback = (event: IpcRendererEvent, model: T) => {
logger.silly(`[STORE]: SYNC ${this.name} from main`, { model }); logger.silly(`[STORE]: SYNC ${this.name} from main`, { model });
this.onSync(model); this.onSyncFromMain(model);
}; };
ipcRenderer.on(this.syncChannel, callback); ipcRenderer.on(this.syncChannel, callback);
this.syncDisposers.push(() => ipcRenderer.off(this.syncChannel, callback)); this.syncDisposers.push(() => ipcRenderer.off(this.syncChannel, callback));
} }
} }
protected onSyncFromMain(model: T) {
this.applyWithoutSync(() => {
this.onSync(model)
})
}
unregisterIpcListener() { unregisterIpcListener() {
ipcRenderer.removeAllListeners(this.syncChannel) ipcRenderer.removeAllListeners(this.syncChannel)
} }

View File

@ -1,6 +1,8 @@
import { createIpcChannel } from "./ipc"; import { createIpcChannel } from "./ipc";
import { ClusterId, clusterStore } from "./cluster-store"; import { ClusterId, clusterStore } from "./cluster-store";
import { tracker } from "./tracker"; import { extensionLoader } from "../extensions/extension-loader"
import { appEventBus } from "./event-bus"
import { ResourceApplier } from "../main/resource-applier";
export const clusterIpc = { export const clusterIpc = {
activate: createIpcChannel({ activate: createIpcChannel({
@ -19,6 +21,7 @@ export const clusterIpc = {
const cluster = clusterStore.getById(clusterId); const cluster = clusterStore.getById(clusterId);
if (cluster) { if (cluster) {
if (frameId) cluster.frameId = frameId; // save cluster's webFrame.routingId to be able to send push-updates if (frameId) cluster.frameId = frameId; // save cluster's webFrame.routingId to be able to send push-updates
extensionLoader.broadcastExtensions(frameId)
return cluster.pushState(); return cluster.pushState();
} }
}, },
@ -28,44 +31,29 @@ export const clusterIpc = {
channel: "cluster:refresh", channel: "cluster:refresh",
handle: (clusterId: ClusterId) => { handle: (clusterId: ClusterId) => {
const cluster = clusterStore.getById(clusterId); const cluster = clusterStore.getById(clusterId);
if (cluster) return cluster.refresh(); if (cluster) return cluster.refresh({ refreshMetadata: true })
}, },
}), }),
disconnect: createIpcChannel({ disconnect: createIpcChannel({
channel: "cluster:disconnect", channel: "cluster:disconnect",
handle: (clusterId: ClusterId) => { handle: (clusterId: ClusterId) => {
tracker.event("cluster", "stop"); appEventBus.emit({name: "cluster", action: "stop"});
return clusterStore.getById(clusterId)?.disconnect(); return clusterStore.getById(clusterId)?.disconnect();
}, },
}), }),
installFeature: createIpcChannel({ kubectlApplyAll: createIpcChannel({
channel: "cluster:install-feature", channel: "cluster:kubectl-apply-all",
handle: async (clusterId: ClusterId, feature: string, config?: any) => { handle: (clusterId: ClusterId, resources: string[]) => {
tracker.event("cluster", "install", feature); appEventBus.emit({name: "cluster", action: "kubectl-apply-all"})
const cluster = clusterStore.getById(clusterId); const cluster = clusterStore.getById(clusterId);
if (cluster) { if (cluster) {
await cluster.installFeature(feature, config) const applier = new ResourceApplier(cluster)
applier.kubectlApplyAll(resources)
} else { } else {
throw `${clusterId} is not a valid cluster id`; throw `${clusterId} is not a valid cluster id`;
} }
} }
}), }),
uninstallFeature: createIpcChannel({
channel: "cluster:uninstall-feature",
handle: (clusterId: ClusterId, feature: string) => {
tracker.event("cluster", "uninstall", feature);
return clusterStore.getById(clusterId)?.uninstallFeature(feature)
}
}),
upgradeFeature: createIpcChannel({
channel: "cluster:upgrade-feature",
handle: (clusterId: ClusterId, feature: string, config?: any) => {
tracker.event("cluster", "upgrade", feature);
return clusterStore.getById(clusterId)?.upgradeFeature(feature, config)
}
}),
} }

View File

@ -1,18 +1,18 @@
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";
import { Cluster, ClusterState } from "../main/cluster"; import { Cluster, ClusterState } from "../main/cluster";
import migrations from "../migrations/cluster-store" import migrations from "../migrations/cluster-store"
import logger from "../main/logger"; import logger from "../main/logger";
import { tracker } from "./tracker"; import { appEventBus } from "./event-bus"
import { dumpConfigYaml } from "./kube-helpers"; import { dumpConfigYaml } from "./kube-helpers";
import { saveToAppFiles } from "./utils/saveToAppFiles"; import { saveToAppFiles } from "./utils/saveToAppFiles";
import { KubeConfig } from "@kubernetes/client-node"; import { KubeConfig } from "@kubernetes/client-node";
import _ from "lodash"; import _ from "lodash";
import move from "array-move"; import move from "array-move";
import type { WorkspaceId } from "./workspace-store";
export interface ClusterIconUpload { export interface ClusterIconUpload {
clusterId: string; clusterId: string;
@ -20,6 +20,10 @@ export interface ClusterIconUpload {
path: string; path: string;
} }
export interface ClusterMetadata {
[key: string]: string | number | boolean;
}
export interface ClusterStoreModel { export interface ClusterStoreModel {
activeCluster?: ClusterId; // last opened cluster activeCluster?: ClusterId; // last opened cluster
clusters?: ClusterModel[] clusters?: ClusterModel[]
@ -29,10 +33,12 @@ export type ClusterId = string;
export interface ClusterModel { export interface ClusterModel {
id: ClusterId; id: ClusterId;
kubeConfigPath: string;
workspace?: WorkspaceId; workspace?: WorkspaceId;
contextName?: string; contextName?: string;
preferences?: ClusterPreferences; preferences?: ClusterPreferences;
kubeConfigPath: string; metadata?: ClusterMetadata;
ownerRef?: string;
/** @deprecated */ /** @deprecated */
kubeConfig?: string; // yaml kubeConfig?: string; // yaml
@ -67,25 +73,34 @@ export class ClusterStore extends BaseStore<ClusterStoreModel> {
return filePath; return filePath;
} }
@observable activeCluster: ClusterId;
@observable removedClusters = observable.map<ClusterId, Cluster>();
@observable clusters = observable.map<ClusterId, Cluster>();
private constructor() { private constructor() {
super({ super({
configName: "lens-cluster-store", configName: "lens-cluster-store",
accessPropertiesByDotNotation: false, // To make dots safe in cluster context names accessPropertiesByDotNotation: false, // To make dots safe in cluster context names
migrations: migrations, migrations: migrations,
}); });
this.pushStateToViewsPeriodically()
} }
@observable activeClusterId: ClusterId; protected pushStateToViewsPeriodically() {
@observable removedClusters = observable.map<ClusterId, Cluster>(); if (!ipcRenderer) {
@observable clusters = observable.map<ClusterId, Cluster>(); // This is a bit of a hack, we need to do this because we might loose messages that are sent before a view is ready
setInterval(() => {
this.pushState()
}, 5000)
}
}
registerIpcListener() { registerIpcListener() {
logger.info(`[CLUSTER-STORE] start to listen (${webFrame.routingId})`) logger.info(`[CLUSTER-STORE] start to listen (${webFrame.routingId})`)
ipcRenderer.on("cluster:state", (event, model: ClusterState) => { ipcRenderer.on("cluster:state", (event, clusterId: string, state: ClusterState) => {
this.applyWithoutSync(() => { logger.silly(`[CLUSTER-STORE]: received push-state at ${location.host} (${webFrame.routingId})`, clusterId, state);
logger.silly(`[CLUSTER-STORE]: received push-state at ${location.host} (${webFrame.routingId})`, model); this.getById(clusterId)?.setState(state)
this.getById(model.id)?.updateModel(model);
})
}) })
} }
@ -94,21 +109,35 @@ export class ClusterStore extends BaseStore<ClusterStoreModel> {
ipcRenderer.removeAllListeners("cluster:state") ipcRenderer.removeAllListeners("cluster:state")
} }
@computed get activeCluster(): Cluster | null { pushState() {
return this.getById(this.activeClusterId); this.clusters.forEach((c) => {
c.pushState()
})
}
get activeClusterId() {
return this.activeCluster
} }
@computed get clustersList(): Cluster[] { @computed get clustersList(): Cluster[] {
return Array.from(this.clusters.values()); return Array.from(this.clusters.values());
} }
@computed get enabledClustersList(): Cluster[] {
return this.clustersList.filter((c) => c.enabled)
}
@computed get active(): Cluster | null {
return this.getById(this.activeCluster);
}
isActive(id: ClusterId) { isActive(id: ClusterId) {
return this.activeClusterId === id; return this.activeCluster === id;
} }
@action @action
setActive(id: ClusterId) { setActive(id: ClusterId) {
this.activeClusterId = id; this.activeCluster = this.clusters.has(id) ? id : null;
} }
@action @action
@ -140,22 +169,38 @@ export class ClusterStore extends BaseStore<ClusterStoreModel> {
} }
@action @action
addCluster(...models: ClusterModel[]) { addClusters(...models: ClusterModel[]): Cluster[] {
const clusters: Cluster[] = []
models.forEach(model => { models.forEach(model => {
tracker.event("cluster", "add"); clusters.push(this.addCluster(model))
const cluster = new Cluster(model);
this.clusters.set(model.id, cluster);
}) })
return clusters
}
@action
addCluster(model: ClusterModel | Cluster ): Cluster {
appEventBus.emit({name: "cluster", action: "add"})
let cluster = model as Cluster;
if (!(model instanceof Cluster)) {
cluster = new Cluster(model)
}
this.clusters.set(model.id, cluster);
return cluster
}
async removeCluster(model: ClusterModel) {
await this.removeById(model.id)
} }
@action @action
async removeById(clusterId: ClusterId) { async removeById(clusterId: ClusterId) {
tracker.event("cluster", "remove"); appEventBus.emit({name: "cluster", action: "remove"})
const cluster = this.getById(clusterId); const cluster = this.getById(clusterId);
if (cluster) { if (cluster) {
this.clusters.delete(clusterId); this.clusters.delete(clusterId);
if (this.activeClusterId === clusterId) { if (this.activeCluster === 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)) {
@ -184,6 +229,9 @@ export class ClusterStore extends BaseStore<ClusterStoreModel> {
cluster.updateModel(clusterModel); cluster.updateModel(clusterModel);
} else { } else {
cluster = new Cluster(clusterModel); cluster = new Cluster(clusterModel);
if (!cluster.isManaged) {
cluster.enabled = true
}
} }
newClusters.set(clusterModel.id, cluster); newClusters.set(clusterModel.id, cluster);
} }
@ -195,14 +243,14 @@ export class ClusterStore extends BaseStore<ClusterStoreModel> {
} }
}); });
this.activeClusterId = newClusters.has(activeCluster) ? activeCluster : null; this.activeCluster = newClusters.has(activeCluster) ? activeCluster : null;
this.clusters.replace(newClusters); this.clusters.replace(newClusters);
this.removedClusters.replace(removedClusters); this.removedClusters.replace(removedClusters);
} }
toJSON(): ClusterStoreModel { toJSON(): ClusterStoreModel {
return toJS({ return toJS({
activeCluster: this.activeClusterId, activeCluster: this.activeCluster,
clusters: this.clustersList.map(cluster => cluster.toJSON()), clusters: this.clustersList.map(cluster => cluster.toJSON()),
}, { }, {
recurseEverything: true recurseEverything: true

9
src/common/event-bus.ts Normal file
View File

@ -0,0 +1,9 @@
import { EventEmitter } from "./event-emitter"
export type AppEvent = {
name: string;
action: string;
params?: object;
}
export const appEventBus = new EventEmitter<[AppEvent]>()

View File

@ -1,49 +0,0 @@
import { app, App, remote } from "electron"
import ua from "universal-analytics"
import { machineIdSync } from "node-machine-id"
import Singleton from "./utils/singleton";
import { userStore } from "./user-store"
import logger from "../main/logger";
export class Tracker extends Singleton {
static readonly GA_ID = "UA-159377374-1"
protected visitor: ua.Visitor
protected machineId: string = null;
protected ip: string = null;
protected appVersion: string;
protected locale: string;
protected electronUA: string;
private constructor(app: App) {
super();
try {
this.visitor = ua(Tracker.GA_ID, machineIdSync(), { strictCidFormat: false })
} catch (error) {
this.visitor = ua(Tracker.GA_ID)
}
this.visitor.set("dl", "https://telemetry.k8slens.dev")
}
protected async isTelemetryAllowed(): Promise<boolean> {
return userStore.preferences.allowTelemetry;
}
async event(eventCategory: string, eventAction: string, otherParams = {}) {
try {
const allowed = await this.isTelemetryAllowed();
if (!allowed) {
return;
}
this.visitor.event({
ec: eventCategory,
ea: eventAction,
...otherParams,
}).send()
} catch (err) {
logger.error(`Failed to track "${eventCategory}:${eventAction}"`, err)
}
}
}
export const tracker = Tracker.getInstance<Tracker>(app || remote.app);

View File

@ -7,7 +7,7 @@ import { BaseStore } from "./base-store";
import migrations from "../migrations/user-store" import migrations from "../migrations/user-store"
import { getAppVersion } from "./utils/app-version"; import { getAppVersion } from "./utils/app-version";
import { kubeConfigDefaultPath, loadConfig } from "./kube-helpers"; import { kubeConfigDefaultPath, loadConfig } from "./kube-helpers";
import { tracker } from "./tracker"; import { appEventBus } from "./event-bus"
import logger from "../main/logger"; import logger from "../main/logger";
import path from 'path'; import path from 'path';
@ -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 => {
tracker.event("telemetry", 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);
} }
@ -77,7 +94,7 @@ export class UserStore extends BaseStore<UserStoreModel> {
@action @action
saveLastSeenAppVersion() { saveLastSeenAppVersion() {
tracker.event("app", "whats-new-seen") appEventBus.emit({name: "app", action: "whats-new-seen"})
this.lastSeenAppVersion = getAppVersion(); this.lastSeenAppVersion = getAppVersion();
} }

View File

@ -7,3 +7,7 @@ export function getAppVersion(): string {
export function getBundledKubectlVersion(): string { export function getBundledKubectlVersion(): string {
return packageInfo.config.bundledKubectlVersion; return packageInfo.config.bundledKubectlVersion;
} }
export function getBundledExtensions(): string[] {
return packageInfo.lens?.extensions || []
}

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;

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