mirror of
https://github.com/lensapp/lens.git
synced 2025-05-20 05:10:56 +00:00
Merge pull request #1131 from lensapp/extensions-api
Initial extensions API
This commit is contained in:
commit
8fbdb8cc8e
@ -4,6 +4,7 @@ module.exports = {
|
||||
files: [
|
||||
"src/renderer/**/*.js",
|
||||
"build/**/*.js",
|
||||
"extensions/**/*.js"
|
||||
],
|
||||
extends: [
|
||||
'eslint:recommended',
|
||||
@ -24,7 +25,9 @@ module.exports = {
|
||||
files: [
|
||||
"build/*.ts",
|
||||
"src/**/*.ts",
|
||||
"integration/**/*.ts"
|
||||
"integration/**/*.ts",
|
||||
"src/extensions/**/*.ts*",
|
||||
"extensions/**/*.ts*"
|
||||
],
|
||||
parser: "@typescript-eslint/parser",
|
||||
extends: [
|
||||
|
||||
13
.gitignore
vendored
13
.gitignore
vendored
@ -1,12 +1,17 @@
|
||||
dist/
|
||||
out/
|
||||
node_modules/
|
||||
.DS_Store
|
||||
yarn-error.log
|
||||
coverage/
|
||||
tmp/
|
||||
static/build/**
|
||||
binaries/client/
|
||||
binaries/server/
|
||||
locales/**/**.js
|
||||
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
|
||||
|
||||
7
Makefile
7
Makefile
@ -1,3 +1,5 @@
|
||||
EXTENSIONS_DIR = ./extensions
|
||||
|
||||
ifeq ($(OS),Windows_NT)
|
||||
DETECTED_OS := Windows
|
||||
else
|
||||
@ -46,13 +48,16 @@ integration-win:
|
||||
test-app:
|
||||
yarn test
|
||||
|
||||
build: install-deps download-bins
|
||||
build: install-deps download-bins build-extensions
|
||||
ifeq "$(DETECTED_OS)" "Windows"
|
||||
yarn dist:win
|
||||
else
|
||||
yarn dist
|
||||
endif
|
||||
|
||||
build-extensions:
|
||||
$(foreach file, $(wildcard $(EXTENSIONS_DIR)/*), $(MAKE) -C $(file) build;)
|
||||
|
||||
clean:
|
||||
ifeq "$(DETECTED_OS)" "Windows"
|
||||
if exist binaries\client del /s /q binaries\client\*.*
|
||||
|
||||
2
extensions/example-extension/.gitignore
vendored
Normal file
2
extensions/example-extension/.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
node_modules/
|
||||
dist/
|
||||
5
extensions/example-extension/Makefile
Normal file
5
extensions/example-extension/Makefile
Normal file
@ -0,0 +1,5 @@
|
||||
install-deps:
|
||||
npm install
|
||||
|
||||
build: install-deps
|
||||
npm run build
|
||||
11
extensions/example-extension/README.md
Normal file
11
extensions/example-extension/README.md
Normal file
@ -0,0 +1,11 @@
|
||||
# Lens Example Extension
|
||||
|
||||
*TODO*: add more info
|
||||
|
||||
## Build
|
||||
|
||||
`npm run build`
|
||||
|
||||
## Dev
|
||||
|
||||
`npm run dev`
|
||||
11
extensions/example-extension/main.ts
Normal file
11
extensions/example-extension/main.ts
Normal 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());
|
||||
}
|
||||
}
|
||||
3464
extensions/example-extension/package-lock.json
generated
Normal file
3464
extensions/example-extension/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
23
extensions/example-extension/package.json
Normal file
23
extensions/example-extension/package.json
Normal file
@ -0,0 +1,23 @@
|
||||
{
|
||||
"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": {
|
||||
"ts-loader": "^8.0.4",
|
||||
"typescript": "^4.0.3",
|
||||
"webpack": "^4.44.2"
|
||||
}
|
||||
}
|
||||
29
extensions/example-extension/page.tsx
Normal file
29
extensions/example-extension/page.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
}
|
||||
26
extensions/example-extension/renderer.tsx
Normal file
26
extensions/example-extension/renderer.tsx
Normal file
@ -0,0 +1,26 @@
|
||||
import { LensRendererExtension, Registry } from "@k8slens/extensions";
|
||||
import { ExamplePage, ExampleIcon } from "./page"
|
||||
import React from "react"
|
||||
|
||||
export default class ExampleExtension extends LensRendererExtension {
|
||||
onActivate() {
|
||||
console.log('EXAMPLE EXTENSION RENDERER: ACTIVATED', this.getMeta());
|
||||
}
|
||||
|
||||
registerClusterPage(registry: Registry.ClusterPageRegistry) {
|
||||
this.disposers.push(
|
||||
registry.add({
|
||||
path: "/extension-example",
|
||||
title: "Example Extension",
|
||||
components: {
|
||||
Page: () => <ExamplePage extension={this} />,
|
||||
MenuIcon: ExampleIcon,
|
||||
}
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
onDeactivate() {
|
||||
console.log('EXAMPLE EXTENSION RENDERER: DEACTIVATED', this.getMeta());
|
||||
}
|
||||
}
|
||||
27
extensions/example-extension/tsconfig.json
Normal file
27
extensions/example-extension/tsconfig.json
Normal file
@ -0,0 +1,27 @@
|
||||
{
|
||||
"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": [
|
||||
"../../src/extensions/npm/**/*.d.ts",
|
||||
"./*.ts",
|
||||
"./*.tsx"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules",
|
||||
"*.js"
|
||||
]
|
||||
}
|
||||
65
extensions/example-extension/webpack.config.js
Normal file
65
extensions/example-extension/webpack.config.js
Normal 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'),
|
||||
},
|
||||
},
|
||||
];
|
||||
5
extensions/metrics-cluster-feature/Makefile
Normal file
5
extensions/metrics-cluster-feature/Makefile
Normal file
@ -0,0 +1,5 @@
|
||||
install-deps:
|
||||
npm install
|
||||
|
||||
build: install-deps
|
||||
npm run build
|
||||
3570
extensions/metrics-cluster-feature/package-lock.json
generated
Normal file
3570
extensions/metrics-cluster-feature/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
24
extensions/metrics-cluster-feature/package.json
Normal file
24
extensions/metrics-cluster-feature/package.json
Normal file
@ -0,0 +1,24 @@
|
||||
{
|
||||
"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": {
|
||||
"ts-loader": "^8.0.4",
|
||||
"typescript": "^4.0.3",
|
||||
"webpack": "^4.44.2",
|
||||
"mobx": "^5.15.5",
|
||||
"react": "^16.13.1"
|
||||
}
|
||||
}
|
||||
25
extensions/metrics-cluster-feature/renderer.tsx
Normal file
25
extensions/metrics-cluster-feature/renderer.tsx
Normal file
@ -0,0 +1,25 @@
|
||||
import { Registry, LensRendererExtension } from "@k8slens/extensions"
|
||||
import { MetricsFeature } from "./src/metrics-feature"
|
||||
import React from "react"
|
||||
|
||||
export default class ClusterMetricsFeatureExtension extends LensRendererExtension {
|
||||
registerClusterFeatures(registry: Registry.ClusterFeatureRegistry) {
|
||||
this.disposers.push(
|
||||
registry.add({
|
||||
title: "Metrics Stack",
|
||||
components: {
|
||||
Description: () => {
|
||||
return (
|
||||
<span>
|
||||
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()
|
||||
})
|
||||
)
|
||||
}
|
||||
}
|
||||
96
extensions/metrics-cluster-feature/src/metrics-feature.ts
Normal file
96
extensions/metrics-cluster-feature/src/metrics-feature.ts
Normal 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"}) }
|
||||
}
|
||||
27
extensions/metrics-cluster-feature/tsconfig.json
Normal file
27
extensions/metrics-cluster-feature/tsconfig.json
Normal file
@ -0,0 +1,27 @@
|
||||
{
|
||||
"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": [
|
||||
"../../src/extensions/npm/**/*.d.ts",
|
||||
"./*.ts",
|
||||
"./*.tsx"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules",
|
||||
"*.js"
|
||||
]
|
||||
}
|
||||
38
extensions/metrics-cluster-feature/webpack.config.js
Normal file
38
extensions/metrics-cluster-feature/webpack.config.js
Normal 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
|
||||
}
|
||||
},
|
||||
];
|
||||
5
extensions/node-menu/Makefile
Normal file
5
extensions/node-menu/Makefile
Normal file
@ -0,0 +1,5 @@
|
||||
install-deps:
|
||||
npm install
|
||||
|
||||
build: install-deps
|
||||
npm run build
|
||||
3508
extensions/node-menu/package-lock.json
generated
Normal file
3508
extensions/node-menu/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
22
extensions/node-menu/package.json
Normal file
22
extensions/node-menu/package.json
Normal file
@ -0,0 +1,22 @@
|
||||
{
|
||||
"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": {
|
||||
"ts-loader": "^8.0.4",
|
||||
"typescript": "^4.0.3",
|
||||
"webpack": "^4.44.2",
|
||||
"mobx": "^5.15.5",
|
||||
"react": "^16.13.1"
|
||||
}
|
||||
}
|
||||
21
extensions/node-menu/renderer.tsx
Normal file
21
extensions/node-menu/renderer.tsx
Normal file
@ -0,0 +1,21 @@
|
||||
import { Registry, LensRendererExtension } from "@k8slens/extensions";
|
||||
import React from "react"
|
||||
import { NodeMenu } from "./src/node-menu"
|
||||
|
||||
export default class NodeMenuRendererExtension extends LensRendererExtension {
|
||||
async onActivate() {
|
||||
console.log("node-menu extension activated")
|
||||
}
|
||||
|
||||
registerKubeObjectMenus(registry: Registry.KubeObjectMenuRegistry) {
|
||||
this.disposers.push(
|
||||
registry.add({
|
||||
kind: "Node",
|
||||
apiVersions: ["v1"],
|
||||
components: {
|
||||
MenuItem: (props) => <NodeMenu {...props} />
|
||||
}
|
||||
})
|
||||
)
|
||||
}
|
||||
}
|
||||
70
extensions/node-menu/src/node-menu.tsx
Normal file
70
extensions/node-menu/src/node-menu.tsx
Normal file
@ -0,0 +1,70 @@
|
||||
import React from "react";
|
||||
import { Component, K8sApi, Navigation} from "@k8slens/extensions"
|
||||
|
||||
export function NodeMenu(props: Component.KubeObjectMenuProps<K8sApi.Node>) {
|
||||
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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
27
extensions/node-menu/tsconfig.json
Normal file
27
extensions/node-menu/tsconfig.json
Normal file
@ -0,0 +1,27 @@
|
||||
{
|
||||
"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": [
|
||||
"../../src/extensions/npm/**/*.d.ts",
|
||||
"./*.ts",
|
||||
"./*.tsx"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules",
|
||||
"*.js"
|
||||
]
|
||||
}
|
||||
35
extensions/node-menu/webpack.config.js
Normal file
35
extensions/node-menu/webpack.config.js
Normal 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'),
|
||||
},
|
||||
},
|
||||
];
|
||||
5
extensions/pod-menu/Makefile
Normal file
5
extensions/pod-menu/Makefile
Normal file
@ -0,0 +1,5 @@
|
||||
install-deps:
|
||||
npm install
|
||||
|
||||
build: install-deps
|
||||
npm run build
|
||||
3508
extensions/pod-menu/package-lock.json
generated
Normal file
3508
extensions/pod-menu/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
22
extensions/pod-menu/package.json
Normal file
22
extensions/pod-menu/package.json
Normal file
@ -0,0 +1,22 @@
|
||||
{
|
||||
"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"
|
||||
}
|
||||
}
|
||||
31
extensions/pod-menu/renderer.tsx
Normal file
31
extensions/pod-menu/renderer.tsx
Normal file
@ -0,0 +1,31 @@
|
||||
import { Registry, LensRendererExtension } from "@k8slens/extensions";
|
||||
import { PodShellMenu } from "./src/shell-menu"
|
||||
import { PodLogsMenu } from "./src/logs-menu"
|
||||
import React from "react"
|
||||
|
||||
export default class PodMenuRendererExtension extends LensRendererExtension {
|
||||
async onActivate() {
|
||||
console.log("pod-menu extension activated")
|
||||
}
|
||||
|
||||
registerKubeObjectMenus(registry: Registry.KubeObjectMenuRegistry) {
|
||||
this.disposers.push(
|
||||
registry.add({
|
||||
kind: "Pod",
|
||||
apiVersions: ["v1"],
|
||||
components: {
|
||||
MenuItem: (props) => <PodShellMenu {...props} />
|
||||
}
|
||||
})
|
||||
)
|
||||
this.disposers.push(
|
||||
registry.add({
|
||||
kind: "Pod",
|
||||
apiVersions: ["v1"],
|
||||
components: {
|
||||
MenuItem: (props) => <PodLogsMenu {...props} />
|
||||
}
|
||||
})
|
||||
)
|
||||
}
|
||||
}
|
||||
57
extensions/pod-menu/src/logs-menu.tsx
Normal file
57
extensions/pod-menu/src/logs-menu.tsx
Normal file
@ -0,0 +1,57 @@
|
||||
import React from "react";
|
||||
import { Component, K8sApi, Util, Navigation } from "@k8slens/extensions";
|
||||
|
||||
interface Props extends Component.KubeObjectMenuProps<K8sApi.Pod> {
|
||||
}
|
||||
|
||||
export class PodLogsMenu extends React.Component<Props> {
|
||||
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>
|
||||
)
|
||||
}
|
||||
}
|
||||
63
extensions/pod-menu/src/shell-menu.tsx
Normal file
63
extensions/pod-menu/src/shell-menu.tsx
Normal file
@ -0,0 +1,63 @@
|
||||
|
||||
|
||||
import React from "react";
|
||||
import { Component, K8sApi, Util, Navigation } from "@k8slens/extensions";
|
||||
|
||||
interface Props extends Component.KubeObjectMenuProps<K8sApi.Pod> {
|
||||
}
|
||||
|
||||
export class PodShellMenu extends React.Component<Props> {
|
||||
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>
|
||||
)
|
||||
}
|
||||
}
|
||||
27
extensions/pod-menu/tsconfig.json
Normal file
27
extensions/pod-menu/tsconfig.json
Normal file
@ -0,0 +1,27 @@
|
||||
{
|
||||
"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": [
|
||||
"../../src/extensions/npm/**/*.d.ts",
|
||||
"./*.ts",
|
||||
"./*.tsx"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules",
|
||||
"*.js"
|
||||
]
|
||||
}
|
||||
35
extensions/pod-menu/webpack.config.js
Normal file
35
extensions/pod-menu/webpack.config.js
Normal 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'),
|
||||
},
|
||||
},
|
||||
];
|
||||
5
extensions/support-page/Makefile
Normal file
5
extensions/support-page/Makefile
Normal file
@ -0,0 +1,5 @@
|
||||
install-deps:
|
||||
npm install
|
||||
|
||||
build: install-deps
|
||||
npm run build
|
||||
23
extensions/support-page/main.ts
Normal file
23
extensions/support-page/main.ts
Normal file
@ -0,0 +1,23 @@
|
||||
import { LensMainExtension, Registry, windowManager } from "@k8slens/extensions";
|
||||
import { supportPageURL } from "./src/support.route";
|
||||
|
||||
export default class SupportPageMainExtension extends LensMainExtension {
|
||||
async onActivate() {
|
||||
console.log("support page extension activated")
|
||||
}
|
||||
|
||||
async registerAppMenus(registry: Registry.MenuRegistry) {
|
||||
this.disposers.push(
|
||||
registry.add({
|
||||
parentId: "help",
|
||||
label: "Support",
|
||||
click() {
|
||||
windowManager.navigate({
|
||||
channel: "menu:navigate", // fixme: use windowManager.ensureMainWindow from Tray's PR
|
||||
url: supportPageURL(),
|
||||
});
|
||||
}
|
||||
})
|
||||
)
|
||||
}
|
||||
}
|
||||
3623
extensions/support-page/package-lock.json
generated
Normal file
3623
extensions/support-page/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
24
extensions/support-page/package.json
Normal file
24
extensions/support-page/package.json
Normal file
@ -0,0 +1,24 @@
|
||||
{
|
||||
"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": {
|
||||
"@types/node": "^14.11.11",
|
||||
"@types/react": "^16.9.53",
|
||||
"@types/react-router": "^5.1.8",
|
||||
"@types/webpack": "^4.41.17",
|
||||
"mobx": "^5.15.5",
|
||||
"react": "^16.13.1",
|
||||
"ts-loader": "^8.0.4",
|
||||
"ts-node": "^9.0.0",
|
||||
"typescript": "^4.0.3",
|
||||
"webpack": "^4.44.2"
|
||||
}
|
||||
}
|
||||
39
extensions/support-page/renderer.tsx
Normal file
39
extensions/support-page/renderer.tsx
Normal file
@ -0,0 +1,39 @@
|
||||
import React from "react";
|
||||
import { Component, LensRendererExtension, Navigation, Registry } from "@k8slens/extensions";
|
||||
import { supportPageRoute, supportPageURL } from "./src/support.route";
|
||||
import { Support } from "./src/support";
|
||||
|
||||
export default class SupportPageRendererExtension extends LensRendererExtension {
|
||||
async onActivate() {
|
||||
console.log("support page extension activated")
|
||||
}
|
||||
|
||||
registerGlobalPage(registry: Registry.GlobalPageRegistry) {
|
||||
this.disposers.push(
|
||||
registry.add({
|
||||
...supportPageRoute,
|
||||
url: supportPageURL(),
|
||||
hideInMenu: true,
|
||||
components: {
|
||||
Page: Support,
|
||||
}
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
registerStatusBarItem(registry: Registry.StatusBarRegistry) {
|
||||
this.disposers.push(
|
||||
registry.add({
|
||||
item: (
|
||||
<div
|
||||
className="flex align-center gaps hover-highlight"
|
||||
onClick={() => Navigation.navigate(supportPageURL())}
|
||||
>
|
||||
<Component.Icon material="help_outline" small />
|
||||
<span>Support</span>
|
||||
</div>
|
||||
)
|
||||
})
|
||||
)
|
||||
}
|
||||
}
|
||||
7
extensions/support-page/src/support.route.ts
Normal file
7
extensions/support-page/src/support.route.ts
Normal file
@ -0,0 +1,7 @@
|
||||
import type { RouteProps } from "react-router";
|
||||
|
||||
export const supportPageRoute: RouteProps = {
|
||||
path: "/support"
|
||||
}
|
||||
|
||||
export const supportPageURL = () => supportPageRoute.path.toString();
|
||||
29
extensions/support-page/src/support.tsx
Normal file
29
extensions/support-page/src/support.tsx
Normal file
@ -0,0 +1,29 @@
|
||||
// TODO: figure out how to consume styles / handle import "./support.scss"
|
||||
// TODO: support localization / figure out how to extract / consume i18n strings
|
||||
|
||||
import React from "react"
|
||||
import { observer } from "mobx-react"
|
||||
import { CommonVars, Component } from "@k8slens/extensions";
|
||||
|
||||
@observer
|
||||
export class Support extends React.Component {
|
||||
render() {
|
||||
const { PageLayout } = Component;
|
||||
const { slackUrl, issuesTrackerUrl } = CommonVars;
|
||||
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>
|
||||
);
|
||||
}
|
||||
}
|
||||
30
extensions/support-page/tsconfig.json
Normal file
30
extensions/support-page/tsconfig.json
Normal file
@ -0,0 +1,30 @@
|
||||
{
|
||||
"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/extensions/npm/**/*.d.ts",
|
||||
"src/**/*"
|
||||
]
|
||||
}
|
||||
68
extensions/support-page/webpack.config.ts
Normal file
68
extensions/support-page/webpack.config.ts
Normal file
@ -0,0 +1,68 @@
|
||||
import path from "path"
|
||||
|
||||
const outputPath = path.resolve(__dirname, 'dist');
|
||||
|
||||
// TODO: figure out how to share base TS and Webpack configs from Lens (npm, filesystem, etc?)
|
||||
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/,
|
||||
},
|
||||
],
|
||||
},
|
||||
externals: [
|
||||
lensExternals,
|
||||
],
|
||||
resolve: {
|
||||
extensions: ['.tsx', '.ts', '.js'],
|
||||
},
|
||||
output: {
|
||||
libraryTarget: "commonjs2",
|
||||
globalObject: "this",
|
||||
filename: 'renderer.js',
|
||||
path: outputPath,
|
||||
},
|
||||
},
|
||||
];
|
||||
5
extensions/telemetry/Makefile
Normal file
5
extensions/telemetry/Makefile
Normal file
@ -0,0 +1,5 @@
|
||||
install-deps:
|
||||
npm install
|
||||
|
||||
build: install-deps
|
||||
npm run build
|
||||
17
extensions/telemetry/main.ts
Normal file
17
extensions/telemetry/main.ts
Normal 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")
|
||||
}
|
||||
}
|
||||
3857
extensions/telemetry/package-lock.json
generated
Normal file
3857
extensions/telemetry/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
25
extensions/telemetry/package.json
Normal file
25
extensions/telemetry/package.json
Normal file
@ -0,0 +1,25 @@
|
||||
{
|
||||
"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": {
|
||||
"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"
|
||||
}
|
||||
}
|
||||
29
extensions/telemetry/renderer.tsx
Normal file
29
extensions/telemetry/renderer.tsx
Normal file
@ -0,0 +1,29 @@
|
||||
import { LensRendererExtension, Registry } 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 {
|
||||
async onActivate() {
|
||||
console.log("telemetry extension activated")
|
||||
tracker.start()
|
||||
await telemetryPreferencesStore.loadExtension(this)
|
||||
}
|
||||
|
||||
registerAppPreferences(registry: Registry.AppPreferenceRegistry) {
|
||||
this.disposers.push(
|
||||
registry.add({
|
||||
title: "Telemetry & Usage Tracking",
|
||||
components: {
|
||||
Hint: () => <TelemetryPreferenceHint />,
|
||||
Input: () => <TelemetryPreferenceInput telemetry={telemetryPreferencesStore} />
|
||||
}
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
onDeactivate() {
|
||||
console.log("telemetry extension deactivated")
|
||||
}
|
||||
}
|
||||
26
extensions/telemetry/src/telemetry-preference.tsx
Normal file
26
extensions/telemetry/src/telemetry-preference.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
}
|
||||
35
extensions/telemetry/src/telemetry-preferences-store.ts
Normal file
35
extensions/telemetry/src/telemetry-preferences-store.ts
Normal 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>()
|
||||
72
extensions/telemetry/src/tracker.ts
Normal file
72
extensions/telemetry/src/tracker.ts
Normal file
@ -0,0 +1,72 @@
|
||||
import { EventBus, Util } 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;
|
||||
|
||||
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")
|
||||
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
stop() {
|
||||
if (!this.started) { return }
|
||||
|
||||
this.started = false
|
||||
|
||||
for (const handler of this.eventHandlers) {
|
||||
EventBus.appEventBus.removeListener(handler)
|
||||
}
|
||||
}
|
||||
|
||||
protected async isTelemetryAllowed(): Promise<boolean> {
|
||||
return telemetryPreferencesStore.enabled
|
||||
}
|
||||
|
||||
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>();
|
||||
30
extensions/telemetry/tsconfig.json
Normal file
30
extensions/telemetry/tsconfig.json
Normal file
@ -0,0 +1,30 @@
|
||||
{
|
||||
"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/extensions/npm/**/*.d.ts",
|
||||
"src/**/*"
|
||||
]
|
||||
}
|
||||
67
extensions/telemetry/webpack.config.js
Normal file
67
extensions/telemetry/webpack.config.js
Normal 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'),
|
||||
},
|
||||
},
|
||||
];
|
||||
75
package.json
75
package.json
@ -11,14 +11,17 @@
|
||||
"email": "info@k8slens.dev"
|
||||
},
|
||||
"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:main": "yarn compile:main --watch",
|
||||
"dev:renderer": "yarn compile:renderer --watch",
|
||||
"dev:extension-rollup": "yarn compile:extension-rollup --watch",
|
||||
"compile": "env NODE_ENV=production concurrently yarn:compile:*",
|
||||
"compile:main": "webpack --config webpack.main.ts",
|
||||
"compile:renderer": "webpack --config webpack.renderer.ts",
|
||||
"compile:i18n": "lingui compile",
|
||||
"compile:extension-rollup": "rollup --config src/extensions/rollup.config.js",
|
||||
"build:linux": "yarn compile && electron-builder --linux --dir -c.productName=Lens",
|
||||
"build:mac": "yarn compile && electron-builder --mac --dir -c.productName=Lens",
|
||||
"build:win": "yarn compile && electron-builder --win --dir -c.productName=Lens",
|
||||
@ -32,8 +35,7 @@
|
||||
"download-bins": "concurrently yarn:download:*",
|
||||
"download:kubectl": "yarn run ts-node build/download_kubectl.ts",
|
||||
"download:helm": "yarn run ts-node build/download_helm.ts",
|
||||
"lint": "eslint $@ --ext js,ts,tsx --max-warnings=0 src/",
|
||||
"rebuild-pty": "yarn run electron-rebuild -f -w node-pty"
|
||||
"lint": "eslint $@ --ext js,ts,tsx --max-warnings=0 src/"
|
||||
},
|
||||
"config": {
|
||||
"bundledKubectlVersion": "1.17.11",
|
||||
@ -68,9 +70,15 @@
|
||||
},
|
||||
"moduleNameMapper": {
|
||||
"\\.(css|scss)$": "<rootDir>/__mocks__/styleMock.ts"
|
||||
}
|
||||
},
|
||||
"modulePathIgnorePatterns": [
|
||||
"<rootDir>/dist"
|
||||
]
|
||||
},
|
||||
"build": {
|
||||
"files": [
|
||||
"static/build/main.js"
|
||||
],
|
||||
"afterSign": "build/notarize.js",
|
||||
"extraResources": [
|
||||
{
|
||||
@ -88,6 +96,14 @@
|
||||
"to": "static/",
|
||||
"filter": "!**/main.js"
|
||||
},
|
||||
{
|
||||
"from": "extensions/",
|
||||
"to": "./extensions/",
|
||||
"filter": [
|
||||
"**/*.js*",
|
||||
"!**/node_modules"
|
||||
]
|
||||
},
|
||||
"LICENSE"
|
||||
],
|
||||
"linux": {
|
||||
@ -156,23 +172,19 @@
|
||||
"confinement": "classic"
|
||||
}
|
||||
},
|
||||
"lens": {
|
||||
"extensions": [
|
||||
"telemetry",
|
||||
"pod-menu",
|
||||
"node-menu",
|
||||
"metrics-cluster-feature",
|
||||
"support-page"
|
||||
]
|
||||
},
|
||||
"dependencies": {
|
||||
"@hapi/call": "^8.0.0",
|
||||
"@hapi/subtext": "^7.0.3",
|
||||
"@kubernetes/client-node": "^0.12.0",
|
||||
"@types/crypto-js": "^3.1.47",
|
||||
"@types/electron-window-state": "^2.0.34",
|
||||
"@types/fs-extra": "^9.0.1",
|
||||
"@types/http-proxy": "^1.17.4",
|
||||
"@types/js-yaml": "^3.12.4",
|
||||
"@types/jsonpath": "^0.2.0",
|
||||
"@types/lodash": "^4.14.155",
|
||||
"@types/marked": "^0.7.4",
|
||||
"@types/mock-fs": "^4.10.0",
|
||||
"@types/node": "^12.12.45",
|
||||
"@types/proper-lockfile": "^4.1.1",
|
||||
"@types/react-beautiful-dnd": "^13.0.0",
|
||||
"@types/tar": "^4.0.3",
|
||||
"array-move": "^3.0.0",
|
||||
"chalk": "^4.1.0",
|
||||
"command-exists": "1.2.9",
|
||||
@ -195,13 +207,11 @@
|
||||
"mobx": "^5.15.5",
|
||||
"mobx-observable-history": "^1.0.3",
|
||||
"mock-fs": "^4.12.0",
|
||||
"node-machine-id": "^1.1.12",
|
||||
"node-pty": "^0.9.0",
|
||||
"npm": "^6.14.8",
|
||||
"openid-client": "^3.15.2",
|
||||
"path-to-regexp": "^6.1.0",
|
||||
"proper-lockfile": "^4.1.1",
|
||||
"react-beautiful-dnd": "^13.0.0",
|
||||
"react-router": "^5.2.0",
|
||||
"request": "^2.88.2",
|
||||
"request-promise-native": "^1.0.8",
|
||||
"semver": "^7.3.2",
|
||||
@ -211,7 +221,6 @@
|
||||
"tar": "^6.0.2",
|
||||
"tcp-port-used": "^1.0.1",
|
||||
"tempy": "^0.5.0",
|
||||
"universal-analytics": "^0.4.20",
|
||||
"uuid": "^8.1.0",
|
||||
"win-ca": "^3.2.0",
|
||||
"winston": "^3.2.1",
|
||||
@ -232,19 +241,34 @@
|
||||
"@lingui/macro": "^3.0.0-13",
|
||||
"@lingui/react": "^3.0.0-13",
|
||||
"@material-ui/core": "^4.10.1",
|
||||
"@rollup/plugin-json": "^4.1.0",
|
||||
"@types/chart.js": "^2.9.21",
|
||||
"@types/circular-dependency-plugin": "^5.0.1",
|
||||
"@types/color": "^3.0.1",
|
||||
"@types/crypto-js": "^3.1.47",
|
||||
"@types/dompurify": "^2.0.2",
|
||||
"@types/electron-window-state": "^2.0.34",
|
||||
"@types/fs-extra": "^9.0.1",
|
||||
"@types/hapi": "^18.0.3",
|
||||
"@types/hoist-non-react-statics": "^3.3.1",
|
||||
"@types/html-webpack-plugin": "^3.2.3",
|
||||
"@types/http-proxy": "^1.17.4",
|
||||
"@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/md5-file": "^4.0.2",
|
||||
"@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/proper-lockfile": "^4.1.1",
|
||||
"@types/react": "^16.9.35",
|
||||
"@types/react-beautiful-dnd": "^13.0.0",
|
||||
"@types/react-router-dom": "^5.1.5",
|
||||
"@types/react-select": "^3.0.13",
|
||||
"@types/react-window": "^1.8.2",
|
||||
@ -253,6 +277,7 @@
|
||||
"@types/semver": "^7.2.0",
|
||||
"@types/shelljs": "^0.8.8",
|
||||
"@types/spdy": "^3.4.4",
|
||||
"@types/tar": "^4.0.3",
|
||||
"@types/tcp-port-used": "^1.0.0",
|
||||
"@types/tempy": "^0.3.0",
|
||||
"@types/terser-webpack-plugin": "^3.0.0",
|
||||
@ -280,7 +305,6 @@
|
||||
"electron": "^9.1.2",
|
||||
"electron-builder": "^22.7.0",
|
||||
"electron-notarize": "^0.3.0",
|
||||
"electron-rebuild": "^1.11.0",
|
||||
"eslint": "^7.7.0",
|
||||
"file-loader": "^6.0.0",
|
||||
"flex.box": "^3.4.4",
|
||||
@ -292,7 +316,6 @@
|
||||
"jest": "^26.0.1",
|
||||
"jest-mock-extended": "^1.0.10",
|
||||
"make-plural": "^6.2.1",
|
||||
"material-design-icons": "^3.0.1",
|
||||
"mini-css-extract-plugin": "^0.9.0",
|
||||
"mobx-react": "^6.2.2",
|
||||
"moment": "^2.26.0",
|
||||
@ -304,10 +327,16 @@
|
||||
"progress-bar-webpack-plugin": "^2.1.0",
|
||||
"raw-loader": "^4.0.1",
|
||||
"react": "^16.13.1",
|
||||
"react-beautiful-dnd": "^13.0.0",
|
||||
"react-dom": "^16.13.1",
|
||||
"react-router": "^5.2.0",
|
||||
"react-router-dom": "^5.2.0",
|
||||
"react-select": "^3.1.0",
|
||||
"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",
|
||||
"spectron": "11.0.0",
|
||||
"style-loader": "^1.2.1",
|
||||
|
||||
15
src/common/__tests__/event-bus.test.ts
Normal file
15
src/common/__tests__/event-bus.test.ts
Normal 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")
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -56,13 +56,17 @@ export class BaseStore<T = any> extends Singleton {
|
||||
...confOptions,
|
||||
projectName: "lens",
|
||||
projectVersion: getAppVersion(),
|
||||
cwd: (app || remote.app).getPath("userData"),
|
||||
cwd: this.storePath(),
|
||||
});
|
||||
logger.info(`[STORE]: LOADED from ${this.storeConfig.path}`);
|
||||
this.fromStore(this.storeConfig.store);
|
||||
this.isLoaded = true;
|
||||
}
|
||||
|
||||
protected storePath() {
|
||||
return (app || remote.app).getPath("userData")
|
||||
}
|
||||
|
||||
protected async saveToFile(model: T) {
|
||||
logger.info(`[STORE]: SAVING ${this.name}`);
|
||||
// todo: update when fixed https://github.com/sindresorhus/conf/issues/114
|
||||
@ -86,13 +90,19 @@ export class BaseStore<T = any> extends Singleton {
|
||||
if (ipcRenderer) {
|
||||
const callback = (event: IpcRendererEvent, model: T) => {
|
||||
logger.silly(`[STORE]: SYNC ${this.name} from main`, { model });
|
||||
this.onSync(model);
|
||||
this.onSyncFromMain(model);
|
||||
};
|
||||
ipcRenderer.on(this.syncChannel, callback);
|
||||
this.syncDisposers.push(() => ipcRenderer.off(this.syncChannel, callback));
|
||||
}
|
||||
}
|
||||
|
||||
protected onSyncFromMain(model: T) {
|
||||
this.applyWithoutSync(() => {
|
||||
this.onSync(model)
|
||||
})
|
||||
}
|
||||
|
||||
unregisterIpcListener() {
|
||||
ipcRenderer.removeAllListeners(this.syncChannel)
|
||||
}
|
||||
|
||||
@ -1,6 +1,8 @@
|
||||
import { createIpcChannel } from "./ipc";
|
||||
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 = {
|
||||
activate: createIpcChannel({
|
||||
@ -19,6 +21,7 @@ export const clusterIpc = {
|
||||
const cluster = clusterStore.getById(clusterId);
|
||||
if (cluster) {
|
||||
if (frameId) cluster.frameId = frameId; // save cluster's webFrame.routingId to be able to send push-updates
|
||||
extensionLoader.broadcastExtensions(frameId)
|
||||
return cluster.pushState();
|
||||
}
|
||||
},
|
||||
@ -28,44 +31,29 @@ export const clusterIpc = {
|
||||
channel: "cluster:refresh",
|
||||
handle: (clusterId: ClusterId) => {
|
||||
const cluster = clusterStore.getById(clusterId);
|
||||
if (cluster) return cluster.refresh();
|
||||
if (cluster) return cluster.refresh({ refreshMetadata: true })
|
||||
},
|
||||
}),
|
||||
|
||||
disconnect: createIpcChannel({
|
||||
channel: "cluster:disconnect",
|
||||
handle: (clusterId: ClusterId) => {
|
||||
tracker.event("cluster", "stop");
|
||||
appEventBus.emit({name: "cluster", action: "stop"});
|
||||
return clusterStore.getById(clusterId)?.disconnect();
|
||||
},
|
||||
}),
|
||||
|
||||
installFeature: createIpcChannel({
|
||||
channel: "cluster:install-feature",
|
||||
handle: async (clusterId: ClusterId, feature: string, config?: any) => {
|
||||
tracker.event("cluster", "install", feature);
|
||||
kubectlApplyAll: createIpcChannel({
|
||||
channel: "cluster:kubectl-apply-all",
|
||||
handle: (clusterId: ClusterId, resources: string[]) => {
|
||||
appEventBus.emit({name: "cluster", action: "kubectl-apply-all"})
|
||||
const cluster = clusterStore.getById(clusterId);
|
||||
if (cluster) {
|
||||
await cluster.installFeature(feature, config)
|
||||
const applier = new ResourceApplier(cluster)
|
||||
applier.kubectlApplyAll(resources)
|
||||
} else {
|
||||
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)
|
||||
}
|
||||
}),
|
||||
}
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import type { WorkspaceId } from "./workspace-store";
|
||||
import path from "path";
|
||||
import { app, ipcRenderer, remote, webFrame, webContents } from "electron";
|
||||
import { unlink } from "fs-extra";
|
||||
@ -6,13 +7,12 @@ import { BaseStore } from "./base-store";
|
||||
import { Cluster, ClusterState } from "../main/cluster";
|
||||
import migrations from "../migrations/cluster-store"
|
||||
import logger from "../main/logger";
|
||||
import { tracker } from "./tracker";
|
||||
import { appEventBus } from "./event-bus"
|
||||
import { dumpConfigYaml } from "./kube-helpers";
|
||||
import { saveToAppFiles } from "./utils/saveToAppFiles";
|
||||
import { KubeConfig } from "@kubernetes/client-node";
|
||||
import _ from "lodash";
|
||||
import move from "array-move";
|
||||
import type { WorkspaceId } from "./workspace-store";
|
||||
|
||||
export interface ClusterIconUpload {
|
||||
clusterId: string;
|
||||
@ -20,6 +20,10 @@ export interface ClusterIconUpload {
|
||||
path: string;
|
||||
}
|
||||
|
||||
export interface ClusterMetadata {
|
||||
[key: string]: string | number | boolean;
|
||||
}
|
||||
|
||||
export interface ClusterStoreModel {
|
||||
activeCluster?: ClusterId; // last opened cluster
|
||||
clusters?: ClusterModel[]
|
||||
@ -32,6 +36,7 @@ export interface ClusterModel {
|
||||
workspace?: WorkspaceId;
|
||||
contextName?: string;
|
||||
preferences?: ClusterPreferences;
|
||||
metadata?: ClusterMetadata;
|
||||
kubeConfigPath: string;
|
||||
|
||||
/** @deprecated */
|
||||
@ -142,7 +147,7 @@ export class ClusterStore extends BaseStore<ClusterStoreModel> {
|
||||
@action
|
||||
addCluster(...models: ClusterModel[]) {
|
||||
models.forEach(model => {
|
||||
tracker.event("cluster", "add");
|
||||
appEventBus.emit({name: "cluster", action: "add"})
|
||||
const cluster = new Cluster(model);
|
||||
this.clusters.set(model.id, cluster);
|
||||
})
|
||||
@ -150,7 +155,7 @@ export class ClusterStore extends BaseStore<ClusterStoreModel> {
|
||||
|
||||
@action
|
||||
async removeById(clusterId: ClusterId) {
|
||||
tracker.event("cluster", "remove");
|
||||
appEventBus.emit({name: "cluster", action: "remove"})
|
||||
const cluster = this.getById(clusterId);
|
||||
if (cluster) {
|
||||
this.clusters.delete(clusterId);
|
||||
|
||||
9
src/common/event-bus.ts
Normal file
9
src/common/event-bus.ts
Normal 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]>()
|
||||
@ -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);
|
||||
@ -7,7 +7,7 @@ import { BaseStore } from "./base-store";
|
||||
import migrations from "../migrations/user-store"
|
||||
import { getAppVersion } from "./utils/app-version";
|
||||
import { kubeConfigDefaultPath, loadConfig } from "./kube-helpers";
|
||||
import { tracker } from "./tracker";
|
||||
import { appEventBus } from "./event-bus"
|
||||
import logger from "../main/logger";
|
||||
import path from 'path';
|
||||
|
||||
@ -40,7 +40,7 @@ export class UserStore extends BaseStore<UserStoreModel> {
|
||||
|
||||
// track telemetry availability
|
||||
reaction(() => this.preferences.allowTelemetry, allowed => {
|
||||
tracker.event("telemetry", allowed ? "enabled" : "disabled");
|
||||
appEventBus.emit({name: "telemetry", action: allowed ? "enabled" : "disabled"})
|
||||
});
|
||||
|
||||
// refresh new contexts
|
||||
@ -77,7 +77,7 @@ export class UserStore extends BaseStore<UserStoreModel> {
|
||||
|
||||
@action
|
||||
saveLastSeenAppVersion() {
|
||||
tracker.event("app", "whats-new-seen")
|
||||
appEventBus.emit({name: "app", action: "whats-new-seen"})
|
||||
this.lastSeenAppVersion = getAppVersion();
|
||||
}
|
||||
|
||||
|
||||
@ -7,3 +7,7 @@ export function getAppVersion(): string {
|
||||
export function getBundledKubectlVersion(): string {
|
||||
return packageInfo.config.bundledKubectlVersion;
|
||||
}
|
||||
|
||||
export function getBundledExtensions(): string[] {
|
||||
return packageInfo.lens?.extensions || []
|
||||
}
|
||||
|
||||
62
src/extensions/cluster-feature.ts
Normal file
62
src/extensions/cluster-feature.ts
Normal file
@ -0,0 +1,62 @@
|
||||
import fs from "fs";
|
||||
import path from "path"
|
||||
import hb from "handlebars"
|
||||
import { observable } from "mobx"
|
||||
import { ResourceApplier } from "../main/resource-applier"
|
||||
import { Cluster } from "../main/cluster";
|
||||
import logger from "../main/logger";
|
||||
import { app } from "electron"
|
||||
import { clusterIpc } from "../common/cluster-ipc"
|
||||
|
||||
export interface ClusterFeatureStatus {
|
||||
currentVersion: string;
|
||||
installed: boolean;
|
||||
latestVersion: string;
|
||||
canUpgrade: boolean;
|
||||
}
|
||||
|
||||
export abstract class ClusterFeature {
|
||||
name: string;
|
||||
latestVersion: string;
|
||||
config: any;
|
||||
|
||||
@observable status: ClusterFeatureStatus = {
|
||||
currentVersion: null,
|
||||
installed: false,
|
||||
latestVersion: null,
|
||||
canUpgrade: false
|
||||
}
|
||||
|
||||
abstract async install(cluster: Cluster): Promise<void>;
|
||||
|
||||
abstract async upgrade(cluster: Cluster): Promise<void>;
|
||||
|
||||
abstract async uninstall(cluster: Cluster): Promise<void>;
|
||||
|
||||
abstract async updateStatus(cluster: Cluster): Promise<ClusterFeatureStatus>;
|
||||
|
||||
protected async applyResources(cluster: Cluster, resources: string[]) {
|
||||
if (app) {
|
||||
await new ResourceApplier(cluster).kubectlApplyAll(resources)
|
||||
} else {
|
||||
await clusterIpc.kubectlApplyAll.invokeFromRenderer(cluster.id, resources)
|
||||
}
|
||||
}
|
||||
|
||||
protected renderTemplates(folderPath: string): string[] {
|
||||
const resources: string[] = [];
|
||||
logger.info(`[FEATURE]: render templates from ${folderPath}`);
|
||||
fs.readdirSync(folderPath).forEach(filename => {
|
||||
const file = path.join(folderPath, filename);
|
||||
const raw = fs.readFileSync(file);
|
||||
if (filename.endsWith('.hb')) {
|
||||
const template = hb.compile(raw.toString());
|
||||
resources.push(template(this.config));
|
||||
} else {
|
||||
resources.push(raw.toString());
|
||||
}
|
||||
});
|
||||
|
||||
return resources;
|
||||
}
|
||||
}
|
||||
1
src/extensions/core-api/cluster-feature.ts
Normal file
1
src/extensions/core-api/cluster-feature.ts
Normal file
@ -0,0 +1 @@
|
||||
export { ClusterFeature as Feature, ClusterFeatureStatus as FeatureStatus } from "../cluster-feature"
|
||||
2
src/extensions/core-api/event-bus.ts
Normal file
2
src/extensions/core-api/event-bus.ts
Normal file
@ -0,0 +1,2 @@
|
||||
export { appEventBus } from "../../common/event-bus"
|
||||
export type { AppEvent } from "../../common/event-bus"
|
||||
24
src/extensions/core-api/index.ts
Normal file
24
src/extensions/core-api/index.ts
Normal file
@ -0,0 +1,24 @@
|
||||
// Lens-extensions api developer's kit
|
||||
export * from "../lens-main-extension"
|
||||
export * from "../lens-renderer-extension"
|
||||
|
||||
import type { WindowManager } from "../../main/window-manager";
|
||||
|
||||
// APIs
|
||||
import * as EventBus from "./event-bus"
|
||||
import * as Store from "./stores"
|
||||
import * as Util from "./utils"
|
||||
import * as Registry from "../registries"
|
||||
import * as CommonVars from "../../common/vars";
|
||||
import * as ClusterFeature from "./cluster-feature"
|
||||
|
||||
export let windowManager: WindowManager;
|
||||
|
||||
export {
|
||||
EventBus,
|
||||
ClusterFeature,
|
||||
Store,
|
||||
Util,
|
||||
Registry,
|
||||
CommonVars,
|
||||
}
|
||||
2
src/extensions/core-api/stores.ts
Normal file
2
src/extensions/core-api/stores.ts
Normal file
@ -0,0 +1,2 @@
|
||||
export { ExtensionStore } from "../extension-store"
|
||||
export type { Cluster } from "../../main/cluster"
|
||||
3
src/extensions/core-api/utils.ts
Normal file
3
src/extensions/core-api/utils.ts
Normal file
@ -0,0 +1,3 @@
|
||||
export { Singleton } from "../../common/utils"
|
||||
export { prevDefault, stopPropagation } from "../../renderer/utils/prevDefault"
|
||||
export { cssNames } from "../../renderer/utils/cssNames"
|
||||
15
src/extensions/dynamic-page.tsx
Normal file
15
src/extensions/dynamic-page.tsx
Normal file
@ -0,0 +1,15 @@
|
||||
import React from "react";
|
||||
import { cssNames } from "../renderer/utils";
|
||||
import { TabLayout } from "../renderer/components/layout/tab-layout";
|
||||
import { PageRegistration } from "./registries/page-registry"
|
||||
|
||||
export class DynamicPage extends React.Component<{ page: PageRegistration }> {
|
||||
render() {
|
||||
const { className, components: { Page }, subPages = [] } = this.props.page;
|
||||
return (
|
||||
<TabLayout className={cssNames("ExtensionPage", className)} tabs={subPages}>
|
||||
<Page/>
|
||||
</TabLayout>
|
||||
)
|
||||
}
|
||||
}
|
||||
4
src/extensions/extension-api.ts
Normal file
4
src/extensions/extension-api.ts
Normal file
@ -0,0 +1,4 @@
|
||||
// Extension-api types generation bundle (used by rollup.js)
|
||||
|
||||
export * from "./core-api"
|
||||
export * from "./renderer-api"
|
||||
137
src/extensions/extension-loader.ts
Normal file
137
src/extensions/extension-loader.ts
Normal file
@ -0,0 +1,137 @@
|
||||
import type { ExtensionId, ExtensionManifest, ExtensionModel, LensExtension } from "./lens-extension"
|
||||
import type { LensMainExtension } from "./lens-main-extension"
|
||||
import type { LensRendererExtension } from "./lens-renderer-extension"
|
||||
import path from "path"
|
||||
import { broadcastIpc } from "../common/ipc"
|
||||
import { observable, reaction, toJS, } from "mobx"
|
||||
import logger from "../main/logger"
|
||||
import { app, ipcRenderer, remote } from "electron"
|
||||
import { appPreferenceRegistry, clusterFeatureRegistry, clusterPageRegistry, globalPageRegistry, kubeObjectMenuRegistry, menuRegistry, statusBarRegistry } from "./registries";
|
||||
|
||||
export interface InstalledExtension extends ExtensionModel {
|
||||
manifestPath: string;
|
||||
manifest: ExtensionManifest;
|
||||
}
|
||||
|
||||
// lazy load so that we get correct userData
|
||||
export function extensionPackagesRoot() {
|
||||
return path.join((app || remote.app).getPath("userData"))
|
||||
}
|
||||
|
||||
export class ExtensionLoader {
|
||||
@observable extensions = observable.map<ExtensionId, InstalledExtension>([], { deep: false });
|
||||
@observable instances = observable.map<ExtensionId, LensExtension>([], { deep: false })
|
||||
|
||||
constructor() {
|
||||
if (ipcRenderer) {
|
||||
ipcRenderer.on("extensions:loaded", (event, extensions: InstalledExtension[]) => {
|
||||
extensions.forEach((ext) => {
|
||||
if (!this.getById(ext.manifestPath)) {
|
||||
this.extensions.set(ext.manifestPath, ext)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
loadOnMain() {
|
||||
logger.info('[EXTENSIONS-LOADER]: load on main')
|
||||
this.autoloadExtensions((instance: LensMainExtension) => {
|
||||
instance.registerAppMenus(menuRegistry);
|
||||
})
|
||||
}
|
||||
|
||||
loadOnClusterManagerRenderer() {
|
||||
logger.info('[EXTENSIONS-LOADER]: load on main renderer (cluster manager)')
|
||||
this.autoloadExtensions((instance: LensRendererExtension) => {
|
||||
instance.registerGlobalPage(globalPageRegistry)
|
||||
instance.registerAppPreferences(appPreferenceRegistry)
|
||||
instance.registerClusterFeatures(clusterFeatureRegistry)
|
||||
instance.registerStatusBarItem(statusBarRegistry)
|
||||
})
|
||||
}
|
||||
|
||||
loadOnClusterRenderer() {
|
||||
logger.info('[EXTENSIONS-LOADER]: load on cluster renderer (dashboard)')
|
||||
this.autoloadExtensions((instance: LensRendererExtension) => {
|
||||
instance.registerClusterPage(clusterPageRegistry)
|
||||
instance.registerKubeObjectMenus(kubeObjectMenuRegistry)
|
||||
})
|
||||
}
|
||||
|
||||
protected autoloadExtensions(callback: (instance: LensExtension) => void) {
|
||||
return reaction(() => this.extensions.toJS(), (installedExtensions) => {
|
||||
for(const [id, ext] of installedExtensions) {
|
||||
let instance = this.instances.get(ext.id)
|
||||
if (!instance) {
|
||||
const extensionModule = this.requireExtension(ext)
|
||||
if (!extensionModule) {
|
||||
continue
|
||||
}
|
||||
const LensExtensionClass = extensionModule.default;
|
||||
instance = new LensExtensionClass({ ...ext.manifest, manifestPath: ext.manifestPath, id: ext.manifestPath }, ext.manifest);
|
||||
try {
|
||||
instance.enable()
|
||||
callback(instance)
|
||||
} finally {
|
||||
this.instances.set(ext.id, instance)
|
||||
}
|
||||
}
|
||||
}
|
||||
}, {
|
||||
fireImmediately: true,
|
||||
delay: 0,
|
||||
})
|
||||
}
|
||||
|
||||
protected requireExtension(extension: InstalledExtension) {
|
||||
let extEntrypoint = ""
|
||||
try {
|
||||
if (ipcRenderer && extension.manifest.renderer) {
|
||||
extEntrypoint = path.resolve(path.join(path.dirname(extension.manifestPath), extension.manifest.renderer))
|
||||
} else if (!ipcRenderer && extension.manifest.main) {
|
||||
extEntrypoint = path.resolve(path.join(path.dirname(extension.manifestPath), extension.manifest.main))
|
||||
}
|
||||
if (extEntrypoint !== "") {
|
||||
return __non_webpack_require__(extEntrypoint)
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(`[EXTENSION-LOADER]: can't load extension main at ${extEntrypoint}: ${err}`, { extension });
|
||||
console.trace(err)
|
||||
}
|
||||
}
|
||||
|
||||
getById(id: ExtensionId): InstalledExtension {
|
||||
return this.extensions.get(id);
|
||||
}
|
||||
|
||||
async removeById(id: ExtensionId) {
|
||||
const extension = this.getById(id);
|
||||
if (extension) {
|
||||
const instance = this.instances.get(extension.id)
|
||||
if (instance) {
|
||||
await instance.disable()
|
||||
}
|
||||
this.extensions.delete(id);
|
||||
}
|
||||
}
|
||||
|
||||
broadcastExtensions(frameId?: number) {
|
||||
broadcastIpc({
|
||||
channel: "extensions:loaded",
|
||||
frameId: frameId,
|
||||
frameOnly: !!frameId,
|
||||
args: [this.toJSON().extensions],
|
||||
})
|
||||
}
|
||||
|
||||
toJSON() {
|
||||
return toJS({
|
||||
extensions: Array.from(this.extensions).map(([id, instance]) => instance),
|
||||
}, {
|
||||
recurseEverything: true,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export const extensionLoader = new ExtensionLoader()
|
||||
105
src/extensions/extension-manager.ts
Normal file
105
src/extensions/extension-manager.ts
Normal file
@ -0,0 +1,105 @@
|
||||
import type { ExtensionManifest } from "./lens-extension"
|
||||
import path from "path"
|
||||
import fs from "fs-extra"
|
||||
import logger from "../main/logger"
|
||||
import { extensionPackagesRoot, InstalledExtension } from "./extension-loader"
|
||||
import * as child_process from 'child_process';
|
||||
import { getBundledExtensions } from "../common/utils/app-version"
|
||||
|
||||
type Dependencies = {
|
||||
[name: string]: string;
|
||||
}
|
||||
|
||||
type PackageJson = {
|
||||
dependencies: Dependencies;
|
||||
}
|
||||
|
||||
export class ExtensionManager {
|
||||
|
||||
protected packagesJson: PackageJson = {
|
||||
dependencies: {}
|
||||
}
|
||||
|
||||
get extensionPackagesRoot() {
|
||||
return extensionPackagesRoot()
|
||||
}
|
||||
|
||||
get folderPath(): string {
|
||||
return path.resolve(__static, "../extensions");
|
||||
}
|
||||
|
||||
get npmPath() {
|
||||
return __non_webpack_require__.resolve('npm/bin/npm-cli')
|
||||
}
|
||||
|
||||
async load() {
|
||||
logger.info("[EXTENSION-MANAGER] loading extensions from " + this.extensionPackagesRoot)
|
||||
await fs.ensureDir(path.join(this.extensionPackagesRoot, "node_modules"))
|
||||
|
||||
return await this.loadExtensions();
|
||||
}
|
||||
|
||||
async getExtensionByManifest(manifestPath: string): Promise<InstalledExtension> {
|
||||
let manifestJson: ExtensionManifest;
|
||||
try {
|
||||
manifestJson = __non_webpack_require__(manifestPath)
|
||||
this.packagesJson.dependencies[manifestJson.name] = path.dirname(manifestPath)
|
||||
|
||||
logger.info("[EXTENSION-MANAGER] installed extension " + manifestJson.name)
|
||||
return {
|
||||
id: manifestJson.name,
|
||||
version: manifestJson.version,
|
||||
name: manifestJson.name,
|
||||
manifestPath: path.join(this.extensionPackagesRoot, "node_modules", manifestJson.name, "package.json"),
|
||||
manifest: manifestJson
|
||||
}
|
||||
} catch (err) {
|
||||
logger.error(`[EXTENSION-MANAGER]: can't install extension at ${manifestPath}: ${err}`, { manifestJson });
|
||||
}
|
||||
}
|
||||
|
||||
protected installPackages(): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const child = child_process.fork(this.npmPath, ["install", "--silent"], {
|
||||
cwd: extensionPackagesRoot(),
|
||||
silent: true
|
||||
})
|
||||
child.on("close", () => {
|
||||
resolve()
|
||||
})
|
||||
child.on("error", (err) => {
|
||||
reject(err)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
async loadExtensions() {
|
||||
const extensions = await this.loadFromFolder(this.folderPath);
|
||||
return new Map(extensions.map(ext => [ext.id, ext]));
|
||||
}
|
||||
|
||||
async loadFromFolder(folderPath: string): Promise<InstalledExtension[]> {
|
||||
const paths = await fs.readdir(folderPath);
|
||||
const extensions: InstalledExtension[] = []
|
||||
const bundledExtensions = getBundledExtensions()
|
||||
for (const fileName of paths) {
|
||||
if (!bundledExtensions.includes(fileName)) {
|
||||
continue
|
||||
}
|
||||
const absPath = path.resolve(folderPath, fileName);
|
||||
const manifestPath = path.resolve(absPath, "package.json");
|
||||
await fs.access(manifestPath, fs.constants.F_OK)
|
||||
const ext = await this.getExtensionByManifest(manifestPath).catch(() => null)
|
||||
if (ext) {
|
||||
extensions.push(ext)
|
||||
}
|
||||
}
|
||||
await fs.writeFile(path.join(this.extensionPackagesRoot, "package.json"), JSON.stringify(this.packagesJson), {mode: 0o600})
|
||||
await this.installPackages()
|
||||
|
||||
logger.debug(`[EXTENSION-MANAGER]: ${extensions.length} extensions loaded`, { folderPath, extensions });
|
||||
return extensions;
|
||||
}
|
||||
}
|
||||
|
||||
export const extensionManager = new ExtensionManager()
|
||||
21
src/extensions/extension-store.ts
Normal file
21
src/extensions/extension-store.ts
Normal file
@ -0,0 +1,21 @@
|
||||
import { BaseStore } from "../common/base-store"
|
||||
import * as path from "path"
|
||||
import { LensExtension } from "./lens-extension"
|
||||
|
||||
export class ExtensionStore<T = any> extends BaseStore<T> {
|
||||
protected extension: LensExtension
|
||||
|
||||
async loadExtension(extension: LensExtension) {
|
||||
this.extension = extension
|
||||
await super.load()
|
||||
}
|
||||
|
||||
async load() {
|
||||
if (!this.extension) { return }
|
||||
await super.load()
|
||||
}
|
||||
|
||||
protected storePath() {
|
||||
return path.join(super.storePath(), "extension-store", this.extension.name)
|
||||
}
|
||||
}
|
||||
104
src/extensions/lens-extension.ts
Normal file
104
src/extensions/lens-extension.ts
Normal file
@ -0,0 +1,104 @@
|
||||
import { readJsonSync } from "fs-extra";
|
||||
import { action, observable, toJS } from "mobx";
|
||||
import logger from "../main/logger";
|
||||
|
||||
export type ExtensionId = string | ExtensionPackageJsonPath;
|
||||
export type ExtensionPackageJsonPath = string;
|
||||
export type ExtensionVersion = string | number;
|
||||
|
||||
export interface ExtensionModel {
|
||||
id: ExtensionId;
|
||||
version: ExtensionVersion;
|
||||
name: string;
|
||||
manifestPath: string;
|
||||
description?: string;
|
||||
enabled?: boolean;
|
||||
updateUrl?: string;
|
||||
}
|
||||
|
||||
export interface ExtensionManifest extends ExtensionModel {
|
||||
main?: string;
|
||||
renderer?: string;
|
||||
description?: string; // todo: add more fields similar to package.json + some extra
|
||||
}
|
||||
|
||||
export class LensExtension implements ExtensionModel {
|
||||
public id: ExtensionId;
|
||||
public updateUrl: string;
|
||||
protected disposers: Function[] = [];
|
||||
|
||||
@observable name = "";
|
||||
@observable description = "";
|
||||
@observable version: ExtensionVersion = "0.0.0";
|
||||
@observable manifest: ExtensionManifest;
|
||||
@observable manifestPath: string;
|
||||
@observable isEnabled = false;
|
||||
|
||||
constructor(model: ExtensionModel, manifest: ExtensionManifest) {
|
||||
this.importModel(model, manifest);
|
||||
}
|
||||
|
||||
@action
|
||||
async importModel({ enabled, manifestPath, ...model }: ExtensionModel, manifest?: ExtensionManifest) {
|
||||
try {
|
||||
this.manifest = manifest || await readJsonSync(manifestPath, { throws: true })
|
||||
this.manifestPath = manifestPath;
|
||||
Object.assign(this, model);
|
||||
} catch (err) {
|
||||
logger.error(`[EXTENSION]: cannot read manifest at ${manifestPath}`, { ...model, err: String(err) })
|
||||
this.disable();
|
||||
}
|
||||
}
|
||||
|
||||
async migrate(appVersion: string) {
|
||||
// mock
|
||||
}
|
||||
|
||||
async enable() {
|
||||
this.isEnabled = true;
|
||||
logger.info(`[EXTENSION]: enabled ${this.name}@${this.version}`);
|
||||
this.onActivate();
|
||||
}
|
||||
|
||||
async disable() {
|
||||
this.onDeactivate();
|
||||
this.isEnabled = false;
|
||||
this.disposers.forEach(cleanUp => cleanUp());
|
||||
this.disposers.length = 0;
|
||||
logger.info(`[EXTENSION]: disabled ${this.name}@${this.version}`);
|
||||
}
|
||||
|
||||
// todo: add more hooks
|
||||
protected onActivate() {
|
||||
// mock
|
||||
}
|
||||
|
||||
protected onDeactivate() {
|
||||
// mock
|
||||
}
|
||||
|
||||
getMeta() {
|
||||
return toJS({
|
||||
id: this.id,
|
||||
manifest: this.manifest,
|
||||
manifestPath: this.manifestPath,
|
||||
enabled: this.isEnabled
|
||||
}, {
|
||||
recurseEverything: true
|
||||
})
|
||||
}
|
||||
|
||||
toJSON(): ExtensionModel {
|
||||
return toJS({
|
||||
id: this.id,
|
||||
name: this.name,
|
||||
version: this.version,
|
||||
description: this.description,
|
||||
manifestPath: this.manifestPath,
|
||||
enabled: this.isEnabled,
|
||||
updateUrl: this.updateUrl,
|
||||
}, {
|
||||
recurseEverything: true,
|
||||
})
|
||||
}
|
||||
}
|
||||
12
src/extensions/lens-main-extension.ts
Normal file
12
src/extensions/lens-main-extension.ts
Normal file
@ -0,0 +1,12 @@
|
||||
import { LensExtension } from "./lens-extension"
|
||||
import type { MenuRegistry } from "./registries/menu-registry";
|
||||
|
||||
export class LensMainExtension extends LensExtension {
|
||||
registerAppMenus(registry: MenuRegistry) {
|
||||
//
|
||||
}
|
||||
|
||||
registerPrometheusProviders(registry: any) {
|
||||
//
|
||||
}
|
||||
}
|
||||
28
src/extensions/lens-renderer-extension.ts
Normal file
28
src/extensions/lens-renderer-extension.ts
Normal file
@ -0,0 +1,28 @@
|
||||
import { LensExtension } from "./lens-extension"
|
||||
import type { GlobalPageRegistry, ClusterPageRegistry, AppPreferenceRegistry, StatusBarRegistry, KubeObjectMenuRegistry, ClusterFeatureRegistry } from "./registries"
|
||||
|
||||
export class LensRendererExtension extends LensExtension {
|
||||
registerGlobalPage(registry: GlobalPageRegistry) {
|
||||
return
|
||||
}
|
||||
|
||||
registerClusterPage(registry: ClusterPageRegistry) {
|
||||
return
|
||||
}
|
||||
|
||||
registerAppPreferences(registry: AppPreferenceRegistry) {
|
||||
return
|
||||
}
|
||||
|
||||
registerClusterFeatures(registry: ClusterFeatureRegistry) {
|
||||
return
|
||||
}
|
||||
|
||||
registerStatusBarItem(registry: StatusBarRegistry) {
|
||||
return
|
||||
}
|
||||
|
||||
registerKubeObjectMenus(registry: KubeObjectMenuRegistry) {
|
||||
return
|
||||
}
|
||||
}
|
||||
1
src/extensions/npm/extensions/.gitignore
vendored
Normal file
1
src/extensions/npm/extensions/.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
api.d.ts
|
||||
15
src/extensions/npm/extensions/package.json
Normal file
15
src/extensions/npm/extensions/package.json
Normal file
@ -0,0 +1,15 @@
|
||||
{
|
||||
"name": "@k8slens/extensions",
|
||||
"productName": "Lens extensions",
|
||||
"description": "Lens - The Kubernetes IDE: extensions",
|
||||
"version": "0.0.0",
|
||||
"copyright": "© 2020, Mirantis, Inc.",
|
||||
"license": "MIT",
|
||||
"files": [
|
||||
"api.d.ts"
|
||||
],
|
||||
"author": {
|
||||
"name": "Mirantis, Inc.",
|
||||
"email": "info@k8slens.dev"
|
||||
}
|
||||
}
|
||||
17
src/extensions/registries/app-preference-registry.ts
Normal file
17
src/extensions/registries/app-preference-registry.ts
Normal file
@ -0,0 +1,17 @@
|
||||
import type React from "react"
|
||||
import { BaseRegistry } from "./base-registry";
|
||||
|
||||
export interface AppPreferenceComponents {
|
||||
Hint: React.ComponentType<any>;
|
||||
Input: React.ComponentType<any>;
|
||||
}
|
||||
|
||||
export interface AppPreferenceRegistration {
|
||||
title: string;
|
||||
components: AppPreferenceComponents;
|
||||
}
|
||||
|
||||
export class AppPreferenceRegistry extends BaseRegistry<AppPreferenceRegistration> {
|
||||
}
|
||||
|
||||
export const appPreferenceRegistry = new AppPreferenceRegistry()
|
||||
17
src/extensions/registries/base-registry.ts
Normal file
17
src/extensions/registries/base-registry.ts
Normal file
@ -0,0 +1,17 @@
|
||||
// Base class for extensions-api registries
|
||||
import { observable } from "mobx";
|
||||
|
||||
export class BaseRegistry<T = any> {
|
||||
protected items = observable<T>([], { deep: false });
|
||||
|
||||
getItems(): T[] {
|
||||
return this.items.toJS();
|
||||
}
|
||||
|
||||
add(item: T) {
|
||||
this.items.push(item);
|
||||
return () => {
|
||||
this.items.remove(item); // works because of {deep: false};
|
||||
}
|
||||
}
|
||||
}
|
||||
16
src/extensions/registries/cluster-feature-registry.ts
Normal file
16
src/extensions/registries/cluster-feature-registry.ts
Normal file
@ -0,0 +1,16 @@
|
||||
import { BaseRegistry } from "./base-registry";
|
||||
import { ClusterFeature } from "../cluster-feature";
|
||||
|
||||
export interface ClusterFeatureComponents {
|
||||
Description: React.ComponentType<any>;
|
||||
}
|
||||
|
||||
export interface ClusterFeatureRegistration {
|
||||
title: string;
|
||||
components: ClusterFeatureComponents
|
||||
feature: ClusterFeature
|
||||
}
|
||||
|
||||
export class ClusterFeatureRegistry extends BaseRegistry<ClusterFeatureRegistration> {}
|
||||
|
||||
export const clusterFeatureRegistry = new ClusterFeatureRegistry()
|
||||
8
src/extensions/registries/index.ts
Normal file
8
src/extensions/registries/index.ts
Normal file
@ -0,0 +1,8 @@
|
||||
// All registries managed by extensions api
|
||||
|
||||
export * from "./page-registry"
|
||||
export * from "./menu-registry"
|
||||
export * from "./app-preference-registry"
|
||||
export * from "./status-bar-registry"
|
||||
export * from "./kube-object-menu-registry";
|
||||
export * from "./cluster-feature-registry"
|
||||
22
src/extensions/registries/kube-object-menu-registry.ts
Normal file
22
src/extensions/registries/kube-object-menu-registry.ts
Normal file
@ -0,0 +1,22 @@
|
||||
import React from "react"
|
||||
import { BaseRegistry } from "./base-registry";
|
||||
|
||||
export interface KubeObjectMenuComponents {
|
||||
MenuItem: React.ComponentType<any>;
|
||||
}
|
||||
|
||||
export interface KubeObjectMenuRegistration {
|
||||
kind: string;
|
||||
apiVersions: string[];
|
||||
components: KubeObjectMenuComponents;
|
||||
}
|
||||
|
||||
export class KubeObjectMenuRegistry extends BaseRegistry<KubeObjectMenuRegistration> {
|
||||
getItemsForKind(kind: string, apiVersion: string) {
|
||||
return this.items.filter((item) => {
|
||||
return item.kind === kind && item.apiVersions.includes(apiVersion)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export const kubeObjectMenuRegistry = new KubeObjectMenuRegistry()
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user