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

Ban circular dependencies

- ApiManager now creates the instances of all stores

- ReleaseStore is Singleton-like

- Move most types and helper functions into seperate files

Signed-off-by: Sebastian Malton <sebastian@malton.name>
This commit is contained in:
Sebastian Malton 2021-05-19 08:17:51 -04:00
parent 492fa6f649
commit 33422ce975
232 changed files with 2537 additions and 1633 deletions

View File

@ -33,12 +33,22 @@ module.exports = {
} }
}, },
overrides: [ overrides: [
{
files: [
"extensions/**/*.ts",
"extensions/**/*.tsx",
],
rules: {
"import/no-unresolved": "off", // warns on @k8slens/extensions
}
},
{ {
files: [ files: [
"**/*.js" "**/*.js"
], ],
extends: [ extends: [
"eslint:recommended", "eslint:recommended",
"plugin:import/recommended",
], ],
env: { env: {
node: true node: true
@ -53,6 +63,7 @@ module.exports = {
], ],
rules: { rules: {
"header/header": [2, "./license-header"], "header/header": [2, "./license-header"],
// "import/no-cycle": 2,
"indent": ["error", 2, { "indent": ["error", 2, {
"SwitchCase": 1, "SwitchCase": 1,
}], }],
@ -93,6 +104,8 @@ module.exports = {
parser: "@typescript-eslint/parser", parser: "@typescript-eslint/parser",
extends: [ extends: [
"plugin:@typescript-eslint/recommended", "plugin:@typescript-eslint/recommended",
"plugin:import/recommended",
"plugin:import/typescript",
], ],
plugins: [ plugins: [
"header", "header",
@ -104,6 +117,7 @@ module.exports = {
}, },
rules: { rules: {
"header/header": [2, "./license-header"], "header/header": [2, "./license-header"],
// "import/no-cycle": 2,
"no-invalid-this": "off", "no-invalid-this": "off",
"@typescript-eslint/no-invalid-this": ["error"], "@typescript-eslint/no-invalid-this": ["error"],
"@typescript-eslint/explicit-function-return-type": "off", "@typescript-eslint/explicit-function-return-type": "off",
@ -158,6 +172,8 @@ module.exports = {
extends: [ extends: [
"plugin:@typescript-eslint/recommended", "plugin:@typescript-eslint/recommended",
"plugin:react/recommended", "plugin:react/recommended",
"plugin:import/recommended",
"plugin:import/typescript",
], ],
parserOptions: { parserOptions: {
ecmaVersion: 2018, ecmaVersion: 2018,
@ -166,6 +182,7 @@ module.exports = {
}, },
rules: { rules: {
"header/header": [2, "./license-header"], "header/header": [2, "./license-header"],
// "import/no-cycle": 2,
"no-invalid-this": "off", "no-invalid-this": "off",
"@typescript-eslint/no-invalid-this": ["error"], "@typescript-eslint/no-invalid-this": ["error"],
"@typescript-eslint/explicit-function-return-type": "off", "@typescript-eslint/explicit-function-return-type": "off",

View File

@ -22,7 +22,7 @@
import { K8sApi } from "@k8slens/extensions"; import { K8sApi } from "@k8slens/extensions";
export function resolveStatus(object: K8sApi.KubeObject): K8sApi.KubeObjectStatus { export function resolveStatus(object: K8sApi.KubeObject): K8sApi.KubeObjectStatus {
const eventStore = K8sApi.apiManager.getStore(K8sApi.eventApi); const eventStore = K8sApi.ApiManager.getInstance().getStore(K8sApi.eventApi);
const events = (eventStore as K8sApi.EventStore).getEventsByObject(object); const events = (eventStore as K8sApi.EventStore).getEventsByObject(object);
const warnings = events.filter(evt => evt.isWarning()); const warnings = events.filter(evt => evt.isWarning());
@ -42,7 +42,7 @@ export function resolveStatusForPods(pod: K8sApi.Pod): K8sApi.KubeObjectStatus {
if (!pod.hasIssues()) { if (!pod.hasIssues()) {
return null; return null;
} }
const eventStore = K8sApi.apiManager.getStore(K8sApi.eventApi); const eventStore = K8sApi.ApiManager.getInstance().getStore(K8sApi.eventApi);
const events = (eventStore as K8sApi.EventStore).getEventsByObject(pod); const events = (eventStore as K8sApi.EventStore).getEventsByObject(pod);
const warnings = events.filter(evt => evt.isWarning()); const warnings = events.filter(evt => evt.isWarning());
@ -59,7 +59,7 @@ export function resolveStatusForPods(pod: K8sApi.Pod): K8sApi.KubeObjectStatus {
} }
export function resolveStatusForCronJobs(cronJob: K8sApi.CronJob): K8sApi.KubeObjectStatus { export function resolveStatusForCronJobs(cronJob: K8sApi.CronJob): K8sApi.KubeObjectStatus {
const eventStore = K8sApi.apiManager.getStore(K8sApi.eventApi); const eventStore = K8sApi.ApiManager.getInstance().getStore(K8sApi.eventApi);
let events = (eventStore as K8sApi.EventStore).getEventsByObject(cronJob); let events = (eventStore as K8sApi.EventStore).getEventsByObject(cronJob);
const warnings = events.filter(evt => evt.isWarning()); const warnings = events.filter(evt => evt.isWarning());

View File

@ -315,6 +315,7 @@
"electron-notarize": "^0.3.0", "electron-notarize": "^0.3.0",
"eslint": "^7.7.0", "eslint": "^7.7.0",
"eslint-plugin-header": "^3.1.1", "eslint-plugin-header": "^3.1.1",
"eslint-plugin-import": "^2.23.2",
"eslint-plugin-react": "^7.21.5", "eslint-plugin-react": "^7.21.5",
"eslint-plugin-unused-imports": "^1.0.1", "eslint-plugin-unused-imports": "^1.0.1",
"file-loader": "^6.0.0", "file-loader": "^6.0.0",

View File

View File

@ -23,9 +23,11 @@ import fs from "fs";
import mockFs from "mock-fs"; import mockFs from "mock-fs";
import yaml from "js-yaml"; import yaml from "js-yaml";
import { Cluster } from "../../main/cluster"; import { Cluster } from "../../main/cluster";
import { ClusterStore, getClusterIdFromHost } from "../cluster-store"; import { ClusterStore } from "../cluster-store";
import { Console } from "console"; import { Console } from "console";
import { stdout, stderr } from "process"; import { stdout, stderr } from "process";
import { embedCustomKubeConfig } from "../utils";
import { getClusterIdFromHost } from "../cluster-types";
console = new Console(stdout, stderr); console = new Console(stdout, stderr);
@ -102,7 +104,7 @@ describe("empty config", () => {
icon: "data:image/jpeg;base64, iVBORw0KGgoAAAANSUhEUgAAA1wAAAKoCAYAAABjkf5", icon: "data:image/jpeg;base64, iVBORw0KGgoAAAANSUhEUgAAA1wAAAKoCAYAAABjkf5",
clusterName: "minikube" clusterName: "minikube"
}, },
kubeConfigPath: ClusterStore.embedCustomKubeConfig("foo", kubeconfig) kubeConfigPath: embedCustomKubeConfig("foo", kubeconfig)
}) })
); );
}); });
@ -135,7 +137,7 @@ describe("empty config", () => {
preferences: { preferences: {
clusterName: "prod" clusterName: "prod"
}, },
kubeConfigPath: ClusterStore.embedCustomKubeConfig("prod", kubeconfig) kubeConfigPath: embedCustomKubeConfig("prod", kubeconfig)
}), }),
new Cluster({ new Cluster({
id: "dev", id: "dev",
@ -143,7 +145,7 @@ describe("empty config", () => {
preferences: { preferences: {
clusterName: "dev" clusterName: "dev"
}, },
kubeConfigPath: ClusterStore.embedCustomKubeConfig("dev", kubeconfig) kubeConfigPath: embedCustomKubeConfig("dev", kubeconfig)
}) })
); );
}); });
@ -154,7 +156,7 @@ describe("empty config", () => {
}); });
it("check if cluster's kubeconfig file saved", () => { it("check if cluster's kubeconfig file saved", () => {
const file = ClusterStore.embedCustomKubeConfig("boo", "kubeconfig"); const file = embedCustomKubeConfig("boo", "kubeconfig");
expect(fs.readFileSync(file, "utf8")).toBe("kubeconfig"); expect(fs.readFileSync(file, "utf8")).toBe("kubeconfig");
}); });

View File

@ -24,8 +24,7 @@ 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, ipcRenderer, remote } from "electron"; import { app, ipcMain, ipcRenderer, remote } from "electron";
import { IReactionOptions, observable, reaction, runInAction, when } from "mobx"; import { IReactionOptions, observable, reaction, runInAction, when } from "mobx";
import Singleton from "./utils/singleton"; import { Singleton, getAppVersion } from "./utils";
import { getAppVersion } from "./utils/app-version";
import logger from "../main/logger"; import logger from "../main/logger";
import { broadcastMessage, ipcMainOn, ipcRendererOn } from "./ipc"; import { broadcastMessage, ipcMainOn, ipcRendererOn } from "./ipc";
import isEqual from "lodash/isEqual"; import isEqual from "lodash/isEqual";

View File

@ -20,13 +20,13 @@
*/ */
import { catalogCategoryRegistry } from "../catalog/catalog-category-registry"; import { catalogCategoryRegistry } from "../catalog/catalog-category-registry";
import { CatalogEntity, CatalogEntityActionContext, CatalogEntityAddMenuContext, CatalogEntityContextMenuContext, CatalogEntityMetadata, CatalogEntityStatus } from "../catalog"; import { CatalogEntity, CatalogEntityActionContext, CatalogEntityAddMenuContext, CatalogEntityContextMenuContext, CatalogEntityMetadata, CatalogEntityStatus, CatalogCategory, CatalogCategorySpec } from "../catalog";
import { clusterActivateHandler, clusterDisconnectHandler } from "../cluster-ipc"; import { clusterActivateHandler, clusterDisconnectHandler } from "../cluster-ipc";
import { ClusterStore } from "../cluster-store"; import { ClusterStore } from "../cluster-store";
import { requestMain } from "../ipc"; import { requestMain } from "../ipc";
import { productName } from "../vars"; import { productName } from "../vars";
import { CatalogCategory, CatalogCategorySpec } from "../catalog";
import { addClusterURL } from "../routes"; import { addClusterURL } from "../routes";
import { storedKubeConfigFolder } from "../utils";
import { app } from "electron"; import { app } from "electron";
export type KubernetesClusterPrometheusMetrics = { export type KubernetesClusterPrometheusMetrics = {
@ -109,7 +109,7 @@ export class KubernetesCluster extends CatalogEntity<CatalogEntityMetadata, Kube
}, },
]; ];
if (this.metadata.labels["file"]?.startsWith(ClusterStore.storedKubeConfigFolder)) { if (this.metadata.labels["file"]?.startsWith(storedKubeConfigFolder())) {
context.menuItems.push({ context.menuItems.push({
title: "Delete", title: "Delete",
onlyVisibleForSource: "local", onlyVisibleForSource: "local",

View File

@ -19,86 +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 { ClusterId, ClusterStore } from "./cluster-store";
import { appEventBus } from "./event-bus";
import { ResourceApplier } from "../main/resource-applier";
import { ipcMain, IpcMainInvokeEvent } from "electron";
import { clusterFrameMap } from "./cluster-frames";
export const clusterActivateHandler = "cluster:activate"; export const clusterActivateHandler = "cluster:activate";
export const clusterSetFrameIdHandler = "cluster:set-frame-id"; export const clusterSetFrameIdHandler = "cluster:set-frame-id";
export const clusterRefreshHandler = "cluster:refresh"; export const clusterRefreshHandler = "cluster:refresh";
export const clusterDisconnectHandler = "cluster:disconnect"; export const clusterDisconnectHandler = "cluster:disconnect";
export const clusterKubectlApplyAllHandler = "cluster:kubectl-apply-all"; export const clusterKubectlApplyAllHandler = "cluster:kubectl-apply-all";
export const clusterKubectlDeleteAllHandler = "cluster:kubectl-delete-all"; export const clusterKubectlDeleteAllHandler = "cluster:kubectl-delete-all";
if (ipcMain) {
ipcMain.handle(clusterActivateHandler, (event, clusterId: ClusterId, force = false) => {
return ClusterStore.getInstance()
.getById(clusterId)
?.activate(force);
});
ipcMain.handle(clusterSetFrameIdHandler, (event: IpcMainInvokeEvent, clusterId: ClusterId) => {
const cluster = ClusterStore.getInstance().getById(clusterId);
if (cluster) {
clusterFrameMap.set(cluster.id, { frameId: event.frameId, processId: event.processId });
cluster.pushState();
}
});
ipcMain.handle(clusterRefreshHandler, (event, clusterId: ClusterId) => {
return ClusterStore.getInstance()
.getById(clusterId)
?.refresh({ refreshMetadata: true });
});
ipcMain.handle(clusterDisconnectHandler, (event, clusterId: ClusterId) => {
appEventBus.emit({name: "cluster", action: "stop"});
const cluster = ClusterStore.getInstance().getById(clusterId);
if (cluster) {
cluster.disconnect();
clusterFrameMap.delete(cluster.id);
}
});
ipcMain.handle(clusterKubectlApplyAllHandler, async (event, clusterId: ClusterId, resources: string[], extraArgs: string[]) => {
appEventBus.emit({name: "cluster", action: "kubectl-apply-all"});
const cluster = ClusterStore.getInstance().getById(clusterId);
if (cluster) {
const applier = new ResourceApplier(cluster);
try {
const stdout = await applier.kubectlApplyAll(resources, extraArgs);
return { stdout };
} catch (error: any) {
return { stderr: error };
}
} else {
throw `${clusterId} is not a valid cluster id`;
}
});
ipcMain.handle(clusterKubectlDeleteAllHandler, async (event, clusterId: ClusterId, resources: string[], extraArgs: string[]) => {
appEventBus.emit({name: "cluster", action: "kubectl-delete-all"});
const cluster = ClusterStore.getInstance().getById(clusterId);
if (cluster) {
const applier = new ResourceApplier(cluster);
try {
const stdout = await applier.kubectlDeleteAll(resources, extraArgs);
return { stdout };
} catch (error: any) {
return { stderr: error };
}
} else {
throw `${clusterId} is not a valid cluster id`;
}
});
}

View File

@ -19,120 +19,24 @@
* 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 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 * as fse from "fs-extra";
import path from "path";
import { action, comparer, computed, observable, reaction, toJS } from "mobx"; import { action, comparer, computed, observable, reaction, toJS } from "mobx";
import { BaseStore } from "./base-store"; import { BaseStore } from "./base-store";
import { Cluster, ClusterState } from "../main/cluster"; import { Cluster } from "../main/cluster";
import migrations from "../migrations/cluster-store"; import migrations from "../migrations/cluster-store";
import logger from "../main/logger"; import logger from "../main/logger";
import { appEventBus } from "./event-bus"; import { appEventBus } from "./event-bus";
import { dumpConfigYaml } from "./kube-helpers";
import { saveToAppFiles } from "./utils/saveToAppFiles";
import type { KubeConfig } from "@kubernetes/client-node";
import { ipcMainOn, ipcRendererOn, requestMain } from "./ipc"; import { ipcMainOn, ipcRendererOn, requestMain } 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, getCustomKubeConfigPath, noop } from "./utils";
import type { ClusterStoreModel, ClusterId, ClusterModel, ClusterState } from "./cluster-types";
export interface ClusterIconUpload { import { getHostedClusterId } from "./cluster-types";
clusterId: string;
name: string;
path: string;
}
export interface ClusterMetadata {
[key: string]: string | number | boolean | object;
}
export type ClusterPrometheusMetadata = {
success?: boolean;
provider?: string;
autoDetected?: boolean;
};
export interface ClusterStoreModel {
activeCluster?: ClusterId; // last opened cluster
clusters?: ClusterModel[];
}
export type ClusterId = string;
export interface UpdateClusterModel extends Omit<ClusterModel, "id"> {
id?: ClusterId;
}
export interface ClusterModel {
/** Unique id for a cluster */
id: ClusterId;
/** Path to cluster kubeconfig */
kubeConfigPath: string;
/**
* Workspace id
*
* @deprecated
*/
workspace?: string;
/** User context in kubeconfig */
contextName?: string;
/** Preferences */
preferences?: ClusterPreferences;
/** Metadata */
metadata?: ClusterMetadata;
/** List of accessible namespaces */
accessibleNamespaces?: string[];
/** @deprecated */
kubeConfig?: string; // yaml
}
export interface ClusterPreferences extends ClusterPrometheusPreferences {
terminalCWD?: string;
clusterName?: string;
iconOrder?: number;
icon?: string;
httpsProxy?: string;
hiddenMetrics?: string[];
}
export interface ClusterPrometheusPreferences {
prometheus?: {
namespace: string;
service: string;
port: number;
prefix: string;
};
prometheusProvider?: {
type: string;
};
}
export class ClusterStore extends BaseStore<ClusterStoreModel> { export class ClusterStore extends BaseStore<ClusterStoreModel> {
private static StateChannel = "cluster:state"; private static StateChannel = "cluster:state";
static get storedKubeConfigFolder(): string {
return path.resolve((app || remote.app).getPath("userData"), "kubeconfigs");
}
static getCustomKubeConfigPath(clusterId: ClusterId): string {
return path.resolve(ClusterStore.storedKubeConfigFolder, clusterId);
}
static embedCustomKubeConfig(clusterId: ClusterId, kubeConfig: KubeConfig | string): string {
const filePath = ClusterStore.getCustomKubeConfigPath(clusterId);
const fileContents = typeof kubeConfig == "string" ? kubeConfig : dumpConfigYaml(kubeConfig);
saveToAppFiles(filePath, fileContents, { mode: 0o600 });
return filePath;
}
@observable activeCluster: ClusterId; @observable activeCluster: ClusterId;
@observable removedClusters = observable.map<ClusterId, Cluster>(); @observable removedClusters = observable.map<ClusterId, Cluster>();
@observable clusters = observable.map<ClusterId, Cluster>(); @observable clusters = observable.map<ClusterId, Cluster>();
@ -311,9 +215,13 @@ export class ClusterStore extends BaseStore<ClusterStoreModel> {
} }
// remove only custom kubeconfigs (pasted as text) // remove only custom kubeconfigs (pasted as text)
if (cluster.kubeConfigPath == ClusterStore.getCustomKubeConfigPath(clusterId)) { if (cluster.kubeConfigPath == getCustomKubeConfigPath(clusterId)) {
await unlink(cluster.kubeConfigPath).catch(noop); await fse.unlink(cluster.kubeConfigPath).catch(noop);
} }
const localStorage = path.resolve((app || remote.app).getPath("userData"), "lens-local-storage", `${cluster.id}.json`);
await fse.unlink(localStorage).catch(noop);
} }
} }
@ -361,21 +269,6 @@ export class ClusterStore extends BaseStore<ClusterStoreModel> {
} }
} }
export function getClusterIdFromHost(host: string): ClusterId | undefined {
// e.g host == "%clusterId.localhost:45345"
const subDomains = host.split(":")[0].split(".");
return subDomains.slice(-2, -1)[0]; // ClusterId or undefined
}
export function getClusterFrameUrl(clusterId: ClusterId) {
return `//${clusterId}.${location.host}`;
}
export function getHostedClusterId() {
return getClusterIdFromHost(location.host);
}
export function getHostedCluster(): Cluster { export function getHostedCluster(): Cluster {
return ClusterStore.getInstance().getById(getHostedClusterId()); return ClusterStore.getInstance().getById(getHostedClusterId());
} }

145
src/common/cluster-types.ts Normal file
View File

@ -0,0 +1,145 @@
/**
* 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.
*/
export enum ClusterStatus {
AccessGranted = 2,
AccessDenied = 1,
Offline = 0
}
export enum ClusterMetadataKey {
VERSION = "version",
CLUSTER_ID = "id",
DISTRIBUTION = "distribution",
NODES_COUNT = "nodes",
LAST_SEEN = "lastSeen",
PROMETHEUS = "prometheus"
}
export type ClusterRefreshOptions = {
refreshMetadata?: boolean
};
export interface ClusterState {
apiUrl: string;
online: boolean;
disconnected: boolean;
accessible: boolean;
ready: boolean;
failureReason: string;
isAdmin: boolean;
allowedNamespaces: string[]
allowedResources: string[]
isGlobalWatchEnabled: boolean;
}
export interface ClusterIconUpload {
clusterId: string;
name: string;
path: string;
}
export interface ClusterMetadata {
[key: string]: string | number | boolean | object;
}
export type ClusterPrometheusMetadata = {
success?: boolean;
provider?: string;
autoDetected?: boolean;
};
export interface ClusterStoreModel {
activeCluster?: ClusterId; // last opened cluster
clusters?: ClusterModel[];
}
export type ClusterId = string;
export interface UpdateClusterModel extends Omit<ClusterModel, "id"> {
id?: ClusterId;
}
export interface ClusterModel {
/** Unique id for a cluster */
id: ClusterId;
/** Path to cluster kubeconfig */
kubeConfigPath: string;
/**
* Workspace id
*
* @deprecated
*/
workspace?: string;
/** User context in kubeconfig */
contextName?: string;
/** Preferences */
preferences?: ClusterPreferences;
/** Metadata */
metadata?: ClusterMetadata;
/** List of accessible namespaces */
accessibleNamespaces?: string[];
/** @deprecated */
kubeConfig?: string; // yaml
}
export interface ClusterPreferences extends ClusterPrometheusPreferences {
terminalCWD?: string;
clusterName?: string;
iconOrder?: number;
icon?: string;
httpsProxy?: string;
hiddenMetrics?: string[];
}
export interface ClusterPrometheusPreferences {
prometheus?: {
namespace: string;
service: string;
port: number;
prefix: string;
};
prometheusProvider?: {
type: string;
};
}
export function getClusterIdFromHost(host: string): ClusterId | undefined {
// e.g host == "%clusterId.localhost:45345"
const subDomains = host.split(":")[0].split(".");
return subDomains.slice(-2, -1)[0]; // ClusterId or undefined
}
export function getClusterFrameUrl(clusterId: ClusterId) {
return `//${clusterId}.${location.host}`;
}
export function getHostedClusterId() {
return getClusterIdFromHost(location.host);
}

