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:
parent
492fa6f649
commit
33422ce975
17
.eslintrc.js
17
.eslintrc.js
@ -33,12 +33,22 @@ module.exports = {
|
||||
}
|
||||
},
|
||||
overrides: [
|
||||
{
|
||||
files: [
|
||||
"extensions/**/*.ts",
|
||||
"extensions/**/*.tsx",
|
||||
],
|
||||
rules: {
|
||||
"import/no-unresolved": "off", // warns on @k8slens/extensions
|
||||
}
|
||||
},
|
||||
{
|
||||
files: [
|
||||
"**/*.js"
|
||||
],
|
||||
extends: [
|
||||
"eslint:recommended",
|
||||
"plugin:import/recommended",
|
||||
],
|
||||
env: {
|
||||
node: true
|
||||
@ -53,6 +63,7 @@ module.exports = {
|
||||
],
|
||||
rules: {
|
||||
"header/header": [2, "./license-header"],
|
||||
// "import/no-cycle": 2,
|
||||
"indent": ["error", 2, {
|
||||
"SwitchCase": 1,
|
||||
}],
|
||||
@ -93,6 +104,8 @@ module.exports = {
|
||||
parser: "@typescript-eslint/parser",
|
||||
extends: [
|
||||
"plugin:@typescript-eslint/recommended",
|
||||
"plugin:import/recommended",
|
||||
"plugin:import/typescript",
|
||||
],
|
||||
plugins: [
|
||||
"header",
|
||||
@ -104,6 +117,7 @@ module.exports = {
|
||||
},
|
||||
rules: {
|
||||
"header/header": [2, "./license-header"],
|
||||
// "import/no-cycle": 2,
|
||||
"no-invalid-this": "off",
|
||||
"@typescript-eslint/no-invalid-this": ["error"],
|
||||
"@typescript-eslint/explicit-function-return-type": "off",
|
||||
@ -158,6 +172,8 @@ module.exports = {
|
||||
extends: [
|
||||
"plugin:@typescript-eslint/recommended",
|
||||
"plugin:react/recommended",
|
||||
"plugin:import/recommended",
|
||||
"plugin:import/typescript",
|
||||
],
|
||||
parserOptions: {
|
||||
ecmaVersion: 2018,
|
||||
@ -166,6 +182,7 @@ module.exports = {
|
||||
},
|
||||
rules: {
|
||||
"header/header": [2, "./license-header"],
|
||||
// "import/no-cycle": 2,
|
||||
"no-invalid-this": "off",
|
||||
"@typescript-eslint/no-invalid-this": ["error"],
|
||||
"@typescript-eslint/explicit-function-return-type": "off",
|
||||
|
||||
@ -22,7 +22,7 @@
|
||||
import { K8sApi } from "@k8slens/extensions";
|
||||
|
||||
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 warnings = events.filter(evt => evt.isWarning());
|
||||
|
||||
@ -42,7 +42,7 @@ export function resolveStatusForPods(pod: K8sApi.Pod): K8sApi.KubeObjectStatus {
|
||||
if (!pod.hasIssues()) {
|
||||
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 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 {
|
||||
const eventStore = K8sApi.apiManager.getStore(K8sApi.eventApi);
|
||||
const eventStore = K8sApi.ApiManager.getInstance().getStore(K8sApi.eventApi);
|
||||
let events = (eventStore as K8sApi.EventStore).getEventsByObject(cronJob);
|
||||
const warnings = events.filter(evt => evt.isWarning());
|
||||
|
||||
|
||||
@ -315,6 +315,7 @@
|
||||
"electron-notarize": "^0.3.0",
|
||||
"eslint": "^7.7.0",
|
||||
"eslint-plugin-header": "^3.1.1",
|
||||
"eslint-plugin-import": "^2.23.2",
|
||||
"eslint-plugin-react": "^7.21.5",
|
||||
"eslint-plugin-unused-imports": "^1.0.1",
|
||||
"file-loader": "^6.0.0",
|
||||
|
||||
@ -23,9 +23,11 @@ import fs from "fs";
|
||||
import mockFs from "mock-fs";
|
||||
import yaml from "js-yaml";
|
||||
import { Cluster } from "../../main/cluster";
|
||||
import { ClusterStore, getClusterIdFromHost } from "../cluster-store";
|
||||
import { ClusterStore } from "../cluster-store";
|
||||
import { Console } from "console";
|
||||
import { stdout, stderr } from "process";
|
||||
import { embedCustomKubeConfig } from "../utils";
|
||||
import { getClusterIdFromHost } from "../cluster-types";
|
||||
|
||||
console = new Console(stdout, stderr);
|
||||
|
||||
@ -102,7 +104,7 @@ describe("empty config", () => {
|
||||
icon: "data:image/jpeg;base64, iVBORw0KGgoAAAANSUhEUgAAA1wAAAKoCAYAAABjkf5",
|
||||
clusterName: "minikube"
|
||||
},
|
||||
kubeConfigPath: ClusterStore.embedCustomKubeConfig("foo", kubeconfig)
|
||||
kubeConfigPath: embedCustomKubeConfig("foo", kubeconfig)
|
||||
})
|
||||
);
|
||||
});
|
||||
@ -135,7 +137,7 @@ describe("empty config", () => {
|
||||
preferences: {
|
||||
clusterName: "prod"
|
||||
},
|
||||
kubeConfigPath: ClusterStore.embedCustomKubeConfig("prod", kubeconfig)
|
||||
kubeConfigPath: embedCustomKubeConfig("prod", kubeconfig)
|
||||
}),
|
||||
new Cluster({
|
||||
id: "dev",
|
||||
@ -143,7 +145,7 @@ describe("empty config", () => {
|
||||
preferences: {
|
||||
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", () => {
|
||||
const file = ClusterStore.embedCustomKubeConfig("boo", "kubeconfig");
|
||||
const file = embedCustomKubeConfig("boo", "kubeconfig");
|
||||
|
||||
expect(fs.readFileSync(file, "utf8")).toBe("kubeconfig");
|
||||
});
|
||||
|
||||
@ -24,8 +24,7 @@ import Config from "conf";
|
||||
import type { Options as ConfOptions } from "conf/dist/source/types";
|
||||
import { app, ipcMain, ipcRenderer, remote } from "electron";
|
||||
import { IReactionOptions, observable, reaction, runInAction, when } from "mobx";
|
||||
import Singleton from "./utils/singleton";
|
||||
import { getAppVersion } from "./utils/app-version";
|
||||
import { Singleton, getAppVersion } from "./utils";
|
||||
import logger from "../main/logger";
|
||||
import { broadcastMessage, ipcMainOn, ipcRendererOn } from "./ipc";
|
||||
import isEqual from "lodash/isEqual";
|
||||
|
||||
@ -20,13 +20,13 @@
|
||||
*/
|
||||
|
||||
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 { ClusterStore } from "../cluster-store";
|
||||
import { requestMain } from "../ipc";
|
||||
import { productName } from "../vars";
|
||||
import { CatalogCategory, CatalogCategorySpec } from "../catalog";
|
||||
import { addClusterURL } from "../routes";
|
||||
import { storedKubeConfigFolder } from "../utils";
|
||||
import { app } from "electron";
|
||||
|
||||
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({
|
||||
title: "Delete",
|
||||
onlyVisibleForSource: "local",
|
||||
|
||||
@ -19,86 +19,9 @@
|
||||
* 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 clusterSetFrameIdHandler = "cluster:set-frame-id";
|
||||
export const clusterRefreshHandler = "cluster:refresh";
|
||||
export const clusterDisconnectHandler = "cluster:disconnect";
|
||||
export const clusterKubectlApplyAllHandler = "cluster:kubectl-apply-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`;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@ -19,120 +19,24 @@
|
||||
* 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 { unlink } from "fs-extra";
|
||||
import * as fse from "fs-extra";
|
||||
import path from "path";
|
||||
import { action, comparer, computed, observable, reaction, toJS } from "mobx";
|
||||
import { BaseStore } from "./base-store";
|
||||
import { Cluster, ClusterState } from "../main/cluster";
|
||||
import { Cluster } from "../main/cluster";
|
||||
import migrations from "../migrations/cluster-store";
|
||||
import logger from "../main/logger";
|
||||
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 type { ResourceType } from "../renderer/components/cluster-settings/components/cluster-metrics-setting";
|
||||
import { disposer, noop } from "./utils";
|
||||
|
||||
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;
|
||||
};
|
||||
}
|
||||
import { disposer, getCustomKubeConfigPath, noop } from "./utils";
|
||||
import type { ClusterStoreModel, ClusterId, ClusterModel, ClusterState } from "./cluster-types";
|
||||
import { getHostedClusterId } from "./cluster-types";
|
||||
|
||||
export class ClusterStore extends BaseStore<ClusterStoreModel> {
|
||||
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 removedClusters = 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)
|
||||
if (cluster.kubeConfigPath == ClusterStore.getCustomKubeConfigPath(clusterId)) {
|
||||
await unlink(cluster.kubeConfigPath).catch(noop);
|
||||
if (cluster.kubeConfigPath == getCustomKubeConfigPath(clusterId)) {
|
||||
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 {
|
||||
return ClusterStore.getInstance().getById(getHostedClusterId());
|
||||
}
|
||||
|
||||
145
src/common/cluster-types.ts
Normal file
145
src/common/cluster-types.ts
Normal 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);
|
||||
}
|
||||
@ -80,6 +80,6 @@ export function ipcRendererOn(channel: string, listener: (event: Electron.IpcRen
|
||||
return () => ipcRenderer.off(channel, listener);
|
||||
}
|
||||
|
||||
export function bindBroadcastHandlers() {
|
||||
export function initGetSubFramesHandler() {
|
||||
ipcMain.handle(subFramesChannel, getSubFrames);
|
||||
}
|
||||
|
||||
@ -19,8 +19,6 @@
|
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
import { getHostedCluster } from "./cluster-store";
|
||||
|
||||
export type KubeResource =
|
||||
"namespaces" | "nodes" | "events" | "resourcequotas" | "services" | "limitranges" |
|
||||
"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)
|
||||
export const apiResources: KubeApiResource[] = Object.entries(apiResourceRecord)
|
||||
.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;
|
||||
}
|
||||
|
||||
@ -26,14 +26,13 @@ import { readFile } from "fs-extra";
|
||||
import { action, computed, observable, reaction, toJS } from "mobx";
|
||||
import moment from "moment-timezone";
|
||||
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 { kubeConfigDefaultPath, loadConfig } from "./kube-helpers";
|
||||
import { appEventBus } from "./event-bus";
|
||||
import logger from "../main/logger";
|
||||
import path from "path";
|
||||
import os from "os";
|
||||
import { fileNameMigration } from "../migrations/user-store";
|
||||
import { ObservableToggleSet } from "../renderer/utils";
|
||||
|
||||
export interface UserStoreModel {
|
||||
|
||||
@ -32,14 +32,13 @@ export * from "./debouncePromise";
|
||||
export * from "./defineGlobal";
|
||||
export * from "./delay";
|
||||
export * from "./disposer";
|
||||
export * from "./disposer";
|
||||
export * from "./downloadFile";
|
||||
export * from "./escapeRegExp";
|
||||
export * from "./extended-map";
|
||||
export * from "./getRandId";
|
||||
export * from "./openExternal";
|
||||
export * from "./reject-promise";
|
||||
export * from "./saveToAppFiles";
|
||||
export * from "./kubeconfig-files";
|
||||
export * from "./singleton";
|
||||
export * from "./splitArray";
|
||||
export * from "./tar";
|
||||
|
||||
@ -19,17 +19,27 @@
|
||||
* 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 path from "path";
|
||||
import type { KubeConfig } from "@kubernetes/client-node";
|
||||
import { app, remote } from "electron";
|
||||
import { ensureDirSync, writeFileSync } from "fs-extra";
|
||||
import type { WriteFileOptions } from "fs";
|
||||
import path from "path";
|
||||
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 {
|
||||
const absPath = path.resolve((app || remote.app).getPath("userData"), filePath);
|
||||
|
||||
ensureDirSync(path.dirname(absPath));
|
||||
writeFileSync(absPath, contents, options);
|
||||
|
||||
return absPath;
|
||||
export function storedKubeConfigFolder(): string {
|
||||
return path.resolve((app || remote.app).getPath("userData"), "kubeconfigs");
|
||||
}
|
||||
|
||||
export function getCustomKubeConfigPath(clusterId: ClusterId): string {
|
||||
return path.resolve(storedKubeConfigFolder(), clusterId);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
@ -53,4 +53,8 @@ export class CommandRegistry extends BaseRegistry<CommandRegistration> {
|
||||
|
||||
return super.add(filteredItems, extension);
|
||||
}
|
||||
|
||||
getById(commandId: string): CommandRegistration | undefined {
|
||||
return this.getItems().find(({ id }) => id == commandId);
|
||||
}
|
||||
}
|
||||
|
||||
@ -19,9 +19,8 @@
|
||||
* 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 { apiManager } from "../../renderer/api/api-manager";
|
||||
export { ApiManager } from "../../renderer/api/api-manager";
|
||||
export { KubeObjectStore } from "../../renderer/kube-object.store";
|
||||
export { KubeApi, forCluster } from "../../renderer/api/kube-api";
|
||||
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 { LimitRange, limitRangeApi } 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 { Endpoint, endpointApi } from "../../renderer/api/endpoints";
|
||||
export { Ingress, ingressApi, IngressApi } from "../../renderer/api/endpoints";
|
||||
export { NetworkPolicy, networkPolicyApi } 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 { Namespace, namespacesApi } 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";
|
||||
|
||||
// stores
|
||||
export type { EventStore } from "../../renderer/components/+events/event.store";
|
||||
export type { PodsStore } from "../../renderer/components/+workloads-pods/pods.store";
|
||||
export type { NodesStore } from "../../renderer/components/+nodes/nodes.store";
|
||||
export type { DeploymentStore } from "../../renderer/components/+workloads-deployments/deployments.store";
|
||||
export type { DaemonSetStore } from "../../renderer/components/+workloads-daemonsets/daemonsets.store";
|
||||
export type { StatefulSetStore } from "../../renderer/components/+workloads-statefulsets/statefulset.store";
|
||||
export type { JobStore } from "../../renderer/components/+workloads-jobs/job.store";
|
||||
export type { CronJobStore } from "../../renderer/components/+workloads-cronjobs/cronjob.store";
|
||||
export type { ConfigMapsStore } from "../../renderer/components/+config-maps/config-maps.store";
|
||||
export type { SecretsStore } from "../../renderer/components/+config-secrets/secrets.store";
|
||||
export type { ReplicaSetStore } from "../../renderer/components/+workloads-replicasets/replicasets.store";
|
||||
export type { ResourceQuotasStore } from "../../renderer/components/+config-resource-quotas/resource-quotas.store";
|
||||
export type { LimitRangesStore } from "../../renderer/components/+config-limit-ranges/limit-ranges.store";
|
||||
export type { HPAStore } from "../../renderer/components/+config-autoscalers/hpa.store";
|
||||
export type { PodDisruptionBudgetsStore } from "../../renderer/components/+config-pod-disruption-budgets/pod-disruption-budgets.store";
|
||||
export type { ServiceStore } from "../../renderer/components/+network-services/services.store";
|
||||
export type { EndpointStore } from "../../renderer/components/+network-endpoints/endpoints.store";
|
||||
export type { IngressStore } from "../../renderer/components/+network-ingresses/ingress.store";
|
||||
export type { NetworkPolicyStore } from "../../renderer/components/+network-policies/network-policy.store";
|
||||
export type { PersistentVolumesStore } from "../../renderer/components/+storage-volumes/volumes.store";
|
||||
export type { VolumeClaimStore } from "../../renderer/components/+storage-volume-claims/volume-claim.store";
|
||||
export type { StorageClassStore } from "../../renderer/components/+storage-classes/storage-class.store";
|
||||
export type { NamespaceStore } from "../../renderer/components/+namespaces/namespace.store";
|
||||
export type { ServiceAccountsStore } from "../../renderer/components/+user-management-service-accounts/service-accounts.store";
|
||||
export type { RolesStore } from "../../renderer/components/+user-management-roles/roles.store";
|
||||
export type { RoleBindingsStore } from "../../renderer/components/+user-management-roles-bindings/role-bindings.store";
|
||||
export type { CRDStore } from "../../renderer/components/+custom-resources/crd.store";
|
||||
export type { CRDResourceStore } from "../../renderer/components/+custom-resources/crd-resource.store";
|
||||
export type { EventStore } from "../../renderer/components/+events";
|
||||
export type { PodsStore } from "../../renderer/components/+workloads-pods";
|
||||
export type { NodesStore } from "../../renderer/components/+nodes";
|
||||
export type { DeploymentStore } from "../../renderer/components/+workloads-deployments";
|
||||
export type { DaemonSetStore } from "../../renderer/components/+workloads-daemonsets";
|
||||
export type { StatefulSetStore } from "../../renderer/components/+workloads-statefulsets";
|
||||
export type { JobStore } from "../../renderer/components/+workloads-jobs";
|
||||
export type { CronJobStore } from "../../renderer/components/+workloads-cronjobs";
|
||||
export type { ConfigMapsStore } from "../../renderer/components/+config-maps";
|
||||
export type { SecretsStore } from "../../renderer/components/+config-secrets";
|
||||
export type { ReplicaSetStore } from "../../renderer/components/+workloads-replicasets";
|
||||
export type { ResourceQuotasStore } from "../../renderer/components/+config-resource-quotas";
|
||||
export type { LimitRangesStore } from "../../renderer/components/+config-limit-ranges";
|
||||
export type { HpaStore as HPAStore } from "../../renderer/components/+config-autoscalers";
|
||||
export type { PodDisruptionBudgetsStore } from "../../renderer/components/+config-pod-disruption-budgets";
|
||||
export type { ServiceStore } from "../../renderer/components/+network-services";
|
||||
export type { EndpointStore } from "../../renderer/components/+network-endpoints";
|
||||
export type { IngressStore } from "../../renderer/components/+network-ingresses";
|
||||
export type { NetworkPolicyStore } from "../../renderer/components/+network-policies";
|
||||
export type { PersistentVolumesStore } from "../../renderer/components/+storage-volumes";
|
||||
export type { PersistentVolumeClaimStore as VolumeClaimStore } from "../../renderer/components/+storage-volume-claims";
|
||||
export type { StorageClassStore } from "../../renderer/components/+storage-classes";
|
||||
export type { NamespaceStore } from "../../renderer/components/+namespaces";
|
||||
export type { ServiceAccountsStore } from "../../renderer/components/+user-management-service-accounts";
|
||||
export type { RolesStore } from "../../renderer/components/+user-management-roles";
|
||||
export type { RoleBindingsStore } from "../../renderer/components/+user-management-roles-bindings";
|
||||
export type { CrdStore as CRDStore } from "../../renderer/components/+custom-resources";
|
||||
|
||||
@ -24,7 +24,7 @@ import { navigation } from "../../renderer/navigation";
|
||||
|
||||
export type { PageParamInit, PageParam } from "../../renderer/navigation/page-param";
|
||||
export { navigate, isActiveRoute } from "../../renderer/navigation/helpers";
|
||||
export { hideDetails, showDetails, getDetailsUrl } from "../../renderer/components/kube-object/kube-object-details";
|
||||
export { hideDetails, showDetails, getDetailsUrl } from "../../renderer/components/kube-object/params";
|
||||
export type { URLParams } from "../../common/utils/buildUrl";
|
||||
|
||||
// exporting to extensions-api version of helper without `isSystem` flag
|
||||
|
||||
@ -25,14 +25,15 @@ import { watch } from "chokidar";
|
||||
import fs from "fs";
|
||||
import fse from "fs-extra";
|
||||
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 type { KubeConfig } from "@kubernetes/client-node";
|
||||
import { loadConfigFromString, splitConfig, validateKubeConfig } from "../../common/kube-helpers";
|
||||
import { Cluster } from "../cluster";
|
||||
import { catalogEntityFromCluster } from "../cluster-manager";
|
||||
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";
|
||||
|
||||
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.startNewSync(ClusterStore.storedKubeConfigFolder);
|
||||
this.startNewSync(storedKubeConfigFolder());
|
||||
|
||||
for (const filePath of UserStore.getInstance().syncKubeconfigEntries.keys()) {
|
||||
this.startNewSync(filePath);
|
||||
|
||||
@ -21,7 +21,7 @@
|
||||
|
||||
import { BaseClusterDetector } from "./base-cluster-detector";
|
||||
import { createHash } from "crypto";
|
||||
import { ClusterMetadataKey } from "../cluster";
|
||||
import { ClusterMetadataKey } from "../../common/cluster-types";
|
||||
|
||||
export class ClusterIdDetector extends BaseClusterDetector {
|
||||
key = ClusterMetadataKey.CLUSTER_ID;
|
||||
|
||||
@ -20,7 +20,7 @@
|
||||
*/
|
||||
|
||||
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 { BaseClusterDetector, ClusterDetectionResult } from "./base-cluster-detector";
|
||||
import { ClusterIdDetector } from "./cluster-id-detector";
|
||||
|
||||
@ -20,7 +20,7 @@
|
||||
*/
|
||||
|
||||
import { BaseClusterDetector } from "./base-cluster-detector";
|
||||
import { ClusterMetadataKey } from "../cluster";
|
||||
import { ClusterMetadataKey } from "../../common/cluster-types";
|
||||
|
||||
export class DistributionDetector extends BaseClusterDetector {
|
||||
key = ClusterMetadataKey.DISTRIBUTION;
|
||||
@ -60,7 +60,7 @@ export class DistributionDetector extends BaseClusterDetector {
|
||||
if (this.isK0s()) {
|
||||
return { value: "k0s", accuracy: 80};
|
||||
}
|
||||
|
||||
|
||||
if (this.isVMWare()) {
|
||||
return { value: "vmware", accuracy: 90};
|
||||
}
|
||||
@ -179,7 +179,7 @@ export class DistributionDetector extends BaseClusterDetector {
|
||||
protected isK0s() {
|
||||
return this.version.includes("-k0s");
|
||||
}
|
||||
|
||||
|
||||
protected isAlibaba() {
|
||||
return this.version.includes("-aliyun");
|
||||
}
|
||||
|
||||
@ -20,7 +20,7 @@
|
||||
*/
|
||||
|
||||
import { BaseClusterDetector } from "./base-cluster-detector";
|
||||
import { ClusterMetadataKey } from "../cluster";
|
||||
import { ClusterMetadataKey } from "../../common/cluster-types";
|
||||
|
||||
export class LastSeenDetector extends BaseClusterDetector {
|
||||
key = ClusterMetadataKey.LAST_SEEN;
|
||||
|
||||
@ -19,8 +19,8 @@
|
||||
* 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 { ClusterMetadataKey } from "../cluster";
|
||||
|
||||
export class NodesCountDetector extends BaseClusterDetector {
|
||||
key = ClusterMetadataKey.NODES_COUNT;
|
||||
|
||||
@ -19,8 +19,8 @@
|
||||
* 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 { ClusterMetadataKey } from "../cluster";
|
||||
|
||||
export class VersionDetector extends BaseClusterDetector {
|
||||
key = ClusterMetadataKey.VERSION;
|
||||
|
||||
@ -19,11 +19,11 @@
|
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
import "../common/cluster-ipc";
|
||||
import type http from "http";
|
||||
import { ipcMain } from "electron";
|
||||
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 logger from "./logger";
|
||||
import { apiKubePrefix } from "../common/vars";
|
||||
|
||||
@ -20,7 +20,6 @@
|
||||
*/
|
||||
|
||||
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 { broadcastMessage, ClusterListNamespaceForbiddenChannel } from "../common/ipc";
|
||||
import { ContextHandler } from "./context-handler";
|
||||
@ -33,38 +32,8 @@ import logger from "./logger";
|
||||
import { VersionDetector } from "./cluster-detectors/version-detector";
|
||||
import { detectorRegistry } from "./cluster-detectors/detector-registry";
|
||||
import plimit from "p-limit";
|
||||
|
||||
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;
|
||||
}
|
||||
import type { ClusterModel, ClusterState, ClusterId, ClusterPreferences, ClusterMetadata, ClusterPrometheusPreferences, UpdateClusterModel, ClusterRefreshOptions } from "../common/cluster-types";
|
||||
import { ClusterStatus } from "../common/cluster-types";
|
||||
|
||||
/**
|
||||
* Cluster
|
||||
@ -711,4 +680,12 @@ export class Cluster implements ClusterModel, ClusterState {
|
||||
|
||||
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));
|
||||
}
|
||||
}
|
||||
|
||||
@ -20,7 +20,7 @@
|
||||
*/
|
||||
|
||||
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 httpProxy from "http-proxy";
|
||||
import url, { UrlWithStringQuery } from "url";
|
||||
|
||||
@ -26,17 +26,11 @@ import { ClusterManager } from "./cluster-manager";
|
||||
import logger from "./logger";
|
||||
|
||||
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" });
|
||||
windowManager?.hide();
|
||||
clusterManager?.stop();
|
||||
|
||||
WindowManager.getInstance(false)?.hide();
|
||||
ClusterManager.getInstance(false)?.stop();
|
||||
|
||||
logger.info("SERVICE:QUIT");
|
||||
setTimeout(() => {
|
||||
app.exit();
|
||||
|
||||
@ -19,7 +19,7 @@
|
||||
* 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 * as yaml from "js-yaml";
|
||||
import { promiseExec} from "../promise-exec";
|
||||
|
||||
@ -45,8 +45,8 @@ import type { LensExtensionId } from "../extensions/lens-extension";
|
||||
import { FilesystemProvisionerStore } from "./extension-filesystem";
|
||||
import { installDeveloperTools } from "./developer-tools";
|
||||
import { LensProtocolRouterMain } from "./protocol-handler";
|
||||
import { getAppVersion, getAppVersionFromProxyServer } from "../common/utils";
|
||||
import { bindBroadcastHandlers } from "../common/ipc";
|
||||
import { disposer, getAppVersion, getAppVersionFromProxyServer } from "../common/utils";
|
||||
import { initGetSubFramesHandler } from "../common/ipc";
|
||||
import { startUpdateChecking } from "./app-updater";
|
||||
import { IpcRendererNavigationEvents } from "../renderer/navigation/events";
|
||||
import { CatalogPusher } from "./catalog-pusher";
|
||||
@ -54,10 +54,14 @@ import { catalogEntityRegistry } from "../common/catalog";
|
||||
import { HotbarStore } from "../common/hotbar-store";
|
||||
import { HelmRepoManager } from "./helm/helm-repo-manager";
|
||||
import { KubeconfigSyncManager } from "./catalog-sources";
|
||||
import { handleWsUpgrade } from "./proxy/ws-upgrade";
|
||||
import { 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 cleanup = disposer();
|
||||
|
||||
app.setName(appName);
|
||||
|
||||
@ -114,7 +118,8 @@ app.on("ready", async () => {
|
||||
logger.info("🐚 Syncing shell environment");
|
||||
await shellSync();
|
||||
|
||||
bindBroadcastHandlers();
|
||||
initGetSubFramesHandler();
|
||||
initIpcMainHandlers();
|
||||
|
||||
powerMonitor.on("shutdown", () => {
|
||||
app.exit();
|
||||
@ -140,14 +145,15 @@ app.on("ready", async () => {
|
||||
filesystemStore.load(),
|
||||
]);
|
||||
|
||||
const lensProxy = LensProxy.createInstance(handleWsUpgrade);
|
||||
|
||||
ClusterManager.createInstance();
|
||||
KubeconfigSyncManager.createInstance().startSync();
|
||||
|
||||
try {
|
||||
logger.info("🔌 Starting LensProxy");
|
||||
await lensProxy.listen();
|
||||
await LensProxy.createInstance(
|
||||
new Router(),
|
||||
req => ClusterManager.getInstance().getClusterForRequest(req),
|
||||
).listen();
|
||||
} catch (error) {
|
||||
dialog.showErrorBox("Lens Error", `Could not start proxy: ${error?.message || "unknown error"}`);
|
||||
app.exit();
|
||||
@ -156,7 +162,7 @@ app.on("ready", async () => {
|
||||
// test proxy connection
|
||||
try {
|
||||
logger.info("🔎 Testing LensProxy connection ...");
|
||||
const versionFromProxy = await getAppVersionFromProxyServer(lensProxy.port);
|
||||
const versionFromProxy = await getAppVersionFromProxyServer(LensProxy.getInstance().port);
|
||||
|
||||
if (getAppVersion() !== versionFromProxy) {
|
||||
logger.error("Proxy server responded with invalid response");
|
||||
@ -182,6 +188,8 @@ app.on("ready", async () => {
|
||||
logger.info("🖥️ Starting WindowManager");
|
||||
const windowManager = WindowManager.createInstance();
|
||||
|
||||
cleanup.push(initMenu(windowManager), initTray(windowManager));
|
||||
|
||||
installDeveloperTools();
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
cleanup();
|
||||
});
|
||||
|
||||
app.on("open-url", (event, rawUrl) => {
|
||||
|
||||
93
src/main/initializers/ipc-handlers.ts
Normal file
93
src/main/initializers/ipc-handlers.ts
Normal 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`;
|
||||
}
|
||||
});
|
||||
}
|
||||
@ -25,41 +25,40 @@ import spdy from "spdy";
|
||||
import httpProxy from "http-proxy";
|
||||
import url from "url";
|
||||
import { apiPrefix, apiKubePrefix } from "../../common/vars";
|
||||
import { Router } from "../router";
|
||||
import type { Router } from "../router";
|
||||
import type { ContextHandler } from "../context-handler";
|
||||
import logger from "../logger";
|
||||
import { Singleton } from "../../common/utils";
|
||||
import { ClusterManager } from "../cluster-manager";
|
||||
|
||||
type WSUpgradeHandler = (req: http.IncomingMessage, socket: net.Socket, head: Buffer) => void;
|
||||
import type { Cluster } from "../cluster";
|
||||
import { LocalShellSession, NodeShellSession } from "../shell-session";
|
||||
import type * as WebSocket from "ws";
|
||||
import { Server } from "ws";
|
||||
|
||||
export class LensProxy extends Singleton {
|
||||
protected origin: string;
|
||||
protected proxyServer: http.Server;
|
||||
protected router = new Router();
|
||||
protected closed = false;
|
||||
protected retryCounters = new Map<string, number>();
|
||||
|
||||
public port: number;
|
||||
|
||||
constructor(handleWsUpgrade: WSUpgradeHandler) {
|
||||
constructor(protected router: Router, protected getClusterForRequest: (req: http.IncomingMessage) => Cluster) {
|
||||
super();
|
||||
|
||||
const proxy = this.createProxy();
|
||||
|
||||
this.proxyServer = spdy.createServer({
|
||||
spdy: {
|
||||
plain: true,
|
||||
protocols: ["http/1.1", "spdy/3.1"]
|
||||
}
|
||||
}, (req: http.IncomingMessage, res: http.ServerResponse) => {
|
||||
this.handleRequest(proxy, req, res);
|
||||
});
|
||||
|
||||
this.proxyServer
|
||||
this.proxyServer = spdy
|
||||
.createServer({
|
||||
spdy: {
|
||||
plain: true,
|
||||
protocols: ["http/1.1", "spdy/3.1"]
|
||||
}
|
||||
}, (req: http.IncomingMessage, res: http.ServerResponse) => {
|
||||
this.handleRequest(proxy, req, res);
|
||||
})
|
||||
.on("upgrade", (req: http.IncomingMessage, socket: net.Socket, head: Buffer) => {
|
||||
if (req.url.startsWith(`${apiPrefix}?`)) {
|
||||
handleWsUpgrade(req, socket, head);
|
||||
this.handleWsUpgrade(req, socket, head);
|
||||
} else {
|
||||
this.handleProxyUpgrade(proxy, req, socket, head);
|
||||
}
|
||||
@ -103,8 +102,27 @@ export class LensProxy extends Singleton {
|
||||
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) {
|
||||
const cluster = ClusterManager.getInstance().getClusterForRequest(req);
|
||||
const cluster = this.getClusterForRequest(req);
|
||||
|
||||
if (cluster) {
|
||||
const proxyUrl = await cluster.contextHandler.resolveAuthProxyUrl() + req.url.replace(apiKubePrefix, "");
|
||||
@ -205,7 +223,7 @@ export class LensProxy extends Singleton {
|
||||
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)) {
|
||||
delete req.headers.authorization;
|
||||
req.url = req.url.replace(apiKubePrefix, "");
|
||||
@ -213,6 +231,8 @@ export class LensProxy extends Singleton {
|
||||
|
||||
return contextHandler.getApiTarget(isWatchRequest);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
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) {
|
||||
const cluster = ClusterManager.getInstance().getClusterForRequest(req);
|
||||
const cluster = this.getClusterForRequest(req);
|
||||
|
||||
if (cluster) {
|
||||
const proxyTarget = await this.getProxyTarget(req, cluster.contextHandler);
|
||||
|
||||
@ -25,7 +25,7 @@ import { exec } from "child_process";
|
||||
import fs from "fs";
|
||||
import * as yaml from "js-yaml";
|
||||
import path from "path";
|
||||
import * as tempy from "tempy";
|
||||
import tempy from "tempy";
|
||||
import logger from "./logger";
|
||||
import { appEventBus } from "../common/event-bus";
|
||||
import { cloneJsonObject } from "../common/utils";
|
||||
@ -34,19 +34,19 @@ export class ResourceApplier {
|
||||
constructor(protected cluster: Cluster) {
|
||||
}
|
||||
|
||||
async apply(resource: KubernetesObject | any): Promise<string> {
|
||||
async apply(resource: KubernetesObject | any): Promise<any> {
|
||||
resource = this.sanitizeObject(resource);
|
||||
appEventBus.emit({name: "resource", action: "apply"});
|
||||
|
||||
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 kubectlPath = await kubeCtl.getPath();
|
||||
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" });
|
||||
|
||||
fs.writeFileSync(fileName, content);
|
||||
|
||||
@ -22,10 +22,10 @@
|
||||
import _ from "lodash";
|
||||
import type { LensApiRequest } from "../router";
|
||||
import { respondJson } from "../utils/http-responses";
|
||||
import { Cluster, ClusterMetadataKey } from "../cluster";
|
||||
import type { ClusterPrometheusMetadata } from "../../common/cluster-store";
|
||||
import type { Cluster } from "../cluster";
|
||||
import logger from "../logger";
|
||||
import { getMetrics } from "../k8s-request";
|
||||
import { ClusterPrometheusMetadata, ClusterMetadataKey } from "../../common/cluster-types";
|
||||
|
||||
export type IMetricsQuery = string | string[] | {
|
||||
[metricName: string]: string;
|
||||
|
||||
@ -19,14 +19,12 @@
|
||||
* 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 { app, BrowserWindow, dialog, shell, webContents } from "electron";
|
||||
import windowStateKeeper from "electron-window-state";
|
||||
import { appEventBus } from "../common/event-bus";
|
||||
import { ipcMainOn } from "../common/ipc";
|
||||
import { initMenu } from "./menu";
|
||||
import { initTray } from "./tray";
|
||||
import { Singleton } from "../common/utils";
|
||||
import { ClusterFrameInfo, clusterFrameMap } from "../common/cluster-frames";
|
||||
import { IpcRendererNavigationEvents } from "../renderer/navigation/events";
|
||||
@ -38,15 +36,15 @@ export class WindowManager extends Singleton {
|
||||
protected mainWindow: BrowserWindow;
|
||||
protected splashWindow: BrowserWindow;
|
||||
protected windowState: windowStateKeeper.State;
|
||||
protected disposers: Record<string, Function> = {};
|
||||
|
||||
@observable activeClusterId: ClusterId;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.bindEvents();
|
||||
this.initMenu();
|
||||
this.initTray();
|
||||
|
||||
ipcMainOn(IpcRendererNavigationEvents.CLUSTER_VIEW_CURRENT_ID, (event, clusterId: ClusterId) => {
|
||||
this.activeClusterId = clusterId;
|
||||
});
|
||||
}
|
||||
|
||||
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> {
|
||||
if (!this.mainWindow) await this.initMainWindow();
|
||||
this.mainWindow.show();
|
||||
@ -214,9 +197,5 @@ export class WindowManager extends Singleton {
|
||||
this.splashWindow.destroy();
|
||||
this.mainWindow = null;
|
||||
this.splashWindow = null;
|
||||
Object.entries(this.disposers).forEach(([name, dispose]) => {
|
||||
dispose();
|
||||
delete this.disposers[name];
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@ -26,14 +26,15 @@ import path from "path";
|
||||
import { app, remote } from "electron";
|
||||
import { migration } from "../migration-wrapper";
|
||||
import fse from "fs-extra";
|
||||
import { ClusterModel, ClusterStore } from "../../common/cluster-store";
|
||||
import { loadConfig } from "../../common/kube-helpers";
|
||||
import type { ClusterModel } from "../../common/cluster-types";
|
||||
import { getCustomKubeConfigPath, embedCustomKubeConfig } from "../../common/utils";
|
||||
|
||||
export default migration({
|
||||
version: "3.6.0-beta.1",
|
||||
run(store, printLog) {
|
||||
const userDataPath = (app || remote.app).getPath("userData");
|
||||
const kubeConfigBase = ClusterStore.getCustomKubeConfigPath("");
|
||||
const kubeConfigBase = getCustomKubeConfigPath("");
|
||||
const storedClusters: ClusterModel[] = store.get("clusters") || [];
|
||||
|
||||
if (!storedClusters.length) return;
|
||||
@ -47,7 +48,7 @@ export default migration({
|
||||
*/
|
||||
try {
|
||||
// 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();
|
||||
delete cluster.kubeConfig;
|
||||
|
||||
|
||||
@ -22,9 +22,9 @@
|
||||
// Fix embedded kubeconfig paths under snap config
|
||||
|
||||
import { migration } from "../migration-wrapper";
|
||||
import type { ClusterModel } from "../../common/cluster-store";
|
||||
import { getAppVersion } from "../../common/utils/app-version";
|
||||
import fs from "fs";
|
||||
import type { ClusterModel } from "../../common/cluster-types";
|
||||
|
||||
export default migration({
|
||||
version: getAppVersion(), // Run always after upgrade
|
||||
|
||||
@ -19,43 +19,59 @@
|
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
import { ingressStore } from "../../components/+network-ingresses/ingress.store";
|
||||
import { apiManager } from "../api-manager";
|
||||
import { Cluster } from "../../../main/cluster";
|
||||
import { KubeObjectStore } from "../../kube-object.store";
|
||||
import { ApiManager } from "../api-manager";
|
||||
import { KubeApi } from "../kube-api";
|
||||
import { KubeObject } from "../kube-object";
|
||||
|
||||
class TestApi extends KubeApi {
|
||||
|
||||
protected async checkPreferredVersion() {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
describe("ApiManager", () => {
|
||||
beforeEach(() => {
|
||||
ApiManager.createInstance(new Cluster({
|
||||
id: "foobar",
|
||||
kubeConfigPath: "/foobar",
|
||||
}));
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
ApiManager.resetInstance();
|
||||
});
|
||||
|
||||
describe("registerApi", () => {
|
||||
it("re-register store if apiBase changed", async () => {
|
||||
const apiBase = "apis/v1/foo";
|
||||
const fallbackApiBase = "/apis/extensions/v1beta1/foo";
|
||||
const kubeApi = new TestApi({
|
||||
objectConstructor: KubeObject,
|
||||
apiBase,
|
||||
fallbackApiBases: [fallbackApiBase],
|
||||
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
|
||||
Object.defineProperty(ingressStore, "api", { value: kubeApi });
|
||||
apiManager.registerStore(ingressStore, [kubeApi]);
|
||||
ApiManager.getInstance().registerStore(TestStore);
|
||||
|
||||
// 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
|
||||
Object.defineProperty(kubeApi, "apiBase", { value: fallbackApiBase });
|
||||
apiManager.registerApi(fallbackApiBase, kubeApi);
|
||||
ApiManager.getInstance().registerApi(fallbackApiBase, kubeApi);
|
||||
|
||||
// Test that store is returned with new apiBase
|
||||
expect(apiManager.getStore(kubeApi)).toBe(ingressStore);
|
||||
expect(ApiManager.getInstance().getStore(kubeApi)).toBeInstanceOf(TestStore);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@ -20,6 +20,7 @@
|
||||
*/
|
||||
|
||||
import { KubeApi } from "../kube-api";
|
||||
import { KubeObject } from "../kube-object";
|
||||
|
||||
describe("KubeApi", () => {
|
||||
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 fallbackApiBase = "/apis/extensions/v1beta1/ingresses";
|
||||
const kubeApi = new KubeApi({
|
||||
objectConstructor: KubeObject,
|
||||
apiBase,
|
||||
fallbackApiBases: [fallbackApiBase],
|
||||
checkPreferredVersion: true,
|
||||
@ -91,6 +93,7 @@ describe("KubeApi", () => {
|
||||
const apiBase = "apis/networking.k8s.io/v1/ingresses";
|
||||
const fallbackApiBase = "/apis/extensions/v1beta1/ingresses";
|
||||
const kubeApi = new KubeApi({
|
||||
objectConstructor: KubeObject,
|
||||
apiBase,
|
||||
fallbackApiBases: [fallbackApiBase],
|
||||
checkPreferredVersion: true,
|
||||
|
||||
@ -19,16 +19,37 @@
|
||||
* 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 { autobind } from "../utils";
|
||||
import { KubeApi, parseKubeApi } from "./kube-api";
|
||||
import { autobind, Singleton } from "../utils";
|
||||
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()
|
||||
export class ApiManager {
|
||||
export class ApiManager extends Singleton {
|
||||
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)) {
|
||||
if (typeof pathOrCallback === "string") {
|
||||
@ -71,15 +92,56 @@ export class ApiManager {
|
||||
}
|
||||
|
||||
@action
|
||||
registerStore(store: KubeObjectStore, apis: KubeApi[] = [store.api]) {
|
||||
apis.forEach(api => {
|
||||
registerStore<Store extends KubeObjectStore<KubeObject>>(storeConstructor: KubeObjectStoreConstructor<Store>, apis?: KubeApi[]): this {
|
||||
const store = new storeConstructor(this.cluster);
|
||||
|
||||
for (const api of apis ?? [store.api]) {
|
||||
this.stores.set(api.apiBase, store);
|
||||
});
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
getStore<S extends KubeObjectStore>(api: string | KubeApi): S {
|
||||
return this.stores.get(this.resolveApi(api)?.apiBase) as S;
|
||||
getStore<Store extends KubeObjectStore<KubeObject>>(api: string | KubeApi): Store | undefined {
|
||||
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();
|
||||
|
||||
@ -23,7 +23,7 @@ import { IMetrics, IMetricsReqParams, metricsApi } from "./metrics.api";
|
||||
import { KubeObject } from "../kube-object";
|
||||
import { KubeApi } from "../kube-api";
|
||||
|
||||
export class ClusterApi extends KubeApi<Cluster> {
|
||||
export class ClusterApi extends KubeApi<KubeCluster> {
|
||||
static kind = "Cluster";
|
||||
static namespaced = true;
|
||||
|
||||
@ -71,7 +71,7 @@ export interface IClusterMetrics<T = IMetrics> {
|
||||
fsUsage: T;
|
||||
}
|
||||
|
||||
export class Cluster extends KubeObject {
|
||||
export class KubeCluster extends KubeObject {
|
||||
static kind = "Cluster";
|
||||
static apiBase = "/apis/cluster.k8s.io/v1alpha1/clusters";
|
||||
|
||||
@ -117,5 +117,5 @@ export class Cluster extends KubeObject {
|
||||
}
|
||||
|
||||
export const clusterApi = new ClusterApi({
|
||||
objectConstructor: Cluster,
|
||||
objectConstructor: KubeCluster,
|
||||
});
|
||||
|
||||
@ -108,6 +108,6 @@ export class PersistentVolumeClaim extends KubeObject {
|
||||
}
|
||||
}
|
||||
|
||||
export const pvcApi = new PersistentVolumeClaimsApi({
|
||||
export const persistentVolumeClaimsApi = new PersistentVolumeClaimsApi({
|
||||
objectConstructor: PersistentVolumeClaim,
|
||||
});
|
||||
|
||||
@ -65,6 +65,6 @@ export class PodDisruptionBudget extends KubeObject {
|
||||
|
||||
}
|
||||
|
||||
export const pdbApi = new KubeApi({
|
||||
export const podDisruptionBudgetApi = new KubeApi({
|
||||
objectConstructor: PodDisruptionBudget,
|
||||
});
|
||||
|
||||
@ -110,6 +110,6 @@ export class PodSecurityPolicy extends KubeObject {
|
||||
}
|
||||
}
|
||||
|
||||
export const pspApi = new KubeApi({
|
||||
export const podSecurityPolicyApi = new KubeApi({
|
||||
objectConstructor: PodSecurityPolicy,
|
||||
});
|
||||
|
||||
@ -20,35 +20,21 @@
|
||||
*/
|
||||
|
||||
import jsYaml from "js-yaml";
|
||||
import { KubeObject } from "../kube-object";
|
||||
import type { KubeJsonApiData } from "../kube-json-api";
|
||||
import { apiBase } from "../index";
|
||||
import { apiManager } from "../api-manager";
|
||||
|
||||
export const resourceApplierApi = {
|
||||
annotations: [
|
||||
"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") {
|
||||
resource = jsYaml.safeLoad(resource);
|
||||
}
|
||||
|
||||
return apiBase
|
||||
.post<KubeJsonApiData[]>("/stack", { data: resource })
|
||||
.then(data => {
|
||||
const items = data.map(obj => {
|
||||
const api = apiManager.getApiByKind(obj.kind, obj.apiVersion);
|
||||
const result = await apiBase.post<KubeJsonApiData[]>("/stack", { data: resource });
|
||||
|
||||
if (api) {
|
||||
return new api.objectConstructor(obj);
|
||||
} else {
|
||||
return new KubeObject(obj);
|
||||
}
|
||||
});
|
||||
|
||||
return items.length === 1 ? items[0] : items;
|
||||
});
|
||||
return result[0];
|
||||
}
|
||||
};
|
||||
|
||||
@ -21,9 +21,7 @@
|
||||
|
||||
// Parse kube-api path and get api-version, group, etc.
|
||||
|
||||
import type { KubeObject } from "./kube-object";
|
||||
import { splitArray } from "../../common/utils";
|
||||
import { apiManager } from "./api-manager";
|
||||
|
||||
export interface IKubeObjectRef {
|
||||
kind: string;
|
||||
@ -123,55 +121,3 @@ export function parseKubeApi(path: string): IKubeApiParsed {
|
||||
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 });
|
||||
}
|
||||
|
||||
@ -25,9 +25,9 @@ import merge from "lodash/merge";
|
||||
import { stringify } from "querystring";
|
||||
import { apiKubePrefix, isDevelopment, isTestEnv } from "../../common/vars";
|
||||
import logger from "../../main/logger";
|
||||
import { apiManager } from "./api-manager";
|
||||
import { ApiManager, createKubeApiURL } from "./api-manager";
|
||||
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 byline from "byline";
|
||||
import type { IKubeWatchEvent } from "./kube-watch-api";
|
||||
@ -49,18 +49,13 @@ export interface IKubeApiOptions<T extends KubeObject> {
|
||||
*/
|
||||
fallbackApiBases?: string[];
|
||||
|
||||
objectConstructor?: IKubeObjectConstructor<T>;
|
||||
objectConstructor: IKubeObjectConstructor<T>;
|
||||
request?: KubeJsonApi;
|
||||
isNamespaced?: boolean;
|
||||
kind?: string;
|
||||
checkPreferredVersion?: boolean;
|
||||
}
|
||||
|
||||
export interface KubeApiListOptions {
|
||||
namespace?: string;
|
||||
reqInit?: RequestInit;
|
||||
}
|
||||
|
||||
export interface IKubeApiQueryParams {
|
||||
watch?: boolean | number;
|
||||
resourceVersion?: string;
|
||||
@ -152,7 +147,7 @@ export class KubeApi<T extends KubeObject = any> {
|
||||
|
||||
constructor(protected options: IKubeApiOptions<T>) {
|
||||
const {
|
||||
objectConstructor = KubeObject as IKubeObjectConstructor,
|
||||
objectConstructor,
|
||||
request = apiKube,
|
||||
kind = options.objectConstructor?.kind,
|
||||
isNamespaced = options.objectConstructor?.namespaced
|
||||
@ -174,8 +169,7 @@ export class KubeApi<T extends KubeObject = any> {
|
||||
this.objectConstructor = objectConstructor;
|
||||
|
||||
this.checkPreferredVersion();
|
||||
this.parseResponse = this.parseResponse.bind(this);
|
||||
apiManager.registerApi(apiBase, this);
|
||||
ApiManager.getInstance().registerApi(apiBase, this);
|
||||
}
|
||||
|
||||
get apiVersionWithGroup() {
|
||||
@ -264,7 +258,7 @@ export class KubeApi<T extends KubeObject = any> {
|
||||
|
||||
if (this.apiVersionPreferred) {
|
||||
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";
|
||||
|
||||
@ -265,11 +265,11 @@ export class KubeObject implements ItemObject {
|
||||
}
|
||||
|
||||
// use unified resource-applier api for updating all k8s objects
|
||||
async update<T extends KubeObject>(data: Partial<T>) {
|
||||
return resourceApplierApi.update<T>({
|
||||
async updateReturnNew(data: Partial<this>): Promise<this> {
|
||||
return new (this.constructor as any)(resourceApplierApi.update({
|
||||
...this.toPlainObject(),
|
||||
...data,
|
||||
});
|
||||
}));
|
||||
}
|
||||
|
||||
delete(params?: JsonApiParams) {
|
||||
|
||||
@ -23,14 +23,17 @@
|
||||
// API: https://developer.mozilla.org/en-US/docs/Web/API/Streams_API/Using_readable_streams
|
||||
|
||||
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 { comparer, IReactionDisposer, observable, reaction, when } from "mobx";
|
||||
import { autobind, noop } from "../utils";
|
||||
import type { KubeApi } from "./kube-api";
|
||||
import { comparer, IReactionDisposer, reaction } from "mobx";
|
||||
import { autobind, Disposer, noop, Singleton } from "../utils";
|
||||
import type { KubeJsonApiData } from "./kube-json-api";
|
||||
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> {
|
||||
type: "ADDED" | "MODIFIED" | "DELETED" | "ERROR";
|
||||
@ -51,16 +54,12 @@ export interface IKubeWatchLog {
|
||||
}
|
||||
|
||||
@autobind()
|
||||
export class KubeWatchApi {
|
||||
@observable context: ClusterContext = null;
|
||||
|
||||
contextReady = when(() => Boolean(this.context));
|
||||
|
||||
isAllowedApi(api: KubeApi): boolean {
|
||||
return Boolean(this.context?.cluster.isAllowedResource(api.kind));
|
||||
export class KubeWatchApi extends Singleton {
|
||||
constructor(protected cluster: Cluster) {
|
||||
super();
|
||||
}
|
||||
|
||||
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 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 subscribingNamespaces = opts.namespaces ?? this.context?.allNamespaces ?? [];
|
||||
const subscribingNamespaces = opts.namespaces ?? allNamespaces(this.cluster);
|
||||
const unsubscribeList: Function[] = [];
|
||||
let isUnsubscribed = false;
|
||||
|
||||
@ -109,7 +114,7 @@ export class KubeWatchApi {
|
||||
}
|
||||
|
||||
// reload stores only for context namespaces change
|
||||
cancelReloading = reaction(() => this.context?.contextNamespaces, namespaces => {
|
||||
cancelReloading = reaction(() => selectedNamespaces(), namespaces => {
|
||||
preloading?.cancelLoading();
|
||||
unsubscribeList.forEach(unsubscribe => unsubscribe());
|
||||
unsubscribeList.length = 0;
|
||||
@ -149,5 +154,3 @@ export class KubeWatchApi {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const kubeWatchApi = new KubeWatchApi();
|
||||
|
||||
@ -37,7 +37,7 @@ import { ExtensionDiscovery } from "../extensions/extension-discovery";
|
||||
import { ExtensionLoader } from "../extensions/extension-loader";
|
||||
import { ExtensionsStore } from "../extensions/extensions-store";
|
||||
import { FilesystemProvisionerStore } from "../main/extension-filesystem";
|
||||
import { App } from "./components/app";
|
||||
import { ClusterFrame } from "./components/app";
|
||||
import { LensApp } from "./lens-app";
|
||||
import { ThemeStore } from "./theme.store";
|
||||
import { HelmRepoManager } from "../main/helm/helm-repo-manager";
|
||||
@ -127,4 +127,4 @@ export async function bootstrap(App: AppComponent) {
|
||||
}
|
||||
|
||||
// run
|
||||
bootstrap(process.isMainFrame ? LensApp : App);
|
||||
bootstrap(process.isMainFrame ? LensApp : ClusterFrame);
|
||||
|
||||
@ -38,6 +38,7 @@ import { PageLayout } from "../layout/page-layout";
|
||||
import { docsUrl } from "../../../common/vars";
|
||||
import { Input } from "../input";
|
||||
import { catalogURL, preferencesURL } from "../../../common/routes";
|
||||
import { embedCustomKubeConfig } from "../../utils";
|
||||
|
||||
@observer
|
||||
export class AddCluster extends React.Component {
|
||||
@ -107,7 +108,7 @@ export class AddCluster extends React.Component {
|
||||
}).map(context => {
|
||||
const clusterId = uuid();
|
||||
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 {
|
||||
id: clusterId,
|
||||
|
||||
@ -37,14 +37,14 @@ import { Spinner } from "../spinner";
|
||||
import { Table, TableCell, TableHead, TableRow } from "../table";
|
||||
import { AceEditor } from "../ace-editor";
|
||||
import { Button } from "../button";
|
||||
import { releaseStore } from "./release.store";
|
||||
import { ReleaseStore } from "./release.store";
|
||||
import { Notifications } from "../notifications";
|
||||
import { createUpgradeChartTab } from "../dock/upgrade-chart.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 { secretsStore } from "../+config-secrets/secrets.store";
|
||||
import { Secret } from "../../api/endpoints";
|
||||
import type { SecretsStore } from "../+config-secrets/secrets.store";
|
||||
import { Secret, secretsApi } from "../../api/endpoints";
|
||||
import { getDetailsUrl } from "../kube-object";
|
||||
import { Checkbox } from "../checkbox";
|
||||
|
||||
@ -62,6 +62,10 @@ export class ReleaseDetails extends Component<Props> {
|
||||
@observable saving = false;
|
||||
@observable releaseSecret: Secret;
|
||||
|
||||
private get secretsStore() {
|
||||
return ApiManager.getInstance().getStore<SecretsStore>(secretsApi);
|
||||
}
|
||||
|
||||
@disposeOnUnmount
|
||||
releaseSelector = reaction(() => this.props.release, release => {
|
||||
if (!release) return;
|
||||
@ -72,9 +76,9 @@ export class ReleaseDetails extends Component<Props> {
|
||||
);
|
||||
|
||||
@disposeOnUnmount
|
||||
secretWatcher = reaction(() => secretsStore.items.toJS(), () => {
|
||||
secretWatcher = reaction(() => this.secretsStore.items.toJS(), () => {
|
||||
if (!this.props.release) return;
|
||||
const { getReleaseSecret } = releaseStore;
|
||||
const { getReleaseSecret } = ReleaseStore.getInstance();
|
||||
const { release } = this.props;
|
||||
const secret = getReleaseSecret(release);
|
||||
|
||||
@ -115,7 +119,7 @@ export class ReleaseDetails extends Component<Props> {
|
||||
this.saving = true;
|
||||
|
||||
try {
|
||||
await releaseStore.update(name, namespace, data);
|
||||
await ReleaseStore.getInstance().update(name, namespace, data);
|
||||
Notifications.ok(
|
||||
<p>Release <b>{name}</b> successfully updated!</p>
|
||||
);
|
||||
@ -197,7 +201,7 @@ export class ReleaseDetails extends Component<Props> {
|
||||
{items.map(item => {
|
||||
const name = item.getName();
|
||||
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 })) : "";
|
||||
|
||||
return (
|
||||
|
||||
@ -22,12 +22,12 @@
|
||||
import React from "react";
|
||||
import type { HelmRelease } from "../../api/endpoints/helm-releases.api";
|
||||
import { autobind, cssNames } from "../../utils";
|
||||
import { releaseStore } from "./release.store";
|
||||
import { MenuActions, MenuActionsProps } from "../menu/menu-actions";
|
||||
import { MenuItem } from "../menu";
|
||||
import { Icon } from "../icon";
|
||||
import { ReleaseRollbackDialog } from "./release-rollback-dialog";
|
||||
import { createUpgradeChartTab } from "../dock/upgrade-chart.store";
|
||||
import { ReleaseStore } from "./release.store";
|
||||
|
||||
interface Props extends MenuActionsProps {
|
||||
release: HelmRelease;
|
||||
@ -37,7 +37,7 @@ interface Props extends MenuActionsProps {
|
||||
export class HelmReleaseMenu extends React.Component<Props> {
|
||||
@autobind()
|
||||
remove() {
|
||||
return releaseStore.remove(this.props.release);
|
||||
return ReleaseStore.getInstance().remove(this.props.release);
|
||||
}
|
||||
|
||||
@autobind()
|
||||
|
||||
@ -27,7 +27,7 @@ import { observer } from "mobx-react";
|
||||
import { Dialog, DialogProps } from "../dialog";
|
||||
import { Wizard, WizardStep } from "../wizard";
|
||||
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 { Notifications } from "../notifications";
|
||||
import orderBy from "lodash/orderBy";
|
||||
@ -73,7 +73,7 @@ export class ReleaseRollbackDialog extends React.Component<Props> {
|
||||
const revisionNumber = this.revision.revision;
|
||||
|
||||
try {
|
||||
await releaseStore.rollback(this.release.getName(), this.release.getNs(), revisionNumber);
|
||||
await ReleaseStore.getInstance().rollback(this.release.getName(), this.release.getNs(), revisionNumber);
|
||||
this.close();
|
||||
} catch (err) {
|
||||
Notifications.error(err);
|
||||
|
||||
@ -24,24 +24,44 @@ import { action, observable, reaction, when } from "mobx";
|
||||
import { autobind } from "../../utils";
|
||||
import { createRelease, deleteRelease, HelmRelease, IReleaseCreatePayload, IReleaseUpdatePayload, listReleases, rollbackRelease, updateRelease } from "../../api/endpoints/helm-releases.api";
|
||||
import { ItemStore } from "../../item.store";
|
||||
import type { Secret } from "../../api/endpoints";
|
||||
import { secretsStore } from "../+config-secrets/secrets.store";
|
||||
import { namespaceStore } from "../+namespaces/namespace.store";
|
||||
import { Secret, secretsApi } from "../../api/endpoints";
|
||||
import type { SecretsStore } from "../+config-secrets/secrets.store";
|
||||
import { Notifications } from "../notifications";
|
||||
import { ApiManager } from "../../api/api-manager";
|
||||
import { isLoadingFromAllNamespaces, selectedNamespaces } from "../context";
|
||||
import type { Cluster } from "../../../main/cluster";
|
||||
|
||||
@autobind()
|
||||
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>();
|
||||
|
||||
constructor() {
|
||||
private constructor(protected cluster: Cluster) {
|
||||
super();
|
||||
when(() => secretsStore.isLoaded, () => {
|
||||
when(() => this.secretsStore.isLoaded, () => {
|
||||
this.releaseSecrets.replace(this.getReleaseSecrets());
|
||||
});
|
||||
}
|
||||
|
||||
watchAssociatedSecrets(): (() => void) {
|
||||
return reaction(() => secretsStore.items.toJS(), () => {
|
||||
return reaction(() => this.secretsStore.items.toJS(), () => {
|
||||
if (this.isLoading) return;
|
||||
const newSecrets = this.getReleaseSecrets();
|
||||
const amountChanged = newSecrets.length !== this.releaseSecrets.size;
|
||||
@ -57,19 +77,19 @@ export class ReleaseStore extends ItemStore<HelmRelease> {
|
||||
}
|
||||
|
||||
watchSelecteNamespaces(): (() => void) {
|
||||
return reaction(() => namespaceStore.context.contextNamespaces, namespaces => {
|
||||
return reaction(() => selectedNamespaces(), namespaces => {
|
||||
this.loadAll(namespaces);
|
||||
});
|
||||
}
|
||||
|
||||
private getReleaseSecrets() {
|
||||
return secretsStore
|
||||
return this.secretsStore
|
||||
.getByLabel({ owner: "helm" })
|
||||
.map(s => [s.getId(), s] as const);
|
||||
}
|
||||
|
||||
getReleaseSecret(release: HelmRelease) {
|
||||
return secretsStore.getByLabel({
|
||||
return this.secretsStore.getByLabel({
|
||||
owner: "helm",
|
||||
name: release.getName()
|
||||
})
|
||||
@ -100,15 +120,11 @@ export class ReleaseStore extends ItemStore<HelmRelease> {
|
||||
}
|
||||
|
||||
async loadFromContextNamespaces(): Promise<void> {
|
||||
return this.loadAll(namespaceStore.context.contextNamespaces);
|
||||
return this.loadAll(selectedNamespaces());
|
||||
}
|
||||
|
||||
async loadItems(namespaces: string[]) {
|
||||
const isLoadingAll = namespaceStore.context.allNamespaces?.length > 1
|
||||
&& namespaceStore.context.cluster.accessibleNamespaces.length === 0
|
||||
&& namespaceStore.context.allNamespaces.every(ns => namespaces.includes(ns));
|
||||
|
||||
if (isLoadingAll) {
|
||||
if (isLoadingFromAllNamespaces(this.cluster, namespaces)) {
|
||||
return listReleases();
|
||||
}
|
||||
|
||||
@ -149,5 +165,3 @@ export class ReleaseStore extends ItemStore<HelmRelease> {
|
||||
return Promise.all(this.selectedItems.map(this.remove));
|
||||
}
|
||||
}
|
||||
|
||||
export const releaseStore = new ReleaseStore();
|
||||
|
||||
@ -25,15 +25,17 @@ import React, { Component } from "react";
|
||||
import kebabCase from "lodash/kebabCase";
|
||||
import { disposeOnUnmount, observer } from "mobx-react";
|
||||
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 { ReleaseDetails } from "./release-details";
|
||||
import { ReleaseRollbackDialog } from "./release-rollback-dialog";
|
||||
import { navigation } from "../../navigation";
|
||||
import { ItemListLayout } from "../item-object-list/item-list-layout";
|
||||
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 { secretsApi } from "../../api/endpoints";
|
||||
import { ApiManager } from "../../api/api-manager";
|
||||
|
||||
enum columnId {
|
||||
name = "name",
|
||||
@ -51,17 +53,21 @@ interface Props extends RouteComponentProps<ReleaseRouteParams> {
|
||||
|
||||
@observer
|
||||
export class HelmReleases extends Component<Props> {
|
||||
private get secretsStore() {
|
||||
return ApiManager.getInstance().getStore<SecretsStore>(secretsApi);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
disposeOnUnmount(this, [
|
||||
releaseStore.watchAssociatedSecrets(),
|
||||
releaseStore.watchSelecteNamespaces(),
|
||||
ReleaseStore.getInstance().watchAssociatedSecrets(),
|
||||
ReleaseStore.getInstance().watchSelecteNamespaces(),
|
||||
]);
|
||||
}
|
||||
|
||||
get selectedRelease() {
|
||||
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;
|
||||
});
|
||||
}
|
||||
@ -99,8 +105,8 @@ export class HelmReleases extends Component<Props> {
|
||||
isConfigurable
|
||||
tableId="helm_releases"
|
||||
className="HelmReleases"
|
||||
store={releaseStore}
|
||||
dependentStores={[secretsStore]}
|
||||
store={ReleaseStore.getInstance()}
|
||||
dependentStores={[this.secretsStore]}
|
||||
sortingCallbacks={{
|
||||
[columnId.name]: (release: HelmRelease) => release.getName(),
|
||||
[columnId.namespace]: (release: HelmRelease) => release.getNs(),
|
||||
|
||||
@ -26,10 +26,11 @@ import { HelmCharts } from "../+apps-helm-charts";
|
||||
import { HelmReleases } from "../+apps-releases";
|
||||
import { namespaceUrlParam } from "../+namespaces/namespace.store";
|
||||
import { helmChartsURL, helmChartsRoute, releaseURL, releaseRoute } from "../../../common/routes";
|
||||
import type { Cluster } from "../../../main/cluster";
|
||||
|
||||
@observer
|
||||
export class Apps extends React.Component {
|
||||
static get tabRoutes(): TabLayoutRoute[] {
|
||||
export class Apps extends React.Component<{ cluster: Cluster }> {
|
||||
static tabRoutes(): TabLayoutRoute[] {
|
||||
const query = namespaceUrlParam.toObjectParam();
|
||||
|
||||
return [
|
||||
@ -50,7 +51,7 @@ export class Apps extends React.Component {
|
||||
|
||||
render() {
|
||||
return (
|
||||
<TabLayout className="Apps" tabs={Apps.tabRoutes}/>
|
||||
<TabLayout className="Apps" tabs={Apps.tabRoutes()}/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -27,14 +27,15 @@ import { computed } from "mobx";
|
||||
import { Icon } from "../icon";
|
||||
import { SubHeader } from "../layout/sub-header";
|
||||
import { Table, TableCell, TableHead, TableRow } from "../table";
|
||||
import { nodesStore } from "../+nodes/nodes.store";
|
||||
import { eventStore } from "../+events/event.store";
|
||||
import type { NodesStore } from "../+nodes";
|
||||
import type { EventStore } from "../+events/event.store";
|
||||
import { autobind, cssNames, prevDefault } from "../../utils";
|
||||
import type { ItemObject } from "../../item.store";
|
||||
import { Spinner } from "../spinner";
|
||||
import { ThemeStore } from "../../theme.store";
|
||||
import { lookupApiLink } from "../../api/kube-api";
|
||||
import { kubeSelectedUrlParam, showDetails } from "../kube-object";
|
||||
import { ApiManager } from "../../api/api-manager";
|
||||
import { eventApi, nodesApi } from "../../api/endpoints";
|
||||
|
||||
interface Props {
|
||||
className?: string;
|
||||
@ -62,11 +63,19 @@ export class ClusterIssues extends React.Component<Props> {
|
||||
[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() {
|
||||
const warnings: IWarning[] = [];
|
||||
|
||||
// Node bad conditions
|
||||
nodesStore.items.forEach(node => {
|
||||
this.nodesStore.items.forEach(node => {
|
||||
const { kind, selfLink, getId, getName, getAge, getTimeDiffFromNow } = node;
|
||||
|
||||
node.getWarningConditions().forEach(({ message }) => {
|
||||
@ -83,7 +92,7 @@ export class ClusterIssues extends React.Component<Props> {
|
||||
});
|
||||
|
||||
// Warning events for Workloads
|
||||
const events = eventStore.getWarnings();
|
||||
const events = this.eventStore.getWarnings();
|
||||
|
||||
events.forEach(error => {
|
||||
const { message, involvedObject, getAge, getTimeDiffFromNow } = error;
|
||||
@ -96,7 +105,7 @@ export class ClusterIssues extends React.Component<Props> {
|
||||
age: getAge(),
|
||||
message,
|
||||
kind,
|
||||
selfLink: lookupApiLink(involvedObject, error),
|
||||
selfLink: ApiManager.getInstance().lookupApiLink(involvedObject, error),
|
||||
});
|
||||
});
|
||||
|
||||
@ -135,7 +144,7 @@ export class ClusterIssues extends React.Component<Props> {
|
||||
renderContent() {
|
||||
const { warnings } = this;
|
||||
|
||||
if (!eventStore.isLoaded) {
|
||||
if (!this.eventStore.isLoaded) {
|
||||
return (
|
||||
<Spinner center/>
|
||||
);
|
||||
|
||||
@ -23,12 +23,16 @@ import "./cluster-metric-switchers.scss";
|
||||
|
||||
import React from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { nodesStore } from "../+nodes/nodes.store";
|
||||
import type { NodesStore } from "../+nodes";
|
||||
import { cssNames } from "../../utils";
|
||||
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(() => {
|
||||
const nodesStore = ApiManager.getInstance().getStore<NodesStore>(nodesApi);
|
||||
const clusterOverviewStore = ApiManager.getInstance().getStore<ClusterObjectStore>(clusterApi);
|
||||
const { metricType, metricNodeRole, getMetricsValues, metrics } = clusterOverviewStore;
|
||||
const { masterNodes, workerNodes } = nodesStore;
|
||||
const metricsValues = getMetricsValues(metrics);
|
||||
|
||||
@ -24,7 +24,7 @@ import "./cluster-metrics.scss";
|
||||
import React from "react";
|
||||
import { observer } from "mobx-react";
|
||||
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 { bytesToUnits } from "../../utils";
|
||||
import { Spinner } from "../spinner";
|
||||
@ -32,8 +32,11 @@ import { ZebraStripes } from "../chart/zebra-stripes.plugin";
|
||||
import { ClusterNoMetrics } from "./cluster-no-metrics";
|
||||
import { ClusterMetricSwitchers } from "./cluster-metric-switchers";
|
||||
import { getMetricLastPoints } from "../../api/endpoints/metrics.api";
|
||||
import { ApiManager } from "../../api/api-manager";
|
||||
import { clusterApi } from "../../api/endpoints";
|
||||
|
||||
export const ClusterMetrics = observer(() => {
|
||||
const clusterOverviewStore = ApiManager.getInstance().getStore<ClusterObjectStore>(clusterApi);
|
||||
const { metricType, metricNodeRole, getMetricsValues, metricsLoaded, metrics } = clusterOverviewStore;
|
||||
const { memoryCapacity, cpuCapacity } = getMetricLastPoints(clusterOverviewStore.metrics);
|
||||
const metricValues = getMetricsValues(metrics);
|
||||
|
||||
@ -21,11 +21,11 @@
|
||||
|
||||
import { action, observable, reaction, when } from "mobx";
|
||||
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 { IMetricsReqParams, normalizeMetrics } from "../../api/endpoints/metrics.api";
|
||||
import { nodesStore } from "../+nodes/nodes.store";
|
||||
import { apiManager } from "../../api/api-manager";
|
||||
import type { NodesStore } from "../+nodes/nodes.store";
|
||||
import { ApiManager } from "../../api/api-manager";
|
||||
|
||||
export enum MetricType {
|
||||
MEMORY = "memory",
|
||||
@ -43,7 +43,7 @@ export interface ClusterOverviewStorageState {
|
||||
}
|
||||
|
||||
@autobind()
|
||||
export class ClusterOverviewStore extends KubeObjectStore<Cluster> implements ClusterOverviewStorageState {
|
||||
export class ClusterObjectStore extends KubeObjectStore<KubeCluster> implements ClusterOverviewStorageState {
|
||||
api = clusterApi;
|
||||
|
||||
@observable metrics: Partial<IClusterMetrics> = {};
|
||||
@ -70,12 +70,11 @@ export class ClusterOverviewStore extends KubeObjectStore<Cluster> implements Cl
|
||||
this.storage.merge({ metricNodeRole: value });
|
||||
}
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.init();
|
||||
private get nodesStore() {
|
||||
return ApiManager.getInstance().getStore<NodesStore>(nodesApi);
|
||||
}
|
||||
|
||||
private init() {
|
||||
protected init = () => {
|
||||
// TODO: refactor, seems not a correct place to be
|
||||
// auto-refresh metrics on user-action
|
||||
reaction(() => this.metricNodeRole, () => {
|
||||
@ -85,18 +84,18 @@ export class ClusterOverviewStore extends KubeObjectStore<Cluster> implements Cl
|
||||
});
|
||||
|
||||
// check which node type to select
|
||||
reaction(() => nodesStore.items.length, () => {
|
||||
const { masterNodes, workerNodes } = nodesStore;
|
||||
reaction(() => this.nodesStore.items.length, () => {
|
||||
const { masterNodes, workerNodes } = this.nodesStore;
|
||||
|
||||
if (!masterNodes.length) this.metricNodeRole = MetricNodeRole.WORKER;
|
||||
if (!workerNodes.length) this.metricNodeRole = MetricNodeRole.MASTER;
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@action
|
||||
async loadMetrics(params?: IMetricsReqParams) {
|
||||
await when(() => nodesStore.isLoaded);
|
||||
const { masterNodes, workerNodes } = nodesStore;
|
||||
await when(() => this.nodesStore.isLoaded);
|
||||
const { masterNodes, workerNodes } = this.nodesStore;
|
||||
const nodes = this.metricNodeRole === MetricNodeRole.MASTER && masterNodes.length ? masterNodes : workerNodes;
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
export const clusterOverviewStore = new ClusterOverviewStore();
|
||||
apiManager.registerStore(clusterOverviewStore);
|
||||
|
||||
@ -24,24 +24,38 @@ import "./cluster-overview.scss";
|
||||
import React from "react";
|
||||
import { reaction } from "mobx";
|
||||
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 { interval } from "../../utils";
|
||||
import { TabLayout } from "../layout/tab-layout";
|
||||
import { Spinner } from "../spinner";
|
||||
import { ClusterIssues } from "./cluster-issues";
|
||||
import { ClusterMetrics } from "./cluster-metrics";
|
||||
import { clusterOverviewStore } from "./cluster-overview.store";
|
||||
import { ClusterPieCharts } from "./cluster-pie-charts";
|
||||
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
|
||||
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());
|
||||
|
||||
loadMetrics() {
|
||||
getHostedCluster().available && clusterOverviewStore.loadMetrics();
|
||||
getHostedCluster().available && this.clusterObjectStore.loadMetrics();
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
@ -49,7 +63,7 @@ export class ClusterOverview extends React.Component {
|
||||
|
||||
disposeOnUnmount(this, [
|
||||
reaction(
|
||||
() => clusterOverviewStore.metricNodeRole, // Toggle Master/Worker node switcher
|
||||
() => this.clusterObjectStore.metricNodeRole, // Toggle Master/Worker node switcher
|
||||
() => this.metricPoller.restart(true)
|
||||
),
|
||||
]);
|
||||
@ -86,7 +100,7 @@ export class ClusterOverview extends React.Component {
|
||||
}
|
||||
|
||||
render() {
|
||||
const isLoaded = nodesStore.isLoaded && podsStore.isLoaded;
|
||||
const isLoaded = this.nodesStore.isLoaded && this.podsStore.isLoaded;
|
||||
const isMetricsHidden = ClusterStore.getInstance().isMetricHidden(ResourceType.Cluster);
|
||||
|
||||
return (
|
||||
|
||||
@ -23,17 +23,22 @@ import "./cluster-pie-charts.scss";
|
||||
|
||||
import React from "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 { Icon } from "../icon";
|
||||
import { nodesStore } from "../+nodes/nodes.store";
|
||||
import type { NodesStore } from "../+nodes";
|
||||
import { ChartData, PieChart } from "../chart";
|
||||
import { ClusterNoMetrics } from "./cluster-no-metrics";
|
||||
import { bytesToUnits } from "../../utils";
|
||||
import { ThemeStore } from "../../theme.store";
|
||||
import { getMetricLastPoints } from "../../api/endpoints/metrics.api";
|
||||
import { ApiManager } from "../../api/api-manager";
|
||||
import { clusterApi, nodesApi } from "../../api/endpoints";
|
||||
|
||||
export const ClusterPieCharts = observer(() => {
|
||||
const clusterOverviewStore = ApiManager.getInstance().getStore<ClusterObjectStore>(clusterApi);
|
||||
const nodesStore = ApiManager.getInstance().getStore<NodesStore>(nodesApi);
|
||||
|
||||
const renderLimitWarning = () => {
|
||||
return (
|
||||
<div className="node-warning flex gaps align-center">
|
||||
|
||||
@ -30,8 +30,8 @@ import { KubeObjectDetailsProps, getDetailsUrl } from "../kube-object";
|
||||
import { cssNames } from "../../utils";
|
||||
import { HorizontalPodAutoscaler, HpaMetricType, IHpaMetric } from "../../api/endpoints/hpa.api";
|
||||
import { Table, TableCell, TableHead, TableRow } from "../table";
|
||||
import { lookupApiLink } from "../../api/kube-api";
|
||||
import { KubeObjectMeta } from "../kube-object/kube-object-meta";
|
||||
import { ApiManager } from "../../api/api-manager";
|
||||
|
||||
interface Props extends KubeObjectDetailsProps<HorizontalPodAutoscaler> {
|
||||
}
|
||||
@ -54,7 +54,7 @@ export class HpaDetails extends React.Component<Props> {
|
||||
case HpaMetricType.Object:
|
||||
const { target } = metric.object;
|
||||
const { kind, name } = target;
|
||||
const objectUrl = getDetailsUrl(lookupApiLink(target, hpa));
|
||||
const objectUrl = getDetailsUrl(ApiManager.getInstance().lookupApiLink(target, hpa));
|
||||
|
||||
return (
|
||||
<>
|
||||
@ -107,7 +107,7 @@ export class HpaDetails extends React.Component<Props> {
|
||||
|
||||
<DrawerItem name="Reference">
|
||||
{scaleTargetRef && (
|
||||
<Link to={getDetailsUrl(lookupApiLink(scaleTargetRef, hpa))}>
|
||||
<Link to={getDetailsUrl(ApiManager.getInstance().lookupApiLink(scaleTargetRef, hpa))}>
|
||||
{scaleTargetRef.kind}/{scaleTargetRef.name}
|
||||
</Link>
|
||||
)}
|
||||
|
||||
@ -22,12 +22,8 @@
|
||||
import { autobind } from "../../utils";
|
||||
import { KubeObjectStore } from "../../kube-object.store";
|
||||
import { HorizontalPodAutoscaler, hpaApi } from "../../api/endpoints/hpa.api";
|
||||
import { apiManager } from "../../api/api-manager";
|
||||
|
||||
@autobind()
|
||||
export class HPAStore extends KubeObjectStore<HorizontalPodAutoscaler> {
|
||||
export class HpaStore extends KubeObjectStore<HorizontalPodAutoscaler> {
|
||||
api = hpaApi;
|
||||
}
|
||||
|
||||
export const hpaStore = new HPAStore();
|
||||
apiManager.registerStore(hpaStore);
|
||||
|
||||
@ -25,12 +25,13 @@ import React from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import type { RouteComponentProps } from "react-router";
|
||||
import { KubeObjectListLayout } from "../kube-object";
|
||||
import type { HorizontalPodAutoscaler } from "../../api/endpoints/hpa.api";
|
||||
import { hpaStore } from "./hpa.store";
|
||||
import { HorizontalPodAutoscaler, hpaApi } from "../../api/endpoints/hpa.api";
|
||||
import { Badge } from "../badge";
|
||||
import { cssNames } from "../../utils";
|
||||
import { KubeObjectStatusIcon } from "../kube-object-status-icon";
|
||||
import type { IHpaRouteParams } from "../../../common/routes";
|
||||
import { ApiManager } from "../../api/api-manager";
|
||||
import type { HpaStore } from "./hpa.store";
|
||||
|
||||
enum columnId {
|
||||
name = "name",
|
||||
@ -48,6 +49,10 @@ interface Props extends RouteComponentProps<IHpaRouteParams> {
|
||||
|
||||
@observer
|
||||
export class HorizontalPodAutoscalers extends React.Component<Props> {
|
||||
private get hpaStore() {
|
||||
return ApiManager.getInstance().getStore<HpaStore>(hpaApi);
|
||||
}
|
||||
|
||||
getTargets(hpa: HorizontalPodAutoscaler) {
|
||||
const metrics = hpa.getMetrics();
|
||||
const metricsRemainCount = metrics.length - 1;
|
||||
@ -62,7 +67,8 @@ export class HorizontalPodAutoscalers extends React.Component<Props> {
|
||||
<KubeObjectListLayout
|
||||
isConfigurable
|
||||
tableId="configuration_hpa"
|
||||
className="HorizontalPodAutoscalers" store={hpaStore}
|
||||
className="HorizontalPodAutoscalers"
|
||||
store={this.hpaStore}
|
||||
sortingCallbacks={{
|
||||
[columnId.name]: (item: HorizontalPodAutoscaler) => item.getName(),
|
||||
[columnId.namespace]: (item: HorizontalPodAutoscaler) => item.getNs(),
|
||||
|
||||
@ -20,4 +20,5 @@
|
||||
*/
|
||||
|
||||
export * from "./hpa";
|
||||
export * from "./hpa.store";
|
||||
export * from "./hpa-details";
|
||||
|
||||
@ -20,4 +20,5 @@
|
||||
*/
|
||||
|
||||
export * from "./limit-ranges";
|
||||
export * from "./limit-ranges.store";
|
||||
export * from "./limit-range-details";
|
||||
|
||||
@ -21,13 +21,9 @@
|
||||
|
||||
import { autobind } from "../../../common/utils/autobind";
|
||||
import { KubeObjectStore } from "../../kube-object.store";
|
||||
import { apiManager } from "../../api/api-manager";
|
||||
import { LimitRange, limitRangeApi } from "../../api/endpoints/limit-range.api";
|
||||
|
||||
@autobind()
|
||||
export class LimitRangesStore extends KubeObjectStore<LimitRange> {
|
||||
api = limitRangeApi;
|
||||
}
|
||||
|
||||
export const limitRangeStore = new LimitRangesStore();
|
||||
apiManager.registerStore(limitRangeStore);
|
||||
|
||||
@ -24,11 +24,12 @@ import "./limit-ranges.scss";
|
||||
import type { RouteComponentProps } from "react-router";
|
||||
import { observer } from "mobx-react";
|
||||
import { KubeObjectListLayout } from "../kube-object/kube-object-list-layout";
|
||||
import { limitRangeStore } from "./limit-ranges.store";
|
||||
import React from "react";
|
||||
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 { ApiManager } from "../../api/api-manager";
|
||||
import type { LimitRangesStore } from "./limit-ranges.store";
|
||||
|
||||
enum columnId {
|
||||
name = "name",
|
||||
@ -41,13 +42,17 @@ interface Props extends RouteComponentProps<LimitRangeRouteParams> {
|
||||
|
||||
@observer
|
||||
export class LimitRanges extends React.Component<Props> {
|
||||
private get limitRangeStore() {
|
||||
return ApiManager.getInstance().getStore<LimitRangesStore>(limitRangeApi);
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<KubeObjectListLayout
|
||||
isConfigurable
|
||||
tableId="configuration_limitranges"
|
||||
className="LimitRanges"
|
||||
store={limitRangeStore}
|
||||
store={this.limitRangeStore}
|
||||
sortingCallbacks={{
|
||||
[columnId.name]: (item: LimitRange) => item.getName(),
|
||||
[columnId.namespace]: (item: LimitRange) => item.getNs(),
|
||||
|
||||
@ -28,16 +28,21 @@ import { DrawerTitle } from "../drawer";
|
||||
import { Notifications } from "../notifications";
|
||||
import { Input } from "../input";
|
||||
import { Button } from "../button";
|
||||
import { configMapsStore } from "./config-maps.store";
|
||||
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 type { ConfigMapsStore } from ".";
|
||||
import { ApiManager } from "../../api/api-manager";
|
||||
|
||||
interface Props extends KubeObjectDetailsProps<ConfigMap> {
|
||||
}
|
||||
|
||||
@observer
|
||||
export class ConfigMapDetails extends React.Component<Props> {
|
||||
private get configMapsStore() {
|
||||
return ApiManager.getInstance().getStore<ConfigMapsStore>(configMapApi);
|
||||
}
|
||||
|
||||
@observable isSaving = false;
|
||||
@observable data = observable.map();
|
||||
|
||||
@ -58,7 +63,7 @@ export class ConfigMapDetails extends React.Component<Props> {
|
||||
|
||||
try {
|
||||
this.isSaving = true;
|
||||
await configMapsStore.update(configMap, { ...configMap, data: this.data.toJSON() });
|
||||
await this.configMapsStore.update(configMap, { ...configMap, data: this.data.toJSON() });
|
||||
Notifications.ok(
|
||||
<p>
|
||||
<>ConfigMap <b>{configMap.getName()}</b> successfully updated.</>
|
||||
|
||||
@ -22,12 +22,8 @@
|
||||
import { KubeObjectStore } from "../../kube-object.store";
|
||||
import { autobind } from "../../utils";
|
||||
import { ConfigMap, configMapApi } from "../../api/endpoints/configmap.api";
|
||||
import { apiManager } from "../../api/api-manager";
|
||||
|
||||
@autobind()
|
||||
export class ConfigMapsStore extends KubeObjectStore<ConfigMap> {
|
||||
api = configMapApi;
|
||||
}
|
||||
|
||||
export const configMapsStore = new ConfigMapsStore();
|
||||
apiManager.registerStore(configMapsStore);
|
||||
|
||||
@ -24,11 +24,12 @@ import "./config-maps.scss";
|
||||
import React from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import type { RouteComponentProps } from "react-router";
|
||||
import { configMapsStore } from "./config-maps.store";
|
||||
import type { ConfigMap } from "../../api/endpoints/configmap.api";
|
||||
import { ConfigMap, configMapApi } from "../../api/endpoints/configmap.api";
|
||||
import { KubeObjectListLayout } from "../kube-object";
|
||||
import { KubeObjectStatusIcon } from "../kube-object-status-icon";
|
||||
import type { ConfigMapsRouteParams } from "../../../common/routes";
|
||||
import { ApiManager } from "../../api/api-manager";
|
||||
import type { ConfigMapsStore } from "./config-maps.store";
|
||||
|
||||
enum columnId {
|
||||
name = "name",
|
||||
@ -42,12 +43,17 @@ interface Props extends RouteComponentProps<ConfigMapsRouteParams> {
|
||||
|
||||
@observer
|
||||
export class ConfigMaps extends React.Component<Props> {
|
||||
private get configMapsStore() {
|
||||
return ApiManager.getInstance().getStore<ConfigMapsStore>(configMapApi);
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<KubeObjectListLayout
|
||||
isConfigurable
|
||||
tableId="configuration_configmaps"
|
||||
className="ConfigMaps" store={configMapsStore}
|
||||
className="ConfigMaps"
|
||||
store={this.configMapsStore}
|
||||
sortingCallbacks={{
|
||||
[columnId.name]: (item: ConfigMap) => item.getName(),
|
||||
[columnId.namespace]: (item: ConfigMap) => item.getNs(),
|
||||
|
||||
@ -20,4 +20,5 @@
|
||||
*/
|
||||
|
||||
export * from "./config-maps";
|
||||
export * from "./config-maps.store";
|
||||
export * from "./config-map-details";
|
||||
|
||||
@ -20,4 +20,5 @@
|
||||
*/
|
||||
|
||||
export * from "./pod-disruption-budgets";
|
||||
export * from "./pod-disruption-budgets.store";
|
||||
export * from "./pod-disruption-budgets-details";
|
||||
|
||||
@ -21,13 +21,9 @@
|
||||
|
||||
import { KubeObjectStore } from "../../kube-object.store";
|
||||
import { autobind } from "../../utils";
|
||||
import { PodDisruptionBudget, pdbApi } from "../../api/endpoints/poddisruptionbudget.api";
|
||||
import { apiManager } from "../../api/api-manager";
|
||||
import { PodDisruptionBudget, podDisruptionBudgetApi } from "../../api/endpoints/poddisruptionbudget.api";
|
||||
|
||||
@autobind()
|
||||
export class PodDisruptionBudgetsStore extends KubeObjectStore<PodDisruptionBudget> {
|
||||
api = pdbApi;
|
||||
api = podDisruptionBudgetApi;
|
||||
}
|
||||
|
||||
export const podDisruptionBudgetsStore = new PodDisruptionBudgetsStore();
|
||||
apiManager.registerStore(podDisruptionBudgetsStore);
|
||||
|
||||
@ -23,10 +23,11 @@ import "./pod-disruption-budgets.scss";
|
||||
|
||||
import * as React from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { podDisruptionBudgetsStore } from "./pod-disruption-budgets.store";
|
||||
import type { PodDisruptionBudget } from "../../api/endpoints/poddisruptionbudget.api";
|
||||
import { PodDisruptionBudget, podDisruptionBudgetApi } from "../../api/endpoints/poddisruptionbudget.api";
|
||||
import { KubeObjectDetailsProps, KubeObjectListLayout } from "../kube-object";
|
||||
import { KubeObjectStatusIcon } from "../kube-object-status-icon";
|
||||
import type { PodDisruptionBudgetsStore } from "./pod-disruption-budgets.store";
|
||||
import { ApiManager } from "../../api/api-manager";
|
||||
|
||||
enum columnId {
|
||||
name = "name",
|
||||
@ -43,13 +44,17 @@ interface Props extends KubeObjectDetailsProps<PodDisruptionBudget> {
|
||||
|
||||
@observer
|
||||
export class PodDisruptionBudgets extends React.Component<Props> {
|
||||
private get podDisruptionBudgetsStore() {
|
||||
return ApiManager.getInstance().getStore<PodDisruptionBudgetsStore>(podDisruptionBudgetApi);
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<KubeObjectListLayout
|
||||
isConfigurable
|
||||
tableId="configuration_distribution_budgets"
|
||||
className="PodDisruptionBudgets"
|
||||
store={podDisruptionBudgetsStore}
|
||||
store={this.podDisruptionBudgetsStore}
|
||||
sortingCallbacks={{
|
||||
[columnId.name]: (pdb: PodDisruptionBudget) => pdb.getName(),
|
||||
[columnId.namespace]: (pdb: PodDisruptionBudget) => pdb.getNs(),
|
||||
|
||||
@ -20,4 +20,5 @@
|
||||
*/
|
||||
|
||||
export * from "./resource-quotas";
|
||||
export * from "./resource-quotas.store";
|
||||
export * from "./resource-quota-details";
|
||||
|
||||
@ -22,12 +22,8 @@
|
||||
import { KubeObjectStore } from "../../kube-object.store";
|
||||
import { autobind } from "../../utils";
|
||||
import { ResourceQuota, resourceQuotaApi } from "../../api/endpoints/resource-quota.api";
|
||||
import { apiManager } from "../../api/api-manager";
|
||||
|
||||
@autobind()
|
||||
export class ResourceQuotasStore extends KubeObjectStore<ResourceQuota> {
|
||||
api = resourceQuotaApi;
|
||||
}
|
||||
|
||||
export const resourceQuotaStore = new ResourceQuotasStore();
|
||||
apiManager.registerStore(resourceQuotaStore);
|
||||
|
||||
@ -25,11 +25,12 @@ import React from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import type { RouteComponentProps } from "react-router";
|
||||
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 { resourceQuotaStore } from "./resource-quotas.store";
|
||||
import type { ResourceQuotasStore } from "./resource-quotas.store";
|
||||
import { KubeObjectStatusIcon } from "../kube-object-status-icon";
|
||||
import type { ResourceQuotaRouteParams } from "../../../common/routes";
|
||||
import { ApiManager } from "../../api/api-manager";
|
||||
|
||||
enum columnId {
|
||||
name = "name",
|
||||
@ -42,13 +43,18 @@ interface Props extends RouteComponentProps<ResourceQuotaRouteParams> {
|
||||
|
||||
@observer
|
||||
export class ResourceQuotas extends React.Component<Props> {
|
||||
private get resourceQuotaStore() {
|
||||
return ApiManager.getInstance().getStore<ResourceQuotasStore>(resourceQuotaApi);
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<>
|
||||
<KubeObjectListLayout
|
||||
isConfigurable
|
||||
tableId="configuration_quotas"
|
||||
className="ResourceQuotas" store={resourceQuotaStore}
|
||||
className="ResourceQuotas"
|
||||
store={this.resourceQuotaStore}
|
||||
sortingCallbacks={{
|
||||
[columnId.name]: (item: ResourceQuota) => item.getName(),
|
||||
[columnId.namespace]: (item: ResourceQuota) => item.getNs(),
|
||||
|
||||
@ -20,4 +20,5 @@
|
||||
*/
|
||||
|
||||
export * from "./secrets";
|
||||
export * from "./secrets.store";
|
||||
export * from "./secret-details";
|
||||
|
||||
@ -31,16 +31,21 @@ import { Button } from "../button";
|
||||
import { Notifications } from "../notifications";
|
||||
import { base64 } from "../../utils";
|
||||
import { Icon } from "../icon";
|
||||
import { secretsStore } from "./secrets.store";
|
||||
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 type { SecretsStore } from ".";
|
||||
import { ApiManager } from "../../api/api-manager";
|
||||
|
||||
interface Props extends KubeObjectDetailsProps<Secret> {
|
||||
}
|
||||
|
||||
@observer
|
||||
export class SecretDetails extends React.Component<Props> {
|
||||
private get secretsStore() {
|
||||
return ApiManager.getInstance().getStore<SecretsStore>(secretsApi);
|
||||
}
|
||||
|
||||
@observable isSaving = false;
|
||||
@observable data: { [name: string]: string } = {};
|
||||
@observable revealSecret: { [name: string]: boolean } = {};
|
||||
@ -64,7 +69,7 @@ export class SecretDetails extends React.Component<Props> {
|
||||
this.isSaving = true;
|
||||
|
||||
try {
|
||||
await secretsStore.update(secret, { ...secret, data: this.data });
|
||||
await this.secretsStore.update(secret, { ...secret, data: this.data });
|
||||
Notifications.ok("Secret successfully updated.");
|
||||
} catch (err) {
|
||||
Notifications.error(err);
|
||||
|
||||
@ -22,12 +22,8 @@
|
||||
import { KubeObjectStore } from "../../kube-object.store";
|
||||
import { autobind } from "../../utils";
|
||||
import { Secret, secretsApi } from "../../api/endpoints";
|
||||
import { apiManager } from "../../api/api-manager";
|
||||
|
||||
@autobind()
|
||||
export class SecretsStore extends KubeObjectStore<Secret> {
|
||||
api = secretsApi;
|
||||
}
|
||||
|
||||
export const secretsStore = new SecretsStore();
|
||||
apiManager.registerStore(secretsStore);
|
||||
|
||||
@ -24,13 +24,14 @@ import "./secrets.scss";
|
||||
import React from "react";
|
||||
import { observer } from "mobx-react";
|
||||
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 { KubeObjectListLayout } from "../kube-object";
|
||||
import { Badge } from "../badge";
|
||||
import { secretsStore } from "./secrets.store";
|
||||
import { KubeObjectStatusIcon } from "../kube-object-status-icon";
|
||||
import type { SecretsRouteParams } from "../../../common/routes";
|
||||
import type { SecretsStore } from "./secrets.store";
|
||||
import { ApiManager } from "../../api/api-manager";
|
||||
|
||||
enum columnId {
|
||||
name = "name",
|
||||
@ -46,13 +47,18 @@ interface Props extends RouteComponentProps<SecretsRouteParams> {
|
||||
|
||||
@observer
|
||||
export class Secrets extends React.Component<Props> {
|
||||
private get secretsStore() {
|
||||
return ApiManager.getInstance().getStore<SecretsStore>(secretsApi);
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<>
|
||||
<KubeObjectListLayout
|
||||
isConfigurable
|
||||
tableId="configuration_secrets"
|
||||
className="Secrets" store={secretsStore}
|
||||
className="Secrets"
|
||||
store={this.secretsStore}
|
||||
sortingCallbacks={{
|
||||
[columnId.name]: (item: Secret) => item.getName(),
|
||||
[columnId.namespace]: (item: Secret) => item.getNs(),
|
||||
|
||||
@ -28,17 +28,17 @@ import { namespaceUrlParam } from "../+namespaces/namespace.store";
|
||||
import { ResourceQuotas } from "../+config-resource-quotas";
|
||||
import { PodDisruptionBudgets } from "../+config-pod-disruption-budgets";
|
||||
import { HorizontalPodAutoscalers } from "../+config-autoscalers";
|
||||
import { isAllowedResource } from "../../../common/rbac";
|
||||
import { LimitRanges } from "../+config-limit-ranges";
|
||||
import * as routes from "../../../common/routes";
|
||||
import type { Cluster } from "../../../main/cluster";
|
||||
|
||||
@observer
|
||||
export class Config extends React.Component {
|
||||
static get tabRoutes(): TabLayoutRoute[] {
|
||||
export class Config extends React.Component<{ cluster: Cluster }> {
|
||||
static tabRoutes(cluster: Cluster): TabLayoutRoute[] {
|
||||
const query = namespaceUrlParam.toObjectParam();
|
||||
const tabs: TabLayoutRoute[] = [];
|
||||
|
||||
if (isAllowedResource("configmaps")) {
|
||||
if (cluster.isAllowedResource("configmaps")) {
|
||||
tabs.push({
|
||||
title: "ConfigMaps",
|
||||
component: ConfigMaps,
|
||||
@ -47,7 +47,7 @@ export class Config extends React.Component {
|
||||
});
|
||||
}
|
||||
|
||||
if (isAllowedResource("secrets")) {
|
||||
if (cluster.isAllowedResource("secrets")) {
|
||||
tabs.push({
|
||||
title: "Secrets",
|
||||
component: Secrets,
|
||||
@ -56,7 +56,7 @@ export class Config extends React.Component {
|
||||
});
|
||||
}
|
||||
|
||||
if (isAllowedResource("resourcequotas")) {
|
||||
if (cluster.isAllowedResource("resourcequotas")) {
|
||||
tabs.push({
|
||||
title: "Resource Quotas",
|
||||
component: ResourceQuotas,
|
||||
@ -65,7 +65,7 @@ export class Config extends React.Component {
|
||||
});
|
||||
}
|
||||
|
||||
if (isAllowedResource("limitranges")) {
|
||||
if (cluster.isAllowedResource("limitranges")) {
|
||||
tabs.push({
|
||||
title: "Limit Ranges",
|
||||
component: LimitRanges,
|
||||
@ -74,7 +74,7 @@ export class Config extends React.Component {
|
||||
});
|
||||
}
|
||||
|
||||
if (isAllowedResource("horizontalpodautoscalers")) {
|
||||
if (cluster.isAllowedResource("horizontalpodautoscalers")) {
|
||||
tabs.push({
|
||||
title: "HPA",
|
||||
component: HorizontalPodAutoscalers,
|
||||
@ -83,7 +83,7 @@ export class Config extends React.Component {
|
||||
});
|
||||
}
|
||||
|
||||
if (isAllowedResource("poddisruptionbudgets")) {
|
||||
if (cluster.isAllowedResource("poddisruptionbudgets")) {
|
||||
tabs.push({
|
||||
title: "Pod Disruption Budgets",
|
||||
component: PodDisruptionBudgets,
|
||||
@ -97,7 +97,7 @@ export class Config extends React.Component {
|
||||
|
||||
render() {
|
||||
return (
|
||||
<TabLayout className="Config" tabs={Config.tabRoutes}/>
|
||||
<TabLayout className="Config" tabs={Config.tabRoutes(this.props.cluster)}/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -27,11 +27,12 @@ import { observer } from "mobx-react";
|
||||
import { Link } from "react-router-dom";
|
||||
import { stopPropagation } from "../../utils";
|
||||
import { KubeObjectListLayout } from "../kube-object";
|
||||
import { crdStore } from "./crd.store";
|
||||
import type { CustomResourceDefinition } from "../../api/endpoints/crd.api";
|
||||
import type { CrdStore } from "./crd.store";
|
||||
import { crdApi, CustomResourceDefinition } from "../../api/endpoints/crd.api";
|
||||
import { Select, SelectOption } from "../select";
|
||||
import { createPageParam } from "../../navigation";
|
||||
import { Icon } from "../icon";
|
||||
import { ApiManager } from "../../api/api-manager";
|
||||
|
||||
export const crdGroupsUrlParam = createPageParam<string[]>({
|
||||
name: "groups",
|
||||
@ -54,12 +55,16 @@ export class CrdList extends React.Component {
|
||||
return crdGroupsUrlParam.get();
|
||||
}
|
||||
|
||||
private get crdStore() {
|
||||
return ApiManager.getInstance().getStore<CrdStore>(crdApi);
|
||||
}
|
||||
|
||||
@computed get items() {
|
||||
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) {
|
||||
@ -88,7 +93,7 @@ export class CrdList extends React.Component {
|
||||
tableId="crd"
|
||||
className="CrdList"
|
||||
isClusterScoped={true}
|
||||
store={crdStore}
|
||||
store={this.crdStore}
|
||||
items={items}
|
||||
sortingCallbacks={sortingCallbacks}
|
||||
searchFilters={Object.values(sortingCallbacks)}
|
||||
@ -105,7 +110,7 @@ export class CrdList extends React.Component {
|
||||
<Select
|
||||
className="group-select"
|
||||
placeholder={placeholder}
|
||||
options={Object.keys(crdStore.groups)}
|
||||
options={Object.keys(this.crdStore.groups)}
|
||||
onChange={({ value: group }: SelectOption) => this.toggleSelection(group)}
|
||||
closeMenuOnSelect={false}
|
||||
controlShouldRenderValue={false}
|
||||
|
||||
@ -29,11 +29,12 @@ import { cssNames } from "../../utils";
|
||||
import { Badge } from "../badge";
|
||||
import { DrawerItem } from "../drawer";
|
||||
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 { 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 { ApiManager } from "../../api/api-manager";
|
||||
|
||||
interface Props extends KubeObjectDetailsProps<CustomResourceDefinition> {
|
||||
}
|
||||
@ -61,7 +62,7 @@ function convertSpecValue(value: any): any {
|
||||
@observer
|
||||
export class CrdResourceDetails extends React.Component<Props> {
|
||||
@computed get crd() {
|
||||
return crdStore.getByObject(this.props.object);
|
||||
return ApiManager.getInstance().getStore<CrdStore>(crdApi).getByObject(this.props.object);
|
||||
}
|
||||
|
||||
renderAdditionalColumns(crd: CustomResourceDefinition, columns: AdditionalPrinterColumnsV1[]) {
|
||||
|
||||
@ -28,11 +28,12 @@ import type { RouteComponentProps } from "react-router";
|
||||
import { KubeObjectListLayout } from "../kube-object";
|
||||
import type { KubeObject } from "../../api/kube-object";
|
||||
import { autorun, computed } from "mobx";
|
||||
import { crdStore } from "./crd.store";
|
||||
import type { TableSortCallback } from "../table";
|
||||
import { apiManager } from "../../api/api-manager";
|
||||
import { ApiManager } from "../../api/api-manager";
|
||||
import { parseJsonPath } from "../../utils/jsonPath";
|
||||
import type { CRDRouteParams } from "../../../common/routes";
|
||||
import { crdApi } from "../../api/endpoints";
|
||||
import type { CrdStore } from "./crd.store";
|
||||
|
||||
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() {
|
||||
const { group, name } = this.props.match.params;
|
||||
|
||||
return crdStore.getByGroup(group, name);
|
||||
return this.crdStore.getByGroup(group, name);
|
||||
}
|
||||
|
||||
@computed get store() {
|
||||
if (!this.crd) return null;
|
||||
|
||||
return apiManager.getStore(this.crd.getResourceApiBase());
|
||||
return ApiManager.getInstance().getStore(this.crd.getResourceApiBase());
|
||||
}
|
||||
|
||||
render() {
|
||||
|
||||
@ -23,28 +23,29 @@ import { computed, reaction } from "mobx";
|
||||
import { KubeObjectStore } from "../../kube-object.store";
|
||||
import { autobind } from "../../utils";
|
||||
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 { CRDResourceStore } from "./crd-resource.store";
|
||||
import type { KubeObject } from "../../api/kube-object";
|
||||
import { KubeObject } from "../../api/kube-object";
|
||||
import type { Cluster } from "../../../main/cluster";
|
||||
|
||||
function initStore(crd: CustomResourceDefinition) {
|
||||
const manager = ApiManager.getInstance();
|
||||
const apiBase = crd.getResourceApiBase();
|
||||
const kind = crd.getResourceKind();
|
||||
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)) {
|
||||
apiManager.registerStore(new CRDResourceStore(api));
|
||||
if (!manager.getStore(api)) {
|
||||
manager.registerStore(class CRDResourceStore extends KubeObjectStore<KubeObject> { api = api; });
|
||||
}
|
||||
}
|
||||
|
||||
@autobind()
|
||||
export class CRDStore extends KubeObjectStore<CustomResourceDefinition> {
|
||||
export class CrdStore extends KubeObjectStore<CustomResourceDefinition> {
|
||||
api = crdApi;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
constructor(cluster: Cluster) {
|
||||
super(cluster);
|
||||
|
||||
// auto-init stores for crd-s
|
||||
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);
|
||||
|
||||
@ -26,10 +26,11 @@ import { TabLayout, TabLayoutRoute } from "../layout/tab-layout";
|
||||
import { CrdList } from "./crd-list";
|
||||
import { CrdResources } from "./crd-resources";
|
||||
import { crdURL, crdDefinitionsRoute, crdResourcesRoute } from "../../../common/routes";
|
||||
import type { Cluster } from "../../../main/cluster";
|
||||
|
||||
@observer
|
||||
export class CustomResources extends React.Component {
|
||||
static get tabRoutes(): TabLayoutRoute[] {
|
||||
export class CustomResources extends React.Component<{ cluster: Cluster }> {
|
||||
static tabRoutes(): TabLayoutRoute[] {
|
||||
return [
|
||||
{
|
||||
title: "Definitions",
|
||||
|
||||
@ -19,6 +19,7 @@
|
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
export * from "./crd.store";
|
||||
export * from "./crd-list";
|
||||
export * from "./crd-details";
|
||||
export * from "./crd-resources";
|
||||
|
||||
@ -30,8 +30,8 @@ import { KubeObjectDetailsProps, getDetailsUrl } from "../kube-object";
|
||||
import type { KubeEvent } from "../../api/endpoints/events.api";
|
||||
import { KubeObjectMeta } from "../kube-object/kube-object-meta";
|
||||
import { Table, TableCell, TableHead, TableRow } from "../table";
|
||||
import { lookupApiLink } from "../../api/kube-api";
|
||||
import { LocaleDate } from "../locale-date";
|
||||
import { ApiManager } from "../../api/api-manager";
|
||||
|
||||
interface Props extends KubeObjectDetailsProps<KubeEvent> {
|
||||
}
|
||||
@ -81,7 +81,7 @@ export class EventDetails extends React.Component<Props> {
|
||||
</TableHead>
|
||||
<TableRow>
|
||||
<TableCell>
|
||||
<Link to={getDetailsUrl(lookupApiLink(involvedObject, event))}>
|
||||
<Link to={getDetailsUrl(ApiManager.getInstance().lookupApiLink(involvedObject, event))}>
|
||||
{name}
|
||||
</Link>
|
||||
</TableCell>
|
||||
|
||||
@ -25,12 +25,16 @@ import { KubeObjectStore } from "../../kube-object.store";
|
||||
import { autobind } from "../../utils";
|
||||
import { eventApi, KubeEvent } from "../../api/endpoints/events.api";
|
||||
import type { KubeObject } from "../../api/kube-object";
|
||||
import { Pod } from "../../api/endpoints/pods.api";
|
||||
import { podsStore } from "../+workloads-pods/pods.store";
|
||||
import { apiManager } from "../../api/api-manager";
|
||||
import { Pod, podsApi } from "../../api/endpoints/pods.api";
|
||||
import type { PodsStore } from "../+workloads-pods";
|
||||
import { ApiManager } from "../../api/api-manager";
|
||||
|
||||
@autobind()
|
||||
export class EventStore extends KubeObjectStore<KubeEvent> {
|
||||
private get podsStore() {
|
||||
return ApiManager.getInstance().getStore<PodsStore>(podsApi);
|
||||
}
|
||||
|
||||
api = eventApi;
|
||||
limit = 1000;
|
||||
saveLimit = 50000;
|
||||
@ -63,7 +67,7 @@ export class EventStore extends KubeObjectStore<KubeEvent> {
|
||||
const { kind, uid } = recent.involvedObject;
|
||||
|
||||
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;
|
||||
}
|
||||
@ -78,6 +82,3 @@ export class EventStore extends KubeObjectStore<KubeEvent> {
|
||||
return this.getWarnings().length;
|
||||
}
|
||||
}
|
||||
|
||||
export const eventStore = new EventStore();
|
||||
apiManager.registerStore(eventStore);
|
||||
|
||||
@ -26,17 +26,17 @@ import { computed, observable } from "mobx";
|
||||
import { observer } from "mobx-react";
|
||||
import { orderBy } from "lodash";
|
||||
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 type { KubeEvent } from "../../api/endpoints/events.api";
|
||||
import { eventApi, KubeEvent } from "../../api/endpoints/events.api";
|
||||
import type { TableSortCallbacks, TableSortParams, TableProps } from "../table";
|
||||
import type { IHeaderPlaceholders } from "../item-object-list";
|
||||
import { Tooltip } from "../tooltip";
|
||||
import { Link } from "react-router-dom";
|
||||
import { cssNames, IClassName, stopPropagation } from "../../utils";
|
||||
import { Icon } from "../icon";
|
||||
import { lookupApiLink } from "../../api/kube-api";
|
||||
import { eventsURL } from "../../../common/routes";
|
||||
import { ApiManager } from "../../api/api-manager";
|
||||
|
||||
enum columnId {
|
||||
message = "message",
|
||||
@ -48,7 +48,7 @@ enum columnId {
|
||||
age = "age",
|
||||
}
|
||||
|
||||
interface Props extends Partial<KubeObjectListLayoutProps> {
|
||||
interface Props extends Partial<KubeObjectListLayoutProps<KubeEvent>> {
|
||||
className?: IClassName;
|
||||
compact?: boolean;
|
||||
compactLimit?: number;
|
||||
@ -60,6 +60,10 @@ const defaultProps: Partial<Props> = {
|
||||
|
||||
@observer
|
||||
export class Events extends React.Component<Props> {
|
||||
private get eventStore() {
|
||||
return ApiManager.getInstance().getStore<EventStore>(eventApi);
|
||||
}
|
||||
|
||||
static defaultProps = defaultProps as object;
|
||||
|
||||
@observable sorting: TableSortParams = {
|
||||
@ -81,12 +85,8 @@ export class Events extends React.Component<Props> {
|
||||
onSort: params => this.sorting = params,
|
||||
};
|
||||
|
||||
get store(): EventStore {
|
||||
return eventStore;
|
||||
}
|
||||
|
||||
@computed get items(): KubeEvent[] {
|
||||
const items = this.store.contextItems;
|
||||
const items = this.eventStore.contextItems;
|
||||
const { sortBy, orderBy: order } = this.sorting;
|
||||
|
||||
// we must sort items before passing to "KubeObjectListLayout -> Table"
|
||||
@ -106,7 +106,7 @@ export class Events extends React.Component<Props> {
|
||||
|
||||
customizeHeader = ({ info, title }: IHeaderPlaceholders) => {
|
||||
const { compact } = this.props;
|
||||
const { store, items, visibleItems } = this;
|
||||
const { eventStore, items, visibleItems } = this;
|
||||
const allEventsAreShown = visibleItems.length === items.length;
|
||||
|
||||
// handle "compact"-mode header
|
||||
@ -126,14 +126,14 @@ export class Events extends React.Component<Props> {
|
||||
small
|
||||
material="help_outline"
|
||||
className="help-icon"
|
||||
tooltip={`Limited to ${store.limit}`}
|
||||
tooltip={`Limited to ${eventStore.limit}`}
|
||||
/>
|
||||
</>
|
||||
};
|
||||
};
|
||||
|
||||
render() {
|
||||
const { store, visibleItems } = this;
|
||||
const { eventStore, visibleItems } = this;
|
||||
const { compact, compactLimit, className, ...layoutProps } = this.props;
|
||||
|
||||
const events = (
|
||||
@ -141,7 +141,7 @@ export class Events extends React.Component<Props> {
|
||||
{...layoutProps}
|
||||
isConfigurable
|
||||
tableId="events"
|
||||
store={store}
|
||||
store={eventStore}
|
||||
className={cssNames("Events", className, { compact })}
|
||||
renderHeaderTitle="Events"
|
||||
customizeHeader={this.customizeHeader}
|
||||
@ -184,7 +184,7 @@ export class Events extends React.Component<Props> {
|
||||
)
|
||||
},
|
||||
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}
|
||||
</Link>,
|
||||
event.getSource(),
|
||||
|
||||
@ -20,4 +20,5 @@
|
||||
*/
|
||||
|
||||
export * from "./events";
|
||||
export * from "./event.store";
|
||||
export * from "./event-details";
|
||||
|
||||
@ -26,7 +26,9 @@ import { observer } from "mobx-react";
|
||||
import type { KubeObject } from "../../api/kube-object";
|
||||
import { DrawerItem, DrawerTitle } from "../drawer";
|
||||
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 {
|
||||
object: KubeObject;
|
||||
@ -34,13 +36,17 @@ export interface KubeEventDetailsProps {
|
||||
|
||||
@observer
|
||||
export class KubeEventDetails extends React.Component<KubeEventDetailsProps> {
|
||||
private get eventStore() {
|
||||
return ApiManager.getInstance().getStore<EventStore>(eventApi);
|
||||
}
|
||||
|
||||
async componentDidMount() {
|
||||
eventStore.reloadAll();
|
||||
this.eventStore.reloadAll();
|
||||
}
|
||||
|
||||
render() {
|
||||
const { object } = this.props;
|
||||
const events = eventStore.getEventsByObject(object);
|
||||
const events = this.eventStore.getEventsByObject(object);
|
||||
|
||||
if (!events.length) {
|
||||
return (
|
||||
|
||||
@ -24,9 +24,10 @@ import "./kube-event-icon.scss";
|
||||
import React from "react";
|
||||
import { Icon } from "../icon";
|
||||
import type { KubeObject } from "../../api/kube-object";
|
||||
import { eventStore } from "./event.store";
|
||||
import type { EventStore } from "./event.store";
|
||||
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 {
|
||||
object: KubeObject;
|
||||
@ -39,11 +40,15 @@ const defaultProps: Partial<Props> = {
|
||||
};
|
||||
|
||||
export class KubeEventIcon extends React.Component<Props> {
|
||||
private get eventStore() {
|
||||
return ApiManager.getInstance().getStore<EventStore>(eventApi);
|
||||
}
|
||||
|
||||
static defaultProps = defaultProps as object;
|
||||
|
||||
render() {
|
||||
const { object, showWarningsOnly, filterEvents } = this.props;
|
||||
const events = eventStore.getEventsByObject(object);
|
||||
const events = this.eventStore.getEventsByObject(object);
|
||||
let warnings = events.filter(evt => evt.isWarning());
|
||||
|
||||
if (filterEvents) warnings = filterEvents(warnings);
|
||||
|
||||
@ -26,11 +26,12 @@ import { observable } from "mobx";
|
||||
import { observer } from "mobx-react";
|
||||
import { Dialog, DialogProps } from "../dialog";
|
||||
import { Wizard, WizardStep } from "../wizard";
|
||||
import { namespaceStore } from "./namespace.store";
|
||||
import type { Namespace } from "../../api/endpoints";
|
||||
import { Namespace, namespacesApi } from "../../api/endpoints";
|
||||
import { Input } from "../input";
|
||||
import { systemName } from "../input/input_validators";
|
||||
import { Notifications } from "../notifications";
|
||||
import type { NamespaceStore } from "./namespace.store";
|
||||
import { ApiManager } from "../../api/api-manager";
|
||||
|
||||
interface Props extends DialogProps {
|
||||
onSuccess?(ns: Namespace): void;
|
||||
@ -39,6 +40,10 @@ interface Props extends DialogProps {
|
||||
|
||||
@observer
|
||||
export class AddNamespaceDialog extends React.Component<Props> {
|
||||
private get namespaceStore() {
|
||||
return ApiManager.getInstance().getStore<NamespaceStore>(namespacesApi);
|
||||
}
|
||||
|
||||
@observable static isOpen = false;
|
||||
@observable namespace = "";
|
||||
|
||||
@ -59,7 +64,7 @@ export class AddNamespaceDialog extends React.Component<Props> {
|
||||
const { onSuccess, onError } = this.props;
|
||||
|
||||
try {
|
||||
const created = await namespaceStore.create({ name: namespace });
|
||||
const created = await this.namespaceStore.create({ name: namespace });
|
||||
|
||||
onSuccess?.(created);
|
||||
AddNamespaceDialog.close();
|
||||
|
||||
@ -20,5 +20,6 @@
|
||||
*/
|
||||
|
||||
export * from "./namespaces";
|
||||
export * from "./namespace.store";
|
||||
export * from "./namespace-details";
|
||||
export * from "./add-namespace-dialog";
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user