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

Merge remote-tracking branch 'origin/master' into extensions-list-page

# Conflicts:
#	src/extensions/extension-loader.ts
This commit is contained in:
Roman 2020-11-02 14:41:57 +02:00
commit 59339a4487
50 changed files with 4481 additions and 161 deletions

View File

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

View File

@ -0,0 +1,13 @@
import { LensMainExtension, Util } from "@k8slens/extensions";
export default class LicenseLensMainExtension extends LensMainExtension {
appMenus = [
{
parentId: "help",
label: "License",
async click() {
Util.openExternal("https://k8slens.dev/licenses/eula.md")
}
}
]
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,21 @@
{
"name": "lens-license",
"version": "0.1.0",
"description": "License menu item",
"main": "dist/main.js",
"scripts": {
"build": "webpack -p",
"dev": "webpack --watch"
},
"dependencies": {},
"devDependencies": {
"@types/webpack": "^4.41.17",
"@k8slens/extensions": "file:../../src/extensions/npm/extensions",
"mobx": "^5.15.5",
"react": "^16.13.1",
"ts-loader": "^8.0.4",
"ts-node": "^9.0.0",
"typescript": "^4.0.3",
"webpack": "^4.44.2"
}
}

View File

@ -0,0 +1,19 @@
{
"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"
}
}

View File

@ -0,0 +1,34 @@
import path from "path"
const outputPath = path.resolve(__dirname, 'dist');
export default [
{
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",
},
resolve: {
extensions: ['.tsx', '.ts', '.js'],
},
output: {
libraryTarget: "commonjs2",
globalObject: "this",
filename: 'main.js',
path: outputPath,
},
},
];

View File

