diff --git a/src/common/rbac.ts b/src/common/rbac.ts index de242b114a..fbcf7c98d8 100644 --- a/src/common/rbac.ts +++ b/src/common/rbac.ts @@ -7,38 +7,37 @@ export type KubeResource = "endpoints" | "customresourcedefinitions" | "horizontalpodautoscalers" | "podsecuritypolicies" | "poddisruptionbudgets"; export interface KubeApiResource { - kind: string; // resource type (e.g. "Namespace") - apiName: KubeResource; // valid api resource name (e.g. "namespaces") + resource: KubeResource; // valid resource name group?: string; // api-group } // TODO: auto-populate all resources dynamically (see: kubectl api-resources -o=wide -v=7) export const apiResources: KubeApiResource[] = [ - { kind: "ConfigMap", apiName: "configmaps" }, - { kind: "CronJob", apiName: "cronjobs", group: "batch" }, - { kind: "CustomResourceDefinition", apiName: "customresourcedefinitions", group: "apiextensions.k8s.io" }, - { kind: "DaemonSet", apiName: "daemonsets", group: "apps" }, - { kind: "Deployment", apiName: "deployments", group: "apps" }, - { kind: "Endpoint", apiName: "endpoints" }, - { kind: "Event", apiName: "events" }, - { kind: "HorizontalPodAutoscaler", apiName: "horizontalpodautoscalers" }, - { kind: "Ingress", apiName: "ingresses", group: "networking.k8s.io" }, - { kind: "Job", apiName: "jobs", group: "batch" }, - { kind: "Namespace", apiName: "namespaces" }, - { kind: "LimitRange", apiName: "limitranges" }, - { kind: "NetworkPolicy", apiName: "networkpolicies", group: "networking.k8s.io" }, - { kind: "Node", apiName: "nodes" }, - { kind: "PersistentVolume", apiName: "persistentvolumes" }, - { kind: "PersistentVolumeClaim", apiName: "persistentvolumeclaims" }, - { kind: "Pod", apiName: "pods" }, - { kind: "PodDisruptionBudget", apiName: "poddisruptionbudgets" }, - { kind: "PodSecurityPolicy", apiName: "podsecuritypolicies" }, - { kind: "ResourceQuota", apiName: "resourcequotas" }, - { kind: "ReplicaSet", apiName: "replicasets", group: "apps" }, - { kind: "Secret", apiName: "secrets" }, - { kind: "Service", apiName: "services" }, - { kind: "StatefulSet", apiName: "statefulsets", group: "apps" }, - { kind: "StorageClass", apiName: "storageclasses", group: "storage.k8s.io" }, + { 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: "limitranges" }, + { resource: "namespaces" }, + { resource: "networkpolicies", group: "networking.k8s.io" }, + { resource: "nodes" }, + { resource: "persistentvolumes" }, + { resource: "persistentvolumeclaims" }, + { resource: "pods" }, + { resource: "poddisruptionbudgets" }, + { resource: "podsecuritypolicies" }, + { resource: "resourcequotas" }, + { resource: "replicasets", group: "apps" }, + { resource: "secrets" }, + { resource: "services" }, + { resource: "statefulsets", group: "apps" }, + { resource: "storageclasses", group: "storage.k8s.io" }, ]; export function isAllowedResource(resources: KubeResource | KubeResource[]) { diff --git a/src/main/cluster.ts b/src/main/cluster.ts index 956164e10c..c6c14f6406 100644 --- a/src/main/cluster.ts +++ b/src/main/cluster.ts @@ -190,7 +190,7 @@ export class Cluster implements ClusterModel, ClusterState { */ @observable metadata: ClusterMetadata = {}; /** - * List of allowed namespaces verified via K8S::SelfSubjectAccessReview api + * List of allowed namespaces * * @observable */ @@ -203,7 +203,7 @@ export class Cluster implements ClusterModel, ClusterState { */ @observable allowedResources: string[] = []; /** - * List of accessible namespaces provided by user in the Cluster Settings + * List of accessible namespaces * * @observable */ @@ -224,7 +224,7 @@ export class Cluster implements ClusterModel, ClusterState { * @computed */ @computed get name() { - return this.preferences.clusterName || this.contextName; + return this.preferences.clusterName || this.contextName; } /** @@ -279,8 +279,7 @@ export class Cluster implements ClusterModel, ClusterState { * @param port port where internal auth proxy is listening * @internal */ - @action - async init(port: number) { + @action async init(port: number) { try { this.initializing = true; this.contextHandler = new ContextHandler(this); @@ -335,8 +334,7 @@ export class Cluster implements ClusterModel, ClusterState { * @param force force activation * @internal */ - @action - async activate(force = false) { + @action async activate(force = false) { if (this.activated && !force) { return this.pushState(); } @@ -375,8 +373,7 @@ export class Cluster implements ClusterModel, ClusterState { /** * @internal */ - @action - async reconnect() { + @action async reconnect() { logger.info(`[CLUSTER]: reconnect`, this.getMeta()); this.contextHandler?.stopServer(); await this.contextHandler?.ensureServer(); @@ -403,8 +400,7 @@ export class Cluster implements ClusterModel, ClusterState { * @internal * @param opts refresh options */ - @action - async refresh(opts: ClusterRefreshOptions = {}) { + @action async refresh(opts: ClusterRefreshOptions = {}) { logger.info(`[CLUSTER]: refresh`, this.getMeta()); await this.whenInitialized; await this.refreshConnectionStatus(); @@ -424,8 +420,7 @@ export class Cluster implements ClusterModel, ClusterState { /** * @internal */ - @action - async refreshMetadata() { + @action async refreshMetadata() { logger.info(`[CLUSTER]: refreshMetadata`, this.getMeta()); const metadata = await detectorRegistry.detectForCluster(this); const existingMetadata = this.metadata; @@ -436,8 +431,7 @@ export class Cluster implements ClusterModel, ClusterState { /** * @internal */ - @action - async refreshConnectionStatus() { + @action async refreshConnectionStatus() { const connectionStatus = await this.getConnectionStatus(); this.online = connectionStatus > ClusterStatus.Offline; @@ -447,8 +441,7 @@ export class Cluster implements ClusterModel, ClusterState { /** * @internal */ - @action - async refreshAllowedResources() { + @action async refreshAllowedResources() { this.allowedNamespaces = await this.getAllowedNamespaces(); this.allowedResources = await this.getAllowedResources(); } @@ -675,7 +668,7 @@ export class Cluster implements ClusterModel, ClusterState { for (const namespace of this.allowedNamespaces.slice(0, 10)) { if (!this.resourceAccessStatuses.get(apiResource)) { const result = await this.canI({ - resource: apiResource.apiName, + resource: apiResource.resource, group: apiResource.group, verb: "list", namespace @@ -690,19 +683,9 @@ export class Cluster implements ClusterModel, ClusterState { return apiResources .filter((resource) => this.resourceAccessStatuses.get(resource)) - .map(apiResource => apiResource.apiName); + .map(apiResource => apiResource.resource); } catch (error) { return []; } } - - isAllowedResource(kind: string): boolean { - const apiResource = apiResources.find(resource => resource.kind === kind || resource.apiName === kind); - - if (apiResource) { - return this.allowedResources.includes(apiResource.apiName); - } - - return true; // allowed by default for other resources - } } diff --git a/src/renderer/api/kube-watch-api.ts b/src/renderer/api/kube-watch-api.ts index fe35a04baa..78ca25256e 100644 --- a/src/renderer/api/kube-watch-api.ts +++ b/src/renderer/api/kube-watch-api.ts @@ -11,7 +11,7 @@ import { apiPrefix, isDevelopment } from "../../common/vars"; import { getHostedCluster } from "../../common/cluster-store"; export interface IKubeWatchEvent { - type: "ADDED" | "MODIFIED" | "DELETED" | "ERROR"; + type: "ADDED" | "MODIFIED" | "DELETED"; object?: T; } @@ -62,41 +62,27 @@ export class KubeWatchApi { }); } - // FIXME: use POST to send apis for subscribing (list could be huge) - // TODO: try to use normal fetch res.body stream to consume watch-api updates - // https://github.com/lensapp/lens/issues/1898 - protected async getQuery() { - const { namespaceStore } = await import("../components/+namespaces/namespace.store"); - - await namespaceStore.whenReady; - const { isAdmin } = getHostedCluster(); + protected getQuery(): Partial { + const { isAdmin, allowedNamespaces } = getHostedCluster(); return { api: this.activeApis.map(api => { - if (isAdmin && !api.isNamespaced) { - return api.getWatchUrl(); - } + if (isAdmin) return api.getWatchUrl(); - if (api.isNamespaced) { - return namespaceStore.getContextNamespaces().map(namespace => api.getWatchUrl(namespace)); - } - - return []; + return allowedNamespaces.map(namespace => api.getWatchUrl(namespace)); }).flat() }; } // todo: maybe switch to websocket to avoid often reconnects @autobind() - protected async connect() { + protected connect() { if (this.evtSource) this.disconnect(); // close previous connection - const query = await this.getQuery(); - - if (!this.activeApis.length || !query.api.length) { + if (!this.activeApis.length) { return; } - + const query = this.getQuery(); const apiUrl = `${apiPrefix}/watch?${stringify(query)}`; this.evtSource = new EventSource(apiUrl); @@ -172,10 +158,6 @@ export class KubeWatchApi { addListener(store: KubeObjectStore, callback: (evt: IKubeWatchEvent) => void) { const listener = (evt: IKubeWatchEvent) => { - if (evt.type === "ERROR") { - return; // e.g. evt.object.message == "too old resource version" - } - const { namespace, resourceVersion } = evt.object.metadata; const api = apiManager.getApiByKind(evt.object.kind, evt.object.apiVersion); diff --git a/src/renderer/components/+apps-releases/release.store.ts b/src/renderer/components/+apps-releases/release.store.ts index 6f7ed39fed..b6d5c2fb5f 100644 --- a/src/renderer/components/+apps-releases/release.store.ts +++ b/src/renderer/components/+apps-releases/release.store.ts @@ -5,7 +5,7 @@ import { HelmRelease, helmReleasesApi, IReleaseCreatePayload, IReleaseUpdatePayl import { ItemStore } from "../../item.store"; import { Secret } from "../../api/endpoints"; import { secretsStore } from "../+config-secrets/secrets.store"; -import { namespaceStore } from "../+namespaces/namespace.store"; +import { getHostedCluster } from "../../../common/cluster-store"; @autobind() export class ReleaseStore extends ItemStore { @@ -60,23 +60,30 @@ export class ReleaseStore extends ItemStore { @action async loadAll() { this.isLoading = true; + let items; try { - const items = await this.loadItems(namespaceStore.getContextNamespaces()); + const { isAdmin, allowedNamespaces } = getHostedCluster(); - this.items.replace(this.sortItems(items)); - this.isLoaded = true; - } catch (error) { - console.error(`Loading Helm Chart releases has failed: ${error}`); + items = await this.loadItems(!isAdmin ? allowedNamespaces : null); } finally { + if (items) { + items = this.sortItems(items); + this.items.replace(items); + } + this.isLoaded = true; this.isLoading = false; } } - async loadItems(namespaces: string[]) { - return Promise - .all(namespaces.map(namespace => helmReleasesApi.list(namespace))) - .then(items => items.flat()); + async loadItems(namespaces?: string[]) { + if (!namespaces) { + return helmReleasesApi.list(); + } else { + return Promise + .all(namespaces.map(namespace => helmReleasesApi.list(namespace))) + .then(items => items.flat()); + } } async create(payload: IReleaseCreatePayload) { diff --git a/src/renderer/components/+namespaces/namespace.store.ts b/src/renderer/components/+namespaces/namespace.store.ts index 50ec2c8038..ad02dd137c 100644 --- a/src/renderer/components/+namespaces/namespace.store.ts +++ b/src/renderer/components/+namespaces/namespace.store.ts @@ -1,120 +1,53 @@ -import { action, comparer, IReactionDisposer, IReactionOptions, observable, reaction, toJS, when } from "mobx"; +import { action, comparer, observable, reaction } from "mobx"; import { autobind, createStorage } from "../../utils"; -import { KubeObjectStore, KubeObjectStoreLoadingParams } from "../../kube-object.store"; -import { Namespace, namespacesApi } from "../../api/endpoints/namespaces.api"; +import { KubeObjectStore } from "../../kube-object.store"; +import { Namespace, namespacesApi } from "../../api/endpoints"; import { createPageParam } from "../../navigation"; import { apiManager } from "../../api/api-manager"; -import { clusterStore, getHostedCluster } from "../../../common/cluster-store"; +import { isAllowedResource } from "../../../common/rbac"; +import { getHostedCluster } from "../../../common/cluster-store"; -const storage = createStorage("context_namespaces"); +const storage = createStorage("context_namespaces", []); export const namespaceUrlParam = createPageParam({ name: "namespaces", isSystem: true, multiValues: true, get defaultValue() { - return storage.get() ?? []; // initial namespaces coming from URL or local-storage (default) + return storage.get(); // initial namespaces coming from URL or local-storage (default) } }); -export function getDummyNamespace(name: string) { - return new Namespace({ - kind: Namespace.kind, - apiVersion: "v1", - metadata: { - name, - uid: "", - resourceVersion: "", - selfLink: `/api/v1/namespaces/${name}` - } - }); -} - @autobind() export class NamespaceStore extends KubeObjectStore { api = namespacesApi; - - @observable contextNs = observable.array(); - @observable isReady = false; - - whenReady = when(() => this.isReady); + contextNs = observable.array(); constructor() { super(); this.init(); } - private async init() { - await clusterStore.whenLoaded; - if (!getHostedCluster()) return; - await getHostedCluster().whenReady; // wait for cluster-state from main + private init() { + this.setContext(this.initNamespaces); - this.setContext(this.initialNamespaces); - this.autoLoadAllowedNamespaces(); - this.autoUpdateUrlAndLocalStorage(); - - this.isReady = true; - } - - public onContextChange(callback: (contextNamespaces: string[]) => void, opts: IReactionOptions = {}): IReactionDisposer { - return reaction(() => this.contextNs.toJS(), callback, { - equals: comparer.shallow, - ...opts, - }); - } - - private autoUpdateUrlAndLocalStorage(): IReactionDisposer { - return this.onContextChange(namespaces => { + return reaction(() => this.contextNs.toJS(), namespaces => { storage.set(namespaces); // save to local-storage namespaceUrlParam.set(namespaces, { replaceHistory: true }); // update url }, { fireImmediately: true, + equals: comparer.identity, }); } - private autoLoadAllowedNamespaces(): IReactionDisposer { - return reaction(() => this.allowedNamespaces, () => this.loadAll(), { - fireImmediately: true, - equals: comparer.shallow, - }); + get initNamespaces() { + return namespaceUrlParam.get(); } - get allowedNamespaces(): string[] { - return toJS(getHostedCluster().allowedNamespaces); - } - - private get initialNamespaces(): string[] { - const allowed = new Set(this.allowedNamespaces); - const prevSelected = storage.get(); - - if (Array.isArray(prevSelected)) { - return prevSelected.filter(namespace => allowed.has(namespace)); - } - - // otherwise select "default" or first allowed namespace - if (allowed.has("default")) { - return ["default"]; - } else if (allowed.size) { - return [Array.from(allowed)[0]]; - } - - return []; - } - - getContextNamespaces(): string[] { - const namespaces = this.contextNs.toJS(); - - // show all namespaces when nothing selected - if (!namespaces.length) { - if (this.isLoaded) { - // return actual namespaces list since "allowedNamespaces" updating every 30s in cluster and thus might be stale - return this.items.map(namespace => namespace.getName()); - } - - return this.allowedNamespaces; - } - - return namespaces; + getContextParams() { + return { + namespaces: this.contextNs.toJS(), + }; } subscribe(apis = [this.api]) { @@ -128,18 +61,31 @@ export class NamespaceStore extends KubeObjectStore { return super.subscribe(apis); } - protected async loadItems(params: KubeObjectStoreLoadingParams) { - const { allowedNamespaces } = this; + protected async loadItems(namespaces?: string[]) { + if (!isAllowedResource("namespaces")) { + if (namespaces) return namespaces.map(this.getDummyNamespace); - let namespaces = await super.loadItems(params); - - namespaces = namespaces.filter(namespace => allowedNamespaces.includes(namespace.getName())); - - if (!namespaces.length && allowedNamespaces.length > 0) { - return allowedNamespaces.map(getDummyNamespace); + return []; } - return namespaces; + if (namespaces) { + return Promise.all(namespaces.map(name => this.api.get({ name }))); + } else { + return super.loadItems(); + } + } + + protected getDummyNamespace(name: string) { + return new Namespace({ + kind: "Namespace", + apiVersion: "v1", + metadata: { + name, + uid: "", + resourceVersion: "", + selfLink: `/api/v1/namespaces/${name}` + } + }); } @action @@ -159,6 +105,12 @@ export class NamespaceStore extends KubeObjectStore { else this.contextNs.push(namespace); } + @action + reset() { + super.reset(); + this.contextNs.clear(); + } + @action async remove(item: Namespace) { await super.remove(item); diff --git a/src/renderer/components/+user-management-roles-bindings/role-bindings.store.ts b/src/renderer/components/+user-management-roles-bindings/role-bindings.store.ts index 71890acc44..f293dea6f0 100644 --- a/src/renderer/components/+user-management-roles-bindings/role-bindings.store.ts +++ b/src/renderer/components/+user-management-roles-bindings/role-bindings.store.ts @@ -1,7 +1,7 @@ import difference from "lodash/difference"; import uniqBy from "lodash/uniqBy"; import { clusterRoleBindingApi, IRoleBindingSubject, RoleBinding, roleBindingApi } from "../../api/endpoints"; -import { KubeObjectStore, KubeObjectStoreLoadingParams } from "../../kube-object.store"; +import { KubeObjectStore } from "../../kube-object.store"; import { autobind } from "../../utils"; import { apiManager } from "../../api/api-manager"; @@ -26,13 +26,15 @@ export class RoleBindingsStore extends KubeObjectStore { return clusterRoleBindingApi.get(params); } - protected async loadItems(params: KubeObjectStoreLoadingParams): Promise { - const items = await Promise.all([ - super.loadItems({ ...params, api: clusterRoleBindingApi }), - super.loadItems({ ...params, api: roleBindingApi }), - ]); - - return items.flat(); + protected loadItems(namespaces?: string[]) { + if (namespaces) { + return Promise.all( + namespaces.map(namespace => roleBindingApi.list({ namespace })) + ).then(items => items.flat()); + } else { + return Promise.all([clusterRoleBindingApi.list(), roleBindingApi.list()]) + .then(items => items.flat()); + } } protected async createItem(params: { name: string; namespace?: string }, data?: Partial) { diff --git a/src/renderer/components/+user-management-roles/roles.store.ts b/src/renderer/components/+user-management-roles/roles.store.ts index 7d2e90dd38..7b6c6c2397 100644 --- a/src/renderer/components/+user-management-roles/roles.store.ts +++ b/src/renderer/components/+user-management-roles/roles.store.ts @@ -1,6 +1,6 @@ import { clusterRoleApi, Role, roleApi } from "../../api/endpoints"; import { autobind } from "../../utils"; -import { KubeObjectStore, KubeObjectStoreLoadingParams } from "../../kube-object.store"; +import { KubeObjectStore } from "../../kube-object.store"; import { apiManager } from "../../api/api-manager"; @autobind() @@ -24,13 +24,15 @@ export class RolesStore extends KubeObjectStore { return clusterRoleApi.get(params); } - protected async loadItems(params: KubeObjectStoreLoadingParams): Promise { - const items = await Promise.all([ - super.loadItems({ ...params, api: clusterRoleApi }), - super.loadItems({ ...params, api: roleApi }), - ]); - - return items.flat(); + protected loadItems(namespaces?: string[]): Promise { + if (namespaces) { + return Promise.all( + namespaces.map(namespace => roleApi.list({ namespace })) + ).then(items => items.flat()); + } else { + return Promise.all([clusterRoleApi.list(), roleApi.list()]) + .then(items => items.flat()); + } } protected async createItem(params: { name: string; namespace?: string }, data?: Partial) { diff --git a/src/renderer/components/+workloads-overview/overview-statuses.tsx b/src/renderer/components/+workloads-overview/overview-statuses.tsx index 33e5aa37c5..78adecb6df 100644 --- a/src/renderer/components/+workloads-overview/overview-statuses.tsx +++ b/src/renderer/components/+workloads-overview/overview-statuses.tsx @@ -27,7 +27,7 @@ export class OverviewStatuses extends React.Component { @autobind() renderWorkload(resource: KubeResource): React.ReactElement { const store = workloadStores[resource]; - const items = store.getAllByNs(namespaceStore.getContextNamespaces()); + const items = store.getAllByNs(namespaceStore.contextNs); return (
diff --git a/src/renderer/components/+workloads-overview/overview.tsx b/src/renderer/components/+workloads-overview/overview.tsx index 351b57462c..318ad53f77 100644 --- a/src/renderer/components/+workloads-overview/overview.tsx +++ b/src/renderer/components/+workloads-overview/overview.tsx @@ -17,65 +17,81 @@ import { cronJobStore } from "../+workloads-cronjobs/cronjob.store"; import { Events } from "../+events"; import { KubeObjectStore } from "../../kube-object.store"; import { isAllowedResource } from "../../../common/rbac"; -import { namespaceStore } from "../+namespaces/namespace.store"; interface Props extends RouteComponentProps { } @observer export class WorkloadsOverview extends React.Component { - @observable isLoading = false; @observable isUnmounting = false; async componentDidMount() { - const stores: KubeObjectStore[] = [ - isAllowedResource("pods") && podsStore, - isAllowedResource("deployments") && deploymentStore, - isAllowedResource("daemonsets") && daemonSetStore, - isAllowedResource("statefulsets") && statefulSetStore, - isAllowedResource("replicasets") && replicaSetStore, - isAllowedResource("jobs") && jobStore, - isAllowedResource("cronjobs") && cronJobStore, - isAllowedResource("events") && eventStore, - ].filter(Boolean); + const stores: KubeObjectStore[] = []; - const unsubscribeMap = new Map void>(); + if (isAllowedResource("pods")) { + stores.push(podsStore); + } - const loadStores = async () => { - this.isLoading = true; + if (isAllowedResource("deployments")) { + stores.push(deploymentStore); + } - for (const store of stores) { - if (this.isUnmounting) break; + if (isAllowedResource("daemonsets")) { + stores.push(daemonSetStore); + } - try { - await store.loadAll(); - unsubscribeMap.get(store)?.(); // unsubscribe previous watcher - unsubscribeMap.set(store, store.subscribe()); - } catch (error) { - console.error("loading store error", error); - } - } - this.isLoading = false; - }; + if (isAllowedResource("statefulsets")) { + stores.push(statefulSetStore); + } - namespaceStore.onContextChange(loadStores, { - fireImmediately: true, - }); + if (isAllowedResource("replicasets")) { + stores.push(replicaSetStore); + } - await when(() => this.isUnmounting && !this.isLoading); - unsubscribeMap.forEach(dispose => dispose()); - unsubscribeMap.clear(); + if (isAllowedResource("jobs")) { + stores.push(jobStore); + } + + if (isAllowedResource("cronjobs")) { + stores.push(cronJobStore); + } + + if (isAllowedResource("events")) { + stores.push(eventStore); + } + + const unsubscribeList: Array<() => void> = []; + + for (const store of stores) { + await store.loadAll(); + unsubscribeList.push(store.subscribe()); + } + + await when(() => this.isUnmounting); + unsubscribeList.forEach(dispose => dispose()); } componentWillUnmount() { this.isUnmounting = true; } + get contents() { + return ( + <> + + { isAllowedResource("events") && } + + ); + } + render() { return (
- - {isAllowedResource("events") && } + {this.contents}
); } diff --git a/src/renderer/components/+workloads-pods/pods.tsx b/src/renderer/components/+workloads-pods/pods.tsx index a59c9d79d2..2296b98317 100644 --- a/src/renderer/components/+workloads-pods/pods.tsx +++ b/src/renderer/components/+workloads-pods/pods.tsx @@ -19,7 +19,8 @@ import { lookupApiLink } from "../../api/kube-api"; import { KubeObjectStatusIcon } from "../kube-object-status-icon"; import { Badge } from "../badge"; -enum columnId { + +enum sortBy { name = "name", namespace = "namespace", containers = "containers", @@ -76,15 +77,15 @@ export class Pods extends React.Component { tableId = "workloads_pods" isConfigurable sortingCallbacks={{ - [columnId.name]: (pod: Pod) => pod.getName(), - [columnId.namespace]: (pod: Pod) => pod.getNs(), - [columnId.containers]: (pod: Pod) => pod.getContainers().length, - [columnId.restarts]: (pod: Pod) => pod.getRestartsCount(), - [columnId.owners]: (pod: Pod) => pod.getOwnerRefs().map(ref => ref.kind), - [columnId.qos]: (pod: Pod) => pod.getQosClass(), - [columnId.node]: (pod: Pod) => pod.getNodeName(), - [columnId.age]: (pod: Pod) => pod.metadata.creationTimestamp, - [columnId.status]: (pod: Pod) => pod.getStatusMessage(), + [sortBy.name]: (pod: Pod) => pod.getName(), + [sortBy.namespace]: (pod: Pod) => pod.getNs(), + [sortBy.containers]: (pod: Pod) => pod.getContainers().length, + [sortBy.restarts]: (pod: Pod) => pod.getRestartsCount(), + [sortBy.owners]: (pod: Pod) => pod.getOwnerRefs().map(ref => ref.kind), + [sortBy.qos]: (pod: Pod) => pod.getQosClass(), + [sortBy.node]: (pod: Pod) => pod.getNodeName(), + [sortBy.age]: (pod: Pod) => pod.metadata.creationTimestamp, + [sortBy.status]: (pod: Pod) => pod.getStatusMessage(), }} searchFilters={[ (pod: Pod) => pod.getSearchFields(), @@ -94,16 +95,16 @@ export class Pods extends React.Component { ]} renderHeaderTitle="Pods" renderTableHeader={[ - { title: "Name", className: "name", sortBy: columnId.name, id: columnId.name }, - { className: "warning", showWithColumn: columnId.name }, - { title: "Namespace", className: "namespace", sortBy: columnId.namespace, id: columnId.namespace }, - { title: "Containers", className: "containers", sortBy: columnId.containers, id: columnId.containers }, - { title: "Restarts", className: "restarts", sortBy: columnId.restarts, id: columnId.restarts }, - { title: "Controlled By", className: "owners", sortBy: columnId.owners, id: columnId.owners }, - { title: "Node", className: "node", sortBy: columnId.node, id: columnId.node }, - { title: "QoS", className: "qos", sortBy: columnId.qos, id: columnId.qos }, - { title: "Age", className: "age", sortBy: columnId.age, id: columnId.age }, - { title: "Status", className: "status", sortBy: columnId.status, id: columnId.status }, + { title: "Name", className: "name", sortBy: sortBy.name }, + { className: "warning", showWithColumn: "name" }, + { title: "Namespace", className: "namespace", sortBy: sortBy.namespace }, + { title: "Containers", className: "containers", sortBy: sortBy.containers }, + { title: "Restarts", className: "restarts", sortBy: sortBy.restarts }, + { title: "Controlled By", className: "owners", sortBy: sortBy.owners }, + { title: "Node", className: "node", sortBy: sortBy.node }, + { title: "QoS", className: "qos", sortBy: sortBy.qos }, + { title: "Age", className: "age", sortBy: sortBy.age }, + { title: "Status", className: "status", sortBy: sortBy.status }, ]} renderTableContents={(pod: Pod) => [ , diff --git a/src/renderer/components/item-object-list/item-list-layout.scss b/src/renderer/components/item-object-list/item-list-layout.scss index 0008ffd527..9bdc2f943d 100644 --- a/src/renderer/components/item-object-list/item-list-layout.scss +++ b/src/renderer/components/item-object-list/item-list-layout.scss @@ -36,14 +36,3 @@ } } -.ItemListLayoutVisibilityMenu { - .MenuItem { - padding: 0; - } - - .Checkbox { - width: 100%; - padding: var(--spacing); - cursor: pointer; - } -} diff --git a/src/renderer/components/item-object-list/item-list-layout.tsx b/src/renderer/components/item-object-list/item-list-layout.tsx index 6b4ff4fd16..91fb55aeb1 100644 --- a/src/renderer/components/item-object-list/item-list-layout.tsx +++ b/src/renderer/components/item-object-list/item-list-layout.tsx @@ -129,7 +129,7 @@ export class ItemListLayout extends React.Component { if (!isClusterScoped) { disposeOnUnmount(this, [ - namespaceStore.onContextChange(() => this.loadStores()) + reaction(() => namespaceStore.items.toJS(), () => this.loadStores()) ]); } } @@ -440,9 +440,11 @@ export class ItemListLayout extends React.Component { return ; } })} - - {isConfigurable && this.renderColumnVisibilityMenu()} - + {isConfigurable && ( + + {this.renderColumnVisibilityMenu()} + + )} ); } diff --git a/src/renderer/components/item-object-list/page-filters.store.ts b/src/renderer/components/item-object-list/page-filters.store.ts index d931cd2575..9bff008aa6 100644 --- a/src/renderer/components/item-object-list/page-filters.store.ts +++ b/src/renderer/components/item-object-list/page-filters.store.ts @@ -34,14 +34,14 @@ export class PageFiltersStore { namespaceStore.setContext(filteredNs); } }), - namespaceStore.onContextChange(namespaces => { + reaction(() => namespaceStore.contextNs.toJS(), contextNs => { const filteredNs = this.getValues(FilterType.NAMESPACE); - const isChanged = namespaces.length !== filteredNs.length; + const isChanged = contextNs.length !== filteredNs.length; if (isChanged) { this.filters.replace([ ...this.filters.filter(({ type }) => type !== FilterType.NAMESPACE), - ...namespaces.map(ns => ({ type: FilterType.NAMESPACE, value: ns })), + ...contextNs.map(ns => ({ type: FilterType.NAMESPACE, value: ns })), ]); } }, { diff --git a/src/renderer/components/item-object-list/table-menu.scss b/src/renderer/components/item-object-list/table-menu.scss new file mode 100644 index 0000000000..b7e41f54ca --- /dev/null +++ b/src/renderer/components/item-object-list/table-menu.scss @@ -0,0 +1,4 @@ +.MenuCheckbox { + width: 100%; + height: 100%; +} diff --git a/src/renderer/components/table/table-cell.tsx b/src/renderer/components/table/table-cell.tsx index 81e2f9f85f..7fdb153419 100644 --- a/src/renderer/components/table/table-cell.tsx +++ b/src/renderer/components/table/table-cell.tsx @@ -74,7 +74,7 @@ export class TableCell extends React.Component { const content = displayBooleans(displayBoolean, title || children); return ( -
+
{this.renderCheckbox()} {_nowrap ?
{content}
: content} {this.renderSortIcon()} diff --git a/src/renderer/item.store.ts b/src/renderer/item.store.ts index eccd2b52df..2105954d32 100644 --- a/src/renderer/item.store.ts +++ b/src/renderer/item.store.ts @@ -9,7 +9,7 @@ export interface ItemObject { @autobind() export abstract class ItemStore { - abstract loadAll(...args: any[]): Promise; + abstract loadAll(): Promise; protected defaultSorting = (item: T) => item.getName(); @@ -40,7 +40,8 @@ export abstract class ItemStore { if (item) { return item; - } else { + } + else { const items = this.sortItems([...this.items, newItem]); this.items.replace(items); @@ -82,7 +83,8 @@ export abstract class ItemStore { const index = this.items.findIndex(item => item === existingItem); this.items.splice(index, 1, item); - } else { + } + else { let items = [...this.items, item]; if (sortItems) items = this.sortItems(items); @@ -128,7 +130,8 @@ export abstract class ItemStore { toggleSelection(item: T) { if (this.isSelected(item)) { this.unselect(item); - } else { + } + else { this.select(item); } } @@ -139,7 +142,8 @@ export abstract class ItemStore { if (allSelected) { visibleItems.forEach(this.unselect); - } else { + } + else { visibleItems.forEach(this.select); } } diff --git a/src/renderer/kube-object.store.ts b/src/renderer/kube-object.store.ts index 8a75bc7ae6..2e0dfa7ff5 100644 --- a/src/renderer/kube-object.store.ts +++ b/src/renderer/kube-object.store.ts @@ -1,4 +1,3 @@ -import type { Cluster } from "../main/cluster"; import { action, observable, reaction } from "mobx"; import { autobind } from "./utils"; import { KubeObject } from "./api/kube-object"; @@ -7,11 +6,7 @@ import { ItemStore } from "./item.store"; import { apiManager } from "./api/api-manager"; import { IKubeApiQueryParams, KubeApi } from "./api/kube-api"; import { KubeJsonApiData } from "./api/kube-json-api"; - -export interface KubeObjectStoreLoadingParams { - namespaces: string[]; - api?: KubeApi; -} +import { getHostedCluster } from "../common/cluster-store"; @autobind() export abstract class KubeObjectStore extends ItemStore { @@ -80,26 +75,14 @@ export abstract class KubeObjectStore extends ItemSt } } - protected async resolveCluster(): Promise { - const { getHostedCluster } = await import("../common/cluster-store"); - - return getHostedCluster(); - } - - protected async loadItems({ namespaces, api }: KubeObjectStoreLoadingParams): Promise { - const cluster = await this.resolveCluster(); - - if (cluster.isAllowedResource(api.kind)) { - if (api.isNamespaced) { - return Promise - .all(namespaces.map(namespace => api.list({ namespace }))) - .then(items => items.flat()); - } - - return api.list({}, this.query); + protected async loadItems(allowedNamespaces?: string[]): Promise { + if (!this.api.isNamespaced || !allowedNamespaces) { + return this.api.list({}, this.query); + } else { + return Promise + .all(allowedNamespaces.map(namespace => this.api.list({ namespace }))) + .then(items => items.flat()); } - - return []; } protected filterItemsOnLoad(items: T[]) { @@ -107,35 +90,30 @@ export abstract class KubeObjectStore extends ItemSt } @action - async loadAll({ namespaces: contextNamespaces }: { namespaces?: string[] } = {}) { + async loadAll() { this.isLoading = true; + let items: T[]; try { - if (!contextNamespaces) { - const { namespaceStore } = await import("./components/+namespaces/namespace.store"); + const { allowedNamespaces, accessibleNamespaces, isAdmin } = getHostedCluster(); - contextNamespaces = namespaceStore.getContextNamespaces(); + if (isAdmin && accessibleNamespaces.length == 0) { + items = await this.loadItems(); + } else { + items = await this.loadItems(allowedNamespaces); } - let items = await this.loadItems({ namespaces: contextNamespaces, api: this.api }); - items = this.filterItemsOnLoad(items); - items = this.sortItems(items); - - this.items.replace(items); - this.isLoaded = true; - } catch (error) { - console.error("Loading store items failed", { error, store: this }); - this.resetOnError(error); } finally { + if (items) { + items = this.sortItems(items); + this.items.replace(items); + } this.isLoading = false; + this.isLoaded = true; } } - protected resetOnError(error: any) { - if (error) this.reset(); - } - protected async loadItem(params: { name: string; namespace?: string }): Promise { return this.api.get(params); } @@ -220,7 +198,7 @@ export abstract class KubeObjectStore extends ItemSt // create latest non-observable copy of items to apply updates in one action (==single render) const items = this.items.toJS(); - for (const { type, object } of this.eventsBuffer.clear()) { + for (const {type, object} of this.eventsBuffer.clear()) { const index = items.findIndex(item => item.getId() === object.metadata?.uid); const item = items[index]; const api = apiManager.getApiByKind(object.kind, object.apiVersion);