From d7c7593c0d0b6aabe5a5f2654e85ec3fe4461041 Mon Sep 17 00:00:00 2001 From: Sebastian Malton Date: Wed, 4 Nov 2020 05:49:37 -0500 Subject: [PATCH 1/6] Add CI step to run future extension tests (#1214) Signed-off-by: Sebastian Malton --- .azure-pipelines.yml | 6 ++++++ Makefile | 15 ++++++++++----- build/set_npm_version.ts | 2 +- extensions/example-extension/Makefile | 7 +++++-- extensions/example-extension/package.json | 3 ++- extensions/license-menu-item/Makefile | 7 +++++-- extensions/license-menu-item/package.json | 3 ++- extensions/metrics-cluster-feature/Makefile | 7 +++++-- extensions/metrics-cluster-feature/package.json | 3 ++- extensions/node-menu/Makefile | 7 +++++-- extensions/node-menu/package.json | 3 ++- extensions/pod-menu/Makefile | 7 +++++-- extensions/pod-menu/package.json | 3 ++- extensions/support-page/Makefile | 7 +++++-- extensions/support-page/package.json | 3 ++- extensions/telemetry/Makefile | 7 +++++-- extensions/telemetry/package-lock.json | 12 ++++++------ extensions/telemetry/package.json | 3 ++- 18 files changed, 72 insertions(+), 33 deletions(-) diff --git a/.azure-pipelines.yml b/.azure-pipelines.yml index 56e5fbe57e..f6060ca1dd 100644 --- a/.azure-pipelines.yml +++ b/.azure-pipelines.yml @@ -39,6 +39,8 @@ jobs: displayName: Install dependencies - script: make integration-win displayName: Run integration tests + - script: make test-extensions + displayName: Run In-tree Extension tests - script: make build condition: "and(succeeded(), startsWith(variables['Build.SourceBranch'], 'refs/tags/'))" displayName: Build @@ -78,6 +80,8 @@ jobs: displayName: Run tests - script: make integration-mac displayName: Run integration tests + - script: make test-extensions + displayName: Run In-tree Extension tests - script: make build condition: "and(succeeded(), startsWith(variables['Build.SourceBranch'], 'refs/tags/'))" displayName: Build @@ -119,6 +123,8 @@ jobs: condition: eq(variables.CACHE_RESTORED, 'true') - script: make install-deps displayName: Install dependencies + - script: make test-extensions + displayName: Run In-tree Extension tests - script: make lint displayName: Lint - script: make test diff --git a/Makefile b/Makefile index 514e64ae49..434e1dd6e1 100644 --- a/Makefile +++ b/Makefile @@ -33,15 +33,15 @@ lint: test: download-bins yarn test -integration-linux: +integration-linux: build-extension-types build-extensions yarn build:linux yarn integration -integration-mac: +integration-mac: build-extension-types build-extensions yarn build:mac yarn integration -integration-win: +integration-win: build-extension-types build-extensions yarn build:win yarn integration @@ -58,10 +58,15 @@ endif build-extensions: $(foreach dir, $(wildcard $(EXTENSIONS_DIR)/*), $(MAKE) -C $(dir) build;) -build-npm: - yarn compile:extension-types +test-extensions: + $(foreach dir, $(wildcard $(EXTENSIONS_DIR)/*), $(MAKE) -C $(dir) test;) + +build-npm: build-extension-types yarn npm:fix-package-version +build-extension-types: + yarn compile:extension-types + publish-npm: build-npm npm config set '//registry.npmjs.org/:_authToken' "${NPM_TOKEN}" cd src/extensions/npm/extensions && npm publish --access=public diff --git a/build/set_npm_version.ts b/build/set_npm_version.ts index 34a11da6c9..70ce97416d 100644 --- a/build/set_npm_version.ts +++ b/build/set_npm_version.ts @@ -6,4 +6,4 @@ import appInfo from "../package.json" const packagePath = path.join(__dirname, "../src/extensions/npm/extensions/package.json") packageInfo.version = appInfo.version -fs.writeFileSync(packagePath, JSON.stringify(packageInfo, null, 2)) +fs.writeFileSync(packagePath, JSON.stringify(packageInfo, null, 2) + "\n") diff --git a/extensions/example-extension/Makefile b/extensions/example-extension/Makefile index d73e8524cb..10cb709d20 100644 --- a/extensions/example-extension/Makefile +++ b/extensions/example-extension/Makefile @@ -1,5 +1,8 @@ install-deps: - npm install + yarn install build: install-deps - npm run build + yarn run build + +test: + yarn run test diff --git a/extensions/example-extension/package.json b/extensions/example-extension/package.json index a91b332f38..44f9940d91 100644 --- a/extensions/example-extension/package.json +++ b/extensions/example-extension/package.json @@ -10,7 +10,8 @@ }, "scripts": { "build": "webpack --config webpack.config.js", - "dev": "npm run build --watch" + "dev": "npm run build --watch", + "test": "echo NO TESTS" }, "dependencies": { "react-open-doodles": "^1.0.5" diff --git a/extensions/license-menu-item/Makefile b/extensions/license-menu-item/Makefile index d73e8524cb..10cb709d20 100644 --- a/extensions/license-menu-item/Makefile +++ b/extensions/license-menu-item/Makefile @@ -1,5 +1,8 @@ install-deps: - npm install + yarn install build: install-deps - npm run build + yarn run build + +test: + yarn run test diff --git a/extensions/license-menu-item/package.json b/extensions/license-menu-item/package.json index ee766cc9a1..ebc036ad7c 100644 --- a/extensions/license-menu-item/package.json +++ b/extensions/license-menu-item/package.json @@ -5,7 +5,8 @@ "main": "dist/main.js", "scripts": { "build": "webpack -p", - "dev": "webpack --watch" + "dev": "webpack --watch", + "test": "echo NO TESTS" }, "dependencies": {}, "devDependencies": { diff --git a/extensions/metrics-cluster-feature/Makefile b/extensions/metrics-cluster-feature/Makefile index d73e8524cb..10cb709d20 100644 --- a/extensions/metrics-cluster-feature/Makefile +++ b/extensions/metrics-cluster-feature/Makefile @@ -1,5 +1,8 @@ install-deps: - npm install + yarn install build: install-deps - npm run build + yarn run build + +test: + yarn run test diff --git a/extensions/metrics-cluster-feature/package.json b/extensions/metrics-cluster-feature/package.json index 0843156f0a..147deec783 100644 --- a/extensions/metrics-cluster-feature/package.json +++ b/extensions/metrics-cluster-feature/package.json @@ -9,7 +9,8 @@ }, "scripts": { "build": "webpack --config webpack.config.js", - "dev": "npm run build --watch" + "dev": "npm run build --watch", + "test": "echo NO TESTS" }, "dependencies": { "semver": "^7.3.2" diff --git a/extensions/node-menu/Makefile b/extensions/node-menu/Makefile index d73e8524cb..10cb709d20 100644 --- a/extensions/node-menu/Makefile +++ b/extensions/node-menu/Makefile @@ -1,5 +1,8 @@ install-deps: - npm install + yarn install build: install-deps - npm run build + yarn run build + +test: + yarn run test diff --git a/extensions/node-menu/package.json b/extensions/node-menu/package.json index c183316a78..b78fe185be 100644 --- a/extensions/node-menu/package.json +++ b/extensions/node-menu/package.json @@ -9,7 +9,8 @@ }, "scripts": { "build": "webpack --config webpack.config.js", - "dev": "npm run build --watch" + "dev": "npm run build --watch", + "test": "echo NO TESTS" }, "dependencies": {}, "devDependencies": { diff --git a/extensions/pod-menu/Makefile b/extensions/pod-menu/Makefile index d73e8524cb..10cb709d20 100644 --- a/extensions/pod-menu/Makefile +++ b/extensions/pod-menu/Makefile @@ -1,5 +1,8 @@ install-deps: - npm install + yarn install build: install-deps - npm run build + yarn run build + +test: + yarn run test diff --git a/extensions/pod-menu/package.json b/extensions/pod-menu/package.json index 21b4f3d65b..48fa397c96 100644 --- a/extensions/pod-menu/package.json +++ b/extensions/pod-menu/package.json @@ -9,7 +9,8 @@ }, "scripts": { "build": "webpack --config webpack.config.js", - "dev": "npm run build --watch" + "dev": "npm run build --watch", + "test": "echo NO TESTS" }, "dependencies": {}, "devDependencies": { diff --git a/extensions/support-page/Makefile b/extensions/support-page/Makefile index d73e8524cb..10cb709d20 100644 --- a/extensions/support-page/Makefile +++ b/extensions/support-page/Makefile @@ -1,5 +1,8 @@ install-deps: - npm install + yarn install build: install-deps - npm run build + yarn run build + +test: + yarn run test diff --git a/extensions/support-page/package.json b/extensions/support-page/package.json index a556eeb37b..fd98150423 100644 --- a/extensions/support-page/package.json +++ b/extensions/support-page/package.json @@ -6,7 +6,8 @@ "renderer": "dist/renderer.js", "scripts": { "build": "webpack -p", - "dev": "webpack --watch" + "dev": "webpack --watch", + "test": "echo NO TESTS" }, "dependencies": {}, "devDependencies": { diff --git a/extensions/telemetry/Makefile b/extensions/telemetry/Makefile index d73e8524cb..10cb709d20 100644 --- a/extensions/telemetry/Makefile +++ b/extensions/telemetry/Makefile @@ -1,5 +1,8 @@ install-deps: - npm install + yarn install build: install-deps - npm run build + yarn run build + +test: + yarn run test diff --git a/extensions/telemetry/package-lock.json b/extensions/telemetry/package-lock.json index bff56fc715..bf5717ac5e 100644 --- a/extensions/telemetry/package-lock.json +++ b/extensions/telemetry/package-lock.json @@ -8,12 +8,6 @@ "version": "file:../../src/extensions/npm/extensions", "dev": true }, - "@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 - }, "@segment/loosely-validate-event": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/@segment/loosely-validate-event/-/loosely-validate-event-2.0.0.tgz", @@ -24,6 +18,12 @@ "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": { "version": "1.9.0", "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.9.0.tgz", diff --git a/extensions/telemetry/package.json b/extensions/telemetry/package.json index cfb2bd6fa3..2b7c1f83de 100644 --- a/extensions/telemetry/package.json +++ b/extensions/telemetry/package.json @@ -10,7 +10,8 @@ }, "scripts": { "build": "webpack -p", - "dev": "webpack --watch" + "dev": "webpack --watch", + "test": "echo NO TESTS" }, "dependencies": {}, "devDependencies": { From 88a550fbda1220608080786ba54a7baf276cb057 Mon Sep 17 00:00:00 2001 From: chh <1474479+chenhunghan@users.noreply.github.com> Date: Wed, 4 Nov 2020 21:43:17 +0800 Subject: [PATCH 2/6] Add if to only run labler on lens own repo but not fork (#1221) Signed-off-by: Hung-Han (Henry) Chen <1474479+chenhunghan@users.noreply.github.com> --- .github/workflows/labeler.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/labeler.yml b/.github/workflows/labeler.yml index aed00b3de2..b57d58e567 100644 --- a/.github/workflows/labeler.yml +++ b/.github/workflows/labeler.yml @@ -1,7 +1,7 @@ --- name: "Pull Request Labeler" -'on': +on: - pull_request jobs: @@ -9,6 +9,7 @@ jobs: 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 From 02b9ac6b8b081ba8938b516e628e50ef3379288d Mon Sep 17 00:00:00 2001 From: Sebastian Malton Date: Wed, 4 Nov 2020 08:44:24 -0500 Subject: [PATCH 3/6] Add @types/node to devDeps for npm extension (#1217) Signed-off-by: Sebastian Malton --- extensions/support-page/package-lock.json | 6 ------ extensions/support-page/package.json | 1 - src/extensions/npm/extensions/.gitignore | 1 + src/extensions/npm/extensions/package.json | 3 +++ 4 files changed, 4 insertions(+), 7 deletions(-) diff --git a/extensions/support-page/package-lock.json b/extensions/support-page/package-lock.json index babb7a254a..f67bf34b5f 100644 --- a/extensions/support-page/package-lock.json +++ b/extensions/support-page/package-lock.json @@ -26,12 +26,6 @@ "integrity": "sha512-3c+yGKvVP5Y9TYBEibGNR+kLtijnj7mYrXRg+WpFb2X9xm04g/DXYkfg4hmzJQosc9snFNUPkbYIhu+KAm6jJw==", "dev": true }, - "@types/node": { - "version": "14.11.11", - "resolved": "https://registry.npmjs.org/@types/node/-/node-14.11.11.tgz", - "integrity": "sha512-UcaAZrL8uO5GNS+NLxkYg1RiOMgdLxCXGqs+TTupltXN8rTvUEKTOpqCV3tlcAIZJXzcBQajzmjdrvuPvnuMUw==", - "dev": true - }, "@types/prop-types": { "version": "15.7.3", "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.3.tgz", diff --git a/extensions/support-page/package.json b/extensions/support-page/package.json index fd98150423..550021efd4 100644 --- a/extensions/support-page/package.json +++ b/extensions/support-page/package.json @@ -12,7 +12,6 @@ "dependencies": {}, "devDependencies": { "@k8slens/extensions": "file:../../src/extensions/npm/extensions", - "@types/node": "^14.11.11", "@types/react": "^16.9.53", "@types/react-router": "^5.1.8", "@types/webpack": "^4.41.17", diff --git a/src/extensions/npm/extensions/.gitignore b/src/extensions/npm/extensions/.gitignore index 2b33ba5bed..482f8026a4 100644 --- a/src/extensions/npm/extensions/.gitignore +++ b/src/extensions/npm/extensions/.gitignore @@ -1 +1,2 @@ api.d.ts +yarn.lock diff --git a/src/extensions/npm/extensions/package.json b/src/extensions/npm/extensions/package.json index 8c56c1f6eb..bfb32f75ce 100644 --- a/src/extensions/npm/extensions/package.json +++ b/src/extensions/npm/extensions/package.json @@ -12,5 +12,8 @@ "author": { "name": "Mirantis, Inc.", "email": "info@k8slens.dev" + }, + "devDependencies": { + "@types/node": "^14.14.6" } } From d5214e47c121a0f1a0065026b02e6f5ae1f762a9 Mon Sep 17 00:00:00 2001 From: Lauri Nevala Date: Wed, 4 Nov 2020 17:16:48 +0200 Subject: [PATCH 4/6] Release v4.0.0-alpha.4 (#1224) Signed-off-by: Lauri Nevala --- package.json | 2 +- static/RELEASE_NOTES.md | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index d110e0ed78..f44078b42b 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "kontena-lens", "productName": "Lens", "description": "Lens - The Kubernetes IDE", - "version": "4.0.0-alpha.3", + "version": "4.0.0-alpha.4", "main": "static/build/main.js", "copyright": "© 2020, Mirantis, Inc.", "license": "MIT", diff --git a/static/RELEASE_NOTES.md b/static/RELEASE_NOTES.md index a797d716b8..8012c2c35c 100644 --- a/static/RELEASE_NOTES.md +++ b/static/RELEASE_NOTES.md @@ -2,7 +2,7 @@ Here you can find description of changes we've built into each release. While we try our best to make each upgrade automatic and as smooth as possible, there may be some cases where you might need to do something to ensure the application works smoothly. So please read through the release highlights! -## 4.0.0-alpha.3 (current version) +## 4.0.0-alpha.4 (current version) - Extension API - Improved pod logs @@ -14,6 +14,8 @@ Here you can find description of changes we've built into each release. While we - Status bar visual fixes - Fix proxy upgrade socket timeouts - Fix UI staleness after network issues +- Add +/- buttons in scale deployment popup screen +- Update chart details when selecting another chart ## 3.6.6 - Fix labels' word boundary to cover only drawer badges From f9578ba40794c65840fb918d2eadee2e67e0478a Mon Sep 17 00:00:00 2001 From: Roman Date: Wed, 4 Nov 2020 19:25:36 +0200 Subject: [PATCH 5/6] UI for enabling/disabling extensions (#1208) * Extensions page and menu item Signed-off-by: Alex Andreev * Basic extension list view Signed-off-by: Alex Andreev * Adding get userExtensions filter Signed-off-by: Alex Andreev * Using WizardLayout at extension page Signed-off-by: Alex Andreev * Adding search to extension page Signed-off-by: Alex Andreev * Few style fixes Signed-off-by: Alex Andreev * clean up Signed-off-by: Roman * added folder-icon to open extensions in finder, refactoring Signed-off-by: Roman * remove export warnings in dev:main, tooltip.getPosition() fix Signed-off-by: Roman * refactoring base lens-extension.ts, added `isBundled` flag Signed-off-by: Roman * added enabled/disable buttons Signed-off-by: Roman * auto enable/disable extensions -- part 1 Signed-off-by: Roman * auto enable/disable extensions -- part 2 Signed-off-by: Roman * auto enable/disable extensions -- part 3 Signed-off-by: Roman * auto enable/disable extensions -- part 4 Signed-off-by: Roman * refactoring & fixes Signed-off-by: Roman * fix: use page-layout with fullsize viewport Signed-off-by: Roman Co-authored-by: Alex Andreev --- src/common/base-store.ts | 22 ++- src/extensions/core-api/cluster-feature.ts | 3 +- src/extensions/core-api/stores.ts | 8 +- src/extensions/extension-loader.ts | 116 ++++++------ src/extensions/extension-manager.ts | 32 ++-- src/extensions/extension-store.ts | 4 +- src/extensions/lens-extension.ts | 173 ++++++++++-------- src/extensions/registries/base-registry.ts | 16 +- src/main/index.ts | 13 +- src/main/menu.ts | 8 + .../+extensions/extensions.route.ts | 8 + .../components/+extensions/extensions.scss | 35 ++++ .../components/+extensions/extensions.tsx | 112 ++++++++++++ src/renderer/components/+extensions/index.ts | 2 + .../cluster-manager/cluster-manager.tsx | 2 + .../components/layout/page-layout.scss | 12 +- src/renderer/components/tooltip/tooltip.tsx | 1 - 17 files changed, 376 insertions(+), 191 deletions(-) create mode 100644 src/renderer/components/+extensions/extensions.route.ts create mode 100644 src/renderer/components/+extensions/extensions.scss create mode 100644 src/renderer/components/+extensions/extensions.tsx create mode 100644 src/renderer/components/+extensions/index.ts diff --git a/src/common/base-store.ts b/src/common/base-store.ts index 5261c604d6..b2ba812b0a 100644 --- a/src/common/base-store.ts +++ b/src/common/base-store.ts @@ -2,7 +2,7 @@ import path from "path" import Config from "conf" import { Options as ConfOptions } from "conf/dist/source/types" 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 { getAppVersion } from "./utils/app-version"; import logger from "../main/logger"; @@ -12,6 +12,7 @@ import isEqual from "lodash/isEqual"; export interface BaseStoreParams extends ConfOptions { autoLoad?: boolean; syncEnabled?: boolean; + syncOptions?: IReactionOptions; } export class BaseStore extends Singleton { @@ -20,7 +21,7 @@ export class BaseStore extends Singleton { whenLoaded = when(() => this.isLoaded); @observable isLoaded = false; - @observable protected data: T; + @observable data = {} as T; protected constructor(protected params: BaseStoreParams) { super(); @@ -36,8 +37,12 @@ export class BaseStore extends Singleton { return path.basename(this.storeConfig.path); } + get path() { + return this.storeConfig.path; + } + get syncChannel() { - return `store-sync:${this.name}` + return `STORE-SYNC:${this.path}` } protected async init() { @@ -56,19 +61,19 @@ export class BaseStore extends Singleton { ...confOptions, projectName: "lens", 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.isLoaded = true; } - protected storePath() { + protected cwd() { return (app || remote.app).getPath("userData") } 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 Object.entries(model).forEach(([key, value]) => { this.storeConfig.set(key, value); @@ -77,7 +82,7 @@ export class BaseStore extends Singleton { enableSync() { this.syncDisposers.push( - reaction(() => this.toJSON(), model => this.onModelChange(model)), + reaction(() => this.toJSON(), model => this.onModelChange(model), this.params.syncOptions), ); if (ipcMain) { const callback = (event: IpcMainEvent, model: T) => { @@ -169,6 +174,7 @@ export class BaseStore extends Singleton { @action protected fromStore(data: T) { + if (!data) return; this.data = data; } diff --git a/src/extensions/core-api/cluster-feature.ts b/src/extensions/core-api/cluster-feature.ts index f06eba1436..9f2d3b8a40 100644 --- a/src/extensions/core-api/cluster-feature.ts +++ b/src/extensions/core-api/cluster-feature.ts @@ -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" diff --git a/src/extensions/core-api/stores.ts b/src/extensions/core-api/stores.ts index d39314f762..d44536769f 100644 --- a/src/extensions/core-api/stores.ts +++ b/src/extensions/core-api/stores.ts @@ -1,4 +1,6 @@ export { ExtensionStore } from "../extension-store" -export { clusterStore, ClusterModel } from "../../common/cluster-store" -export { Cluster } from "../../main/cluster" -export { workspaceStore, Workspace, WorkspaceModel } from "../../common/workspace-store" +export { clusterStore } from "../../common/cluster-store" +export type { ClusterModel } from "../../common/cluster-store" +export { Cluster } from "../../main/cluster" +export { workspaceStore, Workspace } from "../../common/workspace-store" +export type { WorkspaceModel } from "../../common/workspace-store" diff --git a/src/extensions/extension-loader.ts b/src/extensions/extension-loader.ts index 5495438555..64c4270439 100644 --- a/src/extensions/extension-loader.ts +++ b/src/extensions/extension-loader.ts @@ -1,20 +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 { LensRendererExtension } from "./lens-renderer-extension" +import type { InstalledExtension } from "./extension-manager"; import path from "path" import { broadcastIpc } from "../common/ipc" -import { observable, reaction, toJS, } from "mobx" +import { computed, observable, reaction, when } from "mobx" import logger from "../main/logger" import { app, ipcRenderer, remote } from "electron" -import { - appPreferenceRegistry, clusterFeatureRegistry, clusterPageRegistry, globalPageRegistry, - kubeObjectDetailRegistry, kubeObjectMenuRegistry, menuRegistry, statusBarRegistry -} from "./registries"; - -export interface InstalledExtension extends ExtensionModel { - manifestPath: string; - manifest: ExtensionManifest; -} +import * as registries from "./registries"; // lazy load so that we get correct userData export function extensionPackagesRoot() { @@ -22,69 +15,82 @@ export function extensionPackagesRoot() { } export class ExtensionLoader { - @observable extensions = observable.map([], { deep: false }); - @observable instances = observable.map([], { deep: false }) + @observable isLoaded = false; + protected extensions = observable.map([], { deep: false }); + protected instances = observable.map([], { deep: false }) constructor() { if (ipcRenderer) { ipcRenderer.on("extensions:loaded", (event, extensions: InstalledExtension[]) => { + this.isLoaded = true; extensions.forEach((ext) => { - if (!this.getById(ext.manifestPath)) { + if (!this.extensions.has(ext.manifestPath)) { 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() { logger.info('[EXTENSIONS-LOADER]: load on main') - this.autoloadExtensions((extension: LensMainExtension) => { - extension.registerTo(menuRegistry, extension.appMenus) - }) + this.autoInitExtensions((extension: LensMainExtension) => [ + registries.menuRegistry.add(...extension.appMenus) + ]); } loadOnClusterManagerRenderer() { logger.info('[EXTENSIONS-LOADER]: load on main renderer (cluster manager)') - this.autoloadExtensions((extension: LensRendererExtension) => { - extension.registerTo(globalPageRegistry, extension.globalPages) - extension.registerTo(appPreferenceRegistry, extension.appPreferences) - extension.registerTo(clusterFeatureRegistry, extension.clusterFeatures) - extension.registerTo(statusBarRegistry, extension.statusBarItems) - }) + this.autoInitExtensions((extension: LensRendererExtension) => [ + registries.globalPageRegistry.add(...extension.globalPages), + registries.appPreferenceRegistry.add(...extension.appPreferences), + registries.clusterFeatureRegistry.add(...extension.clusterFeatures), + registries.statusBarRegistry.add(...extension.statusBarItems), + ]); } loadOnClusterRenderer() { logger.info('[EXTENSIONS-LOADER]: load on cluster renderer (dashboard)') - this.autoloadExtensions((extension: LensRendererExtension) => { - extension.registerTo(clusterPageRegistry, extension.clusterPages) - extension.registerTo(kubeObjectMenuRegistry, extension.kubeObjectMenuItems) - extension.registerTo(kubeObjectDetailRegistry, extension.kubeObjectDetailItems) - }) + this.autoInitExtensions((extension: LensRendererExtension) => [ + registries.clusterPageRegistry.add(...extension.clusterPages), + 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) => { - for(const [id, ext] of installedExtensions) { - let instance = this.instances.get(ext.id) + for (const [id, ext] of installedExtensions) { + let instance = this.instances.get(ext.manifestPath) if (!instance) { const extensionModule = this.requireExtension(ext) if (!extensionModule) { continue } - const LensExtensionClass = extensionModule.default; - instance = new LensExtensionClass({ ...ext.manifest, manifestPath: ext.manifestPath, id: ext.manifestPath }, ext.manifest); try { - instance.enable() - callback(instance) - } finally { - this.instances.set(ext.id, instance) + const LensExtensionClass: LensExtensionConstructor = extensionModule.default; + instance = new LensExtensionClass(ext); + instance.whenEnabled(() => register(instance)); + this.instances.set(ext.manifestPath, instance); + } catch (err) { + logger.error(`[EXTENSIONS-LOADER]: init extension instance error`, { ext, err }) } } } }, { fireImmediately: true, - delay: 0, }) } @@ -105,37 +111,17 @@ export class ExtensionLoader { } } - getById(id: ExtensionId): InstalledExtension { - return this.extensions.get(id); - } - - async removeById(id: ExtensionId) { - const extension = this.getById(id); - if (extension) { - const instance = this.instances.get(extension.id) - if (instance) { - await instance.disable() - } - this.extensions.delete(id); - } - } - - broadcastExtensions(frameId?: number) { + async broadcastExtensions(frameId?: number) { + await when(() => this.isLoaded); broadcastIpc({ channel: "extensions:loaded", frameId: frameId, frameOnly: !!frameId, - args: [this.toJSON().extensions], - }) - } - - toJSON() { - return toJS({ - extensions: Array.from(this.extensions).map(([id, instance]) => instance), - }, { - recurseEverything: true, + args: [ + Array.from(this.extensions.toJS().values()) + ], }) } } -export const extensionLoader = new ExtensionLoader() +export const extensionLoader = new ExtensionLoader(); diff --git a/src/extensions/extension-manager.ts b/src/extensions/extension-manager.ts index 332b0e5691..1d37707596 100644 --- a/src/extensions/extension-manager.ts +++ b/src/extensions/extension-manager.ts @@ -1,12 +1,18 @@ -import type { ExtensionManifest } from "./lens-extension" +import type { LensExtensionId, LensExtensionManifest } from "./lens-extension" import path from "path" import os from "os" import fs from "fs-extra" +import child_process from "child_process"; import logger from "../main/logger" -import { extensionPackagesRoot, InstalledExtension } from "./extension-loader" -import * as child_process from 'child_process'; +import { extensionPackagesRoot } from "./extension-loader" import { getBundledExtensions } from "../common/utils/app-version" +export interface InstalledExtension { + manifest: LensExtensionManifest; + manifestPath: string; + isBundled?: boolean; // defined in package.json +} + type Dependencies = { [name: string]: string; } @@ -51,7 +57,7 @@ export class ExtensionManager { return path.join(this.extensionPackagesRoot, "package.json") } - async load() { + async load(): Promise> { logger.info("[EXTENSION-MANAGER] loading extensions from " + this.extensionPackagesRoot) if (fs.existsSync(path.join(this.extensionPackagesRoot, "package-lock.json"))) { await fs.remove(path.join(this.extensionPackagesRoot, "package-lock.json")) @@ -71,8 +77,8 @@ export class ExtensionManager { return await this.loadExtensions(); } - async getExtensionByManifest(manifestPath: string): Promise { - let manifestJson: ExtensionManifest; + protected async getByManifest(manifestPath: string): Promise { + let manifestJson: LensExtensionManifest; try { fs.accessSync(manifestPath, fs.constants.F_OK); // check manifest file for existence manifestJson = __non_webpack_require__(manifestPath) @@ -80,11 +86,8 @@ export class ExtensionManager { logger.info("[EXTENSION-MANAGER] installed extension " + manifestJson.name) return { - id: manifestJson.name, - version: manifestJson.version, - name: manifestJson.name, manifestPath: path.join(this.nodeModulesPath, manifestJson.name, "package.json"), - manifest: manifestJson + manifest: manifestJson, } } catch (err) { logger.error(`[EXTENSION-MANAGER]: can't install extension at ${manifestPath}: ${err}`, { manifestJson }); @@ -109,10 +112,10 @@ export class ExtensionManager { async loadExtensions() { const bundledExtensions = await this.loadBundledExtensions() const localExtensions = await this.loadFromFolder(this.localFolderPath) - await fs.writeFile(path.join(this.packageJsonPath), JSON.stringify(this.packagesJson, null, 2), {mode: 0o600}) + await fs.writeFile(path.join(this.packageJsonPath), JSON.stringify(this.packagesJson, null, 2), { mode: 0o600 }) await this.installPackages() 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() { @@ -126,8 +129,9 @@ export class ExtensionManager { } const absPath = path.resolve(folderPath, fileName); const manifestPath = path.resolve(absPath, "package.json"); - const ext = await this.getExtensionByManifest(manifestPath).catch(() => null) + const ext = await this.getByManifest(manifestPath).catch(() => null) if (ext) { + ext.isBundled = true; extensions.push(ext) } } @@ -152,7 +156,7 @@ export class ExtensionManager { continue } const manifestPath = path.resolve(absPath, "package.json"); - const ext = await this.getExtensionByManifest(manifestPath).catch(() => null) + const ext = await this.getByManifest(manifestPath).catch(() => null) if (ext) { extensions.push(ext) } diff --git a/src/extensions/extension-store.ts b/src/extensions/extension-store.ts index 5331420cd6..d5372eceff 100644 --- a/src/extensions/extension-store.ts +++ b/src/extensions/extension-store.ts @@ -15,7 +15,7 @@ export class ExtensionStore extends BaseStore { await super.load() } - protected storePath() { - return path.join(super.storePath(), "extension-store", this.extension.name) + protected cwd() { + return path.join(super.cwd(), "extension-store", this.extension.name) } } diff --git a/src/extensions/lens-extension.ts b/src/extensions/lens-extension.ts index 0921dc066a..246edfe110 100644 --- a/src/extensions/lens-extension.ts +++ b/src/extensions/lens-extension.ts @@ -1,75 +1,111 @@ -import { readJsonSync } from "fs-extra"; -import { action, observable, toJS } from "mobx"; +import type { InstalledExtension } from "./extension-manager"; +import { action, reaction } from "mobx"; import logger from "../main/logger"; -import { BaseRegistry } from "./registries/base-registry"; +import { ExtensionStore } from "./extension-store"; -export type ExtensionId = string | ExtensionPackageJsonPath; -export type ExtensionPackageJsonPath = string; -export type ExtensionVersion = string | number; +export type LensExtensionId = string; // path to manifest (package.json) +export type LensExtensionConstructor = new (...args: ConstructorParameters) => LensExtension; -export interface ExtensionModel { - id: ExtensionId; - version: ExtensionVersion; +export interface LensExtensionManifest { name: string; - manifestPath: string; + version: string; description?: string; - enabled?: boolean; - updateUrl?: string; + main?: string; // path to %ext/dist/main.js + renderer?: string; // path to %ext/dist/renderer.js } -export interface ExtensionManifest extends ExtensionModel { - main?: string; - renderer?: string; - description?: string; // todo: add more fields similar to package.json + some extra +export interface LensExtensionStoreModel { + isEnabled: boolean; } -export class LensExtension implements ExtensionModel { - public id: ExtensionId; - public updateUrl: string; - protected disposers: (() => void)[] = []; +export class LensExtension = any> { + protected store: S; + readonly manifest: LensExtensionManifest; + readonly manifestPath: string; + readonly isBundled: boolean; - @observable name = ""; - @observable description = ""; - @observable version: ExtensionVersion = "0.0.0"; - @observable manifest: ExtensionManifest; - @observable manifestPath: string; - @observable isEnabled = false; + constructor({ manifest, manifestPath, isBundled }: InstalledExtension) { + this.manifest = manifest + this.manifestPath = manifestPath + this.isBundled = !!isBundled + this.init(); + } - constructor(model: ExtensionModel, manifest: ExtensionManifest) { - this.importModel(model, manifest); + protected async init(store: S = createBaseStore().getInstance()) { + 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 - async importModel({ enabled, manifestPath, ...model }: ExtensionModel, manifest?: ExtensionManifest) { - try { - this.manifest = manifest || await readJsonSync(manifestPath, { throws: true }) - this.manifestPath = manifestPath; - Object.assign(this, model); - } catch (err) { - logger.error(`[EXTENSION]: cannot read manifest at ${manifestPath}`, { ...model, err: String(err) }) - this.disable(); - } - } - - async migrate(appVersion: string) { - // mock - } - async enable() { - this.isEnabled = true; - logger.info(`[EXTENSION]: enabled ${this.name}@${this.version}`); + if (this.isEnabled) return; + this.store.data.isEnabled = true; this.onActivate(); + logger.info(`[EXTENSION]: enabled ${this.name}@${this.version}`); } + @action async disable() { + if (!this.isEnabled) return; + this.store.data.isEnabled = false; this.onDeactivate(); - this.isEnabled = false; - this.disposers.forEach(cleanUp => cleanUp()); - this.disposers.length = 0; logger.info(`[EXTENSION]: disabled ${this.name}@${this.version}`); } - // todo: add more hooks + 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() { // mock } @@ -77,37 +113,14 @@ export class LensExtension implements ExtensionModel { protected onDeactivate() { // mock } +} - registerTo(registry: BaseRegistry, items: T[] = []) { - const disposers = items.map(item => registry.add(item)); - this.disposers.push(...disposers); - return () => { - this.disposers = this.disposers.filter(disposer => !disposers.includes(disposer)) - }; - } - - getMeta() { - return toJS({ - id: this.id, - 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, - }) +function createBaseStore() { + return class extends ExtensionStore { + constructor() { + super({ + configName: "state" + }); + } } } diff --git a/src/extensions/registries/base-registry.ts b/src/extensions/registries/base-registry.ts index 01613a59eb..ff23e36cad 100644 --- a/src/extensions/registries/base-registry.ts +++ b/src/extensions/registries/base-registry.ts @@ -1,5 +1,5 @@ // Base class for extensions-api registries -import { observable } from "mobx"; +import { action, observable } from "mobx"; export class BaseRegistry { protected items = observable([], { deep: false }); @@ -8,10 +8,16 @@ export class BaseRegistry { return this.items.toJS(); } - add(item: T) { - this.items.push(item); - return () => { + @action + add(...items: T[]) { + this.items.push(...items); + return () => this.remove(...items); + } + + @action + remove(...items: T[]) { + items.forEach(item => { this.items.remove(item); // works because of {deep: false}; - } + }) } } diff --git a/src/main/index.ts b/src/main/index.ts index ff7e050dd2..762c166bc4 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -15,13 +15,12 @@ import { shellSync } from "./shell-sync" import { getFreePort } from "./port" import { mangleProxyEnv } from "./proxy-env" import { registerFileProtocol } from "../common/register-protocol"; +import logger from "./logger" import { clusterStore } from "../common/cluster-store" import { userStore } from "../common/user-store"; import { workspaceStore } from "../common/workspace-store"; import { appEventBus } from "../common/event-bus" -import { extensionManager } from "../extensions/extension-manager"; import { extensionLoader } from "../extensions/extension-loader"; -import logger from "./logger" const workingDir = path.join(app.getPath("appData"), appName); let proxyPort: number; @@ -48,7 +47,7 @@ app.on("ready", async () => { registerFileProtocol("static", __static); - // preload isomorphic stores + // preload await Promise.all([ userStore.load(), clusterStore.load(), @@ -76,12 +75,8 @@ app.on("ready", async () => { app.exit(); } - windowManager = new WindowManager(proxyPort); - - LensExtensionsApi.windowManager = windowManager; // expose to extensions - extensionLoader.loadOnMain() - extensionLoader.extensions.replace(await extensionManager.load()) - extensionLoader.broadcastExtensions() + LensExtensionsApi.windowManager = windowManager = new WindowManager(proxyPort); + extensionLoader.init(); // call after windowManager to see splash earlier setTimeout(() => { appEventBus.emit({ name: "app", action: "start" }) diff --git a/src/main/menu.ts b/src/main/menu.ts index 4d018345e5..037378537d 100644 --- a/src/main/menu.ts +++ b/src/main/menu.ts @@ -6,6 +6,7 @@ import { addClusterURL } from "../renderer/components/+add-cluster/add-cluster.r import { preferencesURL } from "../renderer/components/+preferences/preferences.route"; import { whatsNewURL } from "../renderer/components/+whats-new/whats-new.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 logger from "./logger"; @@ -70,6 +71,13 @@ export function buildMenu(windowManager: WindowManager) { navigate(preferencesURL()) } }, + { + label: 'Extensions', + accelerator: 'CmdOrCtrl+Shift+E', + click() { + navigate(extensionsURL()) + } + }, { type: 'separator' }, { role: 'services' }, { type: 'separator' }, diff --git a/src/renderer/components/+extensions/extensions.route.ts b/src/renderer/components/+extensions/extensions.route.ts new file mode 100644 index 0000000000..78c5579901 --- /dev/null +++ b/src/renderer/components/+extensions/extensions.route.ts @@ -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) diff --git a/src/renderer/components/+extensions/extensions.scss b/src/renderer/components/+extensions/extensions.scss new file mode 100644 index 0000000000..8e9256c201 --- /dev/null +++ b/src/renderer/components/+extensions/extensions.scss @@ -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; + } + } +} \ No newline at end of file diff --git a/src/renderer/components/+extensions/extensions.tsx b/src/renderer/components/+extensions/extensions.tsx new file mode 100644 index 0000000000..7dc5f55726 --- /dev/null +++ b/src/renderer/components/+extensions/extensions.tsx @@ -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 ( +
+

Lens Extension API

+
+ 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. +
+
+ Extensions loaded from: +
+ {this.extensionsPath} + shell.openPath(this.extensionsPath)} + /> +
+
+
+ Check out documentation to learn more +
+
+ ) + } + + renderExtensions() { + const { extensions, extensionsPath, search } = this; + if (!extensions.length) { + return ( +
+ {search && No search results found} + {!search &&

There are no extensions in {extensionsPath}

} +
+ ) + } + return extensions.map(ext => { + const { id, name, description, isEnabled } = ext; + return ( +
+
+
+ Name: {name} +
+
+ Description: {description} +
+
+ {!isEnabled && ( + + )} + {isEnabled && ( + + )} +
+ ) + }) + } + + render() { + return ( + Extensions}> + + this.search = value} + /> +
+ {this.renderExtensions()} +
+
+
+ ); + } +} \ No newline at end of file diff --git a/src/renderer/components/+extensions/index.ts b/src/renderer/components/+extensions/index.ts new file mode 100644 index 0000000000..8946a5f6fe --- /dev/null +++ b/src/renderer/components/+extensions/index.ts @@ -0,0 +1,2 @@ +export * from "./extensions.route" +export * from "./extensions" diff --git a/src/renderer/components/cluster-manager/cluster-manager.tsx b/src/renderer/components/cluster-manager/cluster-manager.tsx index 785fe56e27..4a42a0419d 100644 --- a/src/renderer/components/cluster-manager/cluster-manager.tsx +++ b/src/renderer/components/cluster-manager/cluster-manager.tsx @@ -16,6 +16,7 @@ import { clusterViewRoute, clusterViewURL } from "./cluster-view.route"; import { clusterStore } from "../../../common/cluster-store"; import { hasLoadedView, initView, lensViews, refreshViews } from "./lens-views"; import { globalPageRegistry } from "../../../extensions/registries/page-registry"; +import { Extensions, extensionsRoute } from "../+extensions"; import { getMatchedClusterId } from "../../navigation"; @observer @@ -63,6 +64,7 @@ export class ClusterManager extends React.Component { + diff --git a/src/renderer/components/layout/page-layout.scss b/src/renderer/components/layout/page-layout.scss index 53268f948f..0ae6f54139 100644 --- a/src/renderer/components/layout/page-layout.scss +++ b/src/renderer/components/layout/page-layout.scss @@ -1,5 +1,8 @@ .PageLayout { $spacing: $padding * 2; + --width: 60%; + --max-width: 1000px; + --min-width: 570px; position: relative; height: 100%; @@ -26,12 +29,15 @@ > .content-wrapper { @include custom-scrollbar-themed; padding: $spacing * 2; + display: flex; + flex-direction: column; > .content { + flex: 1; margin: 0 auto; - width: 60%; - min-width: 570px; - max-width: 1000px; + width: var(--width); + min-width: var(--min-width); + max-width: var(--max-width); } } diff --git a/src/renderer/components/tooltip/tooltip.tsx b/src/renderer/components/tooltip/tooltip.tsx index 393651001f..409d5f2bd7 100644 --- a/src/renderer/components/tooltip/tooltip.tsx +++ b/src/renderer/components/tooltip/tooltip.tsx @@ -167,7 +167,6 @@ export class Tooltip extends React.Component { top = topCenter; break; case "top_right": - default: left = targetBounds.right - tooltipBounds.width; top = topCenter; break; From d074e0499ffb3e6ce42e5de14a1b7adeb0fac90d Mon Sep 17 00:00:00 2001 From: pashevskii <53330707+pashevskii@users.noreply.github.com> Date: Thu, 5 Nov 2020 11:23:14 +0400 Subject: [PATCH 6/6] Restart deployment (#1175) Signed-off-by: Pavel Ashevskii --- locales/en/messages.po | 8 ++++ locales/fi/messages.po | 8 ++++ locales/ru/messages.po | 8 ++++ src/renderer/api/endpoints/deployment.api.ts | 22 +++++++++++ src/renderer/api/json-api.ts | 2 +- .../+workloads-deployments/deployments.tsx | 38 ++++++++++++++++--- 6 files changed, 79 insertions(+), 7 deletions(-) diff --git a/locales/en/messages.po b/locales/en/messages.po index c062c63db1..220b5c93a8 100644 --- a/locales/en/messages.po +++ b/locales/en/messages.po @@ -549,6 +549,14 @@ msgstr "Condition" msgid "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}?" +msgstr "Are you sure you want to restart deployment <0>{0}?" + #: src/renderer/components/+config-maps/config-maps.tsx:33 msgid "Config Maps" msgstr "Config Maps" diff --git a/locales/fi/messages.po b/locales/fi/messages.po index ee19bf5187..ac1c8902a0 100644 --- a/locales/fi/messages.po +++ b/locales/fi/messages.po @@ -545,6 +545,14 @@ msgstr "" msgid "Conditions" 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}?" +msgstr "" + #: src/renderer/components/+config-maps/config-maps.tsx:33 msgid "Config Maps" msgstr "" diff --git a/locales/ru/messages.po b/locales/ru/messages.po index 01b8d777a3..9b4fbc99ba 100644 --- a/locales/ru/messages.po +++ b/locales/ru/messages.po @@ -550,6 +550,14 @@ msgstr "Состояние" msgid "Conditions" 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}?" +msgstr "Выполнить перезагрузку деплоймента <0>{0}?" + #: src/renderer/components/+config-maps/config-maps.tsx:33 msgid "Config Maps" msgstr "" diff --git a/src/renderer/api/endpoints/deployment.api.ts b/src/renderer/api/endpoints/deployment.api.ts index 25164e10f9..b21495ecc1 100644 --- a/src/renderer/api/endpoints/deployment.api.ts +++ b/src/renderer/api/endpoints/deployment.api.ts @@ -1,3 +1,5 @@ +import moment from "moment"; + import { IAffinity, WorkloadKubeObject } from "../workload-kube-object"; import { autobind } from "../../utils"; import { KubeApi } from "../kube-api"; @@ -23,6 +25,25 @@ export class DeploymentApi extends KubeApi { } }) } + + 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() @@ -38,6 +59,7 @@ export class Deployment extends WorkloadKubeObject { metadata: { creationTimestamp?: string; labels: { [app: string]: string }; + annotations?: { [app: string]: string }; }; spec: { containers: { diff --git a/src/renderer/api/json-api.ts b/src/renderer/api/json-api.ts index 027c201175..e42996c041 100644 --- a/src/renderer/api/json-api.ts +++ b/src/renderer/api/json-api.ts @@ -64,7 +64,7 @@ export class JsonApi { } patch(path: string, params?: P, reqInit: RequestInit = {}) { - return this.request(path, params, { ...reqInit, method: "patch" }); + return this.request(path, params, { ...reqInit, method: "PATCH" }); } del(path: string, params?: P, reqInit: RequestInit = {}) { diff --git a/src/renderer/components/+workloads-deployments/deployments.tsx b/src/renderer/components/+workloads-deployments/deployments.tsx index a7b9a01ab4..39047bf62a 100644 --- a/src/renderer/components/+workloads-deployments/deployments.tsx +++ b/src/renderer/components/+workloads-deployments/deployments.tsx @@ -4,11 +4,12 @@ import React from "react"; import { observer } from "mobx-react"; import { RouteComponentProps } from "react-router"; 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 { MenuItem } from "../menu"; import { Icon } from "../icon"; import { DeploymentScaleDialog } from "./deployment-scale-dialog"; +import { ConfirmDialog } from "../confirm-dialog"; import { deploymentStore } from "./deployments.store"; import { replicaSetStore } from "../+workloads-replicasets/replicasets.store"; import { podsStore } from "../+workloads-pods/pods.store"; @@ -22,6 +23,8 @@ import kebabCase from "lodash/kebabCase"; import orderBy from "lodash/orderBy"; import { KubeEventIcon } from "../+events/kube-event-icon"; import { kubeObjectMenuRegistry } from "../../../extensions/registries/kube-object-menu-registry"; +import { apiManager } from "../../api/api-manager"; +import { Notifications } from "../notifications"; enum sortBy { name = "name", @@ -96,10 +99,34 @@ export class Deployments extends React.Component { export function DeploymentMenu(props: KubeObjectMenuProps) { const { object, toolbar } = props; return ( - DeploymentScaleDialog.open(object)}> - - Scale - + <> + DeploymentScaleDialog.open(object)}> + + Scale + + ConfirmDialog.open({ + ok: async () => + { + try { + await deploymentApi.restart({ + namespace: object.getNs(), + name: object.getName(), + }) + } catch (err) { + Notifications.error(err); + } + }, + labelOk: _i18n._(t`Restart`), + message: ( +

+ Are you sure you want to restart deployment {object.getName()}? +

+ ), + })}> + + Restart +
+ ) } @@ -110,4 +137,3 @@ kubeObjectMenuRegistry.add({ MenuItem: DeploymentMenu } }) -