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 { FeatureStatusMap } from "./feature"
|
||||
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 { broadcastIpc } from "../common/ipc";
|
||||
import { ContextHandler } from "./context-handler"
|
||||
@ -20,17 +20,19 @@ export enum ClusterStatus {
|
||||
}
|
||||
|
||||
export interface ClusterState extends ClusterModel {
|
||||
initialized?: boolean;
|
||||
initialized: boolean;
|
||||
apiUrl: string;
|
||||
online?: boolean;
|
||||
accessible?: boolean;
|
||||
failureReason?: string;
|
||||
nodes?: number;
|
||||
eventCount?: number;
|
||||
version?: string;
|
||||
distribution?: string;
|
||||
isAdmin?: boolean;
|
||||
features?: FeatureStatusMap;
|
||||
online: boolean;
|
||||
accessible: boolean;
|
||||
failureReason: string;
|
||||
nodes: number;
|
||||
eventCount: number;
|
||||
version: string;
|
||||
distribution: string;
|
||||
isAdmin: boolean;
|
||||
allowedNamespaces: string[]
|
||||
allowedResources: string[]
|
||||
features: FeatureStatusMap;
|
||||
}
|
||||
|
||||
export class Cluster implements ClusterModel {
|
||||
@ -58,6 +60,8 @@ export class Cluster implements ClusterModel {
|
||||
@observable eventCount = 0;
|
||||
@observable preferences: ClusterPreferences = {};
|
||||
@observable features: FeatureStatusMap = {};
|
||||
@observable allowedNamespaces: string[] = [];
|
||||
@observable allowedResources: string[] = [];
|
||||
|
||||
constructor(model: ClusterModel) {
|
||||
this.updateModel(model);
|
||||
@ -120,7 +124,6 @@ export class Cluster implements ClusterModel {
|
||||
}
|
||||
|
||||
async activate() {
|
||||
await when(() => this.initialized);
|
||||
if (this.disconnected) await this.reconnect();
|
||||
await this.refresh();
|
||||
return this.pushState();
|
||||
@ -150,14 +153,28 @@ export class Cluster implements ClusterModel {
|
||||
this.online = connectionStatus > ClusterStatus.Offline;
|
||||
this.accessible = connectionStatus == ClusterStatus.AccessGranted;
|
||||
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.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
|
||||
@ -342,6 +359,8 @@ export class Cluster implements ClusterModel {
|
||||
isAdmin: this.isAdmin,
|
||||
features: this.features,
|
||||
eventCount: this.eventCount,
|
||||
allowedNamespaces: this.allowedNamespaces,
|
||||
allowedResources: this.allowedResources,
|
||||
};
|
||||
return toJS(state, {
|
||||
recurseEverything: true
|
||||
@ -368,4 +387,67 @@ export class Cluster implements ClusterModel {
|
||||
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 { Cluster } from "./cluster"
|
||||
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 {
|
||||
req: http.IncomingMessage;
|
||||
@ -112,7 +112,6 @@ export class Router {
|
||||
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))
|
||||
|
||||
// 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 "./metrics-route"
|
||||
export * from "./port-forward-route"
|
||||
|
||||
@ -6,9 +6,9 @@ import { autobind, EventEmitter } from "../utils";
|
||||
import { KubeJsonApiData } from "./kube-json-api";
|
||||
import type { KubeObjectStore } from "../kube-object.store";
|
||||
import { KubeApi } from "./kube-api";
|
||||
import { configStore } from "../config.store";
|
||||
import { apiManager } from "./api-manager";
|
||||
import { apiPrefix, isDevelopment } from "../../common/vars";
|
||||
import { clusterStore } from "../../common/cluster-store";
|
||||
|
||||
export interface IKubeWatchEvent<T = any> {
|
||||
type: "ADDED" | "MODIFIED" | "DELETED";
|
||||
@ -61,10 +61,10 @@ export class KubeWatchApi {
|
||||
}
|
||||
|
||||
protected getQuery(): Partial<IKubeWatchRouteQuery> {
|
||||
const { isClusterAdmin, allowedNamespaces } = configStore;
|
||||
const { isAdmin, allowedNamespaces } = clusterStore.activeCluster;
|
||||
return {
|
||||
api: this.activeApis.map(api => {
|
||||
if (isClusterAdmin) return api.getWatchUrl();
|
||||
if (isAdmin) return api.getWatchUrl();
|
||||
return allowedNamespaces.map(namespace => api.getWatchUrl(namespace))
|
||||
}).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[]) {
|
||||
if (!Array.isArray(resources)) {
|
||||
resources = [resources];
|
||||
}
|
||||
const { allowedResources } = configStore;
|
||||
const allowedResources = clusterStore.activeCluster?.allowedResources || [];
|
||||
for (const resource of resources) {
|
||||
if (!allowedResources.includes(resource)) {
|
||||
return false;
|
||||
|
||||
@ -1,7 +1,6 @@
|
||||
import { stringify } from "querystring";
|
||||
import { autobind, base64, EventEmitter, interval } from "../utils";
|
||||
import { autobind, base64, EventEmitter } from "../utils";
|
||||
import { WebSocketApi } from "./websocket-api";
|
||||
import { configStore } from "../config.store";
|
||||
import isEqual from "lodash/isEqual"
|
||||
import { isDevelopment } from "../../common/vars";
|
||||
|
||||
@ -25,21 +24,19 @@ enum TerminalColor {
|
||||
NO_COLOR = "\u001b[0m",
|
||||
}
|
||||
|
||||
export interface ITerminalApiOptions {
|
||||
export type TerminalApiQuery = Record<string, string> & {
|
||||
id: string;
|
||||
node?: string;
|
||||
colorTheme?: "light" | "dark";
|
||||
type?: string | "node";
|
||||
}
|
||||
|
||||
export class TerminalApi extends WebSocketApi {
|
||||
protected size: { Width: number; Height: number };
|
||||
protected currentToken: string;
|
||||
protected tokenInterval = interval(60, this.sendNewToken); // refresh every minute
|
||||
|
||||
public onReady = new EventEmitter<[]>();
|
||||
public isReady = false;
|
||||
|
||||
constructor(protected options: ITerminalApiOptions) {
|
||||
constructor(protected options: TerminalApiQuery) {
|
||||
super({
|
||||
logging: isDevelopment,
|
||||
flushOnOpen: false,
|
||||
@ -47,50 +44,33 @@ export class TerminalApi extends WebSocketApi {
|
||||
});
|
||||
}
|
||||
|
||||
async getUrl(token: string) {
|
||||
const { hostname, protocol } = location;
|
||||
async getUrl() {
|
||||
let { port } = location;
|
||||
const { hostname, protocol } = location;
|
||||
const { id, node } = this.options;
|
||||
const wss = `ws${protocol === "https:" ? "s" : ""}://`;
|
||||
const queryParams = { token, id };
|
||||
const query: TerminalApiQuery = { id };
|
||||
if (port) {
|
||||
port = `:${port}`
|
||||
}
|
||||
if (node) {
|
||||
Object.assign(queryParams, {
|
||||
node: node,
|
||||
type: "node"
|
||||
});
|
||||
query.node = node;
|
||||
query.type = "node";
|
||||
}
|
||||
return `${wss}${hostname}${port}/api?${stringify(queryParams)}`;
|
||||
return `${wss}${hostname}${port}/api?${stringify(query)}`;
|
||||
}
|
||||
|
||||
async connect() {
|
||||
const token = await configStore.getToken();
|
||||
const apiUrl = await this.getUrl(token);
|
||||
const { colorTheme } = this.options;
|
||||
this.emitStatus("Connecting ...", {
|
||||
color: colorTheme == "light" ? TerminalColor.GRAY : TerminalColor.LIGHT_GRAY
|
||||
});
|
||||
const apiUrl = await this.getUrl();
|
||||
this.emitStatus("Connecting ...");
|
||||
this.onData.addListener(this._onReady, { prepend: true });
|
||||
this.currentToken = token;
|
||||
this.tokenInterval.start();
|
||||
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() {
|
||||
if (!this.socket) return;
|
||||
const exitCode = String.fromCharCode(4); // ctrl+d
|
||||
this.sendCommand(exitCode);
|
||||
this.tokenInterval.stop();
|
||||
setTimeout(() => super.destroy(), 2000);
|
||||
}
|
||||
|
||||
|
||||
@ -1,11 +1,11 @@
|
||||
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 { HelmRelease, helmReleasesApi, IReleaseCreatePayload, IReleaseUpdatePayload } from "../../api/endpoints/helm-releases.api";
|
||||
import { ItemStore } from "../../item.store";
|
||||
import { configStore } from "../../config.store";
|
||||
import { secretsStore } from "../+config-secrets/secrets.store";
|
||||
import { Secret } from "../../api/endpoints";
|
||||
import { secretsStore } from "../+config-secrets/secrets.store";
|
||||
import { clusterStore } from "../../../common/cluster-store";
|
||||
|
||||
@autobind()
|
||||
export class ReleaseStore extends ItemStore<HelmRelease> {
|
||||
@ -58,8 +58,8 @@ export class ReleaseStore extends ItemStore<HelmRelease> {
|
||||
this.isLoading = true;
|
||||
let items;
|
||||
try {
|
||||
const { isClusterAdmin, allowedNamespaces } = configStore;
|
||||
items = await this.loadItems(!isClusterAdmin ? allowedNamespaces : null);
|
||||
const { isAdmin, allowedNamespaces } = clusterStore.activeCluster;
|
||||
items = await this.loadItems(!isAdmin ? allowedNamespaces : null);
|
||||
} finally {
|
||||
if (items) {
|
||||
items = this.sortItems(items);
|
||||
@ -73,8 +73,7 @@ export class ReleaseStore extends ItemStore<HelmRelease> {
|
||||
async loadItems(namespaces?: string[]) {
|
||||
if (!namespaces) {
|
||||
return helmReleasesApi.list();
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
return Promise
|
||||
.all(namespaces.map(namespace => helmReleasesApi.list(namespace)))
|
||||
.then(items => items.flat());
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import "./cluster.scss"
|
||||
|
||||
import React from "react";
|
||||
import { computed, reaction, when } from "mobx";
|
||||
import { computed, reaction } from "mobx";
|
||||
import { disposeOnUnmount, observer } from "mobx-react";
|
||||
import { MainLayout } from "../layout/main-layout";
|
||||
import { ClusterIssues } from "./cluster-issues";
|
||||
@ -13,22 +13,23 @@ import { nodesStore } from "../+nodes/nodes.store";
|
||||
import { podsStore } from "../+workloads-pods/pods.store";
|
||||
import { clusterStore } from "./cluster.store";
|
||||
import { eventStore } from "../+events/event.store";
|
||||
import { configStore } from "../../config.store";
|
||||
import { isAllowedResource } from "../../api/rbac";
|
||||
|
||||
@observer
|
||||
export class Cluster extends React.Component {
|
||||
private dependentStores = [nodesStore, podsStore];
|
||||
|
||||
private watchers = [
|
||||
interval(60, () => clusterStore.getMetrics()),
|
||||
interval(20, () => eventStore.loadAll())
|
||||
];
|
||||
|
||||
private dependentStores = [nodesStore, podsStore];
|
||||
@computed get isLoaded() {
|
||||
return nodesStore.isLoaded && podsStore.isLoaded
|
||||
}
|
||||
|
||||
// todo: refactor
|
||||
async componentDidMount() {
|
||||
await when(() => configStore.isLoaded);
|
||||
|
||||
const { dependentStores } = this;
|
||||
if (!isAllowedResource("nodes")) {
|
||||
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() {
|
||||
const { isLoaded } = this;
|
||||
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>) => {
|
||||
const isAdded = this.helmAddedRepos.has(repo.name);
|
||||
if (isAdded) {
|
||||
|
||||
@ -9,9 +9,9 @@ import { MainLayout, TabRoute } from "../layout/main-layout";
|
||||
import { PersistentVolumes, volumesRoute, volumesURL } from "../+storage-volumes";
|
||||
import { StorageClasses, storageClassesRoute, storageClassesURL } from "../+storage-classes";
|
||||
import { PersistentVolumeClaims, volumeClaimsRoute, volumeClaimsURL } from "../+storage-volume-claims";
|
||||
import { configStore } from "../../config.store";
|
||||
import { namespaceStore } from "../+namespaces/namespace.store";
|
||||
import { storageURL } from "./storage.route";
|
||||
import { isAllowedResource } from "../../api/rbac";
|
||||
|
||||
interface Props extends RouteComponentProps<{}> {
|
||||
}
|
||||
@ -20,7 +20,6 @@ interface Props extends RouteComponentProps<{}> {
|
||||
export class Storage extends React.Component<Props> {
|
||||
static get tabRoutes() {
|
||||
const tabRoutes: TabRoute[] = [];
|
||||
const { allowedResources } = configStore;
|
||||
const query = namespaceStore.getContextParams()
|
||||
|
||||
tabRoutes.push({
|
||||
@ -30,7 +29,7 @@ export class Storage extends React.Component<Props> {
|
||||
path: volumeClaimsRoute.path,
|
||||
})
|
||||
|
||||
if (allowedResources.includes('persistentvolumes')) {
|
||||
if (isAllowedResource('persistentvolumes')) {
|
||||
tabRoutes.push({
|
||||
title: <Trans>Persistent Volumes</Trans>,
|
||||
component: PersistentVolumes,
|
||||
@ -39,7 +38,7 @@ export class Storage extends React.Component<Props> {
|
||||
});
|
||||
}
|
||||
|
||||
if (allowedResources.includes('storageclasses')) {
|
||||
if (isAllowedResource('storageclasses')) {
|
||||
tabRoutes.push({
|
||||
title: <Trans>Storage Classes</Trans>,
|
||||
component: StorageClasses,
|
||||
|
||||
@ -1,5 +1,4 @@
|
||||
import "./user-management.scss"
|
||||
|
||||
import React from "react";
|
||||
import { observer } from "mobx-react";
|
||||
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 { roleBindingsRoute, roleBindingsURL, rolesRoute, rolesURL, serviceAccountsRoute, serviceAccountsURL, usersManagementURL } from "./user-management.routes";
|
||||
import { namespaceStore } from "../+namespaces/namespace.store";
|
||||
import { configStore } from "../../config.store";
|
||||
import { PodSecurityPolicies, podSecurityPoliciesRoute, podSecurityPoliciesURL } from "../+pod-security-policies";
|
||||
import { isAllowedResource } from "../../api/rbac";
|
||||
|
||||
interface Props extends RouteComponentProps<{}> {
|
||||
}
|
||||
@ -21,7 +20,6 @@ interface Props extends RouteComponentProps<{}> {
|
||||
export class UserManagement extends React.Component<Props> {
|
||||
static get tabRoutes() {
|
||||
const tabRoutes: TabRoute[] = [];
|
||||
const { allowedResources } = configStore;
|
||||
const query = namespaceStore.getContextParams()
|
||||
tabRoutes.push(
|
||||
{
|
||||
@ -43,7 +41,7 @@ export class UserManagement extends React.Component<Props> {
|
||||
path: rolesRoute.path,
|
||||
},
|
||||
)
|
||||
if (allowedResources.includes("podsecuritypolicies")) {
|
||||
if (isAllowedResource("podsecuritypolicies")) {
|
||||
tabRoutes.push({
|
||||
title: <Trans>Pod Security Policies</Trans>,
|
||||
component: PodSecurityPolicies,
|
||||
|
||||
@ -15,13 +15,11 @@ import { cronJobStore } from "../+workloads-cronjobs/cronjob.store";
|
||||
import { namespaceStore } from "../+namespaces/namespace.store";
|
||||
import { PageFiltersList } from "../item-object-list/page-filters-list";
|
||||
import { NamespaceSelectFilter } from "../+namespaces/namespace-select";
|
||||
import { configStore } from "../../config.store";
|
||||
import { isAllowedResource } from "../../api/rbac";
|
||||
|
||||
@observer
|
||||
export class OverviewStatuses extends React.Component {
|
||||
render() {
|
||||
const { allowedResources } = configStore;
|
||||
const { contextNs } = namespaceStore;
|
||||
const pods = isAllowedResource("pods") ? podsStore.getAllByNs(contextNs) : [];
|
||||
const deployments = isAllowedResource("deployments") ? deploymentStore.getAllByNs(contextNs) : [];
|
||||
@ -37,37 +35,37 @@ export class OverviewStatuses extends React.Component {
|
||||
</div>
|
||||
<PageFiltersList/>
|
||||
<div className="workloads">
|
||||
{ isAllowedResource("pods") &&
|
||||
{isAllowedResource("pods") &&
|
||||
<div className="workload">
|
||||
<div className="title"><Link to={podsURL()}><Trans>Pods</Trans> ({pods.length})</Link></div>
|
||||
<OverviewWorkloadStatus status={podsStore.getStatuses(pods)}/>
|
||||
</div>
|
||||
}
|
||||
{ isAllowedResource("deployments") &&
|
||||
{isAllowedResource("deployments") &&
|
||||
<div className="workload">
|
||||
<div className="title"><Link to={deploymentsURL()}><Trans>Deployments</Trans> ({deployments.length})</Link></div>
|
||||
<OverviewWorkloadStatus status={deploymentStore.getStatuses(deployments)}/>
|
||||
</div>
|
||||
}
|
||||
{ isAllowedResource("statefulsets") &&
|
||||
{isAllowedResource("statefulsets") &&
|
||||
<div className="workload">
|
||||
<div className="title"><Link to={statefulSetsURL()}><Trans>StatefulSets</Trans> ({statefulSets.length})</Link></div>
|
||||
<OverviewWorkloadStatus status={statefulSetStore.getStatuses(statefulSets)}/>
|
||||
</div>
|
||||
}
|
||||
{ isAllowedResource("daemonsets") &&
|
||||
{isAllowedResource("daemonsets") &&
|
||||
<div className="workload">
|
||||
<div className="title"><Link to={daemonSetsURL()}><Trans>DaemonSets</Trans> ({daemonSets.length})</Link></div>
|
||||
<OverviewWorkloadStatus status={daemonSetStore.getStatuses(daemonSets)}/>
|
||||
</div>
|
||||
}
|
||||
{ isAllowedResource("jobs") &&
|
||||
{isAllowedResource("jobs") &&
|
||||
<div className="workload">
|
||||
<div className="title"><Link to={jobsURL()}><Trans>Jobs</Trans> ({jobs.length})</Link></div>
|
||||
<OverviewWorkloadStatus status={jobStore.getStatuses(jobs)}/>
|
||||
</div>
|
||||
}
|
||||
{ isAllowedResource("cronjobs") &&
|
||||
{isAllowedResource("cronjobs") &&
|
||||
<div className="workload">
|
||||
<div className="title"><Link to={cronJobsURL()}><Trans>CronJobs</Trans> ({cronJobs.length})</Link></div>
|
||||
<OverviewWorkloadStatus status={cronJobStore.getStatuses(cronJobs)}/>
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import "./app.scss";
|
||||
import React from "react";
|
||||
import { disposeOnUnmount, observer } from "mobx-react";
|
||||
import { computed, observable, reaction } from "mobx";
|
||||
import { Redirect, Route, Switch } from "react-router";
|
||||
import { Notifications } from "./notifications";
|
||||
import { NotFound } from "./+404";
|
||||
@ -31,14 +32,12 @@ import { LandingPage, landingRoute, landingURL } from "./+landing-page";
|
||||
import { ClusterSettings, clusterSettingsRoute } from "./+cluster-settings";
|
||||
import { Workspaces, workspacesRoute } from "./+workspaces";
|
||||
import { ErrorBoundary } from "./error-boundary";
|
||||
import { computed, observable, reaction } from "mobx";
|
||||
import { configStore } from "../config.store";
|
||||
import { navigation } from "../navigation";
|
||||
import { clusterIpc } from "../../common/cluster-ipc";
|
||||
import { clusterStore } from "../../common/cluster-store";
|
||||
import { ClusterStatus } from "./cluster-manager/cluster-status";
|
||||
import { clusterStatusRoute, clusterStatusURL } from "./cluster-manager/cluster-status.route";
|
||||
import { Preferences, preferencesRoute } from "./+preferences";
|
||||
import { navigation } from "../navigation";
|
||||
import { ClusterStatus } from "./cluster-manager/cluster-status";
|
||||
import { CubeSpinner } from "./spinner";
|
||||
|
||||
@observer
|
||||
@ -52,7 +51,6 @@ export class App extends React.Component {
|
||||
|
||||
async componentDidMount() {
|
||||
await clusterIpc.activate.invokeFromRenderer();
|
||||
await configStore.init();
|
||||
this.appReady = true;
|
||||
|
||||
disposeOnUnmount(this, [
|
||||
|
||||
@ -6,7 +6,6 @@ import { TerminalApi } from "../../api/terminal-api";
|
||||
import { dockStore, IDockTab, TabId, TabKind } from "./dock.store";
|
||||
import { WebSocketApiState } from "../../api/websocket-api";
|
||||
import { _i18n } from "../../i18n";
|
||||
import { themeStore } from "../../theme.store";
|
||||
|
||||
export interface ITerminalTab extends IDockTab {
|
||||
node?: string; // activate node shell mode
|
||||
@ -16,7 +15,6 @@ export function isTerminalTab(tab: IDockTab) {
|
||||
return tab && tab.kind === TabKind.TERMINAL;
|
||||
}
|
||||
|
||||
|
||||
export function createTerminalTab(tabParams: Partial<ITerminalTab> = {}) {
|
||||
return dockStore.createTab({
|
||||
kind: TabKind.TERMINAL,
|
||||
@ -56,7 +54,6 @@ export class TerminalStore {
|
||||
const api = new TerminalApi({
|
||||
id: tabId,
|
||||
node: tab.node,
|
||||
colorTheme: themeStore.activeTheme.type
|
||||
});
|
||||
const terminal = new Terminal(tabId, api);
|
||||
this.connections.set(tabId, api);
|
||||
|
||||
@ -19,7 +19,6 @@ import { PageFiltersList } from "./page-filters-list";
|
||||
import { PageFiltersSelect } from "./page-filters-select";
|
||||
import { NamespaceSelectFilter } from "../+namespaces/namespace-select";
|
||||
import { themeStore } from "../../theme.store";
|
||||
import { configStore } from "../../config.store";
|
||||
|
||||
// todo: refactor, split to small re-usable components
|
||||
|
||||
@ -116,7 +115,6 @@ export class ItemListLayout extends React.Component<ItemListLayoutProps> {
|
||||
const stores = [store, ...dependentStores];
|
||||
if (!isClusterScoped) stores.push(namespaceStore);
|
||||
try {
|
||||
await when(() => configStore.isLoaded); // todo: remove
|
||||
await Promise.all(stores.map(store => store.loadAll()));
|
||||
const subscriptions = stores.map(store => store.subscribe());
|
||||
await when(() => this.isUnmounting);
|
||||
|
||||
@ -7,11 +7,11 @@ import { matchPath, RouteProps } from "react-router-dom";
|
||||
import { createStorage, cssNames } from "../../utils";
|
||||
import { Tab, Tabs } from "../tabs";
|
||||
import { Sidebar } from "./sidebar";
|
||||
import { configStore } from "../../config.store";
|
||||
import { ErrorBoundary } from "../error-boundary";
|
||||
import { Dock } from "../dock";
|
||||
import { navigate, navigation } from "../../navigation";
|
||||
import { themeStore } from "../../theme.store";
|
||||
import { clusterStore } from "../../../common/cluster-store";
|
||||
|
||||
export interface TabRoute extends RouteProps {
|
||||
title: React.ReactNode;
|
||||
@ -47,14 +47,12 @@ export class MainLayout extends React.Component<Props> {
|
||||
|
||||
render() {
|
||||
const { className, contentClass, headerClass, tabs, footer, footerClass, children } = this.props;
|
||||
const { clusterName } = configStore.config;
|
||||
const { pathname } = navigation.location;
|
||||
const clusterName = clusterStore.activeCluster?.contextName;
|
||||
const routePath = navigation.location.pathname;
|
||||
return (
|
||||
<div className={cssNames("MainLayout", className, themeStore.activeTheme.type)}>
|
||||
<header className={cssNames("flex gaps align-center", headerClass)}>
|
||||
<div className="box grow flex align-center">
|
||||
{clusterName && <span>{clusterName}</span>}
|
||||
</div>
|
||||
<span className="cluster">{clusterName}</span>
|
||||
</header>
|
||||
|
||||
<aside className={cssNames("flex column", { pinned: this.isPinned, accessible: this.isAccessible })}>
|
||||
@ -68,7 +66,7 @@ export class MainLayout extends React.Component<Props> {
|
||||
{tabs && (
|
||||
<Tabs center onChange={url => navigate(url)}>
|
||||
{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}/>
|
||||
})}
|
||||
</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 { IKubeWatchEvent, kubeWatchApi } from "./api/kube-watch-api";
|
||||
import { ItemStore } from "./item.store";
|
||||
import { configStore } from "./config.store";
|
||||
import { apiManager } from "./api/api-manager";
|
||||
import { IKubeApiQueryParams, KubeApi } from "./api/kube-api";
|
||||
import { KubeJsonApiData } from "./api/kube-json-api";
|
||||
import { clusterStore } from "../common/cluster-store";
|
||||
|
||||
@autobind()
|
||||
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);
|
||||
if (namespaces.length) {
|
||||
return this.items.filter(item => namespaces.includes(item.getNs()));
|
||||
}
|
||||
else if (!strict) {
|
||||
} else if (!strict) {
|
||||
return this.items;
|
||||
}
|
||||
}
|
||||
@ -47,8 +46,7 @@ export abstract class KubeObjectStore<T extends KubeObject = any> extends ItemSt
|
||||
const itemLabels = item.getLabels();
|
||||
return labels.every(label => itemLabels.includes(label));
|
||||
})
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
return this.items.filter((item: T) => {
|
||||
const itemLabels = item.metadata.labels || {};
|
||||
return Object.entries(labels)
|
||||
@ -62,8 +60,7 @@ export abstract class KubeObjectStore<T extends KubeObject = any> extends ItemSt
|
||||
const { limit } = this;
|
||||
const query: IKubeApiQueryParams = limit ? { limit } : {};
|
||||
return this.api.list({}, query);
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
return Promise
|
||||
.all(allowedNamespaces.map(namespace => this.api.list({ namespace })))
|
||||
.then(items => items.flat())
|
||||
@ -79,8 +76,8 @@ export abstract class KubeObjectStore<T extends KubeObject = any> extends ItemSt
|
||||
this.isLoading = true;
|
||||
let items: T[];
|
||||
try {
|
||||
const { isClusterAdmin, allowedNamespaces } = configStore;
|
||||
items = await this.loadItems(!isClusterAdmin ? allowedNamespaces : null);
|
||||
const { isAdmin, allowedNamespaces } = clusterStore.activeCluster;
|
||||
items = await this.loadItems(!isAdmin ? allowedNamespaces : null);
|
||||
items = this.filterItemsOnLoad(items);
|
||||
} finally {
|
||||
if (items) {
|
||||
@ -180,8 +177,7 @@ export abstract class KubeObjectStore<T extends KubeObject = any> extends ItemSt
|
||||
const newItem = new api.objectConstructor(object);
|
||||
if (!item) {
|
||||
items.push(newItem);
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
items.splice(index, 1, newItem);
|
||||
}
|
||||
break;
|
||||
|
||||
Loading…
Reference in New Issue
Block a user