mirror of
https://github.com/lensapp/lens.git
synced 2025-05-20 05:10:56 +00:00
refactoring: get rid of config.store.ts (renderer), moved allowNamespaces/allowedResources to cluster.ts apis
Signed-off-by: Roman <ixrock@gmail.com>
This commit is contained in:
parent
407c4fb4d0
commit
3aef3700de
@ -1,7 +1,7 @@
|
|||||||
import type { ClusterId, ClusterModel, ClusterPreferences } from "../common/cluster-store"
|
import type { ClusterId, ClusterModel, ClusterPreferences } from "../common/cluster-store"
|
||||||
import type { FeatureStatusMap } from "./feature"
|
import type { FeatureStatusMap } from "./feature"
|
||||||
import type { WorkspaceId } from "../common/workspace-store";
|
import type { WorkspaceId } from "../common/workspace-store";
|
||||||
import { action, computed, observable, reaction, toJS, when } from "mobx";
|
import { action, computed, observable, reaction, toJS } from "mobx";
|
||||||
import { apiKubePrefix } from "../common/vars";
|
import { apiKubePrefix } from "../common/vars";
|
||||||
import { broadcastIpc } from "../common/ipc";
|
import { broadcastIpc } from "../common/ipc";
|
||||||
import { ContextHandler } from "./context-handler"
|
import { ContextHandler } from "./context-handler"
|
||||||
@ -20,17 +20,19 @@ export enum ClusterStatus {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface ClusterState extends ClusterModel {
|
export interface ClusterState extends ClusterModel {
|
||||||
initialized?: boolean;
|
initialized: boolean;
|
||||||
apiUrl: string;
|
apiUrl: string;
|
||||||
online?: boolean;
|
online: boolean;
|
||||||
accessible?: boolean;
|
accessible: boolean;
|
||||||
failureReason?: string;
|
failureReason: string;
|
||||||
nodes?: number;
|
nodes: number;
|
||||||
eventCount?: number;
|
eventCount: number;
|
||||||
version?: string;
|
version: string;
|
||||||
distribution?: string;
|
distribution: string;
|
||||||
isAdmin?: boolean;
|
isAdmin: boolean;
|
||||||
features?: FeatureStatusMap;
|
allowedNamespaces: string[]
|
||||||
|
allowedResources: string[]
|
||||||
|
features: FeatureStatusMap;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class Cluster implements ClusterModel {
|
export class Cluster implements ClusterModel {
|
||||||
@ -58,6 +60,8 @@ export class Cluster implements ClusterModel {
|
|||||||
@observable eventCount = 0;
|
@observable eventCount = 0;
|
||||||
@observable preferences: ClusterPreferences = {};
|
@observable preferences: ClusterPreferences = {};
|
||||||
@observable features: FeatureStatusMap = {};
|
@observable features: FeatureStatusMap = {};
|
||||||
|
@observable allowedNamespaces: string[] = [];
|
||||||
|
@observable allowedResources: string[] = [];
|
||||||
|
|
||||||
constructor(model: ClusterModel) {
|
constructor(model: ClusterModel) {
|
||||||
this.updateModel(model);
|
this.updateModel(model);
|
||||||
@ -120,7 +124,6 @@ export class Cluster implements ClusterModel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async activate() {
|
async activate() {
|
||||||
await when(() => this.initialized);
|
|
||||||
if (this.disconnected) await this.reconnect();
|
if (this.disconnected) await this.reconnect();
|
||||||
await this.refresh();
|
await this.refresh();
|
||||||
return this.pushState();
|
return this.pushState();
|
||||||
@ -150,14 +153,28 @@ export class Cluster implements ClusterModel {
|
|||||||
this.online = connectionStatus > ClusterStatus.Offline;
|
this.online = connectionStatus > ClusterStatus.Offline;
|
||||||
this.accessible = connectionStatus == ClusterStatus.AccessGranted;
|
this.accessible = connectionStatus == ClusterStatus.AccessGranted;
|
||||||
if (this.accessible) {
|
if (this.accessible) {
|
||||||
this.distribution = this.detectKubernetesDistribution(this.version)
|
|
||||||
this.features = await getFeatures(this)
|
|
||||||
this.isAdmin = await this.isClusterAdmin()
|
|
||||||
this.nodes = await this.getNodeCount()
|
|
||||||
this.kubeCtl = new Kubectl(this.version)
|
this.kubeCtl = new Kubectl(this.version)
|
||||||
this.kubeCtl.ensureKubectl()
|
this.distribution = this.detectKubernetesDistribution(this.version)
|
||||||
|
const [features, isAdmin, nodesCount] = await Promise.all([
|
||||||
|
getFeatures(this),
|
||||||
|
this.isClusterAdmin(),
|
||||||
|
this.getNodeCount(),
|
||||||
|
this.kubeCtl.ensureKubectl()
|
||||||
|
]);
|
||||||
|
this.features = features;
|
||||||
|
this.isAdmin = isAdmin;
|
||||||
|
this.nodes = nodesCount;
|
||||||
}
|
}
|
||||||
await this.refreshEvents();
|
await Promise.all([
|
||||||
|
this.refreshEvents(),
|
||||||
|
this.refreshAllowedResources(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
async refreshAllowedResources() {
|
||||||
|
this.allowedNamespaces = await this.getAllowedNamespaces();
|
||||||
|
this.allowedResources = await this.getAllowedResources();
|
||||||
}
|
}
|
||||||
|
|
||||||
@action
|
@action
|
||||||
@ -342,6 +359,8 @@ export class Cluster implements ClusterModel {
|
|||||||
isAdmin: this.isAdmin,
|
isAdmin: this.isAdmin,
|
||||||
features: this.features,
|
features: this.features,
|
||||||
eventCount: this.eventCount,
|
eventCount: this.eventCount,
|
||||||
|
allowedNamespaces: this.allowedNamespaces,
|
||||||
|
allowedResources: this.allowedResources,
|
||||||
};
|
};
|
||||||
return toJS(state, {
|
return toJS(state, {
|
||||||
recurseEverything: true
|
recurseEverything: true
|
||||||
@ -368,4 +387,67 @@ export class Cluster implements ClusterModel {
|
|||||||
online: this.online,
|
online: this.online,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected async getAllowedNamespaces() {
|
||||||
|
const api = this.getProxyKubeconfig().makeApiClient(CoreV1Api)
|
||||||
|
try {
|
||||||
|
const namespaceList = await api.listNamespace()
|
||||||
|
const nsAccessStatuses = await Promise.all(
|
||||||
|
namespaceList.body.items.map(ns => this.canI({
|
||||||
|
namespace: ns.metadata.name,
|
||||||
|
resource: "pods",
|
||||||
|
verb: "list",
|
||||||
|
}))
|
||||||
|
)
|
||||||
|
return namespaceList.body.items
|
||||||
|
.filter((ns, i) => nsAccessStatuses[i])
|
||||||
|
.map(ns => ns.metadata.name)
|
||||||
|
} catch (error) {
|
||||||
|
const ctx = this.getProxyKubeconfig().getContextObject(this.contextName)
|
||||||
|
if (ctx.namespace) return [ctx.namespace]
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async getAllowedResources() {
|
||||||
|
// TODO: auto-populate all resources dynamically
|
||||||
|
const apiResources = [
|
||||||
|
{ resource: "configmaps" },
|
||||||
|
{ resource: "cronjobs", group: "batch" },
|
||||||
|
{ resource: "customresourcedefinitions", group: "apiextensions.k8s.io" },
|
||||||
|
{ resource: "daemonsets", group: "apps" },
|
||||||
|
{ resource: "deployments", group: "apps" },
|
||||||
|
{ resource: "endpoints" },
|
||||||
|
{ resource: "events" },
|
||||||
|
{ resource: "horizontalpodautoscalers" },
|
||||||
|
{ resource: "ingresses", group: "networking.k8s.io" },
|
||||||
|
{ resource: "jobs", group: "batch" },
|
||||||
|
{ resource: "namespaces" },
|
||||||
|
{ resource: "networkpolicies", group: "networking.k8s.io" },
|
||||||
|
{ resource: "nodes" },
|
||||||
|
{ resource: "persistentvolumes" },
|
||||||
|
{ resource: "pods" },
|
||||||
|
{ resource: "podsecuritypolicies" },
|
||||||
|
{ resource: "resourcequotas" },
|
||||||
|
{ resource: "secrets" },
|
||||||
|
{ resource: "services" },
|
||||||
|
{ resource: "statefulsets", group: "apps" },
|
||||||
|
{ resource: "storageclasses", group: "storage.k8s.io" },
|
||||||
|
]
|
||||||
|
try {
|
||||||
|
const resourceAccessStatuses = await Promise.all(
|
||||||
|
apiResources.map(apiResource => this.canI({
|
||||||
|
resource: apiResource.resource,
|
||||||
|
group: apiResource.group,
|
||||||
|
verb: "list",
|
||||||
|
namespace: this.allowedNamespaces[0]
|
||||||
|
}))
|
||||||
|
)
|
||||||
|
return apiResources
|
||||||
|
.filter((resource, i) => resourceAccessStatuses[i])
|
||||||
|
.map(apiResource => apiResource.resource)
|
||||||
|
} catch (error) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -5,7 +5,7 @@ import path from "path"
|
|||||||
import { readFile } from "fs-extra"
|
import { readFile } from "fs-extra"
|
||||||
import { Cluster } from "./cluster"
|
import { Cluster } from "./cluster"
|
||||||
import { apiPrefix, appName, outDir } from "../common/vars";
|
import { apiPrefix, appName, outDir } from "../common/vars";
|
||||||
import { configRoute, helmRoute, kubeconfigRoute, metricsRoute, portForwardRoute, resourceApplierRoute, watchRoute } from "./routes";
|
import { helmRoute, kubeconfigRoute, metricsRoute, portForwardRoute, resourceApplierRoute, watchRoute } from "./routes";
|
||||||
|
|
||||||
export interface RouterRequestOpts {
|
export interface RouterRequestOpts {
|
||||||
req: http.IncomingMessage;
|
req: http.IncomingMessage;
|
||||||
@ -112,7 +112,6 @@ export class Router {
|
|||||||
this.handleStaticFile(params.path, response);
|
this.handleStaticFile(params.path, response);
|
||||||
});
|
});
|
||||||
|
|
||||||
this.router.add({ method: "get", path: `${apiPrefix}/config` }, configRoute.routeConfig.bind(configRoute))
|
|
||||||
this.router.add({ method: "get", path: `${apiPrefix}/kubeconfig/service-account/{namespace}/{account}` }, kubeconfigRoute.routeServiceAccountRoute.bind(kubeconfigRoute))
|
this.router.add({ method: "get", path: `${apiPrefix}/kubeconfig/service-account/{namespace}/{account}` }, kubeconfigRoute.routeServiceAccountRoute.bind(kubeconfigRoute))
|
||||||
|
|
||||||
// Watch API
|
// Watch API
|
||||||
|
|||||||
@ -1,105 +0,0 @@
|
|||||||
import { app } from "electron"
|
|
||||||
import { CoreV1Api } from "@kubernetes/client-node"
|
|
||||||
import { LensApiRequest } from "../router"
|
|
||||||
import { LensApi } from "../lens-api"
|
|
||||||
import { Cluster } from "../cluster"
|
|
||||||
|
|
||||||
export interface IConfigRoutePayload {
|
|
||||||
kubeVersion?: string;
|
|
||||||
clusterName?: string;
|
|
||||||
lensVersion?: string;
|
|
||||||
username?: string;
|
|
||||||
token?: string;
|
|
||||||
allowedNamespaces?: string[];
|
|
||||||
allowedResources?: string[];
|
|
||||||
isClusterAdmin?: boolean;
|
|
||||||
chartsEnabled: boolean;
|
|
||||||
kubectlAccess?: boolean; // User accessed via kubectl-lens plugin
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: auto-populate all resources dynamically
|
|
||||||
const apiResources = [
|
|
||||||
{ resource: "configmaps" },
|
|
||||||
{ resource: "cronjobs", group: "batch" },
|
|
||||||
{ resource: "customresourcedefinitions", group: "apiextensions.k8s.io" },
|
|
||||||
{ resource: "daemonsets", group: "apps" },
|
|
||||||
{ resource: "deployments", group: "apps" },
|
|
||||||
{ resource: "endpoints" },
|
|
||||||
{ resource: "events" },
|
|
||||||
{ resource: "horizontalpodautoscalers" },
|
|
||||||
{ resource: "ingresses", group: "networking.k8s.io" },
|
|
||||||
{ resource: "jobs", group: "batch" },
|
|
||||||
{ resource: "namespaces" },
|
|
||||||
{ resource: "networkpolicies", group: "networking.k8s.io" },
|
|
||||||
{ resource: "nodes" },
|
|
||||||
{ resource: "persistentvolumes" },
|
|
||||||
{ resource: "pods" },
|
|
||||||
{ resource: "podsecuritypolicies" },
|
|
||||||
{ resource: "resourcequotas" },
|
|
||||||
{ resource: "secrets" },
|
|
||||||
{ resource: "services" },
|
|
||||||
{ resource: "statefulsets", group: "apps" },
|
|
||||||
{ resource: "storageclasses", group: "storage.k8s.io" },
|
|
||||||
]
|
|
||||||
|
|
||||||
async function getAllowedNamespaces(cluster: Cluster) {
|
|
||||||
const api = cluster.getProxyKubeconfig().makeApiClient(CoreV1Api)
|
|
||||||
try {
|
|
||||||
const namespaceList = await api.listNamespace()
|
|
||||||
const nsAccessStatuses = await Promise.all(
|
|
||||||
namespaceList.body.items.map(ns => cluster.canI({
|
|
||||||
namespace: ns.metadata.name,
|
|
||||||
resource: "pods",
|
|
||||||
verb: "list",
|
|
||||||
}))
|
|
||||||
)
|
|
||||||
return namespaceList.body.items
|
|
||||||
.filter((ns, i) => nsAccessStatuses[i])
|
|
||||||
.map(ns => ns.metadata.name)
|
|
||||||
} catch (error) {
|
|
||||||
const ctx = cluster.getProxyKubeconfig().getContextObject(cluster.contextName)
|
|
||||||
if (ctx.namespace) {
|
|
||||||
return [ctx.namespace]
|
|
||||||
} else {
|
|
||||||
return []
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function getAllowedResources(cluster: Cluster, namespaces: string[]) {
|
|
||||||
try {
|
|
||||||
const resourceAccessStatuses = await Promise.all(
|
|
||||||
apiResources.map(apiResource => cluster.canI({
|
|
||||||
resource: apiResource.resource,
|
|
||||||
group: apiResource.group,
|
|
||||||
verb: "list",
|
|
||||||
namespace: namespaces[0]
|
|
||||||
}))
|
|
||||||
)
|
|
||||||
return apiResources
|
|
||||||
.filter((resource, i) => resourceAccessStatuses[i]).map(apiResource => apiResource.resource)
|
|
||||||
} catch (error) {
|
|
||||||
return []
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class ConfigRoute extends LensApi {
|
|
||||||
public async routeConfig(request: LensApiRequest) {
|
|
||||||
const { params, response, cluster } = request
|
|
||||||
|
|
||||||
const namespaces = await getAllowedNamespaces(cluster)
|
|
||||||
const data: IConfigRoutePayload = {
|
|
||||||
clusterName: cluster.contextName,
|
|
||||||
lensVersion: app.getVersion(),
|
|
||||||
kubeVersion: cluster.version,
|
|
||||||
chartsEnabled: true,
|
|
||||||
isClusterAdmin: cluster.isAdmin,
|
|
||||||
allowedResources: await getAllowedResources(cluster, namespaces),
|
|
||||||
allowedNamespaces: namespaces
|
|
||||||
};
|
|
||||||
|
|
||||||
this.respondJson(response, data)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const configRoute = new ConfigRoute()
|
|
||||||
@ -1,4 +1,3 @@
|
|||||||
export * from "./config-route"
|
|
||||||
export * from "./kubeconfig-route"
|
export * from "./kubeconfig-route"
|
||||||
export * from "./metrics-route"
|
export * from "./metrics-route"
|
||||||
export * from "./port-forward-route"
|
export * from "./port-forward-route"
|
||||||
|
|||||||
@ -6,9 +6,9 @@ import { autobind, EventEmitter } from "../utils";
|
|||||||
import { KubeJsonApiData } from "./kube-json-api";
|
import { KubeJsonApiData } from "./kube-json-api";
|
||||||
import type { KubeObjectStore } from "../kube-object.store";
|
import type { KubeObjectStore } from "../kube-object.store";
|
||||||
import { KubeApi } from "./kube-api";
|
import { KubeApi } from "./kube-api";
|
||||||
import { configStore } from "../config.store";
|
|
||||||
import { apiManager } from "./api-manager";
|
import { apiManager } from "./api-manager";
|
||||||
import { apiPrefix, isDevelopment } from "../../common/vars";
|
import { apiPrefix, isDevelopment } from "../../common/vars";
|
||||||
|
import { clusterStore } from "../../common/cluster-store";
|
||||||
|
|
||||||
export interface IKubeWatchEvent<T = any> {
|
export interface IKubeWatchEvent<T = any> {
|
||||||
type: "ADDED" | "MODIFIED" | "DELETED";
|
type: "ADDED" | "MODIFIED" | "DELETED";
|
||||||
@ -61,10 +61,10 @@ export class KubeWatchApi {
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected getQuery(): Partial<IKubeWatchRouteQuery> {
|
protected getQuery(): Partial<IKubeWatchRouteQuery> {
|
||||||
const { isClusterAdmin, allowedNamespaces } = configStore;
|
const { isAdmin, allowedNamespaces } = clusterStore.activeCluster;
|
||||||
return {
|
return {
|
||||||
api: this.activeApis.map(api => {
|
api: this.activeApis.map(api => {
|
||||||
if (isClusterAdmin) return api.getWatchUrl();
|
if (isAdmin) return api.getWatchUrl();
|
||||||
return allowedNamespaces.map(namespace => api.getWatchUrl(namespace))
|
return allowedNamespaces.map(namespace => api.getWatchUrl(namespace))
|
||||||
}).flat()
|
}).flat()
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,10 +1,12 @@
|
|||||||
import { configStore } from "../config.store";
|
import { clusterStore } from "../../common/cluster-store";
|
||||||
|
|
||||||
|
// todo: refactor / move to cluster-store.ts?
|
||||||
|
|
||||||
export function isAllowedResource(resources: string | string[]) {
|
export function isAllowedResource(resources: string | string[]) {
|
||||||
if (!Array.isArray(resources)) {
|
if (!Array.isArray(resources)) {
|
||||||
resources = [resources];
|
resources = [resources];
|
||||||
}
|
}
|
||||||
const { allowedResources } = configStore;
|
const allowedResources = clusterStore.activeCluster?.allowedResources || [];
|
||||||
for (const resource of resources) {
|
for (const resource of resources) {
|
||||||
if (!allowedResources.includes(resource)) {
|
if (!allowedResources.includes(resource)) {
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
@ -1,7 +1,6 @@
|
|||||||
import { stringify } from "querystring";
|
import { stringify } from "querystring";
|
||||||
import { autobind, base64, EventEmitter, interval } from "../utils";
|
import { autobind, base64, EventEmitter } from "../utils";
|
||||||
import { WebSocketApi } from "./websocket-api";
|
import { WebSocketApi } from "./websocket-api";
|
||||||
import { configStore } from "../config.store";
|
|
||||||
import isEqual from "lodash/isEqual"
|
import isEqual from "lodash/isEqual"
|
||||||
import { isDevelopment } from "../../common/vars";
|
import { isDevelopment } from "../../common/vars";
|
||||||
|
|
||||||
@ -25,21 +24,19 @@ enum TerminalColor {
|
|||||||
NO_COLOR = "\u001b[0m",
|
NO_COLOR = "\u001b[0m",
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ITerminalApiOptions {
|
export type TerminalApiQuery = Record<string, string> & {
|
||||||
id: string;
|
id: string;
|
||||||
node?: string;
|
node?: string;
|
||||||
colorTheme?: "light" | "dark";
|
type?: string | "node";
|
||||||
}
|
}
|
||||||
|
|
||||||
export class TerminalApi extends WebSocketApi {
|
export class TerminalApi extends WebSocketApi {
|
||||||
protected size: { Width: number; Height: number };
|
protected size: { Width: number; Height: number };
|
||||||
protected currentToken: string;
|
|
||||||
protected tokenInterval = interval(60, this.sendNewToken); // refresh every minute
|
|
||||||
|
|
||||||
public onReady = new EventEmitter<[]>();
|
public onReady = new EventEmitter<[]>();
|
||||||
public isReady = false;
|
public isReady = false;
|
||||||
|
|
||||||
constructor(protected options: ITerminalApiOptions) {
|
constructor(protected options: TerminalApiQuery) {
|
||||||
super({
|
super({
|
||||||
logging: isDevelopment,
|
logging: isDevelopment,
|
||||||
flushOnOpen: false,
|
flushOnOpen: false,
|
||||||
@ -47,50 +44,33 @@ export class TerminalApi extends WebSocketApi {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async getUrl(token: string) {
|
async getUrl() {
|
||||||
const { hostname, protocol } = location;
|
|
||||||
let { port } = location;
|
let { port } = location;
|
||||||
|
const { hostname, protocol } = location;
|
||||||
const { id, node } = this.options;
|
const { id, node } = this.options;
|
||||||
const wss = `ws${protocol === "https:" ? "s" : ""}://`;
|
const wss = `ws${protocol === "https:" ? "s" : ""}://`;
|
||||||
const queryParams = { token, id };
|
const query: TerminalApiQuery = { id };
|
||||||
if (port) {
|
if (port) {
|
||||||
port = `:${port}`
|
port = `:${port}`
|
||||||
}
|
}
|
||||||
if (node) {
|
if (node) {
|
||||||
Object.assign(queryParams, {
|
query.node = node;
|
||||||
node: node,
|
query.type = "node";
|
||||||
type: "node"
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
return `${wss}${hostname}${port}/api?${stringify(queryParams)}`;
|
return `${wss}${hostname}${port}/api?${stringify(query)}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
async connect() {
|
async connect() {
|
||||||
const token = await configStore.getToken();
|
const apiUrl = await this.getUrl();
|
||||||
const apiUrl = await this.getUrl(token);
|
this.emitStatus("Connecting ...");
|
||||||
const { colorTheme } = this.options;
|
|
||||||
this.emitStatus("Connecting ...", {
|
|
||||||
color: colorTheme == "light" ? TerminalColor.GRAY : TerminalColor.LIGHT_GRAY
|
|
||||||
});
|
|
||||||
this.onData.addListener(this._onReady, { prepend: true });
|
this.onData.addListener(this._onReady, { prepend: true });
|
||||||
this.currentToken = token;
|
|
||||||
this.tokenInterval.start();
|
|
||||||
return super.connect(apiUrl);
|
return super.connect(apiUrl);
|
||||||
}
|
}
|
||||||
|
|
||||||
@autobind()
|
|
||||||
async sendNewToken() {
|
|
||||||
const token = await configStore.getToken();
|
|
||||||
if (!this.isReady || token == this.currentToken) return;
|
|
||||||
this.sendCommand(token, TerminalChannels.TOKEN);
|
|
||||||
this.currentToken = token;
|
|
||||||
}
|
|
||||||
|
|
||||||
destroy() {
|
destroy() {
|
||||||
if (!this.socket) return;
|
if (!this.socket) return;
|
||||||
const exitCode = String.fromCharCode(4); // ctrl+d
|
const exitCode = String.fromCharCode(4); // ctrl+d
|
||||||
this.sendCommand(exitCode);
|
this.sendCommand(exitCode);
|
||||||
this.tokenInterval.stop();
|
|
||||||
setTimeout(() => super.destroy(), 2000);
|
setTimeout(() => super.destroy(), 2000);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,11 +1,11 @@
|
|||||||
import isEqual from "lodash/isEqual";
|
import isEqual from "lodash/isEqual";
|
||||||
import { action, observable, when, IReactionDisposer, reaction } from "mobx";
|
import { action, IReactionDisposer, observable, reaction, when } from "mobx";
|
||||||
import { autobind } from "../../utils";
|
import { autobind } from "../../utils";
|
||||||
import { HelmRelease, helmReleasesApi, IReleaseCreatePayload, IReleaseUpdatePayload } from "../../api/endpoints/helm-releases.api";
|
import { HelmRelease, helmReleasesApi, IReleaseCreatePayload, IReleaseUpdatePayload } from "../../api/endpoints/helm-releases.api";
|
||||||
import { ItemStore } from "../../item.store";
|
import { ItemStore } from "../../item.store";
|
||||||
import { configStore } from "../../config.store";
|
|
||||||
import { secretsStore } from "../+config-secrets/secrets.store";
|
|
||||||
import { Secret } from "../../api/endpoints";
|
import { Secret } from "../../api/endpoints";
|
||||||
|
import { secretsStore } from "../+config-secrets/secrets.store";
|
||||||
|
import { clusterStore } from "../../../common/cluster-store";
|
||||||
|
|
||||||
@autobind()
|
@autobind()
|
||||||
export class ReleaseStore extends ItemStore<HelmRelease> {
|
export class ReleaseStore extends ItemStore<HelmRelease> {
|
||||||
@ -58,8 +58,8 @@ export class ReleaseStore extends ItemStore<HelmRelease> {
|
|||||||
this.isLoading = true;
|
this.isLoading = true;
|
||||||
let items;
|
let items;
|
||||||
try {
|
try {
|
||||||
const { isClusterAdmin, allowedNamespaces } = configStore;
|
const { isAdmin, allowedNamespaces } = clusterStore.activeCluster;
|
||||||
items = await this.loadItems(!isClusterAdmin ? allowedNamespaces : null);
|
items = await this.loadItems(!isAdmin ? allowedNamespaces : null);
|
||||||
} finally {
|
} finally {
|
||||||
if (items) {
|
if (items) {
|
||||||
items = this.sortItems(items);
|
items = this.sortItems(items);
|
||||||
@ -73,8 +73,7 @@ export class ReleaseStore extends ItemStore<HelmRelease> {
|
|||||||
async loadItems(namespaces?: string[]) {
|
async loadItems(namespaces?: string[]) {
|
||||||
if (!namespaces) {
|
if (!namespaces) {
|
||||||
return helmReleasesApi.list();
|
return helmReleasesApi.list();
|
||||||
}
|
} else {
|
||||||
else {
|
|
||||||
return Promise
|
return Promise
|
||||||
.all(namespaces.map(namespace => helmReleasesApi.list(namespace)))
|
.all(namespaces.map(namespace => helmReleasesApi.list(namespace)))
|
||||||
.then(items => items.flat());
|
.then(items => items.flat());
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import "./cluster.scss"
|
import "./cluster.scss"
|
||||||
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { computed, reaction, when } from "mobx";
|
import { computed, reaction } from "mobx";
|
||||||
import { disposeOnUnmount, observer } from "mobx-react";
|
import { disposeOnUnmount, observer } from "mobx-react";
|
||||||
import { MainLayout } from "../layout/main-layout";
|
import { MainLayout } from "../layout/main-layout";
|
||||||
import { ClusterIssues } from "./cluster-issues";
|
import { ClusterIssues } from "./cluster-issues";
|
||||||
@ -13,22 +13,23 @@ import { nodesStore } from "../+nodes/nodes.store";
|
|||||||
import { podsStore } from "../+workloads-pods/pods.store";
|
import { podsStore } from "../+workloads-pods/pods.store";
|
||||||
import { clusterStore } from "./cluster.store";
|
import { clusterStore } from "./cluster.store";
|
||||||
import { eventStore } from "../+events/event.store";
|
import { eventStore } from "../+events/event.store";
|
||||||
import { configStore } from "../../config.store";
|
|
||||||
import { isAllowedResource } from "../../api/rbac";
|
import { isAllowedResource } from "../../api/rbac";
|
||||||
|
|
||||||
@observer
|
@observer
|
||||||
export class Cluster extends React.Component {
|
export class Cluster extends React.Component {
|
||||||
|
private dependentStores = [nodesStore, podsStore];
|
||||||
|
|
||||||
private watchers = [
|
private watchers = [
|
||||||
interval(60, () => clusterStore.getMetrics()),
|
interval(60, () => clusterStore.getMetrics()),
|
||||||
interval(20, () => eventStore.loadAll())
|
interval(20, () => eventStore.loadAll())
|
||||||
];
|
];
|
||||||
|
|
||||||
private dependentStores = [nodesStore, podsStore];
|
@computed get isLoaded() {
|
||||||
|
return nodesStore.isLoaded && podsStore.isLoaded
|
||||||
|
}
|
||||||
|
|
||||||
// todo: refactor
|
// todo: refactor
|
||||||
async componentDidMount() {
|
async componentDidMount() {
|
||||||
await when(() => configStore.isLoaded);
|
|
||||||
|
|
||||||
const { dependentStores } = this;
|
const { dependentStores } = this;
|
||||||
if (!isAllowedResource("nodes")) {
|
if (!isAllowedResource("nodes")) {
|
||||||
dependentStores.splice(dependentStores.indexOf(nodesStore), 1)
|
dependentStores.splice(dependentStores.indexOf(nodesStore), 1)
|
||||||
@ -50,13 +51,6 @@ export class Cluster extends React.Component {
|
|||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
|
||||||
@computed get isLoaded() {
|
|
||||||
return (
|
|
||||||
nodesStore.isLoaded &&
|
|
||||||
podsStore.isLoaded
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { isLoaded } = this;
|
const { isLoaded } = this;
|
||||||
return (
|
return (
|
||||||
|
|||||||
@ -82,10 +82,6 @@ export class Preferences extends React.Component {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onThemeChange = ({ value }: SelectOption<string>) => {
|
|
||||||
userStore.preferences.colorTheme = value;
|
|
||||||
}
|
|
||||||
|
|
||||||
onRepoSelect = async ({ value: repo }: SelectOption<HelmRepo>) => {
|
onRepoSelect = async ({ value: repo }: SelectOption<HelmRepo>) => {
|
||||||
const isAdded = this.helmAddedRepos.has(repo.name);
|
const isAdded = this.helmAddedRepos.has(repo.name);
|
||||||
if (isAdded) {
|
if (isAdded) {
|
||||||
|
|||||||
@ -9,9 +9,9 @@ import { MainLayout, TabRoute } from "../layout/main-layout";
|
|||||||
import { PersistentVolumes, volumesRoute, volumesURL } from "../+storage-volumes";
|
import { PersistentVolumes, volumesRoute, volumesURL } from "../+storage-volumes";
|
||||||
import { StorageClasses, storageClassesRoute, storageClassesURL } from "../+storage-classes";
|
import { StorageClasses, storageClassesRoute, storageClassesURL } from "../+storage-classes";
|
||||||
import { PersistentVolumeClaims, volumeClaimsRoute, volumeClaimsURL } from "../+storage-volume-claims";
|
import { PersistentVolumeClaims, volumeClaimsRoute, volumeClaimsURL } from "../+storage-volume-claims";
|
||||||
import { configStore } from "../../config.store";
|
|
||||||
import { namespaceStore } from "../+namespaces/namespace.store";
|
import { namespaceStore } from "../+namespaces/namespace.store";
|
||||||
import { storageURL } from "./storage.route";
|
import { storageURL } from "./storage.route";
|
||||||
|
import { isAllowedResource } from "../../api/rbac";
|
||||||
|
|
||||||
interface Props extends RouteComponentProps<{}> {
|
interface Props extends RouteComponentProps<{}> {
|
||||||
}
|
}
|
||||||
@ -20,7 +20,6 @@ interface Props extends RouteComponentProps<{}> {
|
|||||||
export class Storage extends React.Component<Props> {
|
export class Storage extends React.Component<Props> {
|
||||||
static get tabRoutes() {
|
static get tabRoutes() {
|
||||||
const tabRoutes: TabRoute[] = [];
|
const tabRoutes: TabRoute[] = [];
|
||||||
const { allowedResources } = configStore;
|
|
||||||
const query = namespaceStore.getContextParams()
|
const query = namespaceStore.getContextParams()
|
||||||
|
|
||||||
tabRoutes.push({
|
tabRoutes.push({
|
||||||
@ -30,7 +29,7 @@ export class Storage extends React.Component<Props> {
|
|||||||
path: volumeClaimsRoute.path,
|
path: volumeClaimsRoute.path,
|
||||||
})
|
})
|
||||||
|
|
||||||
if (allowedResources.includes('persistentvolumes')) {
|
if (isAllowedResource('persistentvolumes')) {
|
||||||
tabRoutes.push({
|
tabRoutes.push({
|
||||||
title: <Trans>Persistent Volumes</Trans>,
|
title: <Trans>Persistent Volumes</Trans>,
|
||||||
component: PersistentVolumes,
|
component: PersistentVolumes,
|
||||||
@ -39,7 +38,7 @@ export class Storage extends React.Component<Props> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (allowedResources.includes('storageclasses')) {
|
if (isAllowedResource('storageclasses')) {
|
||||||
tabRoutes.push({
|
tabRoutes.push({
|
||||||
title: <Trans>Storage Classes</Trans>,
|
title: <Trans>Storage Classes</Trans>,
|
||||||
component: StorageClasses,
|
component: StorageClasses,
|
||||||
|
|||||||
@ -1,5 +1,4 @@
|
|||||||
import "./user-management.scss"
|
import "./user-management.scss"
|
||||||
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
import { Redirect, Route, Switch } from "react-router";
|
import { Redirect, Route, Switch } from "react-router";
|
||||||
@ -11,8 +10,8 @@ import { RoleBindings } from "../+user-management-roles-bindings";
|
|||||||
import { ServiceAccounts } from "../+user-management-service-accounts";
|
import { ServiceAccounts } from "../+user-management-service-accounts";
|
||||||
import { roleBindingsRoute, roleBindingsURL, rolesRoute, rolesURL, serviceAccountsRoute, serviceAccountsURL, usersManagementURL } from "./user-management.routes";
|
import { roleBindingsRoute, roleBindingsURL, rolesRoute, rolesURL, serviceAccountsRoute, serviceAccountsURL, usersManagementURL } from "./user-management.routes";
|
||||||
import { namespaceStore } from "../+namespaces/namespace.store";
|
import { namespaceStore } from "../+namespaces/namespace.store";
|
||||||
import { configStore } from "../../config.store";
|
|
||||||
import { PodSecurityPolicies, podSecurityPoliciesRoute, podSecurityPoliciesURL } from "../+pod-security-policies";
|
import { PodSecurityPolicies, podSecurityPoliciesRoute, podSecurityPoliciesURL } from "../+pod-security-policies";
|
||||||
|
import { isAllowedResource } from "../../api/rbac";
|
||||||
|
|
||||||
interface Props extends RouteComponentProps<{}> {
|
interface Props extends RouteComponentProps<{}> {
|
||||||
}
|
}
|
||||||
@ -21,7 +20,6 @@ interface Props extends RouteComponentProps<{}> {
|
|||||||
export class UserManagement extends React.Component<Props> {
|
export class UserManagement extends React.Component<Props> {
|
||||||
static get tabRoutes() {
|
static get tabRoutes() {
|
||||||
const tabRoutes: TabRoute[] = [];
|
const tabRoutes: TabRoute[] = [];
|
||||||
const { allowedResources } = configStore;
|
|
||||||
const query = namespaceStore.getContextParams()
|
const query = namespaceStore.getContextParams()
|
||||||
tabRoutes.push(
|
tabRoutes.push(
|
||||||
{
|
{
|
||||||
@ -43,7 +41,7 @@ export class UserManagement extends React.Component<Props> {
|
|||||||
path: rolesRoute.path,
|
path: rolesRoute.path,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
if (allowedResources.includes("podsecuritypolicies")) {
|
if (isAllowedResource("podsecuritypolicies")) {
|
||||||
tabRoutes.push({
|
tabRoutes.push({
|
||||||
title: <Trans>Pod Security Policies</Trans>,
|
title: <Trans>Pod Security Policies</Trans>,
|
||||||
component: PodSecurityPolicies,
|
component: PodSecurityPolicies,
|
||||||
|
|||||||
@ -15,13 +15,11 @@ import { cronJobStore } from "../+workloads-cronjobs/cronjob.store";
|
|||||||
import { namespaceStore } from "../+namespaces/namespace.store";
|
import { namespaceStore } from "../+namespaces/namespace.store";
|
||||||
import { PageFiltersList } from "../item-object-list/page-filters-list";
|
import { PageFiltersList } from "../item-object-list/page-filters-list";
|
||||||
import { NamespaceSelectFilter } from "../+namespaces/namespace-select";
|
import { NamespaceSelectFilter } from "../+namespaces/namespace-select";
|
||||||
import { configStore } from "../../config.store";
|
|
||||||
import { isAllowedResource } from "../../api/rbac";
|
import { isAllowedResource } from "../../api/rbac";
|
||||||
|
|
||||||
@observer
|
@observer
|
||||||
export class OverviewStatuses extends React.Component {
|
export class OverviewStatuses extends React.Component {
|
||||||
render() {
|
render() {
|
||||||
const { allowedResources } = configStore;
|
|
||||||
const { contextNs } = namespaceStore;
|
const { contextNs } = namespaceStore;
|
||||||
const pods = isAllowedResource("pods") ? podsStore.getAllByNs(contextNs) : [];
|
const pods = isAllowedResource("pods") ? podsStore.getAllByNs(contextNs) : [];
|
||||||
const deployments = isAllowedResource("deployments") ? deploymentStore.getAllByNs(contextNs) : [];
|
const deployments = isAllowedResource("deployments") ? deploymentStore.getAllByNs(contextNs) : [];
|
||||||
@ -37,37 +35,37 @@ export class OverviewStatuses extends React.Component {
|
|||||||
</div>
|
</div>
|
||||||
<PageFiltersList/>
|
<PageFiltersList/>
|
||||||
<div className="workloads">
|
<div className="workloads">
|
||||||
{ isAllowedResource("pods") &&
|
{isAllowedResource("pods") &&
|
||||||
<div className="workload">
|
<div className="workload">
|
||||||
<div className="title"><Link to={podsURL()}><Trans>Pods</Trans> ({pods.length})</Link></div>
|
<div className="title"><Link to={podsURL()}><Trans>Pods</Trans> ({pods.length})</Link></div>
|
||||||
<OverviewWorkloadStatus status={podsStore.getStatuses(pods)}/>
|
<OverviewWorkloadStatus status={podsStore.getStatuses(pods)}/>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
{ isAllowedResource("deployments") &&
|
{isAllowedResource("deployments") &&
|
||||||
<div className="workload">
|
<div className="workload">
|
||||||
<div className="title"><Link to={deploymentsURL()}><Trans>Deployments</Trans> ({deployments.length})</Link></div>
|
<div className="title"><Link to={deploymentsURL()}><Trans>Deployments</Trans> ({deployments.length})</Link></div>
|
||||||
<OverviewWorkloadStatus status={deploymentStore.getStatuses(deployments)}/>
|
<OverviewWorkloadStatus status={deploymentStore.getStatuses(deployments)}/>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
{ isAllowedResource("statefulsets") &&
|
{isAllowedResource("statefulsets") &&
|
||||||
<div className="workload">
|
<div className="workload">
|
||||||
<div className="title"><Link to={statefulSetsURL()}><Trans>StatefulSets</Trans> ({statefulSets.length})</Link></div>
|
<div className="title"><Link to={statefulSetsURL()}><Trans>StatefulSets</Trans> ({statefulSets.length})</Link></div>
|
||||||
<OverviewWorkloadStatus status={statefulSetStore.getStatuses(statefulSets)}/>
|
<OverviewWorkloadStatus status={statefulSetStore.getStatuses(statefulSets)}/>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
{ isAllowedResource("daemonsets") &&
|
{isAllowedResource("daemonsets") &&
|
||||||
<div className="workload">
|
<div className="workload">
|
||||||
<div className="title"><Link to={daemonSetsURL()}><Trans>DaemonSets</Trans> ({daemonSets.length})</Link></div>
|
<div className="title"><Link to={daemonSetsURL()}><Trans>DaemonSets</Trans> ({daemonSets.length})</Link></div>
|
||||||
<OverviewWorkloadStatus status={daemonSetStore.getStatuses(daemonSets)}/>
|
<OverviewWorkloadStatus status={daemonSetStore.getStatuses(daemonSets)}/>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
{ isAllowedResource("jobs") &&
|
{isAllowedResource("jobs") &&
|
||||||
<div className="workload">
|
<div className="workload">
|
||||||
<div className="title"><Link to={jobsURL()}><Trans>Jobs</Trans> ({jobs.length})</Link></div>
|
<div className="title"><Link to={jobsURL()}><Trans>Jobs</Trans> ({jobs.length})</Link></div>
|
||||||
<OverviewWorkloadStatus status={jobStore.getStatuses(jobs)}/>
|
<OverviewWorkloadStatus status={jobStore.getStatuses(jobs)}/>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
{ isAllowedResource("cronjobs") &&
|
{isAllowedResource("cronjobs") &&
|
||||||
<div className="workload">
|
<div className="workload">
|
||||||
<div className="title"><Link to={cronJobsURL()}><Trans>CronJobs</Trans> ({cronJobs.length})</Link></div>
|
<div className="title"><Link to={cronJobsURL()}><Trans>CronJobs</Trans> ({cronJobs.length})</Link></div>
|
||||||
<OverviewWorkloadStatus status={cronJobStore.getStatuses(cronJobs)}/>
|
<OverviewWorkloadStatus status={cronJobStore.getStatuses(cronJobs)}/>
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
import "./app.scss";
|
import "./app.scss";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { disposeOnUnmount, observer } from "mobx-react";
|
import { disposeOnUnmount, observer } from "mobx-react";
|
||||||
|
import { computed, observable, reaction } from "mobx";
|
||||||
import { Redirect, Route, Switch } from "react-router";
|
import { Redirect, Route, Switch } from "react-router";
|
||||||
import { Notifications } from "./notifications";
|
import { Notifications } from "./notifications";
|
||||||
import { NotFound } from "./+404";
|
import { NotFound } from "./+404";
|
||||||
@ -31,14 +32,12 @@ import { LandingPage, landingRoute, landingURL } from "./+landing-page";
|
|||||||
import { ClusterSettings, clusterSettingsRoute } from "./+cluster-settings";
|
import { ClusterSettings, clusterSettingsRoute } from "./+cluster-settings";
|
||||||
import { Workspaces, workspacesRoute } from "./+workspaces";
|
import { Workspaces, workspacesRoute } from "./+workspaces";
|
||||||
import { ErrorBoundary } from "./error-boundary";
|
import { ErrorBoundary } from "./error-boundary";
|
||||||
import { computed, observable, reaction } from "mobx";
|
import { navigation } from "../navigation";
|
||||||
import { configStore } from "../config.store";
|
|
||||||
import { clusterIpc } from "../../common/cluster-ipc";
|
import { clusterIpc } from "../../common/cluster-ipc";
|
||||||
import { clusterStore } from "../../common/cluster-store";
|
import { clusterStore } from "../../common/cluster-store";
|
||||||
import { ClusterStatus } from "./cluster-manager/cluster-status";
|
|
||||||
import { clusterStatusRoute, clusterStatusURL } from "./cluster-manager/cluster-status.route";
|
import { clusterStatusRoute, clusterStatusURL } from "./cluster-manager/cluster-status.route";
|
||||||
import { Preferences, preferencesRoute } from "./+preferences";
|
import { Preferences, preferencesRoute } from "./+preferences";
|
||||||
import { navigation } from "../navigation";
|
import { ClusterStatus } from "./cluster-manager/cluster-status";
|
||||||
import { CubeSpinner } from "./spinner";
|
import { CubeSpinner } from "./spinner";
|
||||||
|
|
||||||
@observer
|
@observer
|
||||||
@ -52,7 +51,6 @@ export class App extends React.Component {
|
|||||||
|
|
||||||
async componentDidMount() {
|
async componentDidMount() {
|
||||||
await clusterIpc.activate.invokeFromRenderer();
|
await clusterIpc.activate.invokeFromRenderer();
|
||||||
await configStore.init();
|
|
||||||
this.appReady = true;
|
this.appReady = true;
|
||||||
|
|
||||||
disposeOnUnmount(this, [
|
disposeOnUnmount(this, [
|
||||||
|
|||||||
@ -6,7 +6,6 @@ import { TerminalApi } from "../../api/terminal-api";
|
|||||||
import { dockStore, IDockTab, TabId, TabKind } from "./dock.store";
|
import { dockStore, IDockTab, TabId, TabKind } from "./dock.store";
|
||||||
import { WebSocketApiState } from "../../api/websocket-api";
|
import { WebSocketApiState } from "../../api/websocket-api";
|
||||||
import { _i18n } from "../../i18n";
|
import { _i18n } from "../../i18n";
|
||||||
import { themeStore } from "../../theme.store";
|
|
||||||
|
|
||||||
export interface ITerminalTab extends IDockTab {
|
export interface ITerminalTab extends IDockTab {
|
||||||
node?: string; // activate node shell mode
|
node?: string; // activate node shell mode
|
||||||
@ -16,7 +15,6 @@ export function isTerminalTab(tab: IDockTab) {
|
|||||||
return tab && tab.kind === TabKind.TERMINAL;
|
return tab && tab.kind === TabKind.TERMINAL;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export function createTerminalTab(tabParams: Partial<ITerminalTab> = {}) {
|
export function createTerminalTab(tabParams: Partial<ITerminalTab> = {}) {
|
||||||
return dockStore.createTab({
|
return dockStore.createTab({
|
||||||
kind: TabKind.TERMINAL,
|
kind: TabKind.TERMINAL,
|
||||||
@ -56,7 +54,6 @@ export class TerminalStore {
|
|||||||
const api = new TerminalApi({
|
const api = new TerminalApi({
|
||||||
id: tabId,
|
id: tabId,
|
||||||
node: tab.node,
|
node: tab.node,
|
||||||
colorTheme: themeStore.activeTheme.type
|
|
||||||
});
|
});
|
||||||
const terminal = new Terminal(tabId, api);
|
const terminal = new Terminal(tabId, api);
|
||||||
this.connections.set(tabId, api);
|
this.connections.set(tabId, api);
|
||||||
|
|||||||
@ -19,7 +19,6 @@ import { PageFiltersList } from "./page-filters-list";
|
|||||||
import { PageFiltersSelect } from "./page-filters-select";
|
import { PageFiltersSelect } from "./page-filters-select";
|
||||||
import { NamespaceSelectFilter } from "../+namespaces/namespace-select";
|
import { NamespaceSelectFilter } from "../+namespaces/namespace-select";
|
||||||
import { themeStore } from "../../theme.store";
|
import { themeStore } from "../../theme.store";
|
||||||
import { configStore } from "../../config.store";
|
|
||||||
|
|
||||||
// todo: refactor, split to small re-usable components
|
// todo: refactor, split to small re-usable components
|
||||||
|
|
||||||
@ -116,7 +115,6 @@ export class ItemListLayout extends React.Component<ItemListLayoutProps> {
|
|||||||
const stores = [store, ...dependentStores];
|
const stores = [store, ...dependentStores];
|
||||||
if (!isClusterScoped) stores.push(namespaceStore);
|
if (!isClusterScoped) stores.push(namespaceStore);
|
||||||
try {
|
try {
|
||||||
await when(() => configStore.isLoaded); // todo: remove
|
|
||||||
await Promise.all(stores.map(store => store.loadAll()));
|
await Promise.all(stores.map(store => store.loadAll()));
|
||||||
const subscriptions = stores.map(store => store.subscribe());
|
const subscriptions = stores.map(store => store.subscribe());
|
||||||
await when(() => this.isUnmounting);
|
await when(() => this.isUnmounting);
|
||||||
|
|||||||
@ -7,11 +7,11 @@ import { matchPath, RouteProps } from "react-router-dom";
|
|||||||
import { createStorage, cssNames } from "../../utils";
|
import { createStorage, cssNames } from "../../utils";
|
||||||
import { Tab, Tabs } from "../tabs";
|
import { Tab, Tabs } from "../tabs";
|
||||||
import { Sidebar } from "./sidebar";
|
import { Sidebar } from "./sidebar";
|
||||||
import { configStore } from "../../config.store";
|
|
||||||
import { ErrorBoundary } from "../error-boundary";
|
import { ErrorBoundary } from "../error-boundary";
|
||||||
import { Dock } from "../dock";
|
import { Dock } from "../dock";
|
||||||
import { navigate, navigation } from "../../navigation";
|
import { navigate, navigation } from "../../navigation";
|
||||||
import { themeStore } from "../../theme.store";
|
import { themeStore } from "../../theme.store";
|
||||||
|
import { clusterStore } from "../../../common/cluster-store";
|
||||||
|
|
||||||
export interface TabRoute extends RouteProps {
|
export interface TabRoute extends RouteProps {
|
||||||
title: React.ReactNode;
|
title: React.ReactNode;
|
||||||
@ -47,14 +47,12 @@ export class MainLayout extends React.Component<Props> {
|
|||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { className, contentClass, headerClass, tabs, footer, footerClass, children } = this.props;
|
const { className, contentClass, headerClass, tabs, footer, footerClass, children } = this.props;
|
||||||
const { clusterName } = configStore.config;
|
const clusterName = clusterStore.activeCluster?.contextName;
|
||||||
const { pathname } = navigation.location;
|
const routePath = navigation.location.pathname;
|
||||||
return (
|
return (
|
||||||
<div className={cssNames("MainLayout", className, themeStore.activeTheme.type)}>
|
<div className={cssNames("MainLayout", className, themeStore.activeTheme.type)}>
|
||||||
<header className={cssNames("flex gaps align-center", headerClass)}>
|
<header className={cssNames("flex gaps align-center", headerClass)}>
|
||||||
<div className="box grow flex align-center">
|
<span className="cluster">{clusterName}</span>
|
||||||
{clusterName && <span>{clusterName}</span>}
|
|
||||||
</div>
|
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<aside className={cssNames("flex column", { pinned: this.isPinned, accessible: this.isAccessible })}>
|
<aside className={cssNames("flex column", { pinned: this.isPinned, accessible: this.isAccessible })}>
|
||||||
@ -68,7 +66,7 @@ export class MainLayout extends React.Component<Props> {
|
|||||||
{tabs && (
|
{tabs && (
|
||||||
<Tabs center onChange={url => navigate(url)}>
|
<Tabs center onChange={url => navigate(url)}>
|
||||||
{tabs.map(({ title, path, url, ...routeProps }) => {
|
{tabs.map(({ title, path, url, ...routeProps }) => {
|
||||||
const isActive = !!matchPath(pathname, { path, ...routeProps });
|
const isActive = !!matchPath(routePath, { path, ...routeProps });
|
||||||
return <Tab key={url} label={title} value={url} active={isActive}/>
|
return <Tab key={url} label={title} value={url} active={isActive}/>
|
||||||
})}
|
})}
|
||||||
</Tabs>
|
</Tabs>
|
||||||
|
|||||||
@ -1,51 +0,0 @@
|
|||||||
import type { IConfigRoutePayload } from "../main/routes/config-route";
|
|
||||||
import { observable, when } from "mobx";
|
|
||||||
import { autobind, interval } from "./utils";
|
|
||||||
import { apiBase } from "./api";
|
|
||||||
|
|
||||||
@autobind()
|
|
||||||
export class ConfigStore {
|
|
||||||
protected updater = interval(60, this.load);
|
|
||||||
|
|
||||||
@observable config: Partial<IConfigRoutePayload> = {};
|
|
||||||
@observable isLoaded = false;
|
|
||||||
|
|
||||||
async init() {
|
|
||||||
await this.load();
|
|
||||||
this.updater.start();
|
|
||||||
}
|
|
||||||
|
|
||||||
load() {
|
|
||||||
if (location.hostname === "no-clusters.localhost") {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
return apiBase.get("/config").then((config: IConfigRoutePayload) => {
|
|
||||||
this.config = config;
|
|
||||||
this.isLoaded = true;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async getToken() {
|
|
||||||
await when(() => this.isLoaded);
|
|
||||||
return this.config.token;
|
|
||||||
}
|
|
||||||
|
|
||||||
get allowedNamespaces() {
|
|
||||||
return this.config.allowedNamespaces || [];
|
|
||||||
}
|
|
||||||
|
|
||||||
get allowedResources() {
|
|
||||||
return this.config.allowedResources || [];
|
|
||||||
}
|
|
||||||
|
|
||||||
get isClusterAdmin() {
|
|
||||||
return this.config.isClusterAdmin;
|
|
||||||
}
|
|
||||||
|
|
||||||
reset() {
|
|
||||||
this.isLoaded = false;
|
|
||||||
this.config = {};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const configStore = new ConfigStore();
|
|
||||||
@ -3,10 +3,10 @@ import { autobind } from "./utils";
|
|||||||
import { KubeObject } from "./api/kube-object";
|
import { KubeObject } from "./api/kube-object";
|
||||||
import { IKubeWatchEvent, kubeWatchApi } from "./api/kube-watch-api";
|
import { IKubeWatchEvent, kubeWatchApi } from "./api/kube-watch-api";
|
||||||
import { ItemStore } from "./item.store";
|
import { ItemStore } from "./item.store";
|
||||||
import { configStore } from "./config.store";
|
|
||||||
import { apiManager } from "./api/api-manager";
|
import { apiManager } from "./api/api-manager";
|
||||||
import { IKubeApiQueryParams, KubeApi } from "./api/kube-api";
|
import { IKubeApiQueryParams, KubeApi } from "./api/kube-api";
|
||||||
import { KubeJsonApiData } from "./api/kube-json-api";
|
import { KubeJsonApiData } from "./api/kube-json-api";
|
||||||
|
import { clusterStore } from "../common/cluster-store";
|
||||||
|
|
||||||
@autobind()
|
@autobind()
|
||||||
export abstract class KubeObjectStore<T extends KubeObject = any> extends ItemStore<T> {
|
export abstract class KubeObjectStore<T extends KubeObject = any> extends ItemStore<T> {
|
||||||
@ -23,8 +23,7 @@ export abstract class KubeObjectStore<T extends KubeObject = any> extends ItemSt
|
|||||||
const namespaces: string[] = [].concat(namespace);
|
const namespaces: string[] = [].concat(namespace);
|
||||||
if (namespaces.length) {
|
if (namespaces.length) {
|
||||||
return this.items.filter(item => namespaces.includes(item.getNs()));
|
return this.items.filter(item => namespaces.includes(item.getNs()));
|
||||||
}
|
} else if (!strict) {
|
||||||
else if (!strict) {
|
|
||||||
return this.items;
|
return this.items;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -47,8 +46,7 @@ export abstract class KubeObjectStore<T extends KubeObject = any> extends ItemSt
|
|||||||
const itemLabels = item.getLabels();
|
const itemLabels = item.getLabels();
|
||||||
return labels.every(label => itemLabels.includes(label));
|
return labels.every(label => itemLabels.includes(label));
|
||||||
})
|
})
|
||||||
}
|
} else {
|
||||||
else {
|
|
||||||
return this.items.filter((item: T) => {
|
return this.items.filter((item: T) => {
|
||||||
const itemLabels = item.metadata.labels || {};
|
const itemLabels = item.metadata.labels || {};
|
||||||
return Object.entries(labels)
|
return Object.entries(labels)
|
||||||
@ -62,8 +60,7 @@ export abstract class KubeObjectStore<T extends KubeObject = any> extends ItemSt
|
|||||||
const { limit } = this;
|
const { limit } = this;
|
||||||
const query: IKubeApiQueryParams = limit ? { limit } : {};
|
const query: IKubeApiQueryParams = limit ? { limit } : {};
|
||||||
return this.api.list({}, query);
|
return this.api.list({}, query);
|
||||||
}
|
} else {
|
||||||
else {
|
|
||||||
return Promise
|
return Promise
|
||||||
.all(allowedNamespaces.map(namespace => this.api.list({ namespace })))
|
.all(allowedNamespaces.map(namespace => this.api.list({ namespace })))
|
||||||
.then(items => items.flat())
|
.then(items => items.flat())
|
||||||
@ -79,8 +76,8 @@ export abstract class KubeObjectStore<T extends KubeObject = any> extends ItemSt
|
|||||||
this.isLoading = true;
|
this.isLoading = true;
|
||||||
let items: T[];
|
let items: T[];
|
||||||
try {
|
try {
|
||||||
const { isClusterAdmin, allowedNamespaces } = configStore;
|
const { isAdmin, allowedNamespaces } = clusterStore.activeCluster;
|
||||||
items = await this.loadItems(!isClusterAdmin ? allowedNamespaces : null);
|
items = await this.loadItems(!isAdmin ? allowedNamespaces : null);
|
||||||
items = this.filterItemsOnLoad(items);
|
items = this.filterItemsOnLoad(items);
|
||||||
} finally {
|
} finally {
|
||||||
if (items) {
|
if (items) {
|
||||||
@ -180,8 +177,7 @@ export abstract class KubeObjectStore<T extends KubeObject = any> extends ItemSt
|
|||||||
const newItem = new api.objectConstructor(object);
|
const newItem = new api.objectConstructor(object);
|
||||||
if (!item) {
|
if (!item) {
|
||||||
items.push(newItem);
|
items.push(newItem);
|
||||||
}
|
} else {
|
||||||
else {
|
|
||||||
items.splice(index, 1, newItem);
|
items.splice(index, 1, newItem);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user