@ -20,6 +20,12 @@
"integrity": "sha512-S78QIYirQcUoo6UJZx9CSP0O2ix9IaeAXwQi26Rhr/+mg7qqPy8TzaxHSUut7eGjL8WmLccT7/MXf304WjqHcA==", "integrity": "sha512-S78QIYirQcUoo6UJZx9CSP0O2ix9IaeAXwQi26Rhr/+mg7qqPy8TzaxHSUut7eGjL8WmLccT7/MXf304WjqHcA==",
"dev": true "dev": true
}, },
"@types/json-schema": {
"version": "7.0.6",
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.6.tgz",
"integrity": "sha512-3c+yGKvVP5Y9TYBEibGNR+kLtijnj7mYrXRg+WpFb2X9xm04g/DXYkfg4hmzJQosc9snFNUPkbYIhu+KAm6jJw==",
"dev": true
},
"@types/node": { "@types/node": {
"version": "14.11.11", "version": "14.11.11",
"resolved": "https://registry.npmjs.org/@types/node/-/node-14.11.11.tgz", "resolved": "https://registry.npmjs.org/@types/node/-/node-14.11.11.tgz",
@ -741,6 +747,12 @@
"unset-value": "^1.0.0" "unset-value": "^1.0.0"
} }
}, },
"camelcase": {
"version": "6.2.0",
"resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.2.0.tgz",
"integrity": "sha512-c7wVvbw3f37nuobQNtgsgG9POC9qMbNuMQmTCqZv23b6MIz0fcYpBiOlv9gEN/hdLdnZTDQhg6e9Dq5M1vKvfg==",
"dev": true
},
"chalk": { "chalk": {
"version": "2.4.2", "version": "2.4.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz",
@ -842,6 +854,12 @@
"integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=",
"dev": true "dev": true
}, },
"colorette": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/colorette/-/colorette-1.2.1.tgz",
"integrity": "sha512-puCDz0CzydiSYOrnXpz/PKd69zRrribezjtE9yd4zvytoRc8+RY/KJPvtPFKZS3E3wP6neGyMe0vOTlHO5L3Pw==",
"dev": true
},
"commander": { "commander": {
"version": "2.20.3", "version": "2.20.3",
"resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
@ -980,6 +998,71 @@
"randomfill": "^1.0.3" "randomfill": "^1.0.3"
} }
}, },
"css-loader": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/css-loader/-/css-loader-5.0.0.tgz",
"integrity": "sha512-9g35eXRBgjvswyJWoqq/seWp+BOxvUl8IinVNTsUBFFxtwfEYvlmEn6ciyn0liXGbGh5HyJjPGCuobDSfqMIVg==",
"dev": true,
"requires": {
"camelcase": "^6.1.0",
"cssesc": "^3.0.0",
"icss-utils": "^5.0.0",
"loader-utils": "^2.0.0",
"postcss": "^8.1.1",
"postcss-modules-extract-imports": "^3.0.0",
"postcss-modules-local-by-default": "^4.0.0",
"postcss-modules-scope": "^3.0.0",
"postcss-modules-values": "^4.0.0",
"postcss-value-parser": "^4.1.0",
"schema-utils": "^3.0.0",
"semver": "^7.3.2"
},
"dependencies": {
"json5": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/json5/-/json5-2.1.3.tgz",
"integrity": "sha512-KXPvOm8K9IJKFM0bmdn8QXh7udDh1g/giieX0NLCaMnb4hEiVFqnop2ImTXCc5e0/oHz3LTqmHGtExn5hfMkOA==",
"dev": true,
"requires": {
"minimist": "^1.2.5"
}
},
"loader-utils": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.0.tgz",
"integrity": "sha512-rP4F0h2RaWSvPEkD7BLDFQnvSf+nK+wr3ESUjNTyAGobqrijmW92zc+SO6d4p4B1wh7+B/Jg1mkQe5NYUEHtHQ==",
"dev": true,
"requires": {
"big.js": "^5.2.2",
"emojis-list": "^3.0.0",
"json5": "^2.1.2"
}
},
"schema-utils": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.0.0.tgz",
"integrity": "sha512-6D82/xSzO094ajanoOSbe4YvXWMfn2A//8Y1+MUqFAJul5Bs+yn36xbK9OtNDcRVSBJ9jjeoXftM6CfztsjOAA==",
"dev": true,
"requires": {
"@types/json-schema": "^7.0.6",
"ajv": "^6.12.5",
"ajv-keywords": "^3.5.2"
}
},
"semver": {
"version": "7.3.2",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.3.2.tgz",
"integrity": "sha512-OrOb32TeeambH6UrhtShmF7CRDqhL6/5XpPNp2DuRH6+9QLw/orhp72j87v8Qa1ScDkvrrBNpZcDejAirJmfXQ==",
"dev": true
}
}
},
"cssesc": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz",
"integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==",
"dev": true
},
"csstype": { "csstype": {
"version": "3.0.3", "version": "3.0.3",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.0.3.tgz", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.0.3.tgz",
@ -1600,6 +1683,12 @@
"integrity": "sha1-7AbBDgo0wPL68Zn3/X/Hj//QPHM=", "integrity": "sha1-7AbBDgo0wPL68Zn3/X/Hj//QPHM=",
"dev": true "dev": true
}, },
"icss-utils": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/icss-utils/-/icss-utils-5.0.0.tgz",
"integrity": "sha512-aF2Cf/CkEZrI/vsu5WI/I+akFgdbwQHVE9YRZxATrhH4PVIe6a3BIjwjEcW+z+jP/hNh+YvM3lAAn1wJQ6opSg==",
"dev": true
},
"ieee754": { "ieee754": {
"version": "1.1.13", "version": "1.1.13",
"resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.13.tgz", "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.13.tgz",
@ -1618,6 +1707,12 @@
"integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=", "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=",
"dev": true "dev": true
}, },
"indexes-of": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/indexes-of/-/indexes-of-1.0.1.tgz",
"integrity": "sha1-8w9xbI4r00bHtn0985FVZqfAVgc=",
"dev": true
},
"infer-owner": { "infer-owner": {
"version": "1.0.4", "version": "1.0.4",
"resolved": "https://registry.npmjs.org/infer-owner/-/infer-owner-1.0.4.tgz", "resolved": "https://registry.npmjs.org/infer-owner/-/infer-owner-1.0.4.tgz",
@ -1810,6 +1905,33 @@
"integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==",
"dev": true "dev": true
}, },
"klona": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/klona/-/klona-2.0.4.tgz",
"integrity": "sha512-ZRbnvdg/NxqzC7L9Uyqzf4psi1OM4Cuc+sJAkQPjO6XkQIJTNbfK2Rsmbw8fx1p2mkZdp2FZYo2+LwXYY/uwIA==",
"dev": true
},
"line-column": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/line-column/-/line-column-1.0.2.tgz",
"integrity": "sha1-0lryk2tvSEkXKzEuR5LR2Ye8NKI=",
"dev": true,
"requires": {
"isarray": "^1.0.0",
"isobject": "^2.0.0"
},
"dependencies": {
"isobject": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/isobject/-/isobject-2.1.0.tgz",
"integrity": "sha1-8GVWEJaj8dou9GJy+BXIQNh+DIk=",
"dev": true,
"requires": {
"isarray": "1.0.0"
}
}
}
},
"loader-runner": { "loader-runner": {
"version": "2.4.0", "version": "2.4.0",
"resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-2.4.0.tgz", "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-2.4.0.tgz",
@ -2051,6 +2173,12 @@
"dev": true, "dev": true,
"optional": true "optional": true
}, },
"nanoid": {
"version": "3.1.16",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.1.16.tgz",
"integrity": "sha512-+AK8MN0WHji40lj8AEuwLOvLSbWYApQpre/aFJZD71r43wVRLrOYS4FmJOPQYon1TqB462RzrrxlfA74XRES8w==",
"dev": true
},
"nanomatch": { "nanomatch": {
"version": "1.2.13", "version": "1.2.13",
"resolved": "https://registry.npmjs.org/nanomatch/-/nanomatch-1.2.13.tgz", "resolved": "https://registry.npmjs.org/nanomatch/-/nanomatch-1.2.13.tgz",
@ -2317,6 +2445,71 @@
"integrity": "sha1-AerA/jta9xoqbAL+q7jB/vfgDqs=", "integrity": "sha1-AerA/jta9xoqbAL+q7jB/vfgDqs=",
"dev": true "dev": true
}, },
"postcss": {
"version": "8.1.4",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.1.4.tgz",
"integrity": "sha512-LfqcwgMq9LOd8pX7K2+r2HPitlIGC5p6PoZhVELlqhh2YGDVcXKpkCseqan73Hrdik6nBd2OvoDPUaP/oMj9hQ==",
"dev": true,
"requires": {
"colorette": "^1.2.1",
"line-column": "^1.0.2",
"nanoid": "^3.1.15",
"source-map": "^0.6.1"
}
},
"postcss-modules-extract-imports": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/postcss-modules-extract-imports/-/postcss-modules-extract-imports-3.0.0.tgz",
"integrity": "sha512-bdHleFnP3kZ4NYDhuGlVK+CMrQ/pqUm8bx/oGL93K6gVwiclvX5x0n76fYMKuIGKzlABOy13zsvqjb0f92TEXw==",
"dev": true
},
"postcss-modules-local-by-default": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.0.0.tgz",
"integrity": "sha512-sT7ihtmGSF9yhm6ggikHdV0hlziDTX7oFoXtuVWeDd3hHObNkcHRo9V3yg7vCAY7cONyxJC/XXCmmiHHcvX7bQ==",
"dev": true,
"requires": {
"icss-utils": "^5.0.0",
"postcss-selector-parser": "^6.0.2",
"postcss-value-parser": "^4.1.0"
}
},
"postcss-modules-scope": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/postcss-modules-scope/-/postcss-modules-scope-3.0.0.tgz",
"integrity": "sha512-hncihwFA2yPath8oZ15PZqvWGkWf+XUfQgUGamS4LqoP1anQLOsOJw0vr7J7IwLpoY9fatA2qiGUGmuZL0Iqlg==",
"dev": true,
"requires": {
"postcss-selector-parser": "^6.0.4"
}
},
"postcss-modules-values": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/postcss-modules-values/-/postcss-modules-values-4.0.0.tgz",
"integrity": "sha512-RDxHkAiEGI78gS2ofyvCsu7iycRv7oqw5xMWn9iMoR0N/7mf9D50ecQqUo5BZ9Zh2vH4bCUR/ktCqbB9m8vJjQ==",
"dev": true,
"requires": {
"icss-utils": "^5.0.0"
}
},
"postcss-selector-parser": {
"version": "6.0.4",
"resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.4.tgz",
"integrity": "sha512-gjMeXBempyInaBqpp8gODmwZ52WaYsVOsfr4L4lDQ7n3ncD6mEyySiDtgzCT+NYC0mmeOLvtsF8iaEf0YT6dBw==",
"dev": true,
"requires": {
"cssesc": "^3.0.0",
"indexes-of": "^1.0.1",
"uniq": "^1.0.1",
"util-deprecate": "^1.0.2"
}
},
"postcss-value-parser": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.1.0.tgz",
"integrity": "sha512-97DXOFbQJhk71ne5/Mt6cOu6yxsSfM0QGQyl0L25Gca4yGWEGJaig7l7gbCX623VqTBNGLRLaVUCnNkcedlRSQ==",
"dev": true
},
"process": { "process": {
"version": "0.11.10", "version": "0.11.10",
"resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz",
@ -2576,6 +2769,58 @@
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
"dev": true "dev": true
}, },
"sass-loader": {
"version": "10.0.4",
"resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-10.0.4.tgz",
"integrity": "sha512-zhdZ8qvZM4iL5XjLVEjJLvKWvC+MB+hHgzL2x/Nf7UHpUNmPYsJvypW79bW39g4LZ603dH/dRSsRYzJJIljtdA==",
"dev": true,
"requires": {
"klona": "^2.0.4",
"loader-utils": "^2.0.0",
"neo-async": "^2.6.2",
"schema-utils": "^3.0.0",
"semver": "^7.3.2"
},
"dependencies": {
"json5": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/json5/-/json5-2.1.3.tgz",
"integrity": "sha512-KXPvOm8K9IJKFM0bmdn8QXh7udDh1g/giieX0NLCaMnb4hEiVFqnop2ImTXCc5e0/oHz3LTqmHGtExn5hfMkOA==",
"dev": true,
"requires": {
"minimist": "^1.2.5"
}
},
"loader-utils": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.0.tgz",
"integrity": "sha512-rP4F0h2RaWSvPEkD7BLDFQnvSf+nK+wr3ESUjNTyAGobqrijmW92zc+SO6d4p4B1wh7+B/Jg1mkQe5NYUEHtHQ==",
"dev": true,
"requires": {
"big.js": "^5.2.2",
"emojis-list": "^3.0.0",
"json5": "^2.1.2"
}
},
"schema-utils": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.0.0.tgz",
"integrity": "sha512-6D82/xSzO094ajanoOSbe4YvXWMfn2A//8Y1+MUqFAJul5Bs+yn36xbK9OtNDcRVSBJ9jjeoXftM6CfztsjOAA==",
"dev": true,
"requires": {
"@types/json-schema": "^7.0.6",
"ajv": "^6.12.5",
"ajv-keywords": "^3.5.2"
}
},
"semver": {
"version": "7.3.2",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.3.2.tgz",
"integrity": "sha512-OrOb32TeeambH6UrhtShmF7CRDqhL6/5XpPNp2DuRH6+9QLw/orhp72j87v8Qa1ScDkvrrBNpZcDejAirJmfXQ==",
"dev": true
}
}
},
"schema-utils": { "schema-utils": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-1.0.0.tgz", "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-1.0.0.tgz",
@ -2882,6 +3127,49 @@
"safe-buffer": "~5.1.0" "safe-buffer": "~5.1.0"
} }
}, },
"style-loader": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/style-loader/-/style-loader-2.0.0.tgz",
"integrity": "sha512-Z0gYUJmzZ6ZdRUqpg1r8GsaFKypE+3xAzuFeMuoHgjc9KZv3wMyCRjQIWEbhoFSq7+7yoHXySDJyyWQaPajeiQ==",
"dev": true,
"requires": {
"loader-utils": "^2.0.0",
"schema-utils": "^3.0.0"
},
"dependencies": {
"json5": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/json5/-/json5-2.1.3.tgz",
"integrity": "sha512-KXPvOm8K9IJKFM0bmdn8QXh7udDh1g/giieX0NLCaMnb4hEiVFqnop2ImTXCc5e0/oHz3LTqmHGtExn5hfMkOA==",
"dev": true,
"requires": {
"minimist": "^1.2.5"
}
},
"loader-utils": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.0.tgz",
"integrity": "sha512-rP4F0h2RaWSvPEkD7BLDFQnvSf+nK+wr3ESUjNTyAGobqrijmW92zc+SO6d4p4B1wh7+B/Jg1mkQe5NYUEHtHQ==",
"dev": true,
"requires": {
"big.js": "^5.2.2",
"emojis-list": "^3.0.0",
"json5": "^2.1.2"
}
},
"schema-utils": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.0.0.tgz",
"integrity": "sha512-6D82/xSzO094ajanoOSbe4YvXWMfn2A//8Y1+MUqFAJul5Bs+yn36xbK9OtNDcRVSBJ9jjeoXftM6CfztsjOAA==",
"dev": true,
"requires": {
"@types/json-schema": "^7.0.6",
"ajv": "^6.12.5",
"ajv-keywords": "^3.5.2"
}
}
}
},
"supports-color": { "supports-color": {
"version": "5.5.0", "version": "5.5.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
@ -3053,6 +3341,12 @@
"set-value": "^2.0.1" "set-value": "^2.0.1"
} }
}, },
"uniq": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/uniq/-/uniq-1.0.1.tgz",
"integrity": "sha1-sxxa6CVIRKOoKBVBzisEuGWnNP8=",
"dev": true
},
"unique-filename": { "unique-filename": {
"version": "1.1.1", "version": "1.1.1",
"resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-1.1.1.tgz", "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-1.1.1.tgz",

View File

@ -10,13 +10,16 @@
}, },
"dependencies": {}, "dependencies": {},
"devDependencies": { "devDependencies": {
"@k8slens/extensions": "file:../../src/extensions/npm/extensions",
"@types/node": "^14.11.11", "@types/node": "^14.11.11",
"@types/react": "^16.9.53", "@types/react": "^16.9.53",
"@types/react-router": "^5.1.8", "@types/react-router": "^5.1.8",
"@types/webpack": "^4.41.17", "@types/webpack": "^4.41.17",
"@k8slens/extensions": "file:../../src/extensions/npm/extensions", "css-loader": "^5.0.0",
"mobx": "^5.15.5", "mobx": "^5.15.5",
"react": "^16.13.1", "react": "^16.13.1",
"sass-loader": "^10.0.4",
"style-loader": "^2.0.0",
"ts-loader": "^8.0.4", "ts-loader": "^8.0.4",
"ts-node": "^9.0.0", "ts-node": "^9.0.0",
"typescript": "^4.0.3", "typescript": "^4.0.3",

View File

@ -22,8 +22,7 @@ export default class SupportPageRendererExtension extends LensRendererExtension
className="flex align-center gaps hover-highlight" className="flex align-center gaps hover-highlight"
onClick={() => Navigation.navigate(supportPageURL())} onClick={() => Navigation.navigate(supportPageURL())}
> >
<Component.Icon material="help_outline" small/> <Component.Icon material="help" smallest />
<span>Support</span>
</div> </div>
) )
} }

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