View File

@ -80,6 +80,6 @@ export function ipcRendererOn(channel: string, listener: (event: Electron.IpcRen
return () => ipcRenderer.off(channel, listener); return () => ipcRenderer.off(channel, listener);
} }
export function bindBroadcastHandlers() { export function initGetSubFramesHandler() {
ipcMain.handle(subFramesChannel, getSubFrames); ipcMain.handle(subFramesChannel, getSubFrames);
} }

View File

@ -19,8 +19,6 @@
* 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 { getHostedCluster } from "./cluster-store";
export type KubeResource = export type KubeResource =
"namespaces" | "nodes" | "events" | "resourcequotas" | "services" | "limitranges" | "namespaces" | "nodes" | "events" | "resourcequotas" | "services" | "limitranges" |
"secrets" | "configmaps" | "ingresses" | "networkpolicies" | "persistentvolumeclaims" | "persistentvolumes" | "storageclasses" | "secrets" | "configmaps" | "ingresses" | "networkpolicies" | "persistentvolumeclaims" | "persistentvolumes" | "storageclasses" |
@ -73,18 +71,3 @@ export const apiResourceRecord: Record<KubeResource, KubeApiResourceData> = {
// TODO: auto-populate all resources dynamically (see: kubectl api-resources -o=wide -v=7) // TODO: auto-populate all resources dynamically (see: kubectl api-resources -o=wide -v=7)
export const apiResources: KubeApiResource[] = Object.entries(apiResourceRecord) export const apiResources: KubeApiResource[] = Object.entries(apiResourceRecord)
.map(([apiName, data]) => ({ apiName: apiName as KubeResource, ...data })); .map(([apiName, data]) => ({ apiName: apiName as KubeResource, ...data }));
export function isAllowedResource(resources: KubeResource | KubeResource[]) {
if (!Array.isArray(resources)) {
resources = [resources];
}
const { allowedResources = [] } = getHostedCluster() || {};
for (const resource of resources) {
if (!allowedResources.includes(resource)) {
return false;
}
}
return true;
}

View File

@ -26,14 +26,13 @@ import { readFile } from "fs-extra";
import { action, computed, observable, reaction, toJS } from "mobx"; import { action, computed, observable, reaction, toJS } 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, { fileNameMigration } from "../migrations/user-store";
import { getAppVersion } from "./utils/app-version"; import { getAppVersion } from "./utils/app-version";
import { kubeConfigDefaultPath, loadConfig } from "./kube-helpers"; import { kubeConfigDefaultPath, loadConfig } from "./kube-helpers";
import { appEventBus } from "./event-bus"; import { appEventBus } from "./event-bus";
import logger from "../main/logger"; 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 { ObservableToggleSet } from "../renderer/utils"; import { ObservableToggleSet } from "../renderer/utils";
export interface UserStoreModel { export interface UserStoreModel {

View File

@ -32,14 +32,13 @@ export * from "./debouncePromise";
export * from "./defineGlobal"; export * from "./defineGlobal";
export * from "./delay"; export * from "./delay";
export * from "./disposer"; export * from "./disposer";
export * from "./disposer";
export * from "./downloadFile"; export * from "./downloadFile";
export * from "./escapeRegExp"; export * from "./escapeRegExp";
export * from "./extended-map"; export * from "./extended-map";
export * from "./getRandId"; export * from "./getRandId";
export * from "./openExternal"; export * from "./openExternal";
export * from "./reject-promise"; export * from "./reject-promise";
export * from "./saveToAppFiles"; export * from "./kubeconfig-files";
export * from "./singleton"; export * from "./singleton";
export * from "./splitArray"; export * from "./splitArray";
export * from "./tar"; export * from "./tar";

View File

@ -19,17 +19,27 @@
* 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.
*/ */
// Save file to electron app directory (e.g. "/Users/$USER/Library/Application Support/Lens" for MacOS) import type { KubeConfig } from "@kubernetes/client-node";
import path from "path";
import { app, remote } from "electron"; import { app, remote } from "electron";
import { ensureDirSync, writeFileSync } from "fs-extra"; import path from "path";
import type { WriteFileOptions } from "fs"; import type { ClusterId } from "../cluster-types";
import { dumpConfigYaml } from "../kube-helpers";
import * as fse from "fs-extra";
export function saveToAppFiles(filePath: string, contents: any, options?: WriteFileOptions): string { export function storedKubeConfigFolder(): string {
const absPath = path.resolve((app || remote.app).getPath("userData"), filePath); return path.resolve((app || remote.app).getPath("userData"), "kubeconfigs");
}
ensureDirSync(path.dirname(absPath));
writeFileSync(absPath, contents, options); export function getCustomKubeConfigPath(clusterId: ClusterId): string {
return path.resolve(storedKubeConfigFolder(), clusterId);
return absPath; }
export function embedCustomKubeConfig(clusterId: ClusterId, kubeConfig: KubeConfig | string): string {
const filePath = getCustomKubeConfigPath(clusterId);
const fileContents = typeof kubeConfig == "string" ? kubeConfig : dumpConfigYaml(kubeConfig);
fse.ensureDirSync(path.dirname(filePath));
fse.writeFileSync(filePath, fileContents, { mode: 0o600 });
return filePath;
} }

View File

@ -53,4 +53,8 @@ export class CommandRegistry extends BaseRegistry<CommandRegistration> {
return super.add(filteredItems, extension); return super.add(filteredItems, extension);
} }
getById(commandId: string): CommandRegistration | undefined {
return this.getItems().find(({ id }) => id == commandId);
}
} }

View File

@ -19,9 +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.
*/ */
export { isAllowedResource } from "../../common/rbac";
export { ResourceStack } from "../../common/k8s/resource-stack"; export { ResourceStack } from "../../common/k8s/resource-stack";
export { apiManager } from "../../renderer/api/api-manager"; export { ApiManager } from "../../renderer/api/api-manager";
export { KubeObjectStore } from "../../renderer/kube-object.store"; export { KubeObjectStore } from "../../renderer/kube-object.store";
export { KubeApi, forCluster } from "../../renderer/api/kube-api"; export { KubeApi, forCluster } from "../../renderer/api/kube-api";
export { KubeObject } from "../../renderer/api/kube-object"; export { KubeObject } from "../../renderer/api/kube-object";
@ -38,13 +37,13 @@ export { ReplicaSet, replicaSetApi } from "../../renderer/api/endpoints";
export { ResourceQuota, resourceQuotaApi } from "../../renderer/api/endpoints"; export { ResourceQuota, resourceQuotaApi } from "../../renderer/api/endpoints";
export { LimitRange, limitRangeApi } from "../../renderer/api/endpoints"; export { LimitRange, limitRangeApi } from "../../renderer/api/endpoints";
export { HorizontalPodAutoscaler, hpaApi } from "../../renderer/api/endpoints"; export { HorizontalPodAutoscaler, hpaApi } from "../../renderer/api/endpoints";
export { PodDisruptionBudget, pdbApi } from "../../renderer/api/endpoints"; export { PodDisruptionBudget, podDisruptionBudgetApi } from "../../renderer/api/endpoints";
export { Service, serviceApi } from "../../renderer/api/endpoints"; export { Service, serviceApi } from "../../renderer/api/endpoints";
export { Endpoint, endpointApi } from "../../renderer/api/endpoints"; export { Endpoint, endpointApi } from "../../renderer/api/endpoints";
export { Ingress, ingressApi, IngressApi } from "../../renderer/api/endpoints"; export { Ingress, ingressApi, IngressApi } from "../../renderer/api/endpoints";
export { NetworkPolicy, networkPolicyApi } from "../../renderer/api/endpoints"; export { NetworkPolicy, networkPolicyApi } from "../../renderer/api/endpoints";
export { PersistentVolume, persistentVolumeApi } from "../../renderer/api/endpoints"; export { PersistentVolume, persistentVolumeApi } from "../../renderer/api/endpoints";
export { PersistentVolumeClaim, pvcApi, PersistentVolumeClaimsApi } from "../../renderer/api/endpoints"; export { PersistentVolumeClaim, persistentVolumeClaimsApi, PersistentVolumeClaimsApi } from "../../renderer/api/endpoints";
export { StorageClass, storageClassApi } from "../../renderer/api/endpoints"; export { StorageClass, storageClassApi } from "../../renderer/api/endpoints";
export { Namespace, namespacesApi } from "../../renderer/api/endpoints"; export { Namespace, namespacesApi } from "../../renderer/api/endpoints";
export { KubeEvent, eventApi } from "../../renderer/api/endpoints"; export { KubeEvent, eventApi } from "../../renderer/api/endpoints";
@ -63,31 +62,30 @@ export type { ISecretRef } from "../../renderer/api/endpoints";
export type { KubeObjectStatus } from "./kube-object-status"; export type { KubeObjectStatus } from "./kube-object-status";
// stores // stores
export type { EventStore } from "../../renderer/components/+events/event.store"; export type { EventStore } from "../../renderer/components/+events";
export type { PodsStore } from "../../renderer/components/+workloads-pods/pods.store"; export type { PodsStore } from "../../renderer/components/+workloads-pods";
export type { NodesStore } from "../../renderer/components/+nodes/nodes.store"; export type { NodesStore } from "../../renderer/components/+nodes";
export type { DeploymentStore } from "../../renderer/components/+workloads-deployments/deployments.store"; export type { DeploymentStore } from "../../renderer/components/+workloads-deployments";
export type { DaemonSetStore } from "../../renderer/components/+workloads-daemonsets/daemonsets.store"; export type { DaemonSetStore } from "../../renderer/components/+workloads-daemonsets";
export type { StatefulSetStore } from "../../renderer/components/+workloads-statefulsets/statefulset.store"; export type { StatefulSetStore } from "../../renderer/components/+workloads-statefulsets";
export type { JobStore } from "../../renderer/components/+workloads-jobs/job.store"; export type { JobStore } from "../../renderer/components/+workloads-jobs";
export type { CronJobStore } from "../../renderer/components/+workloads-cronjobs/cronjob.store"; export type { CronJobStore } from "../../renderer/components/+workloads-cronjobs";
export type { ConfigMapsStore } from "../../renderer/components/+config-maps/config-maps.store"; export type { ConfigMapsStore } from "../../renderer/components/+config-maps";
export type { SecretsStore } from "../../renderer/components/+config-secrets/secrets.store"; export type { SecretsStore } from "../../renderer/components/+config-secrets";
export type { ReplicaSetStore } from "../../renderer/components/+workloads-replicasets/replicasets.store"; export type { ReplicaSetStore } from "../../renderer/components/+workloads-replicasets";
export type { ResourceQuotasStore } from "../../renderer/components/+config-resource-quotas/resource-quotas.store"; export type { ResourceQuotasStore } from "../../renderer/components/+config-resource-quotas";
export type { LimitRangesStore } from "../../renderer/components/+config-limit-ranges/limit-ranges.store"; export type { LimitRangesStore } from "../../renderer/components/+config-limit-ranges";
export type { HPAStore } from "../../renderer/components/+config-autoscalers/hpa.store"; export type { HpaStore as HPAStore } from "../../renderer/components/+config-autoscalers";
export type { PodDisruptionBudgetsStore } from "../../renderer/components/+config-pod-disruption-budgets/pod-disruption-budgets.store"; export type { PodDisruptionBudgetsStore } from "../../renderer/components/+config-pod-disruption-budgets";
export type { ServiceStore } from "../../renderer/components/+network-services/services.store"; export type { ServiceStore } from "../../renderer/components/+network-services";
export type { EndpointStore } from "../../renderer/components/+network-endpoints/endpoints.store"; export type { EndpointStore } from "../../renderer/components/+network-endpoints";
export type { IngressStore } from "../../renderer/components/+network-ingresses/ingress.store"; export type { IngressStore } from "../../renderer/components/+network-ingresses";
export type { NetworkPolicyStore } from "../../renderer/components/+network-policies/network-policy.store"; export type { NetworkPolicyStore } from "../../renderer/components/+network-policies";
export type { PersistentVolumesStore } from "../../renderer/components/+storage-volumes/volumes.store"; export type { PersistentVolumesStore } from "../../renderer/components/+storage-volumes";
export type { VolumeClaimStore } from "../../renderer/components/+storage-volume-claims/volume-claim.store"; export type { PersistentVolumeClaimStore as VolumeClaimStore } from "../../renderer/components/+storage-volume-claims";
export type { StorageClassStore } from "../../renderer/components/+storage-classes/storage-class.store"; export type { StorageClassStore } from "../../renderer/components/+storage-classes";
export type { NamespaceStore } from "../../renderer/components/+namespaces/namespace.store"; export type { NamespaceStore } from "../../renderer/components/+namespaces";
export type { ServiceAccountsStore } from "../../renderer/components/+user-management-service-accounts/service-accounts.store"; export type { ServiceAccountsStore } from "../../renderer/components/+user-management-service-accounts";
export type { RolesStore } from "../../renderer/components/+user-management-roles/roles.store"; export type { RolesStore } from "../../renderer/components/+user-management-roles";
export type { RoleBindingsStore } from "../../renderer/components/+user-management-roles-bindings/role-bindings.store"; export type { RoleBindingsStore } from "../../renderer/components/+user-management-roles-bindings";
export type { CRDStore } from "../../renderer/components/+custom-resources/crd.store"; export type { CrdStore as CRDStore } from "../../renderer/components/+custom-resources";
export type { CRDResourceStore } from "../../renderer/components/+custom-resources/crd-resource.store";

View File

@ -24,7 +24,7 @@ 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/params";
export type { URLParams } from "../../common/utils/buildUrl"; export type { URLParams } from "../../common/utils/buildUrl";
// exporting to extensions-api version of helper without `isSystem` flag // exporting to extensions-api version of helper without `isSystem` flag

View File

