diff --git a/.dependabot/config.yml b/.dependabot/config.yml index a77a36c653..8292c7dde2 100644 --- a/.dependabot/config.yml +++ b/.dependabot/config.yml @@ -15,3 +15,7 @@ updates: versioning-strategy: lockfile-only: false increase: true + ignore: + - dependency-name: * + update-types: + - version-update:semver-major diff --git a/Makefile b/Makefile index f4538b256c..fc764d0a94 100644 --- a/Makefile +++ b/Makefile @@ -14,13 +14,13 @@ else DETECTED_OS := $(shell uname) endif -binaries/client: node_modules - yarn download-bins - node_modules: yarn.lock yarn install --frozen-lockfile --network-timeout=100000 yarn check --verify-tree --integrity +binaries/client: node_modules + yarn download-bins + static/build/LensDev.html: node_modules yarn compile:renderer @@ -51,11 +51,6 @@ test: binaries/client .PHONY: integration-linux integration-linux: binaries/client build-extension-types build-extensions -# ifdef XDF_CONFIG_HOME -# rm -rf ${XDG_CONFIG_HOME}/.config/Lens -# else -# rm -rf ${HOME}/.config/Lens -# endif yarn build:linux yarn integration @@ -81,7 +76,7 @@ else yarn run electron-builder --publish onTag endif -$(extension_node_modules): +$(extension_node_modules): node_modules cd $(@:/node_modules=) && ../../node_modules/.bin/npm install --no-audit --no-fund $(extension_dists): src/extensions/npm/extensions/dist @@ -102,7 +97,7 @@ copy-extension-themes: src/extensions/npm/extensions/__mocks__: cp -r __mocks__ src/extensions/npm/extensions/ -src/extensions/npm/extensions/dist: +src/extensions/npm/extensions/dist: node_modules yarn compile:extension-types .PHONY: build-npm diff --git a/docs/extensions/guides/stores.md b/docs/extensions/guides/stores.md index c8a5ec270d..2eaa589198 100644 --- a/docs/extensions/guides/stores.md +++ b/docs/extensions/guides/stores.md @@ -25,7 +25,7 @@ The following example code creates a store for the `appPreferences` guide exampl ``` typescript import { Store } from "@k8slens/extensions"; -import { observable, toJS } from "mobx"; +import { observable, makeObservable } from "mobx"; export type ExamplePreferencesModel = { enabled: boolean; @@ -42,6 +42,7 @@ export class ExamplePreferencesStore extends Store.ExtensionStore { + constructor(props: Props) { + super(props); + makeObservable(this); + } + @observable featureStates = { prometheus: false, kubeStateMetrics: false, diff --git a/extensions/metrics-cluster-feature/tsconfig.json b/extensions/metrics-cluster-feature/tsconfig.json index 016d32b0ba..f60a98c9ad 100644 --- a/extensions/metrics-cluster-feature/tsconfig.json +++ b/extensions/metrics-cluster-feature/tsconfig.json @@ -13,6 +13,7 @@ "esModuleInterop": true, "allowSyntheticDefaultImports": true, "experimentalDecorators": true, + "useDefineForClassFields": true, "jsx": "react" }, "include": [ diff --git a/extensions/node-menu/tsconfig.json b/extensions/node-menu/tsconfig.json index a93ad6fe9f..5b6c61577e 100644 --- a/extensions/node-menu/tsconfig.json +++ b/extensions/node-menu/tsconfig.json @@ -13,6 +13,7 @@ "esModuleInterop": true, "allowSyntheticDefaultImports": true, "experimentalDecorators": true, + "useDefineForClassFields": true, "jsx": "react" }, "include": [ diff --git a/extensions/pod-menu/tsconfig.json b/extensions/pod-menu/tsconfig.json index a93ad6fe9f..5b6c61577e 100644 --- a/extensions/pod-menu/tsconfig.json +++ b/extensions/pod-menu/tsconfig.json @@ -13,6 +13,7 @@ "esModuleInterop": true, "allowSyntheticDefaultImports": true, "experimentalDecorators": true, + "useDefineForClassFields": true, "jsx": "react" }, "include": [ diff --git a/integration/__tests__/app.tests.ts b/integration/__tests__/app.tests.ts index d433513f1e..7b8ff11435 100644 --- a/integration/__tests__/app.tests.ts +++ b/integration/__tests__/app.tests.ts @@ -57,17 +57,17 @@ describe("Lens integration tests", () => { const appName: string = process.platform === "darwin" ? "OpenLens" : "File"; await app.electron.ipcRenderer.send("test-menu-item-click", appName, "Preferences"); - await app.client.waitUntilTextExists("[data-testid=application-header]", "APPLICATION"); + await app.client.waitUntilTextExists("[data-testid=application-header]", "Application"); }); it("shows all tabs and their contents", async () => { await app.client.click("[data-testid=application-tab]"); await app.client.click("[data-testid=proxy-tab]"); - await app.client.waitUntilTextExists("[data-testid=proxy-header]", "PROXY"); + await app.client.waitUntilTextExists("[data-testid=proxy-header]", "Proxy"); await app.client.click("[data-testid=kube-tab]"); - await app.client.waitUntilTextExists("[data-testid=kubernetes-header]", "KUBERNETES"); + await app.client.waitUntilTextExists("[data-testid=kubernetes-header]", "Kubernetes"); await app.client.click("[data-testid=telemetry-tab]"); - await app.client.waitUntilTextExists("[data-testid=telemetry-header]", "TELEMETRY"); + await app.client.waitUntilTextExists("[data-testid=telemetry-header]", "Telemetry"); }); it("ensures helm repos", async () => { diff --git a/package.json b/package.json index d51894f145..e2e8851388 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "open-lens", "productName": "OpenLens", "description": "OpenLens - Open Source IDE for Kubernetes", - "version": "5.0.0-beta.5", + "version": "5.0.0-beta.6", "main": "static/build/main.js", "copyright": "© 2021 OpenLens Authors", "license": "MIT", @@ -183,6 +183,8 @@ "@kubernetes/client-node": "^0.12.0", "abort-controller": "^3.0.0", "array-move": "^3.0.0", + "auto-bind": "^4.0.0", + "autobind-decorator": "^2.4.0", "await-lock": "^2.1.0", "byline": "^5.0.0", "chalk": "^4.1.0", @@ -207,9 +209,9 @@ "mac-ca": "^1.0.4", "marked": "^2.0.3", "md5-file": "^5.0.0", - "mobx": "^5.15.7", - "mobx-observable-history": "^1.0.3", - "mobx-react": "^6.2.2", + "mobx": "^6.3.0", + "mobx-observable-history": "^2.0.1", + "mobx-react": "^7.1.0", "mock-fs": "^4.12.0", "moment": "^2.26.0", "moment-timezone": "^0.5.33", @@ -241,7 +243,7 @@ }, "devDependencies": { "@emeraldpay/hashicon-react": "^0.4.0", - "@material-ui/core": "^4.10.1", + "@material-ui/core": "^4.11.4", "@material-ui/icons": "^4.11.2", "@material-ui/lab": "^4.0.0-alpha.57", "@pmmmwh/react-refresh-webpack-plugin": "^0.4.3", @@ -280,6 +282,7 @@ "@types/react-dom": "^17.0.0", "@types/react-router-dom": "^5.1.6", "@types/react-select": "^3.0.13", + "@types/react-table": "^7.7.0", "@types/react-window": "^1.8.2", "@types/readable-stream": "^2.3.9", "@types/request": "^2.48.5", @@ -336,8 +339,9 @@ "nodemon": "^2.0.4", "open": "^7.3.1", "patch-package": "^6.2.2", + "postcss": "^8.2.14", + "postcss-loader": "~3.0.0", "postinstall-postinstall": "^2.1.0", - "prettier": "^2.2.0", "progress-bar-webpack-plugin": "^2.1.0", "randomcolor": "^0.6.2", "raw-loader": "^4.0.1", @@ -346,11 +350,13 @@ "react-router-dom": "^5.2.0", "react-select": "^3.1.0", "react-select-event": "^5.1.0", + "react-table": "^7.7.0", "react-window": "^1.8.5", "sass-loader": "^8.0.2", "sharp": "^0.26.1", "spectron": "11.0.0", "style-loader": "^1.2.1", + "tailwindcss": "^2.1.2", "ts-jest": "26.3.0", "ts-loader": "^7.0.5", "ts-node": "^8.10.2", @@ -360,6 +366,7 @@ "typedoc-plugin-markdown": "^2.4.0", "typeface-roboto": "^0.0.75", "typescript": "4.0.2", + "typescript-plugin-css-modules": "^3.2.0", "url-loader": "^4.1.0", "webpack": "^4.44.2", "webpack-cli": "^3.3.11", diff --git a/postcss.config.js b/postcss.config.js new file mode 100644 index 0000000000..2ae5d843b6 --- /dev/null +++ b/postcss.config.js @@ -0,0 +1,28 @@ +/** + * Copyright (c) 2021 OpenLens Authors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +const tailwindcss = require("tailwindcss"); + +module.exports = { + plugins: [ + tailwindcss("./tailwind.config.js") + ], +}; diff --git a/src/common/base-store.ts b/src/common/base-store.ts index 608862c2af..893a09b965 100644 --- a/src/common/base-store.ts +++ b/src/common/base-store.ts @@ -23,9 +23,8 @@ import path from "path"; import Config from "conf"; import type { Options as ConfOptions } from "conf/dist/source/types"; import { app, ipcMain, IpcMainEvent, ipcRenderer, IpcRendererEvent, remote } from "electron"; -import { IReactionOptions, observable, reaction, runInAction, when } from "mobx"; -import Singleton from "./utils/singleton"; -import { getAppVersion } from "./utils/app-version"; +import { IReactionOptions, makeObservable, observable, reaction, runInAction, when } from "mobx"; +import { getAppVersion, Singleton, toJS, Disposer } from "./utils"; import logger from "../main/logger"; import { broadcastMessage, subscribeToBroadcast, unsubscribeFromBroadcast } from "./ipc"; import isEqual from "lodash/isEqual"; @@ -41,13 +40,18 @@ export interface BaseStoreParams extends ConfOptions { */ export abstract class BaseStore extends Singleton { protected storeConfig?: Config; - protected syncDisposers: Function[] = []; + protected syncDisposers: Disposer[] = []; - whenLoaded = when(() => this.isLoaded); @observable isLoaded = false; + get whenLoaded() { + return when(() => this.isLoaded); + } + protected constructor(protected params: BaseStoreParams) { super(); + makeObservable(this); + this.params = { autoLoad: false, syncEnabled: true, @@ -114,7 +118,11 @@ export abstract class BaseStore extends Singleton { enableSync() { this.syncDisposers.push( - reaction(() => this.toJSON(), model => this.onModelChange(model), this.params.syncOptions), + reaction( + () => toJS(this.toJSON()), // unwrap possible observables and react to everything + model => this.onModelChange(model), + this.params.syncOptions, + ), ); if (ipcMain) { diff --git a/src/common/catalog/catalog-category-registry.ts b/src/common/catalog/catalog-category-registry.ts index 3b1d5bb00a..ee61429859 100644 --- a/src/common/catalog/catalog-category-registry.ts +++ b/src/common/catalog/catalog-category-registry.ts @@ -19,7 +19,7 @@ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -import { action, computed, observable } from "mobx"; +import { action, computed, observable, makeObservable } from "mobx"; import { Disposer, ExtendedMap } from "../utils"; import { CatalogCategory, CatalogEntityData, CatalogEntityKindData } from "./catalog-entity"; @@ -27,6 +27,10 @@ export class CatalogCategoryRegistry { protected categories = observable.set(); protected groupKinds = new ExtendedMap>(); + constructor() { + makeObservable(this); + } + @action add(category: CatalogCategory): Disposer { this.categories.add(category); this.updateGroupKinds(category); diff --git a/src/common/catalog/catalog-entity.ts b/src/common/catalog/catalog-entity.ts index e632deef37..1f966b192b 100644 --- a/src/common/catalog/catalog-entity.ts +++ b/src/common/catalog/catalog-entity.ts @@ -21,7 +21,7 @@ import EventEmitter from "events"; import type TypedEmitter from "typed-emitter"; -import { observable } from "mobx"; +import { observable, makeObservable } from "mobx"; type ExtractEntityMetadataType = Entity extends CatalogEntity ? Metadata : never; type ExtractEntityStatusType = Entity extends CatalogEntity ? Status : never; @@ -63,6 +63,12 @@ export abstract class CatalogCategory extends (EventEmitter as new () => TypedEm }; abstract spec: CatalogCategorySpec; + static parseId(id = ""): { group?: string, kind?: string } { + const [group, kind] = id.split("/") ?? []; + + return { group, kind }; + } + public getId(): string { return `${this.spec.group}/${this.spec.names.kind}`; } @@ -154,6 +160,7 @@ export abstract class CatalogEntity< @observable spec: Spec; constructor(data: CatalogEntityData) { + makeObservable(this); this.metadata = data.metadata; this.status = data.status; this.spec = data.spec; diff --git a/src/common/cluster-store.ts b/src/common/cluster-store.ts index 89c71255c2..31ecd18031 100644 --- a/src/common/cluster-store.ts +++ b/src/common/cluster-store.ts @@ -22,7 +22,7 @@ import path from "path"; import { app, ipcMain, ipcRenderer, remote, webFrame } from "electron"; import { unlink } from "fs-extra"; -import { action, comparer, computed, observable, reaction, toJS } from "mobx"; +import { action, comparer, computed, observable, reaction, makeObservable } from "mobx"; import { BaseStore } from "./base-store"; import { Cluster, ClusterState } from "../main/cluster"; import migrations from "../migrations/cluster-store"; @@ -33,7 +33,7 @@ import { saveToAppFiles } from "./utils/saveToAppFiles"; import type { KubeConfig } from "@kubernetes/client-node"; import { handleRequest, requestMain, subscribeToBroadcast, unsubscribeAllFromBroadcast } from "./ipc"; import type { ResourceType } from "../renderer/components/cluster-settings/components/cluster-metrics-setting"; -import { disposer, noop } from "./utils"; +import { disposer, noop, toJS } from "./utils"; export interface ClusterIconUpload { clusterId: string; @@ -148,6 +148,8 @@ export class ClusterStore extends BaseStore { migrations, }); + makeObservable(this); + this.pushStateToViewsAutomatically(); } @@ -171,16 +173,16 @@ export class ClusterStore extends BaseStore { }); } else if (ipcMain) { handleRequest(ClusterStore.stateRequestChannel, (): clusterStateSync[] => { - const states: clusterStateSync[] = []; + const clusterStates: clusterStateSync[] = []; this.clustersList.forEach((cluster) => { - states.push({ + clusterStates.push({ state: cluster.getState(), id: cluster.id }); }); - return states; + return clusterStates; }); } } @@ -309,7 +311,7 @@ export class ClusterStore extends BaseStore { @action protected fromStore({ activeCluster, clusters = [] }: ClusterStoreModel = {}) { - const currentClusters = this.clusters.toJS(); + const currentClusters = new Map(this.clusters); const newClusters = new Map(); const removedClusters = new Map(); @@ -345,8 +347,6 @@ export class ClusterStore extends BaseStore { return toJS({ activeCluster: this.activeCluster, clusters: this.clustersList.map(cluster => cluster.toJSON()), - }, { - recurseEverything: true }); } } diff --git a/src/common/configure-packages.ts b/src/common/configure-packages.ts new file mode 100644 index 0000000000..07e47d883c --- /dev/null +++ b/src/common/configure-packages.ts @@ -0,0 +1,44 @@ +/** + * Copyright (c) 2021 OpenLens Authors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +import * as Mobx from "mobx"; +import * as Immer from "immer"; + +/** + * Setup default configuration for external npm-packages + */ +export default function configurePackages() { + // Docs: https://mobx.js.org/configuration.html + Mobx.configure({ + enforceActions: "never", + isolateGlobalState: true, + + // TODO: enable later (read more: https://mobx.js.org/migrating-from-4-or-5.html) + // computedRequiresReaction: true, + // reactionRequiresObservable: true, + // observableRequiresReaction: true, + }); + + // Docs: https://immerjs.github.io/immer/ + // Required in `utils/storage-helper.ts` + Immer.setAutoFreeze(false); // allow to merge mobx observables + Immer.enableMapSet(); // allow to merge maps and sets +} diff --git a/src/common/hotbar-store.ts b/src/common/hotbar-store.ts index 53c1c5b3ef..2d91ec0cc5 100644 --- a/src/common/hotbar-store.ts +++ b/src/common/hotbar-store.ts @@ -19,11 +19,12 @@ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -import { action, comparer, observable, toJS } from "mobx"; +import { action, comparer, observable, makeObservable } from "mobx"; import { BaseStore } from "./base-store"; import migrations from "../migrations/hotbar-store"; import * as uuid from "uuid"; import isNull from "lodash/isNull"; +import { toJS } from "./utils"; import { CatalogEntity } from "./catalog"; export interface HotbarItem { @@ -69,6 +70,7 @@ export class HotbarStore extends BaseStore { }, migrations, }); + makeObservable(this); } get activeHotbarId() { @@ -252,8 +254,6 @@ export class HotbarStore extends BaseStore { activeHotbarId: this.activeHotbarId }; - return toJS(model, { - recurseEverything: true, - }); + return toJS(model); } } diff --git a/src/common/ipc/ipc.ts b/src/common/ipc/ipc.ts index 66e591e765..f541e0d276 100644 --- a/src/common/ipc/ipc.ts +++ b/src/common/ipc/ipc.ts @@ -23,29 +23,34 @@ // https://www.electronjs.org/docs/api/ipc-main // https://www.electronjs.org/docs/api/ipc-renderer -import { ipcMain, ipcRenderer, webContents, remote } from "electron"; -import { toJS } from "mobx"; +import { ipcMain, ipcRenderer, remote, webContents } from "electron"; +import { toJS } from "../utils/toJS"; import logger from "../../main/logger"; -import { ClusterFrameInfo, clusterFrameMap } from "../cluster-frames"; +import { ClusterFrameInfo, clusterFrameMap } from "../cluster-frames"; const subFramesChannel = "ipc:get-sub-frames"; export function handleRequest(channel: string, listener: (event: Electron.IpcMainInvokeEvent, ...args: any[]) => any) { - ipcMain.handle(channel, listener); + ipcMain.handle(channel, async (event, ...args) => { + const payload = await listener(event, ...args); + + return sanitizePayload(payload); + }); } export async function requestMain(channel: string, ...args: any[]) { - return ipcRenderer.invoke(channel, ...args); + return ipcRenderer.invoke(channel, ...args.map(sanitizePayload)); } function getSubFrames(): ClusterFrameInfo[] { - return toJS(Array.from(clusterFrameMap.values()), { recurseEverything: true }); + return Array.from(clusterFrameMap.values()); } export function broadcastMessage(channel: string, ...args: any[]) { const views = (webContents || remote?.webContents)?.getAllWebContents(); if (!views) return; + args = args.map(sanitizePayload); ipcRenderer?.send(channel, ...args); ipcMain?.emit(channel, ...args); @@ -98,7 +103,13 @@ export function unsubscribeAllFromBroadcast(channel: string) { } export function bindBroadcastHandlers() { - handleRequest(subFramesChannel, () => { - return getSubFrames(); - }); + handleRequest(subFramesChannel, () => getSubFrames()); +} + +/** + * Sanitizing data for IPC-messaging before send. + * Removes possible observable values to avoid exceptions like "can't clone object". + */ +function sanitizePayload(data: any): T { + return toJS(data); } diff --git a/src/common/search-store.ts b/src/common/search-store.ts index ae4ba5fa5c..ac77e0ec94 100644 --- a/src/common/search-store.ts +++ b/src/common/search-store.ts @@ -19,9 +19,9 @@ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -import { action, computed, observable,reaction } from "mobx"; +import { action, computed, observable, reaction, makeObservable } from "mobx"; import { dockStore } from "../renderer/components/dock/dock.store"; -import { autobind } from "../renderer/utils"; +import { boundMethod } from "../renderer/utils"; export class SearchStore { /** @@ -54,6 +54,7 @@ export class SearchStore { @observable activeOverlayIndex = -1; constructor() { + makeObservable(this); reaction(() => dockStore.selectedTabId, () => { searchStore.reset(); }); @@ -128,12 +129,12 @@ export class SearchStore { return prev; } - @autobind() + @boundMethod public setNextOverlayActive(): void { this.activeOverlayIndex = this.getNextOverlay(true); } - @autobind() + @boundMethod public setPrevOverlayActive(): void { this.activeOverlayIndex = this.getPrevOverlay(true); } @@ -159,7 +160,7 @@ export class SearchStore { * @param line Index of the line where overlay is located * @param occurrence Number of the overlay within one line */ - @autobind() + @boundMethod public isActiveOverlay(line: number, occurrence: number): boolean { const firstLineIndex = this.occurrences.findIndex(item => item === line); diff --git a/src/common/user-store.ts b/src/common/user-store.ts index 19ba33615c..4722dbb367 100644 --- a/src/common/user-store.ts +++ b/src/common/user-store.ts @@ -23,7 +23,7 @@ import type { ThemeId } from "../renderer/theme.store"; import { app, remote } from "electron"; import semver from "semver"; import { readFile } from "fs-extra"; -import { action, computed, observable, reaction, toJS } from "mobx"; +import { action, computed, observable, reaction, makeObservable } from "mobx"; import moment from "moment-timezone"; import { BaseStore } from "./base-store"; import migrations from "../migrations/user-store"; @@ -34,7 +34,7 @@ import logger from "../main/logger"; import path from "path"; import os from "os"; import { fileNameMigration } from "../migrations/user-store"; -import { ObservableToggleSet } from "../renderer/utils"; +import { ObservableToggleSet, toJS } from "../renderer/utils"; export interface UserStoreModel { kubeConfigPath: string; @@ -73,6 +73,7 @@ export class UserStore extends BaseStore { configName: "lens-user-store", migrations, }); + makeObservable(this); } @observable lastSeenAppVersion = "0.0.0"; @@ -306,9 +307,7 @@ export class UserStore extends BaseStore { }, }; - return toJS(model, { - recurseEverything: true, - }); + return toJS(model); } } diff --git a/src/common/utils/__tests__/toJS.test.ts b/src/common/utils/__tests__/toJS.test.ts new file mode 100644 index 0000000000..95b35db051 --- /dev/null +++ b/src/common/utils/__tests__/toJS.test.ts @@ -0,0 +1,45 @@ +/** + * Copyright (c) 2021 OpenLens Authors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +import { isObservable, observable } from "mobx"; +import { toJS } from "../toJS"; + +describe("utils/toJS(data: any)", () => { + const y = { y: 2 }; + + const data = observable({ x: 1, y }, {}, { + deep: false, // this will keep ref to "y" + }); + const data2 = { + x: 1, // partially observable + y: observable(y), + }; + + test("converts mobx-observable to corresponding js struct with links preserving", () => { + expect(toJS(data).y).toBe(y); + expect(isObservable(toJS(data).y)).toBeFalsy(); + }); + + test("converts partially observable js struct", () => { + expect(toJS(data2).y).not.toBe(y); + expect(toJS(data2).y).toEqual(y); + expect(isObservable(toJS(data2).y)).toBeFalsy(); + }); +}); diff --git a/src/common/utils/autobind.ts b/src/common/utils/autobind.ts index 02eb36a74e..cdb6903259 100644 --- a/src/common/utils/autobind.ts +++ b/src/common/utils/autobind.ts @@ -19,48 +19,22 @@ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -// Decorator for binding class methods -// Can be applied to class or single method as @autobind() -type Constructor = new (...args: any[]) => T; +import {boundMethod, boundClass} from "autobind-decorator"; +import autoBindClass, { Options } from "auto-bind"; +import autoBindReactClass from "auto-bind/react"; -export function autobind() { - return function (target: Constructor | object, prop?: string, descriptor?: PropertyDescriptor) { - if (target instanceof Function) return bindClass(target); - else return bindMethod(target, prop, descriptor); - }; -} - -function bindClass(constructor: T) { - const proto = constructor.prototype; - const descriptors = Object.getOwnPropertyDescriptors(proto); - const skipMethod = (methodName: string) => { - return methodName === "constructor" - || typeof descriptors[methodName].value !== "function"; - }; - - Object.keys(descriptors).forEach(prop => { - if (skipMethod(prop)) return; - const boundDescriptor = bindMethod(proto, prop, descriptors[prop]); - - Object.defineProperty(proto, prop, boundDescriptor); - }); -} - -function bindMethod(target: object, prop?: string, descriptor?: PropertyDescriptor) { - if (!descriptor || typeof descriptor.value !== "function") { - throw new Error(`@autobind() must be used on class or method only`); +// Automatically bind methods to their class instance +export function autoBind(obj: T, opts?: Options): T { + if ("componentWillUnmount" in obj) { + return autoBindReactClass(obj as any, opts); } - const { value: func, enumerable, configurable } = descriptor; - const boundFunc = new WeakMap(); - return Object.defineProperty(target, prop, { - enumerable, - configurable, - get() { - if (this === target) return func; // direct access from prototype - if (!boundFunc.has(this)) boundFunc.set(this, func.bind(this)); - - return boundFunc.get(this); - } - }); + return autoBindClass(obj, opts); } + +// Class/method decorators +// Note: @boundClass doesn't work with mobx-6.x/@action decorator +export { + boundClass, + boundMethod, +}; diff --git a/src/common/utils/extended-map.ts b/src/common/utils/extended-map.ts index c8b7ff4c65..c759fa3459 100644 --- a/src/common/utils/extended-map.ts +++ b/src/common/utils/extended-map.ts @@ -19,7 +19,7 @@ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -import { action, IEnhancer, IObservableMapInitialValues, ObservableMap } from "mobx"; +import { action, ObservableMap } from "mobx"; export class ExtendedMap extends Map { static new(entries?: readonly (readonly [K, V])[] | null): ExtendedMap { @@ -67,34 +67,12 @@ export class ExtendedMap extends Map { } export class ExtendedObservableMap extends ObservableMap { - constructor(protected getDefault: () => V, initialData?: IObservableMapInitialValues, enhancer?: IEnhancer, name?: string) { - super(initialData, enhancer, name); - } - @action - getOrInsert(key: K, val: V): V { - if (this.has(key)) { - return this.get(key); - } - - return this.set(key, val).get(key); - } - - @action - getOrInsertWith(key: K, getVal: () => V): V { + getOrInsert(key: K, getVal: () => V): V { if (this.has(key)) { return this.get(key); } return this.set(key, getVal()).get(key); } - - @action - getOrDefault(key: K): V { - if (this.has(key)) { - return this.get(key); - } - - return this.set(key, this.getDefault()).get(key); - } } diff --git a/src/common/utils/index.ts b/src/common/utils/index.ts index 5b13159549..b1980d3f79 100644 --- a/src/common/utils/index.ts +++ b/src/common/utils/index.ts @@ -27,6 +27,7 @@ export * from "./app-version"; export * from "./autobind"; export * from "./base64"; export * from "./camelCase"; +export * from "./toJS"; export * from "./cloneJson"; export * from "./debouncePromise"; export * from "./defineGlobal"; diff --git a/src/common/utils/toJS.ts b/src/common/utils/toJS.ts new file mode 100644 index 0000000000..020d3dc3bb --- /dev/null +++ b/src/common/utils/toJS.ts @@ -0,0 +1,39 @@ +/** + * Copyright (c) 2021 OpenLens Authors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +/** + * Wrapper for mobx.toJS() to support partially observable objects as data-input (>= mobx6). + * Otherwise, output result won't be recursively converted to corresponding plain JS-structure. + * + * @example + * mobx.toJS({one: 1, two: observable.array([2])}); // "data.two" == ObservableArray + */ +import * as mobx from "mobx"; +import { isObservable, observable } from "mobx"; + +export function toJS(data: T): T { + // make data observable for recursive toJS()-output + if (typeof data === "object" && !isObservable(data)) { + return mobx.toJS(observable.box(data).get()); + } + + return mobx.toJS(data); +} diff --git a/src/common/utils/toggle-set.ts b/src/common/utils/toggle-set.ts index aec53bc748..ace6594207 100644 --- a/src/common/utils/toggle-set.ts +++ b/src/common/utils/toggle-set.ts @@ -19,7 +19,7 @@ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -import { action, ObservableSet } from "mobx"; +import { ObservableSet } from "mobx"; export class ToggleSet extends Set { public toggle(value: T): void { @@ -31,7 +31,6 @@ export class ToggleSet extends Set { } export class ObservableToggleSet extends ObservableSet { - @action public toggle(value: T): void { if (!this.delete(value)) { // Set.prototype.delete returns false if `value` was not in the set diff --git a/src/extensions/extension-api.ts b/src/extensions/extension-api.ts index 4fc22ae730..4e584419e1 100644 --- a/src/extensions/extension-api.ts +++ b/src/extensions/extension-api.ts @@ -19,7 +19,8 @@ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -// Extension-api types generation bundle +// Extensions-api types bundle (main + renderer) +// Available for lens-extensions via NPM-package "@k8slens/extensions" export * from "./core-api"; export * from "./renderer-api"; diff --git a/src/extensions/extension-discovery.ts b/src/extensions/extension-discovery.ts index bc1d5e5331..8a0929d016 100644 --- a/src/extensions/extension-discovery.ts +++ b/src/extensions/extension-discovery.ts @@ -23,11 +23,11 @@ import { watch } from "chokidar"; import { ipcRenderer } from "electron"; import { EventEmitter } from "events"; import fse from "fs-extra"; -import { observable, reaction, toJS, when } from "mobx"; +import { observable, reaction, when, makeObservable } from "mobx"; import os from "os"; import path from "path"; import { broadcastMessage, handleRequest, requestMain, subscribeToBroadcast } from "../common/ipc"; -import { Singleton } from "../common/utils"; +import { Singleton, toJS } from "../common/utils"; import logger from "../main/logger"; import { ExtensionInstallationStateStore } from "../renderer/components/+extensions/extension-install.store"; import { extensionInstaller, PackageJson } from "./extension-installer"; @@ -86,13 +86,22 @@ export class ExtensionDiscovery extends Singleton { // True if extensions have been loaded from the disk after app startup @observable isLoaded = false; - whenLoaded = when(() => this.isLoaded); + + get whenLoaded() { + return when(() => this.isLoaded); + } // IPC channel to broadcast changes to extension-discovery from main protected static readonly extensionDiscoveryChannel = "extension-discovery:main"; public events = new EventEmitter(); + constructor() { + super(); + + makeObservable(this); + } + get localFolderPath(): string { return path.join(os.homedir(), ".k8slens", "extensions"); } @@ -374,7 +383,7 @@ export class ExtensionDiscovery extends Singleton { const userExtensions = await this.loadFromFolder(this.localFolderPath, bundledExtensions.map((extension) => extension.manifest.name)); for (const extension of userExtensions) { - if (await fse.pathExists(extension.manifestPath) === false) { + if ((await fse.pathExists(extension.manifestPath)) === false) { await this.installPackage(extension.absolutePath); } } @@ -468,8 +477,6 @@ export class ExtensionDiscovery extends Singleton { toJSON(): ExtensionDiscoveryChannelMessage { return toJS({ isLoaded: this.isLoaded - }, { - recurseEverything: true }); } diff --git a/src/extensions/extension-loader.ts b/src/extensions/extension-loader.ts index b024ddd761..b3bec33179 100644 --- a/src/extensions/extension-loader.ts +++ b/src/extensions/extension-loader.ts @@ -22,11 +22,11 @@ import { app, ipcRenderer, remote } from "electron"; import { EventEmitter } from "events"; import { isEqual } from "lodash"; -import { action, computed, observable, reaction, toJS, when } from "mobx"; +import { action, computed, makeObservable, observable, reaction, when } from "mobx"; import path from "path"; import { getHostedCluster } from "../common/cluster-store"; import { broadcastMessage, handleRequest, requestMain, subscribeToBroadcast } from "../common/ipc"; -import { Singleton } from "../common/utils"; +import { Singleton, toJS } from "../common/utils"; import logger from "../main/logger"; import type { InstalledExtension } from "./extension-discovery"; import { ExtensionsStore } from "./extensions-store"; @@ -58,10 +58,19 @@ export class ExtensionLoader extends Singleton { private events = new EventEmitter(); @observable isLoaded = false; - whenLoaded = when(() => this.isLoaded); + + get whenLoaded() { + return when(() => this.isLoaded); + } + + constructor() { + super(); + + makeObservable(this); + } @computed get userExtensions(): Map { - const extensions = this.extensions.toJS(); + const extensions = this.toJSON(); extensions.forEach((ext, extId) => { if (ext.isBundled) { @@ -75,7 +84,7 @@ export class ExtensionLoader extends Singleton { @computed get userExtensionsByName(): Map { const extensions = new Map(); - for (const [, val] of this.instances.toJS()) { + for (const [, val] of this.instances.toJSON()) { if (val.isBundled) { continue; } @@ -117,6 +126,11 @@ export class ExtensionLoader extends Singleton { await Promise.all([this.whenLoaded, ExtensionsStore.getInstance().whenLoaded]); + // broadcasting extensions between main/renderer processes + reaction(() => this.toJSON(), () => this.broadcastExtensions(), { + fireImmediately: true, + }); + // save state on change `extension.isEnabled` reaction(() => this.storeState, extensionsState => { ExtensionsStore.getInstance().mergeState(extensionsState); @@ -156,14 +170,10 @@ export class ExtensionLoader extends Singleton { } } - protected async initMain() { + protected async initMain() { this.isLoaded = true; this.loadOnMain(); - reaction(() => this.toJSON(), () => { - this.broadcastExtensions(); - }); - handleRequest(ExtensionLoader.extensionsMainChannel, () => { return Array.from(this.toJSON()); }); @@ -173,7 +183,7 @@ export class ExtensionLoader extends Singleton { }); } - protected async initRenderer() { + protected async initRenderer() { const extensionListHandler = (extensions: [LensExtensionId, InstalledExtension][]) => { this.isLoaded = true; this.syncExtensions(extensions); @@ -188,16 +198,20 @@ export class ExtensionLoader extends Singleton { }); }; - reaction(() => this.toJSON(), () => { - this.broadcastExtensions(false); - }); - requestMain(ExtensionLoader.extensionsMainChannel).then(extensionListHandler); subscribeToBroadcast(ExtensionLoader.extensionsMainChannel, (_event, extensions: [LensExtensionId, InstalledExtension][]) => { extensionListHandler(extensions); }); } + broadcastExtensions() { + const channel = ipcRenderer + ? ExtensionLoader.extensionsRendererChannel + : ExtensionLoader.extensionsMainChannel; + + broadcastMessage(channel, Array.from(this.extensions)); + } + syncExtensions(extensions: [LensExtensionId, InstalledExtension][]) { extensions.forEach(([lensExtensionId, extension]) => { if (!isEqual(this.extensions.get(lensExtensionId), extension)) { @@ -255,7 +269,7 @@ export class ExtensionLoader extends Singleton { const cluster = getHostedCluster(); this.autoInitExtensions(async (extension: LensRendererExtension) => { - if (await extension.isEnabledForCluster(cluster) === false) { + if ((await extension.isEnabledForCluster(cluster)) === false) { return []; } @@ -334,13 +348,6 @@ export class ExtensionLoader extends Singleton { } toJSON(): Map { - return toJS(this.extensions, { - exportMapsAsObjects: false, - recurseEverything: true, - }); - } - - broadcastExtensions(main = true) { - broadcastMessage(main ? ExtensionLoader.extensionsMainChannel : ExtensionLoader.extensionsRendererChannel, Array.from(this.toJSON())); + return toJS(this.extensions); } } diff --git a/src/extensions/extensions-store.ts b/src/extensions/extensions-store.ts index d365b63f35..86a90e0238 100644 --- a/src/extensions/extensions-store.ts +++ b/src/extensions/extensions-store.ts @@ -21,7 +21,8 @@ import type { LensExtensionId } from "./lens-extension"; import { BaseStore } from "../common/base-store"; -import { action, computed, observable, toJS } from "mobx"; +import { action, computed, observable, makeObservable } from "mobx"; +import { toJS } from "../common/utils"; export interface LensExtensionsStoreModel { extensions: Record; @@ -37,6 +38,7 @@ export class ExtensionsStore extends BaseStore { super({ configName: "lens-extensions", }); + makeObservable(this); } @computed @@ -68,9 +70,7 @@ export class ExtensionsStore extends BaseStore { toJSON(): LensExtensionsStoreModel { return toJS({ - extensions: this.state.toJSON(), - }, { - recurseEverything: true + extensions: Object.fromEntries(this.state), }); } } diff --git a/src/extensions/lens-extension.ts b/src/extensions/lens-extension.ts index b6faf2e491..6f03829121 100644 --- a/src/extensions/lens-extension.ts +++ b/src/extensions/lens-extension.ts @@ -20,7 +20,7 @@ */ import type { InstalledExtension } from "./extension-discovery"; -import { action, observable, reaction } from "mobx"; +import { action, observable, reaction, makeObservable } from "mobx"; import { FilesystemProvisionerStore } from "../main/extension-filesystem"; import logger from "../main/logger"; import type { ProtocolHandlerRegistration } from "./registries"; @@ -52,6 +52,7 @@ export class LensExtension { [Disposers] = disposer(); constructor({ id, manifest, manifestPath, isBundled }: InstalledExtension) { + makeObservable(this); this.id = id; this.manifest = manifest; this.manifestPath = manifestPath; diff --git a/src/extensions/registries/__tests__/page-registry.test.ts b/src/extensions/registries/__tests__/page-registry.test.ts index fc40b1baca..8c6582b437 100644 --- a/src/extensions/registries/__tests__/page-registry.test.ts +++ b/src/extensions/registries/__tests__/page-registry.test.ts @@ -75,15 +75,22 @@ describe("getPageUrl", () => { }); it("gets page url with custom params", () => { - const params: PageParams = { test1: "one", test2: "2" }; + const params: PageParams = { test1: "one", test2: "2" }; const searchParams = new URLSearchParams(params); - const pageUrl = getExtensionPageUrl({ extensionId: ext.name, pageId: "page-with-params", params }); + const pageUrl = getExtensionPageUrl({ + extensionId: ext.name, + pageId: "page-with-params", + params, + }); expect(pageUrl).toBe(`/extension/foo-bar/page-with-params?${searchParams}`); }); it("gets page url with default custom params", () => { - const defaultPageUrl = getExtensionPageUrl({ extensionId: ext.name, pageId: "page-with-params", }); + const defaultPageUrl = getExtensionPageUrl({ + extensionId: ext.name, + pageId: "page-with-params", + }); expect(defaultPageUrl).toBe(`/extension/foo-bar/page-with-params?test1=test1-default`); }); diff --git a/src/extensions/registries/base-registry.ts b/src/extensions/registries/base-registry.ts index 7b5e988597..0ebe81bd5f 100644 --- a/src/extensions/registries/base-registry.ts +++ b/src/extensions/registries/base-registry.ts @@ -20,12 +20,16 @@ */ // Base class for extensions-api registries -import { action, observable } from "mobx"; +import { action, observable, makeObservable } from "mobx"; import { LensExtension } from "../lens-extension"; export class BaseRegistry { private items = observable.map(); + constructor() { + makeObservable(this); + } + getItems(): I[] { return Array.from(this.items.values()); } diff --git a/src/extensions/registries/command-registry.ts b/src/extensions/registries/command-registry.ts index ba88f623dd..b7b822d473 100644 --- a/src/extensions/registries/command-registry.ts +++ b/src/extensions/registries/command-registry.ts @@ -22,9 +22,9 @@ // Extensions API -> Commands import { BaseRegistry } from "./base-registry"; -import { action, observable } from "mobx"; -import { LensExtension } from "../lens-extension"; -import { CatalogEntity } from "../../common/catalog"; +import { makeObservable, observable } from "mobx"; +import type { LensExtension } from "../lens-extension"; +import type { CatalogEntity } from "../../common/catalog"; export type CommandContext = { entity?: CatalogEntity; @@ -39,9 +39,14 @@ export interface CommandRegistration { } export class CommandRegistry extends BaseRegistry { - @observable activeEntity: CatalogEntity; + @observable.ref activeEntity: CatalogEntity; + + constructor() { + super(); + + makeObservable(this); + } - @action add(items: CommandRegistration | CommandRegistration[], extension?: LensExtension) { const itemArray = [items].flat(); diff --git a/src/extensions/registries/page-menu-registry.ts b/src/extensions/registries/page-menu-registry.ts index 08df7b1c79..49003717f6 100644 --- a/src/extensions/registries/page-menu-registry.ts +++ b/src/extensions/registries/page-menu-registry.ts @@ -23,9 +23,8 @@ import type { IconProps } from "../../renderer/components/icon"; import type React from "react"; import type { PageTarget, RegisteredPage } from "./page-registry"; -import { action } from "mobx"; +import type { LensExtension } from "../lens-extension"; import { BaseRegistry } from "./base-registry"; -import { LensExtension } from "../lens-extension"; export interface PageMenuRegistration { target?: PageTarget; @@ -43,7 +42,6 @@ export interface PageMenuComponents { } export class PageMenuRegistry extends BaseRegistry { - @action add(items: T[], ext: LensExtension) { const normalizedItems = items.map(menuItem => { menuItem.target = { diff --git a/src/extensions/registries/page-registry.ts b/src/extensions/registries/page-registry.ts index cff1c2d474..3bd4c33878 100644 --- a/src/extensions/registries/page-registry.ts +++ b/src/extensions/registries/page-registry.ts @@ -24,9 +24,8 @@ import React from "react"; import { observer } from "mobx-react"; import { BaseRegistry } from "./base-registry"; -import { LensExtension, sanitizeExtensionName } from "../lens-extension"; -import type { PageParam, PageParamInit } from "../../renderer/navigation/page-param"; -import { createPageParam } from "../../renderer/navigation/helpers"; +import { LensExtension, LensExtensionId, sanitizeExtensionName } from "../lens-extension"; +import { createPageParam, PageParam, PageParamInit, searchParamsOptions } from "../../renderer/navigation"; export interface PageRegistration { /** @@ -34,21 +33,18 @@ export interface PageRegistration { * When not provided, first registered page without "id" would be used for page-menus without target.pageId for same extension */ id?: string; - params?: PageParams; + params?: PageParams, "name" | "prefix">>; components: PageComponents; } -// exclude "name" field since provided as key in page.params -export type ExtensionPageParamInit = Omit; - export interface PageComponents { Page: React.ComponentType; } -export interface PageTarget

