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

Merge branch 'master' into typesafe-catalog-category-events

Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com>
This commit is contained in:
Jari Kolehmainen 2021-05-26 20:19:21 +03:00
commit d31f4b15e7
305 changed files with 4368 additions and 2104 deletions

View File

@ -15,3 +15,7 @@ updates:
versioning-strategy:
lockfile-only: false
increase: true
ignore:
- dependency-name: *
update-types:
- version-update:semver-major

View File

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

View File

@ -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<ExamplePrefere
enabled: false
}
});
makeObservable(this);
}
protected fromStore({ enabled }: ExamplePreferencesModel): void {
@ -49,11 +50,9 @@ export class ExamplePreferencesStore extends Store.ExtensionStore<ExamplePrefere
}
toJSON(): ExamplePreferencesModel {
return toJS({
return {
enabled: this.enabled
}, {
recurseEverything: true
});
};
}
}
```
@ -73,7 +72,6 @@ The `enabled` field of the `ExamplePreferencesStore` is set to the value from th
The `toJSON()` method is complementary to `fromStore()`.
It is called when the store is being saved.
`toJSON()` must provide a JSON serializable object, facilitating its storage in JSON format.
The `toJS()` function from [`mobx`](https://mobx.js.org/README.html) is convenient for this purpose, and is used here.
Finally, `ExamplePreferencesStore` is created by calling `ExamplePreferencesStore.getInstanceOrCreate()`, and exported for use by other parts of the extension.
Note that `ExamplePreferencesStore` is a singleton.

View File

@ -1661,6 +1661,12 @@
"integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=",
"dev": true
},
"js-tokens": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
"integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
"dev": true
},
"json-parse-better-errors": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz",
@ -1715,6 +1721,15 @@
"path-exists": "^3.0.0"
}
},
"loose-envify": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
"integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
"dev": true,
"requires": {
"js-tokens": "^3.0.0 || ^4.0.0"
}
},
"lru-cache": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
@ -1881,6 +1896,27 @@
"minimist": "^1.2.5"
}
},
"mobx": {
"version": "6.3.1",
"resolved": "https://registry.npmjs.org/mobx/-/mobx-6.3.1.tgz",
"integrity": "sha512-by0VQNBltxLKcQ0uIz2h2mnVv1ZV0ief8mRRw+0k7mL2vSS3806rZg42bt8Vy78szaODtij/5iWIGvc46ZC3Cw==",
"dev": true
},
"mobx-react": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/mobx-react/-/mobx-react-7.2.0.tgz",
"integrity": "sha512-KHUjZ3HBmZlNnPd1M82jcdVsQRDlfym38zJhZEs33VxyVQTvL77hODCArq6+C1P1k/6erEeo2R7rpE7ZeOL7dg==",
"dev": true,
"requires": {
"mobx-react-lite": "^3.2.0"
}
},
"mobx-react-lite": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/mobx-react-lite/-/mobx-react-lite-3.2.0.tgz",
"integrity": "sha512-q5+UHIqYCOpBoFm/PElDuOhbcatvTllgRp3M1s+Hp5j0Z6XNgDbgqxawJ0ZAUEyKM8X1zs70PCuhAIzX1f4Q/g==",
"dev": true
},
"move-concurrently": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/move-concurrently/-/move-concurrently-1.0.1.tgz",
@ -2290,6 +2326,16 @@
"safe-buffer": "^5.1.0"
}
},
"react": {
"version": "17.0.2",
"resolved": "https://registry.npmjs.org/react/-/react-17.0.2.tgz",
"integrity": "sha512-gnhPt75i/dq/z3/6q/0asP78D0u592D5L1pd7M8P+dck6Fu/jJeL6iVVK23fptSUZj8Vjf++7wXA8UNclGQcbA==",
"dev": true,
"requires": {
"loose-envify": "^1.1.0",
"object-assign": "^4.1.1"
}
},
"readable-stream": {
"version": "2.3.7",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz",

View File

@ -13,6 +13,7 @@
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"experimentalDecorators": true,
"useDefineForClassFields": true,
"jsx": "react"
},
"include": [

View File

@ -14,7 +14,8 @@
"clean": "rm -rf dist/ && rm *.tgz"
},
"files": [
"dist/**/*"
"dist/**/*",
"resources/**/*"
],
"devDependencies": {
"@k8slens/extensions": "file:../../src/extensions/npm/extensions",
@ -23,6 +24,5 @@
"ts-loader": "^8.0.4",
"typescript": "^4.0.3",
"webpack": "^4.44.2"
},
"dependencies": {}
}
}

View File

@ -54,7 +54,7 @@ spec:
mountPath: /var/lib/prometheus
containers:
- name: prometheus
image: quay.io/prometheus/prometheus:v2.26.0
image: quay.io/prometheus/prometheus:v2.27.1
args:
- --web.listen-address=0.0.0.0:9090
- --config.file=/etc/prometheus/prometheus.yaml

View File

@ -23,23 +23,15 @@ spec:
operator: In
values:
- linux
- key: kubernetes.io/arch
operator: In
values:
- amd64
- matchExpressions:
- key: beta.kubernetes.io/os
operator: In
values:
- linux
- key: beta.kubernetes.io/arch
operator: In
values:
- amd64
serviceAccountName: kube-state-metrics
containers:
- name: kube-state-metrics
image: quay.io/coreos/kube-state-metrics:v1.9.8
image: k8s.gcr.io/kube-state-metrics/kube-state-metrics:v2.0.0
ports:
- name: metrics
containerPort: 8080

View File

@ -21,7 +21,7 @@
import React from "react";
import { Component, Catalog, K8sApi } from "@k8slens/extensions";
import { observer } from "mobx-react";
import { computed, observable } from "mobx";
import { computed, observable, makeObservable } from "mobx";
import { MetricsFeature, MetricsConfiguration } from "./metrics-feature";
interface Props {
@ -30,6 +30,11 @@ interface Props {
@observer
export class MetricsSettings extends React.Component<Props> {
constructor(props: Props) {
super(props);
makeObservable(this);
}
@observable featureStates = {
prometheus: false,
kubeStateMetrics: false,

View File

@ -13,6 +13,7 @@
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"experimentalDecorators": true,
"useDefineForClassFields": true,
"jsx": "react"
},
"include": [

View File

@ -13,6 +13,7 @@
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"experimentalDecorators": true,
"useDefineForClassFields": true,
"jsx": "react"
},
"include": [

View File

@ -13,6 +13,7 @@
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"experimentalDecorators": true,
"useDefineForClassFields": true,
"jsx": "react"
},
"include": [

View File

@ -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 () => {

View File

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

28
postcss.config.js Normal file
View File

@ -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")
],
};

View File

@ -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<T = any> extends ConfOptions<T> {
*/
export abstract class BaseStore<T = any> extends Singleton {
protected storeConfig?: Config<T>;
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<T = any> 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) {

View File

@ -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<CatalogCategory>();
protected groupKinds = new ExtendedMap<string, ExtendedMap<string, CatalogCategory>>();
constructor() {
makeObservable(this);
}
@action add(category: CatalogCategory): Disposer {
this.categories.add(category);
this.updateGroupKinds(category);

View File

@ -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> = Entity extends CatalogEntity<infer Metadata> ? Metadata : never;
type ExtractEntityStatusType<Entity> = Entity extends CatalogEntity<any, infer Status> ? 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<Metadata, Status, Spec>) {
makeObservable(this);
this.metadata = data.metadata;
this.status = data.status;
this.spec = data.spec;

View File

@ -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<ClusterStoreModel> {
migrations,
});
makeObservable(this);
this.pushStateToViewsAutomatically();
}
@ -171,16 +173,16 @@ export class ClusterStore extends BaseStore<ClusterStoreModel> {
});
} 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<ClusterStoreModel> {
@action
protected fromStore({ activeCluster, clusters = [] }: ClusterStoreModel = {}) {
const currentClusters = this.clusters.toJS();
const currentClusters = new Map(this.clusters);
const newClusters = new Map<ClusterId, Cluster>();
const removedClusters = new Map<ClusterId, Cluster>();
@ -345,8 +347,6 @@ export class ClusterStore extends BaseStore<ClusterStoreModel> {
return toJS({
activeCluster: this.activeCluster,
clusters: this.clustersList.map(cluster => cluster.toJSON()),
}, {
recurseEverything: true
});
}
}

View File

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

View File

@ -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<HotbarStoreModel> {
},
migrations,
});
makeObservable(this);
}
get activeHotbarId() {
@ -252,8 +254,6 @@ export class HotbarStore extends BaseStore<HotbarStoreModel> {
activeHotbarId: this.activeHotbarId
};
return toJS(model, {
recurseEverything: true,
});
return toJS(model);
}
}

View File

@ -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<T>(data: any): T {
return toJS(data);
}

View File

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

View File

@ -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<UserStoreModel> {
configName: "lens-user-store",
migrations,
});
makeObservable(this);
}
@observable lastSeenAppVersion = "0.0.0";
@ -306,9 +307,7 @@ export class UserStore extends BaseStore<UserStoreModel> {
},
};
return toJS(model, {
recurseEverything: true,
});
return toJS(model);
}
}

View File

@ -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();
});
});

View File

@ -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<T = {}> = 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<T extends Constructor>(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<T extends object>(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<object, Function>();
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,
};

View File

@ -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<K, V> extends Map<K, V> {
static new<K, V>(entries?: readonly (readonly [K, V])[] | null): ExtendedMap<K, V> {
@ -67,34 +67,12 @@ export class ExtendedMap<K, V> extends Map<K, V> {
}
export class ExtendedObservableMap<K, V> extends ObservableMap<K, V> {
constructor(protected getDefault: () => V, initialData?: IObservableMapInitialValues<K, V>, enhancer?: IEnhancer<V>, 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);
}
}

View File

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

39
src/common/utils/toJS.ts Normal file
View File

@ -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<number>
*/
import * as mobx from "mobx";
import { isObservable, observable } from "mobx";
export function toJS<T>(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);
}

View File

@ -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<T> extends Set<T> {
public toggle(value: T): void {
@ -31,7 +31,6 @@ export class ToggleSet<T> extends Set<T> {
}
export class ObservableToggleSet<T> extends ObservableSet<T> {
@action
public toggle(value: T): void {
if (!this.delete(value)) {
// Set.prototype.delete returns false if `value` was not in the set

View File

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

View File

@ -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
});
}

View File

@ -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<LensExtensionId, InstalledExtension> {
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<string, LensExtension> {
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<LensExtensionId, InstalledExtension> {
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);
}
}

View File

@ -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<LensExtensionId, LensExtensionState>;
@ -37,6 +38,7 @@ export class ExtensionsStore extends BaseStore<LensExtensionsStoreModel> {
super({
configName: "lens-extensions",
});
makeObservable(this);
}
@computed
@ -68,9 +70,7 @@ export class ExtensionsStore extends BaseStore<LensExtensionsStoreModel> {
toJSON(): LensExtensionsStoreModel {
return toJS({
extensions: this.state.toJSON(),
}, {
recurseEverything: true
extensions: Object.fromEntries(this.state),
});
}
}

View File

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

View File

@ -75,15 +75,22 @@ describe("getPageUrl", () => {
});
it("gets page url with custom params", () => {
const params: PageParams<string> = { 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`);
});