@ -25,14 +25,15 @@ import { watch } from "chokidar";
import fs from "fs"; import fs from "fs";
import fse from "fs-extra"; import fse from "fs-extra";
import type stream from "stream"; import type stream from "stream";
import { Disposer, ExtendedObservableMap, iter, Singleton } from "../../common/utils"; import { Disposer, ExtendedObservableMap, iter, Singleton, storedKubeConfigFolder } from "../../common/utils";
import logger from "../logger"; import logger from "../logger";
import type { KubeConfig } from "@kubernetes/client-node"; import type { KubeConfig } from "@kubernetes/client-node";
import { loadConfigFromString, splitConfig, validateKubeConfig } from "../../common/kube-helpers"; import { loadConfigFromString, splitConfig, validateKubeConfig } from "../../common/kube-helpers";
import { Cluster } from "../cluster"; import { Cluster } from "../cluster";
import { catalogEntityFromCluster } from "../cluster-manager"; import { catalogEntityFromCluster } from "../cluster-manager";
import { UserStore } from "../../common/user-store"; import { UserStore } from "../../common/user-store";
import { ClusterStore, UpdateClusterModel } from "../../common/cluster-store"; import { ClusterStore } from "../../common/cluster-store";
import type { UpdateClusterModel } from "../../common/cluster-types";
import { createHash } from "crypto"; import { createHash } from "crypto";
const logPrefix = "[KUBECONFIG-SYNC]:"; const logPrefix = "[KUBECONFIG-SYNC]:";
@ -62,7 +63,7 @@ export class KubeconfigSyncManager extends Singleton {
))); )));
// This must be done so that c&p-ed clusters are visible // This must be done so that c&p-ed clusters are visible
this.startNewSync(ClusterStore.storedKubeConfigFolder); this.startNewSync(storedKubeConfigFolder());
for (const filePath of UserStore.getInstance().syncKubeconfigEntries.keys()) { for (const filePath of UserStore.getInstance().syncKubeconfigEntries.keys()) {
this.startNewSync(filePath); this.startNewSync(filePath);

View File

@ -21,7 +21,7 @@
import { BaseClusterDetector } from "./base-cluster-detector"; import { BaseClusterDetector } from "./base-cluster-detector";
import { createHash } from "crypto"; import { createHash } from "crypto";
import { ClusterMetadataKey } from "../cluster"; import { ClusterMetadataKey } from "../../common/cluster-types";
export class ClusterIdDetector extends BaseClusterDetector { export class ClusterIdDetector extends BaseClusterDetector {
key = ClusterMetadataKey.CLUSTER_ID; key = ClusterMetadataKey.CLUSTER_ID;

View File

@ -20,7 +20,7 @@
*/ */
import { observable } from "mobx"; import { observable } from "mobx";
import type { ClusterMetadata } from "../../common/cluster-store"; import type { ClusterMetadata } from "../../common/cluster-types";
import type { Cluster } from "../cluster"; import type { Cluster } from "../cluster";
import type { BaseClusterDetector, ClusterDetectionResult } from "./base-cluster-detector"; import type { BaseClusterDetector, ClusterDetectionResult } from "./base-cluster-detector";
import { ClusterIdDetector } from "./cluster-id-detector"; import { ClusterIdDetector } from "./cluster-id-detector";

View File

@ -20,7 +20,7 @@
*/ */
import { BaseClusterDetector } from "./base-cluster-detector"; import { BaseClusterDetector } from "./base-cluster-detector";
import { ClusterMetadataKey } from "../cluster"; import { ClusterMetadataKey } from "../../common/cluster-types";
export class DistributionDetector extends BaseClusterDetector { export class DistributionDetector extends BaseClusterDetector {
key = ClusterMetadataKey.DISTRIBUTION; key = ClusterMetadataKey.DISTRIBUTION;
@ -60,7 +60,7 @@ export class DistributionDetector extends BaseClusterDetector {
if (this.isK0s()) { if (this.isK0s()) {
return { value: "k0s", accuracy: 80}; return { value: "k0s", accuracy: 80};
} }
if (this.isVMWare()) { if (this.isVMWare()) {
return { value: "vmware", accuracy: 90}; return { value: "vmware", accuracy: 90};
} }
@ -179,7 +179,7 @@ export class DistributionDetector extends BaseClusterDetector {
protected isK0s() { protected isK0s() {
return this.version.includes("-k0s"); return this.version.includes("-k0s");
} }
protected isAlibaba() { protected isAlibaba() {
return this.version.includes("-aliyun"); return this.version.includes("-aliyun");
} }

View File

@ -20,7 +20,7 @@
*/ */
import { BaseClusterDetector } from "./base-cluster-detector"; import { BaseClusterDetector } from "./base-cluster-detector";
import { ClusterMetadataKey } from "../cluster"; import { ClusterMetadataKey } from "../../common/cluster-types";
export class LastSeenDetector extends BaseClusterDetector { export class LastSeenDetector extends BaseClusterDetector {
key = ClusterMetadataKey.LAST_SEEN; key = ClusterMetadataKey.LAST_SEEN;

View File

@ -19,8 +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.
*/ */
import { ClusterMetadataKey } from "../../common/cluster-types";
import { BaseClusterDetector } from "./base-cluster-detector"; import { BaseClusterDetector } from "./base-cluster-detector";
import { ClusterMetadataKey } from "../cluster";
export class NodesCountDetector extends BaseClusterDetector { export class NodesCountDetector extends BaseClusterDetector {
key = ClusterMetadataKey.NODES_COUNT; key = ClusterMetadataKey.NODES_COUNT;

View File

@ -19,8 +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.
*/ */
import { ClusterMetadataKey } from "../../common/cluster-types";
import { BaseClusterDetector } from "./base-cluster-detector"; import { BaseClusterDetector } from "./base-cluster-detector";
import { ClusterMetadataKey } from "../cluster";
export class VersionDetector extends BaseClusterDetector { export class VersionDetector extends BaseClusterDetector {
key = ClusterMetadataKey.VERSION; key = ClusterMetadataKey.VERSION;

View File

@ -19,11 +19,11 @@
* 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 "../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, reaction, toJS } from "mobx";
import { ClusterStore, getClusterIdFromHost } from "../common/cluster-store"; import { ClusterStore } from "../common/cluster-store";
import { getClusterIdFromHost } from "../common/cluster-types";
import type { Cluster } from "./cluster"; import type { Cluster } from "./cluster";
import logger from "./logger"; import logger from "./logger";
import { apiKubePrefix } from "../common/vars"; import { apiKubePrefix } from "../common/vars";

View File

@ -20,7 +20,6 @@
*/ */
import { ipcMain } from "electron"; import { ipcMain } from "electron";
import type { ClusterId, ClusterMetadata, ClusterModel, ClusterPreferences, ClusterPrometheusPreferences, UpdateClusterModel } from "../common/cluster-store";
import { action, comparer, computed, observable, reaction, toJS, when } from "mobx"; import { action, comparer, computed, observable, reaction, toJS, 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";
@ -33,38 +32,8 @@ 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 type { ClusterModel, ClusterState, ClusterId, ClusterPreferences, ClusterMetadata, ClusterPrometheusPreferences, UpdateClusterModel, ClusterRefreshOptions } from "../common/cluster-types";
export enum ClusterStatus { import { ClusterStatus } from "../common/cluster-types";
AccessGranted = 2,
AccessDenied = 1,
Offline = 0
}
export enum ClusterMetadataKey {
VERSION = "version",
CLUSTER_ID = "id",
DISTRIBUTION = "distribution",
NODES_COUNT = "nodes",
LAST_SEEN = "lastSeen",
PROMETHEUS = "prometheus"
}
export type ClusterRefreshOptions = {
refreshMetadata?: boolean
};
export interface ClusterState {
apiUrl: string;
online: boolean;
disconnected: boolean;
accessible: boolean;
ready: boolean;
failureReason: string;
isAdmin: boolean;
allowedNamespaces: string[]
allowedResources: string[]
isGlobalWatchEnabled: boolean;
}
/** /**
* Cluster * Cluster
@ -711,4 +680,12 @@ export class Cluster implements ClusterModel, ClusterState {
return true; // allowed by default for other resources return true; // allowed by default for other resources
} }
isAllowedResources(...kinds: string[]): boolean {
return kinds.every(kind => this.isAllowedResource(kind));
}
isAnyAllowedResources(...kinds: string[]): boolean {
return kinds.length === 0 || kinds.some(kind => this.isAllowedResource(kind));
}
} }

View File

@ -20,7 +20,7 @@
*/ */
import type { PrometheusService } from "./prometheus/provider-registry"; import type { PrometheusService } from "./prometheus/provider-registry";
import type { ClusterPrometheusPreferences } from "../common/cluster-store"; import type { ClusterPrometheusPreferences } from "../common/cluster-types";
import type { Cluster } from "./cluster"; import type { Cluster } from "./cluster";
import type httpProxy from "http-proxy"; import type httpProxy from "http-proxy";
import url, { UrlWithStringQuery } from "url"; import url, { UrlWithStringQuery } from "url";

View File

@ -26,17 +26,11 @@ import { ClusterManager } from "./cluster-manager";
import logger from "./logger"; import logger from "./logger";
export function exitApp() { export function exitApp() {
console.log("before windowManager");
const windowManager = WindowManager.getInstance(false);
console.log("before clusterManager");
const clusterManager = ClusterManager.getInstance(false);
console.log("after clusterManager");
appEventBus.emit({ name: "service", action: "close" }); appEventBus.emit({ name: "service", action: "close" });
windowManager?.hide();
clusterManager?.stop(); WindowManager.getInstance(false)?.hide();
ClusterManager.getInstance(false)?.stop();
logger.info("SERVICE:QUIT"); logger.info("SERVICE:QUIT");
setTimeout(() => { setTimeout(() => {
app.exit(); app.exit();

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 * as tempy from "tempy"; import tempy from "tempy";
import fse from "fs-extra"; import fse from "fs-extra";
import * as yaml from "js-yaml"; import * as yaml from "js-yaml";
import { promiseExec} from "../promise-exec"; import { promiseExec} from "../promise-exec";

View File

@ -45,8 +45,8 @@ import type { LensExtensionId } from "../extensions/lens-extension";
import { FilesystemProvisionerStore } from "./extension-filesystem"; import { FilesystemProvisionerStore } from "./extension-filesystem";
import { installDeveloperTools } from "./developer-tools"; import { installDeveloperTools } from "./developer-tools";
import { LensProtocolRouterMain } from "./protocol-handler"; import { LensProtocolRouterMain } from "./protocol-handler";
import { getAppVersion, getAppVersionFromProxyServer } from "../common/utils"; import { disposer, getAppVersion, getAppVersionFromProxyServer } from "../common/utils";
import { bindBroadcastHandlers } from "../common/ipc"; import { initGetSubFramesHandler } from "../common/ipc";
import { startUpdateChecking } from "./app-updater"; import { startUpdateChecking } from "./app-updater";
import { IpcRendererNavigationEvents } from "../renderer/navigation/events"; import { IpcRendererNavigationEvents } from "../renderer/navigation/events";
import { CatalogPusher } from "./catalog-pusher"; import { CatalogPusher } from "./catalog-pusher";
@ -54,10 +54,14 @@ import { catalogEntityRegistry } from "../common/catalog";
import { HotbarStore } from "../common/hotbar-store"; 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 { initRegistries } from "./initializers"; import { initRegistries } from "./initializers";
import { initIpcMainHandlers } from "./initializers/ipc-handlers";
import { Router } from "./router";
import { initMenu } from "./menu";
import { initTray } from "./tray";
const workingDir = path.join(app.getPath("appData"), appName); const workingDir = path.join(app.getPath("appData"), appName);
const cleanup = disposer();
app.setName(appName); app.setName(appName);
@ -114,7 +118,8 @@ app.on("ready", async () => {
logger.info("🐚 Syncing shell environment"); logger.info("🐚 Syncing shell environment");
await shellSync(); await shellSync();
bindBroadcastHandlers(); initGetSubFramesHandler();
initIpcMainHandlers();
powerMonitor.on("shutdown", () => { powerMonitor.on("shutdown", () => {
app.exit(); app.exit();
@ -140,14 +145,15 @@ app.on("ready", async () => {
filesystemStore.load(), filesystemStore.load(),
]); ]);
const lensProxy = LensProxy.createInstance(handleWsUpgrade);
ClusterManager.createInstance(); ClusterManager.createInstance();
KubeconfigSyncManager.createInstance().startSync(); KubeconfigSyncManager.createInstance().startSync();
try { try {
logger.info("🔌 Starting LensProxy"); logger.info("🔌 Starting LensProxy");
await lensProxy.listen(); await LensProxy.createInstance(
new Router(),
req => ClusterManager.getInstance().getClusterForRequest(req),
).listen();
} catch (error) { } catch (error) {
dialog.showErrorBox("Lens Error", `Could not start proxy: ${error?.message || "unknown error"}`); dialog.showErrorBox("Lens Error", `Could not start proxy: ${error?.message || "unknown error"}`);
app.exit(); app.exit();
@ -156,7 +162,7 @@ app.on("ready", async () => {
// test proxy connection // test proxy connection
try { try {
logger.info("🔎 Testing LensProxy connection ..."); logger.info("🔎 Testing LensProxy connection ...");
const versionFromProxy = await getAppVersionFromProxyServer(lensProxy.port); const versionFromProxy = await getAppVersionFromProxyServer(LensProxy.getInstance().port);
if (getAppVersion() !== versionFromProxy) { if (getAppVersion() !== versionFromProxy) {
logger.error("Proxy server responded with invalid response"); logger.error("Proxy server responded with invalid response");
@ -182,6 +188,8 @@ app.on("ready", async () => {
logger.info("🖥️ Starting WindowManager"); logger.info("🖥️ Starting WindowManager");
const windowManager = WindowManager.createInstance(); const windowManager = WindowManager.createInstance();
cleanup.push(initMenu(windowManager), initTray(windowManager));
installDeveloperTools(); installDeveloperTools();
if (!startHidden) { if (!startHidden) {
@ -259,6 +267,8 @@ app.on("will-quit", (event) => {
return; // skip exit to make tray work, to quit go to app's global menu or tray's menu return; // skip exit to make tray work, to quit go to app's global menu or tray's menu
} }
cleanup();
}); });
app.on("open-url", (event, rawUrl) => { app.on("open-url", (event, rawUrl) => {

View File

@ -0,0 +1,93 @@
/**
* 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 { ipcMain, IpcMainInvokeEvent } from "electron";
import { clusterFrameMap } from "../../common/cluster-frames";
import * as channels from "../../common/cluster-ipc";
import { ClusterStore } from "../../common/cluster-store";
import type { ClusterId } from "../../common/cluster-types";
import { appEventBus } from "../../common/event-bus";
import { ResourceApplier } from "../resource-applier";
export function initIpcMainHandlers() {
ipcMain.handle(channels.clusterActivateHandler, (event, clusterId: ClusterId, force = false) => {
return ClusterStore.getInstance()
.getById(clusterId)
?.activate(force);
});
ipcMain.handle(channels.clusterSetFrameIdHandler, (event: IpcMainInvokeEvent, clusterId: ClusterId) => {
const cluster = ClusterStore.getInstance().getById(clusterId);
if (cluster) {
clusterFrameMap.set(cluster.id, { frameId: event.frameId, processId: event.processId });
cluster.pushState();
}
});
ipcMain.handle(channels.clusterRefreshHandler, (event, clusterId: ClusterId) => {
return ClusterStore.getInstance()
.getById(clusterId)
?.refresh({ refreshMetadata: true });
});
ipcMain.handle(channels.clusterDisconnectHandler, (event, clusterId: ClusterId) => {
appEventBus.emit({ name: "cluster", action: "stop" });
const cluster = ClusterStore.getInstance().getById(clusterId);
if (cluster) {
cluster.disconnect();
clusterFrameMap.delete(cluster.id);
}
});
ipcMain.handle(channels.clusterKubectlApplyAllHandler, (event, clusterId: ClusterId, resources: string[]) => {
appEventBus.emit({ name: "cluster", action: "kubectl-apply-all" });
const cluster = ClusterStore.getInstance().getById(clusterId);
if (cluster) {
const applier = new ResourceApplier(cluster);
applier.kubectlApplyAll(resources);
} else {
throw `${clusterId} is not a valid cluster id`;
}
});
ipcMain.handle(channels.clusterKubectlDeleteAllHandler, async (event, clusterId: ClusterId, resources: string[], extraArgs: string[]) => {
appEventBus.emit({ name: "cluster", action: "kubectl-delete-all" });
const cluster = ClusterStore.getInstance().getById(clusterId);
if (cluster) {
const applier = new ResourceApplier(cluster);
try {
const stdout = await applier.kubectlDeleteAll(resources, extraArgs);
return { stdout };
} catch (error: any) {
return { stderr: error };
}
} else {
throw `${clusterId} is not a valid cluster id`;
}
});
}

View File

@ -25,41 +25,40 @@ import spdy from "spdy";
import httpProxy from "http-proxy"; import httpProxy from "http-proxy";
import url from "url"; import url from "url";
import { apiPrefix, apiKubePrefix } from "../../common/vars"; import { apiPrefix, apiKubePrefix } from "../../common/vars";
import { Router } from "../router"; import type { Router } from "../router";
import type { ContextHandler } from "../context-handler"; import type { ContextHandler } from "../context-handler";
import logger from "../logger"; import logger from "../logger";
import { Singleton } from "../../common/utils"; import { Singleton } from "../../common/utils";
import { ClusterManager } from "../cluster-manager"; import type { Cluster } from "../cluster";
import { LocalShellSession, NodeShellSession } from "../shell-session";
type WSUpgradeHandler = (req: http.IncomingMessage, socket: net.Socket, head: Buffer) => void; import type * as WebSocket from "ws";
import { Server } from "ws";
export class LensProxy extends Singleton { export class LensProxy extends Singleton {
protected origin: string; protected origin: string;
protected proxyServer: http.Server; protected proxyServer: http.Server;
protected router = new Router();
protected closed = false; protected closed = false;
protected retryCounters = new Map<string, number>(); protected retryCounters = new Map<string, number>();
public port: number; public port: number;
constructor(handleWsUpgrade: WSUpgradeHandler) { constructor(protected router: Router, protected getClusterForRequest: (req: http.IncomingMessage) => Cluster) {
super(); super();
const proxy = this.createProxy(); const proxy = this.createProxy();
this.proxyServer = spdy.createServer({ this.proxyServer = spdy
spdy: { .createServer({
plain: true, spdy: {
protocols: ["http/1.1", "spdy/3.1"] plain: true,
} protocols: ["http/1.1", "spdy/3.1"]
}, (req: http.IncomingMessage, res: http.ServerResponse) => { }
this.handleRequest(proxy, req, res); }, (req: http.IncomingMessage, res: http.ServerResponse) => {
}); this.handleRequest(proxy, req, res);
})
this.proxyServer
.on("upgrade", (req: http.IncomingMessage, socket: net.Socket, head: Buffer) => { .on("upgrade", (req: http.IncomingMessage, socket: net.Socket, head: Buffer) => {
if (req.url.startsWith(`${apiPrefix}?`)) { if (req.url.startsWith(`${apiPrefix}?`)) {
handleWsUpgrade(req, socket, head); this.handleWsUpgrade(req, socket, head);
} else { } else {
this.handleProxyUpgrade(proxy, req, socket, head); this.handleProxyUpgrade(proxy, req, socket, head);
} }
@ -103,8 +102,27 @@ export class LensProxy extends Singleton {
this.closed = true; this.closed = true;
} }
protected async handleWsUpgrade(req: http.IncomingMessage, socket: net.Socket, head: Buffer) {
const wsServer = new Server({ noServer: true });
wsServer.on("connection", ((socket: WebSocket, req: http.IncomingMessage) => {
const cluster = this.getClusterForRequest(req);
const nodeParam = url.parse(req.url, true).query["node"]?.toString();
const shell = nodeParam
? new NodeShellSession(socket, cluster, nodeParam)
: new LocalShellSession(socket, cluster);
shell.open()
.catch(error => logger.error(`[SHELL-SESSION]: failed to open: ${error}`, { error }));
}));
wsServer.handleUpgrade(req, socket, head, (con) => {
wsServer.emit("connection", con, req);
});
}
protected async handleProxyUpgrade(proxy: httpProxy, req: http.IncomingMessage, socket: net.Socket, head: Buffer) { protected async handleProxyUpgrade(proxy: httpProxy, req: http.IncomingMessage, socket: net.Socket, head: Buffer) {
const cluster = ClusterManager.getInstance().getClusterForRequest(req); const cluster = this.getClusterForRequest(req);
if (cluster) { if (cluster) {
const proxyUrl = await cluster.contextHandler.resolveAuthProxyUrl() + req.url.replace(apiKubePrefix, ""); const proxyUrl = await cluster.contextHandler.resolveAuthProxyUrl() + req.url.replace(apiKubePrefix, "");
@ -205,7 +223,7 @@ export class LensProxy extends Singleton {
return proxy; return proxy;
} }
protected async getProxyTarget(req: http.IncomingMessage, contextHandler: ContextHandler): Promise<httpProxy.ServerOptions | void> { protected async getProxyTarget(req: http.IncomingMessage, contextHandler: ContextHandler): Promise<httpProxy.ServerOptions | null> {
if (req.url.startsWith(apiKubePrefix)) { if (req.url.startsWith(apiKubePrefix)) {
delete req.headers.authorization; delete req.headers.authorization;
req.url = req.url.replace(apiKubePrefix, ""); req.url = req.url.replace(apiKubePrefix, "");
@ -213,6 +231,8 @@ export class LensProxy extends Singleton {
return contextHandler.getApiTarget(isWatchRequest); return contextHandler.getApiTarget(isWatchRequest);
} }
return null;
} }
protected getRequestId(req: http.IncomingMessage) { protected getRequestId(req: http.IncomingMessage) {
@ -220,7 +240,7 @@ export class LensProxy extends Singleton {
} }
protected async handleRequest(proxy: httpProxy, req: http.IncomingMessage, res: http.ServerResponse) { protected async handleRequest(proxy: httpProxy, req: http.IncomingMessage, res: http.ServerResponse) {
const cluster = ClusterManager.getInstance().getClusterForRequest(req); const cluster = this.getClusterForRequest(req);
if (cluster) { if (cluster) {
const proxyTarget = await this.getProxyTarget(req, cluster.contextHandler); const proxyTarget = await this.getProxyTarget(req, cluster.contextHandler);

View File

@ -25,7 +25,7 @@ import { exec } from "child_process";
import fs from "fs"; import fs from "fs";
import * as yaml from "js-yaml"; import * as yaml from "js-yaml";
import path from "path"; import path from "path";
import * as tempy from "tempy"; import tempy from "tempy";
import logger from "./logger"; import logger from "./logger";
import { appEventBus } from "../common/event-bus"; import { appEventBus } from "../common/event-bus";
import { cloneJsonObject } from "../common/utils"; import { cloneJsonObject } from "../common/utils";
@ -34,19 +34,19 @@ export class ResourceApplier {
constructor(protected cluster: Cluster) { constructor(protected cluster: Cluster) {
} }
async apply(resource: KubernetesObject | any): Promise<string> { async apply(resource: KubernetesObject | any): Promise<any> {
resource = this.sanitizeObject(resource); resource = this.sanitizeObject(resource);
appEventBus.emit({name: "resource", action: "apply"}); appEventBus.emit({name: "resource", action: "apply"});
return await this.kubectlApply(yaml.safeDump(resource)); return await this.kubectlApply(yaml.safeDump(resource));
} }
protected async kubectlApply(content: string): Promise<string> { protected async kubectlApply(content: string): Promise<any> {
const { kubeCtl } = this.cluster; const { kubeCtl } = this.cluster;
const kubectlPath = await kubeCtl.getPath(); const kubectlPath = await kubeCtl.getPath();
const proxyKubeconfigPath = await this.cluster.getProxyKubeconfigPath(); const proxyKubeconfigPath = await this.cluster.getProxyKubeconfigPath();
return new Promise<string>((resolve, reject) => { return new Promise<any>((resolve, reject) => {
const fileName = tempy.file({ name: "resource.yaml" }); const fileName = tempy.file({ name: "resource.yaml" });
fs.writeFileSync(fileName, content); fs.writeFileSync(fileName, content);

View File

@ -22,10 +22,10 @@
import _ from "lodash"; import _ from "lodash";
import type { LensApiRequest } from "../router"; import type { LensApiRequest } from "../router";
import { respondJson } from "../utils/http-responses"; import { respondJson } from "../utils/http-responses";
import { Cluster, ClusterMetadataKey } from "../cluster"; import type { Cluster } from "../cluster";
import type { ClusterPrometheusMetadata } from "../../common/cluster-store";
import logger from "../logger"; import logger from "../logger";
import { getMetrics } from "../k8s-request"; import { getMetrics } from "../k8s-request";
import { ClusterPrometheusMetadata, ClusterMetadataKey } from "../../common/cluster-types";
export type IMetricsQuery = string | string[] | { export type IMetricsQuery = string | string[] | {
[metricName: string]: string; [metricName: string]: string;

View File

@ -19,14 +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 type { ClusterId } from "../common/cluster-store"; import type { ClusterId } from "../common/cluster-types";
import { observable } from "mobx"; import { 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";
import { ipcMainOn } from "../common/ipc"; import { ipcMainOn } from "../common/ipc";
import { initMenu } from "./menu";
import { initTray } from "./tray";
import { Singleton } from "../common/utils"; import { Singleton } from "../common/utils";
import { ClusterFrameInfo, clusterFrameMap } from "../common/cluster-frames"; import { ClusterFrameInfo, clusterFrameMap } from "../common/cluster-frames";
import { IpcRendererNavigationEvents } from "../renderer/navigation/events"; import { IpcRendererNavigationEvents } from "../renderer/navigation/events";
@ -38,15 +36,15 @@ export class WindowManager extends Singleton {
protected mainWindow: BrowserWindow; protected mainWindow: BrowserWindow;
protected splashWindow: BrowserWindow; protected splashWindow: BrowserWindow;
protected windowState: windowStateKeeper.State; protected windowState: windowStateKeeper.State;
protected disposers: Record<string, Function> = {};
@observable activeClusterId: ClusterId; @observable activeClusterId: ClusterId;
constructor() { constructor() {
super(); super();
this.bindEvents();
this.initMenu(); ipcMainOn(IpcRendererNavigationEvents.CLUSTER_VIEW_CURRENT_ID, (event, clusterId: ClusterId) => {
this.initTray(); this.activeClusterId = clusterId;
});
} }
get mainUrl() { get mainUrl() {
@ -130,21 +128,6 @@ export class WindowManager extends Singleton {
} }
} }
protected async initMenu() {
this.disposers.menuAutoUpdater = initMenu(this);
}
protected initTray() {
this.disposers.trayAutoUpdater = initTray(this);
}
protected bindEvents() {
// track visible cluster from ui
ipcMainOn(IpcRendererNavigationEvents.CLUSTER_VIEW_CURRENT_ID, (event, clusterId: ClusterId) => {
this.activeClusterId = clusterId;
});
}
async ensureMainWindow(): Promise<BrowserWindow> { async ensureMainWindow(): Promise<BrowserWindow> {
if (!this.mainWindow) await this.initMainWindow(); if (!this.mainWindow) await this.initMainWindow();
this.mainWindow.show(); this.mainWindow.show();
@ -214,9 +197,5 @@ export class WindowManager extends Singleton {
this.splashWindow.destroy(); this.splashWindow.destroy();
this.mainWindow = null; this.mainWindow = null;
this.splashWindow = null; this.splashWindow = null;
Object.entries(this.disposers).forEach(([name, dispose]) => {
dispose();
delete this.disposers[name];
});
} }
} }

View File

@ -26,14 +26,15 @@ import path from "path";
import { app, remote } from "electron"; import { app, remote } from "electron";
import { migration } from "../migration-wrapper"; import { migration } from "../migration-wrapper";
import fse from "fs-extra"; import fse from "fs-extra";
import { ClusterModel, ClusterStore } from "../../common/cluster-store";
import { loadConfig } from "../../common/kube-helpers"; import { loadConfig } from "../../common/kube-helpers";
import type { ClusterModel } from "../../common/cluster-types";
import { getCustomKubeConfigPath, embedCustomKubeConfig } from "../../common/utils";
export default migration({ export default migration({
version: "3.6.0-beta.1", version: "3.6.0-beta.1",
run(store, printLog) { run(store, printLog) {
const userDataPath = (app || remote.app).getPath("userData"); const userDataPath = (app || remote.app).getPath("userData");
const kubeConfigBase = ClusterStore.getCustomKubeConfigPath(""); const kubeConfigBase = getCustomKubeConfigPath("");
const storedClusters: ClusterModel[] = store.get("clusters") || []; const storedClusters: ClusterModel[] = store.get("clusters") || [];
if (!storedClusters.length) return; if (!storedClusters.length) return;
@ -47,7 +48,7 @@ export default migration({
*/ */
try { try {
// take the embedded kubeconfig and dump it into a file // take the embedded kubeconfig and dump it into a file
cluster.kubeConfigPath = ClusterStore.embedCustomKubeConfig(cluster.id, cluster.kubeConfig); cluster.kubeConfigPath = embedCustomKubeConfig(cluster.id, cluster.kubeConfig);
cluster.contextName = loadConfig(cluster.kubeConfigPath).getCurrentContext(); cluster.contextName = loadConfig(cluster.kubeConfigPath).getCurrentContext();
delete cluster.kubeConfig; delete cluster.kubeConfig;

View File

@ -22,9 +22,9 @@
// Fix embedded kubeconfig paths under snap config // Fix embedded kubeconfig paths under snap config
import { migration } from "../migration-wrapper"; import { migration } from "../migration-wrapper";
import type { ClusterModel } from "../../common/cluster-store";
import { getAppVersion } from "../../common/utils/app-version"; import { getAppVersion } from "../../common/utils/app-version";
import fs from "fs"; import fs from "fs";
import type { ClusterModel } from "../../common/cluster-types";
export default migration({ export default migration({
version: getAppVersion(), // Run always after upgrade version: getAppVersion(), // Run always after upgrade

View File

@ -19,43 +19,59 @@
* 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 { ingressStore } from "../../components/+network-ingresses/ingress.store"; import { Cluster } from "../../../main/cluster";
import { apiManager } from "../api-manager"; import { KubeObjectStore } from "../../kube-object.store";
import { ApiManager } from "../api-manager";
import { KubeApi } from "../kube-api"; import { KubeApi } from "../kube-api";
import { KubeObject } from "../kube-object";
class TestApi extends KubeApi { class TestApi extends KubeApi {
protected async checkPreferredVersion() { protected async checkPreferredVersion() {
return; return;
} }
} }
describe("ApiManager", () => { describe("ApiManager", () => {
beforeEach(() => {
ApiManager.createInstance(new Cluster({
id: "foobar",
kubeConfigPath: "/foobar",
}));
});
afterEach(() => {
ApiManager.resetInstance();
});
describe("registerApi", () => { describe("registerApi", () => {
it("re-register store if apiBase changed", async () => { it("re-register store if apiBase changed", async () => {
const apiBase = "apis/v1/foo"; const apiBase = "apis/v1/foo";
const fallbackApiBase = "/apis/extensions/v1beta1/foo"; const fallbackApiBase = "/apis/extensions/v1beta1/foo";
const kubeApi = new TestApi({ const kubeApi = new TestApi({
objectConstructor: KubeObject,
apiBase, apiBase,
fallbackApiBases: [fallbackApiBase], fallbackApiBases: [fallbackApiBase],
checkPreferredVersion: true, checkPreferredVersion: true,
}); });
apiManager.registerApi(apiBase, kubeApi); ApiManager.getInstance().registerApi(apiBase, kubeApi);
class TestStore extends KubeObjectStore<KubeObject> {
api = kubeApi;
}
// Define to use test api for ingress store // Define to use test api for ingress store
Object.defineProperty(ingressStore, "api", { value: kubeApi }); ApiManager.getInstance().registerStore(TestStore);
apiManager.registerStore(ingressStore, [kubeApi]);
// Test that store is returned with original apiBase // Test that store is returned with original apiBase
expect(apiManager.getStore(kubeApi)).toBe(ingressStore); expect(ApiManager.getInstance().getStore(kubeApi)).toBeInstanceOf(TestStore);
// Change apiBase similar as checkPreferredVersion does // Change apiBase similar as checkPreferredVersion does
Object.defineProperty(kubeApi, "apiBase", { value: fallbackApiBase }); Object.defineProperty(kubeApi, "apiBase", { value: fallbackApiBase });
apiManager.registerApi(fallbackApiBase, kubeApi); ApiManager.getInstance().registerApi(fallbackApiBase, kubeApi);
// Test that store is returned with new apiBase // Test that store is returned with new apiBase
expect(apiManager.getStore(kubeApi)).toBe(ingressStore); expect(ApiManager.getInstance().getStore(kubeApi)).toBeInstanceOf(TestStore);
}); });
}); });
}); });

View File

@ -20,6 +20,7 @@
*/ */
import { KubeApi } from "../kube-api"; import { KubeApi } from "../kube-api";
import { KubeObject } from "../kube-object";
describe("KubeApi", () => { describe("KubeApi", () => {
it("uses url from apiBase if apiBase contains the resource", async () => { it("uses url from apiBase if apiBase contains the resource", async () => {
@ -53,6 +54,7 @@ describe("KubeApi", () => {
const apiBase = "/apis/networking.k8s.io/v1/ingresses"; const apiBase = "/apis/networking.k8s.io/v1/ingresses";
const fallbackApiBase = "/apis/extensions/v1beta1/ingresses"; const fallbackApiBase = "/apis/extensions/v1beta1/ingresses";
const kubeApi = new KubeApi({ const kubeApi = new KubeApi({
objectConstructor: KubeObject,
apiBase, apiBase,
fallbackApiBases: [fallbackApiBase], fallbackApiBases: [fallbackApiBase],
checkPreferredVersion: true, checkPreferredVersion: true,
@ -91,6 +93,7 @@ describe("KubeApi", () => {
const apiBase = "apis/networking.k8s.io/v1/ingresses"; const apiBase = "apis/networking.k8s.io/v1/ingresses";
const fallbackApiBase = "/apis/extensions/v1beta1/ingresses"; const fallbackApiBase = "/apis/extensions/v1beta1/ingresses";
const kubeApi = new KubeApi({ const kubeApi = new KubeApi({
objectConstructor: KubeObject,
apiBase, apiBase,
fallbackApiBases: [fallbackApiBase], fallbackApiBases: [fallbackApiBase],
checkPreferredVersion: true, checkPreferredVersion: true,

View File

@ -19,16 +19,37 @@
* 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 type { KubeObjectStore } from "../kube-object.store"; import type { KubeObjectStore, KubeObjectStoreConstructor } from "../kube-object.store";
import { action, observable } from "mobx"; import { action, observable } from "mobx";
import { autobind } from "../utils"; import { autobind, Singleton } from "../utils";
import { KubeApi, parseKubeApi } from "./kube-api"; import type { KubeApi } from "./kube-api";
import { parseKubeApi } from "./kube-api-parse";
import type { IKubeApiLinkRef, IKubeObjectRef } from "./kube-api-parse";
import type { KubeObject } from "./kube-object";
import type { Cluster } from "../../main/cluster";
export function createKubeApiURL(ref: IKubeApiLinkRef): string {
const { apiPrefix = "/apis", resource, apiVersion, name } = ref;
let { namespace } = ref;
if (namespace) {
namespace = `namespaces/${namespace}`;
}
return [apiPrefix, apiVersion, namespace, resource, name]
.filter(v => v)
.join("/");
}
@autobind() @autobind()
export class ApiManager { export class ApiManager extends Singleton {
private apis = observable.map<string, KubeApi>(); private apis = observable.map<string, KubeApi>();
private stores = observable.map<string, KubeObjectStore>(); private stores = observable.map<string, KubeObjectStore<KubeObject>>();
constructor(protected cluster: Cluster) {
super();
}
getApi(pathOrCallback: string | ((api: KubeApi) => boolean)) { getApi(pathOrCallback: string | ((api: KubeApi) => boolean)) {
if (typeof pathOrCallback === "string") { if (typeof pathOrCallback === "string") {
@ -71,15 +92,56 @@ export class ApiManager {
} }
@action @action
registerStore(store: KubeObjectStore, apis: KubeApi[] = [store.api]) { registerStore<Store extends KubeObjectStore<KubeObject>>(storeConstructor: KubeObjectStoreConstructor<Store>, apis?: KubeApi[]): this {
apis.forEach(api => { const store = new storeConstructor(this.cluster);
for (const api of apis ?? [store.api]) {
this.stores.set(api.apiBase, store); this.stores.set(api.apiBase, store);
}); }
return this;
} }
getStore<S extends KubeObjectStore>(api: string | KubeApi): S { getStore<Store extends KubeObjectStore<KubeObject>>(api: string | KubeApi): Store | undefined {
return this.stores.get(this.resolveApi(api)?.apiBase) as S; return this.stores.get(this.resolveApi(api)?.apiBase) as Store;
}
lookupApiLink(ref: IKubeObjectRef, parentObject: KubeObject): string {
const {
kind, apiVersion, name,
namespace = parentObject.getNs()
} = ref;
if (!kind) return "";
// search in registered apis by 'kind' & 'apiVersion'
const api = this.getApi(api => api.kind === kind && api.apiVersionWithGroup == apiVersion);
if (api) {
return api.getUrl({ namespace, name });
}
// lookup api by generated resource link
const apiPrefixes = ["/apis", "/api"];
const resource = kind.endsWith("s") ? `${kind.toLowerCase()}es` : `${kind.toLowerCase()}s`;
for (const apiPrefix of apiPrefixes) {
const apiLink = createKubeApiURL({ apiPrefix, apiVersion, name, namespace, resource });
if (this.getApi(apiLink)) {
return apiLink;
}
}
// resolve by kind only (hpa's might use refs to older versions of resources for example)
const apiByKind = this.getApi(api => api.kind === kind);
if (apiByKind) {
return apiByKind.getUrl({ name, namespace });
}
// otherwise generate link with default prefix
// resource still might exists in k8s, but api is not registered in the app
return createKubeApiURL({ apiVersion, name, namespace, resource });
} }
} }
export const apiManager = new ApiManager();

View File

@ -23,7 +23,7 @@ import { IMetrics, IMetricsReqParams, metricsApi } from "./metrics.api";
import { KubeObject } from "../kube-object"; import { KubeObject } from "../kube-object";
import { KubeApi } from "../kube-api"; import { KubeApi } from "../kube-api";
export class ClusterApi extends KubeApi<Cluster> { export class ClusterApi extends KubeApi<KubeCluster> {
static kind = "Cluster"; static kind = "Cluster";
static namespaced = true; static namespaced = true;
@ -71,7 +71,7 @@ export interface IClusterMetrics<T = IMetrics> {
fsUsage: T; fsUsage: T;
} }
export class Cluster extends KubeObject { export class KubeCluster extends KubeObject {
static kind = "Cluster"; static kind = "Cluster";
static apiBase = "/apis/cluster.k8s.io/v1alpha1/clusters"; static apiBase = "/apis/cluster.k8s.io/v1alpha1/clusters";
@ -117,5 +117,5 @@ export class Cluster extends KubeObject {
} }
export const clusterApi = new ClusterApi({ export const clusterApi = new ClusterApi({
objectConstructor: Cluster, objectConstructor: KubeCluster,
}); });

View File

@ -108,6 +108,6 @@ export class PersistentVolumeClaim extends KubeObject {
} }
} }
export const pvcApi = new PersistentVolumeClaimsApi({ export const persistentVolumeClaimsApi = new PersistentVolumeClaimsApi({
objectConstructor: PersistentVolumeClaim, objectConstructor: PersistentVolumeClaim,
}); });

View File

@ -65,6 +65,6 @@ export class PodDisruptionBudget extends KubeObject {
} }
export const pdbApi = new KubeApi({ export const podDisruptionBudgetApi = new KubeApi({
objectConstructor: PodDisruptionBudget, objectConstructor: PodDisruptionBudget,
}); });

View File

@ -110,6 +110,6 @@ export class PodSecurityPolicy extends KubeObject {
} }
} }
export const pspApi = new KubeApi({ export const podSecurityPolicyApi = new KubeApi({
objectConstructor: PodSecurityPolicy, objectConstructor: PodSecurityPolicy,
}); });

View File

@ -20,35 +20,21 @@
*/ */
import jsYaml from "js-yaml"; import jsYaml from "js-yaml";
import { KubeObject } from "../kube-object";
import type { KubeJsonApiData } from "../kube-json-api"; import type { KubeJsonApiData } from "../kube-json-api";
import { apiBase } from "../index"; import { apiBase } from "../index";
import { apiManager } from "../api-manager";
export const resourceApplierApi = { export const resourceApplierApi = {
annotations: [ annotations: [
"kubectl.kubernetes.io/last-applied-configuration" "kubectl.kubernetes.io/last-applied-configuration"
], ],
async update<D extends KubeObject>(resource: object | string): Promise<D> { async update(resource: object | string): Promise<KubeJsonApiData> {
if (typeof resource === "string") { if (typeof resource === "string") {
resource = jsYaml.safeLoad(resource); resource = jsYaml.safeLoad(resource);
} }
return apiBase const result = await apiBase.post<KubeJsonApiData[]>("/stack", { data: resource });
.post<KubeJsonApiData[]>("/stack", { data: resource })
.then(data => {
const items = data.map(obj => {
const api = apiManager.getApiByKind(obj.kind, obj.apiVersion);
if (api) { return result[0];
return new api.objectConstructor(obj);
} else {
return new KubeObject(obj);
}
});
return items.length === 1 ? items[0] : items;
});
} }
}; };

View File

@ -21,9 +21,7 @@
// Parse kube-api path and get api-version, group, etc. // Parse kube-api path and get api-version, group, etc.
import type { KubeObject } from "./kube-object";
import { splitArray } from "../../common/utils"; import { splitArray } from "../../common/utils";
import { apiManager } from "./api-manager";
export interface IKubeObjectRef { export interface IKubeObjectRef {
kind: string; kind: string;
@ -123,55 +121,3 @@ export function parseKubeApi(path: string): IKubeApiParsed {
namespace, resource, name, namespace, resource, name,
}; };
} }
export function createKubeApiURL(ref: IKubeApiLinkRef): string {
const { apiPrefix = "/apis", resource, apiVersion, name } = ref;
let { namespace } = ref;
if (namespace) {
namespace = `namespaces/${namespace}`;
}
return [apiPrefix, apiVersion, namespace, resource, name]
.filter(v => v)
.join("/");
}
export function lookupApiLink(ref: IKubeObjectRef, parentObject: KubeObject): string {
const {
kind, apiVersion, name,
namespace = parentObject.getNs()
} = ref;
if (!kind) return "";
// search in registered apis by 'kind' & 'apiVersion'
const api = apiManager.getApi(api => api.kind === kind && api.apiVersionWithGroup == apiVersion);
if (api) {
return api.getUrl({ namespace, name });
}
// lookup api by generated resource link
const apiPrefixes = ["/apis", "/api"];
const resource = kind.endsWith("s") ? `${kind.toLowerCase()}es` : `${kind.toLowerCase()}s`;
for (const apiPrefix of apiPrefixes) {
const apiLink = createKubeApiURL({ apiPrefix, apiVersion, name, namespace, resource });
if (apiManager.getApi(apiLink)) {
return apiLink;
}
}
// resolve by kind only (hpa's might use refs to older versions of resources for example)
const apiByKind = apiManager.getApi(api => api.kind === kind);
if (apiByKind) {
return apiByKind.getUrl({ name, namespace });
}
// otherwise generate link with default prefix
// resource still might exists in k8s, but api is not registered in the app
return createKubeApiURL({ apiVersion, name, namespace, resource });
}

View File

@ -25,9 +25,9 @@ import merge from "lodash/merge";
import { stringify } from "querystring"; import { stringify } from "querystring";
import { apiKubePrefix, isDevelopment, isTestEnv } from "../../common/vars"; import { apiKubePrefix, isDevelopment, isTestEnv } from "../../common/vars";
import logger from "../../main/logger"; import logger from "../../main/logger";
import { apiManager } from "./api-manager"; import { ApiManager, createKubeApiURL } from "./api-manager";
import { apiKube } from "./index"; import { apiKube } from "./index";
import { createKubeApiURL, parseKubeApi } from "./kube-api-parse"; import { parseKubeApi } from "./kube-api-parse";
import { IKubeObjectConstructor, KubeObject, KubeStatus } from "./kube-object"; import { IKubeObjectConstructor, KubeObject, KubeStatus } from "./kube-object";
import byline from "byline"; import byline from "byline";
import type { IKubeWatchEvent } from "./kube-watch-api"; import type { IKubeWatchEvent } from "./kube-watch-api";
@ -49,18 +49,13 @@ export interface IKubeApiOptions<T extends KubeObject> {
*/ */
fallbackApiBases?: string[]; fallbackApiBases?: string[];
objectConstructor?: IKubeObjectConstructor<T>; objectConstructor: IKubeObjectConstructor<T>;
request?: KubeJsonApi; request?: KubeJsonApi;
isNamespaced?: boolean; isNamespaced?: boolean;
kind?: string; kind?: string;
checkPreferredVersion?: boolean; checkPreferredVersion?: boolean;
} }
export interface KubeApiListOptions {
namespace?: string;
reqInit?: RequestInit;
}
export interface IKubeApiQueryParams { export interface IKubeApiQueryParams {
watch?: boolean | number; watch?: boolean | number;
resourceVersion?: string; resourceVersion?: string;
@ -152,7 +147,7 @@ export class KubeApi<T extends KubeObject = any> {
constructor(protected options: IKubeApiOptions<T>) { constructor(protected options: IKubeApiOptions<T>) {
const { const {
objectConstructor = KubeObject as IKubeObjectConstructor, objectConstructor,
request = apiKube, request = apiKube,
kind = options.objectConstructor?.kind, kind = options.objectConstructor?.kind,
isNamespaced = options.objectConstructor?.namespaced isNamespaced = options.objectConstructor?.namespaced
@ -174,8 +169,7 @@ export class KubeApi<T extends KubeObject = any> {
this.objectConstructor = objectConstructor; this.objectConstructor = objectConstructor;
this.checkPreferredVersion(); this.checkPreferredVersion();
this.parseResponse = this.parseResponse.bind(this); ApiManager.getInstance().registerApi(apiBase, this);
apiManager.registerApi(apiBase, this);
} }
get apiVersionWithGroup() { get apiVersionWithGroup() {
@ -264,7 +258,7 @@ export class KubeApi<T extends KubeObject = any> {
if (this.apiVersionPreferred) { if (this.apiVersionPreferred) {
Object.defineProperty(this, "apiBase", { value: this.getUrl() }); Object.defineProperty(this, "apiBase", { value: this.getUrl() });
apiManager.registerApi(this.apiBase, this); ApiManager.getInstance().registerApi(this.apiBase, this);
} }
} }
} }
@ -506,5 +500,3 @@ export class KubeApi<T extends KubeObject = any> {
} }
} }
} }
export * from "./kube-api-parse";

View File

@ -265,11 +265,11 @@ 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 updateReturnNew(data: Partial<this>): Promise<this> {
return resourceApplierApi.update<T>({ return new (this.constructor as any)(resourceApplierApi.update({
...this.toPlainObject(), ...this.toPlainObject(),
...data, ...data,
}); }));
} }
delete(params?: JsonApiParams) { delete(params?: JsonApiParams) {

View File

@ -23,14 +23,17 @@
// API: https://developer.mozilla.org/en-US/docs/Web/API/Streams_API/Using_readable_streams // API: https://developer.mozilla.org/en-US/docs/Web/API/Streams_API/Using_readable_streams
import type { KubeObjectStore } from "../kube-object.store"; import type { KubeObjectStore } from "../kube-object.store";
import type { ClusterContext } from "../components/context"; import { allNamespaces, selectedNamespaces } from "../components/context";
import plimit from "p-limit"; import plimit from "p-limit";
import { comparer, IReactionDisposer, observable, reaction, when } from "mobx"; import { comparer, IReactionDisposer, reaction } from "mobx";
import { autobind, noop } from "../utils"; import { autobind, Disposer, noop, Singleton } from "../utils";
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";
import type { Cluster } from "../../main/cluster";
import type { KubeObject } from "./kube-object";
import type { KubeApi } from "./kube-api";
import { ApiManager } from "./api-manager";
export interface IKubeWatchEvent<T = KubeJsonApiData> { export interface IKubeWatchEvent<T = KubeJsonApiData> {
type: "ADDED" | "MODIFIED" | "DELETED" | "ERROR"; type: "ADDED" | "MODIFIED" | "DELETED" | "ERROR";
@ -51,16 +54,12 @@ export interface IKubeWatchLog {
} }
@autobind() @autobind()
export class KubeWatchApi { export class KubeWatchApi extends Singleton {
@observable context: ClusterContext = null; constructor(protected cluster: Cluster) {
super();
contextReady = when(() => Boolean(this.context));
isAllowedApi(api: KubeApi): boolean {
return Boolean(this.context?.cluster.isAllowedResource(api.kind));
} }
preloadStores(stores: KubeObjectStore[], opts: { namespaces?: string[], loadOnce?: boolean } = {}) { private preloadStores(stores: KubeObjectStore<KubeObject>[], opts: { namespaces?: string[], loadOnce?: boolean } = {}) {
const limitRequests = plimit(1); // load stores one by one to allow quick skipping when fast clicking btw pages const limitRequests = plimit(1); // load stores one by one to allow quick skipping when fast clicking btw pages
const preloading: Promise<any>[] = []; const preloading: Promise<any>[] = [];
@ -78,9 +77,15 @@ export class KubeWatchApi {
}; };
} }
subscribeStores(stores: KubeObjectStore[], opts: IKubeWatchSubscribeStoreOptions = {}): () => void { subscribeApis(apis: KubeApi[], opts: IKubeWatchSubscribeStoreOptions = {}): Disposer {
const manager = ApiManager.getInstance();
return this.subscribeStores(apis.map(api => manager.getStore(api)), opts);
}
subscribeStores(stores: KubeObjectStore<KubeObject>[], opts: IKubeWatchSubscribeStoreOptions = {}): Disposer {
const { preload = true, waitUntilLoaded = true, loadOnce = false, } = opts; const { preload = true, waitUntilLoaded = true, loadOnce = false, } = opts;
const subscribingNamespaces = opts.namespaces ?? this.context?.allNamespaces ?? []; const subscribingNamespaces = opts.namespaces ?? allNamespaces(this.cluster);
const unsubscribeList: Function[] = []; const unsubscribeList: Function[] = [];
let isUnsubscribed = false; let isUnsubscribed = false;
@ -109,7 +114,7 @@ export class KubeWatchApi {
} }
// reload stores only for context namespaces change // reload stores only for context namespaces change
cancelReloading = reaction(() => this.context?.contextNamespaces, namespaces => { cancelReloading = reaction(() => selectedNamespaces(), namespaces => {
preloading?.cancelLoading(); preloading?.cancelLoading();
unsubscribeList.forEach(unsubscribe => unsubscribe()); unsubscribeList.forEach(unsubscribe => unsubscribe());
unsubscribeList.length = 0; unsubscribeList.length = 0;
@ -149,5 +154,3 @@ export class KubeWatchApi {
} }
} }
} }
export const kubeWatchApi = new KubeWatchApi();

View File

@ -37,7 +37,7 @@ 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";
import { FilesystemProvisionerStore } from "../main/extension-filesystem"; import { FilesystemProvisionerStore } from "../main/extension-filesystem";
import { App } from "./components/app"; import { ClusterFrame } from "./components/app";
import { LensApp } from "./lens-app"; import { LensApp } from "./lens-app";
import { ThemeStore } from "./theme.store"; import { ThemeStore } from "./theme.store";
import { HelmRepoManager } from "../main/helm/helm-repo-manager"; import { HelmRepoManager } from "../main/helm/helm-repo-manager";
@ -127,4 +127,4 @@ export async function bootstrap(App: AppComponent) {
} }
// run // run
bootstrap(process.isMainFrame ? LensApp : App); bootstrap(process.isMainFrame ? LensApp : ClusterFrame);

View File

@ -38,6 +38,7 @@ import { PageLayout } from "../layout/page-layout";
import { docsUrl } from "../../../common/vars"; import { docsUrl } from "../../../common/vars";
import { Input } from "../input"; import { Input } from "../input";
import { catalogURL, preferencesURL } from "../../../common/routes"; import { catalogURL, preferencesURL } from "../../../common/routes";
import { embedCustomKubeConfig } from "../../utils";
@observer @observer
export class AddCluster extends React.Component { export class AddCluster extends React.Component {
@ -107,7 +108,7 @@ export class AddCluster extends React.Component {
}).map(context => { }).map(context => {
const clusterId = uuid(); const clusterId = uuid();
const kubeConfig = this.kubeContexts.get(context); const kubeConfig = this.kubeContexts.get(context);
const kubeConfigPath = ClusterStore.embedCustomKubeConfig(clusterId, kubeConfig); // save in app-files folder const kubeConfigPath = embedCustomKubeConfig(clusterId, kubeConfig); // save in app-files folder
return { return {
id: clusterId, id: clusterId,

View File

@ -37,14 +37,14 @@ import { Spinner } from "../spinner";
import { Table, TableCell, TableHead, TableRow } from "../table"; import { Table, TableCell, TableHead, TableRow } from "../table";
import { AceEditor } from "../ace-editor"; import { AceEditor } from "../ace-editor";
import { Button } from "../button"; import { Button } from "../button";
import { releaseStore } from "./release.store"; import { ReleaseStore } from "./release.store";
import { Notifications } from "../notifications"; import { Notifications } from "../notifications";
import { createUpgradeChartTab } from "../dock/upgrade-chart.store"; import { createUpgradeChartTab } from "../dock/upgrade-chart.store";
import { ThemeStore } from "../../theme.store"; import { ThemeStore } from "../../theme.store";
import { apiManager } from "../../api/api-manager"; import { ApiManager } from "../../api/api-manager";
import { SubTitle } from "../layout/sub-title"; import { SubTitle } from "../layout/sub-title";
import { secretsStore } from "../+config-secrets/secrets.store"; import type { SecretsStore } from "../+config-secrets/secrets.store";
import { Secret } from "../../api/endpoints"; import { Secret, secretsApi } from "../../api/endpoints";
import { getDetailsUrl } from "../kube-object"; import { getDetailsUrl } from "../kube-object";
import { Checkbox } from "../checkbox"; import { Checkbox } from "../checkbox";
@ -62,6 +62,10 @@ export class ReleaseDetails extends Component<Props> {
@observable saving = false; @observable saving = false;
@observable releaseSecret: Secret; @observable releaseSecret: Secret;
private get secretsStore() {
return ApiManager.getInstance().getStore<SecretsStore>(secretsApi);
}
@disposeOnUnmount @disposeOnUnmount
releaseSelector = reaction(() => this.props.release, release => { releaseSelector = reaction(() => this.props.release, release => {
if (!release) return; if (!release) return;
@ -72,9 +76,9 @@ export class ReleaseDetails extends Component<Props> {
); );
@disposeOnUnmount @disposeOnUnmount
secretWatcher = reaction(() => secretsStore.items.toJS(), () => { secretWatcher = reaction(() => this.secretsStore.items.toJS(), () => {
if (!this.props.release) return; if (!this.props.release) return;
const { getReleaseSecret } = releaseStore; const { getReleaseSecret } = ReleaseStore.getInstance();
const { release } = this.props; const { release } = this.props;
const secret = getReleaseSecret(release); const secret = getReleaseSecret(release);
@ -115,7 +119,7 @@ export class ReleaseDetails extends Component<Props> {
this.saving = true; this.saving = true;
try { try {
await releaseStore.update(name, namespace, data); await ReleaseStore.getInstance().update(name, namespace, data);
Notifications.ok( Notifications.ok(
<p>Release <b>{name}</b> successfully updated!</p> <p>Release <b>{name}</b> successfully updated!</p>
); );
@ -197,7 +201,7 @@ export class ReleaseDetails extends Component<Props> {
{items.map(item => { {items.map(item => {
const name = item.getName(); const name = item.getName();
const namespace = item.getNs(); const namespace = item.getNs();
const api = apiManager.getApi(item.metadata.selfLink); const api = ApiManager.getInstance().getApi(item.metadata.selfLink);
const detailsUrl = api ? getDetailsUrl(api.getUrl({ name, namespace })) : ""; const detailsUrl = api ? getDetailsUrl(api.getUrl({ name, namespace })) : "";
return ( return (

View File

@ -22,12 +22,12 @@
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 { autobind, cssNames } from "../../utils";
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";
import { Icon } from "../icon"; import { Icon } from "../icon";
import { ReleaseRollbackDialog } from "./release-rollback-dialog"; import { ReleaseRollbackDialog } from "./release-rollback-dialog";
import { createUpgradeChartTab } from "../dock/upgrade-chart.store"; import { createUpgradeChartTab } from "../dock/upgrade-chart.store";
import { ReleaseStore } from "./release.store";
interface Props extends MenuActionsProps { interface Props extends MenuActionsProps {
release: HelmRelease; release: HelmRelease;
@ -37,7 +37,7 @@ interface Props extends MenuActionsProps {
export class HelmReleaseMenu extends React.Component<Props> { export class HelmReleaseMenu extends React.Component<Props> {
@autobind() @autobind()
remove() { remove() {
return releaseStore.remove(this.props.release); return ReleaseStore.getInstance().remove(this.props.release);
} }
@autobind() @autobind()

View File

@ -27,7 +27,7 @@ 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";
import { getReleaseHistory, HelmRelease, IReleaseRevision } from "../../api/endpoints/helm-releases.api"; import { getReleaseHistory, HelmRelease, IReleaseRevision } from "../../api/endpoints/helm-releases.api";
import { releaseStore } from "./release.store"; import { ReleaseStore } from "./release.store";
import { Select, SelectOption } from "../select"; import { Select, SelectOption } from "../select";
import { Notifications } from "../notifications"; import { Notifications } from "../notifications";
import orderBy from "lodash/orderBy"; import orderBy from "lodash/orderBy";
@ -73,7 +73,7 @@ export class ReleaseRollbackDialog extends React.Component<Props> {
const revisionNumber = this.revision.revision; const revisionNumber = this.revision.revision;
try { try {
await releaseStore.rollback(this.release.getName(), this.release.getNs(), revisionNumber); await ReleaseStore.getInstance().rollback(this.release.getName(), this.release.getNs(), revisionNumber);
this.close(); this.close();
} catch (err) { } catch (err) {
Notifications.error(err); Notifications.error(err);

View File

@ -24,24 +24,44 @@ import { action, observable, reaction, when } 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 { Secret, secretsApi } from "../../api/endpoints";
import { secretsStore } from "../+config-secrets/secrets.store"; import type { SecretsStore } from "../+config-secrets/secrets.store";
import { namespaceStore } from "../+namespaces/namespace.store";
import { Notifications } from "../notifications"; import { Notifications } from "../notifications";
import { ApiManager } from "../../api/api-manager";
import { isLoadingFromAllNamespaces, selectedNamespaces } from "../context";
import type { Cluster } from "../../../main/cluster";
@autobind() @autobind()
export class ReleaseStore extends ItemStore<HelmRelease> { export class ReleaseStore extends ItemStore<HelmRelease> {
private static instance?: ReleaseStore;
static getInstance() {
if (!this.instance) {
throw new TypeError("instance of ReleaseStore is not created");
}
return this.instance;
}
static createInstance(cluster: Cluster) {
return this.instance ??= new this(cluster);
}
private get secretsStore() {
return ApiManager.getInstance().getStore<SecretsStore>(secretsApi);
}
releaseSecrets = observable.map<string, Secret>(); releaseSecrets = observable.map<string, Secret>();
constructor() { private constructor(protected cluster: Cluster) {
super(); super();
when(() => secretsStore.isLoaded, () => { when(() => this.secretsStore.isLoaded, () => {
this.releaseSecrets.replace(this.getReleaseSecrets()); this.releaseSecrets.replace(this.getReleaseSecrets());
}); });
} }
watchAssociatedSecrets(): (() => void) { watchAssociatedSecrets(): (() => void) {
return reaction(() => secretsStore.items.toJS(), () => { return reaction(() => this.secretsStore.items.toJS(), () => {
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;
@ -57,19 +77,19 @@ export class ReleaseStore extends ItemStore<HelmRelease> {
} }
watchSelecteNamespaces(): (() => void) { watchSelecteNamespaces(): (() => void) {
return reaction(() => namespaceStore.context.contextNamespaces, namespaces => { return reaction(() => selectedNamespaces(), namespaces => {
this.loadAll(namespaces); this.loadAll(namespaces);
}); });
} }
private getReleaseSecrets() { private getReleaseSecrets() {
return secretsStore return this.secretsStore
.getByLabel({ owner: "helm" }) .getByLabel({ owner: "helm" })
.map(s => [s.getId(), s] as const); .map(s => [s.getId(), s] as const);
} }
getReleaseSecret(release: HelmRelease) { getReleaseSecret(release: HelmRelease) {
return secretsStore.getByLabel({ return this.secretsStore.getByLabel({
owner: "helm", owner: "helm",
name: release.getName() name: release.getName()
}) })
@ -100,15 +120,11 @@ export class ReleaseStore extends ItemStore<HelmRelease> {
} }
async loadFromContextNamespaces(): Promise<void> { async loadFromContextNamespaces(): Promise<void> {
return this.loadAll(namespaceStore.context.contextNamespaces); return this.loadAll(selectedNamespaces());
} }
async loadItems(namespaces: string[]) { async loadItems(namespaces: string[]) {
const isLoadingAll = namespaceStore.context.allNamespaces?.length > 1 if (isLoadingFromAllNamespaces(this.cluster, namespaces)) {
&& namespaceStore.context.cluster.accessibleNamespaces.length === 0
&& namespaceStore.context.allNamespaces.every(ns => namespaces.includes(ns));
if (isLoadingAll) {
return listReleases(); return listReleases();
} }
@ -149,5 +165,3 @@ export class ReleaseStore extends ItemStore<HelmRelease> {
return Promise.all(this.selectedItems.map(this.remove)); return Promise.all(this.selectedItems.map(this.remove));
} }
} }
export const releaseStore = new ReleaseStore();

View File

@ -25,15 +25,17 @@ import React, { Component } from "react";
import kebabCase from "lodash/kebabCase"; import kebabCase from "lodash/kebabCase";
import { disposeOnUnmount, observer } from "mobx-react"; import { disposeOnUnmount, observer } from "mobx-react";
import type { RouteComponentProps } from "react-router"; import type { RouteComponentProps } from "react-router";
import { releaseStore } from "./release.store"; import { ReleaseStore } from "./release.store";
import type { HelmRelease } from "../../api/endpoints/helm-releases.api"; import type { HelmRelease } from "../../api/endpoints/helm-releases.api";
import { ReleaseDetails } from "./release-details"; import { ReleaseDetails } from "./release-details";
import { ReleaseRollbackDialog } from "./release-rollback-dialog"; import { ReleaseRollbackDialog } from "./release-rollback-dialog";
import { navigation } from "../../navigation"; import { navigation } from "../../navigation";
import { ItemListLayout } from "../item-object-list/item-list-layout"; import { ItemListLayout } from "../item-object-list/item-list-layout";
import { HelmReleaseMenu } from "./release-menu"; import { HelmReleaseMenu } from "./release-menu";
import { secretsStore } from "../+config-secrets/secrets.store"; import type { SecretsStore } from "../+config-secrets/secrets.store";
import { ReleaseRouteParams, releaseURL } from "../../../common/routes"; import { ReleaseRouteParams, releaseURL } from "../../../common/routes";
import { secretsApi } from "../../api/endpoints";
import { ApiManager } from "../../api/api-manager";
enum columnId { enum columnId {
name = "name", name = "name",
@ -51,17 +53,21 @@ interface Props extends RouteComponentProps<ReleaseRouteParams> {
@observer @observer
export class HelmReleases extends Component<Props> { export class HelmReleases extends Component<Props> {
private get secretsStore() {
return ApiManager.getInstance().getStore<SecretsStore>(secretsApi);
}
componentDidMount() { componentDidMount() {
disposeOnUnmount(this, [ disposeOnUnmount(this, [
releaseStore.watchAssociatedSecrets(), ReleaseStore.getInstance().watchAssociatedSecrets(),
releaseStore.watchSelecteNamespaces(), ReleaseStore.getInstance().watchSelecteNamespaces(),
]); ]);
} }
get selectedRelease() { get selectedRelease() {
const { match: { params: { name, namespace } } } = this.props; const { match: { params: { name, namespace } } } = this.props;
return releaseStore.items.find(release => { return ReleaseStore.getInstance().items.find(release => {
return release.getName() == name && release.getNs() == namespace; return release.getName() == name && release.getNs() == namespace;
}); });
} }
@ -99,8 +105,8 @@ export class HelmReleases extends Component<Props> {
isConfigurable isConfigurable
tableId="helm_releases" tableId="helm_releases"
className="HelmReleases" className="HelmReleases"
store={releaseStore} store={ReleaseStore.getInstance()}
dependentStores={[secretsStore]} dependentStores={[this.secretsStore]}
sortingCallbacks={{ sortingCallbacks={{
[columnId.name]: (release: HelmRelease) => release.getName(), [columnId.name]: (release: HelmRelease) => release.getName(),
[columnId.namespace]: (release: HelmRelease) => release.getNs(), [columnId.namespace]: (release: HelmRelease) => release.getNs(),

View File

@ -26,10 +26,11 @@ import { HelmCharts } from "../+apps-helm-charts";
import { HelmReleases } from "../+apps-releases"; import { HelmReleases } from "../+apps-releases";
import { namespaceUrlParam } from "../+namespaces/namespace.store"; import { namespaceUrlParam } from "../+namespaces/namespace.store";
import { helmChartsURL, helmChartsRoute, releaseURL, releaseRoute } from "../../../common/routes"; import { helmChartsURL, helmChartsRoute, releaseURL, releaseRoute } from "../../../common/routes";
import type { Cluster } from "../../../main/cluster";
@observer @observer
export class Apps extends React.Component { export class Apps extends React.Component<{ cluster: Cluster }> {
static get tabRoutes(): TabLayoutRoute[] { static tabRoutes(): TabLayoutRoute[] {
const query = namespaceUrlParam.toObjectParam(); const query = namespaceUrlParam.toObjectParam();
return [ return [
@ -50,7 +51,7 @@ export class Apps extends React.Component {
render() { render() {
return ( return (
<TabLayout className="Apps" tabs={Apps.tabRoutes}/> <TabLayout className="Apps" tabs={Apps.tabRoutes()}/>
); );
} }
} }

View File

@ -27,14 +27,15 @@ import { computed } from "mobx";
import { Icon } from "../icon"; import { Icon } from "../icon";
import { SubHeader } from "../layout/sub-header"; import { SubHeader } from "../layout/sub-header";
import { Table, TableCell, TableHead, TableRow } from "../table"; import { Table, TableCell, TableHead, TableRow } from "../table";
import { nodesStore } from "../+nodes/nodes.store"; import type { NodesStore } from "../+nodes";
import { eventStore } from "../+events/event.store"; import type { EventStore } from "../+events/event.store";
import { autobind, cssNames, prevDefault } from "../../utils"; import { autobind, cssNames, prevDefault } from "../../utils";
import type { ItemObject } from "../../item.store"; import type { ItemObject } from "../../item.store";
import { Spinner } from "../spinner"; import { Spinner } from "../spinner";
import { ThemeStore } from "../../theme.store"; import { ThemeStore } from "../../theme.store";
import { lookupApiLink } from "../../api/kube-api";
import { kubeSelectedUrlParam, showDetails } from "../kube-object"; import { kubeSelectedUrlParam, showDetails } from "../kube-object";
import { ApiManager } from "../../api/api-manager";
import { eventApi, nodesApi } from "../../api/endpoints";
interface Props { interface Props {
className?: string; className?: string;
@ -62,11 +63,19 @@ export class ClusterIssues extends React.Component<Props> {
[sortBy.age]: (warning: IWarning) => warning.timeDiffFromNow, [sortBy.age]: (warning: IWarning) => warning.timeDiffFromNow,
}; };
private get nodesStore() {
return ApiManager.getInstance().getStore<NodesStore>(nodesApi);
}
private get eventStore() {
return ApiManager.getInstance().getStore<EventStore>(eventApi);
}
@computed get warnings() { @computed get warnings() {
const warnings: IWarning[] = []; const warnings: IWarning[] = [];
// Node bad conditions // Node bad conditions
nodesStore.items.forEach(node => { this.nodesStore.items.forEach(node => {
const { kind, selfLink, getId, getName, getAge, getTimeDiffFromNow } = node; const { kind, selfLink, getId, getName, getAge, getTimeDiffFromNow } = node;
node.getWarningConditions().forEach(({ message }) => { node.getWarningConditions().forEach(({ message }) => {
@ -83,7 +92,7 @@ export class ClusterIssues extends React.Component<Props> {
}); });
// Warning events for Workloads // Warning events for Workloads
const events = eventStore.getWarnings(); const events = this.eventStore.getWarnings();
events.forEach(error => { events.forEach(error => {
const { message, involvedObject, getAge, getTimeDiffFromNow } = error; const { message, involvedObject, getAge, getTimeDiffFromNow } = error;
@ -96,7 +105,7 @@ export class ClusterIssues extends React.Component<Props> {
age: getAge(), age: getAge(),
message, message,
kind, kind,
selfLink: lookupApiLink(involvedObject, error), selfLink: ApiManager.getInstance().lookupApiLink(involvedObject, error),
}); });
}); });
@ -135,7 +144,7 @@ export class ClusterIssues extends React.Component<Props> {
renderContent() { renderContent() {
const { warnings } = this; const { warnings } = this;
if (!eventStore.isLoaded) { if (!this.eventStore.isLoaded) {
return ( return (
<Spinner center/> <Spinner center/>
); );

View File

@ -23,12 +23,16 @@ import "./cluster-metric-switchers.scss";
import React from "react"; import React from "react";
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import { nodesStore } from "../+nodes/nodes.store"; import type { NodesStore } from "../+nodes";
import { cssNames } from "../../utils"; import { cssNames } from "../../utils";
import { Radio, RadioGroup } from "../radio"; import { Radio, RadioGroup } from "../radio";
import { clusterOverviewStore, MetricNodeRole, MetricType } from "./cluster-overview.store"; import { ClusterObjectStore, MetricNodeRole, MetricType } from "./cluster-overview.store";
import { ApiManager } from "../../api/api-manager";
import { clusterApi, nodesApi } from "../../api/endpoints";
export const ClusterMetricSwitchers = observer(() => { export const ClusterMetricSwitchers = observer(() => {
const nodesStore = ApiManager.getInstance().getStore<NodesStore>(nodesApi);
const clusterOverviewStore = ApiManager.getInstance().getStore<ClusterObjectStore>(clusterApi);
const { metricType, metricNodeRole, getMetricsValues, metrics } = clusterOverviewStore; const { metricType, metricNodeRole, getMetricsValues, metrics } = clusterOverviewStore;
const { masterNodes, workerNodes } = nodesStore; const { masterNodes, workerNodes } = nodesStore;
const metricsValues = getMetricsValues(metrics); const metricsValues = getMetricsValues(metrics);

View File

@ -24,7 +24,7 @@ import "./cluster-metrics.scss";
import React from "react"; import React from "react";
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import type { ChartOptions, ChartPoint } from "chart.js"; import type { ChartOptions, ChartPoint } from "chart.js";
import { clusterOverviewStore, MetricType } from "./cluster-overview.store"; import { ClusterObjectStore, MetricType } from "./cluster-overview.store";
import { BarChart } from "../chart"; import { BarChart } from "../chart";
import { bytesToUnits } from "../../utils"; import { bytesToUnits } from "../../utils";
import { Spinner } from "../spinner"; import { Spinner } from "../spinner";
@ -32,8 +32,11 @@ import { ZebraStripes } from "../chart/zebra-stripes.plugin";
import { ClusterNoMetrics } from "./cluster-no-metrics"; import { ClusterNoMetrics } from "./cluster-no-metrics";
import { ClusterMetricSwitchers } from "./cluster-metric-switchers"; import { ClusterMetricSwitchers } from "./cluster-metric-switchers";
import { getMetricLastPoints } from "../../api/endpoints/metrics.api"; import { getMetricLastPoints } from "../../api/endpoints/metrics.api";
import { ApiManager } from "../../api/api-manager";
import { clusterApi } from "../../api/endpoints";
export const ClusterMetrics = observer(() => { export const ClusterMetrics = observer(() => {
const clusterOverviewStore = ApiManager.getInstance().getStore<ClusterObjectStore>(clusterApi);
const { metricType, metricNodeRole, getMetricsValues, metricsLoaded, metrics } = clusterOverviewStore; const { metricType, metricNodeRole, getMetricsValues, metricsLoaded, metrics } = clusterOverviewStore;
const { memoryCapacity, cpuCapacity } = getMetricLastPoints(clusterOverviewStore.metrics); const { memoryCapacity, cpuCapacity } = getMetricLastPoints(clusterOverviewStore.metrics);
const metricValues = getMetricsValues(metrics); const metricValues = getMetricsValues(metrics);

View File

@ -21,11 +21,11 @@
import { action, observable, reaction, when } from "mobx"; import { action, observable, reaction, when } from "mobx";
import { KubeObjectStore } from "../../kube-object.store"; import { KubeObjectStore } from "../../kube-object.store";
import { Cluster, clusterApi, IClusterMetrics } from "../../api/endpoints"; import { KubeCluster, clusterApi, IClusterMetrics, nodesApi } from "../../api/endpoints";
import { autobind, createStorage } from "../../utils"; import { autobind, createStorage } from "../../utils";
import { IMetricsReqParams, normalizeMetrics } from "../../api/endpoints/metrics.api"; import { IMetricsReqParams, normalizeMetrics } from "../../api/endpoints/metrics.api";
import { nodesStore } from "../+nodes/nodes.store"; import type { NodesStore } from "../+nodes/nodes.store";
import { apiManager } from "../../api/api-manager"; import { ApiManager } from "../../api/api-manager";
export enum MetricType { export enum MetricType {
MEMORY = "memory", MEMORY = "memory",
@ -43,7 +43,7 @@ export interface ClusterOverviewStorageState {
} }
@autobind() @autobind()
export class ClusterOverviewStore extends KubeObjectStore<Cluster> implements ClusterOverviewStorageState { export class ClusterObjectStore extends KubeObjectStore<KubeCluster> implements ClusterOverviewStorageState {
api = clusterApi; api = clusterApi;
@observable metrics: Partial<IClusterMetrics> = {}; @observable metrics: Partial<IClusterMetrics> = {};
@ -70,12 +70,11 @@ export class ClusterOverviewStore extends KubeObjectStore<Cluster> implements Cl
this.storage.merge({ metricNodeRole: value }); this.storage.merge({ metricNodeRole: value });
} }
constructor() { private get nodesStore() {
super(); return ApiManager.getInstance().getStore<NodesStore>(nodesApi);
this.init();
} }
private init() { protected init = () => {
// TODO: refactor, seems not a correct place to be // TODO: refactor, seems not a correct place to be
// auto-refresh metrics on user-action // auto-refresh metrics on user-action
reaction(() => this.metricNodeRole, () => { reaction(() => this.metricNodeRole, () => {
@ -85,18 +84,18 @@ export class ClusterOverviewStore extends KubeObjectStore<Cluster> implements Cl
}); });
// check which node type to select // check which node type to select
reaction(() => nodesStore.items.length, () => { reaction(() => this.nodesStore.items.length, () => {
const { masterNodes, workerNodes } = nodesStore; const { masterNodes, workerNodes } = this.nodesStore;
if (!masterNodes.length) this.metricNodeRole = MetricNodeRole.WORKER; if (!masterNodes.length) this.metricNodeRole = MetricNodeRole.WORKER;
if (!workerNodes.length) this.metricNodeRole = MetricNodeRole.MASTER; if (!workerNodes.length) this.metricNodeRole = MetricNodeRole.MASTER;
}); });
} };
@action @action
async loadMetrics(params?: IMetricsReqParams) { async loadMetrics(params?: IMetricsReqParams) {
await when(() => nodesStore.isLoaded); await when(() => this.nodesStore.isLoaded);
const { masterNodes, workerNodes } = nodesStore; const { masterNodes, workerNodes } = this.nodesStore;
const nodes = this.metricNodeRole === MetricNodeRole.MASTER && masterNodes.length ? masterNodes : workerNodes; const nodes = this.metricNodeRole === MetricNodeRole.MASTER && masterNodes.length ? masterNodes : workerNodes;
this.metrics = await clusterApi.getMetrics(nodes.map(node => node.getName()), params); this.metrics = await clusterApi.getMetrics(nodes.map(node => node.getName()), params);
@ -126,6 +125,3 @@ export class ClusterOverviewStore extends KubeObjectStore<Cluster> implements Cl
this.storage?.reset(); this.storage?.reset();
} }
} }
export const clusterOverviewStore = new ClusterOverviewStore();
apiManager.registerStore(clusterOverviewStore);

View File

@ -24,24 +24,38 @@ import "./cluster-overview.scss";
import React from "react"; import React from "react";
import { reaction } from "mobx"; import { reaction } from "mobx";
import { disposeOnUnmount, observer } from "mobx-react"; import { disposeOnUnmount, observer } from "mobx-react";
import { nodesStore } from "../+nodes/nodes.store";
import { podsStore } from "../+workloads-pods/pods.store";
import { ClusterStore, getHostedCluster } from "../../../common/cluster-store"; import { ClusterStore, getHostedCluster } from "../../../common/cluster-store";
import { interval } from "../../utils"; import { interval } from "../../utils";
import { TabLayout } from "../layout/tab-layout"; import { TabLayout } from "../layout/tab-layout";
import { Spinner } from "../spinner"; import { Spinner } from "../spinner";
import { ClusterIssues } from "./cluster-issues"; import { ClusterIssues } from "./cluster-issues";
import { ClusterMetrics } from "./cluster-metrics"; import { ClusterMetrics } from "./cluster-metrics";
import { clusterOverviewStore } from "./cluster-overview.store";
import { ClusterPieCharts } from "./cluster-pie-charts"; import { ClusterPieCharts } from "./cluster-pie-charts";
import { ResourceType } from "../cluster-settings/components/cluster-metrics-setting"; import { ResourceType } from "../cluster-settings/components/cluster-metrics-setting";
import { clusterApi, nodesApi, podsApi } from "../../api/endpoints";
import type { NodesStore } from "../+nodes";
import type { PodsStore } from "../+workloads-pods";
import { ApiManager } from "../../api/api-manager";
import type { ClusterObjectStore } from "./cluster-overview.store";
@observer @observer
export class ClusterOverview extends React.Component { export class ClusterOverview extends React.Component {
private get nodesStore() {
return ApiManager.getInstance().getStore<NodesStore>(nodesApi);
}
private get podsStore() {
return ApiManager.getInstance().getStore<PodsStore>(podsApi);
}
private get clusterObjectStore() {
return ApiManager.getInstance().getStore<ClusterObjectStore>(clusterApi);
}
private metricPoller = interval(60, () => this.loadMetrics()); private metricPoller = interval(60, () => this.loadMetrics());
loadMetrics() { loadMetrics() {
getHostedCluster().available && clusterOverviewStore.loadMetrics(); getHostedCluster().available && this.clusterObjectStore.loadMetrics();
} }
componentDidMount() { componentDidMount() {
@ -49,7 +63,7 @@ export class ClusterOverview extends React.Component {
disposeOnUnmount(this, [ disposeOnUnmount(this, [
reaction( reaction(
() => clusterOverviewStore.metricNodeRole, // Toggle Master/Worker node switcher () => this.clusterObjectStore.metricNodeRole, // Toggle Master/Worker node switcher
() => this.metricPoller.restart(true) () => this.metricPoller.restart(true)
), ),
]); ]);
@ -86,7 +100,7 @@ export class ClusterOverview extends React.Component {
} }
render() { render() {
const isLoaded = nodesStore.isLoaded && podsStore.isLoaded; const isLoaded = this.nodesStore.isLoaded && this.podsStore.isLoaded;
const isMetricsHidden = ClusterStore.getInstance().isMetricHidden(ResourceType.Cluster); const isMetricsHidden = ClusterStore.getInstance().isMetricHidden(ResourceType.Cluster);
return ( return (

View File

@ -23,17 +23,22 @@ import "./cluster-pie-charts.scss";
import React from "react"; import React from "react";
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import { clusterOverviewStore, MetricNodeRole } from "./cluster-overview.store"; import { ClusterObjectStore, MetricNodeRole } from "./cluster-overview.store";
import { Spinner } from "../spinner"; import { Spinner } from "../spinner";
import { Icon } from "../icon"; import { Icon } from "../icon";
import { nodesStore } from "../+nodes/nodes.store"; import type { NodesStore } from "../+nodes";
import { ChartData, PieChart } from "../chart"; import { ChartData, PieChart } from "../chart";
import { ClusterNoMetrics } from "./cluster-no-metrics"; import { ClusterNoMetrics } from "./cluster-no-metrics";
import { bytesToUnits } from "../../utils"; import { bytesToUnits } from "../../utils";
import { ThemeStore } from "../../theme.store"; import { ThemeStore } from "../../theme.store";
import { getMetricLastPoints } from "../../api/endpoints/metrics.api"; import { getMetricLastPoints } from "../../api/endpoints/metrics.api";
import { ApiManager } from "../../api/api-manager";
import { clusterApi, nodesApi } from "../../api/endpoints";
export const ClusterPieCharts = observer(() => { export const ClusterPieCharts = observer(() => {
const clusterOverviewStore = ApiManager.getInstance().getStore<ClusterObjectStore>(clusterApi);
const nodesStore = ApiManager.getInstance().getStore<NodesStore>(nodesApi);
const renderLimitWarning = () => { const renderLimitWarning = () => {
return ( return (
<div className="node-warning flex gaps align-center"> <div className="node-warning flex gaps align-center">

View File

@ -30,8 +30,8 @@ import { KubeObjectDetailsProps, getDetailsUrl } from "../kube-object";
import { cssNames } from "../../utils"; import { cssNames } from "../../utils";
import { HorizontalPodAutoscaler, HpaMetricType, IHpaMetric } from "../../api/endpoints/hpa.api"; import { HorizontalPodAutoscaler, HpaMetricType, IHpaMetric } from "../../api/endpoints/hpa.api";
import { Table, TableCell, TableHead, TableRow } from "../table"; import { Table, TableCell, TableHead, TableRow } from "../table";
import { lookupApiLink } from "../../api/kube-api";
import { KubeObjectMeta } from "../kube-object/kube-object-meta"; import { KubeObjectMeta } from "../kube-object/kube-object-meta";
import { ApiManager } from "../../api/api-manager";
interface Props extends KubeObjectDetailsProps<HorizontalPodAutoscaler> { interface Props extends KubeObjectDetailsProps<HorizontalPodAutoscaler> {
} }
@ -54,7 +54,7 @@ export class HpaDetails extends React.Component<Props> {
case HpaMetricType.Object: case HpaMetricType.Object:
const { target } = metric.object; const { target } = metric.object;
const { kind, name } = target; const { kind, name } = target;
const objectUrl = getDetailsUrl(lookupApiLink(target, hpa)); const objectUrl = getDetailsUrl(ApiManager.getInstance().lookupApiLink(target, hpa));
return ( return (
<> <>
@ -107,7 +107,7 @@ export class HpaDetails extends React.Component<Props> {
<DrawerItem name="Reference"> <DrawerItem name="Reference">
{scaleTargetRef && ( {scaleTargetRef && (
<Link to={getDetailsUrl(lookupApiLink(scaleTargetRef, hpa))}> <Link to={getDetailsUrl(ApiManager.getInstance().lookupApiLink(scaleTargetRef, hpa))}>
{scaleTargetRef.kind}/{scaleTargetRef.name} {scaleTargetRef.kind}/{scaleTargetRef.name}
</Link> </Link>
)} )}

View File

@ -22,12 +22,8 @@
import { autobind } from "../../utils"; import { autobind } from "../../utils";
import { KubeObjectStore } from "../../kube-object.store"; import { KubeObjectStore } from "../../kube-object.store";
import { HorizontalPodAutoscaler, hpaApi } from "../../api/endpoints/hpa.api"; import { HorizontalPodAutoscaler, hpaApi } from "../../api/endpoints/hpa.api";
import { apiManager } from "../../api/api-manager";
@autobind() @autobind()
export class HPAStore extends KubeObjectStore<HorizontalPodAutoscaler> { export class HpaStore extends KubeObjectStore<HorizontalPodAutoscaler> {
api = hpaApi; api = hpaApi;
} }
export const hpaStore = new HPAStore();
apiManager.registerStore(hpaStore);

View File

@ -25,12 +25,13 @@ import React from "react";
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import type { RouteComponentProps } from "react-router"; import type { RouteComponentProps } from "react-router";
import { KubeObjectListLayout } from "../kube-object"; import { KubeObjectListLayout } from "../kube-object";
import type { HorizontalPodAutoscaler } from "../../api/endpoints/hpa.api"; import { HorizontalPodAutoscaler, hpaApi } from "../../api/endpoints/hpa.api";
import { hpaStore } from "./hpa.store";
import { Badge } from "../badge"; import { Badge } from "../badge";
import { cssNames } from "../../utils"; import { cssNames } from "../../utils";
import { KubeObjectStatusIcon } from "../kube-object-status-icon"; import { KubeObjectStatusIcon } from "../kube-object-status-icon";
import type { IHpaRouteParams } from "../../../common/routes"; import type { IHpaRouteParams } from "../../../common/routes";
import { ApiManager } from "../../api/api-manager";
import type { HpaStore } from "./hpa.store";
enum columnId { enum columnId {
name = "name", name = "name",
@ -48,6 +49,10 @@ interface Props extends RouteComponentProps<IHpaRouteParams> {
@observer @observer
export class HorizontalPodAutoscalers extends React.Component<Props> { export class HorizontalPodAutoscalers extends React.Component<Props> {
private get hpaStore() {
return ApiManager.getInstance().getStore<HpaStore>(hpaApi);
}
getTargets(hpa: HorizontalPodAutoscaler) { getTargets(hpa: HorizontalPodAutoscaler) {
const metrics = hpa.getMetrics(); const metrics = hpa.getMetrics();
const metricsRemainCount = metrics.length - 1; const metricsRemainCount = metrics.length - 1;
@ -62,7 +67,8 @@ export class HorizontalPodAutoscalers extends React.Component<Props> {
<KubeObjectListLayout <KubeObjectListLayout
isConfigurable isConfigurable
tableId="configuration_hpa" tableId="configuration_hpa"
className="HorizontalPodAutoscalers" store={hpaStore} className="HorizontalPodAutoscalers"
store={this.hpaStore}
sortingCallbacks={{ sortingCallbacks={{
[columnId.name]: (item: HorizontalPodAutoscaler) => item.getName(), [columnId.name]: (item: HorizontalPodAutoscaler) => item.getName(),
[columnId.namespace]: (item: HorizontalPodAutoscaler) => item.getNs(), [columnId.namespace]: (item: HorizontalPodAutoscaler) => item.getNs(),

View File

@ -20,4 +20,5 @@
*/ */
export * from "./hpa"; export * from "./hpa";
export * from "./hpa.store";
export * from "./hpa-details"; export * from "./hpa-details";

View File

@ -20,4 +20,5 @@
*/ */
export * from "./limit-ranges"; export * from "./limit-ranges";
export * from "./limit-ranges.store";
export * from "./limit-range-details"; export * from "./limit-range-details";

View File

@ -21,13 +21,9 @@
import { autobind } from "../../../common/utils/autobind"; import { autobind } from "../../../common/utils/autobind";
import { KubeObjectStore } from "../../kube-object.store"; import { KubeObjectStore } from "../../kube-object.store";
import { apiManager } from "../../api/api-manager";
import { LimitRange, limitRangeApi } from "../../api/endpoints/limit-range.api"; import { LimitRange, limitRangeApi } from "../../api/endpoints/limit-range.api";
@autobind() @autobind()
export class LimitRangesStore extends KubeObjectStore<LimitRange> { export class LimitRangesStore extends KubeObjectStore<LimitRange> {
api = limitRangeApi; api = limitRangeApi;
} }
export const limitRangeStore = new LimitRangesStore();
apiManager.registerStore(limitRangeStore);

View File

@ -24,11 +24,12 @@ import "./limit-ranges.scss";
import type { RouteComponentProps } from "react-router"; import type { RouteComponentProps } from "react-router";
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import { KubeObjectListLayout } from "../kube-object/kube-object-list-layout"; import { KubeObjectListLayout } from "../kube-object/kube-object-list-layout";
import { limitRangeStore } from "./limit-ranges.store";
import React from "react"; import React from "react";
import { KubeObjectStatusIcon } from "../kube-object-status-icon"; import { KubeObjectStatusIcon } from "../kube-object-status-icon";
import type { LimitRange } from "../../api/endpoints/limit-range.api"; import { LimitRange, limitRangeApi } from "../../api/endpoints/limit-range.api";
import type { LimitRangeRouteParams } from "../../../common/routes"; import type { LimitRangeRouteParams } from "../../../common/routes";
import { ApiManager } from "../../api/api-manager";
import type { LimitRangesStore } from "./limit-ranges.store";
enum columnId { enum columnId {
name = "name", name = "name",
@ -41,13 +42,17 @@ interface Props extends RouteComponentProps<LimitRangeRouteParams> {
@observer @observer
export class LimitRanges extends React.Component<Props> { export class LimitRanges extends React.Component<Props> {
private get limitRangeStore() {
return ApiManager.getInstance().getStore<LimitRangesStore>(limitRangeApi);
}
render() { render() {
return ( return (
<KubeObjectListLayout <KubeObjectListLayout
isConfigurable isConfigurable
tableId="configuration_limitranges" tableId="configuration_limitranges"
className="LimitRanges" className="LimitRanges"
store={limitRangeStore} store={this.limitRangeStore}
sortingCallbacks={{ sortingCallbacks={{
[columnId.name]: (item: LimitRange) => item.getName(), [columnId.name]: (item: LimitRange) => item.getName(),
[columnId.namespace]: (item: LimitRange) => item.getNs(), [columnId.namespace]: (item: LimitRange) => item.getNs(),

View File

@ -28,16 +28,21 @@ import { DrawerTitle } from "../drawer";
import { Notifications } from "../notifications"; import { Notifications } from "../notifications";
import { Input } from "../input"; import { Input } from "../input";
import { Button } from "../button"; import { Button } from "../button";
import { configMapsStore } from "./config-maps.store";
import type { KubeObjectDetailsProps } from "../kube-object"; import type { KubeObjectDetailsProps } from "../kube-object";
import type { ConfigMap } from "../../api/endpoints"; import { ConfigMap, configMapApi } from "../../api/endpoints";
import { KubeObjectMeta } from "../kube-object/kube-object-meta"; import { KubeObjectMeta } from "../kube-object/kube-object-meta";
import type { ConfigMapsStore } from ".";
import { ApiManager } from "../../api/api-manager";
interface Props extends KubeObjectDetailsProps<ConfigMap> { interface Props extends KubeObjectDetailsProps<ConfigMap> {
} }
@observer @observer
export class ConfigMapDetails extends React.Component<Props> { export class ConfigMapDetails extends React.Component<Props> {
private get configMapsStore() {
return ApiManager.getInstance().getStore<ConfigMapsStore>(configMapApi);
}
@observable isSaving = false; @observable isSaving = false;
@observable data = observable.map(); @observable data = observable.map();
@ -58,7 +63,7 @@ export class ConfigMapDetails extends React.Component<Props> {
try { try {
this.isSaving = true; this.isSaving = true;
await configMapsStore.update(configMap, { ...configMap, data: this.data.toJSON() }); await this.configMapsStore.update(configMap, { ...configMap, data: this.data.toJSON() });
Notifications.ok( Notifications.ok(
<p> <p>
<>ConfigMap <b>{configMap.getName()}</b> successfully updated.</> <>ConfigMap <b>{configMap.getName()}</b> successfully updated.</>

View File

@ -22,12 +22,8 @@
import { KubeObjectStore } from "../../kube-object.store"; import { KubeObjectStore } from "../../kube-object.store";
import { autobind } from "../../utils"; import { autobind } from "../../utils";
import { ConfigMap, configMapApi } from "../../api/endpoints/configmap.api"; import { ConfigMap, configMapApi } from "../../api/endpoints/configmap.api";
import { apiManager } from "../../api/api-manager";
@autobind() @autobind()
export class ConfigMapsStore extends KubeObjectStore<ConfigMap> { export class ConfigMapsStore extends KubeObjectStore<ConfigMap> {
api = configMapApi; api = configMapApi;
} }
export const configMapsStore = new ConfigMapsStore();
apiManager.registerStore(configMapsStore);

View File

@ -24,11 +24,12 @@ import "./config-maps.scss";
import React from "react"; import React from "react";
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import type { RouteComponentProps } from "react-router"; import type { RouteComponentProps } from "react-router";
import { configMapsStore } from "./config-maps.store"; import { ConfigMap, configMapApi } from "../../api/endpoints/configmap.api";
import type { ConfigMap } from "../../api/endpoints/configmap.api";
import { KubeObjectListLayout } from "../kube-object"; import { KubeObjectListLayout } from "../kube-object";
import { KubeObjectStatusIcon } from "../kube-object-status-icon"; import { KubeObjectStatusIcon } from "../kube-object-status-icon";
import type { ConfigMapsRouteParams } from "../../../common/routes"; import type { ConfigMapsRouteParams } from "../../../common/routes";
import { ApiManager } from "../../api/api-manager";
import type { ConfigMapsStore } from "./config-maps.store";
enum columnId { enum columnId {
name = "name", name = "name",
@ -42,12 +43,17 @@ interface Props extends RouteComponentProps<ConfigMapsRouteParams> {
@observer @observer
export class ConfigMaps extends React.Component<Props> { export class ConfigMaps extends React.Component<Props> {
private get configMapsStore() {
return ApiManager.getInstance().getStore<ConfigMapsStore>(configMapApi);
}
render() { render() {
return ( return (
<KubeObjectListLayout <KubeObjectListLayout
isConfigurable isConfigurable
tableId="configuration_configmaps" tableId="configuration_configmaps"
className="ConfigMaps" store={configMapsStore} className="ConfigMaps"
store={this.configMapsStore}
sortingCallbacks={{ sortingCallbacks={{
[columnId.name]: (item: ConfigMap) => item.getName(), [columnId.name]: (item: ConfigMap) => item.getName(),
[columnId.namespace]: (item: ConfigMap) => item.getNs(), [columnId.namespace]: (item: ConfigMap) => item.getNs(),

View File

@ -20,4 +20,5 @@
*/ */
export * from "./config-maps"; export * from "./config-maps";
export * from "./config-maps.store";
export * from "./config-map-details"; export * from "./config-map-details";

View File

@ -20,4 +20,5 @@
*/ */
export * from "./pod-disruption-budgets"; export * from "./pod-disruption-budgets";
export * from "./pod-disruption-budgets.store";
export * from "./pod-disruption-budgets-details"; export * from "./pod-disruption-budgets-details";

View File

@ -21,13 +21,9 @@
import { KubeObjectStore } from "../../kube-object.store"; import { KubeObjectStore } from "../../kube-object.store";
import { autobind } from "../../utils"; import { autobind } from "../../utils";
import { PodDisruptionBudget, pdbApi } from "../../api/endpoints/poddisruptionbudget.api"; import { PodDisruptionBudget, podDisruptionBudgetApi } from "../../api/endpoints/poddisruptionbudget.api";
import { apiManager } from "../../api/api-manager";
@autobind() @autobind()
export class PodDisruptionBudgetsStore extends KubeObjectStore<PodDisruptionBudget> { export class PodDisruptionBudgetsStore extends KubeObjectStore<PodDisruptionBudget> {
api = pdbApi; api = podDisruptionBudgetApi;
} }
export const podDisruptionBudgetsStore = new PodDisruptionBudgetsStore();
apiManager.registerStore(podDisruptionBudgetsStore);

View File

@ -23,10 +23,11 @@ import "./pod-disruption-budgets.scss";
import * as React from "react"; import * as React from "react";
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import { podDisruptionBudgetsStore } from "./pod-disruption-budgets.store"; import { PodDisruptionBudget, podDisruptionBudgetApi } from "../../api/endpoints/poddisruptionbudget.api";
import type { PodDisruptionBudget } from "../../api/endpoints/poddisruptionbudget.api";
import { KubeObjectDetailsProps, KubeObjectListLayout } from "../kube-object"; import { KubeObjectDetailsProps, KubeObjectListLayout } from "../kube-object";
import { KubeObjectStatusIcon } from "../kube-object-status-icon"; import { KubeObjectStatusIcon } from "../kube-object-status-icon";
import type { PodDisruptionBudgetsStore } from "./pod-disruption-budgets.store";
import { ApiManager } from "../../api/api-manager";
enum columnId { enum columnId {
name = "name", name = "name",
@ -43,13 +44,17 @@ interface Props extends KubeObjectDetailsProps<PodDisruptionBudget> {
@observer @observer
export class PodDisruptionBudgets extends React.Component<Props> { export class PodDisruptionBudgets extends React.Component<Props> {
private get podDisruptionBudgetsStore() {
return ApiManager.getInstance().getStore<PodDisruptionBudgetsStore>(podDisruptionBudgetApi);
}
render() { render() {
return ( return (
<KubeObjectListLayout <KubeObjectListLayout
isConfigurable isConfigurable
tableId="configuration_distribution_budgets" tableId="configuration_distribution_budgets"
className="PodDisruptionBudgets" className="PodDisruptionBudgets"
store={podDisruptionBudgetsStore} store={this.podDisruptionBudgetsStore}
sortingCallbacks={{ sortingCallbacks={{
[columnId.name]: (pdb: PodDisruptionBudget) => pdb.getName(), [columnId.name]: (pdb: PodDisruptionBudget) => pdb.getName(),
[columnId.namespace]: (pdb: PodDisruptionBudget) => pdb.getNs(), [columnId.namespace]: (pdb: PodDisruptionBudget) => pdb.getNs(),

View File

@ -20,4 +20,5 @@
*/ */
export * from "./resource-quotas"; export * from "./resource-quotas";
export * from "./resource-quotas.store";
export * from "./resource-quota-details"; export * from "./resource-quota-details";

View File

@ -22,12 +22,8 @@
import { KubeObjectStore } from "../../kube-object.store"; import { KubeObjectStore } from "../../kube-object.store";
import { autobind } from "../../utils"; import { autobind } from "../../utils";
import { ResourceQuota, resourceQuotaApi } from "../../api/endpoints/resource-quota.api"; import { ResourceQuota, resourceQuotaApi } from "../../api/endpoints/resource-quota.api";
import { apiManager } from "../../api/api-manager";
@autobind() @autobind()
export class ResourceQuotasStore extends KubeObjectStore<ResourceQuota> { export class ResourceQuotasStore extends KubeObjectStore<ResourceQuota> {
api = resourceQuotaApi; api = resourceQuotaApi;
} }
export const resourceQuotaStore = new ResourceQuotasStore();
apiManager.registerStore(resourceQuotaStore);

View File

@ -25,11 +25,12 @@ import React from "react";
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import type { RouteComponentProps } from "react-router"; import type { RouteComponentProps } from "react-router";
import { KubeObjectListLayout } from "../kube-object"; import { KubeObjectListLayout } from "../kube-object";
import type { ResourceQuota } from "../../api/endpoints/resource-quota.api"; import { ResourceQuota, resourceQuotaApi } from "../../api/endpoints/resource-quota.api";
import { AddQuotaDialog } from "./add-quota-dialog"; import { AddQuotaDialog } from "./add-quota-dialog";
import { resourceQuotaStore } from "./resource-quotas.store"; import type { ResourceQuotasStore } from "./resource-quotas.store";
import { KubeObjectStatusIcon } from "../kube-object-status-icon"; import { KubeObjectStatusIcon } from "../kube-object-status-icon";
import type { ResourceQuotaRouteParams } from "../../../common/routes"; import type { ResourceQuotaRouteParams } from "../../../common/routes";
import { ApiManager } from "../../api/api-manager";
enum columnId { enum columnId {
name = "name", name = "name",
@ -42,13 +43,18 @@ interface Props extends RouteComponentProps<ResourceQuotaRouteParams> {
@observer @observer
export class ResourceQuotas extends React.Component<Props> { export class ResourceQuotas extends React.Component<Props> {
private get resourceQuotaStore() {
return ApiManager.getInstance().getStore<ResourceQuotasStore>(resourceQuotaApi);
}
render() { render() {
return ( return (
<> <>
<KubeObjectListLayout <KubeObjectListLayout
isConfigurable isConfigurable
tableId="configuration_quotas" tableId="configuration_quotas"
className="ResourceQuotas" store={resourceQuotaStore} className="ResourceQuotas"
store={this.resourceQuotaStore}
sortingCallbacks={{ sortingCallbacks={{
[columnId.name]: (item: ResourceQuota) => item.getName(), [columnId.name]: (item: ResourceQuota) => item.getName(),
[columnId.namespace]: (item: ResourceQuota) => item.getNs(), [columnId.namespace]: (item: ResourceQuota) => item.getNs(),

View File

@ -20,4 +20,5 @@
*/ */
export * from "./secrets"; export * from "./secrets";
export * from "./secrets.store";
export * from "./secret-details"; export * from "./secret-details";

View File

@ -31,16 +31,21 @@ import { Button } from "../button";
import { Notifications } from "../notifications"; import { Notifications } from "../notifications";
import { base64 } from "../../utils"; import { base64 } from "../../utils";
import { Icon } from "../icon"; import { Icon } from "../icon";
import { secretsStore } from "./secrets.store";
import type { KubeObjectDetailsProps } from "../kube-object"; import type { KubeObjectDetailsProps } from "../kube-object";
import type { Secret } from "../../api/endpoints"; import { Secret, secretsApi } from "../../api/endpoints";
import { KubeObjectMeta } from "../kube-object/kube-object-meta"; import { KubeObjectMeta } from "../kube-object/kube-object-meta";
import type { SecretsStore } from ".";
import { ApiManager } from "../../api/api-manager";
interface Props extends KubeObjectDetailsProps<Secret> { interface Props extends KubeObjectDetailsProps<Secret> {
} }
@observer @observer
export class SecretDetails extends React.Component<Props> { export class SecretDetails extends React.Component<Props> {
private get secretsStore() {
return ApiManager.getInstance().getStore<SecretsStore>(secretsApi);
}
@observable isSaving = false; @observable isSaving = false;
@observable data: { [name: string]: string } = {}; @observable data: { [name: string]: string } = {};
@observable revealSecret: { [name: string]: boolean } = {}; @observable revealSecret: { [name: string]: boolean } = {};
@ -64,7 +69,7 @@ export class SecretDetails extends React.Component<Props> {
this.isSaving = true; this.isSaving = true;
try { try {
await secretsStore.update(secret, { ...secret, data: this.data }); await this.secretsStore.update(secret, { ...secret, data: this.data });
Notifications.ok("Secret successfully updated."); Notifications.ok("Secret successfully updated.");
} catch (err) { } catch (err) {
Notifications.error(err); Notifications.error(err);

View File

@ -22,12 +22,8 @@
import { KubeObjectStore } from "../../kube-object.store"; import { KubeObjectStore } from "../../kube-object.store";
import { autobind } from "../../utils"; import { autobind } from "../../utils";
import { Secret, secretsApi } from "../../api/endpoints"; import { Secret, secretsApi } from "../../api/endpoints";
import { apiManager } from "../../api/api-manager";
@autobind() @autobind()
export class SecretsStore extends KubeObjectStore<Secret> { export class SecretsStore extends KubeObjectStore<Secret> {
api = secretsApi; api = secretsApi;
} }
export const secretsStore = new SecretsStore();
apiManager.registerStore(secretsStore);

View File

@ -24,13 +24,14 @@ import "./secrets.scss";
import React from "react"; import React from "react";
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import type { RouteComponentProps } from "react-router"; import type { RouteComponentProps } from "react-router";
import type { Secret } from "../../api/endpoints"; import { Secret, secretsApi } from "../../api/endpoints";
import { AddSecretDialog } from "./add-secret-dialog"; import { AddSecretDialog } from "./add-secret-dialog";
import { KubeObjectListLayout } from "../kube-object"; import { KubeObjectListLayout } from "../kube-object";
import { Badge } from "../badge"; import { Badge } from "../badge";
import { secretsStore } from "./secrets.store";
import { KubeObjectStatusIcon } from "../kube-object-status-icon"; import { KubeObjectStatusIcon } from "../kube-object-status-icon";
import type { SecretsRouteParams } from "../../../common/routes"; import type { SecretsRouteParams } from "../../../common/routes";
import type { SecretsStore } from "./secrets.store";
import { ApiManager } from "../../api/api-manager";
enum columnId { enum columnId {
name = "name", name = "name",
@ -46,13 +47,18 @@ interface Props extends RouteComponentProps<SecretsRouteParams> {
@observer @observer
export class Secrets extends React.Component<Props> { export class Secrets extends React.Component<Props> {
private get secretsStore() {
return ApiManager.getInstance().getStore<SecretsStore>(secretsApi);
}
render() { render() {
return ( return (
<> <>
<KubeObjectListLayout <KubeObjectListLayout
isConfigurable isConfigurable
tableId="configuration_secrets" tableId="configuration_secrets"
className="Secrets" store={secretsStore} className="Secrets"
store={this.secretsStore}
sortingCallbacks={{ sortingCallbacks={{
[columnId.name]: (item: Secret) => item.getName(), [columnId.name]: (item: Secret) => item.getName(),
[columnId.namespace]: (item: Secret) => item.getNs(), [columnId.namespace]: (item: Secret) => item.getNs(),

View File

@ -28,17 +28,17 @@ import { namespaceUrlParam } from "../+namespaces/namespace.store";
import { ResourceQuotas } from "../+config-resource-quotas"; import { ResourceQuotas } from "../+config-resource-quotas";
import { PodDisruptionBudgets } from "../+config-pod-disruption-budgets"; import { PodDisruptionBudgets } from "../+config-pod-disruption-budgets";
import { HorizontalPodAutoscalers } from "../+config-autoscalers"; import { HorizontalPodAutoscalers } from "../+config-autoscalers";
import { isAllowedResource } from "../../../common/rbac";
import { LimitRanges } from "../+config-limit-ranges"; import { LimitRanges } from "../+config-limit-ranges";
import * as routes from "../../../common/routes"; import * as routes from "../../../common/routes";
import type { Cluster } from "../../../main/cluster";
@observer @observer
export class Config extends React.Component { export class Config extends React.Component<{ cluster: Cluster }> {
static get tabRoutes(): TabLayoutRoute[] { static tabRoutes(cluster: Cluster): TabLayoutRoute[] {
const query = namespaceUrlParam.toObjectParam(); const query = namespaceUrlParam.toObjectParam();
const tabs: TabLayoutRoute[] = []; const tabs: TabLayoutRoute[] = [];
if (isAllowedResource("configmaps")) { if (cluster.isAllowedResource("configmaps")) {
tabs.push({ tabs.push({
title: "ConfigMaps", title: "ConfigMaps",
component: ConfigMaps, component: ConfigMaps,
@ -47,7 +47,7 @@ export class Config extends React.Component {
}); });
} }
if (isAllowedResource("secrets")) { if (cluster.isAllowedResource("secrets")) {
tabs.push({ tabs.push({
title: "Secrets", title: "Secrets",
component: Secrets, component: Secrets,
@ -56,7 +56,7 @@ export class Config extends React.Component {
}); });
} }
if (isAllowedResource("resourcequotas")) { if (cluster.isAllowedResource("resourcequotas")) {
tabs.push({ tabs.push({
title: "Resource Quotas", title: "Resource Quotas",
component: ResourceQuotas, component: ResourceQuotas,
@ -65,7 +65,7 @@ export class Config extends React.Component {
}); });
} }
if (isAllowedResource("limitranges")) { if (cluster.isAllowedResource("limitranges")) {
tabs.push({ tabs.push({
title: "Limit Ranges", title: "Limit Ranges",
component: LimitRanges, component: LimitRanges,
@ -74,7 +74,7 @@ export class Config extends React.Component {
}); });
} }
if (isAllowedResource("horizontalpodautoscalers")) { if (cluster.isAllowedResource("horizontalpodautoscalers")) {
tabs.push({ tabs.push({
title: "HPA", title: "HPA",
component: HorizontalPodAutoscalers, component: HorizontalPodAutoscalers,
@ -83,7 +83,7 @@ export class Config extends React.Component {
}); });
} }
if (isAllowedResource("poddisruptionbudgets")) { if (cluster.isAllowedResource("poddisruptionbudgets")) {
tabs.push({ tabs.push({
title: "Pod Disruption Budgets", title: "Pod Disruption Budgets",
component: PodDisruptionBudgets, component: PodDisruptionBudgets,
@ -97,7 +97,7 @@ export class Config extends React.Component {
render() { render() {
return ( return (
<TabLayout className="Config" tabs={Config.tabRoutes}/> <TabLayout className="Config" tabs={Config.tabRoutes(this.props.cluster)}/>
); );
} }
} }

View File

@ -27,11 +27,12 @@ import { observer } from "mobx-react";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
import { stopPropagation } from "../../utils"; import { stopPropagation } from "../../utils";
import { KubeObjectListLayout } from "../kube-object"; import { KubeObjectListLayout } from "../kube-object";
import { crdStore } from "./crd.store"; import type { CrdStore } from "./crd.store";
import type { CustomResourceDefinition } from "../../api/endpoints/crd.api"; import { crdApi, CustomResourceDefinition } from "../../api/endpoints/crd.api";
import { Select, SelectOption } from "../select"; import { Select, SelectOption } from "../select";
import { createPageParam } from "../../navigation"; import { createPageParam } from "../../navigation";
import { Icon } from "../icon"; import { Icon } from "../icon";
import { ApiManager } from "../../api/api-manager";
export const crdGroupsUrlParam = createPageParam<string[]>({ export const crdGroupsUrlParam = createPageParam<string[]>({
name: "groups", name: "groups",
@ -54,12 +55,16 @@ export class CrdList extends React.Component {
return crdGroupsUrlParam.get(); return crdGroupsUrlParam.get();
} }
private get crdStore() {
return ApiManager.getInstance().getStore<CrdStore>(crdApi);
}
@computed get items() { @computed get items() {
if (this.selectedGroups.length) { if (this.selectedGroups.length) {
return crdStore.items.filter(item => this.selectedGroups.includes(item.getGroup())); return this.crdStore.items.filter(item => this.selectedGroups.includes(item.getGroup()));
} }
return crdStore.items; // show all by default return this.crdStore.items; // show all by default
} }
toggleSelection(group: string) { toggleSelection(group: string) {
@ -88,7 +93,7 @@ export class CrdList extends React.Component {
tableId="crd" tableId="crd"
className="CrdList" className="CrdList"
isClusterScoped={true} isClusterScoped={true}
store={crdStore} store={this.crdStore}
items={items} items={items}
sortingCallbacks={sortingCallbacks} sortingCallbacks={sortingCallbacks}
searchFilters={Object.values(sortingCallbacks)} searchFilters={Object.values(sortingCallbacks)}
@ -105,7 +110,7 @@ export class CrdList extends React.Component {
<Select <Select
className="group-select" className="group-select"
placeholder={placeholder} placeholder={placeholder}
options={Object.keys(crdStore.groups)} options={Object.keys(this.crdStore.groups)}
onChange={({ value: group }: SelectOption) => this.toggleSelection(group)} onChange={({ value: group }: SelectOption) => this.toggleSelection(group)}
closeMenuOnSelect={false} closeMenuOnSelect={false}
controlShouldRenderValue={false} controlShouldRenderValue={false}

View File

@ -29,11 +29,12 @@ import { cssNames } from "../../utils";
import { Badge } from "../badge"; import { Badge } from "../badge";
import { DrawerItem } from "../drawer"; import { DrawerItem } from "../drawer";
import type { KubeObjectDetailsProps } from "../kube-object"; import type { KubeObjectDetailsProps } from "../kube-object";
import { crdStore } from "./crd.store"; import type { CrdStore } from "./crd.store";
import { KubeObjectMeta } from "../kube-object/kube-object-meta"; import { KubeObjectMeta } from "../kube-object/kube-object-meta";
import { Input } from "../input"; import { Input } from "../input";
import type { AdditionalPrinterColumnsV1, CustomResourceDefinition } from "../../api/endpoints/crd.api"; import { AdditionalPrinterColumnsV1, crdApi, CustomResourceDefinition } from "../../api/endpoints/crd.api";
import { parseJsonPath } from "../../utils/jsonPath"; import { parseJsonPath } from "../../utils/jsonPath";
import { ApiManager } from "../../api/api-manager";
interface Props extends KubeObjectDetailsProps<CustomResourceDefinition> { interface Props extends KubeObjectDetailsProps<CustomResourceDefinition> {
} }
@ -61,7 +62,7 @@ function convertSpecValue(value: any): any {
@observer @observer
export class CrdResourceDetails extends React.Component<Props> { export class CrdResourceDetails extends React.Component<Props> {
@computed get crd() { @computed get crd() {
return crdStore.getByObject(this.props.object); return ApiManager.getInstance().getStore<CrdStore>(crdApi).getByObject(this.props.object);
} }
renderAdditionalColumns(crd: CustomResourceDefinition, columns: AdditionalPrinterColumnsV1[]) { renderAdditionalColumns(crd: CustomResourceDefinition, columns: AdditionalPrinterColumnsV1[]) {

View File

@ -28,11 +28,12 @@ import type { RouteComponentProps } from "react-router";
import { KubeObjectListLayout } from "../kube-object"; import { KubeObjectListLayout } from "../kube-object";
import type { KubeObject } from "../../api/kube-object"; import type { KubeObject } from "../../api/kube-object";
import { autorun, computed } from "mobx"; import { autorun, computed } from "mobx";
import { crdStore } from "./crd.store";
import type { TableSortCallback } from "../table"; import type { TableSortCallback } from "../table";
import { apiManager } from "../../api/api-manager"; import { ApiManager } from "../../api/api-manager";
import { parseJsonPath } from "../../utils/jsonPath"; import { parseJsonPath } from "../../utils/jsonPath";
import type { CRDRouteParams } from "../../../common/routes"; import type { CRDRouteParams } from "../../../common/routes";
import { crdApi } from "../../api/endpoints";
import type { CrdStore } from "./crd.store";
interface Props extends RouteComponentProps<CRDRouteParams> { interface Props extends RouteComponentProps<CRDRouteParams> {
} }
@ -57,16 +58,20 @@ export class CrdResources extends React.Component<Props> {
]); ]);
} }
private get crdStore() {
return ApiManager.getInstance().getStore<CrdStore>(crdApi);
}
@computed get crd() { @computed get crd() {
const { group, name } = this.props.match.params; const { group, name } = this.props.match.params;
return crdStore.getByGroup(group, name); return this.crdStore.getByGroup(group, name);
} }
@computed get store() { @computed get store() {
if (!this.crd) return null; if (!this.crd) return null;
return apiManager.getStore(this.crd.getResourceApiBase()); return ApiManager.getInstance().getStore(this.crd.getResourceApiBase());
} }
render() { render() {

View File

@ -23,28 +23,29 @@ import { computed, reaction } from "mobx";
import { KubeObjectStore } from "../../kube-object.store"; import { KubeObjectStore } from "../../kube-object.store";
import { autobind } from "../../utils"; import { autobind } from "../../utils";
import { crdApi, CustomResourceDefinition } from "../../api/endpoints/crd.api"; import { crdApi, CustomResourceDefinition } from "../../api/endpoints/crd.api";
import { apiManager } from "../../api/api-manager"; import { ApiManager } from "../../api/api-manager";
import { KubeApi } from "../../api/kube-api"; import { KubeApi } from "../../api/kube-api";
import { CRDResourceStore } from "./crd-resource.store"; import { KubeObject } from "../../api/kube-object";
import type { KubeObject } from "../../api/kube-object"; import type { Cluster } from "../../../main/cluster";
function initStore(crd: CustomResourceDefinition) { function initStore(crd: CustomResourceDefinition) {
const manager = ApiManager.getInstance();
const apiBase = crd.getResourceApiBase(); const apiBase = crd.getResourceApiBase();
const kind = crd.getResourceKind(); const kind = crd.getResourceKind();
const isNamespaced = crd.isNamespaced(); const isNamespaced = crd.isNamespaced();
const api = apiManager.getApi(apiBase) || new KubeApi({ apiBase, kind, isNamespaced }); const api = manager.getApi(apiBase) || new KubeApi({ objectConstructor: KubeObject, apiBase, kind, isNamespaced });
if (!apiManager.getStore(api)) { if (!manager.getStore(api)) {
apiManager.registerStore(new CRDResourceStore(api)); manager.registerStore(class CRDResourceStore extends KubeObjectStore<KubeObject> { api = api; });
} }
} }
@autobind() @autobind()
export class CRDStore extends KubeObjectStore<CustomResourceDefinition> { export class CrdStore extends KubeObjectStore<CustomResourceDefinition> {
api = crdApi; api = crdApi;
constructor() { constructor(cluster: Cluster) {
super(); super(cluster);
// auto-init stores for crd-s // auto-init stores for crd-s
reaction(() => this.items.toJS(), items => items.forEach(initStore)); reaction(() => this.items.toJS(), items => items.forEach(initStore));
@ -87,7 +88,3 @@ export class CRDStore extends KubeObjectStore<CustomResourceDefinition> {
)); ));
} }
} }
export const crdStore = new CRDStore();
apiManager.registerStore(crdStore);

View File

@ -26,10 +26,11 @@ import { TabLayout, TabLayoutRoute } from "../layout/tab-layout";
import { CrdList } from "./crd-list"; import { CrdList } from "./crd-list";
import { CrdResources } from "./crd-resources"; import { CrdResources } from "./crd-resources";
import { crdURL, crdDefinitionsRoute, crdResourcesRoute } from "../../../common/routes"; import { crdURL, crdDefinitionsRoute, crdResourcesRoute } from "../../../common/routes";
import type { Cluster } from "../../../main/cluster";
@observer @observer
export class CustomResources extends React.Component { export class CustomResources extends React.Component<{ cluster: Cluster }> {
static get tabRoutes(): TabLayoutRoute[] { static tabRoutes(): TabLayoutRoute[] {
return [ return [
{ {
title: "Definitions", title: "Definitions",

View File

@ -19,6 +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.
*/ */
export * from "./crd.store";
export * from "./crd-list"; export * from "./crd-list";
export * from "./crd-details"; export * from "./crd-details";
export * from "./crd-resources"; export * from "./crd-resources";

View File

@ -30,8 +30,8 @@ import { KubeObjectDetailsProps, getDetailsUrl } from "../kube-object";
import type { KubeEvent } from "../../api/endpoints/events.api"; import type { KubeEvent } from "../../api/endpoints/events.api";
import { KubeObjectMeta } from "../kube-object/kube-object-meta"; import { KubeObjectMeta } from "../kube-object/kube-object-meta";
import { Table, TableCell, TableHead, TableRow } from "../table"; import { Table, TableCell, TableHead, TableRow } from "../table";
import { lookupApiLink } from "../../api/kube-api";
import { LocaleDate } from "../locale-date"; import { LocaleDate } from "../locale-date";
import { ApiManager } from "../../api/api-manager";
interface Props extends KubeObjectDetailsProps<KubeEvent> { interface Props extends KubeObjectDetailsProps<KubeEvent> {
} }
@ -81,7 +81,7 @@ export class EventDetails extends React.Component<Props> {
</TableHead> </TableHead>
<TableRow> <TableRow>
<TableCell> <TableCell>
<Link to={getDetailsUrl(lookupApiLink(involvedObject, event))}> <Link to={getDetailsUrl(ApiManager.getInstance().lookupApiLink(involvedObject, event))}>
{name} {name}
</Link> </Link>
</TableCell> </TableCell>

View File

@ -25,12 +25,16 @@ import { KubeObjectStore } from "../../kube-object.store";
import { autobind } from "../../utils"; import { autobind } from "../../utils";
import { eventApi, KubeEvent } from "../../api/endpoints/events.api"; import { eventApi, KubeEvent } from "../../api/endpoints/events.api";
import type { KubeObject } from "../../api/kube-object"; import type { KubeObject } from "../../api/kube-object";
import { Pod } from "../../api/endpoints/pods.api"; import { Pod, podsApi } from "../../api/endpoints/pods.api";
import { podsStore } from "../+workloads-pods/pods.store"; import type { PodsStore } from "../+workloads-pods";
import { apiManager } from "../../api/api-manager"; import { ApiManager } from "../../api/api-manager";
@autobind() @autobind()
export class EventStore extends KubeObjectStore<KubeEvent> { export class EventStore extends KubeObjectStore<KubeEvent> {
private get podsStore() {
return ApiManager.getInstance().getStore<PodsStore>(podsApi);
}
api = eventApi; api = eventApi;
limit = 1000; limit = 1000;
saveLimit = 50000; saveLimit = 50000;
@ -63,7 +67,7 @@ export class EventStore extends KubeObjectStore<KubeEvent> {
const { kind, uid } = recent.involvedObject; const { kind, uid } = recent.involvedObject;
if (kind == Pod.kind) { // Wipe out running pods if (kind == Pod.kind) { // Wipe out running pods
const pod = podsStore.items.find(pod => pod.getId() == uid); const pod = this.podsStore.items.find(pod => pod.getId() == uid);
if (!pod || (!pod.hasIssues() && pod.spec.priority < 500000)) return undefined; if (!pod || (!pod.hasIssues() && pod.spec.priority < 500000)) return undefined;
} }
@ -78,6 +82,3 @@ export class EventStore extends KubeObjectStore<KubeEvent> {
return this.getWarnings().length; return this.getWarnings().length;
} }
} }
export const eventStore = new EventStore();
apiManager.registerStore(eventStore);

View File

@ -26,17 +26,17 @@ import { computed, observable } from "mobx";
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import { orderBy } from "lodash"; import { orderBy } from "lodash";
import { TabLayout } from "../layout/tab-layout"; import { TabLayout } from "../layout/tab-layout";
import { EventStore, eventStore } from "./event.store"; import type { EventStore } from "./event.store";
import { getDetailsUrl, KubeObjectListLayout, KubeObjectListLayoutProps } from "../kube-object"; import { getDetailsUrl, KubeObjectListLayout, KubeObjectListLayoutProps } from "../kube-object";
import type { KubeEvent } from "../../api/endpoints/events.api"; import { eventApi, KubeEvent } from "../../api/endpoints/events.api";
import type { TableSortCallbacks, TableSortParams, TableProps } from "../table"; import type { TableSortCallbacks, TableSortParams, TableProps } from "../table";
import type { IHeaderPlaceholders } from "../item-object-list"; import type { IHeaderPlaceholders } from "../item-object-list";
import { Tooltip } from "../tooltip"; import { Tooltip } from "../tooltip";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
import { cssNames, IClassName, stopPropagation } from "../../utils"; import { cssNames, IClassName, stopPropagation } from "../../utils";
import { Icon } from "../icon"; import { Icon } from "../icon";
import { lookupApiLink } from "../../api/kube-api";
import { eventsURL } from "../../../common/routes"; import { eventsURL } from "../../../common/routes";
import { ApiManager } from "../../api/api-manager";
enum columnId { enum columnId {
message = "message", message = "message",
@ -48,7 +48,7 @@ enum columnId {
age = "age", age = "age",
} }
interface Props extends Partial<KubeObjectListLayoutProps> { interface Props extends Partial<KubeObjectListLayoutProps<KubeEvent>> {
className?: IClassName; className?: IClassName;
compact?: boolean; compact?: boolean;
compactLimit?: number; compactLimit?: number;
@ -60,6 +60,10 @@ const defaultProps: Partial<Props> = {
@observer @observer
export class Events extends React.Component<Props> { export class Events extends React.Component<Props> {
private get eventStore() {
return ApiManager.getInstance().getStore<EventStore>(eventApi);
}
static defaultProps = defaultProps as object; static defaultProps = defaultProps as object;
@observable sorting: TableSortParams = { @observable sorting: TableSortParams = {
@ -81,12 +85,8 @@ export class Events extends React.Component<Props> {
onSort: params => this.sorting = params, onSort: params => this.sorting = params,
}; };
get store(): EventStore {
return eventStore;
}
@computed get items(): KubeEvent[] { @computed get items(): KubeEvent[] {
const items = this.store.contextItems; const items = this.eventStore.contextItems;
const { sortBy, orderBy: order } = this.sorting; const { sortBy, orderBy: order } = this.sorting;
// we must sort items before passing to "KubeObjectListLayout -> Table" // we must sort items before passing to "KubeObjectListLayout -> Table"
@ -106,7 +106,7 @@ export class Events extends React.Component<Props> {
customizeHeader = ({ info, title }: IHeaderPlaceholders) => { customizeHeader = ({ info, title }: IHeaderPlaceholders) => {
const { compact } = this.props; const { compact } = this.props;
const { store, items, visibleItems } = this; const { eventStore, items, visibleItems } = this;
const allEventsAreShown = visibleItems.length === items.length; const allEventsAreShown = visibleItems.length === items.length;
// handle "compact"-mode header // handle "compact"-mode header
@ -126,14 +126,14 @@ export class Events extends React.Component<Props> {
small small
material="help_outline" material="help_outline"
className="help-icon" className="help-icon"
tooltip={`Limited to ${store.limit}`} tooltip={`Limited to ${eventStore.limit}`}
/> />
</> </>
}; };
}; };
render() { render() {
const { store, visibleItems } = this; const { eventStore, visibleItems } = this;
const { compact, compactLimit, className, ...layoutProps } = this.props; const { compact, compactLimit, className, ...layoutProps } = this.props;
const events = ( const events = (
@ -141,7 +141,7 @@ export class Events extends React.Component<Props> {
{...layoutProps} {...layoutProps}
isConfigurable isConfigurable
tableId="events" tableId="events"
store={store} store={eventStore}
className={cssNames("Events", className, { compact })} className={cssNames("Events", className, { compact })}
renderHeaderTitle="Events" renderHeaderTitle="Events"
customizeHeader={this.customizeHeader} customizeHeader={this.customizeHeader}
@ -184,7 +184,7 @@ export class Events extends React.Component<Props> {
) )
}, },
event.getNs(), event.getNs(),
<Link key="link" to={getDetailsUrl(lookupApiLink(involvedObject, event))} onClick={stopPropagation}> <Link key="link" to={getDetailsUrl(ApiManager.getInstance().lookupApiLink(involvedObject, event))} onClick={stopPropagation}>
{involvedObject.kind}: {involvedObject.name} {involvedObject.kind}: {involvedObject.name}
</Link>, </Link>,
event.getSource(), event.getSource(),

View File

@ -20,4 +20,5 @@
*/ */
export * from "./events"; export * from "./events";
export * from "./event.store";
export * from "./event-details"; export * from "./event-details";

View File

@ -26,7 +26,9 @@ import { observer } from "mobx-react";
import type { KubeObject } from "../../api/kube-object"; import type { KubeObject } from "../../api/kube-object";
import { DrawerItem, DrawerTitle } from "../drawer"; import { DrawerItem, DrawerTitle } from "../drawer";
import { cssNames } from "../../utils"; import { cssNames } from "../../utils";
import { eventStore } from "./event.store"; import type { EventStore } from "./event.store";
import { ApiManager } from "../../api/api-manager";
import { eventApi } from "../../api/endpoints";
export interface KubeEventDetailsProps { export interface KubeEventDetailsProps {
object: KubeObject; object: KubeObject;
@ -34,13 +36,17 @@ export interface KubeEventDetailsProps {
@observer @observer
export class KubeEventDetails extends React.Component<KubeEventDetailsProps> { export class KubeEventDetails extends React.Component<KubeEventDetailsProps> {
private get eventStore() {
return ApiManager.getInstance().getStore<EventStore>(eventApi);
}
async componentDidMount() { async componentDidMount() {
eventStore.reloadAll(); this.eventStore.reloadAll();
} }
render() { render() {
const { object } = this.props; const { object } = this.props;
const events = eventStore.getEventsByObject(object); const events = this.eventStore.getEventsByObject(object);
if (!events.length) { if (!events.length) {
return ( return (

View File

@ -24,9 +24,10 @@ import "./kube-event-icon.scss";
import React from "react"; import React from "react";
import { Icon } from "../icon"; import { Icon } from "../icon";
import type { KubeObject } from "../../api/kube-object"; import type { KubeObject } from "../../api/kube-object";
import { eventStore } from "./event.store"; import type { EventStore } from "./event.store";
import { cssNames } from "../../utils"; import { cssNames } from "../../utils";
import type { KubeEvent } from "../../api/endpoints/events.api"; import { eventApi, KubeEvent } from "../../api/endpoints/events.api";
import { ApiManager } from "../../api/api-manager";
interface Props { interface Props {
object: KubeObject; object: KubeObject;
@ -39,11 +40,15 @@ const defaultProps: Partial<Props> = {
}; };
export class KubeEventIcon extends React.Component<Props> { export class KubeEventIcon extends React.Component<Props> {
private get eventStore() {
return ApiManager.getInstance().getStore<EventStore>(eventApi);
}
static defaultProps = defaultProps as object; static defaultProps = defaultProps as object;
render() { render() {
const { object, showWarningsOnly, filterEvents } = this.props; const { object, showWarningsOnly, filterEvents } = this.props;
const events = eventStore.getEventsByObject(object); const events = this.eventStore.getEventsByObject(object);
let warnings = events.filter(evt => evt.isWarning()); let warnings = events.filter(evt => evt.isWarning());
if (filterEvents) warnings = filterEvents(warnings); if (filterEvents) warnings = filterEvents(warnings);

View File

@ -26,11 +26,12 @@ import { observable } 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";
import { namespaceStore } from "./namespace.store"; import { Namespace, namespacesApi } from "../../api/endpoints";
import type { Namespace } from "../../api/endpoints";
import { Input } from "../input"; import { Input } from "../input";
import { systemName } from "../input/input_validators"; import { systemName } from "../input/input_validators";
import { Notifications } from "../notifications"; import { Notifications } from "../notifications";
import type { NamespaceStore } from "./namespace.store";
import { ApiManager } from "../../api/api-manager";
interface Props extends DialogProps { interface Props extends DialogProps {
onSuccess?(ns: Namespace): void; onSuccess?(ns: Namespace): void;
@ -39,6 +40,10 @@ interface Props extends DialogProps {
@observer @observer
export class AddNamespaceDialog extends React.Component<Props> { export class AddNamespaceDialog extends React.Component<Props> {
private get namespaceStore() {
return ApiManager.getInstance().getStore<NamespaceStore>(namespacesApi);
}
@observable static isOpen = false; @observable static isOpen = false;
@observable namespace = ""; @observable namespace = "";
@ -59,7 +64,7 @@ export class AddNamespaceDialog extends React.Component<Props> {
const { onSuccess, onError } = this.props; const { onSuccess, onError } = this.props;
try { try {
const created = await namespaceStore.create({ name: namespace }); const created = await this.namespaceStore.create({ name: namespace });
onSuccess?.(created); onSuccess?.(created);
AddNamespaceDialog.close(); AddNamespaceDialog.close();

View File

@ -20,5 +20,6 @@
*/ */
export * from "./namespaces"; export * from "./namespaces";
export * from "./namespace.store";
export * from "./namespace-details"; export * from "./namespace-details";
export * from "./add-namespace-dialog"; export * from "./add-namespace-dialog";

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