{ +export interface PageTarget { extensionId?: string; pageId?: string; - params?: P; + params?: PageParams; } export interface PageParams { @@ -83,13 +79,12 @@ export function getExtensionPageUrl(target: PageTarget): string { if (registeredPage?.params) { Object.entries(registeredPage.params).forEach(([name, param]) => { - const paramValue = param.stringify(targetParams[name]); + pageUrl.searchParams.delete(name); // first off, clear existing value(s) - if (param.init.skipEmpty && param.isEmpty(paramValue)) { - pageUrl.searchParams.delete(name); - } else { - pageUrl.searchParams.set(name, paramValue); - } + param.stringify(targetParams[name]).forEach(value => { + if (searchParamsOptions.skipEmpty && !value) return; + pageUrl.searchParams.append(name, value); + }); }); } @@ -100,7 +95,7 @@ export class PageRegistry extends BaseRegistry protected getRegisteredItem(page: PageRegistration, ext: LensExtension): RegisteredPage { const { id: pageId } = page; const extensionId = ext.name; - const params = this.normalizeParams(page.params); + const params = this.normalizeParams(extensionId, page.params); const components = this.normalizeComponents(page.components, params); const url = getExtensionPageUrl({ extensionId, pageId }); @@ -113,25 +108,48 @@ export class PageRegistry extends BaseRegistry if (params) { const { Page } = components; + // inject extension's page component props.params components.Page = observer((props: object) => React.createElement(Page, { params, ...props })); } return components; } - protected normalizeParams(params?: PageParams): PageParams | undefined { - if (!params) { - return undefined; - } - Object.entries(params).forEach(([name, value]) => { - const paramInit: PageParamInit = typeof value === "object" - ? { name, ...value } - : { name, defaultValue: value }; + protected normalizeParams(extensionId: LensExtensionId, params?: PageParams>): PageParams { + if (!params) return undefined; + const normalizedParams: PageParams = {}; - params[paramInit.name] = createPageParam(paramInit); + Object.entries(params).forEach(([paramName, paramValue]) => { + const paramInit: PageParamInit = { + name: paramName, + prefix: `${extensionId}:`, + defaultValue: paramValue, + }; + + // handle non-string params + if (typeof paramValue !== "string") { + const { defaultValue: value, parse, stringify } = paramValue; + + const notAStringValue = typeof value !== "string" || ( + Array.isArray(value) && !value.every(value => typeof value === "string") + ); + + if (notAStringValue && !(parse || stringify)) { + throw new Error( + `PageRegistry: param's "${paramName}" initialization has failed: + paramInit.parse() and paramInit.stringify() are required for non string | string[] "defaultValue"` + ); + } + + paramInit.defaultValue = value; + paramInit.parse = parse; + paramInit.stringify = stringify; + } + + normalizedParams[paramName] = createPageParam(paramInit); }); - return params as PageParams; + return normalizedParams; } getByPageTarget(target: PageTarget): RegisteredPage | null { diff --git a/src/extensions/renderer-api/navigation.ts b/src/extensions/renderer-api/navigation.ts index f8a338ac3f..48896920b5 100644 --- a/src/extensions/renderer-api/navigation.ts +++ b/src/extensions/renderer-api/navigation.ts @@ -19,15 +19,13 @@ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -import { PageParam, PageParamInit } from "../../renderer/navigation/page-param"; -import { navigation } from "../../renderer/navigation"; +import { navigation, PageParam, PageParamInit } from "../../renderer/navigation"; export type { PageParamInit, PageParam } from "../../renderer/navigation/page-param"; export { navigate, isActiveRoute } from "../../renderer/navigation/helpers"; export { hideDetails, showDetails, getDetailsUrl } from "../../renderer/components/kube-object/kube-object-details"; export type { IURLParams } from "../../common/utils/buildUrl"; -// exporting to extensions-api version of helper without `isSystem` flag -export function createPageParam(init: PageParamInit) { +export function createPageParam(init: PageParamInit) { return new PageParam(init, navigation); } diff --git a/src/jest.setup.ts b/src/jest.setup.ts index 4feade0f1f..29b0465c8b 100644 --- a/src/jest.setup.ts +++ b/src/jest.setup.ts @@ -20,6 +20,11 @@ */ import fetchMock from "jest-fetch-mock"; +import configurePackages from "./common/configure-packages"; + +// setup default configuration for external npm-packages +configurePackages(); + // rewire global.fetch to call 'fetchMock' fetchMock.enableMocks(); diff --git a/src/main/catalog-pusher.ts b/src/main/catalog-pusher.ts index c2d7267b92..e97dc07038 100644 --- a/src/main/catalog-pusher.ts +++ b/src/main/catalog-pusher.ts @@ -19,13 +19,14 @@ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -import { reaction, toJS } from "mobx"; +import { reaction } from "mobx"; import { broadcastMessage } from "../common/ipc"; -import type { CatalogEntityRegistry} from "./catalog"; +import type { CatalogEntityRegistry } from "./catalog"; import "../common/catalog-entities/kubernetes-cluster"; +import { toJS } from "../common/utils"; export function pushCatalogToRenderer(catalog: CatalogEntityRegistry) { - return reaction(() => toJS(catalog.items, { recurseEverything: true }), (items) => { + return reaction(() => toJS(catalog.items), (items) => { broadcastMessage("catalog:items", items); }, { fireImmediately: true, diff --git a/src/main/catalog-sources/__test__/kubeconfig-sync.test.ts b/src/main/catalog-sources/__test__/kubeconfig-sync.test.ts index 6f89e12c46..7fcd3c1db6 100644 --- a/src/main/catalog-sources/__test__/kubeconfig-sync.test.ts +++ b/src/main/catalog-sources/__test__/kubeconfig-sync.test.ts @@ -28,6 +28,12 @@ import mockFs from "mock-fs"; import fs from "fs"; import { ClusterStore } from "../../../common/cluster-store"; +jest.mock("electron", () => ({ + app: { + getPath: () => "/foo", + }, +})); + describe("kubeconfig-sync.source tests", () => { beforeEach(() => { mockFs(); diff --git a/src/main/catalog-sources/kubeconfig-sync.ts b/src/main/catalog-sources/kubeconfig-sync.ts index c72b6081f2..c4650644da 100644 --- a/src/main/catalog-sources/kubeconfig-sync.ts +++ b/src/main/catalog-sources/kubeconfig-sync.ts @@ -19,7 +19,7 @@ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -import { action, observable, IComputedValue, computed, ObservableMap, runInAction } from "mobx"; +import { action, observable, IComputedValue, computed, ObservableMap, runInAction, makeObservable, observe } from "mobx"; import type { CatalogEntity } from "../../common/catalog"; import { catalogEntityRegistry } from "../../main/catalog"; import { watch } from "chokidar"; @@ -35,6 +35,7 @@ import { catalogEntityFromCluster } from "../cluster-manager"; import { UserStore } from "../../common/user-store"; import { ClusterStore, UpdateClusterModel } from "../../common/cluster-store"; import { createHash } from "crypto"; +import { homedir } from "os"; const logPrefix = "[KUBECONFIG-SYNC]:"; @@ -45,6 +46,12 @@ export class KubeconfigSyncManager extends Singleton { protected static readonly syncName = "lens:kube-sync"; + constructor() { + super(); + + makeObservable(this); + } + @action startSync(): void { if (this.syncing) { @@ -69,7 +76,7 @@ export class KubeconfigSyncManager extends Singleton { this.startNewSync(filePath); } - this.syncListDisposer = UserStore.getInstance().syncKubeconfigEntries.observe(change => { + this.syncListDisposer = observe(UserStore.getInstance().syncKubeconfigEntries, change => { switch (change.type) { case "add": this.startNewSync(change.name); @@ -187,7 +194,9 @@ export function computeDiff(contents: string, source: RootSource, filePath: stri const entity = catalogEntityFromCluster(cluster); - entity.metadata.labels.file = filePath; + if (!filePath.startsWith(ClusterStore.storedKubeConfigFolder)) { + entity.metadata.labels.file = filePath.replace(homedir(), "~"); + } source.set(contextName, [cluster, entity]); logger.debug(`${logPrefix} Added new cluster from sync`, { filePath, contextName }); @@ -249,17 +258,17 @@ async function watchFileChanges(filePath: string): Promise<[IComputedValue>(observable.map); + const rootSource = new ExtendedObservableMap>(); const derivedSource = computed(() => Array.from(iter.flatMap(rootSource.values(), from => iter.map(from.values(), child => child[1])))); const stoppers = new Map(); watcher .on("change", (childFilePath) => { stoppers.get(childFilePath)(); - stoppers.set(childFilePath, diffChangedConfig(childFilePath, rootSource.getOrDefault(childFilePath))); + stoppers.set(childFilePath, diffChangedConfig(childFilePath, rootSource.getOrInsert(childFilePath, observable.map))); }) .on("add", (childFilePath) => { - stoppers.set(childFilePath, diffChangedConfig(childFilePath, rootSource.getOrDefault(childFilePath))); + stoppers.set(childFilePath, diffChangedConfig(childFilePath, rootSource.getOrInsert(childFilePath, observable.map))); }) .on("unlink", (childFilePath) => { stoppers.get(childFilePath)(); diff --git a/src/main/catalog/catalog-entity-registry.ts b/src/main/catalog/catalog-entity-registry.ts index 48b8316774..55162970a2 100644 --- a/src/main/catalog/catalog-entity-registry.ts +++ b/src/main/catalog/catalog-entity-registry.ts @@ -19,14 +19,16 @@ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -import { action, computed, observable, IComputedValue, IObservableArray } from "mobx"; +import { action, computed, IComputedValue, IObservableArray, makeObservable, observable } from "mobx"; import { CatalogCategoryRegistry, catalogCategoryRegistry, CatalogEntity } from "../../common/catalog"; import { iter } from "../../common/utils"; export class CatalogEntityRegistry { - protected sources = observable.map>([], { deep: true }); + protected sources = observable.map>(); - constructor(private categoryRegistry: CatalogCategoryRegistry) {} + constructor(private categoryRegistry: CatalogCategoryRegistry) { + makeObservable(this); + } @action addObservableSource(id: string, source: IObservableArray) { this.sources.set(id, computed(() => source)); diff --git a/src/main/cluster-manager.ts b/src/main/cluster-manager.ts index 9ef28c8f9f..b0eb2e7471 100644 --- a/src/main/cluster-manager.ts +++ b/src/main/cluster-manager.ts @@ -22,7 +22,7 @@ import "../common/cluster-ipc"; import type http from "http"; import { ipcMain } from "electron"; -import { action, autorun, reaction, toJS } from "mobx"; +import { action, autorun, makeObservable, reaction } from "mobx"; import { ClusterStore, getClusterIdFromHost } from "../common/cluster-store"; import type { Cluster } from "./cluster"; import logger from "./logger"; @@ -32,38 +32,47 @@ import { catalogEntityRegistry } from "./catalog"; import { KubernetesCluster, KubernetesClusterPrometheusMetrics } from "../common/catalog-entities/kubernetes-cluster"; export class ClusterManager extends Singleton { + private store = ClusterStore.getInstance(); + constructor() { super(); + makeObservable(this); + this.bindEvents(); + } - reaction(() => toJS(ClusterStore.getInstance().clustersList, { recurseEverything: true }), () => { - this.updateCatalog(ClusterStore.getInstance().clustersList); - }, { fireImmediately: true }); + private bindEvents() { + // reacting to every cluster's state change and total amount of items + reaction( + () => this.store.clustersList.map(c => c.getState()), + () => this.updateCatalog(this.store.clustersList), + { fireImmediately: true, } + ); reaction(() => catalogEntityRegistry.getItemsForApiKind("entity.k8slens.dev/v1alpha1", "KubernetesCluster"), (entities) => { this.syncClustersFromCatalog(entities); }); - // auto-stop removed clusters autorun(() => { - const removedClusters = Array.from(ClusterStore.getInstance().removedClusters.values()); + const removedClusters = Array.from(this.store.removedClusters.values()); if (removedClusters.length > 0) { const meta = removedClusters.map(cluster => cluster.getMeta()); logger.info(`[CLUSTER-MANAGER]: removing clusters`, meta); removedClusters.forEach(cluster => cluster.disconnect()); - ClusterStore.getInstance().removedClusters.clear(); + this.store.removedClusters.clear(); } }, { delay: 250 }); - ipcMain.on("network:offline", () => { this.onNetworkOffline(); }); - ipcMain.on("network:online", () => { this.onNetworkOnline(); }); + ipcMain.on("network:offline", this.onNetworkOffline); + ipcMain.on("network:online", this.onNetworkOnline); } - @action protected updateCatalog(clusters: Cluster[]) { + @action + protected updateCatalog(clusters: Cluster[]) { for (const cluster of clusters) { const index = catalogEntityRegistry.items.findIndex((entity) => entity.metadata.uid === cluster.id); @@ -94,10 +103,10 @@ export class ClusterManager extends Singleton { @action syncClustersFromCatalog(entities: KubernetesCluster[]) { for (const entity of entities) { - const cluster = ClusterStore.getInstance().getById(entity.metadata.uid); + const cluster = this.store.getById(entity.metadata.uid); if (!cluster) { - ClusterStore.getInstance().addCluster({ + this.store.addCluster({ id: entity.metadata.uid, preferences: { clusterName: entity.metadata.name @@ -117,28 +126,28 @@ export class ClusterManager extends Singleton { } } - protected onNetworkOffline() { + protected onNetworkOffline = () => { logger.info("[CLUSTER-MANAGER]: network is offline"); - ClusterStore.getInstance().clustersList.forEach((cluster) => { + this.store.clustersList.forEach((cluster) => { if (!cluster.disconnected) { cluster.online = false; cluster.accessible = false; cluster.refreshConnectionStatus().catch((e) => e); } }); - } + }; - protected onNetworkOnline() { + protected onNetworkOnline = () => { logger.info("[CLUSTER-MANAGER]: network is online"); - ClusterStore.getInstance().clustersList.forEach((cluster) => { + this.store.clustersList.forEach((cluster) => { if (!cluster.disconnected) { cluster.refreshConnectionStatus().catch((e) => e); } }); - } + }; stop() { - ClusterStore.getInstance().clusters.forEach((cluster: Cluster) => { + this.store.clusters.forEach((cluster: Cluster) => { cluster.disconnect(); }); } @@ -150,18 +159,18 @@ export class ClusterManager extends Singleton { if (req.headers.host.startsWith("127.0.0.1")) { const clusterId = req.url.split("/")[1]; - cluster = ClusterStore.getInstance().getById(clusterId); + cluster = this.store.getById(clusterId); if (cluster) { // we need to swap path prefix so that request is proxied to kube api req.url = req.url.replace(`/${clusterId}`, apiKubePrefix); } } else if (req.headers["x-cluster-id"]) { - cluster = ClusterStore.getInstance().getById(req.headers["x-cluster-id"].toString()); + cluster = this.store.getById(req.headers["x-cluster-id"].toString()); } else { const clusterId = getClusterIdFromHost(req.headers.host); - cluster = ClusterStore.getInstance().getById(clusterId); + cluster = this.store.getById(clusterId); } return cluster; @@ -169,9 +178,7 @@ export class ClusterManager extends Singleton { } export function catalogEntityFromCluster(cluster: Cluster) { - return new KubernetesCluster(toJS({ - apiVersion: "entity.k8slens.dev/v1alpha1", - kind: "KubernetesCluster", + return new KubernetesCluster({ metadata: { uid: cluster.id, name: cluster.name, @@ -190,5 +197,5 @@ export function catalogEntityFromCluster(cluster: Cluster) { message: "", active: !cluster.disconnected } - })); + }); } diff --git a/src/main/cluster.ts b/src/main/cluster.ts index 6def60b08f..09d53ad5cf 100644 --- a/src/main/cluster.ts +++ b/src/main/cluster.ts @@ -21,7 +21,7 @@ import { ipcMain } from "electron"; import type { ClusterId, ClusterMetadata, ClusterModel, ClusterPreferences, ClusterPrometheusPreferences, UpdateClusterModel } from "../common/cluster-store"; -import { action, comparer, computed, observable, reaction, toJS, when } from "mobx"; +import { action, comparer, computed, makeObservable, observable, reaction, when } from "mobx"; import { broadcastMessage, ClusterListNamespaceForbiddenChannel } from "../common/ipc"; import { ContextHandler } from "./context-handler"; import { AuthorizationV1Api, CoreV1Api, HttpError, KubeConfig, V1ResourceAttributes } from "@kubernetes/client-node"; @@ -33,6 +33,7 @@ import logger from "./logger"; import { VersionDetector } from "./cluster-detectors/version-detector"; import { detectorRegistry } from "./cluster-detectors/detector-registry"; import plimit from "p-limit"; +import { toJS } from "../common/utils"; export enum ClusterStatus { AccessGranted = 2, @@ -91,7 +92,9 @@ export class Cluster implements ClusterModel, ClusterState { protected activated = false; private resourceAccessStatuses: Map = new Map(); - whenReady = when(() => this.ready); + get whenReady() { + return when(() => this.ready); + } /** * Kubeconfig context name @@ -227,9 +230,7 @@ export class Cluster implements ClusterModel, ClusterState { @computed get prometheusPreferences(): ClusterPrometheusPreferences { const { prometheus, prometheusProvider } = this.preferences; - return toJS({ prometheus, prometheusProvider }, { - recurseEverything: true, - }); + return toJS({ prometheus, prometheusProvider }); } /** @@ -240,6 +241,7 @@ export class Cluster implements ClusterModel, ClusterState { } constructor(model: ClusterModel) { + makeObservable(this); this.id = model.id; this.updateModel(model); @@ -570,9 +572,7 @@ export class Cluster implements ClusterModel, ClusterState { accessibleNamespaces: this.accessibleNamespaces, }; - return toJS(model, { - recurseEverything: true - }); + return toJS(model); } /** @@ -592,9 +592,7 @@ export class Cluster implements ClusterModel, ClusterState { isGlobalWatchEnabled: this.isGlobalWatchEnabled, }; - return toJS(state, { - recurseEverything: true - }); + return toJS(state); } /** diff --git a/src/main/extension-filesystem.ts b/src/main/extension-filesystem.ts index 5321bc791e..402619bdc9 100644 --- a/src/main/extension-filesystem.ts +++ b/src/main/extension-filesystem.ts @@ -23,23 +23,25 @@ import { randomBytes } from "crypto"; import { SHA256 } from "crypto-js"; import { app, remote } from "electron"; import fse from "fs-extra"; -import { action, observable, toJS } from "mobx"; +import { action, makeObservable, observable } from "mobx"; import path from "path"; import { BaseStore } from "../common/base-store"; import type { LensExtensionId } from "../extensions/lens-extension"; +import { toJS } from "../common/utils"; interface FSProvisionModel { extensions: Record; // extension names to paths } export class FilesystemProvisionerStore extends BaseStore { - @observable registeredExtensions = observable.map(); + registeredExtensions = observable.map(); constructor() { super({ configName: "lens-filesystem-provisioner-store", accessPropertiesByDotNotation: false, // To make dots safe in cluster context names }); + makeObservable(this); } /** @@ -71,9 +73,7 @@ export class FilesystemProvisionerStore extends BaseStore { toJSON(): FSProvisionModel { return toJS({ - extensions: this.registeredExtensions.toJSON(), - }, { - recurseEverything: true + extensions: Object.fromEntries(this.registeredExtensions), }); } } diff --git a/src/main/index.ts b/src/main/index.ts index 2585b0740e..271010d922 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -24,7 +24,7 @@ import "../common/system-ca"; import "../common/prometheus-providers"; import * as Mobx from "mobx"; -import * as LensExtensions from "../extensions/core-api"; +import * as LensExtensionsCoreApi from "../extensions/core-api"; import { app, autoUpdater, ipcMain, dialog, powerMonitor } from "electron"; import { appName, isMac, productName } from "../common/vars"; import path from "path"; @@ -55,6 +55,7 @@ import { HotbarStore } from "../common/hotbar-store"; import { HelmRepoManager } from "./helm/helm-repo-manager"; import { KubeconfigSyncManager } from "./catalog-sources"; import { handleWsUpgrade } from "./proxy/ws-upgrade"; +import configurePackages from "../common/configure-packages"; const workingDir = path.join(app.getPath("appData"), appName); const cleanup = disposer(); @@ -77,6 +78,7 @@ if (process.env.LENS_DISABLE_GPU) { app.disableHardwareAcceleration(); } +configurePackages(); mangleProxyEnv(); if (app.commandLine.getSwitchValue("proxy-server") !== "") { @@ -191,15 +193,11 @@ app.on("ready", async () => { cleanup.push(pushCatalogToRenderer(catalogEntityRegistry)); KubeconfigSyncManager.getInstance().startSync(); startUpdateChecking(); - LensProtocolRouterMain - .getInstance() - .rendererLoaded = true; + LensProtocolRouterMain.getInstance().rendererLoaded = true; }); ExtensionLoader.getInstance().whenLoaded.then(() => { - LensProtocolRouterMain - .getInstance() - .extensionsLoaded = true; + LensProtocolRouterMain.getInstance().extensionsLoaded = true; }); logger.info("🧩 Initializing extensions"); @@ -272,12 +270,16 @@ app.on("open-url", (event, rawUrl) => { .catch(error => logger.error(`${LensProtocolRouterMain.LoggingPrefix}: an error occured`, { error, rawUrl })); }); -// Extensions-api runtime exports -export const LensExtensionsApi = { - ...LensExtensions, +/** + * Exports for virtual package "@k8slens/extensions" for main-process. + * All exporting names available in global runtime scope: + * e.g. global.Mobx, global.LensExtensions + */ +const LensExtensions = { + ...LensExtensionsCoreApi, }; export { Mobx, - LensExtensionsApi as LensExtensions, + LensExtensions, }; diff --git a/src/main/prometheus/lens.ts b/src/main/prometheus/lens.ts index 18c4763dc9..4fb5c8d339 100644 --- a/src/main/prometheus/lens.ts +++ b/src/main/prometheus/lens.ts @@ -88,18 +88,18 @@ export class PrometheusLens implements PrometheusProvider { }; case "pvc": return { - diskUsage: `sum(kubelet_volume_stats_used_bytes{persistentvolumeclaim="${opts.pvc}"}) by (persistentvolumeclaim, namespace)`, - diskCapacity: `sum(kubelet_volume_stats_capacity_bytes{persistentvolumeclaim="${opts.pvc}"}) by (persistentvolumeclaim, namespace)` + diskUsage: `sum(kubelet_volume_stats_used_bytes{persistentvolumeclaim="${opts.pvc}",namespace="${opts.namespace}"}) by (persistentvolumeclaim, namespace)`, + diskCapacity: `sum(kubelet_volume_stats_capacity_bytes{persistentvolumeclaim="${opts.pvc}",namespace="${opts.namespace}"}) by (persistentvolumeclaim, namespace)` }; case "ingress": - const bytesSent = (ingress: string, statuses: string) => - `sum(rate(nginx_ingress_controller_bytes_sent_sum{ingress="${ingress}", status=~"${statuses}"}[${this.rateAccuracy}])) by (ingress)`; + const bytesSent = (ingress: string, namespace: string, statuses: string) => + `sum(rate(nginx_ingress_controller_bytes_sent_sum{ingress="${ingress}",namespace="${namespace}",status=~"${statuses}"}[${this.rateAccuracy}])) by (ingress, namespace)`; return { - bytesSentSuccess: bytesSent(opts.ingress, "^2\\\\d*"), - bytesSentFailure: bytesSent(opts.ingress, "^5\\\\d*"), - requestDurationSeconds: `sum(rate(nginx_ingress_controller_request_duration_seconds_sum{ingress="${opts.ingress}"}[${this.rateAccuracy}])) by (ingress)`, - responseDurationSeconds: `sum(rate(nginx_ingress_controller_response_duration_seconds_sum{ingress="${opts.ingress}"}[${this.rateAccuracy}])) by (ingress)` + bytesSentSuccess: bytesSent(opts.ingress, opts.namespace, "^2\\\\d*"), + bytesSentFailure: bytesSent(opts.ingress, opts.namespace, "^5\\\\d*"), + requestDurationSeconds: `sum(rate(nginx_ingress_controller_request_duration_seconds_sum{ingress="${opts.ingress}",namespace="${opts.namespace}"}[${this.rateAccuracy}])) by (ingress, namespace)`, + responseDurationSeconds: `sum(rate(nginx_ingress_controller_response_duration_seconds_sum{ingress="${opts.ingress}",namespace="${opts.namespace}"}[${this.rateAccuracy}])) by (ingress, namespace)` }; } } diff --git a/src/main/prometheus/operator.ts b/src/main/prometheus/operator.ts index dead850056..b546cac517 100644 --- a/src/main/prometheus/operator.ts +++ b/src/main/prometheus/operator.ts @@ -98,18 +98,18 @@ export class PrometheusOperator implements PrometheusProvider { }; case "pvc": return { - diskUsage: `sum(kubelet_volume_stats_used_bytes{persistentvolumeclaim="${opts.pvc}"}) by (persistentvolumeclaim, namespace)`, - diskCapacity: `sum(kubelet_volume_stats_capacity_bytes{persistentvolumeclaim="${opts.pvc}"}) by (persistentvolumeclaim, namespace)` + diskUsage: `sum(kubelet_volume_stats_used_bytes{persistentvolumeclaim="${opts.pvc}",namespace="${opts.namespace}"}) by (persistentvolumeclaim, namespace)`, + diskCapacity: `sum(kubelet_volume_stats_capacity_bytes{persistentvolumeclaim="${opts.pvc}",namespace="${opts.namespace}"}) by (persistentvolumeclaim, namespace)` }; case "ingress": - const bytesSent = (ingress: string, statuses: string) => - `sum(rate(nginx_ingress_controller_bytes_sent_sum{ingress="${ingress}", status=~"${statuses}"}[${this.rateAccuracy}])) by (ingress)`; + const bytesSent = (ingress: string, namespace: string, statuses: string) => + `sum(rate(nginx_ingress_controller_bytes_sent_sum{ingress="${ingress}",namespace="${namespace}",status=~"${statuses}"}[${this.rateAccuracy}])) by (ingress, namespace)`; return { - bytesSentSuccess: bytesSent(opts.ingress, "^2\\\\d*"), - bytesSentFailure: bytesSent(opts.ingress, "^5\\\\d*"), - requestDurationSeconds: `sum(rate(nginx_ingress_controller_request_duration_seconds_sum{ingress="${opts.ingress}"}[${this.rateAccuracy}])) by (ingress)`, - responseDurationSeconds: `sum(rate(nginx_ingress_controller_response_duration_seconds_sum{ingress="${opts.ingress}"}[${this.rateAccuracy}])) by (ingress)` + bytesSentSuccess: bytesSent(opts.ingress, opts.namespace, "^2\\\\d*"), + bytesSentFailure: bytesSent(opts.ingress, opts.namespace, "^5\\\\d*"), + requestDurationSeconds: `sum(rate(nginx_ingress_controller_request_duration_seconds_sum{ingress="${opts.ingress}",namespace="${opts.namespace}"}[${this.rateAccuracy}])) by (ingress, namespace)`, + responseDurationSeconds: `sum(rate(nginx_ingress_controller_response_duration_seconds_sum{ingress="${opts.ingress}",namespace="${opts.namespace}"}[${this.rateAccuracy}])) by (ingress, namespace)` }; } } diff --git a/src/main/prometheus/stacklight.ts b/src/main/prometheus/stacklight.ts index 0bdda6b96e..40c1963d3f 100644 --- a/src/main/prometheus/stacklight.ts +++ b/src/main/prometheus/stacklight.ts @@ -88,18 +88,18 @@ export class PrometheusStacklight implements PrometheusProvider { }; case "pvc": return { - diskUsage: `sum(kubelet_volume_stats_used_bytes{persistentvolumeclaim="${opts.pvc}"}) by (persistentvolumeclaim, namespace)`, - diskCapacity: `sum(kubelet_volume_stats_capacity_bytes{persistentvolumeclaim="${opts.pvc}"}) by (persistentvolumeclaim, namespace)` + diskUsage: `sum(kubelet_volume_stats_used_bytes{persistentvolumeclaim="${opts.pvc}",namespace="${opts.namespace}"}) by (persistentvolumeclaim, namespace)`, + diskCapacity: `sum(kubelet_volume_stats_capacity_bytes{persistentvolumeclaim="${opts.pvc}",namespace="${opts.namespace}"}) by (persistentvolumeclaim, namespace)` }; case "ingress": - const bytesSent = (ingress: string, statuses: string) => - `sum(rate(nginx_ingress_controller_bytes_sent_sum{ingress="${ingress}", status=~"${statuses}"}[${this.rateAccuracy}])) by (ingress)`; + const bytesSent = (ingress: string, namespace: string, statuses: string) => + `sum(rate(nginx_ingress_controller_bytes_sent_sum{ingress="${ingress}",namespace="${namespace}",status=~"${statuses}"}[${this.rateAccuracy}])) by (ingress, namespace)`; return { - bytesSentSuccess: bytesSent(opts.ingress, "^2\\\\d*"), - bytesSentFailure: bytesSent(opts.ingress, "^5\\\\d*"), - requestDurationSeconds: `sum(rate(nginx_ingress_controller_request_duration_seconds_sum{ingress="${opts.ingress}"}[${this.rateAccuracy}])) by (ingress)`, - responseDurationSeconds: `sum(rate(nginx_ingress_controller_response_duration_seconds_sum{ingress="${opts.ingress}"}[${this.rateAccuracy}])) by (ingress)` + bytesSentSuccess: bytesSent(opts.ingress, opts.namespace, "^2\\\\d*"), + bytesSentFailure: bytesSent(opts.ingress, opts.namespace, "^5\\\\d*"), + requestDurationSeconds: `sum(rate(nginx_ingress_controller_request_duration_seconds_sum{ingress="${opts.ingress}",namespace="${opts.namespace}"}[${this.rateAccuracy}])) by (ingress, namespace)`, + responseDurationSeconds: `sum(rate(nginx_ingress_controller_response_duration_seconds_sum{ingress="${opts.ingress}",namespace="${opts.namespace}"}[${this.rateAccuracy}])) by (ingress, namespace)` }; } } diff --git a/src/main/protocol-handler/router.ts b/src/main/protocol-handler/router.ts index afc0d063b6..087ab3fd88 100644 --- a/src/main/protocol-handler/router.ts +++ b/src/main/protocol-handler/router.ts @@ -24,7 +24,7 @@ import * as proto from "../../common/protocol-handler"; import Url from "url-parse"; import type { LensExtension } from "../../extensions/lens-extension"; import { broadcastMessage } from "../../common/ipc"; -import { observable, when } from "mobx"; +import { observable, when, makeObservable } from "mobx"; export interface FallbackHandler { (name: string): Promise; @@ -36,6 +36,12 @@ export class LensProtocolRouterMain extends proto.LensProtocolRouter { @observable rendererLoaded = false; @observable extensionsLoaded = false; + constructor() { + super(); + + makeObservable(this); + } + /** * Find the most specific registered handler, if it exists, and invoke it. * diff --git a/src/main/shell-sync.ts b/src/main/shell-sync.ts index df542d56ed..ff38ede3bb 100644 --- a/src/main/shell-sync.ts +++ b/src/main/shell-sync.ts @@ -38,7 +38,12 @@ export async function shellSync() { let envVars = {}; try { - envVars = await shellEnv(shell); + envVars = await Promise.race([ + shellEnv(shell), + new Promise((_resolve, reject) => setTimeout(() => { + reject(new Error("Resolving shell environment is taking very long. Please review your shell configuration.")); + }, 5_000)) + ]); } catch (error) { logger.error(`shellEnv: ${error}`); } diff --git a/src/main/window-manager.ts b/src/main/window-manager.ts index f1d4073edb..4e5588e47a 100644 --- a/src/main/window-manager.ts +++ b/src/main/window-manager.ts @@ -20,7 +20,7 @@ */ import type { ClusterId } from "../common/cluster-store"; -import { observable } from "mobx"; +import { makeObservable, observable } from "mobx"; import { app, BrowserWindow, dialog, shell, webContents } from "electron"; import windowStateKeeper from "electron-window-state"; import { appEventBus } from "../common/event-bus"; @@ -44,6 +44,7 @@ export class WindowManager extends Singleton { constructor() { super(); + makeObservable(this); this.bindEvents(); this.initMenu(); this.initTray(); diff --git a/src/renderer/api/api-manager.ts b/src/renderer/api/api-manager.ts index ada5424bef..eafcf0d8ee 100644 --- a/src/renderer/api/api-manager.ts +++ b/src/renderer/api/api-manager.ts @@ -21,15 +21,19 @@ import type { KubeObjectStore } from "../kube-object.store"; -import { action, observable } from "mobx"; -import { autobind } from "../utils"; +import { action, observable, makeObservable } from "mobx"; +import { autoBind } from "../utils"; import { KubeApi, parseKubeApi } from "./kube-api"; -@autobind() export class ApiManager { private apis = observable.map(); private stores = observable.map(); + constructor() { + makeObservable(this); + autoBind(this); + } + getApi(pathOrCallback: string | ((api: KubeApi) => boolean)) { if (typeof pathOrCallback === "string") { return this.apis.get(pathOrCallback) || this.apis.get(parseKubeApi(pathOrCallback).apiBase); diff --git a/src/renderer/api/catalog-entity-registry.ts b/src/renderer/api/catalog-entity-registry.ts index d6e2059edf..63681106bd 100644 --- a/src/renderer/api/catalog-entity-registry.ts +++ b/src/renderer/api/catalog-entity-registry.ts @@ -19,7 +19,7 @@ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -import { computed, observable } from "mobx"; +import { computed, observable, makeObservable } from "mobx"; import { subscribeToBroadcast } from "../../common/ipc"; import { CatalogCategory, CatalogEntity, CatalogEntityData, catalogCategoryRegistry, CatalogCategoryRegistry, CatalogEntityKindData } from "../../common/catalog"; import "../../common/catalog-entities"; @@ -29,7 +29,9 @@ export class CatalogEntityRegistry { protected rawItems = observable.array([], { deep: true }); @observable protected _activeEntity: CatalogEntity; - constructor(private categoryRegistry: CatalogCategoryRegistry) {} + constructor(private categoryRegistry: CatalogCategoryRegistry) { + makeObservable(this); + } init() { subscribeToBroadcast("catalog:items", (ev, items: (CatalogEntityData & CatalogEntityKindData)[]) => { diff --git a/src/renderer/api/endpoints/cluster-role.api.ts b/src/renderer/api/endpoints/cluster-role.api.ts index 6cb995c323..6471b9c5e1 100644 --- a/src/renderer/api/endpoints/cluster-role.api.ts +++ b/src/renderer/api/endpoints/cluster-role.api.ts @@ -19,11 +19,9 @@ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -import { autobind } from "../../utils"; import { Role } from "./role.api"; import { KubeApi } from "../kube-api"; -@autobind() export class ClusterRole extends Role { static kind = "ClusterRole"; static namespaced = false; diff --git a/src/renderer/api/endpoints/cluster.api.ts b/src/renderer/api/endpoints/cluster.api.ts index 309f936ac7..5c3f05ce91 100644 --- a/src/renderer/api/endpoints/cluster.api.ts +++ b/src/renderer/api/endpoints/cluster.api.ts @@ -71,10 +71,7 @@ export interface IClusterMetrics { fsUsage: T; } -export class Cluster extends KubeObject { - static kind = "Cluster"; - static apiBase = "/apis/cluster.k8s.io/v1alpha1/clusters"; - +export interface Cluster { spec: { clusterNetwork?: { serviceDomain?: string; @@ -106,6 +103,11 @@ export class Cluster extends KubeObject { errorMessage?: string; errorReason?: string; }; +} + +export class Cluster extends KubeObject { + static kind = "Cluster"; + static apiBase = "/apis/cluster.k8s.io/v1alpha1/clusters"; getStatus() { if (this.metadata.deletionTimestamp) return ClusterStatus.REMOVING; diff --git a/src/renderer/api/endpoints/component-status.api.ts b/src/renderer/api/endpoints/component-status.api.ts index f9c8444f0b..5b1632bf57 100644 --- a/src/renderer/api/endpoints/component-status.api.ts +++ b/src/renderer/api/endpoints/component-status.api.ts @@ -28,13 +28,15 @@ export interface IComponentStatusCondition { message: string; } +export interface ComponentStatus { + conditions: IComponentStatusCondition[]; +} + export class ComponentStatus extends KubeObject { static kind = "ComponentStatus"; static namespaced = false; static apiBase = "/api/v1/componentstatuses"; - conditions: IComponentStatusCondition[]; - getTruthyConditions() { return this.conditions.filter(c => c.status === "True"); } diff --git a/src/renderer/api/endpoints/configmap.api.ts b/src/renderer/api/endpoints/configmap.api.ts index 2c96494c57..5087f352ca 100644 --- a/src/renderer/api/endpoints/configmap.api.ts +++ b/src/renderer/api/endpoints/configmap.api.ts @@ -21,10 +21,15 @@ import { KubeObject } from "../kube-object"; import type { KubeJsonApiData } from "../kube-json-api"; -import { autobind } from "../../utils"; import { KubeApi } from "../kube-api"; +import { autoBind } from "../../../common/utils"; + +export interface ConfigMap { + data: { + [param: string]: string; + }; +} -@autobind() export class ConfigMap extends KubeObject { static kind = "ConfigMap"; static namespaced = true; @@ -32,12 +37,10 @@ export class ConfigMap extends KubeObject { constructor(data: KubeJsonApiData) { super(data); - this.data = this.data || {}; - } + autoBind(this); - data: { - [param: string]: string; - }; + this.data ??= {}; + } getKeys(): string[] { return Object.keys(this.data); diff --git a/src/renderer/api/endpoints/crd.api.ts b/src/renderer/api/endpoints/crd.api.ts index 5d2b657ce4..1d1d337d9c 100644 --- a/src/renderer/api/endpoints/crd.api.ts +++ b/src/renderer/api/endpoints/crd.api.ts @@ -38,11 +38,7 @@ type AdditionalPrinterColumnsV1Beta = AdditionalPrinterColumnsCommon & { JSONPath: string; }; -export class CustomResourceDefinition extends KubeObject { - static kind = "CustomResourceDefinition"; - static namespaced = false; - static apiBase = "/apis/apiextensions.k8s.io/v1/customresourcedefinitions"; - +export interface CustomResourceDefinition { spec: { group: string; version?: string; // deprecated in v1 api @@ -84,6 +80,12 @@ export class CustomResourceDefinition extends KubeObject { }; storedVersions: string[]; }; +} + +export class CustomResourceDefinition extends KubeObject { + static kind = "CustomResourceDefinition"; + static namespaced = false; + static apiBase = "/apis/apiextensions.k8s.io/v1/customresourcedefinitions"; getResourceUrl() { return crdResourcesURL({ diff --git a/src/renderer/api/endpoints/cron-job.api.ts b/src/renderer/api/endpoints/cron-job.api.ts index 09c18924d4..43743b2bb4 100644 --- a/src/renderer/api/endpoints/cron-job.api.ts +++ b/src/renderer/api/endpoints/cron-job.api.ts @@ -23,8 +23,9 @@ import moment from "moment"; import { KubeObject } from "../kube-object"; import type { IPodContainer } from "./pods.api"; import { formatDuration } from "../../utils/formatDuration"; -import { autobind } from "../../utils"; +import { autoBind } from "../../utils"; import { KubeApi } from "../kube-api"; +import type { KubeJsonApiData } from "../kube-json-api"; export class CronJobApi extends KubeApi { suspend(params: { namespace: string; name: string }) { @@ -58,28 +59,7 @@ export class CronJobApi extends KubeApi { } } -@autobind() -export class CronJob extends KubeObject { - static kind = "CronJob"; - static namespaced = true; - static apiBase = "/apis/batch/v1beta1/cronjobs"; - - kind: string; - apiVersion: string; - metadata: { - name: string; - namespace: string; - selfLink: string; - uid: string; - resourceVersion: string; - creationTimestamp: string; - labels: { - [key: string]: string; - }; - annotations: { - [key: string]: string; - }; - }; +export interface CronJob { spec: { schedule: string; concurrencyPolicy: string; @@ -116,6 +96,17 @@ export class CronJob extends KubeObject { status: { lastScheduleTime?: string; }; +} + +export class CronJob extends KubeObject { + static kind = "CronJob"; + static namespaced = true; + static apiBase = "/apis/batch/v1beta1/cronjobs"; + + constructor(data: KubeJsonApiData) { + super(data); + autoBind(this); + } getSuspendFlag() { return this.spec.suspend.toString(); diff --git a/src/renderer/api/endpoints/daemon-set.api.ts b/src/renderer/api/endpoints/daemon-set.api.ts index 1f2660b3c5..43419b5ac1 100644 --- a/src/renderer/api/endpoints/daemon-set.api.ts +++ b/src/renderer/api/endpoints/daemon-set.api.ts @@ -22,16 +22,21 @@ import get from "lodash/get"; import type { IPodContainer } from "./pods.api"; import { IAffinity, WorkloadKubeObject } from "../workload-kube-object"; -import { autobind } from "../../utils"; +import { autoBind } from "../../utils"; import { KubeApi } from "../kube-api"; +import type { KubeJsonApiData } from "../kube-json-api"; -@autobind() export class DaemonSet extends WorkloadKubeObject { static kind = "DaemonSet"; static namespaced = true; static apiBase = "/apis/apps/v1/daemonsets"; - spec: { + constructor(data: KubeJsonApiData) { + super(data); + autoBind(this); + } + + declare spec: { selector: { matchLabels: { [name: string]: string; @@ -73,7 +78,7 @@ export class DaemonSet extends WorkloadKubeObject { }; revisionHistoryLimit: number; }; - status: { + declare status: { currentNumberScheduled: number; numberMisscheduled: number; desiredNumberScheduled: number; diff --git a/src/renderer/api/endpoints/deployment.api.ts b/src/renderer/api/endpoints/deployment.api.ts index 03d99947a6..ef5b1c9ca7 100644 --- a/src/renderer/api/endpoints/deployment.api.ts +++ b/src/renderer/api/endpoints/deployment.api.ts @@ -22,8 +22,9 @@ import moment from "moment"; import { IAffinity, WorkloadKubeObject } from "../workload-kube-object"; -import { autobind } from "../../utils"; +import { autoBind } from "../../utils"; import { KubeApi } from "../kube-api"; +import type { KubeJsonApiData } from "../kube-json-api"; export class DeploymentApi extends KubeApi { protected getScaleApiUrl(params: { namespace: string; name: string }) { @@ -87,13 +88,17 @@ interface IContainerProbe { failureThreshold?: number; } -@autobind() export class Deployment extends WorkloadKubeObject { static kind = "Deployment"; static namespaced = true; static apiBase = "/apis/apps/v1/deployments"; - spec: { + constructor(data: KubeJsonApiData) { + super(data); + autoBind(this); + } + + declare spec: { replicas: number; selector: { matchLabels: { [app: string]: string } }; template: { @@ -172,7 +177,7 @@ export class Deployment extends WorkloadKubeObject { }; }; }; - status: { + declare status: { observedGeneration: number; replicas: number; updatedReplicas: number; diff --git a/src/renderer/api/endpoints/endpoint.api.ts b/src/renderer/api/endpoints/endpoint.api.ts index 8b179a6d8c..9aa1701c1d 100644 --- a/src/renderer/api/endpoints/endpoint.api.ts +++ b/src/renderer/api/endpoints/endpoint.api.ts @@ -19,9 +19,10 @@ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -import { autobind } from "../../utils"; +import { autoBind } from "../../utils"; import { KubeObject } from "../kube-object"; import { KubeApi } from "../kube-api"; +import type { KubeJsonApiData } from "../kube-json-api"; export interface IEndpointPort { name?: string; @@ -121,13 +122,19 @@ export class EndpointSubset implements IEndpointSubset { } } -@autobind() +export interface Endpoint { + subsets: IEndpointSubset[]; +} + export class Endpoint extends KubeObject { static kind = "Endpoints"; static namespaced = true; static apiBase = "/api/v1/endpoints"; - subsets: IEndpointSubset[]; + constructor(data: KubeJsonApiData) { + super(data); + autoBind(this); + } getEndpointSubsets(): EndpointSubset[] { const subsets = this.subsets || []; diff --git a/src/renderer/api/endpoints/events.api.ts b/src/renderer/api/endpoints/events.api.ts index b424b980d3..ad194a532a 100644 --- a/src/renderer/api/endpoints/events.api.ts +++ b/src/renderer/api/endpoints/events.api.ts @@ -22,15 +22,9 @@ import moment from "moment"; import { KubeObject } from "../kube-object"; import { formatDuration } from "../../utils/formatDuration"; -import { autobind } from "../../utils"; import { KubeApi } from "../kube-api"; -@autobind() -export class KubeEvent extends KubeObject { - static kind = "Event"; - static namespaced = true; - static apiBase = "/api/v1/events"; - +export interface KubeEvent { involvedObject: { kind: string; namespace: string; @@ -53,6 +47,12 @@ export class KubeEvent extends KubeObject { eventTime: null; reportingComponent: string; reportingInstance: string; +} + +export class KubeEvent extends KubeObject { + static kind = "Event"; + static namespaced = true; + static apiBase = "/api/v1/events"; isWarning() { return this.type === "Warning"; diff --git a/src/renderer/api/endpoints/helm-charts.api.ts b/src/renderer/api/endpoints/helm-charts.api.ts index 62eea9596d..2244bca1f2 100644 --- a/src/renderer/api/endpoints/helm-charts.api.ts +++ b/src/renderer/api/endpoints/helm-charts.api.ts @@ -22,7 +22,7 @@ import { compile } from "path-to-regexp"; import { apiBase } from "../index"; import { stringify } from "querystring"; -import { autobind } from "../../utils"; +import { autoBind } from "../../utils"; export type RepoHelmChartList = Record; export type HelmChartList = Record; @@ -83,16 +83,7 @@ export async function getChartValues(repo: string, name: string, version: string return apiBase.get(`/v2/charts/${repo}/${name}/values?${stringify({ version })}`); } -@autobind() -export class HelmChart { - constructor(data: any) { - Object.assign(this, data); - } - - static create(data: any) { - return new HelmChart(data); - } - +export interface HelmChart { apiVersion: string; name: string; version: string; @@ -114,6 +105,17 @@ export class HelmChart { appVersion?: string; deprecated?: boolean; tillerVersion?: string; +} + +export class HelmChart { + constructor(data: HelmChart) { + Object.assign(this, data); + autoBind(this); + } + + static create(data: any) { + return new HelmChart(data); + } getId() { return `${this.repo}:${this.apiVersion}/${this.name}@${this.getAppVersion()}+${this.digest}`; diff --git a/src/renderer/api/endpoints/helm-releases.api.ts b/src/renderer/api/endpoints/helm-releases.api.ts index 45fc55a342..0e62027dbe 100644 --- a/src/renderer/api/endpoints/helm-releases.api.ts +++ b/src/renderer/api/endpoints/helm-releases.api.ts @@ -21,7 +21,7 @@ import jsYaml from "js-yaml"; import { compile } from "path-to-regexp"; -import { autobind, formatDuration } from "../../utils"; +import { autoBind, formatDuration } from "../../utils"; import capitalize from "lodash/capitalize"; import { apiBase } from "../index"; import { helmChartStore } from "../../components/+apps-helm-charts/helm-chart.store"; @@ -155,16 +155,7 @@ export async function rollbackRelease(name: string, namespace: string, revision: }); } -@autobind() -export class HelmRelease implements ItemObject { - constructor(data: any) { - Object.assign(this, data); - } - - static create(data: any) { - return new HelmRelease(data); - } - +export interface HelmRelease { appVersion: string; name: string; namespace: string; @@ -172,6 +163,17 @@ export class HelmRelease implements ItemObject { status: string; updated: string; revision: string; +} + +export class HelmRelease implements ItemObject { + constructor(data: any) { + Object.assign(this, data); + autoBind(this); + } + + static create(data: any) { + return new HelmRelease(data); + } getId() { return this.namespace + this.name; diff --git a/src/renderer/api/endpoints/hpa.api.ts b/src/renderer/api/endpoints/hpa.api.ts index 5d1105acc3..dba89d4111 100644 --- a/src/renderer/api/endpoints/hpa.api.ts +++ b/src/renderer/api/endpoints/hpa.api.ts @@ -59,11 +59,7 @@ export interface IHpaMetric { }>; } -export class HorizontalPodAutoscaler extends KubeObject { - static kind = "HorizontalPodAutoscaler"; - static namespaced = true; - static apiBase = "/apis/autoscaling/v2beta1/horizontalpodautoscalers"; - +export interface HorizontalPodAutoscaler { spec: { scaleTargetRef: { kind: string; @@ -86,6 +82,12 @@ export class HorizontalPodAutoscaler extends KubeObject { type: string; }[]; }; +} + +export class HorizontalPodAutoscaler extends KubeObject { + static kind = "HorizontalPodAutoscaler"; + static namespaced = true; + static apiBase = "/apis/autoscaling/v2beta1/horizontalpodautoscalers"; getMaxPods() { return this.spec.maxReplicas || 0; diff --git a/src/renderer/api/endpoints/ingress.api.ts b/src/renderer/api/endpoints/ingress.api.ts index d79a983e4e..a65af0e326 100644 --- a/src/renderer/api/endpoints/ingress.api.ts +++ b/src/renderer/api/endpoints/ingress.api.ts @@ -20,13 +20,14 @@ */ import { KubeObject } from "../kube-object"; -import { autobind } from "../../utils"; +import { autoBind } from "../../utils"; import { IMetrics, metricsApi } from "./metrics.api"; import { KubeApi } from "../kube-api"; +import type { KubeJsonApiData } from "../kube-json-api"; export class IngressApi extends KubeApi { getMetrics(ingress: string, namespace: string): Promise { - const opts = { category: "ingress", ingress }; + const opts = { category: "ingress", ingress, namespace }; return metricsApi.getMetrics({ bytesSentSuccess: opts, @@ -82,12 +83,7 @@ export const getBackendServiceNamePort = (backend: IIngressBackend) => { return { serviceName, servicePort }; }; -@autobind() -export class Ingress extends KubeObject { - static kind = "Ingress"; - static namespaced = true; - static apiBase = "/apis/networking.k8s.io/v1/ingresses"; - +export interface Ingress { spec: { tls: { secretName: string; @@ -117,6 +113,17 @@ export class Ingress extends KubeObject { ingress: ILoadBalancerIngress[]; }; }; +} + +export class Ingress extends KubeObject { + static kind = "Ingress"; + static namespaced = true; + static apiBase = "/apis/networking.k8s.io/v1/ingresses"; + + constructor(data: KubeJsonApiData) { + super(data); + autoBind(this); + } getRoutes() { const { spec: { tls, rules } } = this; @@ -188,7 +195,7 @@ export class Ingress extends KubeObject { getLoadBalancers() { const { status: { loadBalancer = { ingress: [] } } } = this; - + return (loadBalancer.ingress ?? []).map(address => ( address.hostname || address.ip )); diff --git a/src/renderer/api/endpoints/job.api.ts b/src/renderer/api/endpoints/job.api.ts index dac3cf7a71..f9e3188021 100644 --- a/src/renderer/api/endpoints/job.api.ts +++ b/src/renderer/api/endpoints/job.api.ts @@ -20,19 +20,24 @@ */ import get from "lodash/get"; -import { autobind } from "../../utils"; +import { autoBind } from "../../utils"; import { IAffinity, WorkloadKubeObject } from "../workload-kube-object"; import type { IPodContainer } from "./pods.api"; import { KubeApi } from "../kube-api"; import type { JsonApiParams } from "../json-api"; +import type { KubeJsonApiData } from "../kube-json-api"; -@autobind() export class Job extends WorkloadKubeObject { static kind = "Job"; static namespaced = true; static apiBase = "/apis/batch/v1/jobs"; - spec: { + constructor(data: KubeJsonApiData) { + super(data); + autoBind(this); + } + + declare spec: { parallelism?: number; completions?: number; backoffLimit?: number; @@ -78,7 +83,7 @@ export class Job extends WorkloadKubeObject { serviceAccount?: string; schedulerName?: string; }; - status: { + declare status: { conditions: { type: string; status: string; diff --git a/src/renderer/api/endpoints/limit-range.api.ts b/src/renderer/api/endpoints/limit-range.api.ts index bb6f06cbb1..82b02cfa70 100644 --- a/src/renderer/api/endpoints/limit-range.api.ts +++ b/src/renderer/api/endpoints/limit-range.api.ts @@ -21,7 +21,8 @@ import { KubeObject } from "../kube-object"; import { KubeApi } from "../kube-api"; -import { autobind } from "../../utils"; +import { autoBind } from "../../utils"; +import type { KubeJsonApiData } from "../kube-json-api"; export enum LimitType { CONTAINER = "Container", @@ -50,15 +51,21 @@ export interface LimitRangeItem extends LimitRangeParts { type: string } -@autobind() +export interface LimitRange { + spec: { + limits: LimitRangeItem[]; + }; +} + export class LimitRange extends KubeObject { static kind = "LimitRange"; static namespaced = true; static apiBase = "/api/v1/limitranges"; - spec: { - limits: LimitRangeItem[]; - }; + constructor(data: KubeJsonApiData) { + super(data); + autoBind(this); + } getContainerLimits() { return this.spec.limits.filter(limit => limit.type === LimitType.CONTAINER); diff --git a/src/renderer/api/endpoints/namespaces.api.ts b/src/renderer/api/endpoints/namespaces.api.ts index 448cdcedb3..63dd6a2ab0 100644 --- a/src/renderer/api/endpoints/namespaces.api.ts +++ b/src/renderer/api/endpoints/namespaces.api.ts @@ -21,25 +21,32 @@ import { KubeApi } from "../kube-api"; import { KubeObject } from "../kube-object"; -import { autobind } from "../../utils"; +import { autoBind } from "../../utils"; +import type { KubeJsonApiData } from "../kube-json-api"; export enum NamespaceStatus { ACTIVE = "Active", TERMINATING = "Terminating", } -@autobind() +export interface Namespace { + status?: { + phase: string; + }; +} + export class Namespace extends KubeObject { static kind = "Namespace"; static namespaced = false; static apiBase = "/api/v1/namespaces"; - status?: { - phase: string; - }; + constructor(data: KubeJsonApiData) { + super(data); + autoBind(this); + } getStatus() { - return this.status ? this.status.phase : "-"; + return this.status?.phase ?? "-"; } } diff --git a/src/renderer/api/endpoints/network-policy.api.ts b/src/renderer/api/endpoints/network-policy.api.ts index 8c5e2b57d6..03a06b7c42 100644 --- a/src/renderer/api/endpoints/network-policy.api.ts +++ b/src/renderer/api/endpoints/network-policy.api.ts @@ -20,8 +20,9 @@ */ import { KubeObject } from "../kube-object"; -import { autobind } from "../../utils"; +import { autoBind } from "../../utils"; import { KubeApi } from "../kube-api"; +import type { KubeJsonApiData } from "../kube-json-api"; export interface IPolicyIpBlock { cidr: string; @@ -56,12 +57,7 @@ export interface IPolicyEgress { }[]; } -@autobind() -export class NetworkPolicy extends KubeObject { - static kind = "NetworkPolicy"; - static namespaced = true; - static apiBase = "/apis/networking.k8s.io/v1/networkpolicies"; - +export interface NetworkPolicy { spec: { podSelector: { matchLabels: { @@ -73,6 +69,17 @@ export class NetworkPolicy extends KubeObject { ingress: IPolicyIngress[]; egress: IPolicyEgress[]; }; +} + +export class NetworkPolicy extends KubeObject { + static kind = "NetworkPolicy"; + static namespaced = true; + static apiBase = "/apis/networking.k8s.io/v1/networkpolicies"; + + constructor(data: KubeJsonApiData) { + super(data); + autoBind(this); + } getMatchLabels(): string[] { if (!this.spec.podSelector || !this.spec.podSelector.matchLabels) return []; diff --git a/src/renderer/api/endpoints/nodes.api.ts b/src/renderer/api/endpoints/nodes.api.ts index aea040eb77..4734c47a23 100644 --- a/src/renderer/api/endpoints/nodes.api.ts +++ b/src/renderer/api/endpoints/nodes.api.ts @@ -20,13 +20,14 @@ */ import { KubeObject } from "../kube-object"; -import { autobind, cpuUnitsToNumber, unitsToBytes } from "../../utils"; +import { autoBind, cpuUnitsToNumber, unitsToBytes } from "../../utils"; import { IMetrics, metricsApi } from "./metrics.api"; import { KubeApi } from "../kube-api"; +import type { KubeJsonApiData } from "../kube-json-api"; export class NodesApi extends KubeApi { getMetrics(): Promise { - const opts = { category: "nodes"}; + const opts = { category: "nodes" }; return metricsApi.getMetrics({ memoryUsage: opts, @@ -49,12 +50,7 @@ export interface INodeMetrics { fsSize: T; } -@autobind() -export class Node extends KubeObject { - static kind = "Node"; - static namespaced = false; - static apiBase = "/api/v1/nodes"; - +export interface Node { spec: { podCIDR: string; externalID: string; @@ -105,6 +101,17 @@ export class Node extends KubeObject { sizeBytes: number; }[]; }; +} + +export class Node extends KubeObject { + static kind = "Node"; + static namespaced = false; + static apiBase = "/api/v1/nodes"; + + constructor(data: KubeJsonApiData) { + super(data); + autoBind(this); + } getNodeConditionText() { const { conditions } = this.status; diff --git a/src/renderer/api/endpoints/persistent-volume-claims.api.ts b/src/renderer/api/endpoints/persistent-volume-claims.api.ts index 92641945d7..e8b50683c9 100644 --- a/src/renderer/api/endpoints/persistent-volume-claims.api.ts +++ b/src/renderer/api/endpoints/persistent-volume-claims.api.ts @@ -20,16 +20,17 @@ */ import { KubeObject } from "../kube-object"; -import { autobind } from "../../utils"; +import { autoBind } from "../../utils"; import { IMetrics, metricsApi } from "./metrics.api"; import type { Pod } from "./pods.api"; import { KubeApi } from "../kube-api"; +import type { KubeJsonApiData } from "../kube-json-api"; export class PersistentVolumeClaimsApi extends KubeApi { getMetrics(pvcName: string, namespace: string): Promise { return metricsApi.getMetrics({ - diskUsage: { category: "pvc", pvc: pvcName }, - diskCapacity: { category: "pvc", pvc: pvcName } + diskUsage: { category: "pvc", pvc: pvcName, namespace }, + diskCapacity: { category: "pvc", pvc: pvcName, namespace } }, { namespace }); @@ -42,12 +43,7 @@ export interface IPvcMetrics { diskCapacity: T; } -@autobind() -export class PersistentVolumeClaim extends KubeObject { - static kind = "PersistentVolumeClaim"; - static namespaced = true; - static apiBase = "/api/v1/persistentvolumeclaims"; - +export interface PersistentVolumeClaim { spec: { accessModes: string[]; storageClassName: string; @@ -70,6 +66,17 @@ export class PersistentVolumeClaim extends KubeObject { status: { phase: string; // Pending }; +} + +export class PersistentVolumeClaim extends KubeObject { + static kind = "PersistentVolumeClaim"; + static namespaced = true; + static apiBase = "/api/v1/persistentvolumeclaims"; + + constructor(data: KubeJsonApiData) { + super(data); + autoBind(this); + } getPods(allPods: Pod[]): Pod[] { const pods = allPods.filter(pod => pod.getNs() === this.getNs()); diff --git a/src/renderer/api/endpoints/persistent-volume.api.ts b/src/renderer/api/endpoints/persistent-volume.api.ts index c2bf11e29f..e71c355977 100644 --- a/src/renderer/api/endpoints/persistent-volume.api.ts +++ b/src/renderer/api/endpoints/persistent-volume.api.ts @@ -21,15 +21,11 @@ import { KubeObject } from "../kube-object"; import { unitsToBytes } from "../../utils/convertMemory"; -import { autobind } from "../../utils"; +import { autoBind } from "../../utils"; import { KubeApi } from "../kube-api"; +import type { KubeJsonApiData } from "../kube-json-api"; -@autobind() -export class PersistentVolume extends KubeObject { - static kind = "PersistentVolume"; - static namespaced = false; - static apiBase = "/api/v1/persistentvolumes"; - +export interface PersistentVolume { spec: { capacity: { storage: string; // 8Gi @@ -65,6 +61,17 @@ export class PersistentVolume extends KubeObject { phase: string; reason?: string; }; +} + +export class PersistentVolume extends KubeObject { + static kind = "PersistentVolume"; + static namespaced = false; + static apiBase = "/api/v1/persistentvolumes"; + + constructor(data: KubeJsonApiData) { + super(data); + autoBind(this); + } getCapacity(inBytes = false) { const capacity = this.spec.capacity; diff --git a/src/renderer/api/endpoints/pod-metrics.api.ts b/src/renderer/api/endpoints/pod-metrics.api.ts index d39c578fc1..191a128267 100644 --- a/src/renderer/api/endpoints/pod-metrics.api.ts +++ b/src/renderer/api/endpoints/pod-metrics.api.ts @@ -22,11 +22,7 @@ import { KubeObject } from "../kube-object"; import { KubeApi } from "../kube-api"; -export class PodMetrics extends KubeObject { - static kind = "PodMetrics"; - static namespaced = true; - static apiBase = "/apis/metrics.k8s.io/v1beta1/pods"; - +export interface PodMetrics { timestamp: string; window: string; containers: { @@ -38,6 +34,12 @@ export class PodMetrics extends KubeObject { }[]; } +export class PodMetrics extends KubeObject { + static kind = "PodMetrics"; + static namespaced = true; + static apiBase = "/apis/metrics.k8s.io/v1beta1/pods"; +} + export const podMetricsApi = new KubeApi({ objectConstructor: PodMetrics, }); diff --git a/src/renderer/api/endpoints/poddisruptionbudget.api.ts b/src/renderer/api/endpoints/poddisruptionbudget.api.ts index b9763b093c..daa16933da 100644 --- a/src/renderer/api/endpoints/poddisruptionbudget.api.ts +++ b/src/renderer/api/endpoints/poddisruptionbudget.api.ts @@ -19,16 +19,12 @@ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -import { autobind } from "../../utils"; +import { autoBind } from "../../utils"; import { KubeObject } from "../kube-object"; import { KubeApi } from "../kube-api"; +import type { KubeJsonApiData } from "../kube-json-api"; -@autobind() -export class PodDisruptionBudget extends KubeObject { - static kind = "PodDisruptionBudget"; - static namespaced = true; - static apiBase = "/apis/policy/v1beta1/poddisruptionbudgets"; - +export interface PodDisruptionBudget { spec: { minAvailable: string; maxUnavailable: string; @@ -40,6 +36,17 @@ export class PodDisruptionBudget extends KubeObject { disruptionsAllowed: number expectedPods: number }; +} + +export class PodDisruptionBudget extends KubeObject { + static kind = "PodDisruptionBudget"; + static namespaced = true; + static apiBase = "/apis/policy/v1beta1/poddisruptionbudgets"; + + constructor(data: KubeJsonApiData) { + super(data); + autoBind(this); + } getSelectors() { const selector = this.spec.selector; diff --git a/src/renderer/api/endpoints/pods.api.ts b/src/renderer/api/endpoints/pods.api.ts index 8ce95ff297..a111cd9559 100644 --- a/src/renderer/api/endpoints/pods.api.ts +++ b/src/renderer/api/endpoints/pods.api.ts @@ -20,9 +20,10 @@ */ import { IAffinity, WorkloadKubeObject } from "../workload-kube-object"; -import { autobind } from "../../utils"; +import { autoBind } from "../../utils"; import { IMetrics, metricsApi } from "./metrics.api"; import { KubeApi } from "../kube-api"; +import type { KubeJsonApiData } from "../kube-json-api"; export class PodsApi extends KubeApi { async getLogs(params: { namespace: string; name: string }, query?: IPodLogsQuery): Promise { @@ -202,13 +203,17 @@ export interface IPodContainerStatus { started?: boolean; } -@autobind() export class Pod extends WorkloadKubeObject { static kind = "Pod"; static namespaced = true; static apiBase = "/api/v1/pods"; - spec: { + constructor(data: KubeJsonApiData) { + super(data); + autoBind(this); + } + + declare spec: { volumes?: { name: string; persistentVolumeClaim: { @@ -265,7 +270,7 @@ export class Pod extends WorkloadKubeObject { }; affinity?: IAffinity; }; - status?: { + declare status?: { phase: string; conditions: { type: string; diff --git a/src/renderer/api/endpoints/podsecuritypolicy.api.ts b/src/renderer/api/endpoints/podsecuritypolicy.api.ts index 1244c3e13e..eac827ffcc 100644 --- a/src/renderer/api/endpoints/podsecuritypolicy.api.ts +++ b/src/renderer/api/endpoints/podsecuritypolicy.api.ts @@ -19,16 +19,12 @@ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -import { autobind } from "../../utils"; +import { autoBind } from "../../utils"; import { KubeObject } from "../kube-object"; import { KubeApi } from "../kube-api"; +import type { KubeJsonApiData } from "../kube-json-api"; -@autobind() -export class PodSecurityPolicy extends KubeObject { - static kind = "PodSecurityPolicy"; - static namespaced = false; - static apiBase = "/apis/policy/v1beta1/podsecuritypolicies"; - +export interface PodSecurityPolicy { spec: { allowPrivilegeEscalation?: boolean; allowedCSIDrivers?: { @@ -88,6 +84,17 @@ export class PodSecurityPolicy extends KubeObject { }; volumes?: string[]; }; +} + +export class PodSecurityPolicy extends KubeObject { + static kind = "PodSecurityPolicy"; + static namespaced = false; + static apiBase = "/apis/policy/v1beta1/podsecuritypolicies"; + + constructor(data: KubeJsonApiData) { + super(data); + autoBind(this); + } isPrivileged() { return !!this.spec.privileged; diff --git a/src/renderer/api/endpoints/replica-set.api.ts b/src/renderer/api/endpoints/replica-set.api.ts index cf79c85308..e2311c6b87 100644 --- a/src/renderer/api/endpoints/replica-set.api.ts +++ b/src/renderer/api/endpoints/replica-set.api.ts @@ -20,10 +20,11 @@ */ import get from "lodash/get"; -import { autobind } from "../../utils"; +import { autoBind } from "../../utils"; import { WorkloadKubeObject } from "../workload-kube-object"; import type { IPodContainer, Pod } from "./pods.api"; import { KubeApi } from "../kube-api"; +import type { KubeJsonApiData } from "../kube-json-api"; export class ReplicaSetApi extends KubeApi { protected getScaleApiUrl(params: { namespace: string; name: string }) { @@ -48,12 +49,17 @@ export class ReplicaSetApi extends KubeApi { } } -@autobind() export class ReplicaSet extends WorkloadKubeObject { static kind = "ReplicaSet"; static namespaced = true; static apiBase = "/apis/apps/v1/replicasets"; - spec: { + + constructor(data: KubeJsonApiData) { + super(data); + autoBind(this); + } + + declare spec: { replicas?: number; selector: { matchLabels: { [app: string]: string } }; template?: { @@ -66,7 +72,7 @@ export class ReplicaSet extends WorkloadKubeObject { }; minReadySeconds?: number; }; - status: { + declare status: { replicas: number; fullyLabeledReplicas?: number; readyReplicas?: number; diff --git a/src/renderer/api/endpoints/resource-quota.api.ts b/src/renderer/api/endpoints/resource-quota.api.ts index 4675299a39..762c6682e1 100644 --- a/src/renderer/api/endpoints/resource-quota.api.ts +++ b/src/renderer/api/endpoints/resource-quota.api.ts @@ -21,7 +21,6 @@ import { KubeObject } from "../kube-object"; import { KubeApi } from "../kube-api"; -import type { KubeJsonApiData } from "../kube-json-api"; export interface IResourceQuotaValues { [quota: string]: string; @@ -51,16 +50,7 @@ export interface IResourceQuotaValues { "count/deployments.extensions"?: string; } -export class ResourceQuota extends KubeObject { - static kind = "ResourceQuota"; - static namespaced = true; - static apiBase = "/api/v1/resourcequotas"; - - constructor(data: KubeJsonApiData) { - super(data); - this.spec = this.spec || {} as any; - } - +export interface ResourceQuota { spec: { hard: IResourceQuotaValues; scopeSelector?: { @@ -76,6 +66,12 @@ export class ResourceQuota extends KubeObject { hard: IResourceQuotaValues; used: IResourceQuotaValues; }; +} + +export class ResourceQuota extends KubeObject { + static kind = "ResourceQuota"; + static namespaced = true; + static apiBase = "/api/v1/resourcequotas"; getScopeSelector() { const { matchExpressions = [] } = this.spec.scopeSelector || {}; diff --git a/src/renderer/api/endpoints/role-binding.api.ts b/src/renderer/api/endpoints/role-binding.api.ts index 6fdd27e180..4e53752df1 100644 --- a/src/renderer/api/endpoints/role-binding.api.ts +++ b/src/renderer/api/endpoints/role-binding.api.ts @@ -19,9 +19,10 @@ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -import { autobind } from "../../utils"; +import { autoBind } from "../../utils"; import { KubeObject } from "../kube-object"; import { KubeApi } from "../kube-api"; +import type { KubeJsonApiData } from "../kube-json-api"; export interface IRoleBindingSubject { kind: string; @@ -30,18 +31,24 @@ export interface IRoleBindingSubject { apiGroup?: string; } -@autobind() -export class RoleBinding extends KubeObject { - static kind = "RoleBinding"; - static namespaced = true; - static apiBase = "/apis/rbac.authorization.k8s.io/v1/rolebindings"; - +export interface RoleBinding { subjects?: IRoleBindingSubject[]; roleRef: { kind: string; name: string; apiGroup?: string; }; +} + +export class RoleBinding extends KubeObject { + static kind = "RoleBinding"; + static namespaced = true; + static apiBase = "/apis/rbac.authorization.k8s.io/v1/rolebindings"; + + constructor(data: KubeJsonApiData) { + super(data); + autoBind(this); + } getSubjects() { return this.subjects || []; diff --git a/src/renderer/api/endpoints/role.api.ts b/src/renderer/api/endpoints/role.api.ts index 55c3bbe648..b504c7fc8b 100644 --- a/src/renderer/api/endpoints/role.api.ts +++ b/src/renderer/api/endpoints/role.api.ts @@ -22,17 +22,19 @@ import { KubeObject } from "../kube-object"; import { KubeApi } from "../kube-api"; -export class Role extends KubeObject { - static kind = "Role"; - static namespaced = true; - static apiBase = "/apis/rbac.authorization.k8s.io/v1/roles"; - +export interface Role { rules: { verbs: string[]; apiGroups: string[]; resources: string[]; resourceNames?: string[]; }[]; +} + +export class Role extends KubeObject { + static kind = "Role"; + static namespaced = true; + static apiBase = "/apis/rbac.authorization.k8s.io/v1/roles"; getRules() { return this.rules || []; diff --git a/src/renderer/api/endpoints/secret.api.ts b/src/renderer/api/endpoints/secret.api.ts index b51f467430..5ddd0e1db3 100644 --- a/src/renderer/api/endpoints/secret.api.ts +++ b/src/renderer/api/endpoints/secret.api.ts @@ -21,7 +21,7 @@ import { KubeObject } from "../kube-object"; import type { KubeJsonApiData } from "../kube-json-api"; -import { autobind } from "../../utils"; +import { autoBind } from "../../utils"; import { KubeApi } from "../kube-api"; export enum SecretType { @@ -40,21 +40,24 @@ export interface ISecretRef { name: string; } -@autobind() -export class Secret extends KubeObject { - static kind = "Secret"; - static namespaced = true; - static apiBase = "/api/v1/secrets"; - +export interface Secret { type: SecretType; data: { [prop: string]: string; token?: string; }; +} + +export class Secret extends KubeObject { + static kind = "Secret"; + static namespaced = true; + static apiBase = "/api/v1/secrets"; constructor(data: KubeJsonApiData) { super(data); - this.data = this.data || {}; + autoBind(this); + + this.data ??= {}; } getKeys(): string[] { diff --git a/src/renderer/api/endpoints/selfsubjectrulesreviews.api.ts b/src/renderer/api/endpoints/selfsubjectrulesreviews.api.ts index d008250f9d..84c27f21cb 100644 --- a/src/renderer/api/endpoints/selfsubjectrulesreviews.api.ts +++ b/src/renderer/api/endpoints/selfsubjectrulesreviews.api.ts @@ -28,8 +28,7 @@ export class SelfSubjectRulesReviewApi extends KubeApi { spec: { namespace }, - } - ); + }); } } @@ -41,21 +40,21 @@ export interface ISelfSubjectReviewRule { nonResourceURLs?: string[]; } -export class SelfSubjectRulesReview extends KubeObject { - static kind = "SelfSubjectRulesReview"; - static namespaced = false; - static apiBase = "/apis/authorization.k8s.io/v1/selfsubjectrulesreviews"; - +export interface SelfSubjectRulesReview { spec: { - // todo: add more types from api docs namespace?: string; }; - status: { resourceRules: ISelfSubjectReviewRule[]; nonResourceRules: ISelfSubjectReviewRule[]; incomplete: boolean; }; +} + +export class SelfSubjectRulesReview extends KubeObject { + static kind = "SelfSubjectRulesReview"; + static namespaced = false; + static apiBase = "/apis/authorization.k8s.io/v1/selfsubjectrulesreviews"; getResourceRules() { const rules = this.status && this.status.resourceRules || []; diff --git a/src/renderer/api/endpoints/service-accounts.api.ts b/src/renderer/api/endpoints/service-accounts.api.ts index df3d015a34..6cc9d292ba 100644 --- a/src/renderer/api/endpoints/service-accounts.api.ts +++ b/src/renderer/api/endpoints/service-accounts.api.ts @@ -19,22 +19,29 @@ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -import { autobind } from "../../utils"; +import { autoBind } from "../../utils"; import { KubeObject } from "../kube-object"; import { KubeApi } from "../kube-api"; +import type { KubeJsonApiData } from "../kube-json-api"; -@autobind() -export class ServiceAccount extends KubeObject { - static kind = "ServiceAccount"; - static namespaced = true; - static apiBase = "/api/v1/serviceaccounts"; - +export interface ServiceAccount { secrets?: { name: string; }[]; imagePullSecrets?: { name: string; }[]; +} + +export class ServiceAccount extends KubeObject { + static kind = "ServiceAccount"; + static namespaced = true; + static apiBase = "/api/v1/serviceaccounts"; + + constructor(data: KubeJsonApiData) { + super(data); + autoBind(this); + } getSecrets() { return this.secrets || []; diff --git a/src/renderer/api/endpoints/service.api.ts b/src/renderer/api/endpoints/service.api.ts index 99c25f9fe7..d1833f349d 100644 --- a/src/renderer/api/endpoints/service.api.ts +++ b/src/renderer/api/endpoints/service.api.ts @@ -19,25 +19,21 @@ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -import { autobind } from "../../utils"; +import { autoBind } from "../../utils"; import { KubeObject } from "../kube-object"; import { KubeApi } from "../kube-api"; +import type { KubeJsonApiData } from "../kube-json-api"; -export interface IServicePort { - name?: string; - protocol: string; - port: number; - targetPort: number; -} - -export class ServicePort implements IServicePort { +export interface ServicePort { name?: string; protocol: string; port: number; targetPort: number; nodePort?: number; +} - constructor(data: IServicePort) { +export class ServicePort { + constructor(data: ServicePort) { Object.assign(this, data); } @@ -50,12 +46,7 @@ export class ServicePort implements IServicePort { } } -@autobind() -export class Service extends KubeObject { - static kind = "Service"; - static namespaced = true; - static apiBase = "/api/v1/services"; - +export interface Service { spec: { type: string; clusterIP: string; @@ -75,6 +66,17 @@ export class Service extends KubeObject { }[]; }; }; +} + +export class Service extends KubeObject { + static kind = "Service"; + static namespaced = true; + static apiBase = "/api/v1/services"; + + constructor(data: KubeJsonApiData) { + super(data); + autoBind(this); + } getClusterIp() { return this.spec.clusterIP; diff --git a/src/renderer/api/endpoints/stateful-set.api.ts b/src/renderer/api/endpoints/stateful-set.api.ts index cdbeb5a01a..b91e06c25c 100644 --- a/src/renderer/api/endpoints/stateful-set.api.ts +++ b/src/renderer/api/endpoints/stateful-set.api.ts @@ -22,8 +22,9 @@ import get from "lodash/get"; import type { IPodContainer } from "./pods.api"; import { IAffinity, WorkloadKubeObject } from "../workload-kube-object"; -import { autobind } from "../../utils"; +import { autoBind } from "../../utils"; import { KubeApi } from "../kube-api"; +import type { KubeJsonApiData } from "../kube-json-api"; export class StatefulSetApi extends KubeApi { protected getScaleApiUrl(params: { namespace: string; name: string }) { @@ -48,13 +49,17 @@ export class StatefulSetApi extends KubeApi { } } -@autobind() export class StatefulSet extends WorkloadKubeObject { static kind = "StatefulSet"; static namespaced = true; static apiBase = "/apis/apps/v1/statefulsets"; - spec: { + constructor(data: KubeJsonApiData) { + super(data); + autoBind(this); + } + + declare spec: { serviceName: string; replicas: number; selector: { @@ -107,7 +112,7 @@ export class StatefulSet extends WorkloadKubeObject { }; }[]; }; - status: { + declare status: { observedGeneration: number; replicas: number; currentReplicas: number; diff --git a/src/renderer/api/endpoints/storage-class.api.ts b/src/renderer/api/endpoints/storage-class.api.ts index 6953879413..226e3ab477 100644 --- a/src/renderer/api/endpoints/storage-class.api.ts +++ b/src/renderer/api/endpoints/storage-class.api.ts @@ -19,16 +19,12 @@ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -import { autobind } from "../../utils"; +import { autoBind } from "../../utils"; import { KubeObject } from "../kube-object"; import { KubeApi } from "../kube-api"; +import type { KubeJsonApiData } from "../kube-json-api"; -@autobind() -export class StorageClass extends KubeObject { - static kind = "StorageClass"; - static namespaced = false; - static apiBase = "/apis/storage.k8s.io/v1/storageclasses"; - +export interface StorageClass { provisioner: string; // e.g. "storage.k8s.io/v1" mountOptions?: string[]; volumeBindingMode: string; @@ -36,6 +32,17 @@ export class StorageClass extends KubeObject { parameters: { [param: string]: string; // every provisioner has own set of these parameters }; +} + +export class StorageClass extends KubeObject { + static kind = "StorageClass"; + static namespaced = false; + static apiBase = "/apis/storage.k8s.io/v1/storageclasses"; + + constructor(data: KubeJsonApiData) { + super(data); + autoBind(this); + } isDefault() { const annotations = this.metadata.annotations || {}; diff --git a/src/renderer/api/kube-object.ts b/src/renderer/api/kube-object.ts index 06c431851c..7b5b51cffc 100644 --- a/src/renderer/api/kube-object.ts +++ b/src/renderer/api/kube-object.ts @@ -23,7 +23,7 @@ import moment from "moment"; import type { KubeJsonApiData, KubeJsonApiDataList, KubeJsonApiListMetadata, KubeJsonApiMetadata } from "./kube-json-api"; -import { autobind, formatDuration } from "../utils"; +import { autoBind, formatDuration } from "../utils"; import type { ItemObject } from "../item.store"; import { apiKube } from "./index"; import type { JsonApiParams } from "./json-api"; @@ -87,11 +87,16 @@ export class KubeStatus { export type IKubeMetaField = keyof IKubeObjectMetadata; -@autobind() -export class KubeObject implements ItemObject { +export class KubeObject implements ItemObject { static readonly kind: string; static readonly namespaced: boolean; + apiVersion: string; + kind: string; + metadata: Metadata; + status?: Status; + spec?: Spec; + static create(data: any) { return new KubeObject(data); } @@ -176,13 +181,9 @@ export class KubeObject implements ItemObject { constructor(data: KubeJsonApiData) { Object.assign(this, data); + autoBind(this); } - apiVersion: string; - kind: string; - metadata: IKubeObjectMetadata; - status?: any; // todo: type-safety support - get selfLink() { return this.metadata.selfLink; } @@ -265,7 +266,7 @@ export class KubeObject implements ItemObject { } // use unified resource-applier api for updating all k8s objects - async update(data: Partial) { + async update(data: Partial): Promise { return resourceApplierApi.update({ ...this.toPlainObject(), ...data, diff --git a/src/renderer/api/kube-watch-api.ts b/src/renderer/api/kube-watch-api.ts index 4c68e0fdfa..f8ed7129b1 100644 --- a/src/renderer/api/kube-watch-api.ts +++ b/src/renderer/api/kube-watch-api.ts @@ -26,8 +26,8 @@ import type { KubeObjectStore } from "../kube-object.store"; import type { ClusterContext } from "../components/context"; import plimit from "p-limit"; -import { comparer, IReactionDisposer, observable, reaction, when } from "mobx"; -import { autobind, noop } from "../utils"; +import { comparer, IReactionDisposer, observable, reaction, makeObservable } from "mobx"; +import { autoBind, noop } from "../utils"; import type { KubeApi } from "./kube-api"; import type { KubeJsonApiData } from "./kube-json-api"; import { isDebugging, isProduction } from "../../common/vars"; @@ -50,11 +50,13 @@ export interface IKubeWatchLog { cssStyle?: string; } -@autobind() export class KubeWatchApi { @observable context: ClusterContext = null; - contextReady = when(() => Boolean(this.context)); + constructor() { + makeObservable(this); + autoBind(this); + } isAllowedApi(api: KubeApi): boolean { return Boolean(this.context?.cluster.isAllowedResource(api.kind)); diff --git a/src/renderer/api/terminal-api.ts b/src/renderer/api/terminal-api.ts index 2720fddf51..3b2c8a1cd9 100644 --- a/src/renderer/api/terminal-api.ts +++ b/src/renderer/api/terminal-api.ts @@ -20,7 +20,7 @@ */ import { stringify } from "querystring"; -import { autobind, base64, EventEmitter } from "../utils"; +import { boundMethod, base64, EventEmitter } from "../utils"; import { WebSocketApi } from "./websocket-api"; import isEqual from "lodash/isEqual"; import { isDevelopment } from "../../common/vars"; @@ -106,7 +106,7 @@ export class TerminalApi extends WebSocketApi { this.onReady.removeAllListeners(); } - @autobind() + @boundMethod protected _onReady(data: string) { if (!data) return true; this.isReady = true; diff --git a/src/renderer/api/websocket-api.ts b/src/renderer/api/websocket-api.ts index 27fb8ea264..99917f2f30 100644 --- a/src/renderer/api/websocket-api.ts +++ b/src/renderer/api/websocket-api.ts @@ -19,7 +19,7 @@ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -import { observable } from "mobx"; +import { observable, makeObservable } from "mobx"; import { EventEmitter } from "../../common/event-emitter"; interface IParams { @@ -66,6 +66,7 @@ export class WebSocketApi { }; constructor(protected params: IParams) { + makeObservable(this); this.params = Object.assign({}, WebSocketApi.defaultParams, params); const { autoConnect, pingIntervalSeconds } = this.params; diff --git a/src/renderer/api/workload-kube-object.ts b/src/renderer/api/workload-kube-object.ts index 2510f32f3e..34fa5d5109 100644 --- a/src/renderer/api/workload-kube-object.ts +++ b/src/renderer/api/workload-kube-object.ts @@ -68,8 +68,6 @@ export interface IAffinity { } export class WorkloadKubeObject extends KubeObject { - spec: any; // todo: add proper types - getSelectors(): string[] { const selector = this.spec.selector; diff --git a/src/renderer/bootstrap.tsx b/src/renderer/bootstrap.tsx index 62430ac788..38396cc608 100644 --- a/src/renderer/bootstrap.tsx +++ b/src/renderer/bootstrap.tsx @@ -26,13 +26,14 @@ import * as Mobx from "mobx"; import * as MobxReact from "mobx-react"; import * as ReactRouter from "react-router"; import * as ReactRouterDom from "react-router-dom"; +import * as LensExtensionsCoreApi from "../extensions/core-api"; +import * as LensExtensionsRendererApi from "../extensions/renderer-api"; import { render, unmountComponentAtNode } from "react-dom"; import { delay } from "../common/utils"; import { isMac, isDevelopment } from "../common/vars"; import { HotbarStore } from "../common/hotbar-store"; import { ClusterStore } from "../common/cluster-store"; import { UserStore } from "../common/user-store"; -import * as LensExtensions from "../extensions/extension-api"; import { ExtensionDiscovery } from "../extensions/extension-discovery"; import { ExtensionLoader } from "../extensions/extension-loader"; import { ExtensionsStore } from "../extensions/extensions-store"; @@ -43,6 +44,9 @@ import { ThemeStore } from "./theme.store"; import { HelmRepoManager } from "../main/helm/helm-repo-manager"; import { ExtensionInstallationStateStore } from "./components/+extensions/extension-install.store"; import { DefaultProps } from "./mui-base-theme"; +import configurePackages from "../common/configure-packages"; + +configurePackages(); /** * If this is a development buid, wait a second to attach @@ -59,15 +63,6 @@ type AppComponent = React.ComponentType & { init?(): Promise; }; -export { - React, - ReactRouter, - ReactRouterDom, - Mobx, - MobxReact, - LensExtensions -}; - export async function bootstrap(App: AppComponent) { const rootElem = document.getElementById("app"); @@ -120,3 +115,23 @@ export async function bootstrap(App: AppComponent) { // run bootstrap(process.isMainFrame ? LensApp : App); + + +/** + * Exports for virtual package "@k8slens/extensions" for renderer-process. + * All exporting names available in global runtime scope: + * e.g. Devtools -> Console -> window.LensExtensions (renderer) + */ +const LensExtensions = { + ...LensExtensionsCoreApi, + ...LensExtensionsRendererApi, +}; + +export { + React, + ReactRouter, + ReactRouterDom, + Mobx, + MobxReact, + LensExtensions, +}; diff --git a/src/renderer/components/+add-cluster/add-cluster.tsx b/src/renderer/components/+add-cluster/add-cluster.tsx index 7f706e9198..9c91550d47 100644 --- a/src/renderer/components/+add-cluster/add-cluster.tsx +++ b/src/renderer/components/+add-cluster/add-cluster.tsx @@ -22,7 +22,7 @@ import "./add-cluster.scss"; import React from "react"; import { observer } from "mobx-react"; -import { action, observable, runInAction } from "mobx"; +import { action, observable, runInAction, makeObservable } from "mobx"; import { KubeConfig } from "@kubernetes/client-node"; import { AceEditor } from "../ace-editor"; import { Button } from "../button"; @@ -50,6 +50,11 @@ export class AddCluster extends React.Component { kubeContexts = observable.map(); + constructor(props: {}) { + super(props); + makeObservable(this); + } + componentDidMount() { appEventBus.emit({ name: "cluster-add", action: "start" }); } diff --git a/src/renderer/components/+apps-helm-charts/helm-chart-details.tsx b/src/renderer/components/+apps-helm-charts/helm-chart-details.tsx index 380f0eaa4a..963df018af 100644 --- a/src/renderer/components/+apps-helm-charts/helm-chart-details.tsx +++ b/src/renderer/components/+apps-helm-charts/helm-chart-details.tsx @@ -23,10 +23,10 @@ import "./helm-chart-details.scss"; import React, { Component } from "react"; import { getChartDetails, HelmChart } from "../../api/endpoints/helm-charts.api"; -import { observable, autorun } from "mobx"; +import { observable, autorun, makeObservable } from "mobx"; import { observer } from "mobx-react"; import { Drawer, DrawerItem } from "../drawer"; -import { autobind, stopPropagation } from "../../utils"; +import { boundMethod, stopPropagation } from "../../utils"; import { MarkdownViewer } from "../markdown-viewer"; import { Spinner } from "../spinner"; import { Button } from "../button"; @@ -48,6 +48,11 @@ export class HelmChartDetails extends Component { private abortController?: AbortController; + constructor(props: Props) { + super(props); + makeObservable(this); + } + componentWillUnmount() { this.abortController?.abort(); } @@ -67,7 +72,7 @@ export class HelmChartDetails extends Component { }); }); - @autobind() + @boundMethod async onVersionChange({ value: version }: SelectOption) { this.selectedChart = this.chartVersions.find(chart => chart.version === version); this.readme = null; @@ -84,7 +89,7 @@ export class HelmChartDetails extends Component { } } - @autobind() + @boundMethod install() { createInstallChartTab(this.selectedChart); this.props.hideDetails(); diff --git a/src/renderer/components/+apps-helm-charts/helm-chart.store.ts b/src/renderer/components/+apps-helm-charts/helm-chart.store.ts index aa060e9e4b..c07dfaa886 100644 --- a/src/renderer/components/+apps-helm-charts/helm-chart.store.ts +++ b/src/renderer/components/+apps-helm-charts/helm-chart.store.ts @@ -20,8 +20,8 @@ */ import semver from "semver"; -import { observable } from "mobx"; -import { autobind } from "../../utils"; +import { observable, makeObservable } from "mobx"; +import { autoBind } from "../../utils"; import { getChartDetails, HelmChart, listCharts } from "../../api/endpoints/helm-charts.api"; import { ItemStore } from "../../item.store"; import flatten from "lodash/flatten"; @@ -31,10 +31,16 @@ export interface IChartVersion { version: string; } -@autobind() export class HelmChartStore extends ItemStore { @observable versions = observable.map(); + constructor() { + super(); + + makeObservable(this); + autoBind(this); + } + async loadAll() { try { const res = await this.loadItems(() => listCharts()); diff --git a/src/renderer/components/+apps-helm-charts/helm-charts.tsx b/src/renderer/components/+apps-helm-charts/helm-charts.tsx index 5e451e33e5..cce8090818 100644 --- a/src/renderer/components/+apps-helm-charts/helm-charts.tsx +++ b/src/renderer/components/+apps-helm-charts/helm-charts.tsx @@ -57,10 +57,10 @@ export class HelmCharts extends Component { showDetails = (chart: HelmChart) => { if (!chart) { - navigation.merge(helmChartsURL()); + navigation.push(helmChartsURL()); } else { - navigation.merge(helmChartsURL({ + navigation.push(helmChartsURL({ params: { chartName: chart.getName(), repo: chart.getRepository(), diff --git a/src/renderer/components/+apps-releases/release-details.tsx b/src/renderer/components/+apps-releases/release-details.tsx index 260611e420..2e7eca5e1a 100644 --- a/src/renderer/components/+apps-releases/release-details.tsx +++ b/src/renderer/components/+apps-releases/release-details.tsx @@ -24,7 +24,7 @@ import "./release-details.scss"; import React, { Component } from "react"; import groupBy from "lodash/groupBy"; import isEqual from "lodash/isEqual"; -import { observable, reaction } from "mobx"; +import { observable, reaction, makeObservable } from "mobx"; import { Link } from "react-router-dom"; import kebabCase from "lodash/kebabCase"; import { getRelease, getReleaseValues, HelmRelease, IReleaseDetails } from "../../api/endpoints/helm-releases.api"; @@ -72,7 +72,7 @@ export class ReleaseDetails extends Component { ); @disposeOnUnmount - secretWatcher = reaction(() => secretsStore.items.toJS(), () => { + secretWatcher = reaction(() => secretsStore.getItems(), () => { if (!this.props.release) return; const { getReleaseSecret } = releaseStore; const { release } = this.props; @@ -85,6 +85,11 @@ export class ReleaseDetails extends Component { this.releaseSecret = secret; }); + constructor(props: Props) { + super(props); + makeObservable(this); + } + async loadDetails() { const { release } = this.props; diff --git a/src/renderer/components/+apps-releases/release-menu.tsx b/src/renderer/components/+apps-releases/release-menu.tsx index 78e4c8e94b..2e85c40c05 100644 --- a/src/renderer/components/+apps-releases/release-menu.tsx +++ b/src/renderer/components/+apps-releases/release-menu.tsx @@ -21,7 +21,7 @@ import React from "react"; import type { HelmRelease } from "../../api/endpoints/helm-releases.api"; -import { autobind, cssNames } from "../../utils"; +import { boundMethod, cssNames } from "../../utils"; import { releaseStore } from "./release.store"; import { MenuActions, MenuActionsProps } from "../menu/menu-actions"; import { MenuItem } from "../menu"; @@ -35,12 +35,12 @@ interface Props extends MenuActionsProps { } export class HelmReleaseMenu extends React.Component { - @autobind() + @boundMethod remove() { return releaseStore.remove(this.props.release); } - @autobind() + @boundMethod upgrade() { const { release, hideDetails } = this.props; @@ -48,7 +48,7 @@ export class HelmReleaseMenu extends React.Component { hideDetails && hideDetails(); } - @autobind() + @boundMethod rollback() { ReleaseRollbackDialog.open(this.props.release); } diff --git a/src/renderer/components/+apps-releases/release-rollback-dialog.tsx b/src/renderer/components/+apps-releases/release-rollback-dialog.tsx index bb25f6f5d6..d8ca8c390a 100644 --- a/src/renderer/components/+apps-releases/release-rollback-dialog.tsx +++ b/src/renderer/components/+apps-releases/release-rollback-dialog.tsx @@ -22,7 +22,7 @@ import "./release-rollback-dialog.scss"; import React from "react"; -import { observable } from "mobx"; +import { observable, makeObservable } from "mobx"; import { observer } from "mobx-react"; import { Dialog, DialogProps } from "../dialog"; import { Wizard, WizardStep } from "../wizard"; @@ -35,26 +35,33 @@ import orderBy from "lodash/orderBy"; interface Props extends DialogProps { } +const dialogState = observable.object({ + isOpen: false, + release: null as HelmRelease, +}); + @observer export class ReleaseRollbackDialog extends React.Component { - @observable static isOpen = false; - @observable.ref static release: HelmRelease = null; - @observable isLoading = false; @observable revision: IReleaseRevision; @observable revisions = observable.array(); + constructor(props: Props) { + super(props); + makeObservable(this); + } + static open(release: HelmRelease) { - ReleaseRollbackDialog.isOpen = true; - ReleaseRollbackDialog.release = release; + dialogState.isOpen = true; + dialogState.release = release; } static close() { - ReleaseRollbackDialog.isOpen = false; + dialogState.isOpen = false; } get release(): HelmRelease { - return ReleaseRollbackDialog.release; + return dialogState.release; } onOpen = async () => { @@ -115,7 +122,7 @@ export class ReleaseRollbackDialog extends React.Component {

diff --git a/src/renderer/components/+apps-releases/release.store.ts b/src/renderer/components/+apps-releases/release.store.ts index 7ed2ac42c7..ae69d38bc1 100644 --- a/src/renderer/components/+apps-releases/release.store.ts +++ b/src/renderer/components/+apps-releases/release.store.ts @@ -20,8 +20,8 @@ */ import isEqual from "lodash/isEqual"; -import { action, observable, reaction, when } from "mobx"; -import { autobind } from "../../utils"; +import { action, observable, reaction, when, makeObservable } from "mobx"; +import { autoBind } from "../../utils"; import { createRelease, deleteRelease, HelmRelease, IReleaseCreatePayload, IReleaseUpdatePayload, listReleases, rollbackRelease, updateRelease } from "../../api/endpoints/helm-releases.api"; import { ItemStore } from "../../item.store"; import type { Secret } from "../../api/endpoints"; @@ -29,19 +29,21 @@ import { secretsStore } from "../+config-secrets/secrets.store"; import { namespaceStore } from "../+namespaces/namespace.store"; import { Notifications } from "../notifications"; -@autobind() export class ReleaseStore extends ItemStore { releaseSecrets = observable.map(); constructor() { super(); + makeObservable(this); + autoBind(this); + when(() => secretsStore.isLoaded, () => { this.releaseSecrets.replace(this.getReleaseSecrets()); }); } watchAssociatedSecrets(): (() => void) { - return reaction(() => secretsStore.items.toJS(), () => { + return reaction(() => secretsStore.getItems(), () => { if (this.isLoading) return; const newSecrets = this.getReleaseSecrets(); const amountChanged = newSecrets.length !== this.releaseSecrets.size; diff --git a/src/renderer/components/+apps-releases/releases.tsx b/src/renderer/components/+apps-releases/releases.tsx index bbf3944554..7465ad16b5 100644 --- a/src/renderer/components/+apps-releases/releases.tsx +++ b/src/renderer/components/+apps-releases/releases.tsx @@ -67,7 +67,7 @@ export class HelmReleases extends Component { } showDetails = (item: HelmRelease) => { - navigation.merge(releaseURL({ + navigation.push(releaseURL({ params: { name: item.getName(), namespace: item.getNs() @@ -76,7 +76,7 @@ export class HelmReleases extends Component { }; hideDetails = () => { - navigation.merge(releaseURL()); + navigation.push(releaseURL()); }; renderRemoveDialogMessage(selectedItems: HelmRelease[]) { diff --git a/src/renderer/components/+apps/apps.tsx b/src/renderer/components/+apps/apps.tsx index 1e9ee3623a..3bdac50ce4 100644 --- a/src/renderer/components/+apps/apps.tsx +++ b/src/renderer/components/+apps/apps.tsx @@ -24,12 +24,10 @@ import { observer } from "mobx-react"; import { TabLayout, TabLayoutRoute } from "../layout/tab-layout"; import { HelmCharts, helmChartsRoute, helmChartsURL } from "../+apps-helm-charts"; import { HelmReleases, releaseRoute, releaseURL } from "../+apps-releases"; -import { namespaceUrlParam } from "../+namespaces/namespace.store"; @observer export class Apps extends React.Component { static get tabRoutes(): TabLayoutRoute[] { - const query = namespaceUrlParam.toObjectParam(); return [ { @@ -41,7 +39,7 @@ export class Apps extends React.Component { { title: "Releases", component: HelmReleases, - url: releaseURL({ query }), + url: releaseURL(), routePath: releaseRoute.path.toString(), }, ]; diff --git a/src/renderer/components/+catalog/catalog-add-button.tsx b/src/renderer/components/+catalog/catalog-add-button.tsx index af6b3ebbbb..c839f8dc26 100644 --- a/src/renderer/components/+catalog/catalog-add-button.tsx +++ b/src/renderer/components/+catalog/catalog-add-button.tsx @@ -24,8 +24,8 @@ import React from "react"; import { SpeedDial, SpeedDialAction } from "@material-ui/lab"; import { Icon } from "../icon"; import { disposeOnUnmount, observer } from "mobx-react"; -import { observable, reaction } from "mobx"; -import { autobind } from "../../../common/utils"; +import { observable, reaction, makeObservable } from "mobx"; +import { boundMethod } from "../../../common/utils"; import type { CatalogCategory, CatalogEntityAddMenuContext, CatalogEntityAddMenu } from "../../api/catalog-entity"; import { EventEmitter } from "events"; import { navigate } from "../../navigation"; @@ -39,6 +39,11 @@ export class CatalogAddButton extends React.Component { @observable protected isOpen = false; protected menuItems = observable.array([]); + constructor(props: CatalogAddButtonProps) { + super(props); + makeObservable(this); + } + componentDidMount() { disposeOnUnmount(this, [ reaction(() => this.props.category, (category) => { @@ -56,17 +61,17 @@ export class CatalogAddButton extends React.Component { ]); } - @autobind() + @boundMethod onOpen() { this.isOpen = true; } - @autobind() + @boundMethod onClose() { this.isOpen = false; } - @autobind() + @boundMethod onButtonClick() { if (this.menuItems.length == 1) { this.menuItems[0].onClick(); diff --git a/src/renderer/components/+catalog/catalog-entity.store.ts b/src/renderer/components/+catalog/catalog-entity.store.ts index db596fe937..8737b9873d 100644 --- a/src/renderer/components/+catalog/catalog-entity.store.ts +++ b/src/renderer/components/+catalog/catalog-entity.store.ts @@ -19,13 +19,12 @@ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -import { action, computed, IReactionDisposer, observable, reaction } from "mobx"; +import { action, computed, IReactionDisposer, makeObservable, observable, reaction } from "mobx"; import { catalogEntityRegistry } from "../../api/catalog-entity-registry"; import type { CatalogEntity, CatalogEntityActionContext } from "../../api/catalog-entity"; import { ItemObject, ItemStore } from "../../item.store"; -import { autobind } from "../../utils"; import { CatalogCategory, catalogCategoryRegistry } from "../../../common/catalog"; - +import { autoBind } from "../../../common/utils"; export class CatalogEntityItem implements ItemObject { constructor(public entity: CatalogEntity) {} @@ -84,8 +83,13 @@ export class CatalogEntityItem implements ItemObject { } } -@autobind() export class CatalogEntityStore extends ItemStore { + constructor() { + super(); + makeObservable(this); + autoBind(this); + } + @observable activeCategory?: CatalogCategory; @computed get entities() { diff --git a/src/renderer/components/+catalog/catalog.scss b/src/renderer/components/+catalog/catalog.scss index 5f8131dfaa..e76606567b 100644 --- a/src/renderer/components/+catalog/catalog.scss +++ b/src/renderer/components/+catalog/catalog.scss @@ -69,7 +69,18 @@ } } + .TableCell.icon { + max-width: 40px; + display: flex; + align-items: center; + } + + .TableCell.source { + max-width: 100px; + } + .TableCell.status { + max-width: 100px; &.connected { color: var(--colorSuccess); } @@ -97,4 +108,9 @@ } } } + + .catalogIcon { + font-size: 10px; + -webkit-font-smoothing: auto; + } } diff --git a/src/renderer/components/+catalog/catalog.tsx b/src/renderer/components/+catalog/catalog.tsx index 5211e034c2..cd04892745 100644 --- a/src/renderer/components/+catalog/catalog.tsx +++ b/src/renderer/components/+catalog/catalog.tsx @@ -23,7 +23,7 @@ import "./catalog.scss"; import React from "react"; import { disposeOnUnmount, observer } from "mobx-react"; import { ItemListLayout } from "../item-object-list"; -import { action, observable, reaction, when } from "mobx"; +import { action, makeObservable, observable, reaction, when } from "mobx"; import { CatalogEntityItem, CatalogEntityStore } from "./catalog-entity.store"; import { navigate } from "../../navigation"; import { kebabCase } from "lodash"; @@ -32,7 +32,6 @@ import { MenuItem, MenuActions } from "../menu"; import { CatalogEntityContextMenu, CatalogEntityContextMenuContext, catalogEntityRunContext } from "../../api/catalog-entity"; import { Badge } from "../badge"; import { HotbarStore } from "../../../common/hotbar-store"; -import { autobind } from "../../utils"; import { ConfirmDialog } from "../confirm-dialog"; import { Tab, Tabs } from "../tabs"; import { catalogCategoryRegistry } from "../../../common/catalog"; @@ -40,6 +39,7 @@ import { CatalogAddButton } from "./catalog-add-button"; import type { RouteComponentProps } from "react-router"; import type { ICatalogViewRouteParam } from "./catalog.route"; import { Notifications } from "../notifications"; +import { Avatar } from "../avatar/avatar"; enum sortBy { name = "name", @@ -48,12 +48,18 @@ enum sortBy { } interface Props extends RouteComponentProps {} + @observer export class Catalog extends React.Component { @observable private catalogEntityStore?: CatalogEntityStore; - @observable.deep private contextMenu: CatalogEntityContextMenuContext; + @observable private contextMenu: CatalogEntityContextMenuContext; @observable activeTab?: string; + constructor(props: Props) { + super(props); + makeObservable(this); + } + get routeActiveTab(): string | undefined { const { group, kind } = this.props.match.params ?? {}; @@ -154,8 +160,7 @@ export class Catalog extends React.Component { ); } - @autobind() - renderItemMenu(item: CatalogEntityItem) { + renderItemMenu = (item: CatalogEntityItem) => { const menuItems = this.contextMenu.menuItems.filter((menuItem) => !menuItem.onlyVisibleForSource || menuItem.onlyVisibleForSource === item.entity.metadata.source); return ( @@ -172,6 +177,24 @@ export class Catalog extends React.Component { ); + }; + + renderIcon(item: CatalogEntityItem) { + const category = catalogCategoryRegistry.getCategoryForEntity(item.entity); + + if (!category) { + return null; + } + + return ( + + ); } render() { @@ -202,12 +225,14 @@ export class Catalog extends React.Component { (entity: CatalogEntityItem) => entity.searchFields, ]} renderTableHeader={[ + { title: "", className: "icon" }, { title: "Name", className: "name", sortBy: sortBy.name }, { title: "Source", className: "source", sortBy: sortBy.source }, { title: "Labels", className: "labels" }, { title: "Status", className: "status", sortBy: sortBy.status }, ]} renderTableContents={(item: CatalogEntityItem) => [ + this.renderIcon(item), item.name, item.source, item.labels.map((label) => ), diff --git a/src/renderer/components/+cluster/cluster-issues.tsx b/src/renderer/components/+cluster/cluster-issues.tsx index ce6496f490..90a8a1a74b 100644 --- a/src/renderer/components/+cluster/cluster-issues.tsx +++ b/src/renderer/components/+cluster/cluster-issues.tsx @@ -23,13 +23,13 @@ import "./cluster-issues.scss"; import React from "react"; import { observer } from "mobx-react"; -import { computed } from "mobx"; +import { computed, makeObservable } from "mobx"; import { Icon } from "../icon"; import { SubHeader } from "../layout/sub-header"; import { Table, TableCell, TableHead, TableRow } from "../table"; import { nodesStore } from "../+nodes/nodes.store"; import { eventStore } from "../+events/event.store"; -import { autobind, cssNames, prevDefault } from "../../utils"; +import { boundMethod, cssNames, prevDefault } from "../../utils"; import type { ItemObject } from "../../item.store"; import { Spinner } from "../spinner"; import { ThemeStore } from "../../theme.store"; @@ -62,6 +62,11 @@ export class ClusterIssues extends React.Component { [sortBy.age]: (warning: IWarning) => warning.timeDiffFromNow, }; + constructor(props: Props) { + super(props); + makeObservable(this); + } + @computed get warnings() { const warnings: IWarning[] = []; @@ -103,7 +108,7 @@ export class ClusterIssues extends React.Component { return warnings; } - @autobind() + @boundMethod getTableRow(uid: string) { const { warnings } = this; const warning = warnings.find(warn => warn.getId() == uid); diff --git a/src/renderer/components/+cluster/cluster-overview.store.ts b/src/renderer/components/+cluster/cluster-overview.store.ts index c9ffc4008c..846f2466b4 100644 --- a/src/renderer/components/+cluster/cluster-overview.store.ts +++ b/src/renderer/components/+cluster/cluster-overview.store.ts @@ -19,10 +19,10 @@ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -import { action, observable, reaction, when } from "mobx"; +import { action, observable, reaction, when, makeObservable } from "mobx"; import { KubeObjectStore } from "../../kube-object.store"; import { Cluster, clusterApi, IClusterMetrics } from "../../api/endpoints"; -import { autobind, createStorage } from "../../utils"; +import { autoBind, createStorage } from "../../utils"; import { IMetricsReqParams, normalizeMetrics } from "../../api/endpoints/metrics.api"; import { nodesStore } from "../+nodes/nodes.store"; import { apiManager } from "../../api/api-manager"; @@ -42,7 +42,6 @@ export interface ClusterOverviewStorageState { metricNodeRole: MetricNodeRole, } -@autobind() export class ClusterOverviewStore extends KubeObjectStore implements ClusterOverviewStorageState { api = clusterApi; @@ -72,6 +71,9 @@ export class ClusterOverviewStore extends KubeObjectStore implements Cl constructor() { super(); + makeObservable(this); + autoBind(this); + this.init(); } diff --git a/src/renderer/components/+config-autoscalers/hpa.store.ts b/src/renderer/components/+config-autoscalers/hpa.store.ts index aa94a4a3cd..d92ffd3a6f 100644 --- a/src/renderer/components/+config-autoscalers/hpa.store.ts +++ b/src/renderer/components/+config-autoscalers/hpa.store.ts @@ -19,12 +19,10 @@ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -import { autobind } from "../../utils"; import { KubeObjectStore } from "../../kube-object.store"; import { HorizontalPodAutoscaler, hpaApi } from "../../api/endpoints/hpa.api"; import { apiManager } from "../../api/api-manager"; -@autobind() export class HPAStore extends KubeObjectStore { api = hpaApi; } diff --git a/src/renderer/components/+config-limit-ranges/limit-ranges.store.ts b/src/renderer/components/+config-limit-ranges/limit-ranges.store.ts index a6c7cc770c..af7ebe6b41 100644 --- a/src/renderer/components/+config-limit-ranges/limit-ranges.store.ts +++ b/src/renderer/components/+config-limit-ranges/limit-ranges.store.ts @@ -19,12 +19,10 @@ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -import { autobind } from "../../../common/utils/autobind"; import { KubeObjectStore } from "../../kube-object.store"; import { apiManager } from "../../api/api-manager"; import { LimitRange, limitRangeApi } from "../../api/endpoints/limit-range.api"; -@autobind() export class LimitRangesStore extends KubeObjectStore { api = limitRangeApi; } diff --git a/src/renderer/components/+config-maps/config-map-details.tsx b/src/renderer/components/+config-maps/config-map-details.tsx index b9dd05cfaa..928353ac6b 100644 --- a/src/renderer/components/+config-maps/config-map-details.tsx +++ b/src/renderer/components/+config-maps/config-map-details.tsx @@ -22,7 +22,7 @@ import "./config-map-details.scss"; import React from "react"; -import { autorun, observable } from "mobx"; +import { autorun, makeObservable, observable } from "mobx"; import { disposeOnUnmount, observer } from "mobx-react"; import { DrawerTitle } from "../drawer"; import { Notifications } from "../notifications"; @@ -41,7 +41,12 @@ interface Props extends KubeObjectDetailsProps { @observer export class ConfigMapDetails extends React.Component { @observable isSaving = false; - @observable data = observable.map(); + @observable data = observable.map(); + + constructor(props: Props) { + super(props); + makeObservable(this); + } async componentDidMount() { disposeOnUnmount(this, [ @@ -60,7 +65,10 @@ export class ConfigMapDetails extends React.Component { try { this.isSaving = true; - await configMapsStore.update(configMap, { ...configMap, data: this.data.toJSON() }); + await configMapsStore.update(configMap, { + ...configMap, + data: Object.fromEntries(this.data), + }); Notifications.ok(

<>ConfigMap {configMap.getName()} successfully updated. @@ -75,7 +83,7 @@ export class ConfigMapDetails extends React.Component { const { object: configMap } = this.props; if (!configMap) return null; - const data = Object.entries(this.data.toJSON()); + const data = Array.from(this.data.entries()); return (

diff --git a/src/renderer/components/+config-maps/config-maps.store.ts b/src/renderer/components/+config-maps/config-maps.store.ts index 51457c8c20..e45c9b3b27 100644 --- a/src/renderer/components/+config-maps/config-maps.store.ts +++ b/src/renderer/components/+config-maps/config-maps.store.ts @@ -20,11 +20,9 @@ */ import { KubeObjectStore } from "../../kube-object.store"; -import { autobind } from "../../utils"; import { ConfigMap, configMapApi } from "../../api/endpoints/configmap.api"; import { apiManager } from "../../api/api-manager"; -@autobind() export class ConfigMapsStore extends KubeObjectStore { api = configMapApi; } diff --git a/src/renderer/components/+config-pod-disruption-budgets/pod-disruption-budgets.store.ts b/src/renderer/components/+config-pod-disruption-budgets/pod-disruption-budgets.store.ts index d97593542d..b4a48fbef7 100644 --- a/src/renderer/components/+config-pod-disruption-budgets/pod-disruption-budgets.store.ts +++ b/src/renderer/components/+config-pod-disruption-budgets/pod-disruption-budgets.store.ts @@ -20,11 +20,9 @@ */ import { KubeObjectStore } from "../../kube-object.store"; -import { autobind } from "../../utils"; -import { PodDisruptionBudget, pdbApi } from "../../api/endpoints/poddisruptionbudget.api"; +import { pdbApi, PodDisruptionBudget } from "../../api/endpoints/poddisruptionbudget.api"; import { apiManager } from "../../api/api-manager"; -@autobind() export class PodDisruptionBudgetsStore extends KubeObjectStore { api = pdbApi; } diff --git a/src/renderer/components/+config-resource-quotas/add-quota-dialog.tsx b/src/renderer/components/+config-resource-quotas/add-quota-dialog.tsx index b7d2baa58d..1227e97049 100644 --- a/src/renderer/components/+config-resource-quotas/add-quota-dialog.tsx +++ b/src/renderer/components/+config-resource-quotas/add-quota-dialog.tsx @@ -22,7 +22,7 @@ import "./add-quota-dialog.scss"; import React from "react"; -import { computed, observable } from "mobx"; +import { computed, observable, makeObservable } from "mobx"; import { observer } from "mobx-react"; import { Dialog, DialogProps } from "../dialog"; import { Wizard, WizardStep } from "../wizard"; @@ -39,10 +39,12 @@ import { SubTitle } from "../layout/sub-title"; interface Props extends DialogProps { } +const dialogState = observable.object({ + isOpen: false, +}); + @observer export class AddQuotaDialog extends React.Component { - @observable static isOpen = false; - static defaultQuotas: IResourceQuotaValues = { "limits.cpu": "", "limits.memory": "", @@ -72,12 +74,17 @@ export class AddQuotaDialog extends React.Component { @observable namespace = this.defaultNamespace; @observable quotas = AddQuotaDialog.defaultQuotas; + constructor(props: Props) { + super(props); + makeObservable(this); + } + static open() { - AddQuotaDialog.isOpen = true; + dialogState.isOpen = true; } static close() { - AddQuotaDialog.isOpen = false; + dialogState.isOpen = false; } @computed get quotaEntries() { @@ -154,7 +161,7 @@ export class AddQuotaDialog extends React.Component { diff --git a/src/renderer/components/+config-resource-quotas/resource-quotas.store.ts b/src/renderer/components/+config-resource-quotas/resource-quotas.store.ts index 5d8907a0b0..5e7494b33a 100644 --- a/src/renderer/components/+config-resource-quotas/resource-quotas.store.ts +++ b/src/renderer/components/+config-resource-quotas/resource-quotas.store.ts @@ -20,11 +20,9 @@ */ import { KubeObjectStore } from "../../kube-object.store"; -import { autobind } from "../../utils"; import { ResourceQuota, resourceQuotaApi } from "../../api/endpoints/resource-quota.api"; import { apiManager } from "../../api/api-manager"; -@autobind() export class ResourceQuotasStore extends KubeObjectStore { api = resourceQuotaApi; } diff --git a/src/renderer/components/+config-secrets/add-secret-dialog.tsx b/src/renderer/components/+config-secrets/add-secret-dialog.tsx index f877fbe291..bfd7a3262e 100644 --- a/src/renderer/components/+config-secrets/add-secret-dialog.tsx +++ b/src/renderer/components/+config-secrets/add-secret-dialog.tsx @@ -22,7 +22,7 @@ import "./add-secret-dialog.scss"; import React from "react"; -import { observable } from "mobx"; +import { observable, makeObservable } from "mobx"; import { observer } from "mobx-react"; import { Dialog, DialogProps } from "../dialog"; import { Wizard, WizardStep } from "../wizard"; @@ -57,16 +57,23 @@ interface ISecretTemplate { type ISecretField = keyof ISecretTemplate; +const dialogState = observable.object({ + isOpen: false, +}); + @observer export class AddSecretDialog extends React.Component { - @observable static isOpen = false; + constructor(props: Props) { + super(props); + makeObservable(this); + } static open() { - AddSecretDialog.isOpen = true; + dialogState.isOpen = true; } static close() { - AddSecretDialog.isOpen = false; + dialogState.isOpen = false; } private secretTemplate: { [p: string]: ISecretTemplate } = { @@ -205,7 +212,7 @@ export class AddSecretDialog extends React.Component { diff --git a/src/renderer/components/+config-secrets/secret-details.tsx b/src/renderer/components/+config-secrets/secret-details.tsx index 5a4aaca64e..ae8fc17dee 100644 --- a/src/renderer/components/+config-secrets/secret-details.tsx +++ b/src/renderer/components/+config-secrets/secret-details.tsx @@ -23,7 +23,7 @@ import "./secret-details.scss"; import React from "react"; import isEmpty from "lodash/isEmpty"; -import { autorun, observable } from "mobx"; +import { autorun, observable, makeObservable } from "mobx"; import { disposeOnUnmount, observer } from "mobx-react"; import { DrawerItem, DrawerTitle } from "../drawer"; import { Input } from "../input"; @@ -46,6 +46,11 @@ export class SecretDetails extends React.Component { @observable data: { [name: string]: string } = {}; @observable revealSecret: { [name: string]: boolean } = {}; + constructor(props: Props) { + super(props); + makeObservable(this); + } + async componentDidMount() { disposeOnUnmount(this, [ autorun(() => { diff --git a/src/renderer/components/+config-secrets/secrets.store.ts b/src/renderer/components/+config-secrets/secrets.store.ts index c680c5fb9a..f338232371 100644 --- a/src/renderer/components/+config-secrets/secrets.store.ts +++ b/src/renderer/components/+config-secrets/secrets.store.ts @@ -20,11 +20,9 @@ */ import { KubeObjectStore } from "../../kube-object.store"; -import { autobind } from "../../utils"; import { Secret, secretsApi } from "../../api/endpoints"; import { apiManager } from "../../api/api-manager"; -@autobind() export class SecretsStore extends KubeObjectStore { api = secretsApi; } diff --git a/src/renderer/components/+config/config.tsx b/src/renderer/components/+config/config.tsx index 95c97cb217..51f7ddd184 100644 --- a/src/renderer/components/+config/config.tsx +++ b/src/renderer/components/+config/config.tsx @@ -24,7 +24,6 @@ import { observer } from "mobx-react"; import { TabLayout, TabLayoutRoute } from "../layout/tab-layout"; import { ConfigMaps, configMapsRoute, configMapsURL } from "../+config-maps"; import { Secrets, secretsRoute, secretsURL } from "../+config-secrets"; -import { namespaceUrlParam } from "../+namespaces/namespace.store"; import { resourceQuotaRoute, ResourceQuotas, resourceQuotaURL } from "../+config-resource-quotas"; import { pdbRoute, pdbURL, PodDisruptionBudgets } from "../+config-pod-disruption-budgets"; import { HorizontalPodAutoscalers, hpaRoute, hpaURL } from "../+config-autoscalers"; @@ -34,14 +33,13 @@ import { LimitRanges, limitRangesRoute, limitRangeURL } from "../+config-limit-r @observer export class Config extends React.Component { static get tabRoutes(): TabLayoutRoute[] { - const query = namespaceUrlParam.toObjectParam(); const routes: TabLayoutRoute[] = []; if (isAllowedResource("configmaps")) { routes.push({ title: "ConfigMaps", component: ConfigMaps, - url: configMapsURL({ query }), + url: configMapsURL(), routePath: configMapsRoute.path.toString(), }); } @@ -50,7 +48,7 @@ export class Config extends React.Component { routes.push({ title: "Secrets", component: Secrets, - url: secretsURL({ query }), + url: secretsURL(), routePath: secretsRoute.path.toString(), }); } @@ -59,7 +57,7 @@ export class Config extends React.Component { routes.push({ title: "Resource Quotas", component: ResourceQuotas, - url: resourceQuotaURL({ query }), + url: resourceQuotaURL(), routePath: resourceQuotaRoute.path.toString(), }); } @@ -68,7 +66,7 @@ export class Config extends React.Component { routes.push({ title: "Limit Ranges", component: LimitRanges, - url: limitRangeURL({ query }), + url: limitRangeURL(), routePath: limitRangesRoute.path.toString(), }); } @@ -77,7 +75,7 @@ export class Config extends React.Component { routes.push({ title: "HPA", component: HorizontalPodAutoscalers, - url: hpaURL({ query }), + url: hpaURL(), routePath: hpaRoute.path.toString(), }); } @@ -86,7 +84,7 @@ export class Config extends React.Component { routes.push({ title: "Pod Disruption Budgets", component: PodDisruptionBudgets, - url: pdbURL({ query }), + url: pdbURL(), routePath: pdbRoute.path.toString(), }); } diff --git a/src/renderer/components/+custom-resources/crd-list.tsx b/src/renderer/components/+custom-resources/crd-list.tsx index bb6753dce2..2682efea63 100644 --- a/src/renderer/components/+custom-resources/crd-list.tsx +++ b/src/renderer/components/+custom-resources/crd-list.tsx @@ -22,7 +22,7 @@ import "./crd-list.scss"; import React from "react"; -import { computed } from "mobx"; +import { computed, makeObservable } from "mobx"; import { observer } from "mobx-react"; import { Link } from "react-router-dom"; import { stopPropagation } from "../../utils"; @@ -35,8 +35,6 @@ import { Icon } from "../icon"; export const crdGroupsUrlParam = createPageParam({ name: "groups", - multiValues: true, - isSystem: true, defaultValue: [], }); @@ -50,6 +48,11 @@ enum columnId { @observer export class CrdList extends React.Component { + constructor(props: {}) { + super(props); + makeObservable(this); + } + get selectedGroups(): string[] { return crdGroupsUrlParam.get(); } diff --git a/src/renderer/components/+custom-resources/crd-resource-details.tsx b/src/renderer/components/+custom-resources/crd-resource-details.tsx index b681b23c9a..f7305788eb 100644 --- a/src/renderer/components/+custom-resources/crd-resource-details.tsx +++ b/src/renderer/components/+custom-resources/crd-resource-details.tsx @@ -24,7 +24,7 @@ import "./crd-resource-details.scss"; import React from "react"; import jsonPath from "jsonpath"; import { observer } from "mobx-react"; -import { computed } from "mobx"; +import { computed, makeObservable } from "mobx"; import { cssNames } from "../../utils"; import { Badge } from "../badge"; import { DrawerItem } from "../drawer"; @@ -60,6 +60,11 @@ function convertSpecValue(value: any): any { @observer export class CrdResourceDetails extends React.Component { + constructor(props: Props) { + super(props); + makeObservable(this); + } + @computed get crd() { return crdStore.getByObject(this.props.object); } diff --git a/src/renderer/components/+custom-resources/crd-resource.store.ts b/src/renderer/components/+custom-resources/crd-resource.store.ts index b980f30890..8717884785 100644 --- a/src/renderer/components/+custom-resources/crd-resource.store.ts +++ b/src/renderer/components/+custom-resources/crd-resource.store.ts @@ -19,12 +19,10 @@ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -import { autobind } from "../../utils"; -import { KubeApi } from "../../api/kube-api"; +import type { KubeApi } from "../../api/kube-api"; import { KubeObjectStore } from "../../kube-object.store"; import type { KubeObject } from "../../api/kube-object"; -@autobind() export class CRDResourceStore extends KubeObjectStore { api: KubeApi; diff --git a/src/renderer/components/+custom-resources/crd-resources.tsx b/src/renderer/components/+custom-resources/crd-resources.tsx index a8128f42e1..d83bb0a075 100644 --- a/src/renderer/components/+custom-resources/crd-resources.tsx +++ b/src/renderer/components/+custom-resources/crd-resources.tsx @@ -28,7 +28,7 @@ import type { RouteComponentProps } from "react-router"; import { KubeObjectListLayout } from "../kube-object"; import type { KubeObject } from "../../api/kube-object"; import type { ICRDRouteParams } from "./crd.route"; -import { autorun, computed } from "mobx"; +import { autorun, computed, makeObservable } from "mobx"; import { crdStore } from "./crd.store"; import type { TableSortCallback } from "../table"; import { apiManager } from "../../api/api-manager"; @@ -45,6 +45,11 @@ enum columnId { @observer export class CrdResources extends React.Component { + constructor(props: Props) { + super(props); + makeObservable(this); + } + componentDidMount() { disposeOnUnmount(this, [ autorun(() => { diff --git a/src/renderer/components/+custom-resources/crd.store.ts b/src/renderer/components/+custom-resources/crd.store.ts index 9813175b8a..613438bca7 100644 --- a/src/renderer/components/+custom-resources/crd.store.ts +++ b/src/renderer/components/+custom-resources/crd.store.ts @@ -19,9 +19,9 @@ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -import { computed, reaction } from "mobx"; +import { computed, reaction, makeObservable } from "mobx"; import { KubeObjectStore } from "../../kube-object.store"; -import { autobind } from "../../utils"; +import { autoBind } from "../../utils"; import { crdApi, CustomResourceDefinition } from "../../api/endpoints/crd.api"; import { apiManager } from "../../api/api-manager"; import { KubeApi } from "../../api/kube-api"; @@ -39,15 +39,17 @@ function initStore(crd: CustomResourceDefinition) { } } -@autobind() export class CRDStore extends KubeObjectStore { api = crdApi; constructor() { super(); + makeObservable(this); + autoBind(this); + // auto-init stores for crd-s - reaction(() => this.items.toJS(), items => items.forEach(initStore)); + reaction(() => this.getItems(), items => items.forEach(initStore)); } protected sortItems(items: CustomResourceDefinition[]) { diff --git a/src/renderer/components/+entity-settings/entity-settings.tsx b/src/renderer/components/+entity-settings/entity-settings.tsx index 18eee4ffaa..486b6eb105 100644 --- a/src/renderer/components/+entity-settings/entity-settings.tsx +++ b/src/renderer/components/+entity-settings/entity-settings.tsx @@ -22,7 +22,7 @@ import "./entity-settings.scss"; import React from "react"; -import { observable } from "mobx"; +import { observable, makeObservable } from "mobx"; import type { RouteComponentProps } from "react-router"; import { observer } from "mobx-react"; import { PageLayout } from "../layout/page-layout"; @@ -41,6 +41,11 @@ interface Props extends RouteComponentProps { export class EntitySettings extends React.Component { @observable activeTab: string; + constructor(props: Props) { + super(props); + makeObservable(this); + } + get entityId() { return this.props.match.params.entityId; } diff --git a/src/renderer/components/+events/event.store.ts b/src/renderer/components/+events/event.store.ts index 7f51104802..c2f90475d0 100644 --- a/src/renderer/components/+events/event.store.ts +++ b/src/renderer/components/+events/event.store.ts @@ -22,19 +22,23 @@ import groupBy from "lodash/groupBy"; import compact from "lodash/compact"; import { KubeObjectStore } from "../../kube-object.store"; -import { autobind } from "../../utils"; +import { autoBind } from "../../utils"; import { eventApi, KubeEvent } from "../../api/endpoints/events.api"; import type { KubeObject } from "../../api/kube-object"; import { Pod } from "../../api/endpoints/pods.api"; import { podsStore } from "../+workloads-pods/pods.store"; import { apiManager } from "../../api/api-manager"; -@autobind() export class EventStore extends KubeObjectStore { api = eventApi; limit = 1000; saveLimit = 50000; + constructor() { + super(); + autoBind(this); + } + protected bindWatchEventsUpdater() { return super.bindWatchEventsUpdater(5000); } diff --git a/src/renderer/components/+events/events.tsx b/src/renderer/components/+events/events.tsx index 15aafdbf43..adf7b2d105 100644 --- a/src/renderer/components/+events/events.tsx +++ b/src/renderer/components/+events/events.tsx @@ -22,7 +22,7 @@ import "./events.scss"; import React, { Fragment } from "react"; -import { computed, observable } from "mobx"; +import { computed, observable, makeObservable } from "mobx"; import { observer } from "mobx-react"; import { orderBy } from "lodash"; import { TabLayout } from "../layout/tab-layout"; @@ -46,6 +46,7 @@ enum columnId { count = "count", source = "source", age = "age", + lastSeen = "last-seen", } interface Props extends Partial { @@ -61,6 +62,7 @@ const defaultProps: Partial = { @observer export class Events extends React.Component { static defaultProps = defaultProps as object; + now = Date.now(); @observable sorting: TableSortParams = { sortBy: columnId.age, @@ -73,6 +75,7 @@ export class Events extends React.Component { [columnId.object]: (event: KubeEvent) => event.involvedObject.name, [columnId.count]: (event: KubeEvent) => event.count, [columnId.age]: (event: KubeEvent) => event.getTimeDiffFromNow(), + [columnId.lastSeen]: (event: KubeEvent) => this.now - new Date(event.lastTimestamp).getTime(), }; private tableConfiguration: TableProps = { @@ -81,6 +84,11 @@ export class Events extends React.Component { onSort: params => this.sorting = params, }; + constructor(props: Props) { + super(props); + makeObservable(this); + } + get store(): EventStore { return eventStore; } @@ -163,7 +171,8 @@ export class Events extends React.Component { { title: "Involved Object", className: "object", sortBy: columnId.object, id: columnId.object }, { title: "Source", className: "source", id: columnId.source }, { title: "Count", className: "count", sortBy: columnId.count, id: columnId.count }, - { title: "Last Seen", className: "age", sortBy: columnId.age, id: columnId.age }, + { title: "Age", className: "age", sortBy: columnId.age, id: columnId.age }, + { title: "Last Seen", className: "last-seen", sortBy: columnId.lastSeen, id: columnId.lastSeen }, ]} renderTableContents={(event: KubeEvent) => { const { involvedObject, type, message } = event; @@ -190,6 +199,7 @@ export class Events extends React.Component { event.getSource(), event.count, event.getAge(), + event.getLastSeenTime(), ]; }} /> diff --git a/src/renderer/components/+extensions/__tests__/extensions.test.tsx b/src/renderer/components/+extensions/__tests__/extensions.test.tsx index 5aee03bb6e..40a523d8a3 100644 --- a/src/renderer/components/+extensions/__tests__/extensions.test.tsx +++ b/src/renderer/components/+extensions/__tests__/extensions.test.tsx @@ -88,9 +88,13 @@ describe("Extensions", () => { ExtensionDiscovery.getInstance().isLoaded = true; const res = render(<>); + const table = res.getByTestId("extensions-table"); + const menuTrigger = table.querySelector("div[role=row]:first-of-type .actions .Icon"); - expect(res.getByText("Disable").closest("button")).not.toBeDisabled(); - expect(res.getByText("Uninstall").closest("button")).not.toBeDisabled(); + fireEvent.click(menuTrigger); + + expect(res.getByText("Disable")).toHaveAttribute("aria-disabled", "false"); + expect(res.getByText("Uninstall")).toHaveAttribute("aria-disabled", "false"); fireEvent.click(res.getByText("Uninstall")); @@ -99,8 +103,9 @@ describe("Extensions", () => { await waitFor(() => { expect(ExtensionDiscovery.getInstance().uninstallExtension).toHaveBeenCalled(); - expect(res.getByText("Disable").closest("button")).toBeDisabled(); - expect(res.getByText("Uninstall").closest("button")).toBeDisabled(); + fireEvent.click(menuTrigger); + expect(res.getByText("Disable")).toHaveAttribute("aria-disabled", "true"); + expect(res.getByText("Uninstall")).toHaveAttribute("aria-disabled", "true"); }, { timeout: 30000, }); @@ -111,7 +116,7 @@ describe("Extensions", () => { (fse.unlink as jest.MockedFunction).mockReturnValue(Promise.resolve() as any); - fireEvent.change(res.getByPlaceholderText("Path or URL to an extension package", { + fireEvent.change(res.getByPlaceholderText("File path or URL", { exact: false }), { target: { @@ -134,8 +139,6 @@ describe("Extensions", () => { ExtensionDiscovery.getInstance().isLoaded = true; const { container } = render(); - waitFor(() => - expect(container.querySelector(".Spinner")).not.toBeInTheDocument() - ); + expect(container.querySelector(".Spinner")).not.toBeInTheDocument(); }); }); diff --git a/src/renderer/components/+extensions/extension-install.store.ts b/src/renderer/components/+extensions/extension-install.store.ts index 79ba16fa01..c02bb482fd 100644 --- a/src/renderer/components/+extensions/extension-install.store.ts +++ b/src/renderer/components/+extensions/extension-install.store.ts @@ -210,31 +210,45 @@ export class ExtensionInstallationStateStore { return ExtensionInstallationStateStore.InstallingExtensions.size; } + /** + * The current number of extensions uninstalling + */ + static get uninstalling(): number { + return ExtensionInstallationStateStore.UninstallingExtensions.size; + } + /** * If there is at least one extension currently installing */ - @computed static get anyInstalling(): boolean { + static get anyInstalling(): boolean { return ExtensionInstallationStateStore.installing > 0; } + /** + * If there is at least one extension currently ininstalling + */ + static get anyUninstalling(): boolean { + return ExtensionInstallationStateStore.uninstalling > 0; + } + /** * The current number of extensions preinstalling */ - @computed static get preinstalling(): number { + static get preinstalling(): number { return ExtensionInstallationStateStore.PreInstallIds.size; } /** * If there is at least one extension currently downloading */ - @computed static get anyPreinstalling(): boolean { + static get anyPreinstalling(): boolean { return ExtensionInstallationStateStore.preinstalling > 0; } /** - * If there is at least one installing or preinstalling step taking place - */ - @computed static get anyPreInstallingOrInstalling(): boolean { + * If there is at least one installing or preinstalling step taking place + */ + static get anyPreInstallingOrInstalling(): boolean { return ExtensionInstallationStateStore.anyInstalling || ExtensionInstallationStateStore.anyPreinstalling; } } diff --git a/src/renderer/components/+extensions/extensions.scss b/src/renderer/components/+extensions/extensions.scss index 13b9be1f2f..0369eb4798 100644 --- a/src/renderer/components/+extensions/extensions.scss +++ b/src/renderer/components/+extensions/extensions.scss @@ -21,52 +21,16 @@ .PageLayout.Extensions { $spacing: $padding * 2; - --width: 50%; + width: 100%; - h2 { - margin-bottom: $padding; - } + .contentRegion { + .content { + max-width: 740px; - .no-extensions { - --flex-gap: #{$padding}; - padding: $padding; - - code { - font-size: $font-size-small; - } - } - - .install-extension { - margin: $spacing * 2 0; - } - - .installed-extensions { - --flex-gap: #{$spacing}; - - .extension { - padding: $padding $spacing; - background: $layoutBackground; - border-radius: $radius; - - .actions > button:not(:last-child) { - margin-right: $spacing / 2; - } - - h5, h6 { - color: var(--textColorSecondary); + > section { + height: 100%; } } - - > .spinner-wrapper { - display: flex; - justify-content: center; - } - } - - .SearchInput { - --spacing: 10px; - - max-width: none; } } diff --git a/src/renderer/components/+extensions/extensions.tsx b/src/renderer/components/+extensions/extensions.tsx index 93923d3093..1c55c86ecd 100644 --- a/src/renderer/components/+extensions/extensions.tsx +++ b/src/renderer/components/+extensions/extensions.tsx @@ -20,33 +20,31 @@ */ import "./extensions.scss"; + import { remote, shell } from "electron"; import fse from "fs-extra"; -import { computed, observable, reaction, when } from "mobx"; +import _ from "lodash"; +import { makeObservable, observable, reaction, when } from "mobx"; import { disposeOnUnmount, observer } from "mobx-react"; import os from "os"; import path from "path"; import React from "react"; -import { autobind, disposer, Disposer, downloadFile, downloadJson, ExtendableDisposer, extractTar, listTarEntries, noop, readFileFromTar } from "../../../common/utils"; -import { docsUrl } from "../../../common/vars"; +import { SemVer } from "semver"; +import URLParse from "url-parse"; +import { Disposer, disposer, downloadFile, downloadJson, ExtendableDisposer, extractTar, listTarEntries, noop, readFileFromTar, } from "../../../common/utils"; import { ExtensionDiscovery, InstalledExtension, manifestFilename } from "../../../extensions/extension-discovery"; import { ExtensionLoader } from "../../../extensions/extension-loader"; -import { extensionDisplayName, LensExtensionId, LensExtensionManifest, sanitizeExtensionName } from "../../../extensions/lens-extension"; +import { extensionDisplayName, LensExtensionId, LensExtensionManifest, sanitizeExtensionName, } from "../../../extensions/lens-extension"; import logger from "../../../main/logger"; -import { prevDefault } from "../../utils"; import { Button } from "../button"; import { ConfirmDialog } from "../confirm-dialog"; -import { Icon } from "../icon"; -import { DropFileInput, Input, InputValidator, InputValidators, SearchInput } from "../input"; +import { DropFileInput, InputValidators } from "../input"; import { PageLayout } from "../layout/page-layout"; -import { SubTitle } from "../layout/sub-title"; import { Notifications } from "../notifications"; -import { Spinner } from "../spinner/spinner"; -import { TooltipPosition } from "../tooltip"; import { ExtensionInstallationState, ExtensionInstallationStateStore } from "./extension-install.store"; -import URLParse from "url-parse"; -import { SemVer } from "semver"; -import _ from "lodash"; +import { Install } from "./install"; +import { InstalledExtensions } from "./installed-extensions"; +import { Notice } from "./notice"; function getMessageFromError(error: any): string { if (!error || typeof error !== "object") { @@ -89,6 +87,22 @@ interface InstallRequestValidated { tempFile: string; // temp system path to packed extension for unpacking } +function setExtensionEnabled(id: LensExtensionId, isEnabled: boolean): void { + const extension = ExtensionLoader.getInstance().getExtension(id); + + if (extension) { + extension.isEnabled = isEnabled; + } +} + +function enableExtension(id: LensExtensionId) { + setExtensionEnabled(id, true); +} + +function disableExtension(id: LensExtensionId) { + setExtensionEnabled(id, false); +} + async function uninstallExtension(extensionId: LensExtensionId): Promise { const loader = ExtensionLoader.getInstance(); const { manifest } = loader.getExtension(extensionId); @@ -163,7 +177,7 @@ async function validatePackage(filePath: string): Promise // tarball from npm contains single root folder "package/*" const firstFile = tarFiles[0]; - if(!firstFile) { + if (!firstFile) { throw new Error(`invalid extension bundle, ${manifestFilename} not found`); } @@ -171,7 +185,7 @@ async function validatePackage(filePath: string): Promise const packedInRootFolder = tarFiles.every(entry => entry.startsWith(rootFolder)); const manifestLocation = packedInRootFolder ? path.join(rootFolder, manifestFilename) : manifestFilename; - if(!tarFiles.includes(manifestLocation)) { + if (!tarFiles.includes(manifestLocation)) { throw new Error(`invalid extension bundle, ${manifestFilename} not found`); } @@ -385,7 +399,7 @@ async function attemptInstall(request: InstallRequest, d?: ExtendableDisposer): } else { dispose(); } - }} /> + }}/>
, { onClose: dispose, @@ -430,7 +444,7 @@ async function installFromInput(input: string) { await attemptInstall({ fileName, dataP: readFileNotify(input) }); } else if (InputValidators.isExtensionNameInstall.validate(input)) { - const [{ groups: { name, version }}] = [...input.matchAll(InputValidators.isExtensionNameInstallRegex)]; + const [{ groups: { name, version } }] = [...input.matchAll(InputValidators.isExtensionNameInstallRegex)]; await attemptInstallByInfo({ name, version }); } @@ -463,32 +477,16 @@ async function installFromSelectFileDialog() { } } +interface Props { +} + @observer -export class Extensions extends React.Component { - private static installInputValidators = [ - InputValidators.isUrl, - InputValidators.isPath, - InputValidators.isExtensionNameInstall, - ]; - - private static installInputValidator: InputValidator = { - message: "Invalid URL, absolute path, or extension name", - validate: (value: string) => ( - Extensions.installInputValidators.some(({ validate }) => validate(value)) - ), - }; - - @observable search = ""; +export class Extensions extends React.Component { @observable installPath = ""; - @computed get searchedForExtensions() { - const searchText = this.search.toLowerCase(); - - return Array.from(ExtensionLoader.getInstance().userExtensions.values()) - .filter(({ manifest: { name, description }}) => ( - name.toLowerCase().includes(searchText) - || description?.toLowerCase().includes(searchText) - )); + constructor(props: Props) { + super(props); + makeObservable(this); } componentDidMount() { @@ -509,138 +507,34 @@ export class Extensions extends React.Component { ]); } - renderNoExtensionsHelpText() { - if (this.search) { - return

No search results found

; - } - - return ( -

- There are no installed extensions. - See list of available extensions. -

- ); - } - - renderNoExtensions() { - return ( -
- -
- {this.renderNoExtensionsHelpText()} -
-
- ); - } - - @autobind() - renderExtension(extension: InstalledExtension) { - const { id, isEnabled, manifest } = extension; - const { name, description, version } = manifest; - const isUninstalling = ExtensionInstallationStateStore.isExtensionUninstalling(id); - - return ( -
-
-
{name}
-
{version}
-

{description}

-
-
- { - isEnabled - ? - : - } - -
-
- ); - } - - renderExtensions() { - if (!ExtensionDiscovery.getInstance().isLoaded) { - return
; - } - - const { searchedForExtensions } = this; - - if (!searchedForExtensions.length) { - return this.renderNoExtensions(); - } - - return ( - <> - {...searchedForExtensions.map(this.renderExtension)} - - ); - } - render() { - const { installPath } = this; + const extensions = Array.from(ExtensionLoader.getInstance().userExtensions.values()); return ( -

Lens Extensions

-
- Add new features and functionality via Lens Extensions. - Check out documentation to learn more or see the list of available extensions. -
+
+

Extensions

-
- -
- this.installPath = value} - onSubmit={() => installFromInput(this.installPath)} - iconLeft="link" - iconRight={ - - } - /> -
-
+ -

Installed Extensions

-
- this.search = value} + this.installPath = value} + installFromInput={() => installFromInput(this.installPath)} + installFromSelectFileDialog={installFromSelectFileDialog} + installPath={this.installPath} /> - {this.renderExtensions()} -
+ + {extensions.length > 0 &&
} + + +
); diff --git a/src/renderer/components/+extensions/install.module.css b/src/renderer/components/+extensions/install.module.css new file mode 100644 index 0000000000..fbf236177a --- /dev/null +++ b/src/renderer/components/+extensions/install.module.css @@ -0,0 +1,8 @@ +.icon { + @apply h-5 w-5 mr-3 cursor-pointer; + background-color: transparent!important; +} + +.icon:hover { + color: var(--textColorAccent); +} \ No newline at end of file diff --git a/src/renderer/components/+extensions/install.tsx b/src/renderer/components/+extensions/install.tsx new file mode 100644 index 0000000000..aab8477deb --- /dev/null +++ b/src/renderer/components/+extensions/install.tsx @@ -0,0 +1,101 @@ +/** + * Copyright (c) 2021 OpenLens Authors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +import styles from "./install.module.css"; +import React from "react"; +import { prevDefault } from "../../utils"; +import { Button } from "../button"; +import { Icon } from "../icon"; +import { Input, InputValidator, InputValidators } from "../input"; +import { SubTitle } from "../layout/sub-title"; +import { TooltipPosition } from "../tooltip"; +import { ExtensionInstallationStateStore } from "./extension-install.store"; +import { observer } from "mobx-react"; +import { MaterialTooltip } from "../material-tooltip/material-tooltip"; + +interface Props { + installPath: string; + supportedFormats: string[]; + onChange: (path: string) => void; + installFromInput: () => void; + installFromSelectFileDialog: () => void; +} + +const installInputValidators = [ + InputValidators.isUrl, + InputValidators.isPath, + InputValidators.isExtensionNameInstall, +]; + +const installInputValidator: InputValidator = { + message: "Invalid URL, absolute path, or extension name", + validate: (value: string) => ( + installInputValidators.some(({ validate }) => validate(value)) + ), +}; + +export const Install = observer((props: Props) => { + const { installPath, supportedFormats, onChange, installFromInput, installFromSelectFileDialog } = props; + + return ( +
+ +
+
+ + + + } + /> +
+
+
+
+ + Pro-Tip: you can drag-n-drop tarball-file to this area + +
+ ); +}); + diff --git a/src/renderer/components/+extensions/installed-extensions.module.css b/src/renderer/components/+extensions/installed-extensions.module.css new file mode 100644 index 0000000000..831ece9024 --- /dev/null +++ b/src/renderer/components/+extensions/installed-extensions.module.css @@ -0,0 +1,25 @@ +.extensionName { + @apply font-bold; + color: var(--textColorAccent); +} + +.extensionDescription { + @apply mt-1; +} + +.enabled { + color: var(--colorOk); +} + +.title { + margin-bottom: 0!important; +} + +.noItemsIcon { + @apply opacity-10; + --size: 180px; +} + +.frozenRow { + @apply opacity-30 pointer-events-none; +} \ No newline at end of file diff --git a/src/renderer/components/+extensions/installed-extensions.tsx b/src/renderer/components/+extensions/installed-extensions.tsx new file mode 100644 index 0000000000..f896f410fd --- /dev/null +++ b/src/renderer/components/+extensions/installed-extensions.tsx @@ -0,0 +1,169 @@ +/** + * Copyright (c) 2021 OpenLens Authors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +import styles from "./installed-extensions.module.css"; +import React, { useMemo } from "react"; +import { ExtensionDiscovery, InstalledExtension } from "../../../extensions/extension-discovery"; +import { Icon } from "../icon"; +import { List } from "../list/list"; +import { MenuActions, MenuItem } from "../menu"; +import { Spinner } from "../spinner"; +import { ExtensionInstallationStateStore } from "./extension-install.store"; +import { cssNames } from "../../utils"; +import { observer } from "mobx-react"; +import type { Row } from "react-table"; +import type { LensExtensionId } from "../../../extensions/lens-extension"; + +interface Props { + extensions: InstalledExtension[]; + enable: (id: LensExtensionId) => void; + disable: (id: LensExtensionId) => void; + uninstall: (extension: InstalledExtension) => void; +} + +function getStatus(isEnabled: boolean) { + return isEnabled ? "Enabled" : "Disabled"; +} + +export const InstalledExtensions = observer(({ extensions, uninstall, enable, disable }: Props) => { + const filters = [ + (extension: InstalledExtension) => extension.manifest.name, + (extension: InstalledExtension) => getStatus(extension.isEnabled), + (extension: InstalledExtension) => extension.manifest.version, + ]; + + const columns = useMemo( + () => [ + { + Header: "Name", + accessor: "extension", + width: 200, + sortType: (rowA: Row, rowB: Row) => { // Custom sorting for extension name + const nameA = extensions[rowA.index].manifest.name; + const nameB = extensions[rowB.index].manifest.name; + + if (nameA > nameB) return -1; + if (nameB > nameA) return 1; + + return 0; + }, + }, + { + Header: "Version", + accessor: "version", + }, + { + Header: "Status", + accessor: "status" + }, + { + Header: "", + accessor: "actions", + disableSortBy: true, + width: 20, + className: "actions" + } + ], [] + ); + + const data = useMemo( + () => { + return extensions.map(extension => { + const { id, isEnabled, manifest } = extension; + const { name, description, version } = manifest; + const isUninstalling = ExtensionInstallationStateStore.isExtensionUninstalling(id); + + return { + extension: ( +
+
+
{name}
+
{description}
+
+
+ ), + version, + status: ( +
+ {getStatus(isEnabled)} +
+ ), + actions: ( + + {isEnabled ? ( + disable(id)} + > + + Disable + + ) : ( + enable(id)} + > + + Enable + + )} + uninstall(extension)} + > + + Uninstall + + + ) + }; + }); + }, [extensions, ExtensionInstallationStateStore.anyUninstalling] + ); + + if (!ExtensionDiscovery.getInstance().isLoaded) { + return
; + } + + if (extensions.length == 0) { + return ( +
+ +

+ There are no extensions installed. +

+

Please use the form above to install or drag tarbar-file here.

+
+ ); + } + + return ( +
+ Installed extensions} + columns={columns} + data={data} + items={extensions} + filters={filters} + /> +
+ ); +}); diff --git a/src/renderer/components/+extensions/notice.module.css b/src/renderer/components/+extensions/notice.module.css new file mode 100644 index 0000000000..81b90c48ad --- /dev/null +++ b/src/renderer/components/+extensions/notice.module.css @@ -0,0 +1,27 @@ +/** + * Copyright (c) 2021 OpenLens Authors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +.notice { + @apply p-8 flex flex-col gap-2 mb-14 mt-3 rounded-lg; + background-color: var(--inputControlBackground); + border: 1px solid var(--boxShadow); + color: var(--textColorTertiary); +} \ No newline at end of file diff --git a/src/renderer/components/+extensions/notice.tsx b/src/renderer/components/+extensions/notice.tsx new file mode 100644 index 0000000000..d224b65a7d --- /dev/null +++ b/src/renderer/components/+extensions/notice.tsx @@ -0,0 +1,36 @@ +/** + * Copyright (c) 2021 OpenLens Authors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +import styles from "./notice.module.css"; +import React from "react"; +import { docsUrl } from "../../../common/vars"; + +export function Notice() { + return ( +
+

+ Add new features via Lens Extensions.{" "} + Check out docs{" "} + and list of available extensions. +

+
+ ); +} diff --git a/src/renderer/components/+namespaces/add-namespace-dialog.tsx b/src/renderer/components/+namespaces/add-namespace-dialog.tsx index 50b4d207c3..213891c233 100644 --- a/src/renderer/components/+namespaces/add-namespace-dialog.tsx +++ b/src/renderer/components/+namespaces/add-namespace-dialog.tsx @@ -22,7 +22,7 @@ import "./add-namespace-dialog.scss"; import React from "react"; -import { observable } from "mobx"; +import { observable, makeObservable } from "mobx"; import { observer } from "mobx-react"; import { Dialog, DialogProps } from "../dialog"; import { Wizard, WizardStep } from "../wizard"; @@ -37,17 +37,25 @@ interface Props extends DialogProps { onError?(error: any): void; } +const dialogState = observable.object({ + isOpen: false, +}); + @observer export class AddNamespaceDialog extends React.Component { - @observable static isOpen = false; @observable namespace = ""; + constructor(props: Props) { + super(props); + makeObservable(this); + } + static open() { - AddNamespaceDialog.isOpen = true; + dialogState.isOpen = true; } static close() { - AddNamespaceDialog.isOpen = false; + dialogState.isOpen = false; } reset = () => { @@ -78,7 +86,7 @@ export class AddNamespaceDialog extends React.Component { diff --git a/src/renderer/components/+namespaces/namespace-details.tsx b/src/renderer/components/+namespaces/namespace-details.tsx index d8e5e90f7b..76ccb5addb 100644 --- a/src/renderer/components/+namespaces/namespace-details.tsx +++ b/src/renderer/components/+namespaces/namespace-details.tsx @@ -22,7 +22,7 @@ import "./namespace-details.scss"; import React from "react"; -import { computed } from "mobx"; +import { computed, makeObservable } from "mobx"; import { observer } from "mobx-react"; import { DrawerItem } from "../drawer"; import { cssNames } from "../../utils"; @@ -40,6 +40,11 @@ interface Props extends KubeObjectDetailsProps { @observer export class NamespaceDetails extends React.Component { + constructor(props: Props) { + super(props); + makeObservable(this); + } + @computed get quotas() { const namespace = this.props.object.getName(); diff --git a/src/renderer/components/+namespaces/namespace-select-filter.scss b/src/renderer/components/+namespaces/namespace-select-filter.scss new file mode 100644 index 0000000000..da59651ce5 --- /dev/null +++ b/src/renderer/components/+namespaces/namespace-select-filter.scss @@ -0,0 +1,47 @@ +.NamespaceSelectFilter { + --gradientColor: var(--select-menu-bgc); + + .Select { + &.theme-light { + --gradientColor: white; + } + + &__placeholder { + width: 100%; + white-space: nowrap; + overflow: scroll!important; + text-overflow: unset!important; + margin-left: -8px; + padding-left: 8px; + margin-right: -8px; + padding-right: 8px; + + &::-webkit-scrollbar { + display: none; + } + } + + &__value-container { + position: relative; + + &::before, &::after { + content: ' '; + position: absolute; + z-index: 20; + display: block; + width: 8px; + height: var(--font-size); + } + + &::before { + left: 0px; + background: linear-gradient(to right, var(--gradientColor) 0px, transparent); + } + + &::after { + right: 0px; + background: linear-gradient(to left, var(--gradientColor) 0px, transparent); + } + } + } +} diff --git a/src/renderer/components/+namespaces/namespace-select-filter.tsx b/src/renderer/components/+namespaces/namespace-select-filter.tsx index 8f4e9df6da..ad2c1fe87b 100644 --- a/src/renderer/components/+namespaces/namespace-select-filter.tsx +++ b/src/renderer/components/+namespaces/namespace-select-filter.tsx @@ -19,7 +19,7 @@ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -import "./namespace-select.scss"; +import "./namespace-select-filter.scss"; import React from "react"; import { observer } from "mobx-react"; @@ -28,10 +28,11 @@ import { components, PlaceholderProps } from "react-select"; import { Icon } from "../icon"; import { FilterIcon } from "../item-object-list/filter-icon"; import { FilterType } from "../item-object-list/page-filters.store"; -import type { SelectOption } from "../select"; import { NamespaceSelect } from "./namespace-select"; import { namespaceStore } from "./namespace.store"; +import type { SelectOption, SelectProps } from "../select"; + const Placeholder = observer((props: PlaceholderProps) => { const getPlaceholder = (): React.ReactNode => { const namespaces = namespaceStore.contextNamespaces; @@ -54,9 +55,8 @@ const Placeholder = observer((props: PlaceholderProps) => { ); }); - @observer -export class NamespaceSelectFilter extends React.Component { +export class NamespaceSelectFilter extends React.Component { formatOptionLabel({ value: namespace, label }: SelectOption) { if (namespace) { const isSelected = namespaceStore.hasContext(namespace); @@ -92,6 +92,7 @@ export class NamespaceSelectFilter extends React.Component { placeholder={""} onChange={this.onChange} formatOptionLabel={this.formatOptionLabel} + className="NamespaceSelectFilter" /> ); } diff --git a/src/renderer/components/+namespaces/namespace-select.scss b/src/renderer/components/+namespaces/namespace-select.scss index 72df94a9a7..e348f7025b 100644 --- a/src/renderer/components/+namespaces/namespace-select.scss +++ b/src/renderer/components/+namespaces/namespace-select.scss @@ -25,41 +25,8 @@ } } -.GradientValueContainer { - width: 8px; - height: var(--font-size); - position: absolute; - z-index: 20; - - &.front { - left: 0px; - background: linear-gradient(to right, var(--contentColor) 0px, transparent); - } - - &.back { - right: 0px; - background: linear-gradient(to left, var(--contentColor) 0px, transparent); - } -} - .NamespaceSelect { @include namespaceSelectCommon; - - .Select { - &__placeholder { - width: 100%; - white-space: nowrap; - overflow: scroll; - margin-left: -8px; - padding-left: 8px; - margin-right: -8px; - padding-right: 8px; - - &::-webkit-scrollbar { - display: none; - } - } - } } .NamespaceSelectMenu { diff --git a/src/renderer/components/+namespaces/namespace-select.tsx b/src/renderer/components/+namespaces/namespace-select.tsx index 3a17e791db..c35ae72513 100644 --- a/src/renderer/components/+namespaces/namespace-select.tsx +++ b/src/renderer/components/+namespaces/namespace-select.tsx @@ -22,14 +22,13 @@ import "./namespace-select.scss"; import React from "react"; -import { computed } from "mobx"; +import { computed, makeObservable } from "mobx"; import { disposeOnUnmount, observer } from "mobx-react"; import { Select, SelectOption, SelectProps } from "../select"; import { cssNames } from "../../utils"; import { Icon } from "../icon"; import { namespaceStore } from "./namespace.store"; import { kubeWatchApi } from "../../api/kube-watch-api"; -import { components, ValueContainerProps } from "react-select"; interface Props extends SelectProps { showIcons?: boolean; @@ -43,20 +42,15 @@ const defaultProps: Partial = { showClusterOption: false, }; -function GradientValueContainer({children, ...rest}: ValueContainerProps) { - return ( - -
- {children} -
- - ); -} - @observer export class NamespaceSelect extends React.Component { static defaultProps = defaultProps as object; + constructor(props: Props) { + super(props); + makeObservable(this); + } + componentDidMount() { disposeOnUnmount(this, [ kubeWatchApi.subscribeStores([namespaceStore], { @@ -98,8 +92,6 @@ export class NamespaceSelect extends React.Component { render() { const { className, showIcons, customizeOptions, components = {}, ...selectProps } = this.props; - components.ValueContainer ??= GradientValueContainer; - return (