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

Mobx-6 migration (#2718)

* mobx-6 migration -- part 1

Signed-off-by: Roman <ixrock@gmail.com>

* mobx-6 migration -- part 2 (npx mobx-undecorate --keepDecorators)

Signed-off-by: Roman <ixrock@gmail.com>

* mobx-6 migration -- part 3 (more fixes)

Signed-off-by: Roman <ixrock@gmail.com>

* unwrap possible observables from IPC-messaging

Signed-off-by: Roman <ixrock@gmail.com>

* mobx-6 migration -- remove @autobind as class-decorator

Signed-off-by: Roman <ixrock@gmail.com>

* mobx-6: replacing @autobind() as method-decorator to @boundMethod

Signed-off-by: Roman <ixrock@gmail.com>

* mobx-6: use toJS()-wrapper since monkey-patching require(mobx).toJS doesn't work

Signed-off-by: Roman <ixrock@gmail.com>

* removed `@observable static`

Signed-off-by: Roman <ixrock@gmail.com>

* use {useDefineForClassFields: true} in tsconfig.json

Signed-off-by: Roman <ixrock@gmail.com>

* remove ExtendedObservableMap

Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com>

* fix: removed makeObservable(this) from "terminal-tab.tsx"

Signed-off-by: Roman <ixrock@gmail.com>

* storage-helper refactoring

Signed-off-by: Roman <ixrock@gmail.com>

* normalize usages of #observable-value.toJSON() / attempt to catch the wind

Signed-off-by: Roman <ixrock@gmail.com>

* refactoring, more possible branch fixes + lint

Signed-off-by: Roman <ixrock@gmail.com>

* debugging cluster-view error -- part 1

Signed-off-by: Roman <ixrock@gmail.com>

* fix: refreshing cluster-view on ready

Signed-off-by: Roman <ixrock@gmail.com>

* fix: various app-crashes related to KubeObject.spec.* access from "undefined"
fix: config-map-details crash

Signed-off-by: Roman <ixrock@gmail.com>

* fix: namespace-store refactoring / saving selected-namespaces to external json-file

Signed-off-by: Roman <ixrock@gmail.com>

* fix: don't cache mobx.when(() => this.someObservable) cause might not work as expected due later call of makeObservable(this) in constructor

Signed-off-by: Roman <ixrock@gmail.com>

* fix: app-crash on editing k8s resource

Signed-off-by: Roman <ixrock@gmail.com>

* fix: restore "all namespaces" on page reload

Signed-off-by: Roman <ixrock@gmail.com>

* - fix: persist table-sort params and cluster-view's sidebar state to lens-local-storage
- new-feature: auto-open main-window's devtools in development-mode (yes/no/ugly?)

Signed-off-by: Roman <ixrock@gmail.com>

* fix: crd definition details -> crashing with <AceEditor mode="json"> (added missing mode-file in ace-editor.tsx)

Signed-off-by: Roman <ixrock@gmail.com>

* fix: crd definitions -> groups selector couldn't deselect last selected option

Signed-off-by: Roman <ixrock@gmail.com>

* refactoring: extensions-api exports clarification for "@k8slens/extensions"

Signed-off-by: Roman <ixrock@gmail.com>

* fix: various app-crashes related to kube-events (events page, some details page, overview, etc.)

Signed-off-by: Roman <ixrock@gmail.com>

* Reverted "use {useDefineForClassFields: true} in tsconfig.json" (various app-crash fixes)
This flag seems to be not possible to use with class-inheritance in some cases.

Example / demo:
`KubeObject` class has initial type definitions for the fields like: "metadata", "kind", etc.
and constructor() has Object.assign(this, data);
Meanwhile child class, e.g. KubeEvent inherited from KubeObject and has it's own extra type definitions for underlying resource, e.g. "involvedObject", "source", etc.

So calling super(data) doesn't work as expected for child class as it's own type definitions overwrites data from parent's constructor with `undefined` at later point.

Signed-off-by: Roman <ixrock@gmail.com>

* master-merge lint-fixes

Signed-off-by: Roman <ixrock@gmail.com>

* catalog.tsx / catalog-entities.store.ts refactoring & fixes

Signed-off-by: Roman <ixrock@gmail.com>

* fix: Catalog -> Browse all tab

Signed-off-by: Roman <ixrock@gmail.com>

* fix: CommandPalette doesn't appear from global menu by click/hotkey

Signed-off-by: Roman <ixrock@gmail.com>

* - Merging interfaces & classses to avoid overwriting fields from parent's super(data)-call with Object.assign(this, data). Otherwise use "declare" keyword at class field definition.

- Revamping {useDefineForClassFields: true} to avoid issues with non-observable class fields in some cases (from previous commit):

```
@observer
export class CommandContainer extends React.Component<CommandContainerProps> {
  // without some defined initial value "commandComponent" is non-observable for some reasons
  // when tsconfig.ts has {useDefineForClassFields:false}
  @observable.ref commandComponent: React.ReactNode = null;

  constructor(props: CommandContainerProps) {
    super(props);
    makeObservable(this);
  }
```

Signed-off-by: Roman <ixrock@gmail.com>

* update KubeObject class type definition

Signed-off-by: Roman <ixrock@gmail.com>

* clean up / responding to comments

Signed-off-by: Roman <ixrock@gmail.com>

* fix: app-crash when navigating to catalog from active cluster-view, refactoring `catalog-entity-store`

Signed-off-by: Roman <ixrock@gmail.com>

* catalog-pusher clean up, replaced .observe_() to external observe() helper from "mobx"

Signed-off-by: Roman <ixrock@gmail.com>

* fix: catalog's items stale/non-observable (after connection to the cluster status still "disconnected"), lint-fixes

Signed-off-by: Roman <ixrock@gmail.com>

* fix: Catalog is empty after closing main-window and re-opening app from Tray

Signed-off-by: Roman <ixrock@gmail.com>

* fix: HotBar's icon context menu items non-observable (no "disconnect cluster", etc.)

Signed-off-by: Roman <ixrock@gmail.com>

* lint-fix/license check

Signed-off-by: Roman <ixrock@gmail.com>

* fix: redirect to catalog when disconnecting active cluster

Signed-off-by: Roman <ixrock@gmail.com>

* fix: refresh visibility of active cluster-view on switching from hotbar/catalog

Signed-off-by: Roman <ixrock@gmail.com>

* updated package.json for built-in extensions to use "*" version for packages served from main app

Signed-off-by: Roman <ixrock@gmail.com>

* - added missing makeObservable(this) to metrics-settings.tsx
- updated package-lock.json for built-in extensions
- lint fixes

Signed-off-by: Roman <ixrock@gmail.com>

* master-merge clean up fix, updated package-lock.json for built-in extensions after `make clean-extensions && make build-extensions`

Signed-off-by: Roman <ixrock@gmail.com>

* fix unit-tests

Signed-off-by: Roman <ixrock@gmail.com>

* master-merge fixes

Signed-off-by: Roman <ixrock@gmail.com>

* make lint happy

Signed-off-by: Roman <ixrock@gmail.com>

* reverted some changes, removed auto-opening devtools in dev-mode

Signed-off-by: Roman <ixrock@gmail.com>

* merge fixes

Signed-off-by: Roman <ixrock@gmail.com>

* master-merge conflict fixes:
- proper handling and navigating into catalog's active category via URL-builder

Signed-off-by: Roman <ixrock@gmail.com>

* reverting splitted params for catalog's page route to "/catalog/:group?/:kind?"

Signed-off-by: Roman <ixrock@gmail.com>

* clean-up: remove app's injecting dependencies from `extensions/kube-object-event-status/package.json`

Signed-off-by: Roman <ixrock@gmail.com>

* master-merge fix: added missing makeObservable(this) for extensions.tsx

Signed-off-by: Roman <ixrock@gmail.com>

* fix: catalog entity context menu stale/unobservable

Signed-off-by: Roman <ixrock@gmail.com>

Co-authored-by: Jari Kolehmainen <jari.kolehmainen@gmail.com>
This commit is contained in:
Roman 2021-05-25 10:24:31 +03:00 committed by GitHub
parent 0c45dd807e
commit 2c3b510997
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
256 changed files with 2359 additions and 1473 deletions

View File

@ -25,7 +25,7 @@ The following example code creates a store for the `appPreferences` guide exampl
``` typescript ``` typescript
import { Store } from "@k8slens/extensions"; import { Store } from "@k8slens/extensions";
import { observable, toJS } from "mobx"; import { observable, makeObservable } from "mobx";
export type ExamplePreferencesModel = { export type ExamplePreferencesModel = {
enabled: boolean; enabled: boolean;
@ -42,6 +42,7 @@ export class ExamplePreferencesStore extends Store.ExtensionStore<ExamplePrefere
enabled: false enabled: false
} }
}); });
makeObservable(this);
} }
protected fromStore({ enabled }: ExamplePreferencesModel): void { protected fromStore({ enabled }: ExamplePreferencesModel): void {
@ -49,11 +50,9 @@ export class ExamplePreferencesStore extends Store.ExtensionStore<ExamplePrefere
} }
toJSON(): ExamplePreferencesModel { toJSON(): ExamplePreferencesModel {
return toJS({ return {
enabled: this.enabled 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()`. The `toJSON()` method is complementary to `fromStore()`.
It is called when the store is being saved. It is called when the store is being saved.
`toJSON()` must provide a JSON serializable object, facilitating its storage in JSON format. `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. Finally, `ExamplePreferencesStore` is created by calling `ExamplePreferencesStore.getInstanceOrCreate()`, and exported for use by other parts of the extension.
Note that `ExamplePreferencesStore` is a singleton. Note that `ExamplePreferencesStore` is a singleton.

View File

@ -1661,6 +1661,12 @@
"integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=",
"dev": true "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": { "json-parse-better-errors": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz", "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" "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": { "lru-cache": {
"version": "5.1.1", "version": "5.1.1",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
@ -1881,6 +1896,27 @@
"minimist": "^1.2.5" "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": { "move-concurrently": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/move-concurrently/-/move-concurrently-1.0.1.tgz", "resolved": "https://registry.npmjs.org/move-concurrently/-/move-concurrently-1.0.1.tgz",
@ -2290,6 +2326,16 @@
"safe-buffer": "^5.1.0" "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": { "readable-stream": {
"version": "2.3.7", "version": "2.3.7",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz",

View File

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

View File

@ -23,6 +23,5 @@
"ts-loader": "^8.0.4", "ts-loader": "^8.0.4",
"typescript": "^4.0.3", "typescript": "^4.0.3",
"webpack": "^4.44.2" "webpack": "^4.44.2"
}, }
"dependencies": {}
} }

View File

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

View File

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

View File

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

View File

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

View File

@ -183,6 +183,8 @@
"@kubernetes/client-node": "^0.12.0", "@kubernetes/client-node": "^0.12.0",
"abort-controller": "^3.0.0", "abort-controller": "^3.0.0",
"array-move": "^3.0.0", "array-move": "^3.0.0",
"auto-bind": "^4.0.0",
"autobind-decorator": "^2.4.0",
"await-lock": "^2.1.0", "await-lock": "^2.1.0",
"byline": "^5.0.0", "byline": "^5.0.0",
"chalk": "^4.1.0", "chalk": "^4.1.0",
@ -207,9 +209,9 @@
"mac-ca": "^1.0.4", "mac-ca": "^1.0.4",
"marked": "^2.0.3", "marked": "^2.0.3",
"md5-file": "^5.0.0", "md5-file": "^5.0.0",
"mobx": "^5.15.7", "mobx": "^6.3.0",
"mobx-observable-history": "^1.0.3", "mobx-observable-history": "^2.0.1",
"mobx-react": "^6.2.2", "mobx-react": "^7.1.0",
"mock-fs": "^4.12.0", "mock-fs": "^4.12.0",
"moment": "^2.26.0", "moment": "^2.26.0",
"moment-timezone": "^0.5.33", "moment-timezone": "^0.5.33",

View File

@ -23,9 +23,8 @@ import path from "path";
import Config from "conf"; import Config from "conf";
import type { Options as ConfOptions } from "conf/dist/source/types"; import type { Options as ConfOptions } from "conf/dist/source/types";
import { app, ipcMain, IpcMainEvent, ipcRenderer, IpcRendererEvent, remote } from "electron"; import { app, ipcMain, IpcMainEvent, ipcRenderer, IpcRendererEvent, remote } from "electron";
import { IReactionOptions, observable, reaction, runInAction, when } from "mobx"; import { IReactionOptions, makeObservable, observable, reaction, runInAction, when } from "mobx";
import Singleton from "./utils/singleton"; import { getAppVersion, Singleton, toJS, Disposer } from "./utils";
import { getAppVersion } from "./utils/app-version";
import logger from "../main/logger"; import logger from "../main/logger";
import { broadcastMessage, subscribeToBroadcast, unsubscribeFromBroadcast } from "./ipc"; import { broadcastMessage, subscribeToBroadcast, unsubscribeFromBroadcast } from "./ipc";
import isEqual from "lodash/isEqual"; 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 { export abstract class BaseStore<T = any> extends Singleton {
protected storeConfig?: Config<T>; protected storeConfig?: Config<T>;
protected syncDisposers: Function[] = []; protected syncDisposers: Disposer[] = [];
whenLoaded = when(() => this.isLoaded);
@observable isLoaded = false; @observable isLoaded = false;
get whenLoaded() {
return when(() => this.isLoaded);
}
protected constructor(protected params: BaseStoreParams) { protected constructor(protected params: BaseStoreParams) {
super(); super();
makeObservable(this);
this.params = { this.params = {
autoLoad: false, autoLoad: false,
syncEnabled: true, syncEnabled: true,
@ -114,7 +118,11 @@ export abstract class BaseStore<T = any> extends Singleton {
enableSync() { enableSync() {
this.syncDisposers.push( 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) { if (ipcMain) {

View File

@ -19,7 +19,7 @@
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * 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 { Disposer, ExtendedMap } from "../utils";
import { CatalogCategory, CatalogEntityData, CatalogEntityKindData } from "./catalog-entity"; import { CatalogCategory, CatalogEntityData, CatalogEntityKindData } from "./catalog-entity";
@ -27,6 +27,10 @@ export class CatalogCategoryRegistry {
protected categories = observable.set<CatalogCategory>(); protected categories = observable.set<CatalogCategory>();
protected groupKinds = new ExtendedMap<string, ExtendedMap<string, CatalogCategory>>(); protected groupKinds = new ExtendedMap<string, ExtendedMap<string, CatalogCategory>>();
constructor() {
makeObservable(this);
}
@action add(category: CatalogCategory): Disposer { @action add(category: CatalogCategory): Disposer {
this.categories.add(category); this.categories.add(category);
this.updateGroupKinds(category); this.updateGroupKinds(category);

View File

@ -20,7 +20,7 @@
*/ */
import { EventEmitter } from "events"; import { EventEmitter } from "events";
import { observable } from "mobx"; import { observable, makeObservable } from "mobx";
type ExtractEntityMetadataType<Entity> = Entity extends CatalogEntity<infer Metadata> ? Metadata : never; type ExtractEntityMetadataType<Entity> = Entity extends CatalogEntity<infer Metadata> ? Metadata : never;
type ExtractEntityStatusType<Entity> = Entity extends CatalogEntity<any, infer Status> ? Status : never; type ExtractEntityStatusType<Entity> = Entity extends CatalogEntity<any, infer Status> ? Status : never;
@ -56,6 +56,12 @@ export abstract class CatalogCategory extends EventEmitter {
}; };
abstract spec: CatalogCategorySpec; abstract spec: CatalogCategorySpec;
static parseId(id = ""): { group?: string, kind?: string } {
const [group, kind] = id.split("/") ?? [];
return { group, kind };
}
public getId(): string { public getId(): string {
return `${this.spec.group}/${this.spec.names.kind}`; return `${this.spec.group}/${this.spec.names.kind}`;
} }
@ -147,6 +153,7 @@ export abstract class CatalogEntity<
@observable spec: Spec; @observable spec: Spec;
constructor(data: CatalogEntityData<Metadata, Status, Spec>) { constructor(data: CatalogEntityData<Metadata, Status, Spec>) {
makeObservable(this);
this.metadata = data.metadata; this.metadata = data.metadata;
this.status = data.status; this.status = data.status;
this.spec = data.spec; this.spec = data.spec;

View File

@ -22,7 +22,7 @@
import path from "path"; import path from "path";
import { app, ipcMain, ipcRenderer, remote, webFrame } from "electron"; import { app, ipcMain, ipcRenderer, remote, webFrame } from "electron";
import { unlink } from "fs-extra"; 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 { BaseStore } from "./base-store";
import { Cluster, ClusterState } from "../main/cluster"; import { Cluster, ClusterState } from "../main/cluster";
import migrations from "../migrations/cluster-store"; import migrations from "../migrations/cluster-store";
@ -33,7 +33,7 @@ import { saveToAppFiles } from "./utils/saveToAppFiles";
import type { KubeConfig } from "@kubernetes/client-node"; import type { KubeConfig } from "@kubernetes/client-node";
import { handleRequest, requestMain, subscribeToBroadcast, unsubscribeAllFromBroadcast } from "./ipc"; import { handleRequest, requestMain, subscribeToBroadcast, unsubscribeAllFromBroadcast } from "./ipc";
import type { ResourceType } from "../renderer/components/cluster-settings/components/cluster-metrics-setting"; 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 { export interface ClusterIconUpload {
clusterId: string; clusterId: string;
@ -148,6 +148,8 @@ export class ClusterStore extends BaseStore<ClusterStoreModel> {
migrations, migrations,
}); });
makeObservable(this);
this.pushStateToViewsAutomatically(); this.pushStateToViewsAutomatically();
} }
@ -171,16 +173,16 @@ export class ClusterStore extends BaseStore<ClusterStoreModel> {
}); });
} else if (ipcMain) { } else if (ipcMain) {
handleRequest(ClusterStore.stateRequestChannel, (): clusterStateSync[] => { handleRequest(ClusterStore.stateRequestChannel, (): clusterStateSync[] => {
const states: clusterStateSync[] = []; const clusterStates: clusterStateSync[] = [];
this.clustersList.forEach((cluster) => { this.clustersList.forEach((cluster) => {
states.push({ clusterStates.push({
state: cluster.getState(), state: cluster.getState(),
id: cluster.id id: cluster.id
}); });
}); });
return states; return clusterStates;
}); });
} }
} }
@ -309,7 +311,7 @@ export class ClusterStore extends BaseStore<ClusterStoreModel> {
@action @action
protected fromStore({ activeCluster, clusters = [] }: ClusterStoreModel = {}) { protected fromStore({ activeCluster, clusters = [] }: ClusterStoreModel = {}) {
const currentClusters = this.clusters.toJS(); const currentClusters = new Map(this.clusters);
const newClusters = new Map<ClusterId, Cluster>(); const newClusters = new Map<ClusterId, Cluster>();
const removedClusters = new Map<ClusterId, Cluster>(); const removedClusters = new Map<ClusterId, Cluster>();
@ -345,8 +347,6 @@ export class ClusterStore extends BaseStore<ClusterStoreModel> {
return toJS({ return toJS({
activeCluster: this.activeCluster, activeCluster: this.activeCluster,
clusters: this.clustersList.map(cluster => cluster.toJSON()), 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. * 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 { BaseStore } from "./base-store";
import migrations from "../migrations/hotbar-store"; import migrations from "../migrations/hotbar-store";
import * as uuid from "uuid"; import * as uuid from "uuid";
import isNull from "lodash/isNull"; import isNull from "lodash/isNull";
import { toJS } from "./utils";
import { CatalogEntity } from "./catalog"; import { CatalogEntity } from "./catalog";
export interface HotbarItem { export interface HotbarItem {
@ -69,6 +70,7 @@ export class HotbarStore extends BaseStore<HotbarStoreModel> {
}, },
migrations, migrations,
}); });
makeObservable(this);
} }
get activeHotbarId() { get activeHotbarId() {
@ -252,8 +254,6 @@ export class HotbarStore extends BaseStore<HotbarStoreModel> {
activeHotbarId: this.activeHotbarId activeHotbarId: this.activeHotbarId
}; };
return toJS(model, { return toJS(model);
recurseEverything: true,
});
} }
} }

View File

@ -23,29 +23,34 @@
// https://www.electronjs.org/docs/api/ipc-main // https://www.electronjs.org/docs/api/ipc-main
// https://www.electronjs.org/docs/api/ipc-renderer // https://www.electronjs.org/docs/api/ipc-renderer
import { ipcMain, ipcRenderer, webContents, remote } from "electron"; import { ipcMain, ipcRenderer, remote, webContents } from "electron";
import { toJS } from "mobx"; import { toJS } from "../utils/toJS";
import logger from "../../main/logger"; import logger from "../../main/logger";
import { ClusterFrameInfo, clusterFrameMap } from "../cluster-frames"; import { ClusterFrameInfo, clusterFrameMap } from "../cluster-frames";
const subFramesChannel = "ipc:get-sub-frames"; const subFramesChannel = "ipc:get-sub-frames";
export function handleRequest(channel: string, listener: (event: Electron.IpcMainInvokeEvent, ...args: any[]) => any) { 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[]) { export async function requestMain(channel: string, ...args: any[]) {
return ipcRenderer.invoke(channel, ...args); return ipcRenderer.invoke(channel, ...args.map(sanitizePayload));
} }
function getSubFrames(): ClusterFrameInfo[] { function getSubFrames(): ClusterFrameInfo[] {
return toJS(Array.from(clusterFrameMap.values()), { recurseEverything: true }); return Array.from(clusterFrameMap.values());
} }
export function broadcastMessage(channel: string, ...args: any[]) { export function broadcastMessage(channel: string, ...args: any[]) {
const views = (webContents || remote?.webContents)?.getAllWebContents(); const views = (webContents || remote?.webContents)?.getAllWebContents();
if (!views) return; if (!views) return;
args = args.map(sanitizePayload);
ipcRenderer?.send(channel, ...args); ipcRenderer?.send(channel, ...args);
ipcMain?.emit(channel, ...args); ipcMain?.emit(channel, ...args);
@ -98,7 +103,13 @@ export function unsubscribeAllFromBroadcast(channel: string) {
} }
export function bindBroadcastHandlers() { export function bindBroadcastHandlers() {
handleRequest(subFramesChannel, () => { handleRequest(subFramesChannel, () => getSubFrames());
return 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. * 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 { dockStore } from "../renderer/components/dock/dock.store";
import { autobind } from "../renderer/utils"; import { boundMethod } from "../renderer/utils";
export class SearchStore { export class SearchStore {
/** /**
@ -54,6 +54,7 @@ export class SearchStore {
@observable activeOverlayIndex = -1; @observable activeOverlayIndex = -1;
constructor() { constructor() {
makeObservable(this);
reaction(() => dockStore.selectedTabId, () => { reaction(() => dockStore.selectedTabId, () => {
searchStore.reset(); searchStore.reset();
}); });
@ -128,12 +129,12 @@ export class SearchStore {
return prev; return prev;
} }
@autobind() @boundMethod
public setNextOverlayActive(): void { public setNextOverlayActive(): void {
this.activeOverlayIndex = this.getNextOverlay(true); this.activeOverlayIndex = this.getNextOverlay(true);
} }
@autobind() @boundMethod
public setPrevOverlayActive(): void { public setPrevOverlayActive(): void {
this.activeOverlayIndex = this.getPrevOverlay(true); this.activeOverlayIndex = this.getPrevOverlay(true);
} }
@ -159,7 +160,7 @@ export class SearchStore {
* @param line Index of the line where overlay is located * @param line Index of the line where overlay is located
* @param occurrence Number of the overlay within one line * @param occurrence Number of the overlay within one line
*/ */
@autobind() @boundMethod
public isActiveOverlay(line: number, occurrence: number): boolean { public isActiveOverlay(line: number, occurrence: number): boolean {
const firstLineIndex = this.occurrences.findIndex(item => item === line); 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 { app, remote } from "electron";
import semver from "semver"; import semver from "semver";
import { readFile } from "fs-extra"; 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 moment from "moment-timezone";
import { BaseStore } from "./base-store"; import { BaseStore } from "./base-store";
import migrations from "../migrations/user-store"; import migrations from "../migrations/user-store";
@ -34,7 +34,7 @@ import logger from "../main/logger";
import path from "path"; import path from "path";
import os from "os"; import os from "os";
import { fileNameMigration } from "../migrations/user-store"; import { fileNameMigration } from "../migrations/user-store";
import { ObservableToggleSet } from "../renderer/utils"; import { ObservableToggleSet, toJS } from "../renderer/utils";
export interface UserStoreModel { export interface UserStoreModel {
kubeConfigPath: string; kubeConfigPath: string;
@ -73,6 +73,7 @@ export class UserStore extends BaseStore<UserStoreModel> {
configName: "lens-user-store", configName: "lens-user-store",
migrations, migrations,
}); });
makeObservable(this);
} }
@observable lastSeenAppVersion = "0.0.0"; @observable lastSeenAppVersion = "0.0.0";
@ -306,9 +307,7 @@ export class UserStore extends BaseStore<UserStoreModel> {
}, },
}; };
return toJS(model, { return toJS(model);
recurseEverything: true,
});
} }
} }

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. * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/ */
// Decorator for binding class methods import {boundMethod, boundClass} from "autobind-decorator";
// Can be applied to class or single method as @autobind() import autoBindClass, { Options } from "auto-bind";
type Constructor<T = {}> = new (...args: any[]) => T; import autoBindReactClass from "auto-bind/react";
export function autobind() { // Automatically bind methods to their class instance
return function (target: Constructor | object, prop?: string, descriptor?: PropertyDescriptor) { export function autoBind<T extends object>(obj: T, opts?: Options): T {
if (target instanceof Function) return bindClass(target); if ("componentWillUnmount" in obj) {
else return bindMethod(target, prop, descriptor); return autoBindReactClass(obj as any, opts);
};
}
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`);
} }
const { value: func, enumerable, configurable } = descriptor;
const boundFunc = new WeakMap<object, Function>();
return Object.defineProperty(target, prop, { return autoBindClass(obj, opts);
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);
}
});
} }
// Class/method decorators
// Note: @boundClass doesn't work with mobx-6.x/@action decorator
export {
boundClass,
boundMethod,
};

View File

@ -27,6 +27,7 @@ export * from "./app-version";
export * from "./autobind"; export * from "./autobind";
export * from "./base64"; export * from "./base64";
export * from "./camelCase"; export * from "./camelCase";
export * from "./toJS";
export * from "./cloneJson"; export * from "./cloneJson";
export * from "./debouncePromise"; export * from "./debouncePromise";
export * from "./defineGlobal"; 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. * 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> { export class ToggleSet<T> extends Set<T> {
public toggle(value: T): void { public toggle(value: T): void {
@ -31,7 +31,6 @@ export class ToggleSet<T> extends Set<T> {
} }
export class ObservableToggleSet<T> extends ObservableSet<T> { export class ObservableToggleSet<T> extends ObservableSet<T> {
@action
public toggle(value: T): void { public toggle(value: T): void {
if (!this.delete(value)) { if (!this.delete(value)) {
// Set.prototype.delete returns false if `value` was not in the set // 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. * 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 "./core-api";
export * from "./renderer-api"; export * from "./renderer-api";

View File

@ -23,11 +23,11 @@ import { watch } from "chokidar";
import { ipcRenderer } from "electron"; import { ipcRenderer } from "electron";
import { EventEmitter } from "events"; import { EventEmitter } from "events";
import fse from "fs-extra"; import fse from "fs-extra";
import { observable, reaction, toJS, when } from "mobx"; import { observable, reaction, when, makeObservable } from "mobx";
import os from "os"; import os from "os";
import path from "path"; import path from "path";
import { broadcastMessage, handleRequest, requestMain, subscribeToBroadcast } from "../common/ipc"; 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 logger from "../main/logger";
import { ExtensionInstallationStateStore } from "../renderer/components/+extensions/extension-install.store"; import { ExtensionInstallationStateStore } from "../renderer/components/+extensions/extension-install.store";
import { extensionInstaller, PackageJson } from "./extension-installer"; 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 // True if extensions have been loaded from the disk after app startup
@observable isLoaded = false; @observable isLoaded = false;
whenLoaded = when(() => this.isLoaded);
get whenLoaded() {
return when(() => this.isLoaded);
}
// IPC channel to broadcast changes to extension-discovery from main // IPC channel to broadcast changes to extension-discovery from main
protected static readonly extensionDiscoveryChannel = "extension-discovery:main"; protected static readonly extensionDiscoveryChannel = "extension-discovery:main";
public events = new EventEmitter(); public events = new EventEmitter();
constructor() {
super();
makeObservable(this);
}
get localFolderPath(): string { get localFolderPath(): string {
return path.join(os.homedir(), ".k8slens", "extensions"); 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)); const userExtensions = await this.loadFromFolder(this.localFolderPath, bundledExtensions.map((extension) => extension.manifest.name));
for (const extension of userExtensions) { for (const extension of userExtensions) {
if (await fse.pathExists(extension.manifestPath) === false) { if ((await fse.pathExists(extension.manifestPath)) === false) {
await this.installPackage(extension.absolutePath); await this.installPackage(extension.absolutePath);
} }
} }
@ -468,8 +477,6 @@ export class ExtensionDiscovery extends Singleton {
toJSON(): ExtensionDiscoveryChannelMessage { toJSON(): ExtensionDiscoveryChannelMessage {
return toJS({ return toJS({
isLoaded: this.isLoaded isLoaded: this.isLoaded
}, {
recurseEverything: true
}); });
} }

View File

@ -22,11 +22,11 @@
import { app, ipcRenderer, remote } from "electron"; import { app, ipcRenderer, remote } from "electron";
import { EventEmitter } from "events"; import { EventEmitter } from "events";
import { isEqual } from "lodash"; 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 path from "path";
import { getHostedCluster } from "../common/cluster-store"; import { getHostedCluster } from "../common/cluster-store";
import { broadcastMessage, handleRequest, requestMain, subscribeToBroadcast } from "../common/ipc"; 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 logger from "../main/logger";
import type { InstalledExtension } from "./extension-discovery"; import type { InstalledExtension } from "./extension-discovery";
import { ExtensionsStore } from "./extensions-store"; import { ExtensionsStore } from "./extensions-store";
@ -58,7 +58,16 @@ export class ExtensionLoader extends Singleton {
private events = new EventEmitter(); private events = new EventEmitter();
@observable isLoaded = false; @observable isLoaded = false;
whenLoaded = when(() => this.isLoaded);
get whenLoaded() {
return when(() => this.isLoaded);
}
constructor() {
super();
makeObservable(this);
}
@computed get userExtensions(): Map<LensExtensionId, InstalledExtension> { @computed get userExtensions(): Map<LensExtensionId, InstalledExtension> {
const extensions = this.toJSON(); const extensions = this.toJSON();
@ -75,7 +84,7 @@ export class ExtensionLoader extends Singleton {
@computed get userExtensionsByName(): Map<string, LensExtension> { @computed get userExtensionsByName(): Map<string, LensExtension> {
const extensions = new Map(); const extensions = new Map();
for (const [, val] of this.instances.toJS()) { for (const [, val] of this.instances.toJSON()) {
if (val.isBundled) { if (val.isBundled) {
continue; continue;
} }
@ -117,6 +126,11 @@ export class ExtensionLoader extends Singleton {
await Promise.all([this.whenLoaded, ExtensionsStore.getInstance().whenLoaded]); 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` // save state on change `extension.isEnabled`
reaction(() => this.storeState, extensionsState => { reaction(() => this.storeState, extensionsState => {
ExtensionsStore.getInstance().mergeState(extensionsState); ExtensionsStore.getInstance().mergeState(extensionsState);
@ -156,14 +170,10 @@ export class ExtensionLoader extends Singleton {
} }
} }
protected async initMain() { protected async initMain() {
this.isLoaded = true; this.isLoaded = true;
this.loadOnMain(); this.loadOnMain();
reaction(() => this.toJSON(), () => {
this.broadcastExtensions();
});
handleRequest(ExtensionLoader.extensionsMainChannel, () => { handleRequest(ExtensionLoader.extensionsMainChannel, () => {
return Array.from(this.toJSON()); 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][]) => { const extensionListHandler = (extensions: [LensExtensionId, InstalledExtension][]) => {
this.isLoaded = true; this.isLoaded = true;
this.syncExtensions(extensions); this.syncExtensions(extensions);
@ -188,16 +198,20 @@ export class ExtensionLoader extends Singleton {
}); });
}; };
reaction(() => this.toJSON(), () => {
this.broadcastExtensions(false);
});
requestMain(ExtensionLoader.extensionsMainChannel).then(extensionListHandler); requestMain(ExtensionLoader.extensionsMainChannel).then(extensionListHandler);
subscribeToBroadcast(ExtensionLoader.extensionsMainChannel, (_event, extensions: [LensExtensionId, InstalledExtension][]) => { subscribeToBroadcast(ExtensionLoader.extensionsMainChannel, (_event, extensions: [LensExtensionId, InstalledExtension][]) => {
extensionListHandler(extensions); extensionListHandler(extensions);
}); });
} }
broadcastExtensions() {
const channel = ipcRenderer
? ExtensionLoader.extensionsRendererChannel
: ExtensionLoader.extensionsMainChannel;
broadcastMessage(channel, Array.from(this.extensions));
}
syncExtensions(extensions: [LensExtensionId, InstalledExtension][]) { syncExtensions(extensions: [LensExtensionId, InstalledExtension][]) {
extensions.forEach(([lensExtensionId, extension]) => { extensions.forEach(([lensExtensionId, extension]) => {
if (!isEqual(this.extensions.get(lensExtensionId), extension)) { if (!isEqual(this.extensions.get(lensExtensionId), extension)) {
@ -255,7 +269,7 @@ export class ExtensionLoader extends Singleton {
const cluster = getHostedCluster(); const cluster = getHostedCluster();
this.autoInitExtensions(async (extension: LensRendererExtension) => { this.autoInitExtensions(async (extension: LensRendererExtension) => {
if (await extension.isEnabledForCluster(cluster) === false) { if ((await extension.isEnabledForCluster(cluster)) === false) {
return []; return [];
} }
@ -334,13 +348,6 @@ export class ExtensionLoader extends Singleton {
} }
toJSON(): Map<LensExtensionId, InstalledExtension> { toJSON(): Map<LensExtensionId, InstalledExtension> {
return toJS(this.extensions, { return toJS(this.extensions);
exportMapsAsObjects: false,
recurseEverything: true,
});
}
broadcastExtensions(main = true) {
broadcastMessage(main ? ExtensionLoader.extensionsMainChannel : ExtensionLoader.extensionsRendererChannel, Array.from(this.toJSON()));
} }
} }

View File

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

View File

@ -20,7 +20,7 @@
*/ */
import type { InstalledExtension } from "./extension-discovery"; 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 { FilesystemProvisionerStore } from "../main/extension-filesystem";
import logger from "../main/logger"; import logger from "../main/logger";
import type { ProtocolHandlerRegistration } from "./registries"; import type { ProtocolHandlerRegistration } from "./registries";
@ -52,6 +52,7 @@ export class LensExtension {
[Disposers] = disposer(); [Disposers] = disposer();
constructor({ id, manifest, manifestPath, isBundled }: InstalledExtension) { constructor({ id, manifest, manifestPath, isBundled }: InstalledExtension) {
makeObservable(this);
this.id = id; this.id = id;
this.manifest = manifest; this.manifest = manifest;
this.manifestPath = manifestPath; this.manifestPath = manifestPath;

View File

@ -75,15 +75,22 @@ describe("getPageUrl", () => {
}); });
it("gets page url with custom params", () => { 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 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}`); expect(pageUrl).toBe(`/extension/foo-bar/page-with-params?${searchParams}`);
}); });
it("gets page url with default custom params", () => { 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`); expect(defaultPageUrl).toBe(`/extension/foo-bar/page-with-params?test1=test1-default`);
}); });

View File

@ -20,12 +20,16 @@
*/ */
// Base class for extensions-api registries // Base class for extensions-api registries
import { action, observable } from "mobx"; import { action, observable, makeObservable } from "mobx";
import { LensExtension } from "../lens-extension"; import { LensExtension } from "../lens-extension";
export class BaseRegistry<T, I = T> { export class BaseRegistry<T, I = T> {
private items = observable.map<T, I>(); private items = observable.map<T, I>();
constructor() {
makeObservable(this);
}
getItems(): I[] { getItems(): I[] {
return Array.from(this.items.values()); return Array.from(this.items.values());
} }

View File

@ -22,9 +22,9 @@
// Extensions API -> Commands // Extensions API -> Commands
import { BaseRegistry } from "./base-registry"; import { BaseRegistry } from "./base-registry";
import { action, observable } from "mobx"; import { makeObservable, observable } from "mobx";
import { LensExtension } from "../lens-extension"; import type { LensExtension } from "../lens-extension";
import { CatalogEntity } from "../../common/catalog"; import type { CatalogEntity } from "../../common/catalog";
export type CommandContext = { export type CommandContext = {
entity?: CatalogEntity; entity?: CatalogEntity;
@ -39,9 +39,14 @@ export interface CommandRegistration {
} }
export class CommandRegistry extends BaseRegistry<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) { add(items: CommandRegistration | CommandRegistration[], extension?: LensExtension) {
const itemArray = [items].flat(); const itemArray = [items].flat();

View File

@ -23,9 +23,8 @@
import type { IconProps } from "../../renderer/components/icon"; import type { IconProps } from "../../renderer/components/icon";
import type React from "react"; import type React from "react";
import type { PageTarget, RegisteredPage } from "./page-registry"; import type { PageTarget, RegisteredPage } from "./page-registry";
import { action } from "mobx"; import type { LensExtension } from "../lens-extension";
import { BaseRegistry } from "./base-registry"; import { BaseRegistry } from "./base-registry";
import { LensExtension } from "../lens-extension";
export interface PageMenuRegistration { export interface PageMenuRegistration {
target?: PageTarget; target?: PageTarget;
@ -43,7 +42,6 @@ export interface PageMenuComponents {
} }
export class PageMenuRegistry<T extends PageMenuRegistration> extends BaseRegistry<T> { export class PageMenuRegistry<T extends PageMenuRegistration> extends BaseRegistry<T> {
@action
add(items: T[], ext: LensExtension) { add(items: T[], ext: LensExtension) {
const normalizedItems = items.map(menuItem => { const normalizedItems = items.map(menuItem => {
menuItem.target = { menuItem.target = {

View File

@ -24,9 +24,8 @@
import React from "react"; import React from "react";
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import { BaseRegistry } from "./base-registry"; import { BaseRegistry } from "./base-registry";
import { LensExtension, sanitizeExtensionName } from "../lens-extension"; import { LensExtension, LensExtensionId, sanitizeExtensionName } from "../lens-extension";
import type { PageParam, PageParamInit } from "../../renderer/navigation/page-param"; import { createPageParam, PageParam, PageParamInit, searchParamsOptions } from "../../renderer/navigation";
import { createPageParam } from "../../renderer/navigation/helpers";
export interface PageRegistration { 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 * When not provided, first registered page without "id" would be used for page-menus without target.pageId for same extension
*/ */
id?: string; id?: string;
params?: PageParams<string | ExtensionPageParamInit>; params?: PageParams<string | Omit<PageParamInit<any>, "name" | "prefix">>;
components: PageComponents; components: PageComponents;
} }
// exclude "name" field since provided as key in page.params
export type ExtensionPageParamInit = Omit<PageParamInit, "name" | "isSystem">;
export interface PageComponents { export interface PageComponents {
Page: React.ComponentType<any>; Page: React.ComponentType<any>;
} }
export interface PageTarget<P = PageParams> { export interface PageTarget {
extensionId?: string; extensionId?: string;
pageId?: string; pageId?: string;
params?: P; params?: PageParams;
} }
export interface PageParams<V = any> { export interface PageParams<V = any> {
@ -83,13 +79,12 @@ export function getExtensionPageUrl(target: PageTarget): string {
if (registeredPage?.params) { if (registeredPage?.params) {
Object.entries(registeredPage.params).forEach(([name, param]) => { 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)) { param.stringify(targetParams[name]).forEach(value => {
pageUrl.searchParams.delete(name); if (searchParamsOptions.skipEmpty && !value) return;
} else { pageUrl.searchParams.append(name, value);
pageUrl.searchParams.set(name, paramValue); });
}
}); });
} }
@ -100,7 +95,7 @@ export class PageRegistry extends BaseRegistry<PageRegistration, RegisteredPage>
protected getRegisteredItem(page: PageRegistration, ext: LensExtension): RegisteredPage { protected getRegisteredItem(page: PageRegistration, ext: LensExtension): RegisteredPage {
const { id: pageId } = page; const { id: pageId } = page;
const extensionId = ext.name; 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 components = this.normalizeComponents(page.components, params);
const url = getExtensionPageUrl({ extensionId, pageId }); const url = getExtensionPageUrl({ extensionId, pageId });
@ -113,25 +108,48 @@ export class PageRegistry extends BaseRegistry<PageRegistration, RegisteredPage>
if (params) { if (params) {
const { Page } = components; const { Page } = components;
// inject extension's page component props.params
components.Page = observer((props: object) => React.createElement(Page, { params, ...props })); components.Page = observer((props: object) => React.createElement(Page, { params, ...props }));
} }
return components; return components;
} }
protected normalizeParams(params?: PageParams<string | ExtensionPageParamInit>): PageParams<PageParam> | undefined { protected normalizeParams(extensionId: LensExtensionId, params?: PageParams<string | Partial<PageParamInit>>): PageParams<PageParam> {
if (!params) { if (!params) return undefined;
return undefined; const normalizedParams: PageParams<PageParam> = {};
}
Object.entries(params).forEach(([name, value]) => {
const paramInit: PageParamInit = typeof value === "object"
? { name, ...value }
: { name, defaultValue: value };
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 { getByPageTarget(target: PageTarget): RegisteredPage | null {

View File

@ -19,15 +19,13 @@
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/ */
import { PageParam, PageParamInit } from "../../renderer/navigation/page-param"; import { navigation, PageParam, PageParamInit } from "../../renderer/navigation";
import { navigation } from "../../renderer/navigation";
export type { PageParamInit, PageParam } from "../../renderer/navigation/page-param"; export type { PageParamInit, PageParam } from "../../renderer/navigation/page-param";
export { navigate, isActiveRoute } from "../../renderer/navigation/helpers"; export { navigate, isActiveRoute } from "../../renderer/navigation/helpers";
export { hideDetails, showDetails, getDetailsUrl } from "../../renderer/components/kube-object/kube-object-details"; export { hideDetails, showDetails, getDetailsUrl } from "../../renderer/components/kube-object/kube-object-details";
export type { IURLParams } from "../../common/utils/buildUrl"; export type { IURLParams } from "../../common/utils/buildUrl";
// exporting to extensions-api version of helper without `isSystem` flag export function createPageParam<V>(init: PageParamInit<V>) {
export function createPageParam<V = string>(init: PageParamInit<V>) {
return new PageParam<V>(init, navigation); return new PageParam<V>(init, navigation);
} }

View File

@ -20,6 +20,11 @@
*/ */
import fetchMock from "jest-fetch-mock"; 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' // rewire global.fetch to call 'fetchMock'
fetchMock.enableMocks(); fetchMock.enableMocks();

View File

@ -19,13 +19,14 @@
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * 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 { broadcastMessage } from "../common/ipc";
import type { CatalogEntityRegistry} from "./catalog"; import type { CatalogEntityRegistry } from "./catalog";
import "../common/catalog-entities/kubernetes-cluster"; import "../common/catalog-entities/kubernetes-cluster";
import { toJS } from "../common/utils";
export function pushCatalogToRenderer(catalog: CatalogEntityRegistry) { export function pushCatalogToRenderer(catalog: CatalogEntityRegistry) {
return reaction(() => toJS(catalog.items, { recurseEverything: true }), (items) => { return reaction(() => toJS(catalog.items), (items) => {
broadcastMessage("catalog:items", items); broadcastMessage("catalog:items", items);
}, { }, {
fireImmediately: true, fireImmediately: true,

View File

@ -19,7 +19,7 @@
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * 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 type { CatalogEntity } from "../../common/catalog";
import { catalogEntityRegistry } from "../../main/catalog"; import { catalogEntityRegistry } from "../../main/catalog";
import { watch } from "chokidar"; import { watch } from "chokidar";
@ -45,6 +45,12 @@ export class KubeconfigSyncManager extends Singleton {
protected static readonly syncName = "lens:kube-sync"; protected static readonly syncName = "lens:kube-sync";
constructor() {
super();
makeObservable(this);
}
@action @action
startSync(): void { startSync(): void {
if (this.syncing) { if (this.syncing) {
@ -69,7 +75,7 @@ export class KubeconfigSyncManager extends Singleton {
this.startNewSync(filePath); this.startNewSync(filePath);
} }
this.syncListDisposer = UserStore.getInstance().syncKubeconfigEntries.observe(change => { this.syncListDisposer = observe(UserStore.getInstance().syncKubeconfigEntries, change => {
switch (change.type) { switch (change.type) {
case "add": case "add":
this.startNewSync(change.name); this.startNewSync(change.name);

View File

@ -19,14 +19,16 @@
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * 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 { CatalogCategoryRegistry, catalogCategoryRegistry, CatalogEntity } from "../../common/catalog";
import { iter } from "../../common/utils"; import { iter } from "../../common/utils";
export class CatalogEntityRegistry { 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>) { @action addObservableSource(id: string, source: IObservableArray<CatalogEntity>) {
this.sources.set(id, computed(() => source)); this.sources.set(id, computed(() => source));

View File

@ -22,7 +22,7 @@
import "../common/cluster-ipc"; import "../common/cluster-ipc";
import type http from "http"; import type http from "http";
import { ipcMain } from "electron"; 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 { ClusterStore, getClusterIdFromHost } from "../common/cluster-store";
import type { Cluster } from "./cluster"; import type { Cluster } from "./cluster";
import logger from "./logger"; import logger from "./logger";
@ -32,38 +32,47 @@ import { catalogEntityRegistry } from "./catalog";
import { KubernetesCluster, KubernetesClusterPrometheusMetrics } from "../common/catalog-entities/kubernetes-cluster"; import { KubernetesCluster, KubernetesClusterPrometheusMetrics } from "../common/catalog-entities/kubernetes-cluster";
export class ClusterManager extends Singleton { export class ClusterManager extends Singleton {
private store = ClusterStore.getInstance();
constructor() { constructor() {
super(); super();
makeObservable(this);
this.bindEvents();
}
reaction(() => toJS(ClusterStore.getInstance().clustersList, { recurseEverything: true }), () => { private bindEvents() {
this.updateCatalog(ClusterStore.getInstance().clustersList); // reacting to every cluster's state change and total amount of items
}, { fireImmediately: true }); 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) => { reaction(() => catalogEntityRegistry.getItemsForApiKind<KubernetesCluster>("entity.k8slens.dev/v1alpha1", "KubernetesCluster"), (entities) => {
this.syncClustersFromCatalog(entities); this.syncClustersFromCatalog(entities);
}); });
// auto-stop removed clusters // auto-stop removed clusters
autorun(() => { autorun(() => {
const removedClusters = Array.from(ClusterStore.getInstance().removedClusters.values()); const removedClusters = Array.from(this.store.removedClusters.values());
if (removedClusters.length > 0) { if (removedClusters.length > 0) {
const meta = removedClusters.map(cluster => cluster.getMeta()); const meta = removedClusters.map(cluster => cluster.getMeta());
logger.info(`[CLUSTER-MANAGER]: removing clusters`, meta); logger.info(`[CLUSTER-MANAGER]: removing clusters`, meta);
removedClusters.forEach(cluster => cluster.disconnect()); removedClusters.forEach(cluster => cluster.disconnect());
ClusterStore.getInstance().removedClusters.clear(); this.store.removedClusters.clear();
} }
}, { }, {
delay: 250 delay: 250
}); });
ipcMain.on("network:offline", () => { this.onNetworkOffline(); }); ipcMain.on("network:offline", this.onNetworkOffline);
ipcMain.on("network:online", () => { this.onNetworkOnline(); }); ipcMain.on("network:online", this.onNetworkOnline);
} }
@action protected updateCatalog(clusters: Cluster[]) { @action
protected updateCatalog(clusters: Cluster[]) {
for (const cluster of clusters) { for (const cluster of clusters) {
const index = catalogEntityRegistry.items.findIndex((entity) => entity.metadata.uid === cluster.id); const index = catalogEntityRegistry.items.findIndex((entity) => entity.metadata.uid === cluster.id);
@ -94,10 +103,10 @@ export class ClusterManager extends Singleton {
@action syncClustersFromCatalog(entities: KubernetesCluster[]) { @action syncClustersFromCatalog(entities: KubernetesCluster[]) {
for (const entity of entities) { for (const entity of entities) {
const cluster = ClusterStore.getInstance().getById(entity.metadata.uid); const cluster = this.store.getById(entity.metadata.uid);
if (!cluster) { if (!cluster) {
ClusterStore.getInstance().addCluster({ this.store.addCluster({
id: entity.metadata.uid, id: entity.metadata.uid,
preferences: { preferences: {
clusterName: entity.metadata.name clusterName: entity.metadata.name
@ -117,28 +126,28 @@ export class ClusterManager extends Singleton {
} }
} }
protected onNetworkOffline() { protected onNetworkOffline = () => {
logger.info("[CLUSTER-MANAGER]: network is offline"); logger.info("[CLUSTER-MANAGER]: network is offline");
ClusterStore.getInstance().clustersList.forEach((cluster) => { this.store.clustersList.forEach((cluster) => {
if (!cluster.disconnected) { if (!cluster.disconnected) {
cluster.online = false; cluster.online = false;
cluster.accessible = false; cluster.accessible = false;
cluster.refreshConnectionStatus().catch((e) => e); cluster.refreshConnectionStatus().catch((e) => e);
} }
}); });
} };
protected onNetworkOnline() { protected onNetworkOnline = () => {
logger.info("[CLUSTER-MANAGER]: network is online"); logger.info("[CLUSTER-MANAGER]: network is online");
ClusterStore.getInstance().clustersList.forEach((cluster) => { this.store.clustersList.forEach((cluster) => {
if (!cluster.disconnected) { if (!cluster.disconnected) {
cluster.refreshConnectionStatus().catch((e) => e); cluster.refreshConnectionStatus().catch((e) => e);
} }
}); });
} };
stop() { stop() {
ClusterStore.getInstance().clusters.forEach((cluster: Cluster) => { this.store.clusters.forEach((cluster: Cluster) => {
cluster.disconnect(); cluster.disconnect();
}); });
} }
@ -150,18 +159,18 @@ export class ClusterManager extends Singleton {
if (req.headers.host.startsWith("127.0.0.1")) { if (req.headers.host.startsWith("127.0.0.1")) {
const clusterId = req.url.split("/")[1]; const clusterId = req.url.split("/")[1];
cluster = ClusterStore.getInstance().getById(clusterId); cluster = this.store.getById(clusterId);
if (cluster) { if (cluster) {
// we need to swap path prefix so that request is proxied to kube api // we need to swap path prefix so that request is proxied to kube api
req.url = req.url.replace(`/${clusterId}`, apiKubePrefix); req.url = req.url.replace(`/${clusterId}`, apiKubePrefix);
} }
} else if (req.headers["x-cluster-id"]) { } 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 { } else {
const clusterId = getClusterIdFromHost(req.headers.host); const clusterId = getClusterIdFromHost(req.headers.host);
cluster = ClusterStore.getInstance().getById(clusterId); cluster = this.store.getById(clusterId);
} }
return cluster; return cluster;
@ -169,9 +178,7 @@ export class ClusterManager extends Singleton {
} }
export function catalogEntityFromCluster(cluster: Cluster) { export function catalogEntityFromCluster(cluster: Cluster) {
return new KubernetesCluster(toJS({ return new KubernetesCluster({
apiVersion: "entity.k8slens.dev/v1alpha1",
kind: "KubernetesCluster",
metadata: { metadata: {
uid: cluster.id, uid: cluster.id,
name: cluster.name, name: cluster.name,
@ -190,5 +197,5 @@ export function catalogEntityFromCluster(cluster: Cluster) {
message: "", message: "",
active: !cluster.disconnected active: !cluster.disconnected
} }
})); });
} }

View File

@ -21,7 +21,7 @@
import { ipcMain } from "electron"; import { ipcMain } from "electron";
import type { ClusterId, ClusterMetadata, ClusterModel, ClusterPreferences, ClusterPrometheusPreferences, UpdateClusterModel } from "../common/cluster-store"; 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 { broadcastMessage, ClusterListNamespaceForbiddenChannel } from "../common/ipc";
import { ContextHandler } from "./context-handler"; import { ContextHandler } from "./context-handler";
import { AuthorizationV1Api, CoreV1Api, HttpError, KubeConfig, V1ResourceAttributes } from "@kubernetes/client-node"; 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 { VersionDetector } from "./cluster-detectors/version-detector";
import { detectorRegistry } from "./cluster-detectors/detector-registry"; import { detectorRegistry } from "./cluster-detectors/detector-registry";
import plimit from "p-limit"; import plimit from "p-limit";
import { toJS } from "../common/utils";
export enum ClusterStatus { export enum ClusterStatus {
AccessGranted = 2, AccessGranted = 2,
@ -91,7 +92,9 @@ export class Cluster implements ClusterModel, ClusterState {
protected activated = false; protected activated = false;
private resourceAccessStatuses: Map<KubeApiResource, boolean> = new Map(); private resourceAccessStatuses: Map<KubeApiResource, boolean> = new Map();
whenReady = when(() => this.ready); get whenReady() {
return when(() => this.ready);
}
/** /**
* Kubeconfig context name * Kubeconfig context name
@ -227,9 +230,7 @@ export class Cluster implements ClusterModel, ClusterState {
@computed get prometheusPreferences(): ClusterPrometheusPreferences { @computed get prometheusPreferences(): ClusterPrometheusPreferences {
const { prometheus, prometheusProvider } = this.preferences; const { prometheus, prometheusProvider } = this.preferences;
return toJS({ prometheus, prometheusProvider }, { return toJS({ prometheus, prometheusProvider });
recurseEverything: true,
});
} }
/** /**
@ -240,6 +241,7 @@ export class Cluster implements ClusterModel, ClusterState {
} }
constructor(model: ClusterModel) { constructor(model: ClusterModel) {
makeObservable(this);
this.id = model.id; this.id = model.id;
this.updateModel(model); this.updateModel(model);
@ -570,9 +572,7 @@ export class Cluster implements ClusterModel, ClusterState {
accessibleNamespaces: this.accessibleNamespaces, accessibleNamespaces: this.accessibleNamespaces,
}; };
return toJS(model, { return toJS(model);
recurseEverything: true
});
} }
/** /**
@ -592,9 +592,7 @@ export class Cluster implements ClusterModel, ClusterState {
isGlobalWatchEnabled: this.isGlobalWatchEnabled, isGlobalWatchEnabled: this.isGlobalWatchEnabled,
}; };
return toJS(state, { return toJS(state);
recurseEverything: true
});
} }
/** /**

View File

@ -23,23 +23,25 @@ import { randomBytes } from "crypto";
import { SHA256 } from "crypto-js"; import { SHA256 } from "crypto-js";
import { app, remote } from "electron"; import { app, remote } from "electron";
import fse from "fs-extra"; import fse from "fs-extra";
import { action, observable, toJS } from "mobx"; import { action, makeObservable, observable } from "mobx";
import path from "path"; import path from "path";
import { BaseStore } from "../common/base-store"; import { BaseStore } from "../common/base-store";
import type { LensExtensionId } from "../extensions/lens-extension"; import type { LensExtensionId } from "../extensions/lens-extension";
import { toJS } from "../common/utils";
interface FSProvisionModel { interface FSProvisionModel {
extensions: Record<string, string>; // extension names to paths extensions: Record<string, string>; // extension names to paths
} }
export class FilesystemProvisionerStore extends BaseStore<FSProvisionModel> { export class FilesystemProvisionerStore extends BaseStore<FSProvisionModel> {
@observable registeredExtensions = observable.map<LensExtensionId, string>(); registeredExtensions = observable.map<LensExtensionId, string>();
constructor() { constructor() {
super({ super({
configName: "lens-filesystem-provisioner-store", configName: "lens-filesystem-provisioner-store",
accessPropertiesByDotNotation: false, // To make dots safe in cluster context names accessPropertiesByDotNotation: false, // To make dots safe in cluster context names
}); });
makeObservable(this);
} }
/** /**
@ -71,9 +73,7 @@ export class FilesystemProvisionerStore extends BaseStore<FSProvisionModel> {
toJSON(): FSProvisionModel { toJSON(): FSProvisionModel {
return toJS({ return toJS({
extensions: this.registeredExtensions.toJSON(), extensions: Object.fromEntries(this.registeredExtensions),
}, {
recurseEverything: true
}); });
} }
} }

View File

@ -24,7 +24,7 @@
import "../common/system-ca"; import "../common/system-ca";
import "../common/prometheus-providers"; import "../common/prometheus-providers";
import * as Mobx from "mobx"; 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 { app, autoUpdater, ipcMain, dialog, powerMonitor } from "electron";
import { appName, isMac, productName } from "../common/vars"; import { appName, isMac, productName } from "../common/vars";
import path from "path"; import path from "path";
@ -55,6 +55,7 @@ import { HotbarStore } from "../common/hotbar-store";
import { HelmRepoManager } from "./helm/helm-repo-manager"; import { HelmRepoManager } from "./helm/helm-repo-manager";
import { KubeconfigSyncManager } from "./catalog-sources"; import { KubeconfigSyncManager } from "./catalog-sources";
import { handleWsUpgrade } from "./proxy/ws-upgrade"; import { handleWsUpgrade } from "./proxy/ws-upgrade";
import configurePackages from "../common/configure-packages";
const workingDir = path.join(app.getPath("appData"), appName); const workingDir = path.join(app.getPath("appData"), appName);
const cleanup = disposer(); const cleanup = disposer();
@ -77,6 +78,7 @@ if (process.env.LENS_DISABLE_GPU) {
app.disableHardwareAcceleration(); app.disableHardwareAcceleration();
} }
configurePackages();
mangleProxyEnv(); mangleProxyEnv();
if (app.commandLine.getSwitchValue("proxy-server") !== "") { if (app.commandLine.getSwitchValue("proxy-server") !== "") {
@ -191,15 +193,11 @@ app.on("ready", async () => {
cleanup.push(pushCatalogToRenderer(catalogEntityRegistry)); cleanup.push(pushCatalogToRenderer(catalogEntityRegistry));
KubeconfigSyncManager.getInstance().startSync(); KubeconfigSyncManager.getInstance().startSync();
startUpdateChecking(); startUpdateChecking();
LensProtocolRouterMain LensProtocolRouterMain.getInstance().rendererLoaded = true;
.getInstance()
.rendererLoaded = true;
}); });
ExtensionLoader.getInstance().whenLoaded.then(() => { ExtensionLoader.getInstance().whenLoaded.then(() => {
LensProtocolRouterMain LensProtocolRouterMain.getInstance().extensionsLoaded = true;
.getInstance()
.extensionsLoaded = true;
}); });
logger.info("🧩 Initializing extensions"); 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 })); .catch(error => logger.error(`${LensProtocolRouterMain.LoggingPrefix}: an error occured`, { error, rawUrl }));
}); });
// Extensions-api runtime exports /**
export const LensExtensionsApi = { * Exports for virtual package "@k8slens/extensions" for main-process.
...LensExtensions, * All exporting names available in global runtime scope:
* e.g. global.Mobx, global.LensExtensions
*/
const LensExtensions = {
...LensExtensionsCoreApi,
}; };
export { export {
Mobx, Mobx,
LensExtensionsApi as LensExtensions, LensExtensions,
}; };

View File

@ -24,7 +24,7 @@ import * as proto from "../../common/protocol-handler";
import Url from "url-parse"; import Url from "url-parse";
import type { LensExtension } from "../../extensions/lens-extension"; import type { LensExtension } from "../../extensions/lens-extension";
import { broadcastMessage } from "../../common/ipc"; import { broadcastMessage } from "../../common/ipc";
import { observable, when } from "mobx"; import { observable, when, makeObservable } from "mobx";
export interface FallbackHandler { export interface FallbackHandler {
(name: string): Promise<boolean>; (name: string): Promise<boolean>;
@ -36,6 +36,12 @@ export class LensProtocolRouterMain extends proto.LensProtocolRouter {
@observable rendererLoaded = false; @observable rendererLoaded = false;
@observable extensionsLoaded = false; @observable extensionsLoaded = false;
constructor() {
super();
makeObservable(this);
}
/** /**
* Find the most specific registered handler, if it exists, and invoke it. * Find the most specific registered handler, if it exists, and invoke it.
* *

View File

@ -20,7 +20,7 @@
*/ */
import type { ClusterId } from "../common/cluster-store"; 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 { app, BrowserWindow, dialog, shell, webContents } from "electron";
import windowStateKeeper from "electron-window-state"; import windowStateKeeper from "electron-window-state";
import { appEventBus } from "../common/event-bus"; import { appEventBus } from "../common/event-bus";
@ -44,6 +44,7 @@ export class WindowManager extends Singleton {
constructor() { constructor() {
super(); super();
makeObservable(this);
this.bindEvents(); this.bindEvents();
this.initMenu(); this.initMenu();
this.initTray(); this.initTray();

View File

@ -21,15 +21,19 @@
import type { KubeObjectStore } from "../kube-object.store"; import type { KubeObjectStore } from "../kube-object.store";
import { action, observable } from "mobx"; import { action, observable, makeObservable } from "mobx";
import { autobind } from "../utils"; import { autoBind } from "../utils";
import { KubeApi, parseKubeApi } from "./kube-api"; import { KubeApi, parseKubeApi } from "./kube-api";
@autobind()
export class ApiManager { export class ApiManager {
private apis = observable.map<string, KubeApi>(); private apis = observable.map<string, KubeApi>();
private stores = observable.map<string, KubeObjectStore>(); private stores = observable.map<string, KubeObjectStore>();
constructor() {
makeObservable(this);
autoBind(this);
}
getApi(pathOrCallback: string | ((api: KubeApi) => boolean)) { getApi(pathOrCallback: string | ((api: KubeApi) => boolean)) {
if (typeof pathOrCallback === "string") { if (typeof pathOrCallback === "string") {
return this.apis.get(pathOrCallback) || this.apis.get(parseKubeApi(pathOrCallback).apiBase); 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. * 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 { subscribeToBroadcast } from "../../common/ipc";
import { CatalogCategory, CatalogEntity, CatalogEntityData, catalogCategoryRegistry, CatalogCategoryRegistry, CatalogEntityKindData } from "../../common/catalog"; import { CatalogCategory, CatalogEntity, CatalogEntityData, catalogCategoryRegistry, CatalogCategoryRegistry, CatalogEntityKindData } from "../../common/catalog";
import "../../common/catalog-entities"; import "../../common/catalog-entities";
@ -29,7 +29,9 @@ export class CatalogEntityRegistry {
protected rawItems = observable.array<CatalogEntityData & CatalogEntityKindData>([], { deep: true }); protected rawItems = observable.array<CatalogEntityData & CatalogEntityKindData>([], { deep: true });
@observable protected _activeEntity: CatalogEntity; @observable protected _activeEntity: CatalogEntity;
constructor(private categoryRegistry: CatalogCategoryRegistry) {} constructor(private categoryRegistry: CatalogCategoryRegistry) {
makeObservable(this);
}
init() { init() {
subscribeToBroadcast("catalog:items", (ev, items: (CatalogEntityData & CatalogEntityKindData)[]) => { 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. * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/ */
import { autobind } from "../../utils";
import { Role } from "./role.api"; import { Role } from "./role.api";
import { KubeApi } from "../kube-api"; import { KubeApi } from "../kube-api";
@autobind()
export class ClusterRole extends Role { export class ClusterRole extends Role {
static kind = "ClusterRole"; static kind = "ClusterRole";
static namespaced = false; static namespaced = false;

View File

@ -71,10 +71,7 @@ export interface IClusterMetrics<T = IMetrics> {
fsUsage: T; fsUsage: T;
} }
export class Cluster extends KubeObject { export interface Cluster {
static kind = "Cluster";
static apiBase = "/apis/cluster.k8s.io/v1alpha1/clusters";
spec: { spec: {
clusterNetwork?: { clusterNetwork?: {
serviceDomain?: string; serviceDomain?: string;
@ -106,6 +103,11 @@ export class Cluster extends KubeObject {
errorMessage?: string; errorMessage?: string;
errorReason?: string; errorReason?: string;
}; };
}
export class Cluster extends KubeObject {
static kind = "Cluster";
static apiBase = "/apis/cluster.k8s.io/v1alpha1/clusters";
getStatus() { getStatus() {
if (this.metadata.deletionTimestamp) return ClusterStatus.REMOVING; if (this.metadata.deletionTimestamp) return ClusterStatus.REMOVING;

View File

@ -28,13 +28,15 @@ export interface IComponentStatusCondition {
message: string; message: string;
} }
export interface ComponentStatus {
conditions: IComponentStatusCondition[];
}
export class ComponentStatus extends KubeObject { export class ComponentStatus extends KubeObject {
static kind = "ComponentStatus"; static kind = "ComponentStatus";
static namespaced = false; static namespaced = false;
static apiBase = "/api/v1/componentstatuses"; static apiBase = "/api/v1/componentstatuses";
conditions: IComponentStatusCondition[];
getTruthyConditions() { getTruthyConditions() {
return this.conditions.filter(c => c.status === "True"); return this.conditions.filter(c => c.status === "True");
} }

View File

@ -21,10 +21,15 @@
import { KubeObject } from "../kube-object"; import { KubeObject } from "../kube-object";
import type { KubeJsonApiData } from "../kube-json-api"; import type { KubeJsonApiData } from "../kube-json-api";
import { autobind } from "../../utils";
import { KubeApi } from "../kube-api"; import { KubeApi } from "../kube-api";
import { autoBind } from "../../../common/utils";
export interface ConfigMap {
data: {
[param: string]: string;
};
}
@autobind()
export class ConfigMap extends KubeObject { export class ConfigMap extends KubeObject {
static kind = "ConfigMap"; static kind = "ConfigMap";
static namespaced = true; static namespaced = true;
@ -32,12 +37,10 @@ export class ConfigMap extends KubeObject {
constructor(data: KubeJsonApiData) { constructor(data: KubeJsonApiData) {
super(data); super(data);
this.data = this.data || {}; autoBind(this);
}
data: { this.data ??= {};
[param: string]: string; }
};
getKeys(): string[] { getKeys(): string[] {
return Object.keys(this.data); return Object.keys(this.data);

View File

@ -38,11 +38,7 @@ type AdditionalPrinterColumnsV1Beta = AdditionalPrinterColumnsCommon & {
JSONPath: string; JSONPath: string;
}; };
export class CustomResourceDefinition extends KubeObject { export interface CustomResourceDefinition {
static kind = "CustomResourceDefinition";
static namespaced = false;
static apiBase = "/apis/apiextensions.k8s.io/v1/customresourcedefinitions";
spec: { spec: {
group: string; group: string;
version?: string; // deprecated in v1 api version?: string; // deprecated in v1 api
@ -84,6 +80,12 @@ export class CustomResourceDefinition extends KubeObject {
}; };
storedVersions: string[]; storedVersions: string[];
}; };
}
export class CustomResourceDefinition extends KubeObject {
static kind = "CustomResourceDefinition";
static namespaced = false;
static apiBase = "/apis/apiextensions.k8s.io/v1/customresourcedefinitions";
getResourceUrl() { getResourceUrl() {
return crdResourcesURL({ return crdResourcesURL({

View File

@ -23,8 +23,9 @@ import moment from "moment";
import { KubeObject } from "../kube-object"; import { KubeObject } from "../kube-object";
import type { IPodContainer } from "./pods.api"; import type { IPodContainer } from "./pods.api";
import { formatDuration } from "../../utils/formatDuration"; import { formatDuration } from "../../utils/formatDuration";
import { autobind } from "../../utils"; import { autoBind } from "../../utils";
import { KubeApi } from "../kube-api"; import { KubeApi } from "../kube-api";
import type { KubeJsonApiData } from "../kube-json-api";
export class CronJobApi extends KubeApi<CronJob> { export class CronJobApi extends KubeApi<CronJob> {
suspend(params: { namespace: string; name: string }) { suspend(params: { namespace: string; name: string }) {
@ -58,28 +59,7 @@ export class CronJobApi extends KubeApi<CronJob> {
} }
} }
@autobind() export interface CronJob {
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;
};
};
spec: { spec: {
schedule: string; schedule: string;
concurrencyPolicy: string; concurrencyPolicy: string;
@ -116,6 +96,17 @@ export class CronJob extends KubeObject {
status: { status: {
lastScheduleTime?: string; 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() { getSuspendFlag() {
return this.spec.suspend.toString(); return this.spec.suspend.toString();

View File

@ -22,16 +22,21 @@
import get from "lodash/get"; import get from "lodash/get";
import type { IPodContainer } from "./pods.api"; import type { IPodContainer } from "./pods.api";
import { IAffinity, WorkloadKubeObject } from "../workload-kube-object"; import { IAffinity, WorkloadKubeObject } from "../workload-kube-object";
import { autobind } from "../../utils"; import { autoBind } from "../../utils";
import { KubeApi } from "../kube-api"; import { KubeApi } from "../kube-api";
import type { KubeJsonApiData } from "../kube-json-api";
@autobind()
export class DaemonSet extends WorkloadKubeObject { export class DaemonSet extends WorkloadKubeObject {
static kind = "DaemonSet"; static kind = "DaemonSet";
static namespaced = true; static namespaced = true;
static apiBase = "/apis/apps/v1/daemonsets"; static apiBase = "/apis/apps/v1/daemonsets";
spec: { constructor(data: KubeJsonApiData) {
super(data);
autoBind(this);
}
declare spec: {
selector: { selector: {
matchLabels: { matchLabels: {
[name: string]: string; [name: string]: string;
@ -73,7 +78,7 @@ export class DaemonSet extends WorkloadKubeObject {
}; };
revisionHistoryLimit: number; revisionHistoryLimit: number;
}; };
status: { declare status: {
currentNumberScheduled: number; currentNumberScheduled: number;
numberMisscheduled: number; numberMisscheduled: number;
desiredNumberScheduled: number; desiredNumberScheduled: number;

View File

@ -22,8 +22,9 @@
import moment from "moment"; import moment from "moment";
import { IAffinity, WorkloadKubeObject } from "../workload-kube-object"; import { IAffinity, WorkloadKubeObject } from "../workload-kube-object";
import { autobind } from "../../utils"; import { autoBind } from "../../utils";
import { KubeApi } from "../kube-api"; import { KubeApi } from "../kube-api";
import type { KubeJsonApiData } from "../kube-json-api";
export class DeploymentApi extends KubeApi<Deployment> { export class DeploymentApi extends KubeApi<Deployment> {
protected getScaleApiUrl(params: { namespace: string; name: string }) { protected getScaleApiUrl(params: { namespace: string; name: string }) {
@ -87,13 +88,17 @@ interface IContainerProbe {
failureThreshold?: number; failureThreshold?: number;
} }
@autobind()
export class Deployment extends WorkloadKubeObject { export class Deployment extends WorkloadKubeObject {
static kind = "Deployment"; static kind = "Deployment";
static namespaced = true; static namespaced = true;
static apiBase = "/apis/apps/v1/deployments"; static apiBase = "/apis/apps/v1/deployments";
spec: { constructor(data: KubeJsonApiData) {
super(data);
autoBind(this);
}
declare spec: {
replicas: number; replicas: number;
selector: { matchLabels: { [app: string]: string } }; selector: { matchLabels: { [app: string]: string } };
template: { template: {
@ -172,7 +177,7 @@ export class Deployment extends WorkloadKubeObject {
}; };
}; };
}; };
status: { declare status: {
observedGeneration: number; observedGeneration: number;
replicas: number; replicas: number;
updatedReplicas: number; updatedReplicas: number;

View File

@ -19,9 +19,10 @@
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * 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 { KubeObject } from "../kube-object";
import { KubeApi } from "../kube-api"; import { KubeApi } from "../kube-api";
import type { KubeJsonApiData } from "../kube-json-api";
export interface IEndpointPort { export interface IEndpointPort {
name?: string; name?: string;
@ -121,13 +122,19 @@ export class EndpointSubset implements IEndpointSubset {
} }
} }
@autobind() export interface Endpoint {
subsets: IEndpointSubset[];
}
export class Endpoint extends KubeObject { export class Endpoint extends KubeObject {
static kind = "Endpoints"; static kind = "Endpoints";
static namespaced = true; static namespaced = true;
static apiBase = "/api/v1/endpoints"; static apiBase = "/api/v1/endpoints";
subsets: IEndpointSubset[]; constructor(data: KubeJsonApiData) {
super(data);
autoBind(this);
}
getEndpointSubsets(): EndpointSubset[] { getEndpointSubsets(): EndpointSubset[] {
const subsets = this.subsets || []; const subsets = this.subsets || [];

View File

@ -22,15 +22,9 @@
import moment from "moment"; import moment from "moment";
import { KubeObject } from "../kube-object"; import { KubeObject } from "../kube-object";
import { formatDuration } from "../../utils/formatDuration"; import { formatDuration } from "../../utils/formatDuration";
import { autobind } from "../../utils";
import { KubeApi } from "../kube-api"; import { KubeApi } from "../kube-api";
@autobind() export interface KubeEvent {
export class KubeEvent extends KubeObject {
static kind = "Event";
static namespaced = true;
static apiBase = "/api/v1/events";
involvedObject: { involvedObject: {
kind: string; kind: string;
namespace: string; namespace: string;
@ -53,6 +47,12 @@ export class KubeEvent extends KubeObject {
eventTime: null; eventTime: null;
reportingComponent: string; reportingComponent: string;
reportingInstance: string; reportingInstance: string;
}
export class KubeEvent extends KubeObject {
static kind = "Event";
static namespaced = true;
static apiBase = "/api/v1/events";
isWarning() { isWarning() {
return this.type === "Warning"; return this.type === "Warning";

View File

@ -22,7 +22,7 @@
import { compile } from "path-to-regexp"; import { compile } from "path-to-regexp";
import { apiBase } from "../index"; import { apiBase } from "../index";
import { stringify } from "querystring"; import { stringify } from "querystring";
import { autobind } from "../../utils"; import { autoBind } from "../../utils";
export type RepoHelmChartList = Record<string, HelmChart[]>; export type RepoHelmChartList = Record<string, HelmChart[]>;
export type HelmChartList = Record<string, RepoHelmChartList>; 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 })}`); return apiBase.get<string>(`/v2/charts/${repo}/${name}/values?${stringify({ version })}`);
} }
@autobind() export interface HelmChart {
export class HelmChart {
constructor(data: any) {
Object.assign(this, data);
}
static create(data: any) {
return new HelmChart(data);
}
apiVersion: string; apiVersion: string;
name: string; name: string;
version: string; version: string;
@ -114,6 +105,17 @@ export class HelmChart {
appVersion?: string; appVersion?: string;
deprecated?: boolean; deprecated?: boolean;
tillerVersion?: string; tillerVersion?: string;
}
export class HelmChart {
constructor(data: HelmChart) {
Object.assign(this, data);
autoBind(this);
}
static create(data: any) {
return new HelmChart(data);
}
getId() { getId() {
return `${this.repo}:${this.apiVersion}/${this.name}@${this.getAppVersion()}+${this.digest}`; return `${this.repo}:${this.apiVersion}/${this.name}@${this.getAppVersion()}+${this.digest}`;

View File

@ -21,7 +21,7 @@
import jsYaml from "js-yaml"; import jsYaml from "js-yaml";
import { compile } from "path-to-regexp"; import { compile } from "path-to-regexp";
import { autobind, formatDuration } from "../../utils"; import { autoBind, formatDuration } from "../../utils";
import capitalize from "lodash/capitalize"; import capitalize from "lodash/capitalize";
import { apiBase } from "../index"; import { apiBase } from "../index";
import { helmChartStore } from "../../components/+apps-helm-charts/helm-chart.store"; 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 interface HelmRelease {
export class HelmRelease implements ItemObject {
constructor(data: any) {
Object.assign(this, data);
}
static create(data: any) {
return new HelmRelease(data);
}
appVersion: string; appVersion: string;
name: string; name: string;
namespace: string; namespace: string;
@ -172,6 +163,17 @@ export class HelmRelease implements ItemObject {
status: string; status: string;
updated: string; updated: string;
revision: 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() { getId() {
return this.namespace + this.name; return this.namespace + this.name;

View File

@ -59,11 +59,7 @@ export interface IHpaMetric {
}>; }>;
} }
export class HorizontalPodAutoscaler extends KubeObject { export interface HorizontalPodAutoscaler {
static kind = "HorizontalPodAutoscaler";
static namespaced = true;
static apiBase = "/apis/autoscaling/v2beta1/horizontalpodautoscalers";
spec: { spec: {
scaleTargetRef: { scaleTargetRef: {
kind: string; kind: string;
@ -86,6 +82,12 @@ export class HorizontalPodAutoscaler extends KubeObject {
type: string; type: string;
}[]; }[];
}; };
}
export class HorizontalPodAutoscaler extends KubeObject {
static kind = "HorizontalPodAutoscaler";
static namespaced = true;
static apiBase = "/apis/autoscaling/v2beta1/horizontalpodautoscalers";
getMaxPods() { getMaxPods() {
return this.spec.maxReplicas || 0; return this.spec.maxReplicas || 0;

View File

@ -20,9 +20,10 @@
*/ */
import { KubeObject } from "../kube-object"; import { KubeObject } from "../kube-object";
import { autobind } from "../../utils"; import { autoBind } from "../../utils";
import { IMetrics, metricsApi } from "./metrics.api"; import { IMetrics, metricsApi } from "./metrics.api";
import { KubeApi } from "../kube-api"; import { KubeApi } from "../kube-api";
import type { KubeJsonApiData } from "../kube-json-api";
export class IngressApi extends KubeApi<Ingress> { export class IngressApi extends KubeApi<Ingress> {
getMetrics(ingress: string, namespace: string): Promise<IIngressMetrics> { getMetrics(ingress: string, namespace: string): Promise<IIngressMetrics> {
@ -82,12 +83,7 @@ export const getBackendServiceNamePort = (backend: IIngressBackend) => {
return { serviceName, servicePort }; return { serviceName, servicePort };
}; };
@autobind() export interface Ingress {
export class Ingress extends KubeObject {
static kind = "Ingress";
static namespaced = true;
static apiBase = "/apis/networking.k8s.io/v1/ingresses";
spec: { spec: {
tls: { tls: {
secretName: string; secretName: string;
@ -117,6 +113,17 @@ export class Ingress extends KubeObject {
ingress: ILoadBalancerIngress[]; 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() { getRoutes() {
const { spec: { tls, rules } } = this; const { spec: { tls, rules } } = this;

View File

@ -20,19 +20,24 @@
*/ */
import get from "lodash/get"; import get from "lodash/get";
import { autobind } from "../../utils"; import { autoBind } from "../../utils";
import { IAffinity, WorkloadKubeObject } from "../workload-kube-object"; import { IAffinity, WorkloadKubeObject } from "../workload-kube-object";
import type { IPodContainer } from "./pods.api"; import type { IPodContainer } from "./pods.api";
import { KubeApi } from "../kube-api"; import { KubeApi } from "../kube-api";
import type { JsonApiParams } from "../json-api"; import type { JsonApiParams } from "../json-api";
import type { KubeJsonApiData } from "../kube-json-api";
@autobind()
export class Job extends WorkloadKubeObject { export class Job extends WorkloadKubeObject {
static kind = "Job"; static kind = "Job";
static namespaced = true; static namespaced = true;
static apiBase = "/apis/batch/v1/jobs"; static apiBase = "/apis/batch/v1/jobs";
spec: { constructor(data: KubeJsonApiData) {
super(data);
autoBind(this);
}
declare spec: {
parallelism?: number; parallelism?: number;
completions?: number; completions?: number;
backoffLimit?: number; backoffLimit?: number;
@ -78,7 +83,7 @@ export class Job extends WorkloadKubeObject {
serviceAccount?: string; serviceAccount?: string;
schedulerName?: string; schedulerName?: string;
}; };
status: { declare status: {
conditions: { conditions: {
type: string; type: string;
status: string; status: string;

View File

@ -21,7 +21,8 @@
import { KubeObject } from "../kube-object"; import { KubeObject } from "../kube-object";
import { KubeApi } from "../kube-api"; import { KubeApi } from "../kube-api";
import { autobind } from "../../utils"; import { autoBind } from "../../utils";
import type { KubeJsonApiData } from "../kube-json-api";
export enum LimitType { export enum LimitType {
CONTAINER = "Container", CONTAINER = "Container",
@ -50,15 +51,21 @@ export interface LimitRangeItem extends LimitRangeParts {
type: string type: string
} }
@autobind() export interface LimitRange {
spec: {
limits: LimitRangeItem[];
};
}
export class LimitRange extends KubeObject { export class LimitRange extends KubeObject {
static kind = "LimitRange"; static kind = "LimitRange";
static namespaced = true; static namespaced = true;
static apiBase = "/api/v1/limitranges"; static apiBase = "/api/v1/limitranges";
spec: { constructor(data: KubeJsonApiData) {
limits: LimitRangeItem[]; super(data);
}; autoBind(this);
}
getContainerLimits() { getContainerLimits() {
return this.spec.limits.filter(limit => limit.type === LimitType.CONTAINER); return this.spec.limits.filter(limit => limit.type === LimitType.CONTAINER);

View File

@ -21,25 +21,32 @@
import { KubeApi } from "../kube-api"; import { KubeApi } from "../kube-api";
import { KubeObject } from "../kube-object"; import { KubeObject } from "../kube-object";
import { autobind } from "../../utils"; import { autoBind } from "../../utils";
import type { KubeJsonApiData } from "../kube-json-api";
export enum NamespaceStatus { export enum NamespaceStatus {
ACTIVE = "Active", ACTIVE = "Active",
TERMINATING = "Terminating", TERMINATING = "Terminating",
} }
@autobind() export interface Namespace {
status?: {
phase: string;
};
}
export class Namespace extends KubeObject { export class Namespace extends KubeObject {
static kind = "Namespace"; static kind = "Namespace";
static namespaced = false; static namespaced = false;
static apiBase = "/api/v1/namespaces"; static apiBase = "/api/v1/namespaces";
status?: { constructor(data: KubeJsonApiData) {
phase: string; super(data);
}; autoBind(this);
}
getStatus() { getStatus() {
return this.status ? this.status.phase : "-"; return this.status?.phase ?? "-";
} }
} }

View File

@ -20,8 +20,9 @@
*/ */
import { KubeObject } from "../kube-object"; import { KubeObject } from "../kube-object";
import { autobind } from "../../utils"; import { autoBind } from "../../utils";
import { KubeApi } from "../kube-api"; import { KubeApi } from "../kube-api";
import type { KubeJsonApiData } from "../kube-json-api";
export interface IPolicyIpBlock { export interface IPolicyIpBlock {
cidr: string; cidr: string;
@ -56,12 +57,7 @@ export interface IPolicyEgress {
}[]; }[];
} }
@autobind() export interface NetworkPolicy {
export class NetworkPolicy extends KubeObject {
static kind = "NetworkPolicy";
static namespaced = true;
static apiBase = "/apis/networking.k8s.io/v1/networkpolicies";
spec: { spec: {
podSelector: { podSelector: {
matchLabels: { matchLabels: {
@ -73,6 +69,17 @@ export class NetworkPolicy extends KubeObject {
ingress: IPolicyIngress[]; ingress: IPolicyIngress[];
egress: IPolicyEgress[]; 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[] { getMatchLabels(): string[] {
if (!this.spec.podSelector || !this.spec.podSelector.matchLabels) return []; if (!this.spec.podSelector || !this.spec.podSelector.matchLabels) return [];

View File

@ -20,13 +20,14 @@
*/ */
import { KubeObject } from "../kube-object"; import { KubeObject } from "../kube-object";
import { autobind, cpuUnitsToNumber, unitsToBytes } from "../../utils"; import { autoBind, cpuUnitsToNumber, unitsToBytes } from "../../utils";
import { IMetrics, metricsApi } from "./metrics.api"; import { IMetrics, metricsApi } from "./metrics.api";
import { KubeApi } from "../kube-api"; import { KubeApi } from "../kube-api";
import type { KubeJsonApiData } from "../kube-json-api";
export class NodesApi extends KubeApi<Node> { export class NodesApi extends KubeApi<Node> {
getMetrics(): Promise<INodeMetrics> { getMetrics(): Promise<INodeMetrics> {
const opts = { category: "nodes"}; const opts = { category: "nodes" };
return metricsApi.getMetrics({ return metricsApi.getMetrics({
memoryUsage: opts, memoryUsage: opts,
@ -49,12 +50,7 @@ export interface INodeMetrics<T = IMetrics> {
fsSize: T; fsSize: T;
} }
@autobind() export interface Node {
export class Node extends KubeObject {
static kind = "Node";
static namespaced = false;
static apiBase = "/api/v1/nodes";
spec: { spec: {
podCIDR: string; podCIDR: string;
externalID: string; externalID: string;
@ -105,6 +101,17 @@ export class Node extends KubeObject {
sizeBytes: number; 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() { getNodeConditionText() {
const { conditions } = this.status; const { conditions } = this.status;

View File

@ -20,10 +20,11 @@
*/ */
import { KubeObject } from "../kube-object"; import { KubeObject } from "../kube-object";
import { autobind } from "../../utils"; import { autoBind } from "../../utils";
import { IMetrics, metricsApi } from "./metrics.api"; import { IMetrics, metricsApi } from "./metrics.api";
import type { Pod } from "./pods.api"; import type { Pod } from "./pods.api";
import { KubeApi } from "../kube-api"; import { KubeApi } from "../kube-api";
import type { KubeJsonApiData } from "../kube-json-api";
export class PersistentVolumeClaimsApi extends KubeApi<PersistentVolumeClaim> { export class PersistentVolumeClaimsApi extends KubeApi<PersistentVolumeClaim> {
getMetrics(pvcName: string, namespace: string): Promise<IPvcMetrics> { getMetrics(pvcName: string, namespace: string): Promise<IPvcMetrics> {
@ -42,12 +43,7 @@ export interface IPvcMetrics<T = IMetrics> {
diskCapacity: T; diskCapacity: T;
} }
@autobind() export interface PersistentVolumeClaim {
export class PersistentVolumeClaim extends KubeObject {
static kind = "PersistentVolumeClaim";
static namespaced = true;
static apiBase = "/api/v1/persistentvolumeclaims";
spec: { spec: {
accessModes: string[]; accessModes: string[];
storageClassName: string; storageClassName: string;
@ -70,6 +66,17 @@ export class PersistentVolumeClaim extends KubeObject {
status: { status: {
phase: string; // Pending 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[] { getPods(allPods: Pod[]): Pod[] {
const pods = allPods.filter(pod => pod.getNs() === this.getNs()); const pods = allPods.filter(pod => pod.getNs() === this.getNs());

View File

@ -21,15 +21,11 @@
import { KubeObject } from "../kube-object"; import { KubeObject } from "../kube-object";
import { unitsToBytes } from "../../utils/convertMemory"; import { unitsToBytes } from "../../utils/convertMemory";
import { autobind } from "../../utils"; import { autoBind } from "../../utils";
import { KubeApi } from "../kube-api"; import { KubeApi } from "../kube-api";
import type { KubeJsonApiData } from "../kube-json-api";
@autobind() export interface PersistentVolume {
export class PersistentVolume extends KubeObject {
static kind = "PersistentVolume";
static namespaced = false;
static apiBase = "/api/v1/persistentvolumes";
spec: { spec: {
capacity: { capacity: {
storage: string; // 8Gi storage: string; // 8Gi
@ -65,6 +61,17 @@ export class PersistentVolume extends KubeObject {
phase: string; phase: string;
reason?: 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) { getCapacity(inBytes = false) {
const capacity = this.spec.capacity; const capacity = this.spec.capacity;

View File

@ -22,11 +22,7 @@
import { KubeObject } from "../kube-object"; import { KubeObject } from "../kube-object";
import { KubeApi } from "../kube-api"; import { KubeApi } from "../kube-api";
export class PodMetrics extends KubeObject { export interface PodMetrics {
static kind = "PodMetrics";
static namespaced = true;
static apiBase = "/apis/metrics.k8s.io/v1beta1/pods";
timestamp: string; timestamp: string;
window: string; window: string;
containers: { 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({ export const podMetricsApi = new KubeApi({
objectConstructor: PodMetrics, objectConstructor: PodMetrics,
}); });

View File

@ -19,16 +19,12 @@
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * 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 { KubeObject } from "../kube-object";
import { KubeApi } from "../kube-api"; import { KubeApi } from "../kube-api";
import type { KubeJsonApiData } from "../kube-json-api";
@autobind() export interface PodDisruptionBudget {
export class PodDisruptionBudget extends KubeObject {
static kind = "PodDisruptionBudget";
static namespaced = true;
static apiBase = "/apis/policy/v1beta1/poddisruptionbudgets";
spec: { spec: {
minAvailable: string; minAvailable: string;
maxUnavailable: string; maxUnavailable: string;
@ -40,6 +36,17 @@ export class PodDisruptionBudget extends KubeObject {
disruptionsAllowed: number disruptionsAllowed: number
expectedPods: 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() { getSelectors() {
const selector = this.spec.selector; const selector = this.spec.selector;

View File

@ -20,9 +20,10 @@
*/ */
import { IAffinity, WorkloadKubeObject } from "../workload-kube-object"; import { IAffinity, WorkloadKubeObject } from "../workload-kube-object";
import { autobind } from "../../utils"; import { autoBind } from "../../utils";
import { IMetrics, metricsApi } from "./metrics.api"; import { IMetrics, metricsApi } from "./metrics.api";
import { KubeApi } from "../kube-api"; import { KubeApi } from "../kube-api";
import type { KubeJsonApiData } from "../kube-json-api";
export class PodsApi extends KubeApi<Pod> { export class PodsApi extends KubeApi<Pod> {
async getLogs(params: { namespace: string; name: string }, query?: IPodLogsQuery): Promise<string> { async getLogs(params: { namespace: string; name: string }, query?: IPodLogsQuery): Promise<string> {
@ -202,13 +203,17 @@ export interface IPodContainerStatus {
started?: boolean; started?: boolean;
} }
@autobind()
export class Pod extends WorkloadKubeObject { export class Pod extends WorkloadKubeObject {
static kind = "Pod"; static kind = "Pod";
static namespaced = true; static namespaced = true;
static apiBase = "/api/v1/pods"; static apiBase = "/api/v1/pods";
spec: { constructor(data: KubeJsonApiData) {
super(data);
autoBind(this);
}
declare spec: {
volumes?: { volumes?: {
name: string; name: string;
persistentVolumeClaim: { persistentVolumeClaim: {
@ -265,7 +270,7 @@ export class Pod extends WorkloadKubeObject {
}; };
affinity?: IAffinity; affinity?: IAffinity;
}; };
status?: { declare status?: {
phase: string; phase: string;
conditions: { conditions: {
type: string; type: string;

View File

@ -19,16 +19,12 @@
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * 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 { KubeObject } from "../kube-object";
import { KubeApi } from "../kube-api"; import { KubeApi } from "../kube-api";
import type { KubeJsonApiData } from "../kube-json-api";
@autobind() export interface PodSecurityPolicy {
export class PodSecurityPolicy extends KubeObject {
static kind = "PodSecurityPolicy";
static namespaced = false;
static apiBase = "/apis/policy/v1beta1/podsecuritypolicies";
spec: { spec: {
allowPrivilegeEscalation?: boolean; allowPrivilegeEscalation?: boolean;
allowedCSIDrivers?: { allowedCSIDrivers?: {
@ -88,6 +84,17 @@ export class PodSecurityPolicy extends KubeObject {
}; };
volumes?: string[]; 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() { isPrivileged() {
return !!this.spec.privileged; return !!this.spec.privileged;

View File

@ -20,10 +20,11 @@
*/ */
import get from "lodash/get"; import get from "lodash/get";
import { autobind } from "../../utils"; import { autoBind } from "../../utils";
import { WorkloadKubeObject } from "../workload-kube-object"; import { WorkloadKubeObject } from "../workload-kube-object";
import type { IPodContainer, Pod } from "./pods.api"; import type { IPodContainer, Pod } from "./pods.api";
import { KubeApi } from "../kube-api"; import { KubeApi } from "../kube-api";
import type { KubeJsonApiData } from "../kube-json-api";
export class ReplicaSetApi extends KubeApi<ReplicaSet> { export class ReplicaSetApi extends KubeApi<ReplicaSet> {
protected getScaleApiUrl(params: { namespace: string; name: string }) { protected getScaleApiUrl(params: { namespace: string; name: string }) {
@ -48,12 +49,17 @@ export class ReplicaSetApi extends KubeApi<ReplicaSet> {
} }
} }
@autobind()
export class ReplicaSet extends WorkloadKubeObject { export class ReplicaSet extends WorkloadKubeObject {
static kind = "ReplicaSet"; static kind = "ReplicaSet";
static namespaced = true; static namespaced = true;
static apiBase = "/apis/apps/v1/replicasets"; static apiBase = "/apis/apps/v1/replicasets";
spec: {
constructor(data: KubeJsonApiData) {
super(data);
autoBind(this);
}
declare spec: {
replicas?: number; replicas?: number;
selector: { matchLabels: { [app: string]: string } }; selector: { matchLabels: { [app: string]: string } };
template?: { template?: {
@ -66,7 +72,7 @@ export class ReplicaSet extends WorkloadKubeObject {
}; };
minReadySeconds?: number; minReadySeconds?: number;
}; };
status: { declare status: {
replicas: number; replicas: number;
fullyLabeledReplicas?: number; fullyLabeledReplicas?: number;
readyReplicas?: number; readyReplicas?: number;

View File

@ -21,7 +21,6 @@
import { KubeObject } from "../kube-object"; import { KubeObject } from "../kube-object";
import { KubeApi } from "../kube-api"; import { KubeApi } from "../kube-api";
import type { KubeJsonApiData } from "../kube-json-api";
export interface IResourceQuotaValues { export interface IResourceQuotaValues {
[quota: string]: string; [quota: string]: string;
@ -51,16 +50,7 @@ export interface IResourceQuotaValues {
"count/deployments.extensions"?: string; "count/deployments.extensions"?: string;
} }
export class ResourceQuota extends KubeObject { export interface ResourceQuota {
static kind = "ResourceQuota";
static namespaced = true;
static apiBase = "/api/v1/resourcequotas";
constructor(data: KubeJsonApiData) {
super(data);
this.spec = this.spec || {} as any;
}
spec: { spec: {
hard: IResourceQuotaValues; hard: IResourceQuotaValues;
scopeSelector?: { scopeSelector?: {
@ -76,6 +66,12 @@ export class ResourceQuota extends KubeObject {
hard: IResourceQuotaValues; hard: IResourceQuotaValues;
used: IResourceQuotaValues; used: IResourceQuotaValues;
}; };
}
export class ResourceQuota extends KubeObject {
static kind = "ResourceQuota";
static namespaced = true;
static apiBase = "/api/v1/resourcequotas";
getScopeSelector() { getScopeSelector() {
const { matchExpressions = [] } = this.spec.scopeSelector || {}; const { matchExpressions = [] } = this.spec.scopeSelector || {};

View File

@ -19,9 +19,10 @@
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * 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 { KubeObject } from "../kube-object";
import { KubeApi } from "../kube-api"; import { KubeApi } from "../kube-api";
import type { KubeJsonApiData } from "../kube-json-api";
export interface IRoleBindingSubject { export interface IRoleBindingSubject {
kind: string; kind: string;
@ -30,18 +31,24 @@ export interface IRoleBindingSubject {
apiGroup?: string; apiGroup?: string;
} }
@autobind() export interface RoleBinding {
export class RoleBinding extends KubeObject {
static kind = "RoleBinding";
static namespaced = true;
static apiBase = "/apis/rbac.authorization.k8s.io/v1/rolebindings";
subjects?: IRoleBindingSubject[]; subjects?: IRoleBindingSubject[];
roleRef: { roleRef: {
kind: string; kind: string;
name: string; name: string;
apiGroup?: 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() { getSubjects() {
return this.subjects || []; return this.subjects || [];

View File

@ -22,17 +22,19 @@
import { KubeObject } from "../kube-object"; import { KubeObject } from "../kube-object";
import { KubeApi } from "../kube-api"; import { KubeApi } from "../kube-api";
export class Role extends KubeObject { export interface Role {
static kind = "Role";
static namespaced = true;
static apiBase = "/apis/rbac.authorization.k8s.io/v1/roles";
rules: { rules: {
verbs: string[]; verbs: string[];
apiGroups: string[]; apiGroups: string[];
resources: string[]; resources: string[];
resourceNames?: string[]; resourceNames?: string[];
}[]; }[];
}
export class Role extends KubeObject {
static kind = "Role";
static namespaced = true;
static apiBase = "/apis/rbac.authorization.k8s.io/v1/roles";
getRules() { getRules() {
return this.rules || []; return this.rules || [];

View File

@ -21,7 +21,7 @@
import { KubeObject } from "../kube-object"; import { KubeObject } from "../kube-object";
import type { KubeJsonApiData } from "../kube-json-api"; import type { KubeJsonApiData } from "../kube-json-api";
import { autobind } from "../../utils"; import { autoBind } from "../../utils";
import { KubeApi } from "../kube-api"; import { KubeApi } from "../kube-api";
export enum SecretType { export enum SecretType {
@ -40,21 +40,24 @@ export interface ISecretRef {
name: string; name: string;
} }
@autobind() export interface Secret {
export class Secret extends KubeObject {
static kind = "Secret";
static namespaced = true;
static apiBase = "/api/v1/secrets";
type: SecretType; type: SecretType;
data: { data: {
[prop: string]: string; [prop: string]: string;
token?: string; token?: string;
}; };
}
export class Secret extends KubeObject {
static kind = "Secret";
static namespaced = true;
static apiBase = "/api/v1/secrets";
constructor(data: KubeJsonApiData) { constructor(data: KubeJsonApiData) {
super(data); super(data);
this.data = this.data || {}; autoBind(this);
this.data ??= {};
} }
getKeys(): string[] { getKeys(): string[] {

View File

@ -28,8 +28,7 @@ export class SelfSubjectRulesReviewApi extends KubeApi<SelfSubjectRulesReview> {
spec: { spec: {
namespace namespace
}, },
} });
);
} }
} }
@ -41,21 +40,21 @@ export interface ISelfSubjectReviewRule {
nonResourceURLs?: string[]; nonResourceURLs?: string[];
} }
export class SelfSubjectRulesReview extends KubeObject { export interface SelfSubjectRulesReview {
static kind = "SelfSubjectRulesReview";
static namespaced = false;
static apiBase = "/apis/authorization.k8s.io/v1/selfsubjectrulesreviews";
spec: { spec: {
// todo: add more types from api docs
namespace?: string; namespace?: string;
}; };
status: { status: {
resourceRules: ISelfSubjectReviewRule[]; resourceRules: ISelfSubjectReviewRule[];
nonResourceRules: ISelfSubjectReviewRule[]; nonResourceRules: ISelfSubjectReviewRule[];
incomplete: boolean; incomplete: boolean;
}; };
}
export class SelfSubjectRulesReview extends KubeObject {
static kind = "SelfSubjectRulesReview";
static namespaced = false;
static apiBase = "/apis/authorization.k8s.io/v1/selfsubjectrulesreviews";
getResourceRules() { getResourceRules() {
const rules = this.status && this.status.resourceRules || []; 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. * 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 { KubeObject } from "../kube-object";
import { KubeApi } from "../kube-api"; import { KubeApi } from "../kube-api";
import type { KubeJsonApiData } from "../kube-json-api";
@autobind() export interface ServiceAccount {
export class ServiceAccount extends KubeObject {
static kind = "ServiceAccount";
static namespaced = true;
static apiBase = "/api/v1/serviceaccounts";
secrets?: { secrets?: {
name: string; name: string;
}[]; }[];
imagePullSecrets?: { imagePullSecrets?: {
name: string; 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() { getSecrets() {
return this.secrets || []; return this.secrets || [];

View File

@ -19,25 +19,21 @@
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * 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 { KubeObject } from "../kube-object";
import { KubeApi } from "../kube-api"; import { KubeApi } from "../kube-api";
import type { KubeJsonApiData } from "../kube-json-api";
export interface IServicePort { export interface ServicePort {
name?: string;
protocol: string;
port: number;
targetPort: number;
}
export class ServicePort implements IServicePort {
name?: string; name?: string;
protocol: string; protocol: string;
port: number; port: number;
targetPort: number; targetPort: number;
nodePort?: number; nodePort?: number;
}
constructor(data: IServicePort) { export class ServicePort {
constructor(data: ServicePort) {
Object.assign(this, data); Object.assign(this, data);
} }
@ -50,12 +46,7 @@ export class ServicePort implements IServicePort {
} }
} }
@autobind() export interface Service {
export class Service extends KubeObject {
static kind = "Service";
static namespaced = true;
static apiBase = "/api/v1/services";
spec: { spec: {
type: string; type: string;
clusterIP: 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() { getClusterIp() {
return this.spec.clusterIP; return this.spec.clusterIP;

View File

@ -22,8 +22,9 @@
import get from "lodash/get"; import get from "lodash/get";
import type { IPodContainer } from "./pods.api"; import type { IPodContainer } from "./pods.api";
import { IAffinity, WorkloadKubeObject } from "../workload-kube-object"; import { IAffinity, WorkloadKubeObject } from "../workload-kube-object";
import { autobind } from "../../utils"; import { autoBind } from "../../utils";
import { KubeApi } from "../kube-api"; import { KubeApi } from "../kube-api";
import type { KubeJsonApiData } from "../kube-json-api";
export class StatefulSetApi extends KubeApi<StatefulSet> { export class StatefulSetApi extends KubeApi<StatefulSet> {
protected getScaleApiUrl(params: { namespace: string; name: string }) { protected getScaleApiUrl(params: { namespace: string; name: string }) {
@ -48,13 +49,17 @@ export class StatefulSetApi extends KubeApi<StatefulSet> {
} }
} }
@autobind()
export class StatefulSet extends WorkloadKubeObject { export class StatefulSet extends WorkloadKubeObject {
static kind = "StatefulSet"; static kind = "StatefulSet";
static namespaced = true; static namespaced = true;
static apiBase = "/apis/apps/v1/statefulsets"; static apiBase = "/apis/apps/v1/statefulsets";
spec: { constructor(data: KubeJsonApiData) {
super(data);
autoBind(this);
}
declare spec: {
serviceName: string; serviceName: string;
replicas: number; replicas: number;
selector: { selector: {
@ -107,7 +112,7 @@ export class StatefulSet extends WorkloadKubeObject {
}; };
}[]; }[];
}; };
status: { declare status: {
observedGeneration: number; observedGeneration: number;
replicas: number; replicas: number;
currentReplicas: number; currentReplicas: number;

View File

@ -19,16 +19,12 @@
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * 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 { KubeObject } from "../kube-object";
import { KubeApi } from "../kube-api"; import { KubeApi } from "../kube-api";
import type { KubeJsonApiData } from "../kube-json-api";
@autobind() export interface StorageClass {
export class StorageClass extends KubeObject {
static kind = "StorageClass";
static namespaced = false;
static apiBase = "/apis/storage.k8s.io/v1/storageclasses";
provisioner: string; // e.g. "storage.k8s.io/v1" provisioner: string; // e.g. "storage.k8s.io/v1"
mountOptions?: string[]; mountOptions?: string[];
volumeBindingMode: string; volumeBindingMode: string;
@ -36,6 +32,17 @@ export class StorageClass extends KubeObject {
parameters: { parameters: {
[param: string]: string; // every provisioner has own set of these 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() { isDefault() {
const annotations = this.metadata.annotations || {}; const annotations = this.metadata.annotations || {};

View File

@ -23,7 +23,7 @@
import moment from "moment"; import moment from "moment";
import type { KubeJsonApiData, KubeJsonApiDataList, KubeJsonApiListMetadata, KubeJsonApiMetadata } from "./kube-json-api"; 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 type { ItemObject } from "../item.store";
import { apiKube } from "./index"; import { apiKube } from "./index";
import type { JsonApiParams } from "./json-api"; import type { JsonApiParams } from "./json-api";
@ -87,11 +87,16 @@ export class KubeStatus {
export type IKubeMetaField = keyof IKubeObjectMetadata; export type IKubeMetaField = keyof IKubeObjectMetadata;
@autobind() export class KubeObject<Metadata extends IKubeObjectMetadata = IKubeObjectMetadata, Status = any, Spec = any> implements ItemObject {
export class KubeObject implements ItemObject {
static readonly kind: string; static readonly kind: string;
static readonly namespaced: boolean; static readonly namespaced: boolean;
apiVersion: string;
kind: string;
metadata: Metadata;
status?: Status;
spec?: Spec;
static create(data: any) { static create(data: any) {
return new KubeObject(data); return new KubeObject(data);
} }
@ -176,13 +181,9 @@ export class KubeObject implements ItemObject {
constructor(data: KubeJsonApiData) { constructor(data: KubeJsonApiData) {
Object.assign(this, data); Object.assign(this, data);
autoBind(this);
} }
apiVersion: string;
kind: string;
metadata: IKubeObjectMetadata;
status?: any; // todo: type-safety support
get selfLink() { get selfLink() {
return this.metadata.selfLink; return this.metadata.selfLink;
} }
@ -265,7 +266,7 @@ export class KubeObject implements ItemObject {
} }
// use unified resource-applier api for updating all k8s objects // 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>({ return resourceApplierApi.update<T>({
...this.toPlainObject(), ...this.toPlainObject(),
...data, ...data,

View File

@ -26,8 +26,8 @@ import type { KubeObjectStore } from "../kube-object.store";
import type { ClusterContext } from "../components/context"; import type { ClusterContext } from "../components/context";
import plimit from "p-limit"; import plimit from "p-limit";
import { comparer, IReactionDisposer, observable, reaction, when } from "mobx"; import { comparer, IReactionDisposer, observable, reaction, makeObservable } from "mobx";
import { autobind, noop } from "../utils"; import { autoBind, noop } from "../utils";
import type { KubeApi } from "./kube-api"; import type { KubeApi } from "./kube-api";
import type { KubeJsonApiData } from "./kube-json-api"; import type { KubeJsonApiData } from "./kube-json-api";
import { isDebugging, isProduction } from "../../common/vars"; import { isDebugging, isProduction } from "../../common/vars";
@ -50,11 +50,13 @@ export interface IKubeWatchLog {
cssStyle?: string; cssStyle?: string;
} }
@autobind()
export class KubeWatchApi { export class KubeWatchApi {
@observable context: ClusterContext = null; @observable context: ClusterContext = null;
contextReady = when(() => Boolean(this.context)); constructor() {
makeObservable(this);
autoBind(this);
}
isAllowedApi(api: KubeApi): boolean { isAllowedApi(api: KubeApi): boolean {
return Boolean(this.context?.cluster.isAllowedResource(api.kind)); return Boolean(this.context?.cluster.isAllowedResource(api.kind));

View File

@ -20,7 +20,7 @@
*/ */
import { stringify } from "querystring"; import { stringify } from "querystring";
import { autobind, base64, EventEmitter } from "../utils"; import { boundMethod, base64, EventEmitter } from "../utils";
import { WebSocketApi } from "./websocket-api"; import { WebSocketApi } from "./websocket-api";
import isEqual from "lodash/isEqual"; import isEqual from "lodash/isEqual";
import { isDevelopment } from "../../common/vars"; import { isDevelopment } from "../../common/vars";
@ -106,7 +106,7 @@ export class TerminalApi extends WebSocketApi {
this.onReady.removeAllListeners(); this.onReady.removeAllListeners();
} }
@autobind() @boundMethod
protected _onReady(data: string) { protected _onReady(data: string) {
if (!data) return true; if (!data) return true;
this.isReady = true; this.isReady = true;

View File

@ -19,7 +19,7 @@
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * 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"; import { EventEmitter } from "../../common/event-emitter";
interface IParams { interface IParams {
@ -66,6 +66,7 @@ export class WebSocketApi {
}; };
constructor(protected params: IParams) { constructor(protected params: IParams) {
makeObservable(this);
this.params = Object.assign({}, WebSocketApi.defaultParams, params); this.params = Object.assign({}, WebSocketApi.defaultParams, params);
const { autoConnect, pingIntervalSeconds } = this.params; const { autoConnect, pingIntervalSeconds } = this.params;

View File

@ -68,8 +68,6 @@ export interface IAffinity {
} }
export class WorkloadKubeObject extends KubeObject { export class WorkloadKubeObject extends KubeObject {
spec: any; // todo: add proper types
getSelectors(): string[] { getSelectors(): string[] {
const selector = this.spec.selector; const selector = this.spec.selector;

View File

@ -26,13 +26,14 @@ import * as Mobx from "mobx";
import * as MobxReact from "mobx-react"; import * as MobxReact from "mobx-react";
import * as ReactRouter from "react-router"; import * as ReactRouter from "react-router";
import * as ReactRouterDom from "react-router-dom"; 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 { render, unmountComponentAtNode } from "react-dom";
import { delay } from "../common/utils"; import { delay } from "../common/utils";
import { isMac, isDevelopment } from "../common/vars"; import { isMac, isDevelopment } from "../common/vars";
import { HotbarStore } from "../common/hotbar-store"; import { HotbarStore } from "../common/hotbar-store";
import { ClusterStore } from "../common/cluster-store"; import { ClusterStore } from "../common/cluster-store";
import { UserStore } from "../common/user-store"; import { UserStore } from "../common/user-store";
import * as LensExtensions from "../extensions/extension-api";
import { ExtensionDiscovery } from "../extensions/extension-discovery"; import { ExtensionDiscovery } from "../extensions/extension-discovery";
import { ExtensionLoader } from "../extensions/extension-loader"; import { ExtensionLoader } from "../extensions/extension-loader";
import { ExtensionsStore } from "../extensions/extensions-store"; import { ExtensionsStore } from "../extensions/extensions-store";
@ -43,6 +44,9 @@ import { ThemeStore } from "./theme.store";
import { HelmRepoManager } from "../main/helm/helm-repo-manager"; import { HelmRepoManager } from "../main/helm/helm-repo-manager";
import { ExtensionInstallationStateStore } from "./components/+extensions/extension-install.store"; import { ExtensionInstallationStateStore } from "./components/+extensions/extension-install.store";
import { DefaultProps } from "./mui-base-theme"; import { DefaultProps } from "./mui-base-theme";
import configurePackages from "../common/configure-packages";
configurePackages();
/** /**
* If this is a development buid, wait a second to attach * If this is a development buid, wait a second to attach
@ -59,15 +63,6 @@ type AppComponent = React.ComponentType & {
init?(): Promise<void>; init?(): Promise<void>;
}; };
export {
React,
ReactRouter,
ReactRouterDom,
Mobx,
MobxReact,
LensExtensions
};
export async function bootstrap(App: AppComponent) { export async function bootstrap(App: AppComponent) {
const rootElem = document.getElementById("app"); const rootElem = document.getElementById("app");
@ -120,3 +115,23 @@ export async function bootstrap(App: AppComponent) {
// run // run
bootstrap(process.isMainFrame ? LensApp : App); 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 "./add-cluster.scss";
import React from "react"; import React from "react";
import { observer } from "mobx-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 { KubeConfig } from "@kubernetes/client-node";
import { AceEditor } from "../ace-editor"; import { AceEditor } from "../ace-editor";
import { Button } from "../button"; import { Button } from "../button";
@ -50,6 +50,11 @@ export class AddCluster extends React.Component {
kubeContexts = observable.map<string, KubeConfig>(); kubeContexts = observable.map<string, KubeConfig>();
constructor(props: {}) {
super(props);
makeObservable(this);
}
componentDidMount() { componentDidMount() {
appEventBus.emit({ name: "cluster-add", action: "start" }); appEventBus.emit({ name: "cluster-add", action: "start" });
} }

View File

@ -23,10 +23,10 @@ import "./helm-chart-details.scss";
import React, { Component } from "react"; import React, { Component } from "react";
import { getChartDetails, HelmChart } from "../../api/endpoints/helm-charts.api"; import { getChartDetails, HelmChart } from "../../api/endpoints/helm-charts.api";
import { observable, autorun } from "mobx"; import { observable, autorun, makeObservable } from "mobx";
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import { Drawer, DrawerItem } from "../drawer"; import { Drawer, DrawerItem } from "../drawer";
import { autobind, stopPropagation } from "../../utils"; import { boundMethod, stopPropagation } from "../../utils";
import { MarkdownViewer } from "../markdown-viewer"; import { MarkdownViewer } from "../markdown-viewer";
import { Spinner } from "../spinner"; import { Spinner } from "../spinner";
import { Button } from "../button"; import { Button } from "../button";
@ -48,6 +48,11 @@ export class HelmChartDetails extends Component<Props> {
private abortController?: AbortController; private abortController?: AbortController;
constructor(props: Props) {
super(props);
makeObservable(this);
}
componentWillUnmount() { componentWillUnmount() {
this.abortController?.abort(); this.abortController?.abort();
} }
@ -67,7 +72,7 @@ export class HelmChartDetails extends Component<Props> {
}); });
}); });
@autobind() @boundMethod
async onVersionChange({ value: version }: SelectOption<string>) { async onVersionChange({ value: version }: SelectOption<string>) {
this.selectedChart = this.chartVersions.find(chart => chart.version === version); this.selectedChart = this.chartVersions.find(chart => chart.version === version);
this.readme = null; this.readme = null;
@ -84,7 +89,7 @@ export class HelmChartDetails extends Component<Props> {
} }
} }
@autobind() @boundMethod
install() { install() {
createInstallChartTab(this.selectedChart); createInstallChartTab(this.selectedChart);
this.props.hideDetails(); this.props.hideDetails();

View File

@ -20,8 +20,8 @@
*/ */
import semver from "semver"; import semver from "semver";
import { observable } from "mobx"; import { observable, makeObservable } from "mobx";
import { autobind } from "../../utils"; import { autoBind } from "../../utils";
import { getChartDetails, HelmChart, listCharts } from "../../api/endpoints/helm-charts.api"; import { getChartDetails, HelmChart, listCharts } from "../../api/endpoints/helm-charts.api";
import { ItemStore } from "../../item.store"; import { ItemStore } from "../../item.store";
import flatten from "lodash/flatten"; import flatten from "lodash/flatten";
@ -31,10 +31,16 @@ export interface IChartVersion {
version: string; version: string;
} }
@autobind()
export class HelmChartStore extends ItemStore<HelmChart> { export class HelmChartStore extends ItemStore<HelmChart> {
@observable versions = observable.map<string, IChartVersion[]>(); @observable versions = observable.map<string, IChartVersion[]>();
constructor() {
super();
makeObservable(this);
autoBind(this);
}
async loadAll() { async loadAll() {
try { try {
const res = await this.loadItems(() => listCharts()); const res = await this.loadItems(() => listCharts());

View File

@ -57,10 +57,10 @@ export class HelmCharts extends Component<Props> {
showDetails = (chart: HelmChart) => { showDetails = (chart: HelmChart) => {
if (!chart) { if (!chart) {
navigation.merge(helmChartsURL()); navigation.push(helmChartsURL());
} }
else { else {
navigation.merge(helmChartsURL({ navigation.push(helmChartsURL({
params: { params: {
chartName: chart.getName(), chartName: chart.getName(),
repo: chart.getRepository(), repo: chart.getRepository(),

View File

@ -24,7 +24,7 @@ import "./release-details.scss";
import React, { Component } from "react"; import React, { Component } from "react";
import groupBy from "lodash/groupBy"; import groupBy from "lodash/groupBy";
import isEqual from "lodash/isEqual"; import isEqual from "lodash/isEqual";
import { observable, reaction } from "mobx"; import { observable, reaction, makeObservable } from "mobx";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
import kebabCase from "lodash/kebabCase"; import kebabCase from "lodash/kebabCase";
import { getRelease, getReleaseValues, HelmRelease, IReleaseDetails } from "../../api/endpoints/helm-releases.api"; import { getRelease, getReleaseValues, HelmRelease, IReleaseDetails } from "../../api/endpoints/helm-releases.api";
@ -72,7 +72,7 @@ export class ReleaseDetails extends Component<Props> {
); );
@disposeOnUnmount @disposeOnUnmount
secretWatcher = reaction(() => secretsStore.items.toJS(), () => { secretWatcher = reaction(() => secretsStore.getItems(), () => {
if (!this.props.release) return; if (!this.props.release) return;
const { getReleaseSecret } = releaseStore; const { getReleaseSecret } = releaseStore;
const { release } = this.props; const { release } = this.props;
@ -85,6 +85,11 @@ export class ReleaseDetails extends Component<Props> {
this.releaseSecret = secret; this.releaseSecret = secret;
}); });
constructor(props: Props) {
super(props);
makeObservable(this);
}
async loadDetails() { async loadDetails() {
const { release } = this.props; const { release } = this.props;

View File

@ -21,7 +21,7 @@
import React from "react"; import React from "react";
import type { HelmRelease } from "../../api/endpoints/helm-releases.api"; import type { HelmRelease } from "../../api/endpoints/helm-releases.api";
import { autobind, cssNames } from "../../utils"; import { boundMethod, cssNames } from "../../utils";
import { releaseStore } from "./release.store"; import { releaseStore } from "./release.store";
import { MenuActions, MenuActionsProps } from "../menu/menu-actions"; import { MenuActions, MenuActionsProps } from "../menu/menu-actions";
import { MenuItem } from "../menu"; import { MenuItem } from "../menu";
@ -35,12 +35,12 @@ interface Props extends MenuActionsProps {
} }
export class HelmReleaseMenu extends React.Component<Props> { export class HelmReleaseMenu extends React.Component<Props> {
@autobind() @boundMethod
remove() { remove() {
return releaseStore.remove(this.props.release); return releaseStore.remove(this.props.release);
} }
@autobind() @boundMethod
upgrade() { upgrade() {
const { release, hideDetails } = this.props; const { release, hideDetails } = this.props;
@ -48,7 +48,7 @@ export class HelmReleaseMenu extends React.Component<Props> {
hideDetails && hideDetails(); hideDetails && hideDetails();
} }
@autobind() @boundMethod
rollback() { rollback() {
ReleaseRollbackDialog.open(this.props.release); ReleaseRollbackDialog.open(this.props.release);
} }

View File

@ -22,7 +22,7 @@
import "./release-rollback-dialog.scss"; import "./release-rollback-dialog.scss";
import React from "react"; import React from "react";
import { observable } from "mobx"; import { observable, makeObservable } from "mobx";
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import { Dialog, DialogProps } from "../dialog"; import { Dialog, DialogProps } from "../dialog";
import { Wizard, WizardStep } from "../wizard"; import { Wizard, WizardStep } from "../wizard";
@ -35,26 +35,33 @@ import orderBy from "lodash/orderBy";
interface Props extends DialogProps { interface Props extends DialogProps {
} }
const dialogState = observable.object({
isOpen: false,
release: null as HelmRelease,
});
@observer @observer
export class ReleaseRollbackDialog extends React.Component<Props> { export class ReleaseRollbackDialog extends React.Component<Props> {
@observable static isOpen = false;
@observable.ref static release: HelmRelease = null;
@observable isLoading = false; @observable isLoading = false;
@observable revision: IReleaseRevision; @observable revision: IReleaseRevision;
@observable revisions = observable.array<IReleaseRevision>(); @observable revisions = observable.array<IReleaseRevision>();
constructor(props: Props) {
super(props);
makeObservable(this);
}
static open(release: HelmRelease) { static open(release: HelmRelease) {
ReleaseRollbackDialog.isOpen = true; dialogState.isOpen = true;
ReleaseRollbackDialog.release = release; dialogState.release = release;
} }
static close() { static close() {
ReleaseRollbackDialog.isOpen = false; dialogState.isOpen = false;
} }
get release(): HelmRelease { get release(): HelmRelease {
return ReleaseRollbackDialog.release; return dialogState.release;
} }
onOpen = async () => { onOpen = async () => {
@ -115,7 +122,7 @@ export class ReleaseRollbackDialog extends React.Component<Props> {
<Dialog <Dialog
{...dialogProps} {...dialogProps}
className="ReleaseRollbackDialog" className="ReleaseRollbackDialog"
isOpen={ReleaseRollbackDialog.isOpen} isOpen={dialogState.isOpen}
onOpen={this.onOpen} onOpen={this.onOpen}
close={this.close} close={this.close}
> >

View File

@ -20,8 +20,8 @@
*/ */
import isEqual from "lodash/isEqual"; import isEqual from "lodash/isEqual";
import { action, observable, reaction, when } from "mobx"; import { action, observable, reaction, when, makeObservable } from "mobx";
import { autobind } from "../../utils"; import { autoBind } from "../../utils";
import { createRelease, deleteRelease, HelmRelease, IReleaseCreatePayload, IReleaseUpdatePayload, listReleases, rollbackRelease, updateRelease } from "../../api/endpoints/helm-releases.api"; import { createRelease, deleteRelease, HelmRelease, IReleaseCreatePayload, IReleaseUpdatePayload, listReleases, rollbackRelease, updateRelease } from "../../api/endpoints/helm-releases.api";
import { ItemStore } from "../../item.store"; import { ItemStore } from "../../item.store";
import type { Secret } from "../../api/endpoints"; import type { Secret } from "../../api/endpoints";
@ -29,19 +29,21 @@ import { secretsStore } from "../+config-secrets/secrets.store";
import { namespaceStore } from "../+namespaces/namespace.store"; import { namespaceStore } from "../+namespaces/namespace.store";
import { Notifications } from "../notifications"; import { Notifications } from "../notifications";
@autobind()
export class ReleaseStore extends ItemStore<HelmRelease> { export class ReleaseStore extends ItemStore<HelmRelease> {
releaseSecrets = observable.map<string, Secret>(); releaseSecrets = observable.map<string, Secret>();
constructor() { constructor() {
super(); super();
makeObservable(this);
autoBind(this);
when(() => secretsStore.isLoaded, () => { when(() => secretsStore.isLoaded, () => {
this.releaseSecrets.replace(this.getReleaseSecrets()); this.releaseSecrets.replace(this.getReleaseSecrets());
}); });
} }
watchAssociatedSecrets(): (() => void) { watchAssociatedSecrets(): (() => void) {
return reaction(() => secretsStore.items.toJS(), () => { return reaction(() => secretsStore.getItems(), () => {
if (this.isLoading) return; if (this.isLoading) return;
const newSecrets = this.getReleaseSecrets(); const newSecrets = this.getReleaseSecrets();
const amountChanged = newSecrets.length !== this.releaseSecrets.size; const amountChanged = newSecrets.length !== this.releaseSecrets.size;

View File

@ -67,7 +67,7 @@ export class HelmReleases extends Component<Props> {
} }
showDetails = (item: HelmRelease) => { showDetails = (item: HelmRelease) => {
navigation.merge(releaseURL({ navigation.push(releaseURL({
params: { params: {
name: item.getName(), name: item.getName(),
namespace: item.getNs() namespace: item.getNs()
@ -76,7 +76,7 @@ export class HelmReleases extends Component<Props> {
}; };
hideDetails = () => { hideDetails = () => {
navigation.merge(releaseURL()); navigation.push(releaseURL());
}; };
renderRemoveDialogMessage(selectedItems: HelmRelease[]) { renderRemoveDialogMessage(selectedItems: HelmRelease[]) {

View File

@ -24,12 +24,10 @@ import { observer } from "mobx-react";
import { TabLayout, TabLayoutRoute } from "../layout/tab-layout"; import { TabLayout, TabLayoutRoute } from "../layout/tab-layout";
import { HelmCharts, helmChartsRoute, helmChartsURL } from "../+apps-helm-charts"; import { HelmCharts, helmChartsRoute, helmChartsURL } from "../+apps-helm-charts";
import { HelmReleases, releaseRoute, releaseURL } from "../+apps-releases"; import { HelmReleases, releaseRoute, releaseURL } from "../+apps-releases";
import { namespaceUrlParam } from "../+namespaces/namespace.store";
@observer @observer
export class Apps extends React.Component { export class Apps extends React.Component {
static get tabRoutes(): TabLayoutRoute[] { static get tabRoutes(): TabLayoutRoute[] {
const query = namespaceUrlParam.toObjectParam();
return [ return [
{ {
@ -41,7 +39,7 @@ export class Apps extends React.Component {
{ {
title: "Releases", title: "Releases",
component: HelmReleases, component: HelmReleases,
url: releaseURL({ query }), url: releaseURL(),
routePath: releaseRoute.path.toString(), routePath: releaseRoute.path.toString(),
}, },
]; ];

View File

@ -24,8 +24,8 @@ import React from "react";
import { SpeedDial, SpeedDialAction } from "@material-ui/lab"; import { SpeedDial, SpeedDialAction } from "@material-ui/lab";
import { Icon } from "../icon"; import { Icon } from "../icon";
import { disposeOnUnmount, observer } from "mobx-react"; import { disposeOnUnmount, observer } from "mobx-react";
import { observable, reaction } from "mobx"; import { observable, reaction, makeObservable } from "mobx";
import { autobind } from "../../../common/utils"; import { boundMethod } from "../../../common/utils";
import type { CatalogCategory, CatalogEntityAddMenuContext, CatalogEntityAddMenu } from "../../api/catalog-entity"; import type { CatalogCategory, CatalogEntityAddMenuContext, CatalogEntityAddMenu } from "../../api/catalog-entity";
import { EventEmitter } from "events"; import { EventEmitter } from "events";
import { navigate } from "../../navigation"; import { navigate } from "../../navigation";
@ -39,6 +39,11 @@ export class CatalogAddButton extends React.Component<CatalogAddButtonProps> {
@observable protected isOpen = false; @observable protected isOpen = false;
protected menuItems = observable.array<CatalogEntityAddMenu>([]); protected menuItems = observable.array<CatalogEntityAddMenu>([]);
constructor(props: CatalogAddButtonProps) {
super(props);
makeObservable(this);
}
componentDidMount() { componentDidMount() {
disposeOnUnmount(this, [ disposeOnUnmount(this, [
reaction(() => this.props.category, (category) => { reaction(() => this.props.category, (category) => {
@ -56,17 +61,17 @@ export class CatalogAddButton extends React.Component<CatalogAddButtonProps> {
]); ]);
} }
@autobind() @boundMethod
onOpen() { onOpen() {
this.isOpen = true; this.isOpen = true;
} }
@autobind() @boundMethod
onClose() { onClose() {
this.isOpen = false; this.isOpen = false;
} }
@autobind() @boundMethod
onButtonClick() { onButtonClick() {
if (this.menuItems.length == 1) { if (this.menuItems.length == 1) {
this.menuItems[0].onClick(); this.menuItems[0].onClick();

View File

@ -19,12 +19,12 @@
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/ */
import { action, computed, IReactionDisposer, observable, reaction } from "mobx"; import { action, computed, IReactionDisposer, makeObservable, observable, reaction } from "mobx";
import { catalogEntityRegistry } from "../../api/catalog-entity-registry"; import { catalogEntityRegistry } from "../../api/catalog-entity-registry";
import type { CatalogEntity, CatalogEntityActionContext } from "../../api/catalog-entity"; import type { CatalogEntity, CatalogEntityActionContext } from "../../api/catalog-entity";
import { ItemObject, ItemStore } from "../../item.store"; import { ItemObject, ItemStore } from "../../item.store";
import { autobind } from "../../utils";
import { CatalogCategory } from "../../../common/catalog"; import { CatalogCategory } from "../../../common/catalog";
import { autoBind } from "../../../common/utils";
export class CatalogEntityItem implements ItemObject { export class CatalogEntityItem implements ItemObject {
constructor(public entity: CatalogEntity) {} constructor(public entity: CatalogEntity) {}
@ -84,8 +84,13 @@ export class CatalogEntityItem implements ItemObject {
} }
} }
@autobind()
export class CatalogEntityStore extends ItemStore<CatalogEntityItem> { export class CatalogEntityStore extends ItemStore<CatalogEntityItem> {
constructor() {
super();
makeObservable(this);
autoBind(this);
}
@observable activeCategory?: CatalogCategory; @observable activeCategory?: CatalogCategory;
@computed get entities() { @computed get entities() {

View File

@ -23,7 +23,7 @@ import "./catalog.scss";
import React from "react"; import React from "react";
import { disposeOnUnmount, observer } from "mobx-react"; import { disposeOnUnmount, observer } from "mobx-react";
import { ItemListLayout } from "../item-object-list"; import { ItemListLayout } from "../item-object-list";
import { action, observable, reaction, when } from "mobx"; import { action, makeObservable, observable, reaction, when } from "mobx";
import { CatalogEntityItem, CatalogEntityStore } from "./catalog-entity.store"; import { CatalogEntityItem, CatalogEntityStore } from "./catalog-entity.store";
import { navigate } from "../../navigation"; import { navigate } from "../../navigation";
import { kebabCase } from "lodash"; import { kebabCase } from "lodash";
@ -32,7 +32,6 @@ import { MenuItem, MenuActions } from "../menu";
import { CatalogEntityContextMenu, CatalogEntityContextMenuContext, catalogEntityRunContext } from "../../api/catalog-entity"; import { CatalogEntityContextMenu, CatalogEntityContextMenuContext, catalogEntityRunContext } from "../../api/catalog-entity";
import { Badge } from "../badge"; import { Badge } from "../badge";
import { HotbarStore } from "../../../common/hotbar-store"; import { HotbarStore } from "../../../common/hotbar-store";
import { autobind } from "../../utils";
import { ConfirmDialog } from "../confirm-dialog"; import { ConfirmDialog } from "../confirm-dialog";
import { Tab, Tabs } from "../tabs"; import { Tab, Tabs } from "../tabs";
import { catalogCategoryRegistry } from "../../../common/catalog"; import { catalogCategoryRegistry } from "../../../common/catalog";
@ -49,12 +48,18 @@ enum sortBy {
} }
interface Props extends RouteComponentProps<ICatalogViewRouteParam> {} interface Props extends RouteComponentProps<ICatalogViewRouteParam> {}
@observer @observer
export class Catalog extends React.Component<Props> { export class Catalog extends React.Component<Props> {
@observable private catalogEntityStore?: CatalogEntityStore; @observable private catalogEntityStore?: CatalogEntityStore;
@observable.deep private contextMenu: CatalogEntityContextMenuContext; @observable private contextMenu: CatalogEntityContextMenuContext;
@observable activeTab?: string; @observable activeTab?: string;
constructor(props: Props) {
super(props);
makeObservable(this);
}
get routeActiveTab(): string | undefined { get routeActiveTab(): string | undefined {
const { group, kind } = this.props.match.params ?? {}; const { group, kind } = this.props.match.params ?? {};
@ -155,8 +160,7 @@ export class Catalog extends React.Component<Props> {
); );
} }
@autobind() renderItemMenu = (item: CatalogEntityItem) => {
renderItemMenu(item: CatalogEntityItem) {
const menuItems = this.contextMenu.menuItems.filter((menuItem) => !menuItem.onlyVisibleForSource || menuItem.onlyVisibleForSource === item.entity.metadata.source); const menuItems = this.contextMenu.menuItems.filter((menuItem) => !menuItem.onlyVisibleForSource || menuItem.onlyVisibleForSource === item.entity.metadata.source);
return ( return (
@ -173,7 +177,7 @@ export class Catalog extends React.Component<Props> {
</MenuItem> </MenuItem>
</MenuActions> </MenuActions>
); );
} };
renderIcon(item: CatalogEntityItem) { renderIcon(item: CatalogEntityItem) {
const category = catalogCategoryRegistry.getCategoryForEntity(item.entity); const category = catalogCategoryRegistry.getCategoryForEntity(item.entity);

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