@ -1,6 +1,6 @@
// TODO: figure out how to consume styles / handle import "./support.scss"
// TODO: support localization / figure out how to extract / consume i18n strings // TODO: support localization / figure out how to extract / consume i18n strings
import "./support.scss"
import React from "react" import React from "react"
import { observer } from "mobx-react" import { observer } from "mobx-react"
import { App, Component } from "@k8slens/extensions"; import { App, Component } from "@k8slens/extensions";

View File

@ -2,7 +2,6 @@ import path from "path"
const outputPath = path.resolve(__dirname, 'dist'); const outputPath = path.resolve(__dirname, 'dist');
// TODO: figure out how to share base TS and Webpack configs from Lens (npm, filesystem, etc?)
const lensExternals = { const lensExternals = {
"@k8slens/extensions": "var global.LensExtensions", "@k8slens/extensions": "var global.LensExtensions",
"react": "var global.React", "react": "var global.React",
@ -50,6 +49,14 @@ export default [
use: 'ts-loader', use: 'ts-loader',
exclude: /node_modules/, exclude: /node_modules/,
}, },
{
test: /\.s?css$/,
use: [
"style-loader",
"css-loader",
"sass-loader",
]
}
], ],
}, },
externals: [ externals: [

View File

@ -7,6 +7,7 @@ export default class TelemetryMainExtension extends LensMainExtension {
async onActivate() { async onActivate() {
console.log("telemetry main extension activated") console.log("telemetry main extension activated")
tracker.start() tracker.start()
tracker.reportPeriodically()
await telemetryPreferencesStore.loadExtension(this) await telemetryPreferencesStore.loadExtension(this)
} }

View File

@ -8,6 +8,16 @@
"version": "file:../../src/extensions/npm/extensions", "version": "file:../../src/extensions/npm/extensions",
"dev": true "dev": true
}, },
"@segment/loosely-validate-event": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/@segment/loosely-validate-event/-/loosely-validate-event-2.0.0.tgz",
"integrity": "sha512-ZMCSfztDBqwotkl848ODgVcAmN4OItEWDCkshcKz0/W6gGSQayuuCtWV/MlodFivAZD793d6UgANd6wCXUfrIw==",
"dev": true,
"requires": {
"component-type": "^1.2.1",
"join-component": "^1.1.0"
}
},
"@webassemblyjs/ast": { "@webassemblyjs/ast": {
"version": "1.9.0", "version": "1.9.0",
"resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.9.0.tgz", "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.9.0.tgz",
@ -225,6 +235,22 @@
"integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==",
"dev": true "dev": true
}, },
"analytics-node": {
"version": "3.4.0-beta.3",
"resolved": "https://registry.npmjs.org/analytics-node/-/analytics-node-3.4.0-beta.3.tgz",
"integrity": "sha512-NIdpxiwlZ4cKgs9MDlDe89b5bg/pMq2W7XTA+cjzCM66IwW3ujZhVE49vk+zG6Yrxk0s/DXmennJ+cCQIsTKMA==",
"dev": true,
"requires": {
"@segment/loosely-validate-event": "^2.0.0",
"axios": "^0.19.2",
"axios-retry": "^3.0.2",
"lodash.isstring": "^4.0.1",
"md5": "^2.2.1",
"ms": "^2.0.0",
"remove-trailing-slash": "^0.1.0",
"uuid": "^3.2.1"
}
},
"ansi-styles": { "ansi-styles": {
"version": "3.2.1", "version": "3.2.1",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
@ -374,6 +400,24 @@
"integrity": "sha512-zg7Hz2k5lI8kb7U32998pRRFin7zJlkfezGJjUc2heaD4Pw2wObakCDVzkKztTm/Ln7eiVvYsjqak0Ed4LkMDA==", "integrity": "sha512-zg7Hz2k5lI8kb7U32998pRRFin7zJlkfezGJjUc2heaD4Pw2wObakCDVzkKztTm/Ln7eiVvYsjqak0Ed4LkMDA==",
"dev": true "dev": true
}, },
"axios": {
"version": "0.19.2",
"resolved": "https://registry.npmjs.org/axios/-/axios-0.19.2.tgz",
"integrity": "sha512-fjgm5MvRHLhx+osE2xoekY70AhARk3a6hkN+3Io1jc00jtquGvxYlKlsFUhmUET0V5te6CcZI7lcv2Ym61mjHA==",
"dev": true,
"requires": {
"follow-redirects": "1.5.10"
}
},
"axios-retry": {
"version": "3.1.9",
"resolved": "https://registry.npmjs.org/axios-retry/-/axios-retry-3.1.9.tgz",
"integrity": "sha512-NFCoNIHq8lYkJa6ku4m+V1837TP6lCa7n79Iuf8/AqATAHYB0ISaAS1eyIenDOfHOLtym34W65Sjke2xjg2fsA==",
"dev": true,
"requires": {
"is-retry-allowed": "^1.1.0"
}
},
"balanced-match": { "balanced-match": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz",
@ -696,6 +740,12 @@
"supports-color": "^5.3.0" "supports-color": "^5.3.0"
} }
}, },
"charenc": {
"version": "0.0.2",
"resolved": "https://registry.npmjs.org/charenc/-/charenc-0.0.2.tgz",
"integrity": "sha1-wKHS86cJLgN3S/qD8UwPxXkKhmc=",
"dev": true
},
"chokidar": { "chokidar": {
"version": "3.4.2", "version": "3.4.2",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.4.2.tgz", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.4.2.tgz",
@ -813,6 +863,12 @@
"integrity": "sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==", "integrity": "sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==",
"dev": true "dev": true
}, },
"component-type": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/component-type/-/component-type-1.2.1.tgz",
"integrity": "sha1-ikeQFwAjjk/DIml3EjAibyS0Fak=",
"dev": true
},
"concat-map": { "concat-map": {
"version": "0.0.1", "version": "0.0.1",
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
@ -914,6 +970,12 @@
"sha.js": "^2.4.8" "sha.js": "^2.4.8"
} }
}, },
"crypt": {
"version": "0.0.2",
"resolved": "https://registry.npmjs.org/crypt/-/crypt-0.0.2.tgz",
"integrity": "sha1-iNf/fsDfuG9xPch7u0LQRNPmxBs=",
"dev": true
},
"crypto-browserify": { "crypto-browserify": {
"version": "3.12.0", "version": "3.12.0",
"resolved": "https://registry.npmjs.org/crypto-browserify/-/crypto-browserify-3.12.0.tgz", "resolved": "https://registry.npmjs.org/crypto-browserify/-/crypto-browserify-3.12.0.tgz",
@ -1377,6 +1439,26 @@
"readable-stream": "^2.3.6" "readable-stream": "^2.3.6"
} }
}, },
"follow-redirects": {
"version": "1.5.10",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.5.10.tgz",
"integrity": "sha512-0V5l4Cizzvqt5D44aTXbFZz+FtyXV1vrDN6qrelxtfYQKW0KO0W2T/hkE8xvGa/540LkZlkaUjO4ailYTFtHVQ==",
"dev": true,
"requires": {
"debug": "=3.1.0"
},
"dependencies": {
"debug": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz",
"integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==",
"dev": true,
"requires": {
"ms": "2.0.0"
}
}
}
},
"for-in": { "for-in": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz", "resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz",
@ -1784,6 +1866,12 @@
"isobject": "^3.0.1" "isobject": "^3.0.1"
} }
}, },
"is-retry-allowed": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/is-retry-allowed/-/is-retry-allowed-1.2.0.tgz",
"integrity": "sha512-RUbUeKwvm3XG2VYamhJL1xFktgjvPzL0Hq8C+6yrWIswDy3BIXGqCxhxkc30N9jqK311gVU137K8Ei55/zVJRg==",
"dev": true
},
"is-typedarray": { "is-typedarray": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz",
@ -1820,6 +1908,12 @@
"integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=", "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=",
"dev": true "dev": true
}, },
"join-component": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/join-component/-/join-component-1.1.0.tgz",
"integrity": "sha1-uEF7dQZho5K+4sJTfGiyqdSXfNU=",
"dev": true
},
"js-tokens": { "js-tokens": {
"version": "4.0.0", "version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
@ -1910,6 +2004,12 @@
"path-exists": "^3.0.0" "path-exists": "^3.0.0"
} }
}, },
"lodash.isstring": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz",
"integrity": "sha1-1SfftUVuynzJu5XV2ur4i6VKVFE=",
"dev": true
},
"loose-envify": { "loose-envify": {
"version": "1.4.0", "version": "1.4.0",
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
@ -1961,6 +2061,17 @@
"object-visit": "^1.0.0" "object-visit": "^1.0.0"
} }
}, },
"md5": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/md5/-/md5-2.3.0.tgz",
"integrity": "sha512-T1GITYmFaKuO91vxyoQMFETst+O71VUPEU3ze5GNzDm0OWdP8v1ziTaAEPUr/3kLsY3Sftgz242A1SetQiDL7g==",
"dev": true,
"requires": {
"charenc": "0.0.2",
"crypt": "0.0.2",
"is-buffer": "~1.1.6"
}
},
"md5.js": { "md5.js": {
"version": "1.3.5", "version": "1.3.5",
"resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.5.tgz", "resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.5.tgz",
@ -2615,6 +2726,12 @@
"dev": true, "dev": true,
"optional": true "optional": true
}, },
"remove-trailing-slash": {
"version": "0.1.1",
"resolved": "https://registry.npmjs.org/remove-trailing-slash/-/remove-trailing-slash-0.1.1.tgz",
"integrity": "sha512-o4S4Qh6L2jpnCy83ysZDau+VORNvnFw07CKSAymkd6ICNVEPisMyzlc00KlvvicsxKck94SEwhDnMNdICzO+tA==",
"dev": true
},
"repeat-element": { "repeat-element": {
"version": "1.1.3", "version": "1.1.3",
"resolved": "https://registry.npmjs.org/repeat-element/-/repeat-element-1.1.3.tgz", "resolved": "https://registry.npmjs.org/repeat-element/-/repeat-element-1.1.3.tgz",

View File

@ -21,6 +21,7 @@
"mobx": "^5.15.5", "mobx": "^5.15.5",
"react": "^16.13.1", "react": "^16.13.1",
"node-machine-id": "^1.1.12", "node-machine-id": "^1.1.12",
"universal-analytics": "^0.4.23" "universal-analytics": "^0.4.23",
"analytics-node": "^3.4.0-beta.3"
} }
} }

