mirror of
https://github.com/lensapp/lens.git
synced 2025-05-20 05:10:56 +00:00
Merge branch 'master' into extensions-docs
# Conflicts: # src/main/menu.ts
This commit is contained in:
commit
69021b121d
@ -39,6 +39,8 @@ jobs:
|
|||||||
displayName: Install dependencies
|
displayName: Install dependencies
|
||||||
- script: make integration-win
|
- script: make integration-win
|
||||||
displayName: Run integration tests
|
displayName: Run integration tests
|
||||||
|
- script: make test-extensions
|
||||||
|
displayName: Run In-tree Extension tests
|
||||||
- script: make build
|
- script: make build
|
||||||
condition: "and(succeeded(), startsWith(variables['Build.SourceBranch'], 'refs/tags/'))"
|
condition: "and(succeeded(), startsWith(variables['Build.SourceBranch'], 'refs/tags/'))"
|
||||||
displayName: Build
|
displayName: Build
|
||||||
@ -78,6 +80,8 @@ jobs:
|
|||||||
displayName: Run tests
|
displayName: Run tests
|
||||||
- script: make integration-mac
|
- script: make integration-mac
|
||||||
displayName: Run integration tests
|
displayName: Run integration tests
|
||||||
|
- script: make test-extensions
|
||||||
|
displayName: Run In-tree Extension tests
|
||||||
- script: make build
|
- script: make build
|
||||||
condition: "and(succeeded(), startsWith(variables['Build.SourceBranch'], 'refs/tags/'))"
|
condition: "and(succeeded(), startsWith(variables['Build.SourceBranch'], 'refs/tags/'))"
|
||||||
displayName: Build
|
displayName: Build
|
||||||
@ -119,6 +123,8 @@ jobs:
|
|||||||
condition: eq(variables.CACHE_RESTORED, 'true')
|
condition: eq(variables.CACHE_RESTORED, 'true')
|
||||||
- script: make install-deps
|
- script: make install-deps
|
||||||
displayName: Install dependencies
|
displayName: Install dependencies
|
||||||
|
- script: make test-extensions
|
||||||
|
displayName: Run In-tree Extension tests
|
||||||
- script: make lint
|
- script: make lint
|
||||||
displayName: Lint
|
displayName: Lint
|
||||||
- script: make test
|
- script: make test
|
||||||
|
|||||||
@ -28,7 +28,8 @@ module.exports = {
|
|||||||
"src/**/*.ts",
|
"src/**/*.ts",
|
||||||
"integration/**/*.ts",
|
"integration/**/*.ts",
|
||||||
"src/extensions/**/*.ts*",
|
"src/extensions/**/*.ts*",
|
||||||
"extensions/**/*.ts*"
|
"extensions/**/*.ts*",
|
||||||
|
"__mocks__/*.ts",
|
||||||
],
|
],
|
||||||
parser: "@typescript-eslint/parser",
|
parser: "@typescript-eslint/parser",
|
||||||
extends: [
|
extends: [
|
||||||
|
|||||||
17
.github/labeler-config.yml
vendored
Normal file
17
.github/labeler-config.yml
vendored
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
---
|
||||||
|
area/ui:
|
||||||
|
- src/renderer/**/*
|
||||||
|
area/test:
|
||||||
|
- integration/**/*
|
||||||
|
- __mocks__/**/*
|
||||||
|
area/extension:
|
||||||
|
- extensions/**/*
|
||||||
|
- src/extensions/**/*
|
||||||
|
area/documentation:
|
||||||
|
- README.md
|
||||||
|
- docs/**/*
|
||||||
|
area/ci:
|
||||||
|
- .github/workflows/**/*
|
||||||
|
- .azure-pipelines.yml
|
||||||
|
dependencies:
|
||||||
|
- yarn.lock
|
||||||
15
.github/workflows/labeler.yml
vendored
Normal file
15
.github/workflows/labeler.yml
vendored
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
---
|
||||||
|
name: "Pull Request Labeler"
|
||||||
|
|
||||||
|
on:
|
||||||
|
- pull_request
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
triage:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/labeler@v2
|
||||||
|
if: github.repository == 'lensapp/lens'
|
||||||
|
with:
|
||||||
|
repo-token: "${{ secrets.GITHUB_TOKEN }}"
|
||||||
|
configuration-path: .github/labeler-config.yml
|
||||||
15
Makefile
15
Makefile
@ -33,15 +33,15 @@ lint:
|
|||||||
test: download-bins
|
test: download-bins
|
||||||
yarn test
|
yarn test
|
||||||
|
|
||||||
integration-linux:
|
integration-linux: build-extension-types build-extensions
|
||||||
yarn build:linux
|
yarn build:linux
|
||||||
yarn integration
|
yarn integration
|
||||||
|
|
||||||
integration-mac:
|
integration-mac: build-extension-types build-extensions
|
||||||
yarn build:mac
|
yarn build:mac
|
||||||
yarn integration
|
yarn integration
|
||||||
|
|
||||||
integration-win:
|
integration-win: build-extension-types build-extensions
|
||||||
yarn build:win
|
yarn build:win
|
||||||
yarn integration
|
yarn integration
|
||||||
|
|
||||||
@ -58,10 +58,15 @@ endif
|
|||||||
build-extensions:
|
build-extensions:
|
||||||
$(foreach dir, $(wildcard $(EXTENSIONS_DIR)/*), $(MAKE) -C $(dir) build;)
|
$(foreach dir, $(wildcard $(EXTENSIONS_DIR)/*), $(MAKE) -C $(dir) build;)
|
||||||
|
|
||||||
build-npm:
|
test-extensions:
|
||||||
yarn compile:extension-types
|
$(foreach dir, $(wildcard $(EXTENSIONS_DIR)/*), $(MAKE) -C $(dir) test;)
|
||||||
|
|
||||||
|
build-npm: build-extension-types
|
||||||
yarn npm:fix-package-version
|
yarn npm:fix-package-version
|
||||||
|
|
||||||
|
build-extension-types:
|
||||||
|
yarn compile:extension-types
|
||||||
|
|
||||||
publish-npm: build-npm
|
publish-npm: build-npm
|
||||||
npm config set '//registry.npmjs.org/:_authToken' "${NPM_TOKEN}"
|
npm config set '//registry.npmjs.org/:_authToken' "${NPM_TOKEN}"
|
||||||
cd src/extensions/npm/extensions && npm publish --access=public
|
cd src/extensions/npm/extensions && npm publish --access=public
|
||||||
|
|||||||
3
__mocks__/@linguiMacro.ts
Normal file
3
__mocks__/@linguiMacro.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
module.exports = {
|
||||||
|
Trans: ({ children }: { children: React.ReactNode }) => children,
|
||||||
|
};
|
||||||
@ -13,5 +13,8 @@ module.exports = {
|
|||||||
getPath: jest.fn()
|
getPath: jest.fn()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
dialog: jest.fn()
|
dialog: jest.fn(),
|
||||||
|
ipcRenderer: {
|
||||||
|
on: jest.fn()
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@ -6,4 +6,4 @@ import appInfo from "../package.json"
|
|||||||
const packagePath = path.join(__dirname, "../src/extensions/npm/extensions/package.json")
|
const packagePath = path.join(__dirname, "../src/extensions/npm/extensions/package.json")
|
||||||
|
|
||||||
packageInfo.version = appInfo.version
|
packageInfo.version = appInfo.version
|
||||||
fs.writeFileSync(packagePath, JSON.stringify(packageInfo, null, 2))
|
fs.writeFileSync(packagePath, JSON.stringify(packageInfo, null, 2) + "\n")
|
||||||
|
|||||||
@ -1,5 +1,8 @@
|
|||||||
install-deps:
|
install-deps:
|
||||||
npm install
|
yarn install
|
||||||
|
|
||||||
build: install-deps
|
build: install-deps
|
||||||
npm run build
|
yarn run build
|
||||||
|
|
||||||
|
test:
|
||||||
|
yarn run test
|
||||||
|
|||||||
@ -10,7 +10,8 @@
|
|||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "webpack --config webpack.config.js",
|
"build": "webpack --config webpack.config.js",
|
||||||
"dev": "npm run build --watch"
|
"dev": "npm run build --watch",
|
||||||
|
"test": "echo NO TESTS"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"react-open-doodles": "^1.0.5"
|
"react-open-doodles": "^1.0.5"
|
||||||
|
|||||||
8
extensions/license-menu-item/Makefile
Normal file
8
extensions/license-menu-item/Makefile
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
install-deps:
|
||||||
|
yarn install
|
||||||
|
|
||||||
|
build: install-deps
|
||||||
|
yarn run build
|
||||||
|
|
||||||
|
test:
|
||||||
|
yarn run test
|
||||||
13
extensions/license-menu-item/main.ts
Normal file
13
extensions/license-menu-item/main.ts
Normal 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")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
3589
extensions/license-menu-item/package-lock.json
generated
Normal file
3589
extensions/license-menu-item/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
22
extensions/license-menu-item/package.json
Normal file
22
extensions/license-menu-item/package.json
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
{
|
||||||
|
"name": "lens-license",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"description": "License menu item",
|
||||||
|
"main": "dist/main.js",
|
||||||
|
"scripts": {
|
||||||
|
"build": "webpack -p",
|
||||||
|
"dev": "webpack --watch",
|
||||||
|
"test": "echo NO TESTS"
|
||||||
|
},
|
||||||
|
"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"
|
||||||
|
}
|
||||||
|
}
|
||||||
19
extensions/license-menu-item/tsconfig.json
Normal file
19
extensions/license-menu-item/tsconfig.json
Normal 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"
|
||||||
|
}
|
||||||
|
}
|
||||||
34
extensions/license-menu-item/webpack.config.ts
Normal file
34
extensions/license-menu-item/webpack.config.ts
Normal 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,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
@ -1,5 +1,8 @@
|
|||||||
install-deps:
|
install-deps:
|
||||||
npm install
|
yarn install
|
||||||
|
|
||||||
build: install-deps
|
build: install-deps
|
||||||
npm run build
|
yarn run build
|
||||||
|
|
||||||
|
test:
|
||||||
|
yarn run test
|
||||||
|
|||||||
@ -9,7 +9,8 @@
|
|||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "webpack --config webpack.config.js",
|
"build": "webpack --config webpack.config.js",
|
||||||
"dev": "npm run build --watch"
|
"dev": "npm run build --watch",
|
||||||
|
"test": "echo NO TESTS"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"semver": "^7.3.2"
|
"semver": "^7.3.2"
|
||||||
|
|||||||
@ -1,5 +1,8 @@
|
|||||||
install-deps:
|
install-deps:
|
||||||
npm install
|
yarn install
|
||||||
|
|
||||||
build: install-deps
|
build: install-deps
|
||||||
npm run build
|
yarn run build
|
||||||
|
|
||||||
|
test:
|
||||||
|
yarn run test
|
||||||
|
|||||||
@ -9,7 +9,8 @@
|
|||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "webpack --config webpack.config.js",
|
"build": "webpack --config webpack.config.js",
|
||||||
"dev": "npm run build --watch"
|
"dev": "npm run build --watch",
|
||||||
|
"test": "echo NO TESTS"
|
||||||
},
|
},
|
||||||
"dependencies": {},
|
"dependencies": {},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|||||||
@ -1,5 +1,8 @@
|
|||||||
install-deps:
|
install-deps:
|
||||||
npm install
|
yarn install
|
||||||
|
|
||||||
build: install-deps
|
build: install-deps
|
||||||
npm run build
|
yarn run build
|
||||||
|
|
||||||
|
test:
|
||||||
|
yarn run test
|
||||||
|
|||||||
@ -9,7 +9,8 @@
|
|||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "webpack --config webpack.config.js",
|
"build": "webpack --config webpack.config.js",
|
||||||
"dev": "npm run build --watch"
|
"dev": "npm run build --watch",
|
||||||
|
"test": "echo NO TESTS"
|
||||||
},
|
},
|
||||||
"dependencies": {},
|
"dependencies": {},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|||||||
@ -22,7 +22,7 @@ export class PodLogsMenu extends React.Component<PodLogsMenuProps> {
|
|||||||
const { object: pod, toolbar } = this.props
|
const { object: pod, toolbar } = this.props
|
||||||
const containers = pod.getAllContainers();
|
const containers = pod.getAllContainers();
|
||||||
const statuses = pod.getContainerStatuses();
|
const statuses = pod.getContainerStatuses();
|
||||||
if (!containers.length) return;
|
if (!containers.length) return null;
|
||||||
return (
|
return (
|
||||||
<Component.MenuItem onClick={Util.prevDefault(() => this.showLogs(containers[0]))}>
|
<Component.MenuItem onClick={Util.prevDefault(() => this.showLogs(containers[0]))}>
|
||||||
<Component.Icon material="subject" title="Logs" interactive={toolbar}/>
|
<Component.Icon material="subject" title="Logs" interactive={toolbar}/>
|
||||||
|
|||||||
@ -34,7 +34,7 @@ export class PodShellMenu extends React.Component<PodShellMenuProps> {
|
|||||||
render() {
|
render() {
|
||||||
const { object, toolbar } = this.props
|
const { object, toolbar } = this.props
|
||||||
const containers = object.getRunningContainers();
|
const containers = object.getRunningContainers();
|
||||||
if (!containers.length) return;
|
if (!containers.length) return null;
|
||||||
return (
|
return (
|
||||||
<Component.MenuItem onClick={Util.prevDefault(() => this.execShell(containers[0].name))}>
|
<Component.MenuItem onClick={Util.prevDefault(() => this.execShell(containers[0].name))}>
|
||||||
<Component.Icon svg="ssh" interactive={toolbar} title="Pod shell"/>
|
<Component.Icon svg="ssh" interactive={toolbar} title="Pod shell"/>
|
||||||
|
|||||||
@ -1,5 +1,8 @@
|
|||||||
install-deps:
|
install-deps:
|
||||||
npm install
|
yarn install
|
||||||
|
|
||||||
build: install-deps
|
build: install-deps
|
||||||
npm run build
|
yarn run build
|
||||||
|
|
||||||
|
test:
|
||||||
|
yarn run test
|
||||||
|
|||||||
296
extensions/support-page/package-lock.json
generated
296
extensions/support-page/package-lock.json
generated
@ -20,10 +20,10 @@
|
|||||||
"integrity": "sha512-S78QIYirQcUoo6UJZx9CSP0O2ix9IaeAXwQi26Rhr/+mg7qqPy8TzaxHSUut7eGjL8WmLccT7/MXf304WjqHcA==",
|
"integrity": "sha512-S78QIYirQcUoo6UJZx9CSP0O2ix9IaeAXwQi26Rhr/+mg7qqPy8TzaxHSUut7eGjL8WmLccT7/MXf304WjqHcA==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"@types/node": {
|
"@types/json-schema": {
|
||||||
"version": "14.11.11",
|
"version": "7.0.6",
|
||||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-14.11.11.tgz",
|
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.6.tgz",
|
||||||
"integrity": "sha512-UcaAZrL8uO5GNS+NLxkYg1RiOMgdLxCXGqs+TTupltXN8rTvUEKTOpqCV3tlcAIZJXzcBQajzmjdrvuPvnuMUw==",
|
"integrity": "sha512-3c+yGKvVP5Y9TYBEibGNR+kLtijnj7mYrXRg+WpFb2X9xm04g/DXYkfg4hmzJQosc9snFNUPkbYIhu+KAm6jJw==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"@types/prop-types": {
|
"@types/prop-types": {
|
||||||
@ -741,6 +741,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 +848,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 +992,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 +1677,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 +1701,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 +1899,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 +2167,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 +2439,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 +2763,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 +3121,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 +3335,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",
|
||||||
|
|||||||
@ -6,17 +6,20 @@
|
|||||||
"renderer": "dist/renderer.js",
|
"renderer": "dist/renderer.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "webpack -p",
|
"build": "webpack -p",
|
||||||
"dev": "webpack --watch"
|
"dev": "webpack --watch",
|
||||||
|
"test": "echo NO TESTS"
|
||||||
},
|
},
|
||||||
"dependencies": {},
|
"dependencies": {},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/node": "^14.11.11",
|
"@k8slens/extensions": "file:../../src/extensions/npm/extensions",
|
||||||
"@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",
|
||||||
|
|||||||
@ -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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
13
extensions/support-page/src/support.scss
Normal file
13
extensions/support-page/src/support.scss
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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";
|
||||||
|
|||||||
@ -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: [
|
||||||
|
|||||||
@ -1,5 +1,8 @@
|
|||||||
install-deps:
|
install-deps:
|
||||||
npm install
|
yarn install
|
||||||
|
|
||||||
build: install-deps
|
build: install-deps
|
||||||
npm run build
|
yarn run build
|
||||||
|
|
||||||
|
test:
|
||||||
|
yarn run test
|
||||||
|
|||||||
@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
123
extensions/telemetry/package-lock.json
generated
123
extensions/telemetry/package-lock.json
generated
@ -8,6 +8,22 @@
|
|||||||
"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"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"@types/analytics-node": {
|
||||||
|
"version": "3.1.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/analytics-node/-/analytics-node-3.1.3.tgz",
|
||||||
|
"integrity": "sha512-Yk299LUqnyJ6fNYQkLFd0yTfUwIvgfxH3f5WEX3ib0PC5T+mZgqcOPMDhNZ4AOD/A9tXKJQeBIb6KvgzuXflaQ==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"@webassemblyjs/ast": {
|
"@webassemblyjs/ast": {
|
||||||
"version": "1.9.0",
|
"version": "1.9.0",
|
||||||
"resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.9.0.tgz",
|
"resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.9.0.tgz",
|
||||||
@ -225,6 +241,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 +406,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 +746,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 +869,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 +976,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 +1445,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 +1872,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 +1914,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 +2010,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 +2067,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 +2732,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",
|
||||||
|
|||||||
@ -10,17 +10,20 @@
|
|||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "webpack -p",
|
"build": "webpack -p",
|
||||||
"dev": "webpack --watch"
|
"dev": "webpack --watch",
|
||||||
|
"test": "echo NO TESTS"
|
||||||
},
|
},
|
||||||
"dependencies": {},
|
"dependencies": {},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@k8slens/extensions": "file:../../src/extensions/npm/extensions",
|
"@k8slens/extensions": "file:../../src/extensions/npm/extensions",
|
||||||
|
"@types/analytics-node": "^3.1.3",
|
||||||
"ts-loader": "^8.0.4",
|
"ts-loader": "^8.0.4",
|
||||||
"typescript": "^4.0.3",
|
"typescript": "^4.0.3",
|
||||||
"webpack": "^4.44.2",
|
"webpack": "^4.44.2",
|
||||||
"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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -549,6 +549,14 @@ msgstr "Condition"
|
|||||||
msgid "Conditions"
|
msgid "Conditions"
|
||||||
msgstr "Conditions"
|
msgstr "Conditions"
|
||||||
|
|
||||||
|
#: src/renderer/components/+workloads-deployments/deployments.tsx: 118
|
||||||
|
msgid "Restart"
|
||||||
|
msgstr "Restart"
|
||||||
|
|
||||||
|
#: src/renderer/components/+workloads-deployments/deployments.tsx: 121
|
||||||
|
msgid "Are you sure you want to restart deployment <0>{0}</0>?"
|
||||||
|
msgstr "Are you sure you want to restart deployment <0>{0}</0>?"
|
||||||
|
|
||||||
#: src/renderer/components/+config-maps/config-maps.tsx:33
|
#: src/renderer/components/+config-maps/config-maps.tsx:33
|
||||||
msgid "Config Maps"
|
msgid "Config Maps"
|
||||||
msgstr "Config Maps"
|
msgstr "Config Maps"
|
||||||
|
|||||||
@ -545,6 +545,14 @@ msgstr ""
|
|||||||
msgid "Conditions"
|
msgid "Conditions"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/renderer/components/+workloads-deployments/deployments.tsx: 118
|
||||||
|
msgid "Restart"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/renderer/components/+workloads-deployments/deployments.tsx: 121
|
||||||
|
msgid "Are you sure you want to restart deployment <0>{0}</0>?"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/renderer/components/+config-maps/config-maps.tsx:33
|
#: src/renderer/components/+config-maps/config-maps.tsx:33
|
||||||
msgid "Config Maps"
|
msgid "Config Maps"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|||||||
@ -550,6 +550,14 @@ msgstr "Состояние"
|
|||||||
msgid "Conditions"
|
msgid "Conditions"
|
||||||
msgstr "Состояния"
|
msgstr "Состояния"
|
||||||
|
|
||||||
|
#: src/renderer/components/+workloads-deployments/deployments.tsx: 118
|
||||||
|
msgid "Restart"
|
||||||
|
msgstr "Перезагрузка"
|
||||||
|
|
||||||
|
#: src/renderer/components/+workloads-deployments/deployments.tsx: 121
|
||||||
|
msgid "Are you sure you want to restart deployment <0>{0}</0>?"
|
||||||
|
msgstr "Выполнить перезагрузку деплоймента <0>{0}</0>?"
|
||||||
|
|
||||||
#: src/renderer/components/+config-maps/config-maps.tsx:33
|
#: src/renderer/components/+config-maps/config-maps.tsx:33
|
||||||
msgid "Config Maps"
|
msgid "Config Maps"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|||||||
12
package.json
12
package.json
@ -2,7 +2,7 @@
|
|||||||
"name": "kontena-lens",
|
"name": "kontena-lens",
|
||||||
"productName": "Lens",
|
"productName": "Lens",
|
||||||
"description": "Lens - The Kubernetes IDE",
|
"description": "Lens - The Kubernetes IDE",
|
||||||
"version": "4.0.0-alpha.2",
|
"version": "4.0.0-alpha.4",
|
||||||
"main": "static/build/main.js",
|
"main": "static/build/main.js",
|
||||||
"copyright": "© 2020, Mirantis, Inc.",
|
"copyright": "© 2020, Mirantis, Inc.",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
@ -72,10 +72,14 @@
|
|||||||
"^.+\\.tsx?$": "ts-jest"
|
"^.+\\.tsx?$": "ts-jest"
|
||||||
},
|
},
|
||||||
"moduleNameMapper": {
|
"moduleNameMapper": {
|
||||||
"\\.(css|scss)$": "<rootDir>/__mocks__/styleMock.ts"
|
"\\.(css|scss)$": "<rootDir>/__mocks__/styleMock.ts",
|
||||||
|
"^@lingui/macro$": "<rootDir>/__mocks__/@linguiMacro.ts"
|
||||||
},
|
},
|
||||||
"modulePathIgnorePatterns": [
|
"modulePathIgnorePatterns": [
|
||||||
"<rootDir>/dist"
|
"<rootDir>/dist"
|
||||||
|
],
|
||||||
|
"setupFiles": [
|
||||||
|
"<rootDir>/src/jest.setup.ts"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"build": {
|
"build": {
|
||||||
@ -186,6 +190,7 @@
|
|||||||
"pod-menu",
|
"pod-menu",
|
||||||
"node-menu",
|
"node-menu",
|
||||||
"metrics-cluster-feature",
|
"metrics-cluster-feature",
|
||||||
|
"license-menu-item",
|
||||||
"support-page"
|
"support-page"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@ -264,6 +269,8 @@
|
|||||||
"@lingui/react": "^3.0.0-13",
|
"@lingui/react": "^3.0.0-13",
|
||||||
"@material-ui/core": "^4.10.1",
|
"@material-ui/core": "^4.10.1",
|
||||||
"@rollup/plugin-json": "^4.1.0",
|
"@rollup/plugin-json": "^4.1.0",
|
||||||
|
"@testing-library/jest-dom": "^5.11.5",
|
||||||
|
"@testing-library/react": "^11.1.0",
|
||||||
"@types/chart.js": "^2.9.21",
|
"@types/chart.js": "^2.9.21",
|
||||||
"@types/circular-dependency-plugin": "^5.0.1",
|
"@types/circular-dependency-plugin": "^5.0.1",
|
||||||
"@types/color": "^3.0.1",
|
"@types/color": "^3.0.1",
|
||||||
@ -337,6 +344,7 @@
|
|||||||
"identity-obj-proxy": "^3.0.0",
|
"identity-obj-proxy": "^3.0.0",
|
||||||
"include-media": "^1.4.9",
|
"include-media": "^1.4.9",
|
||||||
"jest": "^26.0.1",
|
"jest": "^26.0.1",
|
||||||
|
"jest-fetch-mock": "^3.0.3",
|
||||||
"jest-mock-extended": "^1.0.10",
|
"jest-mock-extended": "^1.0.10",
|
||||||
"make-plural": "^6.2.1",
|
"make-plural": "^6.2.1",
|
||||||
"mini-css-extract-plugin": "^0.9.0",
|
"mini-css-extract-plugin": "^0.9.0",
|
||||||
|
|||||||
@ -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",
|
||||||
|
|||||||
@ -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
|
||||||
})
|
})
|
||||||
|
|||||||
@ -2,7 +2,7 @@ import path from "path"
|
|||||||
import Config from "conf"
|
import Config from "conf"
|
||||||
import { Options as ConfOptions } from "conf/dist/source/types"
|
import { Options as ConfOptions } from "conf/dist/source/types"
|
||||||
import { app, ipcMain, IpcMainEvent, ipcRenderer, IpcRendererEvent, remote } from "electron"
|
import { app, ipcMain, IpcMainEvent, ipcRenderer, IpcRendererEvent, remote } from "electron"
|
||||||
import { action, observable, reaction, runInAction, toJS, when } from "mobx";
|
import { action, IReactionOptions, observable, reaction, runInAction, toJS, when } from "mobx";
|
||||||
import Singleton from "./utils/singleton";
|
import Singleton from "./utils/singleton";
|
||||||
import { getAppVersion } from "./utils/app-version";
|
import { getAppVersion } from "./utils/app-version";
|
||||||
import logger from "../main/logger";
|
import logger from "../main/logger";
|
||||||
@ -12,6 +12,7 @@ import isEqual from "lodash/isEqual";
|
|||||||
export interface BaseStoreParams<T = any> extends ConfOptions<T> {
|
export interface BaseStoreParams<T = any> extends ConfOptions<T> {
|
||||||
autoLoad?: boolean;
|
autoLoad?: boolean;
|
||||||
syncEnabled?: boolean;
|
syncEnabled?: boolean;
|
||||||
|
syncOptions?: IReactionOptions;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class BaseStore<T = any> extends Singleton {
|
export class BaseStore<T = any> extends Singleton {
|
||||||
@ -20,7 +21,7 @@ export class BaseStore<T = any> extends Singleton {
|
|||||||
|
|
||||||
whenLoaded = when(() => this.isLoaded);
|
whenLoaded = when(() => this.isLoaded);
|
||||||
@observable isLoaded = false;
|
@observable isLoaded = false;
|
||||||
@observable protected data: T;
|
@observable data = {} as T;
|
||||||
|
|
||||||
protected constructor(protected params: BaseStoreParams) {
|
protected constructor(protected params: BaseStoreParams) {
|
||||||
super();
|
super();
|
||||||
@ -36,8 +37,12 @@ export class BaseStore<T = any> extends Singleton {
|
|||||||
return path.basename(this.storeConfig.path);
|
return path.basename(this.storeConfig.path);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get path() {
|
||||||
|
return this.storeConfig.path;
|
||||||
|
}
|
||||||
|
|
||||||
get syncChannel() {
|
get syncChannel() {
|
||||||
return `store-sync:${this.name}`
|
return `STORE-SYNC:${this.path}`
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async init() {
|
protected async init() {
|
||||||
@ -56,19 +61,19 @@ export class BaseStore<T = any> extends Singleton {
|
|||||||
...confOptions,
|
...confOptions,
|
||||||
projectName: "lens",
|
projectName: "lens",
|
||||||
projectVersion: getAppVersion(),
|
projectVersion: getAppVersion(),
|
||||||
cwd: this.storePath(),
|
cwd: this.cwd(),
|
||||||
});
|
});
|
||||||
logger.info(`[STORE]: LOADED from ${this.storeConfig.path}`);
|
logger.info(`[STORE]: LOADED from ${this.path}`);
|
||||||
this.fromStore(this.storeConfig.store);
|
this.fromStore(this.storeConfig.store);
|
||||||
this.isLoaded = true;
|
this.isLoaded = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected storePath() {
|
protected cwd() {
|
||||||
return (app || remote.app).getPath("userData")
|
return (app || remote.app).getPath("userData")
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async saveToFile(model: T) {
|
protected async saveToFile(model: T) {
|
||||||
logger.info(`[STORE]: SAVING ${this.name}`);
|
logger.info(`[STORE]: SAVING ${this.path}`);
|
||||||
// todo: update when fixed https://github.com/sindresorhus/conf/issues/114
|
// todo: update when fixed https://github.com/sindresorhus/conf/issues/114
|
||||||
Object.entries(model).forEach(([key, value]) => {
|
Object.entries(model).forEach(([key, value]) => {
|
||||||
this.storeConfig.set(key, value);
|
this.storeConfig.set(key, value);
|
||||||
@ -77,7 +82,7 @@ export class BaseStore<T = any> extends Singleton {
|
|||||||
|
|
||||||
enableSync() {
|
enableSync() {
|
||||||
this.syncDisposers.push(
|
this.syncDisposers.push(
|
||||||
reaction(() => this.toJSON(), model => this.onModelChange(model)),
|
reaction(() => this.toJSON(), model => this.onModelChange(model), this.params.syncOptions),
|
||||||
);
|
);
|
||||||
if (ipcMain) {
|
if (ipcMain) {
|
||||||
const callback = (event: IpcMainEvent, model: T) => {
|
const callback = (event: IpcMainEvent, model: T) => {
|
||||||
@ -169,6 +174,7 @@ export class BaseStore<T = any> extends Singleton {
|
|||||||
|
|
||||||
@action
|
@action
|
||||||
protected fromStore(data: T) {
|
protected fromStore(data: T) {
|
||||||
|
if (!data) return;
|
||||||
this.data = data;
|
this.data = data;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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"
|
||||||
|
|||||||
6
src/common/utils/openExternal.ts
Normal file
6
src/common/utils/openExternal.ts
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
// Opens a link in external browser
|
||||||
|
import { shell } from "electron"
|
||||||
|
|
||||||
|
export function openExternal(url: string) {
|
||||||
|
return shell.openExternal(url);
|
||||||
|
}
|
||||||
@ -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
|
||||||
})
|
})
|
||||||
|
|||||||
@ -1 +1,2 @@
|
|||||||
export { ClusterFeature as Feature, ClusterFeatureStatus as FeatureStatus } from "../cluster-feature"
|
export { ClusterFeature as Feature } from "../cluster-feature"
|
||||||
|
export type { ClusterFeatureStatus as FeatureStatus } from "../cluster-feature"
|
||||||
|
|||||||
@ -1,4 +1,6 @@
|
|||||||
export { ExtensionStore } from "../extension-store"
|
export { ExtensionStore } from "../extension-store"
|
||||||
export { clusterStore, ClusterModel } from "../../common/cluster-store"
|
export { clusterStore } from "../../common/cluster-store"
|
||||||
export { workspaceStore} from "../../common/workspace-store"
|
export type { ClusterModel } from "../../common/cluster-store"
|
||||||
export type { Cluster } from "../../main/cluster"
|
export { Cluster } from "../../main/cluster"
|
||||||
|
export { workspaceStore, Workspace } from "../../common/workspace-store"
|
||||||
|
export type { WorkspaceModel } from "../../common/workspace-store"
|
||||||
|
|||||||
@ -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"
|
||||||
|
|||||||
@ -1,17 +1,13 @@
|
|||||||
import type { ExtensionId, ExtensionManifest, ExtensionModel, LensExtension } from "./lens-extension"
|
import type { LensExtension, LensExtensionConstructor, LensExtensionId } from "./lens-extension"
|
||||||
import type { LensMainExtension } from "./lens-main-extension"
|
import type { LensMainExtension } from "./lens-main-extension"
|
||||||
import type { LensRendererExtension } from "./lens-renderer-extension"
|
import type { LensRendererExtension } from "./lens-renderer-extension"
|
||||||
|
import type { InstalledExtension } from "./extension-manager";
|
||||||
import path from "path"
|
import path from "path"
|
||||||
import { broadcastIpc } from "../common/ipc"
|
import { broadcastIpc } from "../common/ipc"
|
||||||
import { observable, reaction, toJS, } from "mobx"
|
import { computed, observable, reaction, when } 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 * as registries from "./registries";
|
||||||
|
|
||||||
export interface InstalledExtension extends ExtensionModel {
|
|
||||||
manifestPath: string;
|
|
||||||
manifest: ExtensionManifest;
|
|
||||||
}
|
|
||||||
|
|
||||||
// lazy load so that we get correct userData
|
// lazy load so that we get correct userData
|
||||||
export function extensionPackagesRoot() {
|
export function extensionPackagesRoot() {
|
||||||
@ -19,68 +15,82 @@ export function extensionPackagesRoot() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export class ExtensionLoader {
|
export class ExtensionLoader {
|
||||||
@observable extensions = observable.map<ExtensionId, InstalledExtension>([], { deep: false });
|
@observable isLoaded = false;
|
||||||
@observable instances = observable.map<ExtensionId, LensExtension>([], { deep: false })
|
protected extensions = observable.map<LensExtensionId, InstalledExtension>([], { deep: false });
|
||||||
|
protected instances = observable.map<LensExtensionId, LensExtension>([], { deep: false })
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
if (ipcRenderer) {
|
if (ipcRenderer) {
|
||||||
ipcRenderer.on("extensions:loaded", (event, extensions: InstalledExtension[]) => {
|
ipcRenderer.on("extensions:loaded", (event, extensions: InstalledExtension[]) => {
|
||||||
|
this.isLoaded = true;
|
||||||
extensions.forEach((ext) => {
|
extensions.forEach((ext) => {
|
||||||
if (!this.getById(ext.manifestPath)) {
|
if (!this.extensions.has(ext.manifestPath)) {
|
||||||
this.extensions.set(ext.manifestPath, ext)
|
this.extensions.set(ext.manifestPath, ext)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@computed get userExtensions(): LensExtension[] {
|
||||||
|
return [...this.instances.values()].filter(ext => !ext.isBundled)
|
||||||
|
}
|
||||||
|
|
||||||
|
async init() {
|
||||||
|
const { extensionManager } = await import("./extension-manager");
|
||||||
|
const installedExtensions = await extensionManager.load();
|
||||||
|
this.extensions.replace(installedExtensions);
|
||||||
|
this.isLoaded = true;
|
||||||
|
this.loadOnMain();
|
||||||
|
}
|
||||||
|
|
||||||
loadOnMain() {
|
loadOnMain() {
|
||||||
logger.info('[EXTENSIONS-LOADER]: load on main')
|
logger.info('[EXTENSIONS-LOADER]: load on main')
|
||||||
this.autoloadExtensions((extension: LensMainExtension) => {
|
this.autoInitExtensions((extension: LensMainExtension) => [
|
||||||
extension.registerTo(menuRegistry, extension.appMenus)
|
registries.menuRegistry.add(...extension.appMenus)
|
||||||
})
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
loadOnClusterManagerRenderer() {
|
loadOnClusterManagerRenderer() {
|
||||||
logger.info('[EXTENSIONS-LOADER]: load on main renderer (cluster manager)')
|
logger.info('[EXTENSIONS-LOADER]: load on main renderer (cluster manager)')
|
||||||
this.autoloadExtensions((extension: LensRendererExtension) => {
|
this.autoInitExtensions((extension: LensRendererExtension) => [
|
||||||
extension.registerTo(globalPageRegistry, extension.globalPages)
|
registries.globalPageRegistry.add(...extension.globalPages),
|
||||||
extension.registerTo(appPreferenceRegistry, extension.appPreferences)
|
registries.appPreferenceRegistry.add(...extension.appPreferences),
|
||||||
extension.registerTo(clusterFeatureRegistry, extension.clusterFeatures)
|
registries.clusterFeatureRegistry.add(...extension.clusterFeatures),
|
||||||
extension.registerTo(statusBarRegistry, extension.statusBarItems)
|
registries.statusBarRegistry.add(...extension.statusBarItems),
|
||||||
})
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
loadOnClusterRenderer() {
|
loadOnClusterRenderer() {
|
||||||
logger.info('[EXTENSIONS-LOADER]: load on cluster renderer (dashboard)')
|
logger.info('[EXTENSIONS-LOADER]: load on cluster renderer (dashboard)')
|
||||||
this.autoloadExtensions((extension: LensRendererExtension) => {
|
this.autoInitExtensions((extension: LensRendererExtension) => [
|
||||||
extension.registerTo(clusterPageRegistry, extension.clusterPages)
|
registries.clusterPageRegistry.add(...extension.clusterPages),
|
||||||
extension.registerTo(kubeObjectMenuRegistry, extension.kubeObjectMenuItems)
|
registries.kubeObjectMenuRegistry.add(...extension.kubeObjectMenuItems),
|
||||||
})
|
registries.kubeObjectDetailRegistry.add(...extension.kubeObjectDetailItems),
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected autoloadExtensions(callback: (instance: LensExtension) => void) {
|
protected autoInitExtensions(register: (ext: LensExtension) => Function[]) {
|
||||||
return reaction(() => this.extensions.toJS(), (installedExtensions) => {
|
return reaction(() => this.extensions.toJS(), (installedExtensions) => {
|
||||||
for(const [id, ext] of installedExtensions) {
|
for (const [id, ext] of installedExtensions) {
|
||||||
let instance = this.instances.get(ext.id)
|
let instance = this.instances.get(ext.manifestPath)
|
||||||
if (!instance) {
|
if (!instance) {
|
||||||
const extensionModule = this.requireExtension(ext)
|
const extensionModule = this.requireExtension(ext)
|
||||||
if (!extensionModule) {
|
if (!extensionModule) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
const LensExtensionClass = extensionModule.default;
|
|
||||||
instance = new LensExtensionClass({ ...ext.manifest, manifestPath: ext.manifestPath, id: ext.manifestPath }, ext.manifest);
|
|
||||||
try {
|
try {
|
||||||
instance.enable()
|
const LensExtensionClass: LensExtensionConstructor = extensionModule.default;
|
||||||
callback(instance)
|
instance = new LensExtensionClass(ext);
|
||||||
} finally {
|
instance.whenEnabled(() => register(instance));
|
||||||
this.instances.set(ext.id, instance)
|
this.instances.set(ext.manifestPath, instance);
|
||||||
|
} catch (err) {
|
||||||
|
logger.error(`[EXTENSIONS-LOADER]: init extension instance error`, { ext, err })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, {
|
}, {
|
||||||
fireImmediately: true,
|
fireImmediately: true,
|
||||||
delay: 0,
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -101,37 +111,17 @@ export class ExtensionLoader {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
getById(id: ExtensionId): InstalledExtension {
|
async broadcastExtensions(frameId?: number) {
|
||||||
return this.extensions.get(id);
|
await when(() => this.isLoaded);
|
||||||
}
|
|
||||||
|
|
||||||
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({
|
broadcastIpc({
|
||||||
channel: "extensions:loaded",
|
channel: "extensions:loaded",
|
||||||
frameId: frameId,
|
frameId: frameId,
|
||||||
frameOnly: !!frameId,
|
frameOnly: !!frameId,
|
||||||
args: [this.toJSON().extensions],
|
args: [
|
||||||
})
|
Array.from(this.extensions.toJS().values())
|
||||||
}
|
],
|
||||||
|
|
||||||
toJSON() {
|
|
||||||
return toJS({
|
|
||||||
extensions: Array.from(this.extensions).map(([id, instance]) => instance),
|
|
||||||
}, {
|
|
||||||
recurseEverything: true,
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const extensionLoader = new ExtensionLoader()
|
export const extensionLoader = new ExtensionLoader();
|
||||||
|
|||||||
@ -1,12 +1,18 @@
|
|||||||
import type { ExtensionManifest } from "./lens-extension"
|
import type { LensExtensionId, LensExtensionManifest } from "./lens-extension"
|
||||||
import path from "path"
|
import path from "path"
|
||||||
import os from "os"
|
import os from "os"
|
||||||
import fs from "fs-extra"
|
import fs from "fs-extra"
|
||||||
|
import child_process from "child_process";
|
||||||
import logger from "../main/logger"
|
import logger from "../main/logger"
|
||||||
import { extensionPackagesRoot, InstalledExtension } from "./extension-loader"
|
import { extensionPackagesRoot } from "./extension-loader"
|
||||||
import * as child_process from 'child_process';
|
|
||||||
import { getBundledExtensions } from "../common/utils/app-version"
|
import { getBundledExtensions } from "../common/utils/app-version"
|
||||||
|
|
||||||
|
export interface InstalledExtension {
|
||||||
|
manifest: LensExtensionManifest;
|
||||||
|
manifestPath: string;
|
||||||
|
isBundled?: boolean; // defined in package.json
|
||||||
|
}
|
||||||
|
|
||||||
type Dependencies = {
|
type Dependencies = {
|
||||||
[name: string]: string;
|
[name: string]: string;
|
||||||
}
|
}
|
||||||
@ -17,6 +23,8 @@ type PackageJson = {
|
|||||||
|
|
||||||
export class ExtensionManager {
|
export class ExtensionManager {
|
||||||
|
|
||||||
|
protected bundledFolderPath: string
|
||||||
|
|
||||||
protected packagesJson: PackageJson = {
|
protected packagesJson: PackageJson = {
|
||||||
dependencies: {}
|
dependencies: {}
|
||||||
}
|
}
|
||||||
@ -45,32 +53,41 @@ export class ExtensionManager {
|
|||||||
return __non_webpack_require__.resolve('npm/bin/npm-cli')
|
return __non_webpack_require__.resolve('npm/bin/npm-cli')
|
||||||
}
|
}
|
||||||
|
|
||||||
async load() {
|
get packageJsonPath() {
|
||||||
|
return path.join(this.extensionPackagesRoot, "package.json")
|
||||||
|
}
|
||||||
|
|
||||||
|
async load(): Promise<Map<LensExtensionId, InstalledExtension>> {
|
||||||
logger.info("[EXTENSION-MANAGER] loading extensions from " + this.extensionPackagesRoot)
|
logger.info("[EXTENSION-MANAGER] loading extensions from " + this.extensionPackagesRoot)
|
||||||
if (this.inTreeFolderPath !== this.inTreeTargetPath) {
|
if (fs.existsSync(path.join(this.extensionPackagesRoot, "package-lock.json"))) {
|
||||||
|
await fs.remove(path.join(this.extensionPackagesRoot, "package-lock.json"))
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
await fs.access(this.inTreeFolderPath, fs.constants.W_OK)
|
||||||
|
this.bundledFolderPath = this.inTreeFolderPath
|
||||||
|
} catch {
|
||||||
// we need to copy in-tree extensions so that we can symlink them properly on "npm install"
|
// we need to copy in-tree extensions so that we can symlink them properly on "npm install"
|
||||||
await fs.remove(this.inTreeTargetPath)
|
await fs.remove(this.inTreeTargetPath)
|
||||||
await fs.ensureDir(this.inTreeTargetPath)
|
await fs.ensureDir(this.inTreeTargetPath)
|
||||||
await fs.copy(this.inTreeFolderPath, this.inTreeTargetPath)
|
await fs.copy(this.inTreeFolderPath, this.inTreeTargetPath)
|
||||||
|
this.bundledFolderPath = this.inTreeTargetPath
|
||||||
}
|
}
|
||||||
await fs.ensureDir(this.nodeModulesPath)
|
await fs.ensureDir(this.nodeModulesPath)
|
||||||
await fs.ensureDir(this.localFolderPath)
|
await fs.ensureDir(this.localFolderPath)
|
||||||
return await this.loadExtensions();
|
return await this.loadExtensions();
|
||||||
}
|
}
|
||||||
|
|
||||||
async getExtensionByManifest(manifestPath: string): Promise<InstalledExtension> {
|
protected async getByManifest(manifestPath: string): Promise<InstalledExtension> {
|
||||||
let manifestJson: ExtensionManifest;
|
let manifestJson: LensExtensionManifest;
|
||||||
try {
|
try {
|
||||||
|
fs.accessSync(manifestPath, fs.constants.F_OK); // check manifest file for existence
|
||||||
manifestJson = __non_webpack_require__(manifestPath)
|
manifestJson = __non_webpack_require__(manifestPath)
|
||||||
this.packagesJson.dependencies[manifestJson.name] = path.dirname(manifestPath)
|
this.packagesJson.dependencies[manifestJson.name] = path.dirname(manifestPath)
|
||||||
|
|
||||||
logger.info("[EXTENSION-MANAGER] installed extension " + manifestJson.name)
|
logger.info("[EXTENSION-MANAGER] installed extension " + manifestJson.name)
|
||||||
return {
|
return {
|
||||||
id: manifestJson.name,
|
|
||||||
version: manifestJson.version,
|
|
||||||
name: manifestJson.name,
|
|
||||||
manifestPath: path.join(this.nodeModulesPath, manifestJson.name, "package.json"),
|
manifestPath: path.join(this.nodeModulesPath, manifestJson.name, "package.json"),
|
||||||
manifest: manifestJson
|
manifest: manifestJson,
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logger.error(`[EXTENSION-MANAGER]: can't install extension at ${manifestPath}: ${err}`, { manifestJson });
|
logger.error(`[EXTENSION-MANAGER]: can't install extension at ${manifestPath}: ${err}`, { manifestJson });
|
||||||
@ -79,7 +96,7 @@ export class ExtensionManager {
|
|||||||
|
|
||||||
protected installPackages(): Promise<void> {
|
protected installPackages(): Promise<void> {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const child = child_process.fork(this.npmPath, ["install", "--silent"], {
|
const child = child_process.fork(this.npmPath, ["install", "--silent", "--no-audit", "--only=prod", "--prefer-offline", "--no-package-lock"], {
|
||||||
cwd: extensionPackagesRoot(),
|
cwd: extensionPackagesRoot(),
|
||||||
silent: true
|
silent: true
|
||||||
})
|
})
|
||||||
@ -95,13 +112,15 @@ export class ExtensionManager {
|
|||||||
async loadExtensions() {
|
async loadExtensions() {
|
||||||
const bundledExtensions = await this.loadBundledExtensions()
|
const bundledExtensions = await this.loadBundledExtensions()
|
||||||
const localExtensions = await this.loadFromFolder(this.localFolderPath)
|
const localExtensions = await this.loadFromFolder(this.localFolderPath)
|
||||||
|
await fs.writeFile(path.join(this.packageJsonPath), JSON.stringify(this.packagesJson, null, 2), { mode: 0o600 })
|
||||||
|
await this.installPackages()
|
||||||
const extensions = bundledExtensions.concat(localExtensions)
|
const extensions = bundledExtensions.concat(localExtensions)
|
||||||
return new Map(extensions.map(ext => [ext.id, ext]));
|
return new Map(extensions.map(ext => [ext.manifestPath, ext]));
|
||||||
}
|
}
|
||||||
|
|
||||||
async loadBundledExtensions() {
|
async loadBundledExtensions() {
|
||||||
const extensions: InstalledExtension[] = []
|
const extensions: InstalledExtension[] = []
|
||||||
const folderPath = this.inTreeTargetPath
|
const folderPath = this.bundledFolderPath
|
||||||
const bundledExtensions = getBundledExtensions()
|
const bundledExtensions = getBundledExtensions()
|
||||||
const paths = await fs.readdir(folderPath);
|
const paths = await fs.readdir(folderPath);
|
||||||
for (const fileName of paths) {
|
for (const fileName of paths) {
|
||||||
@ -110,15 +129,13 @@ export class ExtensionManager {
|
|||||||
}
|
}
|
||||||
const absPath = path.resolve(folderPath, fileName);
|
const absPath = path.resolve(folderPath, fileName);
|
||||||
const manifestPath = path.resolve(absPath, "package.json");
|
const manifestPath = path.resolve(absPath, "package.json");
|
||||||
await fs.access(manifestPath, fs.constants.F_OK)
|
const ext = await this.getByManifest(manifestPath).catch(() => null)
|
||||||
const ext = await this.getExtensionByManifest(manifestPath).catch(() => null)
|
|
||||||
if (ext) {
|
if (ext) {
|
||||||
|
ext.isBundled = true;
|
||||||
extensions.push(ext)
|
extensions.push(ext)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
logger.debug(`[EXTENSION-MANAGER]: ${extensions.length} extensions loaded`, { folderPath, extensions });
|
logger.debug(`[EXTENSION-MANAGER]: ${extensions.length} extensions loaded`, { folderPath, extensions });
|
||||||
await fs.writeFile(path.join(this.extensionPackagesRoot, "package.json"), JSON.stringify(this.packagesJson), {mode: 0o600})
|
|
||||||
await this.installPackages()
|
|
||||||
return extensions
|
return extensions
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -131,18 +148,21 @@ export class ExtensionManager {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
const absPath = path.resolve(folderPath, fileName);
|
const absPath = path.resolve(folderPath, fileName);
|
||||||
|
if (!fs.existsSync(absPath)) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
const lstat = await fs.lstat(absPath)
|
||||||
|
if (!lstat.isDirectory() && !lstat.isSymbolicLink()) { // skip non-directories
|
||||||
|
continue
|
||||||
|
}
|
||||||
const manifestPath = path.resolve(absPath, "package.json");
|
const manifestPath = path.resolve(absPath, "package.json");
|
||||||
await fs.access(manifestPath, fs.constants.F_OK)
|
const ext = await this.getByManifest(manifestPath).catch(() => null)
|
||||||
const ext = await this.getExtensionByManifest(manifestPath).catch(() => null)
|
|
||||||
if (ext) {
|
if (ext) {
|
||||||
extensions.push(ext)
|
extensions.push(ext)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.debug(`[EXTENSION-MANAGER]: ${extensions.length} extensions loaded`, { folderPath, extensions });
|
logger.debug(`[EXTENSION-MANAGER]: ${extensions.length} extensions loaded`, { folderPath, extensions });
|
||||||
await fs.writeFile(path.join(this.extensionPackagesRoot, "package.json"), JSON.stringify(this.packagesJson), {mode: 0o600})
|
|
||||||
await this.installPackages()
|
|
||||||
|
|
||||||
return extensions;
|
return extensions;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -15,7 +15,7 @@ export class ExtensionStore<T = any> extends BaseStore<T> {
|
|||||||
await super.load()
|
await super.load()
|
||||||
}
|
}
|
||||||
|
|
||||||
protected storePath() {
|
protected cwd() {
|
||||||
return path.join(super.storePath(), "extension-store", this.extension.name)
|
return path.join(super.cwd(), "extension-store", this.extension.name)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,75 +1,111 @@
|
|||||||
import { readJsonSync } from "fs-extra";
|
import type { InstalledExtension } from "./extension-manager";
|
||||||
import { action, observable, toJS } from "mobx";
|
import { action, reaction } from "mobx";
|
||||||
import logger from "../main/logger";
|
import logger from "../main/logger";
|
||||||
import { BaseRegistry } from "./registries/base-registry";
|
import { ExtensionStore } from "./extension-store";
|
||||||
|
|
||||||
export type ExtensionId = string | ExtensionPackageJsonPath;
|
export type LensExtensionId = string; // path to manifest (package.json)
|
||||||
export type ExtensionPackageJsonPath = string;
|
export type LensExtensionConstructor = new (...args: ConstructorParameters<typeof LensExtension>) => LensExtension;
|
||||||
export type ExtensionVersion = string | number;
|
|
||||||
|
|
||||||
export interface ExtensionModel {
|
export interface LensExtensionManifest {
|
||||||
id: ExtensionId;
|
|
||||||
version: ExtensionVersion;
|
|
||||||
name: string;
|
name: string;
|
||||||
manifestPath: string;
|
version: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
enabled?: boolean;
|
main?: string; // path to %ext/dist/main.js
|
||||||
updateUrl?: string;
|
renderer?: string; // path to %ext/dist/renderer.js
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ExtensionManifest extends ExtensionModel {
|
export interface LensExtensionStoreModel {
|
||||||
main?: string;
|
isEnabled: boolean;
|
||||||
renderer?: string;
|
|
||||||
description?: string; // todo: add more fields similar to package.json + some extra
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export class LensExtension implements ExtensionModel {
|
export class LensExtension<S extends ExtensionStore<LensExtensionStoreModel> = any> {
|
||||||
public id: ExtensionId;
|
protected store: S;
|
||||||
public updateUrl: string;
|
readonly manifest: LensExtensionManifest;
|
||||||
protected disposers: (() => void)[] = [];
|
readonly manifestPath: string;
|
||||||
|
readonly isBundled: boolean;
|
||||||
|
|
||||||
@observable name = "";
|
constructor({ manifest, manifestPath, isBundled }: InstalledExtension) {
|
||||||
@observable description = "";
|
this.manifest = manifest
|
||||||
@observable version: ExtensionVersion = "0.0.0";
|
this.manifestPath = manifestPath
|
||||||
@observable manifest: ExtensionManifest;
|
this.isBundled = !!isBundled
|
||||||
@observable manifestPath: string;
|
this.init();
|
||||||
@observable isEnabled = false;
|
}
|
||||||
|
|
||||||
constructor(model: ExtensionModel, manifest: ExtensionManifest) {
|
protected async init(store: S = createBaseStore().getInstance()) {
|
||||||
this.importModel(model, manifest);
|
this.store = store;
|
||||||
|
await this.store.loadExtension(this);
|
||||||
|
reaction(() => this.store.data.isEnabled, (isEnabled = true) => {
|
||||||
|
this.toggle(isEnabled); // handle activation & deactivation
|
||||||
|
}, {
|
||||||
|
fireImmediately: true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
get isEnabled() {
|
||||||
|
return !!this.store.data.isEnabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
get id(): LensExtensionId {
|
||||||
|
return this.manifestPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
get name() {
|
||||||
|
return this.manifest.name
|
||||||
|
}
|
||||||
|
|
||||||
|
get version() {
|
||||||
|
return this.manifest.version
|
||||||
|
}
|
||||||
|
|
||||||
|
get description() {
|
||||||
|
return this.manifest.description
|
||||||
}
|
}
|
||||||
|
|
||||||
@action
|
@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() {
|
async enable() {
|
||||||
this.isEnabled = true;
|
if (this.isEnabled) return;
|
||||||
logger.info(`[EXTENSION]: enabled ${this.name}@${this.version}`);
|
this.store.data.isEnabled = true;
|
||||||
this.onActivate();
|
this.onActivate();
|
||||||
|
logger.info(`[EXTENSION]: enabled ${this.name}@${this.version}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
async disable() {
|
async disable() {
|
||||||
|
if (!this.isEnabled) return;
|
||||||
|
this.store.data.isEnabled = false;
|
||||||
this.onDeactivate();
|
this.onDeactivate();
|
||||||
this.isEnabled = false;
|
|
||||||
this.disposers.forEach(cleanUp => cleanUp());
|
|
||||||
this.disposers.length = 0;
|
|
||||||
logger.info(`[EXTENSION]: disabled ${this.name}@${this.version}`);
|
logger.info(`[EXTENSION]: disabled ${this.name}@${this.version}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// todo: add more hooks
|
toggle(enable?: boolean) {
|
||||||
|
if (typeof enable === "boolean") {
|
||||||
|
enable ? this.enable() : this.disable()
|
||||||
|
} else {
|
||||||
|
this.isEnabled ? this.disable() : this.enable()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async whenEnabled(handlers: () => Function[]) {
|
||||||
|
const disposers: Function[] = [];
|
||||||
|
const unregisterHandlers = () => {
|
||||||
|
disposers.forEach(unregister => unregister())
|
||||||
|
disposers.length = 0;
|
||||||
|
}
|
||||||
|
const cancelReaction = reaction(() => this.isEnabled, isEnabled => {
|
||||||
|
if (isEnabled) {
|
||||||
|
disposers.push(...handlers());
|
||||||
|
} else {
|
||||||
|
unregisterHandlers();
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
fireImmediately: true
|
||||||
|
})
|
||||||
|
return () => {
|
||||||
|
unregisterHandlers();
|
||||||
|
cancelReaction();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
protected onActivate() {
|
protected onActivate() {
|
||||||
// mock
|
// mock
|
||||||
}
|
}
|
||||||
@ -77,37 +113,14 @@ export class LensExtension implements ExtensionModel {
|
|||||||
protected onDeactivate() {
|
protected onDeactivate() {
|
||||||
// mock
|
// mock
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
registerTo<T = any>(registry: BaseRegistry<T>, items: T[] = []) {
|
function createBaseStore() {
|
||||||
const disposers = items.map(item => registry.add(item));
|
return class extends ExtensionStore<LensExtensionStoreModel> {
|
||||||
this.disposers.push(...disposers);
|
constructor() {
|
||||||
return () => {
|
super({
|
||||||
this.disposers = this.disposers.filter(disposer => !disposers.includes(disposer))
|
configName: "state"
|
||||||
};
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
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,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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[] = []
|
||||||
}
|
}
|
||||||
|
|||||||
1
src/extensions/npm/extensions/.gitignore
vendored
1
src/extensions/npm/extensions/.gitignore
vendored
@ -1 +1,2 @@
|
|||||||
api.d.ts
|
api.d.ts
|
||||||
|
yarn.lock
|
||||||
|
|||||||
@ -12,5 +12,8 @@
|
|||||||
"author": {
|
"author": {
|
||||||
"name": "Mirantis, Inc.",
|
"name": "Mirantis, Inc.",
|
||||||
"email": "info@k8slens.dev"
|
"email": "info@k8slens.dev"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/node": "^14.14.6"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
// Base class for extensions-api registries
|
// Base class for extensions-api registries
|
||||||
import { observable } from "mobx";
|
import { action, observable } from "mobx";
|
||||||
|
|
||||||
export class BaseRegistry<T = any> {
|
export class BaseRegistry<T = any> {
|
||||||
protected items = observable<T>([], { deep: false });
|
protected items = observable<T>([], { deep: false });
|
||||||
@ -8,10 +8,16 @@ export class BaseRegistry<T = any> {
|
|||||||
return this.items.toJS();
|
return this.items.toJS();
|
||||||
}
|
}
|
||||||
|
|
||||||
add(item: T) {
|
@action
|
||||||
this.items.push(item);
|
add(...items: T[]) {
|
||||||
return () => {
|
this.items.push(...items);
|
||||||
|
return () => this.remove(...items);
|
||||||
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
remove(...items: T[]) {
|
||||||
|
items.forEach(item => {
|
||||||
this.items.remove(item); // works because of {deep: false};
|
this.items.remove(item); // works because of {deep: false};
|
||||||
}
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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"
|
||||||
|
|||||||
22
src/extensions/registries/kube-object-detail-registry.ts
Normal file
22
src/extensions/registries/kube-object-detail-registry.ts
Normal 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()
|
||||||
@ -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
|
||||||
|
|||||||
@ -1,4 +1,6 @@
|
|||||||
|
export { isAllowedResource } from "../../common/rbac"
|
||||||
export { apiManager } from "../../renderer/api/api-manager";
|
export { apiManager } from "../../renderer/api/api-manager";
|
||||||
|
export { KubeObjectStore } from "../../renderer/kube-object.store"
|
||||||
export { KubeApi, forCluster, IKubeApiCluster } from "../../renderer/api/kube-api";
|
export { KubeApi, forCluster, IKubeApiCluster } from "../../renderer/api/kube-api";
|
||||||
export { KubeObject } from "../../renderer/api/kube-object";
|
export { KubeObject } from "../../renderer/api/kube-object";
|
||||||
export { Pod, podsApi, IPodContainer, IPodContainerStatus } from "../../renderer/api/endpoints";
|
export { Pod, podsApi, IPodContainer, IPodContainerStatus } from "../../renderer/api/endpoints";
|
||||||
|
|||||||
@ -1 +1,3 @@
|
|||||||
export { navigate, hideDetails, showDetails } from "../../renderer/navigation"
|
export { navigate, hideDetails, showDetails, getDetailsUrl } from "../../renderer/navigation"
|
||||||
|
export { RouteProps } from "react-router"
|
||||||
|
export { IURLParams } from "../../common/utils/buildUrl";
|
||||||
|
|||||||
4
src/jest.setup.ts
Normal file
4
src/jest.setup.ts
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
|
||||||
|
import fetchMock from "jest-fetch-mock"
|
||||||
|
// rewire global.fetch to call 'fetchMock'
|
||||||
|
fetchMock.enableMocks();
|
||||||
@ -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() {
|
||||||
|
|||||||
@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
import "../common/cluster-ipc";
|
import "../common/cluster-ipc";
|
||||||
import type http from "http"
|
import type http from "http"
|
||||||
|
import { ipcMain } from "electron"
|
||||||
import { autorun } from "mobx";
|
import { autorun } from "mobx";
|
||||||
import { clusterStore, getClusterIdFromHost } from "../common/cluster-store"
|
import { clusterStore, getClusterIdFromHost } from "../common/cluster-store"
|
||||||
import { Cluster } from "./cluster"
|
import { Cluster } from "./cluster"
|
||||||
@ -10,7 +11,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);
|
||||||
@ -30,6 +31,29 @@ export class ClusterManager {
|
|||||||
}, {
|
}, {
|
||||||
delay: 250
|
delay: 250
|
||||||
});
|
});
|
||||||
|
|
||||||
|
ipcMain.on("network:offline", () => { this.onNetworkOffline() })
|
||||||
|
ipcMain.on("network:online", () => { this.onNetworkOnline() })
|
||||||
|
}
|
||||||
|
|
||||||
|
protected onNetworkOffline() {
|
||||||
|
logger.info("[CLUSTER-MANAGER]: network is offline")
|
||||||
|
clusterStore.enabledClustersList.forEach((cluster) => {
|
||||||
|
if (!cluster.disconnected) {
|
||||||
|
cluster.online = false
|
||||||
|
cluster.accessible = false
|
||||||
|
cluster.refreshConnectionStatus().catch((e) => e)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
protected onNetworkOnline() {
|
||||||
|
logger.info("[CLUSTER-MANAGER]: network is online")
|
||||||
|
clusterStore.enabledClustersList.forEach((cluster) => {
|
||||||
|
if (!cluster.disconnected) {
|
||||||
|
cluster.refreshConnectionStatus().catch((e) => e)
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
stop() {
|
stop() {
|
||||||
|
|||||||
@ -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,11 +67,12 @@ 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 online = false;
|
@observable enabled = false; // only enabled clusters are visible to users
|
||||||
@observable accessible = false;
|
@observable online = false; // describes if we can detect that cluster is online
|
||||||
@observable ready = false;
|
@observable accessible = false; // if user is able to access cluster resources
|
||||||
|
@observable ready = false; // cluster is in usable state
|
||||||
@observable reconnecting = false;
|
@observable reconnecting = false;
|
||||||
@observable disconnected = true;
|
@observable disconnected = true; // false if user has selected to connect
|
||||||
@observable failureReason: string;
|
@observable failureReason: string;
|
||||||
@observable isAdmin = false;
|
@observable isAdmin = false;
|
||||||
@observable eventCount = 0;
|
@observable eventCount = 0;
|
||||||
@ -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);
|
||||||
@ -119,17 +127,19 @@ export class Cluster implements ClusterModel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected bindEvents() {
|
protected bindEvents() {
|
||||||
logger.info(`[CLUSTER]: bind events`, this.getMeta());
|
logger.info(`[CLUSTER]: bind events`, this.getMeta())
|
||||||
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"
|
||||||
|
|||||||
@ -15,13 +15,12 @@ import { shellSync } from "./shell-sync"
|
|||||||
import { getFreePort } from "./port"
|
import { getFreePort } from "./port"
|
||||||
import { mangleProxyEnv } from "./proxy-env"
|
import { mangleProxyEnv } from "./proxy-env"
|
||||||
import { registerFileProtocol } from "../common/register-protocol";
|
import { registerFileProtocol } from "../common/register-protocol";
|
||||||
|
import logger from "./logger"
|
||||||
import { clusterStore } from "../common/cluster-store"
|
import { clusterStore } from "../common/cluster-store"
|
||||||
import { userStore } from "../common/user-store";
|
import { userStore } from "../common/user-store";
|
||||||
import { workspaceStore } from "../common/workspace-store";
|
import { workspaceStore } from "../common/workspace-store";
|
||||||
import { appEventBus } from "../common/event-bus"
|
import { appEventBus } from "../common/event-bus"
|
||||||
import { extensionManager } from "../extensions/extension-manager";
|
|
||||||
import { extensionLoader } from "../extensions/extension-loader";
|
import { extensionLoader } from "../extensions/extension-loader";
|
||||||
import logger from "./logger"
|
|
||||||
|
|
||||||
const workingDir = path.join(app.getPath("appData"), appName);
|
const workingDir = path.join(app.getPath("appData"), appName);
|
||||||
let proxyPort: number;
|
let proxyPort: number;
|
||||||
@ -48,7 +47,7 @@ app.on("ready", async () => {
|
|||||||
|
|
||||||
registerFileProtocol("static", __static);
|
registerFileProtocol("static", __static);
|
||||||
|
|
||||||
// preload isomorphic stores
|
// preload
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
userStore.load(),
|
userStore.load(),
|
||||||
clusterStore.load(),
|
clusterStore.load(),
|
||||||
@ -76,12 +75,8 @@ app.on("ready", async () => {
|
|||||||
app.exit();
|
app.exit();
|
||||||
}
|
}
|
||||||
|
|
||||||
windowManager = new WindowManager(proxyPort);
|
LensExtensionsApi.windowManager = windowManager = new WindowManager(proxyPort);
|
||||||
|
extensionLoader.init(); // call after windowManager to see splash earlier
|
||||||
LensExtensionsApi.windowManager = windowManager; // expose to extensions
|
|
||||||
extensionLoader.loadOnMain()
|
|
||||||
extensionLoader.extensions.replace(await extensionManager.load())
|
|
||||||
extensionLoader.broadcastExtensions()
|
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
appEventBus.emit({ name: "app", action: "start" })
|
appEventBus.emit({ name: "app", action: "start" })
|
||||||
|
|||||||
@ -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)
|
||||||
})
|
})
|
||||||
|
|||||||
@ -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";
|
||||||
@ -6,6 +6,7 @@ import { addClusterURL } from "../renderer/components/+add-cluster/add-cluster.r
|
|||||||
import { preferencesURL } from "../renderer/components/+preferences/preferences.route";
|
import { preferencesURL } from "../renderer/components/+preferences/preferences.route";
|
||||||
import { whatsNewURL } from "../renderer/components/+whats-new/whats-new.route";
|
import { whatsNewURL } from "../renderer/components/+whats-new/whats-new.route";
|
||||||
import { clusterSettingsURL } from "../renderer/components/+cluster-settings/cluster-settings.route";
|
import { clusterSettingsURL } from "../renderer/components/+cluster-settings/cluster-settings.route";
|
||||||
|
import { extensionsURL } from "../renderer/components/+extensions/extensions.route";
|
||||||
import { menuRegistry } from "../extensions/registries/menu-registry";
|
import { menuRegistry } from "../extensions/registries/menu-registry";
|
||||||
import logger from "./logger";
|
import logger from "./logger";
|
||||||
|
|
||||||
@ -70,6 +71,13 @@ export function buildMenu(windowManager: WindowManager) {
|
|||||||
navigate(preferencesURL())
|
navigate(preferencesURL())
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
label: 'Extensions',
|
||||||
|
accelerator: 'CmdOrCtrl+Shift+E',
|
||||||
|
click() {
|
||||||
|
navigate(extensionsURL())
|
||||||
|
}
|
||||||
|
},
|
||||||
{ type: 'separator' },
|
{ type: 'separator' },
|
||||||
{ role: 'services' },
|
{ role: 'services' },
|
||||||
{ type: 'separator' },
|
{ type: 'separator' },
|
||||||
@ -185,6 +193,7 @@ export function buildMenu(windowManager: WindowManager) {
|
|||||||
navigate(whatsNewURL())
|
navigate(whatsNewURL())
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
<<<<<<< HEAD
|
||||||
{
|
{
|
||||||
label: "Documentation",
|
label: "Documentation",
|
||||||
click: async () => {
|
click: async () => {
|
||||||
@ -197,6 +206,8 @@ export function buildMenu(windowManager: WindowManager) {
|
|||||||
shell.openExternal('https://k8slens.dev/licenses/eula.md');
|
shell.openExternal('https://k8slens.dev/licenses/eula.md');
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
=======
|
||||||
|
>>>>>>> master
|
||||||
...ignoreOnMac([
|
...ignoreOnMac([
|
||||||
{
|
{
|
||||||
label: "About Lens",
|
label: "About Lens",
|
||||||
|
|||||||
@ -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);
|
||||||
|
|||||||
@ -1,3 +1,5 @@
|
|||||||
|
import moment from "moment";
|
||||||
|
|
||||||
import { IAffinity, WorkloadKubeObject } from "../workload-kube-object";
|
import { IAffinity, WorkloadKubeObject } from "../workload-kube-object";
|
||||||
import { autobind } from "../../utils";
|
import { autobind } from "../../utils";
|
||||||
import { KubeApi } from "../kube-api";
|
import { KubeApi } from "../kube-api";
|
||||||
@ -10,7 +12,7 @@ export class DeploymentApi extends KubeApi<Deployment> {
|
|||||||
getReplicas(params: { namespace: string; name: string }): Promise<number> {
|
getReplicas(params: { namespace: string; name: string }): Promise<number> {
|
||||||
return this.request
|
return this.request
|
||||||
.get(this.getScaleApiUrl(params))
|
.get(this.getScaleApiUrl(params))
|
||||||
.then(({ status }: any) => status.replicas)
|
.then(({ status }: any) => status?.replicas)
|
||||||
}
|
}
|
||||||
|
|
||||||
scale(params: { namespace: string; name: string }, replicas: number) {
|
scale(params: { namespace: string; name: string }, replicas: number) {
|
||||||
@ -23,6 +25,25 @@ export class DeploymentApi extends KubeApi<Deployment> {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
restart(params: { namespace: string; name: string }) {
|
||||||
|
return this.request.patch(this.getUrl(params), {
|
||||||
|
data: {
|
||||||
|
spec: {
|
||||||
|
template: {
|
||||||
|
metadata: {
|
||||||
|
annotations: {"kubectl.kubernetes.io/restartedAt" : moment.utc().format()}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
'content-type': 'application/strategic-merge-patch+json'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@autobind()
|
@autobind()
|
||||||
@ -38,6 +59,7 @@ export class Deployment extends WorkloadKubeObject {
|
|||||||
metadata: {
|
metadata: {
|
||||||
creationTimestamp?: string;
|
creationTimestamp?: string;
|
||||||
labels: { [app: string]: string };
|
labels: { [app: string]: string };
|
||||||
|
annotations?: { [app: string]: string };
|
||||||
};
|
};
|
||||||
spec: {
|
spec: {
|
||||||
containers: {
|
containers: {
|
||||||
|
|||||||
@ -64,7 +64,7 @@ export class JsonApi<D = JsonApiData, P extends JsonApiParams = JsonApiParams> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
patch<T = D>(path: string, params?: P, reqInit: RequestInit = {}) {
|
patch<T = D>(path: string, params?: P, reqInit: RequestInit = {}) {
|
||||||
return this.request<T>(path, params, { ...reqInit, method: "patch" });
|
return this.request<T>(path, params, { ...reqInit, method: "PATCH" });
|
||||||
}
|
}
|
||||||
|
|
||||||
del<T = D>(path: string, params?: P, reqInit: RequestInit = {}) {
|
del<T = D>(path: string, params?: P, reqInit: RequestInit = {}) {
|
||||||
|
|||||||
@ -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()
|
|
||||||
|
|||||||
@ -109,17 +109,22 @@ export class KubeWatchApi {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async onRouteEvent({ type, url }: IKubeWatchRouteEvent) {
|
protected async onRouteEvent(event: IKubeWatchRouteEvent) {
|
||||||
if (type === "STREAM_END") {
|
if (event.type === "STREAM_END") {
|
||||||
this.disconnect();
|
this.disconnect();
|
||||||
const { apiBase, namespace } = KubeApi.parseApi(url);
|
const { apiBase, namespace } = KubeApi.parseApi(event.url);
|
||||||
const api = apiManager.getApi(apiBase);
|
const api = apiManager.getApi(apiBase);
|
||||||
if (api) {
|
if (api) {
|
||||||
try {
|
try {
|
||||||
await api.refreshResourceVersion({ namespace });
|
await api.refreshResourceVersion({ namespace });
|
||||||
this.reconnect();
|
this.reconnect();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.debug("failed to refresh resource version", error)
|
console.error("failed to refresh resource version", error)
|
||||||
|
if (this.subscribers.size > 0) {
|
||||||
|
setTimeout(() => {
|
||||||
|
this.onRouteEvent(event)
|
||||||
|
}, 1000)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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) {
|
||||||
|
|||||||
@ -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);
|
||||||
|
|||||||
@ -3,7 +3,7 @@ import "./helm-chart-details.scss";
|
|||||||
import React, { Component } from "react";
|
import React, { Component } from "react";
|
||||||
import { HelmChart, helmChartsApi } from "../../api/endpoints/helm-charts.api";
|
import { HelmChart, helmChartsApi } from "../../api/endpoints/helm-charts.api";
|
||||||
import { t, Trans } from "@lingui/macro";
|
import { t, Trans } from "@lingui/macro";
|
||||||
import { observable, toJS } from "mobx";
|
import { observable, autorun } from "mobx";
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
import { Drawer, DrawerItem } from "../drawer";
|
import { Drawer, DrawerItem } from "../drawer";
|
||||||
import { autobind, stopPropagation } from "../../utils";
|
import { autobind, stopPropagation } from "../../utils";
|
||||||
@ -30,23 +30,23 @@ export class HelmChartDetails extends Component<Props> {
|
|||||||
|
|
||||||
private chartPromise: CancelablePromise<{ readme: string; versions: HelmChart[] }>;
|
private chartPromise: CancelablePromise<{ readme: string; versions: HelmChart[] }>;
|
||||||
|
|
||||||
async componentDidMount() {
|
|
||||||
const { chart: { name, repo, version } } = this.props
|
|
||||||
|
|
||||||
try {
|
|
||||||
const { readme, versions } = await (this.chartPromise = helmChartsApi.get(repo, name, version))
|
|
||||||
this.readme = readme
|
|
||||||
this.chartVersions = versions
|
|
||||||
this.selectedChart = versions[0]
|
|
||||||
} catch (error) {
|
|
||||||
this.error = error
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
componentWillUnmount() {
|
componentWillUnmount() {
|
||||||
this.chartPromise?.cancel();
|
this.chartPromise?.cancel();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
chartUpdater = autorun(() => {
|
||||||
|
this.selectedChart = null
|
||||||
|
const { chart: { name, repo, version } } = this.props
|
||||||
|
helmChartsApi.get(repo, name, version).then(result => {
|
||||||
|
this.readme = result.readme
|
||||||
|
this.chartVersions = result.versions
|
||||||
|
this.selectedChart = result.versions[0]
|
||||||
|
},
|
||||||
|
error => {
|
||||||
|
this.error = error;
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
@autobind()
|
@autobind()
|
||||||
async onVersionChange({ value: version }: SelectOption) {
|
async onVersionChange({ value: version }: SelectOption) {
|
||||||
this.selectedChart = this.chartVersions.find(chart => chart.version === version);
|
this.selectedChart = this.chartVersions.find(chart => chart.version === version);
|
||||||
|
|||||||
@ -26,7 +26,7 @@ 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})
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@ -28,8 +28,9 @@ export class RemoveClusterButton extends React.Component<Props> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
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>
|
||||||
);
|
);
|
||||||
|
|||||||
8
src/renderer/components/+extensions/extensions.route.ts
Normal file
8
src/renderer/components/+extensions/extensions.route.ts
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
import { RouteProps } from "react-router";
|
||||||
|
import { buildURL } from "../../../common/utils/buildUrl";
|
||||||
|
|
||||||
|
export const extensionsRoute: RouteProps = {
|
||||||
|
path: "/extensions"
|
||||||
|
}
|
||||||
|
|
||||||
|
export const extensionsURL = buildURL(extensionsRoute.path)
|
||||||
35
src/renderer/components/+extensions/extensions.scss
Normal file
35
src/renderer/components/+extensions/extensions.scss
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
.Extensions {
|
||||||
|
--width: 100%;
|
||||||
|
--max-width: auto;
|
||||||
|
|
||||||
|
.extension {
|
||||||
|
--flex-gap: $padding / 3;
|
||||||
|
padding: $padding $padding * 2;
|
||||||
|
background: $colorVague;
|
||||||
|
border-radius: $radius;
|
||||||
|
}
|
||||||
|
|
||||||
|
.extensions-path {
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
|
||||||
|
.WizardLayout {
|
||||||
|
padding: 0;
|
||||||
|
|
||||||
|
.info-col {
|
||||||
|
flex: 0.6;
|
||||||
|
align-self: flex-start;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.SearchInput {
|
||||||
|
margin-top: $margin / 2;
|
||||||
|
margin-bottom: $margin * 2;
|
||||||
|
max-width: none;
|
||||||
|
|
||||||
|
> label {
|
||||||
|
padding: $padding $padding * 2;
|
||||||
|
border-radius: $radius;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
112
src/renderer/components/+extensions/extensions.tsx
Normal file
112
src/renderer/components/+extensions/extensions.tsx
Normal file
@ -0,0 +1,112 @@
|
|||||||
|
import "./extensions.scss";
|
||||||
|
import { shell } from "electron";
|
||||||
|
import React from "react";
|
||||||
|
import { computed, observable } from "mobx";
|
||||||
|
import { observer } from "mobx-react";
|
||||||
|
import { t, Trans } from "@lingui/macro";
|
||||||
|
import { _i18n } from "../../i18n";
|
||||||
|
import { Button } from "../button";
|
||||||
|
import { WizardLayout } from "../layout/wizard-layout";
|
||||||
|
import { Input } from "../input";
|
||||||
|
import { Icon } from "../icon";
|
||||||
|
import { PageLayout } from "../layout/page-layout";
|
||||||
|
import { extensionLoader } from "../../../extensions/extension-loader";
|
||||||
|
import { extensionManager } from "../../../extensions/extension-manager";
|
||||||
|
|
||||||
|
@observer
|
||||||
|
export class Extensions extends React.Component {
|
||||||
|
@observable search = ""
|
||||||
|
|
||||||
|
@computed get extensions() {
|
||||||
|
const searchText = this.search.toLowerCase();
|
||||||
|
return extensionLoader.userExtensions.filter(({ name, description }) => {
|
||||||
|
return [
|
||||||
|
name.toLowerCase().includes(searchText),
|
||||||
|
description.toLowerCase().includes(searchText),
|
||||||
|
].some(v => v)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
get extensionsPath() {
|
||||||
|
return extensionManager.localFolderPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
renderInfo() {
|
||||||
|
return (
|
||||||
|
<div className="flex column gaps">
|
||||||
|
<h2>Lens Extension API</h2>
|
||||||
|
<div>
|
||||||
|
The Extensions API in Lens allows users to customize and enhance the Lens experience by creating their own menus or page content that is extended from the existing pages. Many of the core
|
||||||
|
features of Lens are built as extensions and use the same Extension API.
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
Extensions loaded from:
|
||||||
|
<div className="extensions-path flex inline">
|
||||||
|
<code>{this.extensionsPath}</code>
|
||||||
|
<Icon
|
||||||
|
material="folder"
|
||||||
|
tooltip="Open folder"
|
||||||
|
onClick={() => shell.openPath(this.extensionsPath)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
Check out documentation to <a href="https://docs.k8slens.dev/" target="_blank">learn more</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
renderExtensions() {
|
||||||
|
const { extensions, extensionsPath, search } = this;
|
||||||
|
if (!extensions.length) {
|
||||||
|
return (
|
||||||
|
<div className="flex align-center box grow justify-center gaps">
|
||||||
|
{search && <Trans>No search results found</Trans>}
|
||||||
|
{!search && <p><Trans>There are no extensions in</Trans> <code>{extensionsPath}</code></p>}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return extensions.map(ext => {
|
||||||
|
const { id, name, description, isEnabled } = ext;
|
||||||
|
return (
|
||||||
|
<div key={id} className="extension flex gaps align-center">
|
||||||
|
<div className="box grow flex column gaps">
|
||||||
|
<div className="package">
|
||||||
|
Name: <code className="name">{name}</code>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
Description: <span className="text-secondary">{description}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{!isEnabled && (
|
||||||
|
<Button plain active onClick={() => ext.enable()}>Enable</Button>
|
||||||
|
)}
|
||||||
|
{isEnabled && (
|
||||||
|
<Button accent onClick={() => ext.disable()}>Disable</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<PageLayout showOnTop className="Extensions" header={<h2>Extensions</h2>}>
|
||||||
|
<WizardLayout infoPanel={this.renderInfo()}>
|
||||||
|
<Input
|
||||||
|
autoFocus
|
||||||
|
theme="round-black"
|
||||||
|
className="SearchInput"
|
||||||
|
placeholder={_i18n._(t`Search extensions`)}
|
||||||
|
value={this.search}
|
||||||
|
onChange={(value) => this.search = value}
|
||||||
|
/>
|
||||||
|
<div className="extension-list flex column gaps">
|
||||||
|
{this.renderExtensions()}
|
||||||
|
</div>
|
||||||
|
</WizardLayout>
|
||||||
|
</PageLayout>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
2
src/renderer/components/+extensions/index.ts
Normal file
2
src/renderer/components/+extensions/index.ts
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
export * from "./extensions.route"
|
||||||
|
export * from "./extensions"
|
||||||
@ -20,11 +20,18 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.desired-scale {
|
.desired-scale {
|
||||||
flex: 1 0;
|
flex: 1.1 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.slider-container {
|
.slider-container {
|
||||||
flex: 1.3 0;
|
flex: 1 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.plus-minus-container {
|
||||||
|
margin-left: $margin * 2;
|
||||||
|
.Icon {
|
||||||
|
--color-active: black;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.warning {
|
.warning {
|
||||||
|
|||||||
@ -0,0 +1,151 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { render, waitFor, fireEvent } from '@testing-library/react';
|
||||||
|
import '@testing-library/jest-dom/extend-expect'
|
||||||
|
|
||||||
|
import { DeploymentScaleDialog } from "./deployment-scale-dialog";
|
||||||
|
jest.mock("../../api/endpoints");
|
||||||
|
import { deploymentApi } from "../../api/endpoints";
|
||||||
|
|
||||||
|
const dummyDeployment = {
|
||||||
|
apiVersion: 'v1',
|
||||||
|
kind: 'dummy',
|
||||||
|
metadata: {
|
||||||
|
uid: 'dummy',
|
||||||
|
name: 'dummy',
|
||||||
|
creationTimestamp: 'dummy',
|
||||||
|
resourceVersion: 'dummy',
|
||||||
|
selfLink: 'link',
|
||||||
|
},
|
||||||
|
selfLink: 'link',
|
||||||
|
spec: {
|
||||||
|
replicas: 1,
|
||||||
|
selector: { matchLabels: { dummy: 'label' } },
|
||||||
|
template: {
|
||||||
|
metadata: {
|
||||||
|
labels: { dummy: 'label' },
|
||||||
|
},
|
||||||
|
spec: {
|
||||||
|
containers: [{
|
||||||
|
name: 'dummy',
|
||||||
|
image: 'dummy',
|
||||||
|
resources: {
|
||||||
|
requests: {
|
||||||
|
cpu: '1',
|
||||||
|
memory: '10Mi',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
terminationMessagePath: 'dummy',
|
||||||
|
terminationMessagePolicy: 'dummy',
|
||||||
|
imagePullPolicy: 'dummy',
|
||||||
|
}],
|
||||||
|
restartPolicy: 'dummy',
|
||||||
|
terminationGracePeriodSeconds: 10,
|
||||||
|
dnsPolicy: 'dummy',
|
||||||
|
serviceAccountName: 'dummy',
|
||||||
|
serviceAccount: 'dummy',
|
||||||
|
securityContext: {},
|
||||||
|
schedulerName: 'dummy',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
strategy: {
|
||||||
|
type: 'dummy',
|
||||||
|
rollingUpdate: {
|
||||||
|
maxUnavailable: 1,
|
||||||
|
maxSurge: 10,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
status: {
|
||||||
|
observedGeneration: 1,
|
||||||
|
replicas: 1,
|
||||||
|
updatedReplicas: 1,
|
||||||
|
readyReplicas: 1,
|
||||||
|
conditions: [{
|
||||||
|
type: 'dummy',
|
||||||
|
status: 'dummy',
|
||||||
|
lastUpdateTime: 'dummy',
|
||||||
|
lastTransitionTime: 'dummy',
|
||||||
|
reason: 'dummy',
|
||||||
|
message: 'dummy',
|
||||||
|
}],
|
||||||
|
},
|
||||||
|
getConditions: jest.fn(),
|
||||||
|
getConditionsText: jest.fn(),
|
||||||
|
getReplicas: jest.fn(),
|
||||||
|
getSelectors: jest.fn(),
|
||||||
|
getTemplateLabels: jest.fn(),
|
||||||
|
getAffinity: jest.fn(),
|
||||||
|
getTolerations: jest.fn(),
|
||||||
|
getNodeSelectors: jest.fn(),
|
||||||
|
getAffinityNumber: jest.fn(),
|
||||||
|
getId: jest.fn(),
|
||||||
|
getResourceVersion: jest.fn(),
|
||||||
|
getName: jest.fn(),
|
||||||
|
getNs: jest.fn(),
|
||||||
|
getAge: jest.fn(),
|
||||||
|
getFinalizers: jest.fn(),
|
||||||
|
getLabels: jest.fn(),
|
||||||
|
getAnnotations: jest.fn(),
|
||||||
|
getOwnerRefs: jest.fn(),
|
||||||
|
getSearchFields: jest.fn(),
|
||||||
|
toPlainObject: jest.fn(),
|
||||||
|
update: jest.fn(),
|
||||||
|
delete: jest.fn(),
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('<DeploymentScaleDialog />', () => {
|
||||||
|
|
||||||
|
it('renders w/o errors', () => {
|
||||||
|
const { container } = render(<DeploymentScaleDialog />);
|
||||||
|
expect(container).toBeInstanceOf(HTMLElement);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('inits with a dummy deployment with mocked current/desired scale', async () => {
|
||||||
|
// mock deploymentApi.getReplicas() which will be called
|
||||||
|
// when <DeploymentScaleDialog /> rendered.
|
||||||
|
const initReplicas = 3
|
||||||
|
deploymentApi.getReplicas = jest.fn().mockImplementationOnce(async () => initReplicas);
|
||||||
|
const { getByTestId } = render(<DeploymentScaleDialog />);
|
||||||
|
DeploymentScaleDialog.open(dummyDeployment);
|
||||||
|
// we need to wait for the DeploymentScaleDialog to show up
|
||||||
|
// because there is an <Animate /> in <Dialog /> which renders null at start.
|
||||||
|
await waitFor(async () => {
|
||||||
|
const [currentScale, desiredScale] = await Promise.all([
|
||||||
|
getByTestId('current-scale'),
|
||||||
|
getByTestId('desired-scale'),
|
||||||
|
])
|
||||||
|
expect(currentScale).toHaveTextContent(`${initReplicas}`);
|
||||||
|
expect(desiredScale).toHaveTextContent(`${initReplicas}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
it('changes the desired scale when clicking the icon buttons +/-', async () => {
|
||||||
|
const initReplicas = 1
|
||||||
|
deploymentApi.getReplicas = jest.fn().mockImplementationOnce(async () => initReplicas);
|
||||||
|
const { getByTestId } = render(<DeploymentScaleDialog />);
|
||||||
|
DeploymentScaleDialog.open(dummyDeployment);
|
||||||
|
await waitFor(async () => {
|
||||||
|
const desiredScale = await getByTestId('desired-scale');
|
||||||
|
expect(desiredScale).toHaveTextContent(`${initReplicas}`);
|
||||||
|
});
|
||||||
|
const up = await getByTestId('desired-replicas-up');
|
||||||
|
const down = await getByTestId('desired-replicas-down')
|
||||||
|
fireEvent.click(up);
|
||||||
|
expect(await getByTestId('desired-scale')).toHaveTextContent(`${initReplicas + 1}`);
|
||||||
|
fireEvent.click(down);
|
||||||
|
expect(await getByTestId('desired-scale')).toHaveTextContent('1');
|
||||||
|
// edge case, desiredScale must > 0
|
||||||
|
fireEvent.click(down);
|
||||||
|
fireEvent.click(down);
|
||||||
|
expect(await getByTestId('desired-scale')).toHaveTextContent('1');
|
||||||
|
const times = 120;
|
||||||
|
// edge case, desiredScale must < scaleMax (100)
|
||||||
|
for (let i = 0; i < times; i++) {
|
||||||
|
fireEvent.click(up);
|
||||||
|
}
|
||||||
|
expect(await getByTestId('desired-scale')).toHaveTextContent('100');
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
@ -83,21 +83,41 @@ export class DeploymentScaleDialog extends Component<Props> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
desiredReplicasUp = () => {
|
||||||
|
this.desiredReplicas < this.scaleMax && this.desiredReplicas++
|
||||||
|
}
|
||||||
|
|
||||||
|
desiredReplicasDown = () => {
|
||||||
|
this.desiredReplicas > 1 && this.desiredReplicas--
|
||||||
|
};
|
||||||
|
|
||||||
renderContents() {
|
renderContents() {
|
||||||
const { currentReplicas, desiredReplicas, onChange, scaleMax } = this;
|
const { currentReplicas, desiredReplicas, onChange, scaleMax } = this;
|
||||||
const warning = currentReplicas < 10 && desiredReplicas > 90;
|
const warning = currentReplicas < 10 && desiredReplicas > 90;
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="current-scale">
|
<div className="current-scale" data-testid="current-scale">
|
||||||
<Trans>Current replica scale: {currentReplicas}</Trans>
|
<Trans>Current replica scale: {currentReplicas}</Trans>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex gaps align-center">
|
<div className="flex gaps align-center">
|
||||||
<div className="desired-scale">
|
<div className="desired-scale" data-testid="desired-scale">
|
||||||
<Trans>Desired number of replicas</Trans>: {desiredReplicas}
|
<Trans>Desired number of replicas</Trans>: {desiredReplicas}
|
||||||
</div>
|
</div>
|
||||||
<div className="slider-container">
|
<div className="slider-container flex align-center">
|
||||||
<Slider value={desiredReplicas} max={scaleMax} onChange={onChange as any /** see: https://github.com/mui-org/material-ui/issues/20191 */}/>
|
<Slider value={desiredReplicas} max={scaleMax} onChange={onChange as any /** see: https://github.com/mui-org/material-ui/issues/20191 */}/>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="plus-minus-container flex gaps">
|
||||||
|
<Icon
|
||||||
|
material="add_circle_outline"
|
||||||
|
onClick={this.desiredReplicasUp}
|
||||||
|
data-testid="desired-replicas-up"
|
||||||
|
/>
|
||||||
|
<Icon
|
||||||
|
material="remove_circle_outline"
|
||||||
|
onClick={this.desiredReplicasDown}
|
||||||
|
data-testid="desired-replicas-down"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{warning &&
|
{warning &&
|
||||||
<div className="warning">
|
<div className="warning">
|
||||||
|
|||||||
@ -4,11 +4,12 @@ import React from "react";
|
|||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
import { RouteComponentProps } from "react-router";
|
import { RouteComponentProps } from "react-router";
|
||||||
import { t, Trans } from "@lingui/macro";
|
import { t, Trans } from "@lingui/macro";
|
||||||
import { Deployment } from "../../api/endpoints";
|
import { Deployment, deploymentApi } from "../../api/endpoints";
|
||||||
import { KubeObjectMenuProps } from "../kube-object/kube-object-menu";
|
import { KubeObjectMenuProps } from "../kube-object/kube-object-menu";
|
||||||
import { MenuItem } from "../menu";
|
import { MenuItem } from "../menu";
|
||||||
import { Icon } from "../icon";
|
import { Icon } from "../icon";
|
||||||
import { DeploymentScaleDialog } from "./deployment-scale-dialog";
|
import { DeploymentScaleDialog } from "./deployment-scale-dialog";
|
||||||
|
import { ConfirmDialog } from "../confirm-dialog";
|
||||||
import { deploymentStore } from "./deployments.store";
|
import { deploymentStore } from "./deployments.store";
|
||||||
import { replicaSetStore } from "../+workloads-replicasets/replicasets.store";
|
import { replicaSetStore } from "../+workloads-replicasets/replicasets.store";
|
||||||
import { podsStore } from "../+workloads-pods/pods.store";
|
import { podsStore } from "../+workloads-pods/pods.store";
|
||||||
@ -22,6 +23,8 @@ import kebabCase from "lodash/kebabCase";
|
|||||||
import orderBy from "lodash/orderBy";
|
import orderBy from "lodash/orderBy";
|
||||||
import { KubeEventIcon } from "../+events/kube-event-icon";
|
import { KubeEventIcon } from "../+events/kube-event-icon";
|
||||||
import { kubeObjectMenuRegistry } from "../../../extensions/registries/kube-object-menu-registry";
|
import { kubeObjectMenuRegistry } from "../../../extensions/registries/kube-object-menu-registry";
|
||||||
|
import { apiManager } from "../../api/api-manager";
|
||||||
|
import { Notifications } from "../notifications";
|
||||||
|
|
||||||
enum sortBy {
|
enum sortBy {
|
||||||
name = "name",
|
name = "name",
|
||||||
@ -96,10 +99,34 @@ export class Deployments extends React.Component<Props> {
|
|||||||
export function DeploymentMenu(props: KubeObjectMenuProps<Deployment>) {
|
export function DeploymentMenu(props: KubeObjectMenuProps<Deployment>) {
|
||||||
const { object, toolbar } = props;
|
const { object, toolbar } = props;
|
||||||
return (
|
return (
|
||||||
<MenuItem onClick={() => DeploymentScaleDialog.open(object)}>
|
<>
|
||||||
<Icon material="open_with" title={_i18n._(t`Scale`)} interactive={toolbar}/>
|
<MenuItem onClick={() => DeploymentScaleDialog.open(object)}>
|
||||||
<span className="title"><Trans>Scale</Trans></span>
|
<Icon material="open_with" title={_i18n._(t`Scale`)} interactive={toolbar}/>
|
||||||
</MenuItem>
|
<span className="title"><Trans>Scale</Trans></span>
|
||||||
|
</MenuItem>
|
||||||
|
<MenuItem onClick={() => ConfirmDialog.open({
|
||||||
|
ok: async () =>
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
await deploymentApi.restart({
|
||||||
|
namespace: object.getNs(),
|
||||||
|
name: object.getName(),
|
||||||
|
})
|
||||||
|
} catch (err) {
|
||||||
|
Notifications.error(err);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
labelOk: _i18n._(t`Restart`),
|
||||||
|
message: (
|
||||||
|
<p>
|
||||||
|
<Trans>Are you sure you want to restart deployment <b>{object.getName()}</b>?</Trans>
|
||||||
|
</p>
|
||||||
|
),
|
||||||
|
})}>
|
||||||
|
<Icon material="autorenew" title={_i18n._(t`Restart`)} interactive={toolbar}/>
|
||||||
|
<span className="title"><Trans>Restart</Trans></span>
|
||||||
|
</MenuItem>
|
||||||
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -110,4 +137,3 @@ kubeObjectMenuRegistry.add({
|
|||||||
MenuItem: DeploymentMenu
|
MenuItem: DeploymentMenu
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@ -65,7 +65,6 @@ export class PodDetails extends React.Component<Props> {
|
|||||||
const { nodeName } = spec;
|
const { nodeName } = spec;
|
||||||
const nodeSelector = pod.getNodeSelectors();
|
const nodeSelector = pod.getNodeSelectors();
|
||||||
const volumes = pod.getVolumes();
|
const volumes = pod.getVolumes();
|
||||||
const labels = pod.getLabels();
|
|
||||||
const metrics = podsStore.metrics;
|
const metrics = podsStore.metrics;
|
||||||
return (
|
return (
|
||||||
<div className="PodDetails">
|
<div className="PodDetails">
|
||||||
|
|||||||
@ -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}
|
||||||
|
|||||||
@ -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"
|
||||||
|
|||||||
@ -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",
|
||||||
|
|||||||
@ -54,6 +54,9 @@ export class App extends React.Component {
|
|||||||
appEventBus.emit({name: "cluster", action: "open", params: {
|
appEventBus.emit({name: "cluster", action: "open", params: {
|
||||||
clusterId: clusterId
|
clusterId: clusterId
|
||||||
}})
|
}})
|
||||||
|
window.addEventListener("online", () => {
|
||||||
|
window.location.reload()
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
get startURL() {
|
get startURL() {
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -16,6 +16,7 @@ import { clusterViewRoute, clusterViewURL } from "./cluster-view.route";
|
|||||||
import { clusterStore } from "../../../common/cluster-store";
|
import { clusterStore } from "../../../common/cluster-store";
|
||||||
import { hasLoadedView, initView, lensViews, refreshViews } from "./lens-views";
|
import { hasLoadedView, initView, lensViews, refreshViews } from "./lens-views";
|
||||||
import { globalPageRegistry } from "../../../extensions/registries/page-registry";
|
import { globalPageRegistry } from "../../../extensions/registries/page-registry";
|
||||||
|
import { Extensions, extensionsRoute } from "../+extensions";
|
||||||
import { getMatchedClusterId } from "../../navigation";
|
import { getMatchedClusterId } from "../../navigation";
|
||||||
|
|
||||||
@observer
|
@observer
|
||||||
@ -63,6 +64,7 @@ export class ClusterManager extends React.Component {
|
|||||||
<Switch>
|
<Switch>
|
||||||
<Route component={LandingPage} {...landingRoute} />
|
<Route component={LandingPage} {...landingRoute} />
|
||||||
<Route component={Preferences} {...preferencesRoute} />
|
<Route component={Preferences} {...preferencesRoute} />
|
||||||
|
<Route component={Extensions} {...extensionsRoute} />
|
||||||
<Route component={Workspaces} {...workspacesRoute} />
|
<Route component={Workspaces} {...workspacesRoute} />
|
||||||
<Route component={AddCluster} {...addClusterRoute} />
|
<Route component={AddCluster} {...addClusterRoute} />
|
||||||
<Route component={ClusterView} {...clusterViewRoute} />
|
<Route component={ClusterView} {...clusterViewRoute} />
|
||||||
|
|||||||
@ -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>}/>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -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);
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user