View File

@ -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<T, I = T> {
private items = observable.map<T, I>();
constructor() {
makeObservable(this);
}
getItems(): I[] {
return Array.from(this.items.values());
}

View File

@ -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<CommandRegistration> {
@observable activeEntity: CatalogEntity;
@observable.ref activeEntity: CatalogEntity;
constructor() {
super();
makeObservable(this);
}
@action
add(items: CommandRegistration | CommandRegistration[], extension?: LensExtension) {
const itemArray = [items].flat();

View File

@ -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<T extends PageMenuRegistration> extends BaseRegistry<T> {
@action
add(items: T[], ext: LensExtension) {
const normalizedItems = items.map(menuItem => {
menuItem.target = {

View File

@ -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<string | ExtensionPageParamInit>;
params?: PageParams<string | Omit<PageParamInit<any>, "name" | "prefix">>;
components: PageComponents;
}
// exclude "name" field since provided as key in page.params
export type ExtensionPageParamInit = Omit<PageParamInit, "name" | "isSystem">;
export interface PageComponents {
Page: React.ComponentType<any>;
}
export interface PageTarget<P = PageParams> {
export interface PageTarget {
extensionId?: string;
pageId?: string;
params?: P;
params?: PageParams;
}
export interface PageParams<V = any> {
@ -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<PageRegistration, RegisteredPage>
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<PageRegistration, RegisteredPage>
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<string | ExtensionPageParamInit>): PageParams<PageParam> | 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<string | Partial<PageParamInit>>): PageParams<PageParam> {
if (!params) return undefined;
const normalizedParams: PageParams<PageParam> = {};
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<PageParam>;
return normalizedParams;
}
getByPageTarget(target: PageTarget): RegisteredPage | null {

View File

@ -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<V = string>(init: PageParamInit<V>) {
export function createPageParam<V>(init: PageParamInit<V>) {
return new PageParam<V>(init, navigation);
}

View File

@ -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();

View File

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

View File

@ -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();

View File

@ -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<Catal
depth: stat.isDirectory() ? 0 : 1, // DIRs works with 0 but files need 1 (bug: https://github.com/paulmillr/chokidar/issues/1095)
disableGlobbing: true,
});
const rootSource = new ExtendedObservableMap<string, ObservableMap<string, RootSourceValue>>(observable.map);
const rootSource = new ExtendedObservableMap<string, ObservableMap<string, RootSourceValue>>();
const derivedSource = computed(() => Array.from(iter.flatMap(rootSource.values(), from => iter.map(from.values(), child => child[1]))));
const stoppers = new Map<string, Disposer>();
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)();

View File

@ -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<string, IComputedValue<CatalogEntity[]>>([], { deep: true });
protected sources = observable.map<string, IComputedValue<CatalogEntity[]>>();
constructor(private categoryRegistry: CatalogCategoryRegistry) {}
constructor(private categoryRegistry: CatalogCategoryRegistry) {
makeObservable(this);
}
@action addObservableSource(id: string, source: IObservableArray<CatalogEntity>) {
this.sources.set(id, computed(() => source));

View File

@ -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<KubernetesCluster>("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
}
}));
});
}

View File

@ -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<KubeApiResource, boolean> = 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);
}
/**

View File

@ -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<string, string>; // extension names to paths
}
export class FilesystemProvisionerStore extends BaseStore<FSProvisionModel> {
@observable registeredExtensions = observable.map<LensExtensionId, string>();
registeredExtensions = observable.map<LensExtensionId, string>();
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<FSProvisionModel> {
toJSON(): FSProvisionModel {
return toJS({
extensions: this.registeredExtensions.toJSON(),
}, {
recurseEverything: true
extensions: Object.fromEntries(this.registeredExtensions),
});
}
}

View File

@ -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,
};

View File

@ -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)`
};
}
}

View File

@ -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)`
};
}
}

View File

@ -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)`
};
}
}

View File

@ -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<boolean>;
@ -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.
*

View File

@ -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}`);
}

View File

@ -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();

View File

@ -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<string, KubeApi>();
private stores = observable.map<string, KubeObjectStore>();
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);

View File

@ -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<CatalogEntityData & CatalogEntityKindData>([], { deep: true });
@observable protected _activeEntity: CatalogEntity;
constructor(private categoryRegistry: CatalogCategoryRegistry) {}
constructor(private categoryRegistry: CatalogCategoryRegistry) {
makeObservable(this);
}
init() {
subscribeToBroadcast("catalog:items", (ev, items: (CatalogEntityData & CatalogEntityKindData)[]) => {

View File

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

View File

@ -71,10 +71,7 @@ export interface IClusterMetrics<T = IMetrics> {
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;

View File

@ -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");
}

View File

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

View File

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

View File

@ -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<CronJob> {
suspend(params: { namespace: string; name: string }) {
@ -58,28 +59,7 @@ export class CronJobApi extends KubeApi<CronJob> {
}
}
@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();

View File

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

View File

@ -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<Deployment> {
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;

View File

@ -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 || [];

View File

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

View File

@ -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<string, HelmChart[]>;
export type HelmChartList = Record<string, RepoHelmChartList>;
@ -83,16 +83,7 @@ export async function getChartValues(repo: string, name: string, version: string
return apiBase.get<string>(`/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}`;

View File

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

View File

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

View File

@ -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<Ingress> {
getMetrics(ingress: string, namespace: string): Promise<IIngressMetrics> {
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
));

View File

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

View File

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

View File

@ -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 ?? "-";
}
}

View File

@ -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 [];

View File

@ -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<Node> {
getMetrics(): Promise<INodeMetrics> {
const opts = { category: "nodes"};
const opts = { category: "nodes" };
return metricsApi.getMetrics({
memoryUsage: opts,
@ -49,12 +50,7 @@ export interface INodeMetrics<T = IMetrics> {
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;

View File

@ -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<PersistentVolumeClaim> {
getMetrics(pvcName: string, namespace: string): Promise<IPvcMetrics> {
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<T = IMetrics> {
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());

View File

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

View File

@ -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,
});

View File

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

View File

@ -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<Pod> {
async getLogs(params: { namespace: string; name: string }, query?: IPodLogsQuery): Promise<string> {
@ -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;

View File

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

View File

@ -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<ReplicaSet> {
protected getScaleApiUrl(params: { namespace: string; name: string }) {
@ -48,12 +49,17 @@ export class ReplicaSetApi extends KubeApi<ReplicaSet> {
}
}
@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;

View File

@ -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 || {};

View File

@ -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 || [];

View File

@ -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 || [];

View File

@ -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[] {

View File

@ -28,8 +28,7 @@ export class SelfSubjectRulesReviewApi extends KubeApi<SelfSubjectRulesReview> {
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 || [];

View File

@ -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 || [];

View File

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

View File

@ -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<StatefulSet> {
protected getScaleApiUrl(params: { namespace: string; name: string }) {
@ -48,13 +49,17 @@ export class StatefulSetApi extends KubeApi<StatefulSet> {
}
}
@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;

View File

@ -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 || {};

View File

@ -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<Metadata extends IKubeObjectMetadata = IKubeObjectMetadata, Status = any, Spec = any> 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<T extends KubeObject>(data: Partial<T>) {
async update<T extends KubeObject>(data: Partial<T>): Promise<T> {
return resourceApplierApi.update<T>({
...this.toPlainObject(),
...data,

View File

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

View File

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

View File

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

View File

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

View File

@ -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<void>;
};
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,
};

View File

@ -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<string, KubeConfig>();
constructor(props: {}) {
super(props);
makeObservable(this);
}
componentDidMount() {
appEventBus.emit({ name: "cluster-add", action: "start" });
}

Some files were not shown because too many files have changed in this diff Show More