View File

@ -1,31 +1,40 @@
import { EventBus, Util, Store, App } from "@k8slens/extensions" import { EventBus, Util, Store, App } from "@k8slens/extensions"
import ua from "universal-analytics" import ua from "universal-analytics"
import Analytics from "analytics-node"
import { machineIdSync } from "node-machine-id" import { machineIdSync } from "node-machine-id"
import { telemetryPreferencesStore } from "./telemetry-preferences-store" import { telemetryPreferencesStore } from "./telemetry-preferences-store"
export class Tracker extends Util.Singleton { export class Tracker extends Util.Singleton {
static readonly GA_ID = "UA-159377374-1" static readonly GA_ID = "UA-159377374-1"
static readonly SEGMENT_KEY = "YENwswyhlOgz8P7EFKUtIZ2MfON7Yxqb"
protected eventHandlers: Array<(ev: EventBus.AppEvent ) => void> = [] protected eventHandlers: Array<(ev: EventBus.AppEvent ) => void> = []
protected started = false protected started = false
protected visitor: ua.Visitor protected visitor: ua.Visitor
protected analytics: Analytics
protected machineId: string = null; protected machineId: string = null;
protected ip: string = null; protected ip: string = null;
protected appVersion: string; protected appVersion: string;
protected locale: string; protected locale: string;
protected electronUA: string; protected userAgent: string;
protected anonymousId: string;
protected os: string
protected reportInterval: NodeJS.Timeout protected reportInterval: NodeJS.Timeout
private constructor() { private constructor() {
super(); super();
this.anonymousId = machineIdSync()
this.os = this.resolveOS()
this.userAgent = `Lens ${App.version} (${this.os})`
try { try {
this.visitor = ua(Tracker.GA_ID, machineIdSync(), { strictCidFormat: false }) this.visitor = ua(Tracker.GA_ID, this.anonymousId, { strictCidFormat: false })
} catch (error) { } catch (error) {
this.visitor = ua(Tracker.GA_ID) this.visitor = ua(Tracker.GA_ID)
} }
this.analytics = new Analytics(Tracker.SEGMENT_KEY, { flushAt: 1 })
this.visitor.set("dl", "https://telemetry.k8slens.dev") this.visitor.set("dl", "https://telemetry.k8slens.dev")
this.visitor.set("ua", `Lens ${App.version} (${this.getOS()})`) this.visitor.set("ua", this.userAgent)
} }
start() { start() {
@ -38,6 +47,9 @@ export class Tracker extends Util.Singleton {
} }
this.eventHandlers.push(handler) this.eventHandlers.push(handler)
EventBus.appEventBus.addListener(handler) EventBus.appEventBus.addListener(handler)
}
reportPeriodically() {
this.reportInterval = setInterval(() => { this.reportInterval = setInterval(() => {
this.reportData() this.reportData()
}, 60 * 60 * 1000) // report every 1h }, 60 * 60 * 1000) // report every 1h
@ -61,12 +73,13 @@ export class Tracker extends Util.Singleton {
} }
protected reportData() { protected reportData() {
const clustersList = Store.clusterStore.clustersList const clustersList = Store.clusterStore.enabledClustersList
this.event("generic-data", "report", { this.event("generic-data", "report", {
appVersion: App.version, appVersion: App.version,
os: this.os,
clustersCount: clustersList.length, clustersCount: clustersList.length,
workspacesCount: Store.workspaceStore.workspacesList.length workspacesCount: Store.workspaceStore.enabledWorkspacesList.length
}) })
clustersList.forEach((cluster) => { clustersList.forEach((cluster) => {
@ -78,6 +91,7 @@ export class Tracker extends Util.Singleton {
protected reportClusterData(cluster: Store.ClusterModel) { protected reportClusterData(cluster: Store.ClusterModel) {
this.event("cluster-data", "report", { this.event("cluster-data", "report", {
id: cluster.metadata.id, id: cluster.metadata.id,
managed: !!cluster.ownerRef,
kubernetesVersion: cluster.metadata.version, kubernetesVersion: cluster.metadata.version,
distribution: cluster.metadata.distribution, distribution: cluster.metadata.distribution,
nodesCount: cluster.metadata.nodes, nodesCount: cluster.metadata.nodes,
@ -85,7 +99,7 @@ export class Tracker extends Util.Singleton {
}) })
} }
protected getOS() { protected resolveOS() {
let os = "" let os = ""
if (App.isMac) { if (App.isMac) {
os = "MacOS" os = "MacOS"
@ -115,6 +129,19 @@ export class Tracker extends Util.Singleton {
ea: eventAction, ea: eventAction,
...otherParams, ...otherParams,
}).send() }).send()
this.analytics.track({
anonymousId: this.anonymousId,
event: `${eventCategory} ${eventAction}`,
context: {
userAgent: this.userAgent,
},
properties: {
category: eventCategory,
...otherParams,
},
})
} catch (err) { } catch (err) {
console.error(`Failed to track "${eventCategory}:${eventAction}"`, err) console.error(`Failed to track "${eventCategory}:${eventAction}"`, err)
} }

View File

@ -185,6 +185,7 @@
"pod-menu", "pod-menu",
"node-menu", "node-menu",
"metrics-cluster-feature", "metrics-cluster-feature",
"license-menu-item",
"support-page" "support-page"
] ]
}, },

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

@ -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

@ -33,11 +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;
metadata?: ClusterMetadata; metadata?: ClusterMetadata;
kubeConfigPath: string; ownerRef?: string;
/** @deprecated */ /** @deprecated */
kubeConfig?: string; // yaml kubeConfig?: string; // yaml
@ -72,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);
})
}) })
} }
@ -99,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 = this.clusters.has(id) ? id : null; this.activeCluster = this.clusters.has(id) ? id : null;
} }
@action @action
@ -145,12 +169,28 @@ export class ClusterStore extends BaseStore<ClusterStoreModel> {
} }
@action @action
addCluster(...models: ClusterModel[]) { addClusters(...models: ClusterModel[]): Cluster[] {
const clusters: Cluster[] = []
models.forEach(model => { models.forEach(model => {
appEventBus.emit({name: "cluster", action: "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
@ -159,7 +199,7 @@ export class ClusterStore extends BaseStore<ClusterStoreModel> {
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.setActive(null); this.setActive(null);
} }
// remove only custom kubeconfigs (pasted as text) // remove only custom kubeconfigs (pasted as text)
@ -189,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);
} }
@ -200,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

View File

@ -11,4 +11,4 @@ export * from "./getRandId"
export * from "./splitArray" export * from "./splitArray"
export * from "./saveToAppFiles" export * from "./saveToAppFiles"
export * from "./singleton" export * from "./singleton"
export * from "./cloneJson" export * from "./openExternal"

View File

@ -0,0 +1,6 @@
// Opens a link in external browser
import { shell } from "electron"
export function openExternal(url: string) {
return shell.openExternal(url);
}

View File

@ -1,19 +1,77 @@
import { action, computed, observable, toJS } from "mobx"; import { ipcRenderer } from "electron";
import { action, computed, observable, toJS, reaction } from "mobx";
import { BaseStore } from "./base-store"; import { BaseStore } from "./base-store";
import { clusterStore } from "./cluster-store" import { clusterStore } from "./cluster-store"
import { appEventBus } from "./event-bus"; import { appEventBus } from "./event-bus";
import { broadcastIpc } from "../common/ipc";
import logger from "../main/logger";
export type WorkspaceId = string; export type WorkspaceId = string;
export interface WorkspaceStoreModel { export interface WorkspaceStoreModel {
currentWorkspace?: WorkspaceId; currentWorkspace?: WorkspaceId;
workspaces: Workspace[] workspaces: WorkspaceModel[]
} }
export interface Workspace { export interface WorkspaceModel {
id: WorkspaceId; id: WorkspaceId;
name: string; name: string;
description?: string; description?: string;
ownerRef?: string;
}
export interface WorkspaceState {
enabled: boolean;
}
export class Workspace implements WorkspaceModel, WorkspaceState {
@observable id: WorkspaceId
@observable name: string
@observable description?: string
@observable ownerRef?: string
@observable enabled: boolean
constructor(data: WorkspaceModel) {
Object.assign(this, data)
if (!ipcRenderer) {
reaction(() => this.getState(), () => {
this.pushState()
})
}
}
get isManaged(): boolean {
return !!this.ownerRef
}
getState(): WorkspaceState {
return {
enabled: this.enabled
}
}
pushState(state = this.getState()) {
logger.silly("[WORKSPACE] pushing state", {...state, id: this.id})
broadcastIpc({
channel: "workspace:state",
args: [this.id, toJS(state)],
});
}
@action
setState(state: WorkspaceState) {
Object.assign(this, state)
}
toJSON(): WorkspaceModel {
return toJS({
id: this.id,
name: this.name,
description: this.description,
ownerRef: this.ownerRef
})
}
} }
export class WorkspaceStore extends BaseStore<WorkspaceStoreModel> { export class WorkspaceStore extends BaseStore<WorkspaceStoreModel> {
@ -23,15 +81,33 @@ export class WorkspaceStore extends BaseStore<WorkspaceStoreModel> {
super({ super({
configName: "lens-workspace-store", configName: "lens-workspace-store",
}); });
if (!ipcRenderer) {
setInterval(() => {
this.pushState()
}, 5000)
}
}
registerIpcListener() {
logger.info("[WORKSPACE-STORE] starting to listen state events")
ipcRenderer.on("workspace:state", (event, workspaceId: string, state: WorkspaceState) => {
this.getById(workspaceId)?.setState(state)
})
}
unregisterIpcListener() {
super.unregisterIpcListener()
ipcRenderer.removeAllListeners("workspace:state")
} }
@observable currentWorkspaceId = WorkspaceStore.defaultId; @observable currentWorkspaceId = WorkspaceStore.defaultId;
@observable workspaces = observable.map<WorkspaceId, Workspace>({ @observable workspaces = observable.map<WorkspaceId, Workspace>({
[WorkspaceStore.defaultId]: { [WorkspaceStore.defaultId]: new Workspace({
id: WorkspaceStore.defaultId, id: WorkspaceStore.defaultId,
name: "default" name: "default"
} })
}); });
@computed get currentWorkspace(): Workspace { @computed get currentWorkspace(): Workspace {
@ -42,6 +118,16 @@ export class WorkspaceStore extends BaseStore<WorkspaceStoreModel> {
return Array.from(this.workspaces.values()); return Array.from(this.workspaces.values());
} }
@computed get enabledWorkspacesList() {
return this.workspacesList.filter((w) => w.enabled);
}
pushState() {
this.workspaces.forEach((w) => {
w.pushState()
})
}
isDefault(id: WorkspaceId) { isDefault(id: WorkspaceId) {
return id === WorkspaceStore.defaultId; return id === WorkspaceStore.defaultId;
} }
@ -61,11 +147,11 @@ export class WorkspaceStore extends BaseStore<WorkspaceStoreModel> {
throw new Error(`workspace ${id} doesn't exist`); throw new Error(`workspace ${id} doesn't exist`);
} }
this.currentWorkspaceId = id; this.currentWorkspaceId = id;
clusterStore.activeClusterId = null; // fixme: handle previously selected cluster from current workspace clusterStore.activeCluster = null; // fixme: handle previously selected cluster from current workspace
} }
@action @action
saveWorkspace(workspace: Workspace) { addWorkspace(workspace: Workspace) {
const { id, name } = workspace; const { id, name } = workspace;
const existingWorkspace = this.getById(id); const existingWorkspace = this.getById(id);
if (!name.trim() || this.getByName(name.trim())) { if (!name.trim() || this.getByName(name.trim())) {
@ -82,7 +168,12 @@ export class WorkspaceStore extends BaseStore<WorkspaceStoreModel> {
} }
@action @action
removeWorkspace(id: WorkspaceId) { removeWorkspace(workspace: Workspace) {
this.removeWorkspaceById(workspace.id)
}
@action
removeWorkspaceById(id: WorkspaceId) {
const workspace = this.getById(id); const workspace = this.getById(id);
if (!workspace) return; if (!workspace) return;
if (this.isDefault(id)) { if (this.isDefault(id)) {
@ -103,7 +194,11 @@ export class WorkspaceStore extends BaseStore<WorkspaceStoreModel> {
} }
if (workspaces.length) { if (workspaces.length) {
this.workspaces.clear(); this.workspaces.clear();
workspaces.forEach(workspace => { workspaces.forEach(ws => {
const workspace = new Workspace(ws)
if (!workspace.isManaged) {
workspace.enabled = true
}
this.workspaces.set(workspace.id, workspace) this.workspaces.set(workspace.id, workspace)
}) })
} }
@ -112,7 +207,7 @@ export class WorkspaceStore extends BaseStore<WorkspaceStoreModel> {
toJSON(): WorkspaceStoreModel { toJSON(): WorkspaceStoreModel {
return toJS({ return toJS({
currentWorkspace: this.currentWorkspaceId, currentWorkspace: this.currentWorkspaceId,
workspaces: this.workspacesList, workspaces: this.workspacesList.map((w) => w.toJSON()),
}, { }, {
recurseEverything: true recurseEverything: true
}) })

View File

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

View File

@ -1,3 +1,3 @@
export { Singleton } from "../../common/utils" export { Singleton, openExternal } from "../../common/utils"
export { prevDefault, stopPropagation } from "../../renderer/utils/prevDefault" export { prevDefault, stopPropagation } from "../../renderer/utils/prevDefault"
export { cssNames } from "../../renderer/utils/cssNames" export { cssNames } from "../../renderer/utils/cssNames"

View File

@ -6,7 +6,10 @@ import { broadcastIpc } from "../common/ipc"
import { computed, observable, reaction, toJS, } from "mobx" import { computed, observable, reaction, toJS, } from "mobx"
import logger from "../main/logger" import logger from "../main/logger"
import { app, ipcRenderer, remote } from "electron" import { app, ipcRenderer, remote } from "electron"
import { appPreferenceRegistry, clusterFeatureRegistry, clusterPageRegistry, globalPageRegistry, kubeObjectMenuRegistry, menuRegistry, statusBarRegistry } from "./registries"; import {
appPreferenceRegistry, clusterFeatureRegistry, clusterPageRegistry, globalPageRegistry,
kubeObjectDetailRegistry, kubeObjectMenuRegistry, menuRegistry, statusBarRegistry
} from "./registries";
import { getBundledExtensions } from "../common/utils" import { getBundledExtensions } from "../common/utils"
export interface InstalledExtension extends ExtensionModel { export interface InstalledExtension extends ExtensionModel {
@ -67,6 +70,7 @@ export class ExtensionLoader {
this.autoloadExtensions((extension: LensRendererExtension) => { this.autoloadExtensions((extension: LensRendererExtension) => {
extension.registerTo(clusterPageRegistry, extension.clusterPages) extension.registerTo(clusterPageRegistry, extension.clusterPages)
extension.registerTo(kubeObjectMenuRegistry, extension.kubeObjectMenuItems) extension.registerTo(kubeObjectMenuRegistry, extension.kubeObjectMenuItems)
extension.registerTo(kubeObjectDetailRegistry, extension.kubeObjectDetailItems)
}) })
} }

View File

@ -1,4 +1,8 @@
import type { AppPreferenceRegistration, ClusterFeatureRegistration, KubeObjectMenuRegistration, PageRegistration, StatusBarRegistration } from "./registries" import type {
AppPreferenceRegistration, ClusterFeatureRegistration,
KubeObjectMenuRegistration, KubeObjectDetailRegistration,
PageRegistration, StatusBarRegistration
} from "./registries"
import { observable } from "mobx"; import { observable } from "mobx";
import { LensExtension } from "./lens-extension" import { LensExtension } from "./lens-extension"
@ -8,5 +12,6 @@ export class LensRendererExtension extends LensExtension {
@observable.shallow appPreferences: AppPreferenceRegistration[] = [] @observable.shallow appPreferences: AppPreferenceRegistration[] = []
@observable.shallow clusterFeatures: ClusterFeatureRegistration[] = [] @observable.shallow clusterFeatures: ClusterFeatureRegistration[] = []
@observable.shallow statusBarItems: StatusBarRegistration[] = [] @observable.shallow statusBarItems: StatusBarRegistration[] = []
@observable.shallow kubeObjectDetailItems: KubeObjectDetailRegistration[] = []
@observable.shallow kubeObjectMenuItems: KubeObjectMenuRegistration[] = [] @observable.shallow kubeObjectMenuItems: KubeObjectMenuRegistration[] = []
} }

View File

@ -4,5 +4,6 @@ export * from "./page-registry"
export * from "./menu-registry" export * from "./menu-registry"
export * from "./app-preference-registry" export * from "./app-preference-registry"
export * from "./status-bar-registry" export * from "./status-bar-registry"
export * from "./kube-object-detail-registry";
export * from "./kube-object-menu-registry"; export * from "./kube-object-menu-registry";
export * from "./cluster-feature-registry" export * from "./cluster-feature-registry"

View File

@ -0,0 +1,22 @@
import React from "react"
import { BaseRegistry } from "./base-registry";
export interface KubeObjectDetailComponents {
Details: React.ComponentType<any>;
}
export interface KubeObjectDetailRegistration {
kind: string;
apiVersions: string[];
components: KubeObjectDetailComponents;
}
export class KubeObjectDetailRegistry extends BaseRegistry<KubeObjectDetailRegistration> {
getItemsForKind(kind: string, apiVersion: string) {
return this.items.filter((item) => {
return item.kind === kind && item.apiVersions.includes(apiVersion)
})
}
}
export const kubeObjectDetailRegistry = new KubeObjectDetailRegistry()

View File

@ -11,7 +11,8 @@ export * from "../../renderer/components/drawer"
// kube helpers // kube helpers
export { KubeObjectDetailsProps, KubeObjectMenuProps } from "../../renderer/components/kube-object" export { KubeObjectDetailsProps, KubeObjectMenuProps } from "../../renderer/components/kube-object"
export { KubeObjectMeta } from "../../renderer/components/kube-object/kube-object-meta"; export { KubeObjectMeta } from "../../renderer/components/kube-object/kube-object-meta"
export { KubeObjectListLayout, KubeObjectListLayoutProps } from "../../renderer/components/kube-object/kube-object-list-layout";
export { KubeEventDetails } from "../../renderer/components/+events/kube-event-details" export { KubeEventDetails } from "../../renderer/components/+events/kube-event-details"
// specific exports // specific exports

View File

@ -31,7 +31,7 @@ export class DistributionDetector extends BaseClusterDetector {
if (this.isCustom()) { if (this.isCustom()) {
return { value: "custom", accuracy: 10} return { value: "custom", accuracy: 10}
} }
return { value: "vanilla", accuracy: 10} return { value: "unknown", accuracy: 10}
} }
public async getKubernetesVersion() { public async getKubernetesVersion() {

View File

@ -5,13 +5,12 @@ export class NodesCountDetector extends BaseClusterDetector {
key = ClusterMetadataKey.NODES_COUNT key = ClusterMetadataKey.NODES_COUNT
public async detect() { public async detect() {
if (!this.cluster.accessible) return null;
const nodeCount = await this.getNodeCount() const nodeCount = await this.getNodeCount()
return { value: nodeCount, accuracy: 100} return { value: nodeCount, accuracy: 100}
} }
protected async getNodeCount(): Promise<number> { protected async getNodeCount(): Promise<number> {
if (!this.cluster.accessible) return null;
const response = await this.k8sRequest("/api/v1/nodes") const response = await this.k8sRequest("/api/v1/nodes")
return response.items.length return response.items.length
} }

View File

@ -10,7 +10,7 @@ export class ClusterManager {
constructor(public readonly port: number) { constructor(public readonly port: number) {
// auto-init clusters // auto-init clusters
autorun(() => { autorun(() => {
clusterStore.clusters.forEach(cluster => { clusterStore.enabledClustersList.forEach(cluster => {
if (!cluster.initialized) { if (!cluster.initialized) {
logger.info(`[CLUSTER-MANAGER]: init cluster`, cluster.getMeta()); logger.info(`[CLUSTER-MANAGER]: init cluster`, cluster.getMeta());
cluster.init(port); cluster.init(port);

View File

@ -1,3 +1,4 @@
import { ipcMain } from "electron"
import type { ClusterId, ClusterMetadata, ClusterModel, ClusterPreferences } from "../common/cluster-store" import type { ClusterId, ClusterMetadata, ClusterModel, ClusterPreferences } from "../common/cluster-store"
import type { IMetricsReqParams } from "../renderer/api/endpoints/metrics.api"; import type { IMetricsReqParams } from "../renderer/api/endpoints/metrics.api";
import type { WorkspaceId } from "../common/workspace-store"; import type { WorkspaceId } from "../common/workspace-store";
@ -33,7 +34,7 @@ export type ClusterRefreshOptions = {
refreshMetadata?: boolean refreshMetadata?: boolean
} }
export interface ClusterState extends ClusterModel { export interface ClusterState {
initialized: boolean; initialized: boolean;
apiUrl: string; apiUrl: string;
online: boolean; online: boolean;
@ -47,11 +48,12 @@ export interface ClusterState extends ClusterModel {
allowedResources: string[] allowedResources: string[]
} }
export class Cluster implements ClusterModel { export class Cluster implements ClusterModel, ClusterState {
public id: ClusterId; public id: ClusterId;
public frameId: number; public frameId: number;
public kubeCtl: Kubectl public kubeCtl: Kubectl
public contextHandler: ContextHandler; public contextHandler: ContextHandler;
public ownerRef: string;
protected kubeconfigManager: KubeconfigManager; protected kubeconfigManager: KubeconfigManager;
protected eventDisposers: Function[] = []; protected eventDisposers: Function[] = [];
protected activated = false; protected activated = false;
@ -65,6 +67,7 @@ export class Cluster implements ClusterModel {
@observable kubeConfigPath: string; @observable kubeConfigPath: string;
@observable apiUrl: string; // cluster server url @observable apiUrl: string; // cluster server url
@observable kubeProxyUrl: string; // lens-proxy to kube-api url @observable kubeProxyUrl: string; // lens-proxy to kube-api url
@observable enabled = false;
@observable online = false; @observable online = false;
@observable accessible = false; @observable accessible = false;
@observable ready = false; @observable ready = false;
@ -81,6 +84,7 @@ export class Cluster implements ClusterModel {
@computed get available() { @computed get available() {
return this.accessible && !this.disconnected; return this.accessible && !this.disconnected;
} }
get version(): string { get version(): string {
return String(this.metadata?.version) || "" return String(this.metadata?.version) || ""
} }
@ -93,6 +97,10 @@ export class Cluster implements ClusterModel {
} }
} }
get isManaged(): boolean {
return !!this.ownerRef
}
@action @action
updateModel(model: ClusterModel) { updateModel(model: ClusterModel) {
Object.assign(this, model); Object.assign(this, model);
@ -123,13 +131,15 @@ export class Cluster implements ClusterModel {
const refreshTimer = setInterval(() => !this.disconnected && this.refresh(), 30000); // every 30s const refreshTimer = setInterval(() => !this.disconnected && this.refresh(), 30000); // every 30s
const refreshMetadataTimer = setInterval(() => !this.disconnected && this.refreshMetadata(), 900000); // every 15 minutes const refreshMetadataTimer = setInterval(() => !this.disconnected && this.refreshMetadata(), 900000); // every 15 minutes
this.eventDisposers.push( if (ipcMain) {
reaction(this.getState, this.pushState), this.eventDisposers.push(
() => { reaction(() => this.getState(), () => this.pushState()),
clearInterval(refreshTimer); () => {
clearInterval(refreshMetadataTimer); clearInterval(refreshTimer);
}, clearInterval(refreshMetadataTimer);
); },
);
}
} }
protected unbindEvents() { protected unbindEvents() {
@ -361,6 +371,7 @@ export class Cluster implements ClusterModel {
workspace: this.workspace, workspace: this.workspace,
preferences: this.preferences, preferences: this.preferences,
metadata: this.metadata, metadata: this.metadata,
ownerRef: this.ownerRef
}; };
return toJS(model, { return toJS(model, {
recurseEverything: true recurseEverything: true
@ -368,9 +379,8 @@ export class Cluster implements ClusterModel {
} }
// serializable cluster-state used for sync btw main <-> renderer // serializable cluster-state used for sync btw main <-> renderer
getState = (): ClusterState => { getState(): ClusterState {
const state: ClusterState = { const state: ClusterState = {
...this.toJSON(),
initialized: this.initialized, initialized: this.initialized,
apiUrl: this.apiUrl, apiUrl: this.apiUrl,
online: this.online, online: this.online,
@ -388,14 +398,18 @@ export class Cluster implements ClusterModel {
}) })
} }
pushState = (state = this.getState()): ClusterState => { @action
setState(state: ClusterState) {
Object.assign(this, state)
}
pushState(state = this.getState()) {
logger.silly(`[CLUSTER]: push-state`, state); logger.silly(`[CLUSTER]: push-state`, state);
broadcastIpc({ broadcastIpc({
channel: "cluster:state", channel: "cluster:state",
frameId: this.frameId, frameId: this.frameId,
args: [state], args: [this.id, state],
}); })
return state;
} }
// get cluster system meta, e.g. use in "logger" // get cluster system meta, e.g. use in "logger"

View File

@ -82,6 +82,12 @@ export class LensProxy {
proxySocket.write("\r\n") proxySocket.write("\r\n")
proxySocket.write(head) proxySocket.write(head)
}) })
proxySocket.setKeepAlive(true)
socket.setKeepAlive(true)
proxySocket.setTimeout(0)
socket.setTimeout(0)
proxySocket.on('data', function (chunk) { proxySocket.on('data', function (chunk) {
socket.write(chunk) socket.write(chunk)
}) })

View File

@ -1,4 +1,4 @@
import { app, BrowserWindow, dialog, Menu, MenuItem, MenuItemConstructorOptions, shell, webContents } from "electron" import { app, BrowserWindow, dialog, Menu, MenuItem, MenuItemConstructorOptions, webContents } from "electron"
import { autorun } from "mobx"; import { autorun } from "mobx";
import { WindowManager } from "./window-manager"; import { WindowManager } from "./window-manager";
import { appName, isMac, isWindows } from "../common/vars"; import { appName, isMac, isWindows } from "../common/vars";
@ -193,12 +193,6 @@ export function buildMenu(windowManager: WindowManager) {
navigate(whatsNewURL()) navigate(whatsNewURL())
}, },
}, },
{
label: "License",
click: async () => {
shell.openExternal('https://k8slens.dev/licenses/eula.md');
},
},
...ignoreOnMac([ ...ignoreOnMac([
{ {
label: "About Lens", label: "About Lens",

View File

@ -80,7 +80,7 @@ export function createTrayMenu(windowManager: WindowManager): Menu {
}, },
{ {
label: "Clusters", label: "Clusters",
submenu: workspaceStore.workspacesList submenu: workspaceStore.enabledWorkspacesList
.filter(workspace => clusterStore.getByWorkspaceId(workspace.id).length > 0) // hide empty workspaces .filter(workspace => clusterStore.getByWorkspaceId(workspace.id).length > 0) // hide empty workspaces
.map(workspace => { .map(workspace => {
const clusters = clusterStore.getByWorkspaceId(workspace.id); const clusters = clusterStore.getByWorkspaceId(workspace.id);

View File

@ -1,33 +1 @@
import { observable } from "mobx" export { kubeObjectDetailRegistry } from "../../extensions/registries/kube-object-detail-registry"
import React from "react"
export interface KubeObjectDetailComponents {
Details: React.ComponentType<any>;
}
export interface KubeObjectDetailRegistration {
kind: string;
apiVersions: string[];
components: KubeObjectDetailComponents;
}
export class KubeObjectDetailRegistry {
items = observable.array<KubeObjectDetailRegistration>([], { deep: false });
add(item: KubeObjectDetailRegistration) {
this.items.push(item)
return () => {
this.items.replace(
this.items.filter(c => c !== item)
)
};
}
getItemsForKind(kind: string, apiVersion: string) {
return this.items.filter((item) => {
return item.kind === kind && item.apiVersions.includes(apiVersion)
})
}
}
export const kubeObjectDetailRegistry = new KubeObjectDetailRegistry()

View File

@ -40,6 +40,7 @@ export async function bootstrap(App: AppComponent) {
// Register additional store listeners // Register additional store listeners
clusterStore.registerIpcListener(); clusterStore.registerIpcListener();
workspaceStore.registerIpcListener();
// init app's dependencies if any // init app's dependencies if any
if (App.init) { if (App.init) {

View File

@ -163,7 +163,7 @@ export class AddCluster extends React.Component {
}) })
runInAction(() => { runInAction(() => {
clusterStore.addCluster(...newClusters); clusterStore.addClusters(...newClusters);
if (newClusters.length === 1) { if (newClusters.length === 1) {
const clusterId = newClusters[0].id; const clusterId = newClusters[0].id;
clusterStore.setActive(clusterId); clusterStore.setActive(clusterId);

View File

@ -26,11 +26,11 @@ export class ClusterWorkspaceSetting extends React.Component<Props> {
<Select <Select
value={this.props.cluster.workspace} value={this.props.cluster.workspace}
onChange={({value}) => this.props.cluster.workspace = value} onChange={({value}) => this.props.cluster.workspace = value}
options={workspaceStore.workspacesList.map(w => options={workspaceStore.enabledWorkspacesList.map(w =>
({value: w.id, label: w.name}) ({value: w.id, label: w.name})
)} )}
/> />
</> </>
); );
} }
} }

View File

@ -22,16 +22,17 @@ export class RemoveClusterButton extends React.Component<Props> {
labelOk: <Trans>Yes</Trans>, labelOk: <Trans>Yes</Trans>,
labelCancel: <Trans>No</Trans>, labelCancel: <Trans>No</Trans>,
ok: async () => { ok: async () => {
await clusterStore.removeById(cluster.id); await clusterStore.removeById(cluster.id);
} }
}) })
} }
render() { render() {
const { cluster } = this.props;
return ( return (
<Button accent onClick={this.confirmRemoveCluster} className="button-area"> <Button accent onClick={this.confirmRemoveCluster} className="button-area" disabled={cluster.isManaged}>
Remove Cluster Remove Cluster
</Button> </Button>
); );
} }
} }

View File

@ -19,7 +19,7 @@ export class WorkspaceMenu extends React.Component<Props> {
render() { render() {
const { className, ...menuProps } = this.props; const { className, ...menuProps } = this.props;
const { workspacesList, currentWorkspace } = workspaceStore; const { enabledWorkspacesList, currentWorkspace } = workspaceStore;
return ( return (
<Menu <Menu
{...menuProps} {...menuProps}
@ -32,7 +32,7 @@ export class WorkspaceMenu extends React.Component<Props> {
<Link className="workspaces-title" to={workspacesURL()}> <Link className="workspaces-title" to={workspacesURL()}>
<Trans>Workspaces</Trans> <Trans>Workspaces</Trans>
</Link> </Link>
{workspacesList.map(({ id: workspaceId, name, description }) => { {enabledWorkspacesList.map(({ id: workspaceId, name, description }) => {
return ( return (
<MenuItem <MenuItem
key={workspaceId} key={workspaceId}

View File

@ -19,8 +19,12 @@ export class Workspaces extends React.Component {
@observable editingWorkspaces = observable.map<WorkspaceId, Workspace>(); @observable editingWorkspaces = observable.map<WorkspaceId, Workspace>();
@computed get workspaces(): Workspace[] { @computed get workspaces(): Workspace[] {
const currentWorkspaces: Map<WorkspaceId, Workspace> = new Map()
workspaceStore.enabledWorkspacesList.forEach((w) => {
currentWorkspaces.set(w.id, w)
})
const allWorkspaces = new Map([ const allWorkspaces = new Map([
...workspaceStore.workspaces, ...currentWorkspaces,
...this.editingWorkspaces, ...this.editingWorkspaces,
]); ]);
return Array.from(allWorkspaces.values()); return Array.from(allWorkspaces.values());
@ -42,7 +46,7 @@ export class Workspaces extends React.Component {
saveWorkspace = (id: WorkspaceId) => { saveWorkspace = (id: WorkspaceId) => {
const draft = toJS(this.editingWorkspaces.get(id)); const draft = toJS(this.editingWorkspaces.get(id));
const workspace = workspaceStore.saveWorkspace(draft); const workspace = workspaceStore.addWorkspace(draft);
if (workspace) { if (workspace) {
this.clearEditing(id); this.clearEditing(id);
} }
@ -50,11 +54,11 @@ export class Workspaces extends React.Component {
addWorkspace = () => { addWorkspace = () => {
const workspaceId = uuid(); const workspaceId = uuid();
this.editingWorkspaces.set(workspaceId, { this.editingWorkspaces.set(workspaceId, new Workspace({
id: workspaceId, id: workspaceId,
name: "", name: "",
description: "", description: ""
}) }))
} }
editWorkspace = (id: WorkspaceId) => { editWorkspace = (id: WorkspaceId) => {
@ -76,7 +80,7 @@ export class Workspaces extends React.Component {
}, },
ok: () => { ok: () => {
this.clearEditing(id); this.clearEditing(id);
workspaceStore.removeWorkspace(id); workspaceStore.removeWorkspace(workspace);
}, },
message: ( message: (
<div className="confirm flex column gaps"> <div className="confirm flex column gaps">
@ -107,11 +111,12 @@ export class Workspaces extends React.Component {
<Trans>Workspaces</Trans> <Trans>Workspaces</Trans>
</h2> </h2>
<div className="items flex column gaps"> <div className="items flex column gaps">
{this.workspaces.map(({ id: workspaceId, name, description }) => { {this.workspaces.map(({ id: workspaceId, name, description, ownerRef }) => {
const isActive = workspaceStore.currentWorkspaceId === workspaceId; const isActive = workspaceStore.currentWorkspaceId === workspaceId;
const isDefault = workspaceStore.isDefault(workspaceId); const isDefault = workspaceStore.isDefault(workspaceId);
const isEditing = this.editingWorkspaces.has(workspaceId); const isEditing = this.editingWorkspaces.has(workspaceId);
const editingWorkspace = this.editingWorkspaces.get(workspaceId); const editingWorkspace = this.editingWorkspaces.get(workspaceId);
const managed = !!ownerRef
const className = cssNames("workspace flex gaps", { const className = cssNames("workspace flex gaps", {
active: isActive, active: isActive,
editing: isEditing, editing: isEditing,
@ -130,7 +135,7 @@ export class Workspaces extends React.Component {
{isActive && <span> <Trans>(current)</Trans></span>} {isActive && <span> <Trans>(current)</Trans></span>}
</span> </span>
<span className="description">{description}</span> <span className="description">{description}</span>
{!isDefault && ( {!isDefault && !managed && (
<Fragment> <Fragment>
<Icon <Icon
material="edit" material="edit"

View File

@ -15,7 +15,7 @@ export interface AnimateProps {
@observer @observer
export class Animate extends React.Component<AnimateProps> { export class Animate extends React.Component<AnimateProps> {
static VISIBILITY_DELAY_MS = 100; static VISIBILITY_DELAY_MS = 0;
static defaultProps: AnimateProps = { static defaultProps: AnimateProps = {
name: "opacity", name: "opacity",

View File

@ -4,8 +4,9 @@
font-size: $font-size-small; font-size: $font-size-small;
background-color: #3d90ce; background-color: #3d90ce;
padding: 0 $padding; padding: 0 2px;
color: white; color: white;
height: 22px;
#current-workspace { #current-workspace {
padding: $padding / 4 $padding / 2; padding: $padding / 4 $padding / 2;

View File

@ -14,7 +14,7 @@ export class BottomBar extends React.Component {
return ( return (
<div className="BottomBar flex gaps"> <div className="BottomBar flex gaps">
<div id="current-workspace" className="flex gaps align-center hover-highlight"> <div id="current-workspace" className="flex gaps align-center hover-highlight">
<Icon small material="layers"/> <Icon smallest material="layers"/>
<span className="workspace-name">{currentWorkspace.name}</span> <span className="workspace-name">{currentWorkspace.name}</span>
</div> </div>
<WorkspaceMenu <WorkspaceMenu

View File

@ -101,9 +101,11 @@ export class ClustersMenu extends React.Component<Props> {
} }
render() { render() {
const { className } = this.props; const { className } = this.props
const { newContexts } = userStore; const { newContexts } = userStore
const clusters = clusterStore.getByWorkspaceId(workspaceStore.currentWorkspaceId); const workspace = workspaceStore.getById(workspaceStore.currentWorkspaceId)
const clusters = clusterStore.getByWorkspaceId(workspace.id)
const activeClusterId = clusterStore.activeCluster
return ( return (
<div className={cssNames("ClustersMenu flex column", className)}> <div className={cssNames("ClustersMenu flex column", className)}>
<div className="clusters flex column gaps"> <div className="clusters flex column gaps">
@ -112,7 +114,7 @@ export class ClustersMenu extends React.Component<Props> {
{({ innerRef, droppableProps, placeholder }: DroppableProvided) => ( {({ innerRef, droppableProps, placeholder }: DroppableProvided) => (
<div ref={innerRef} {...droppableProps}> <div ref={innerRef} {...droppableProps}>
{clusters.map((cluster, index) => { {clusters.map((cluster, index) => {
const isActive = cluster.id === clusterStore.activeClusterId; const isActive = cluster.id === activeClusterId;
return ( return (
<Draggable draggableId={cluster.id} index={index} key={cluster.id}> <Draggable draggableId={cluster.id} index={index} key={cluster.id}>
{({ draggableProps, dragHandleProps, innerRef }: DraggableProvided) => ( {({ draggableProps, dragHandleProps, innerRef }: DraggableProvided) => (
@ -136,11 +138,11 @@ export class ClustersMenu extends React.Component<Props> {
</Droppable> </Droppable>
</DragDropContext> </DragDropContext>
</div> </div>
<div className="add-cluster" onClick={this.addCluster}> <div className="add-cluster" >
<Tooltip targetId="add-cluster-icon"> <Tooltip targetId="add-cluster-icon">
<Trans>Add Cluster</Trans> <Trans>Add Cluster</Trans>
</Tooltip> </Tooltip>
<Icon big material="add" id="add-cluster-icon"/> <Icon big material="add" id="add-cluster-icon" disabled={workspace.isManaged} onClick={this.addCluster}/>
{newContexts.size > 0 && ( {newContexts.size > 0 && (
<Badge className="counter" label={newContexts.size} tooltip={<Trans>new</Trans>}/> <Badge className="counter" label={newContexts.size} tooltip={<Trans>new</Trans>}/>
)} )}

View File

@ -1,6 +1,7 @@
.Icon { .Icon {
--size: 21px; --size: 21px;
--small-size: 18px; --small-size: 18px;
--smallest-size: 16px;
--big-size: 32px; --big-size: 32px;
--color-active: #{$iconActiveColor}; --color-active: #{$iconActiveColor};
--bgc-active: #{$iconActiveBackground}; --bgc-active: #{$iconActiveBackground};
@ -21,6 +22,12 @@
width: var(--size); width: var(--size);
height: var(--size); height: var(--size);
&.smallest {
font-size: var(--smallest-size);
width: var(--smallest-size);
height: var(--smallest-size);
}
&.small { &.small {
font-size: var(--small-size); font-size: var(--small-size);
width: var(--small-size); width: var(--small-size);

View File

@ -15,6 +15,7 @@ export interface IconProps extends React.HTMLAttributes<any>, TooltipDecoratorPr
href?: string; // render icon as hyperlink href?: string; // render icon as hyperlink
size?: string | number; // icon-size size?: string | number; // icon-size
small?: boolean; // pre-defined icon-size small?: boolean; // pre-defined icon-size
smallest?: boolean; // pre-defined icon-size
big?: boolean; // pre-defined icon-size big?: boolean; // pre-defined icon-size
active?: boolean; // apply active-state styles active?: boolean; // apply active-state styles
interactive?: boolean; // indicates that icon is interactive and highlight it on focus/hover interactive?: boolean; // indicates that icon is interactive and highlight it on focus/hover
@ -63,7 +64,7 @@ export class Icon extends React.PureComponent<IconProps> {
const { isInteractive } = this; const { isInteractive } = this;
const { const {
// skip passing props to icon's html element // skip passing props to icon's html element
className, href, link, material, svg, size, small, big, className, href, link, material, svg, size, smallest, small, big,
disabled, sticker, active, focusable, children, disabled, sticker, active, focusable, children,
interactive: _interactive, interactive: _interactive,
onClick: _onClick, onClick: _onClick,
@ -75,7 +76,7 @@ export class Icon extends React.PureComponent<IconProps> {
const iconProps: Partial<IconProps> = { const iconProps: Partial<IconProps> = {
className: cssNames("Icon", className, className: cssNames("Icon", className,
{ svg, material, interactive: isInteractive, disabled, sticker, active, focusable }, { svg, material, interactive: isInteractive, disabled, sticker, active, focusable },
!size ? { small, big } : {} !size ? { smallest, small, big } : {}
), ),
onClick: isInteractive ? this.onClick : undefined, onClick: isInteractive ? this.onClick : undefined,
onKeyDown: isInteractive ? this.onKeyDown : undefined, onKeyDown: isInteractive ? this.onKeyDown : undefined,