mirror of
https://github.com/lensapp/lens.git
synced 2025-05-20 05:10:56 +00:00
Load k8s resources only for selected namespaces (#1918)
* loading k8s resources into stores per selected namespaces -- part 1 Signed-off-by: Roman <ixrock@gmail.com> * loading k8s resources into stores per selected namespaces -- part 2 - fix: generating helm chart id Signed-off-by: Roman <ixrock@gmail.com> * loading k8s resources into stores per selected namespaces -- part 3 Signed-off-by: Roman <ixrock@gmail.com> * fixes Signed-off-by: Roman <ixrock@gmail.com> * fixes / responding to comments Signed-off-by: Roman <ixrock@gmail.com> * chore / small fixes Signed-off-by: Roman <ixrock@gmail.com> * fixes & refactoring Signed-off-by: Roman <ixrock@gmail.com> * make lint happy Signed-off-by: Roman <ixrock@gmail.com> * reset store on loading error Signed-off-by: Roman <ixrock@gmail.com> * added new cluster method: cluster.isAllowedResource Signed-off-by: Roman <ixrock@gmail.com> * fix: loading namespaces optimizations Signed-off-by: Roman <ixrock@gmail.com> * fixes & refactoring Signed-off-by: Roman <ixrock@gmail.com>
This commit is contained in:
parent
a92ed46f0d
commit
f8c111ddd8
@ -7,37 +7,38 @@ export type KubeResource =
|
|||||||
"endpoints" | "customresourcedefinitions" | "horizontalpodautoscalers" | "podsecuritypolicies" | "poddisruptionbudgets";
|
"endpoints" | "customresourcedefinitions" | "horizontalpodautoscalers" | "podsecuritypolicies" | "poddisruptionbudgets";
|
||||||
|
|
||||||
export interface KubeApiResource {
|
export interface KubeApiResource {
|
||||||
resource: KubeResource; // valid resource name
|
kind: string; // resource type (e.g. "Namespace")
|
||||||
|
apiName: KubeResource; // valid api resource name (e.g. "namespaces")
|
||||||
group?: string; // api-group
|
group?: string; // api-group
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: auto-populate all resources dynamically (see: kubectl api-resources -o=wide -v=7)
|
// TODO: auto-populate all resources dynamically (see: kubectl api-resources -o=wide -v=7)
|
||||||
export const apiResources: KubeApiResource[] = [
|
export const apiResources: KubeApiResource[] = [
|
||||||
{ resource: "configmaps" },
|
{ kind: "ConfigMap", apiName: "configmaps" },
|
||||||
{ resource: "cronjobs", group: "batch" },
|
{ kind: "CronJob", apiName: "cronjobs", group: "batch" },
|
||||||
{ resource: "customresourcedefinitions", group: "apiextensions.k8s.io" },
|
{ kind: "CustomResourceDefinition", apiName: "customresourcedefinitions", group: "apiextensions.k8s.io" },
|
||||||
{ resource: "daemonsets", group: "apps" },
|
{ kind: "DaemonSet", apiName: "daemonsets", group: "apps" },
|
||||||
{ resource: "deployments", group: "apps" },
|
{ kind: "Deployment", apiName: "deployments", group: "apps" },
|
||||||
{ resource: "endpoints" },
|
{ kind: "Endpoint", apiName: "endpoints" },
|
||||||
{ resource: "events" },
|
{ kind: "Event", apiName: "events" },
|
||||||
{ resource: "horizontalpodautoscalers" },
|
{ kind: "HorizontalPodAutoscaler", apiName: "horizontalpodautoscalers" },
|
||||||
{ resource: "ingresses", group: "networking.k8s.io" },
|
{ kind: "Ingress", apiName: "ingresses", group: "networking.k8s.io" },
|
||||||
{ resource: "jobs", group: "batch" },
|
{ kind: "Job", apiName: "jobs", group: "batch" },
|
||||||
{ resource: "limitranges" },
|
{ kind: "Namespace", apiName: "namespaces" },
|
||||||
{ resource: "namespaces" },
|
{ kind: "LimitRange", apiName: "limitranges" },
|
||||||
{ resource: "networkpolicies", group: "networking.k8s.io" },
|
{ kind: "NetworkPolicy", apiName: "networkpolicies", group: "networking.k8s.io" },
|
||||||
{ resource: "nodes" },
|
{ kind: "Node", apiName: "nodes" },
|
||||||
{ resource: "persistentvolumes" },
|
{ kind: "PersistentVolume", apiName: "persistentvolumes" },
|
||||||
{ resource: "persistentvolumeclaims" },
|
{ kind: "PersistentVolumeClaim", apiName: "persistentvolumeclaims" },
|
||||||
{ resource: "pods" },
|
{ kind: "Pod", apiName: "pods" },
|
||||||
{ resource: "poddisruptionbudgets" },
|
{ kind: "PodDisruptionBudget", apiName: "poddisruptionbudgets" },
|
||||||
{ resource: "podsecuritypolicies" },
|
{ kind: "PodSecurityPolicy", apiName: "podsecuritypolicies" },
|
||||||
{ resource: "resourcequotas" },
|
{ kind: "ResourceQuota", apiName: "resourcequotas" },
|
||||||
{ resource: "replicasets", group: "apps" },
|
{ kind: "ReplicaSet", apiName: "replicasets", group: "apps" },
|
||||||
{ resource: "secrets" },
|
{ kind: "Secret", apiName: "secrets" },
|
||||||
{ resource: "services" },
|
{ kind: "Service", apiName: "services" },
|
||||||
{ resource: "statefulsets", group: "apps" },
|
{ kind: "StatefulSet", apiName: "statefulsets", group: "apps" },
|
||||||
{ resource: "storageclasses", group: "storage.k8s.io" },
|
{ kind: "StorageClass", apiName: "storageclasses", group: "storage.k8s.io" },
|
||||||
];
|
];
|
||||||
|
|
||||||
export function isAllowedResource(resources: KubeResource | KubeResource[]) {
|
export function isAllowedResource(resources: KubeResource | KubeResource[]) {
|
||||||
|
|||||||
@ -84,6 +84,15 @@ export class UserStore extends BaseStore<UserStoreModel> {
|
|||||||
return semver.gt(getAppVersion(), this.lastSeenAppVersion);
|
return semver.gt(getAppVersion(), this.lastSeenAppVersion);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
setHiddenTableColumns(tableId: string, names: Set<string> | string[]) {
|
||||||
|
this.preferences.hiddenTableColumns[tableId] = Array.from(names);
|
||||||
|
}
|
||||||
|
|
||||||
|
getHiddenTableColumns(tableId: string): Set<string> {
|
||||||
|
return new Set(this.preferences.hiddenTableColumns[tableId]);
|
||||||
|
}
|
||||||
|
|
||||||
@action
|
@action
|
||||||
resetKubeConfigPath() {
|
resetKubeConfigPath() {
|
||||||
this.kubeConfigPath = kubeConfigDefaultPath;
|
this.kubeConfigPath = kubeConfigDefaultPath;
|
||||||
|
|||||||
@ -190,7 +190,7 @@ export class Cluster implements ClusterModel, ClusterState {
|
|||||||
*/
|
*/
|
||||||
@observable metadata: ClusterMetadata = {};
|
@observable metadata: ClusterMetadata = {};
|
||||||
/**
|
/**
|
||||||
* List of allowed namespaces
|
* List of allowed namespaces verified via K8S::SelfSubjectAccessReview api
|
||||||
*
|
*
|
||||||
* @observable
|
* @observable
|
||||||
*/
|
*/
|
||||||
@ -203,7 +203,7 @@ export class Cluster implements ClusterModel, ClusterState {
|
|||||||
*/
|
*/
|
||||||
@observable allowedResources: string[] = [];
|
@observable allowedResources: string[] = [];
|
||||||
/**
|
/**
|
||||||
* List of accessible namespaces
|
* List of accessible namespaces provided by user in the Cluster Settings
|
||||||
*
|
*
|
||||||
* @observable
|
* @observable
|
||||||
*/
|
*/
|
||||||
@ -224,7 +224,7 @@ export class Cluster implements ClusterModel, ClusterState {
|
|||||||
* @computed
|
* @computed
|
||||||
*/
|
*/
|
||||||
@computed get name() {
|
@computed get name() {
|
||||||
return this.preferences.clusterName || this.contextName;
|
return this.preferences.clusterName || this.contextName;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -279,7 +279,8 @@ export class Cluster implements ClusterModel, ClusterState {
|
|||||||
* @param port port where internal auth proxy is listening
|
* @param port port where internal auth proxy is listening
|
||||||
* @internal
|
* @internal
|
||||||
*/
|
*/
|
||||||
@action async init(port: number) {
|
@action
|
||||||
|
async init(port: number) {
|
||||||
try {
|
try {
|
||||||
this.initializing = true;
|
this.initializing = true;
|
||||||
this.contextHandler = new ContextHandler(this);
|
this.contextHandler = new ContextHandler(this);
|
||||||
@ -334,7 +335,8 @@ export class Cluster implements ClusterModel, ClusterState {
|
|||||||
* @param force force activation
|
* @param force force activation
|
||||||
* @internal
|
* @internal
|
||||||
*/
|
*/
|
||||||
@action async activate(force = false) {
|
@action
|
||||||
|
async activate(force = false) {
|
||||||
if (this.activated && !force) {
|
if (this.activated && !force) {
|
||||||
return this.pushState();
|
return this.pushState();
|
||||||
}
|
}
|
||||||
@ -373,7 +375,8 @@ export class Cluster implements ClusterModel, ClusterState {
|
|||||||
/**
|
/**
|
||||||
* @internal
|
* @internal
|
||||||
*/
|
*/
|
||||||
@action async reconnect() {
|
@action
|
||||||
|
async reconnect() {
|
||||||
logger.info(`[CLUSTER]: reconnect`, this.getMeta());
|
logger.info(`[CLUSTER]: reconnect`, this.getMeta());
|
||||||
this.contextHandler?.stopServer();
|
this.contextHandler?.stopServer();
|
||||||
await this.contextHandler?.ensureServer();
|
await this.contextHandler?.ensureServer();
|
||||||
@ -400,7 +403,8 @@ export class Cluster implements ClusterModel, ClusterState {
|
|||||||
* @internal
|
* @internal
|
||||||
* @param opts refresh options
|
* @param opts refresh options
|
||||||
*/
|
*/
|
||||||
@action async refresh(opts: ClusterRefreshOptions = {}) {
|
@action
|
||||||
|
async refresh(opts: ClusterRefreshOptions = {}) {
|
||||||
logger.info(`[CLUSTER]: refresh`, this.getMeta());
|
logger.info(`[CLUSTER]: refresh`, this.getMeta());
|
||||||
await this.whenInitialized;
|
await this.whenInitialized;
|
||||||
await this.refreshConnectionStatus();
|
await this.refreshConnectionStatus();
|
||||||
@ -420,7 +424,8 @@ export class Cluster implements ClusterModel, ClusterState {
|
|||||||
/**
|
/**
|
||||||
* @internal
|
* @internal
|
||||||
*/
|
*/
|
||||||
@action async refreshMetadata() {
|
@action
|
||||||
|
async refreshMetadata() {
|
||||||
logger.info(`[CLUSTER]: refreshMetadata`, this.getMeta());
|
logger.info(`[CLUSTER]: refreshMetadata`, this.getMeta());
|
||||||
const metadata = await detectorRegistry.detectForCluster(this);
|
const metadata = await detectorRegistry.detectForCluster(this);
|
||||||
const existingMetadata = this.metadata;
|
const existingMetadata = this.metadata;
|
||||||
@ -431,7 +436,8 @@ export class Cluster implements ClusterModel, ClusterState {
|
|||||||
/**
|
/**
|
||||||
* @internal
|
* @internal
|
||||||
*/
|
*/
|
||||||
@action async refreshConnectionStatus() {
|
@action
|
||||||
|
async refreshConnectionStatus() {
|
||||||
const connectionStatus = await this.getConnectionStatus();
|
const connectionStatus = await this.getConnectionStatus();
|
||||||
|
|
||||||
this.online = connectionStatus > ClusterStatus.Offline;
|
this.online = connectionStatus > ClusterStatus.Offline;
|
||||||
@ -441,7 +447,8 @@ export class Cluster implements ClusterModel, ClusterState {
|
|||||||
/**
|
/**
|
||||||
* @internal
|
* @internal
|
||||||
*/
|
*/
|
||||||
@action async refreshAllowedResources() {
|
@action
|
||||||
|
async refreshAllowedResources() {
|
||||||
this.allowedNamespaces = await this.getAllowedNamespaces();
|
this.allowedNamespaces = await this.getAllowedNamespaces();
|
||||||
this.allowedResources = await this.getAllowedResources();
|
this.allowedResources = await this.getAllowedResources();
|
||||||
}
|
}
|
||||||
@ -668,7 +675,7 @@ export class Cluster implements ClusterModel, ClusterState {
|
|||||||
for (const namespace of this.allowedNamespaces.slice(0, 10)) {
|
for (const namespace of this.allowedNamespaces.slice(0, 10)) {
|
||||||
if (!this.resourceAccessStatuses.get(apiResource)) {
|
if (!this.resourceAccessStatuses.get(apiResource)) {
|
||||||
const result = await this.canI({
|
const result = await this.canI({
|
||||||
resource: apiResource.resource,
|
resource: apiResource.apiName,
|
||||||
group: apiResource.group,
|
group: apiResource.group,
|
||||||
verb: "list",
|
verb: "list",
|
||||||
namespace
|
namespace
|
||||||
@ -683,9 +690,19 @@ export class Cluster implements ClusterModel, ClusterState {
|
|||||||
|
|
||||||
return apiResources
|
return apiResources
|
||||||
.filter((resource) => this.resourceAccessStatuses.get(resource))
|
.filter((resource) => this.resourceAccessStatuses.get(resource))
|
||||||
.map(apiResource => apiResource.resource);
|
.map(apiResource => apiResource.apiName);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return [];
|
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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -11,7 +11,7 @@ import { apiPrefix, isDevelopment } from "../../common/vars";
|
|||||||
import { getHostedCluster } from "../../common/cluster-store";
|
import { getHostedCluster } from "../../common/cluster-store";
|
||||||
|
|
||||||
export interface IKubeWatchEvent<T = any> {
|
export interface IKubeWatchEvent<T = any> {
|
||||||
type: "ADDED" | "MODIFIED" | "DELETED";
|
type: "ADDED" | "MODIFIED" | "DELETED" | "ERROR";
|
||||||
object?: T;
|
object?: T;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -62,27 +62,41 @@ export class KubeWatchApi {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
protected getQuery(): Partial<IKubeWatchRouteQuery> {
|
// FIXME: use POST to send apis for subscribing (list could be huge)
|
||||||
const { isAdmin, allowedNamespaces } = getHostedCluster();
|
// 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();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
api: this.activeApis.map(api => {
|
api: this.activeApis.map(api => {
|
||||||
if (isAdmin) return api.getWatchUrl();
|
if (isAdmin && !api.isNamespaced) {
|
||||||
|
return api.getWatchUrl();
|
||||||
|
}
|
||||||
|
|
||||||
return allowedNamespaces.map(namespace => api.getWatchUrl(namespace));
|
if (api.isNamespaced) {
|
||||||
|
return namespaceStore.getContextNamespaces().map(namespace => api.getWatchUrl(namespace));
|
||||||
|
}
|
||||||
|
|
||||||
|
return [];
|
||||||
}).flat()
|
}).flat()
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// todo: maybe switch to websocket to avoid often reconnects
|
// todo: maybe switch to websocket to avoid often reconnects
|
||||||
@autobind()
|
@autobind()
|
||||||
protected connect() {
|
protected async connect() {
|
||||||
if (this.evtSource) this.disconnect(); // close previous connection
|
if (this.evtSource) this.disconnect(); // close previous connection
|
||||||
|
|
||||||
if (!this.activeApis.length) {
|
const query = await this.getQuery();
|
||||||
|
|
||||||
|
if (!this.activeApis.length || !query.api.length) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const query = this.getQuery();
|
|
||||||
const apiUrl = `${apiPrefix}/watch?${stringify(query)}`;
|
const apiUrl = `${apiPrefix}/watch?${stringify(query)}`;
|
||||||
|
|
||||||
this.evtSource = new EventSource(apiUrl);
|
this.evtSource = new EventSource(apiUrl);
|
||||||
@ -158,6 +172,10 @@ export class KubeWatchApi {
|
|||||||
|
|
||||||
addListener(store: KubeObjectStore, callback: (evt: IKubeWatchEvent) => void) {
|
addListener(store: KubeObjectStore, callback: (evt: IKubeWatchEvent) => void) {
|
||||||
const listener = (evt: IKubeWatchEvent<KubeJsonApiData>) => {
|
const listener = (evt: IKubeWatchEvent<KubeJsonApiData>) => {
|
||||||
|
if (evt.type === "ERROR") {
|
||||||
|
return; // e.g. evt.object.message == "too old resource version"
|
||||||
|
}
|
||||||
|
|
||||||
const { namespace, resourceVersion } = evt.object.metadata;
|
const { namespace, resourceVersion } = evt.object.metadata;
|
||||||
const api = apiManager.getApiByKind(evt.object.kind, evt.object.apiVersion);
|
const api = apiManager.getApiByKind(evt.object.kind, evt.object.apiVersion);
|
||||||
|
|
||||||
|
|||||||
@ -5,7 +5,7 @@ import { HelmRelease, helmReleasesApi, IReleaseCreatePayload, IReleaseUpdatePayl
|
|||||||
import { ItemStore } from "../../item.store";
|
import { ItemStore } from "../../item.store";
|
||||||
import { Secret } from "../../api/endpoints";
|
import { Secret } from "../../api/endpoints";
|
||||||
import { secretsStore } from "../+config-secrets/secrets.store";
|
import { secretsStore } from "../+config-secrets/secrets.store";
|
||||||
import { getHostedCluster } from "../../../common/cluster-store";
|
import { namespaceStore } from "../+namespaces/namespace.store";
|
||||||
|
|
||||||
@autobind()
|
@autobind()
|
||||||
export class ReleaseStore extends ItemStore<HelmRelease> {
|
export class ReleaseStore extends ItemStore<HelmRelease> {
|
||||||
@ -60,30 +60,23 @@ export class ReleaseStore extends ItemStore<HelmRelease> {
|
|||||||
@action
|
@action
|
||||||
async loadAll() {
|
async loadAll() {
|
||||||
this.isLoading = true;
|
this.isLoading = true;
|
||||||
let items;
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const { isAdmin, allowedNamespaces } = getHostedCluster();
|
const items = await this.loadItems(namespaceStore.getContextNamespaces());
|
||||||
|
|
||||||
items = await this.loadItems(!isAdmin ? allowedNamespaces : null);
|
this.items.replace(this.sortItems(items));
|
||||||
} finally {
|
|
||||||
if (items) {
|
|
||||||
items = this.sortItems(items);
|
|
||||||
this.items.replace(items);
|
|
||||||
}
|
|
||||||
this.isLoaded = true;
|
this.isLoaded = true;
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Loading Helm Chart releases has failed: ${error}`);
|
||||||
|
} finally {
|
||||||
this.isLoading = false;
|
this.isLoading = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async loadItems(namespaces?: string[]) {
|
async loadItems(namespaces: string[]) {
|
||||||
if (!namespaces) {
|
return Promise
|
||||||
return helmReleasesApi.list();
|
.all(namespaces.map(namespace => helmReleasesApi.list(namespace)))
|
||||||
} else {
|
.then(items => items.flat());
|
||||||
return Promise
|
|
||||||
.all(namespaces.map(namespace => helmReleasesApi.list(namespace)))
|
|
||||||
.then(items => items.flat());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async create(payload: IReleaseCreatePayload) {
|
async create(payload: IReleaseCreatePayload) {
|
||||||
|
|||||||
@ -1,53 +1,120 @@
|
|||||||
import { action, comparer, observable, reaction } from "mobx";
|
import { action, comparer, IReactionDisposer, IReactionOptions, observable, reaction, toJS, when } from "mobx";
|
||||||
import { autobind, createStorage } from "../../utils";
|
import { autobind, createStorage } from "../../utils";
|
||||||
import { KubeObjectStore } from "../../kube-object.store";
|
import { KubeObjectStore, KubeObjectStoreLoadingParams } from "../../kube-object.store";
|
||||||
import { Namespace, namespacesApi } from "../../api/endpoints";
|
import { Namespace, namespacesApi } from "../../api/endpoints/namespaces.api";
|
||||||
import { createPageParam } from "../../navigation";
|
import { createPageParam } from "../../navigation";
|
||||||
import { apiManager } from "../../api/api-manager";
|
import { apiManager } from "../../api/api-manager";
|
||||||
import { isAllowedResource } from "../../../common/rbac";
|
import { clusterStore, getHostedCluster } from "../../../common/cluster-store";
|
||||||
import { getHostedCluster } from "../../../common/cluster-store";
|
|
||||||
|
|
||||||
const storage = createStorage<string[]>("context_namespaces", []);
|
const storage = createStorage<string[]>("context_namespaces");
|
||||||
|
|
||||||
export const namespaceUrlParam = createPageParam<string[]>({
|
export const namespaceUrlParam = createPageParam<string[]>({
|
||||||
name: "namespaces",
|
name: "namespaces",
|
||||||
isSystem: true,
|
isSystem: true,
|
||||||
multiValues: true,
|
multiValues: true,
|
||||||
get defaultValue() {
|
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()
|
@autobind()
|
||||||
export class NamespaceStore extends KubeObjectStore<Namespace> {
|
export class NamespaceStore extends KubeObjectStore<Namespace> {
|
||||||
api = namespacesApi;
|
api = namespacesApi;
|
||||||
contextNs = observable.array<string>();
|
|
||||||
|
@observable contextNs = observable.array<string>();
|
||||||
|
@observable isReady = false;
|
||||||
|
|
||||||
|
whenReady = when(() => this.isReady);
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
this.init();
|
this.init();
|
||||||
}
|
}
|
||||||
|
|
||||||
private init() {
|
private async init() {
|
||||||
this.setContext(this.initNamespaces);
|
await clusterStore.whenLoaded;
|
||||||
|
if (!getHostedCluster()) return;
|
||||||
|
await getHostedCluster().whenReady; // wait for cluster-state from main
|
||||||
|
|
||||||
return reaction(() => this.contextNs.toJS(), namespaces => {
|
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 => {
|
||||||
storage.set(namespaces); // save to local-storage
|
storage.set(namespaces); // save to local-storage
|
||||||
namespaceUrlParam.set(namespaces, { replaceHistory: true }); // update url
|
namespaceUrlParam.set(namespaces, { replaceHistory: true }); // update url
|
||||||
}, {
|
}, {
|
||||||
fireImmediately: true,
|
fireImmediately: true,
|
||||||
equals: comparer.identity,
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
get initNamespaces() {
|
private autoLoadAllowedNamespaces(): IReactionDisposer {
|
||||||
return namespaceUrlParam.get();
|
return reaction(() => this.allowedNamespaces, () => this.loadAll(), {
|
||||||
|
fireImmediately: true,
|
||||||
|
equals: comparer.shallow,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
getContextParams() {
|
get allowedNamespaces(): string[] {
|
||||||
return {
|
return toJS(getHostedCluster().allowedNamespaces);
|
||||||
namespaces: this.contextNs.toJS(),
|
}
|
||||||
};
|
|
||||||
|
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
subscribe(apis = [this.api]) {
|
subscribe(apis = [this.api]) {
|
||||||
@ -61,31 +128,18 @@ export class NamespaceStore extends KubeObjectStore<Namespace> {
|
|||||||
return super.subscribe(apis);
|
return super.subscribe(apis);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async loadItems(namespaces?: string[]) {
|
protected async loadItems(params: KubeObjectStoreLoadingParams) {
|
||||||
if (!isAllowedResource("namespaces")) {
|
const { allowedNamespaces } = this;
|
||||||
if (namespaces) return namespaces.map(this.getDummyNamespace);
|
|
||||||
|
|
||||||
return [];
|
let namespaces = await super.loadItems(params);
|
||||||
|
|
||||||
|
namespaces = namespaces.filter(namespace => allowedNamespaces.includes(namespace.getName()));
|
||||||
|
|
||||||
|
if (!namespaces.length && allowedNamespaces.length > 0) {
|
||||||
|
return allowedNamespaces.map(getDummyNamespace);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (namespaces) {
|
return 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
|
@action
|
||||||
@ -105,12 +159,6 @@ export class NamespaceStore extends KubeObjectStore<Namespace> {
|
|||||||
else this.contextNs.push(namespace);
|
else this.contextNs.push(namespace);
|
||||||
}
|
}
|
||||||
|
|
||||||
@action
|
|
||||||
reset() {
|
|
||||||
super.reset();
|
|
||||||
this.contextNs.clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
@action
|
@action
|
||||||
async remove(item: Namespace) {
|
async remove(item: Namespace) {
|
||||||
await super.remove(item);
|
await super.remove(item);
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import difference from "lodash/difference";
|
import difference from "lodash/difference";
|
||||||
import uniqBy from "lodash/uniqBy";
|
import uniqBy from "lodash/uniqBy";
|
||||||
import { clusterRoleBindingApi, IRoleBindingSubject, RoleBinding, roleBindingApi } from "../../api/endpoints";
|
import { clusterRoleBindingApi, IRoleBindingSubject, RoleBinding, roleBindingApi } from "../../api/endpoints";
|
||||||
import { KubeObjectStore } from "../../kube-object.store";
|
import { KubeObjectStore, KubeObjectStoreLoadingParams } from "../../kube-object.store";
|
||||||
import { autobind } from "../../utils";
|
import { autobind } from "../../utils";
|
||||||
import { apiManager } from "../../api/api-manager";
|
import { apiManager } from "../../api/api-manager";
|
||||||
|
|
||||||
@ -26,15 +26,13 @@ export class RoleBindingsStore extends KubeObjectStore<RoleBinding> {
|
|||||||
return clusterRoleBindingApi.get(params);
|
return clusterRoleBindingApi.get(params);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected loadItems(namespaces?: string[]) {
|
protected async loadItems(params: KubeObjectStoreLoadingParams): Promise<RoleBinding[]> {
|
||||||
if (namespaces) {
|
const items = await Promise.all([
|
||||||
return Promise.all(
|
super.loadItems({ ...params, api: clusterRoleBindingApi }),
|
||||||
namespaces.map(namespace => roleBindingApi.list({ namespace }))
|
super.loadItems({ ...params, api: roleBindingApi }),
|
||||||
).then(items => items.flat());
|
]);
|
||||||
} else {
|
|
||||||
return Promise.all([clusterRoleBindingApi.list(), roleBindingApi.list()])
|
return items.flat();
|
||||||
.then(items => items.flat());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async createItem(params: { name: string; namespace?: string }, data?: Partial<RoleBinding>) {
|
protected async createItem(params: { name: string; namespace?: string }, data?: Partial<RoleBinding>) {
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import { clusterRoleApi, Role, roleApi } from "../../api/endpoints";
|
import { clusterRoleApi, Role, roleApi } from "../../api/endpoints";
|
||||||
import { autobind } from "../../utils";
|
import { autobind } from "../../utils";
|
||||||
import { KubeObjectStore } from "../../kube-object.store";
|
import { KubeObjectStore, KubeObjectStoreLoadingParams } from "../../kube-object.store";
|
||||||
import { apiManager } from "../../api/api-manager";
|
import { apiManager } from "../../api/api-manager";
|
||||||
|
|
||||||
@autobind()
|
@autobind()
|
||||||
@ -24,15 +24,13 @@ export class RolesStore extends KubeObjectStore<Role> {
|
|||||||
return clusterRoleApi.get(params);
|
return clusterRoleApi.get(params);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected loadItems(namespaces?: string[]): Promise<Role[]> {
|
protected async loadItems(params: KubeObjectStoreLoadingParams): Promise<Role[]> {
|
||||||
if (namespaces) {
|
const items = await Promise.all([
|
||||||
return Promise.all(
|
super.loadItems({ ...params, api: clusterRoleApi }),
|
||||||
namespaces.map(namespace => roleApi.list({ namespace }))
|
super.loadItems({ ...params, api: roleApi }),
|
||||||
).then(items => items.flat());
|
]);
|
||||||
} else {
|
|
||||||
return Promise.all([clusterRoleApi.list(), roleApi.list()])
|
return items.flat();
|
||||||
.then(items => items.flat());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async createItem(params: { name: string; namespace?: string }, data?: Partial<Role>) {
|
protected async createItem(params: { name: string; namespace?: string }, data?: Partial<Role>) {
|
||||||
|
|||||||
@ -27,7 +27,7 @@ export class OverviewStatuses extends React.Component {
|
|||||||
@autobind()
|
@autobind()
|
||||||
renderWorkload(resource: KubeResource): React.ReactElement {
|
renderWorkload(resource: KubeResource): React.ReactElement {
|
||||||
const store = workloadStores[resource];
|
const store = workloadStores[resource];
|
||||||
const items = store.getAllByNs(namespaceStore.contextNs);
|
const items = store.getAllByNs(namespaceStore.getContextNamespaces());
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="workload" key={resource}>
|
<div className="workload" key={resource}>
|
||||||
|
|||||||
@ -17,81 +17,65 @@ import { cronJobStore } from "../+workloads-cronjobs/cronjob.store";
|
|||||||
import { Events } from "../+events";
|
import { Events } from "../+events";
|
||||||
import { KubeObjectStore } from "../../kube-object.store";
|
import { KubeObjectStore } from "../../kube-object.store";
|
||||||
import { isAllowedResource } from "../../../common/rbac";
|
import { isAllowedResource } from "../../../common/rbac";
|
||||||
|
import { namespaceStore } from "../+namespaces/namespace.store";
|
||||||
|
|
||||||
interface Props extends RouteComponentProps<IWorkloadsOverviewRouteParams> {
|
interface Props extends RouteComponentProps<IWorkloadsOverviewRouteParams> {
|
||||||
}
|
}
|
||||||
|
|
||||||
@observer
|
@observer
|
||||||
export class WorkloadsOverview extends React.Component<Props> {
|
export class WorkloadsOverview extends React.Component<Props> {
|
||||||
|
@observable isLoading = false;
|
||||||
@observable isUnmounting = false;
|
@observable isUnmounting = false;
|
||||||
|
|
||||||
async componentDidMount() {
|
async componentDidMount() {
|
||||||
const stores: KubeObjectStore[] = [];
|
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);
|
||||||
|
|
||||||
if (isAllowedResource("pods")) {
|
const unsubscribeMap = new Map<KubeObjectStore, () => void>();
|
||||||
stores.push(podsStore);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isAllowedResource("deployments")) {
|
const loadStores = async () => {
|
||||||
stores.push(deploymentStore);
|
this.isLoading = true;
|
||||||
}
|
|
||||||
|
|
||||||
if (isAllowedResource("daemonsets")) {
|
for (const store of stores) {
|
||||||
stores.push(daemonSetStore);
|
if (this.isUnmounting) break;
|
||||||
}
|
|
||||||
|
|
||||||
if (isAllowedResource("statefulsets")) {
|
try {
|
||||||
stores.push(statefulSetStore);
|
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("replicasets")) {
|
namespaceStore.onContextChange(loadStores, {
|
||||||
stores.push(replicaSetStore);
|
fireImmediately: true,
|
||||||
}
|
});
|
||||||
|
|
||||||
if (isAllowedResource("jobs")) {
|
await when(() => this.isUnmounting && !this.isLoading);
|
||||||
stores.push(jobStore);
|
unsubscribeMap.forEach(dispose => dispose());
|
||||||
}
|
unsubscribeMap.clear();
|
||||||
|
|
||||||
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() {
|
componentWillUnmount() {
|
||||||
this.isUnmounting = true;
|
this.isUnmounting = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
get contents() {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<OverviewStatuses/>
|
|
||||||
{ isAllowedResource("events") && <Events
|
|
||||||
compact
|
|
||||||
hideFilters
|
|
||||||
className="box grow"
|
|
||||||
/> }
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<div className="WorkloadsOverview flex column gaps">
|
<div className="WorkloadsOverview flex column gaps">
|
||||||
{this.contents}
|
<OverviewStatuses/>
|
||||||
|
{isAllowedResource("events") && <Events compact hideFilters className="box grow"/>}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -19,8 +19,7 @@ import { lookupApiLink } from "../../api/kube-api";
|
|||||||
import { KubeObjectStatusIcon } from "../kube-object-status-icon";
|
import { KubeObjectStatusIcon } from "../kube-object-status-icon";
|
||||||
import { Badge } from "../badge";
|
import { Badge } from "../badge";
|
||||||
|
|
||||||
|
enum columnId {
|
||||||
enum sortBy {
|
|
||||||
name = "name",
|
name = "name",
|
||||||
namespace = "namespace",
|
namespace = "namespace",
|
||||||
containers = "containers",
|
containers = "containers",
|
||||||
@ -77,15 +76,15 @@ export class Pods extends React.Component<Props> {
|
|||||||
tableId = "workloads_pods"
|
tableId = "workloads_pods"
|
||||||
isConfigurable
|
isConfigurable
|
||||||
sortingCallbacks={{
|
sortingCallbacks={{
|
||||||
[sortBy.name]: (pod: Pod) => pod.getName(),
|
[columnId.name]: (pod: Pod) => pod.getName(),
|
||||||
[sortBy.namespace]: (pod: Pod) => pod.getNs(),
|
[columnId.namespace]: (pod: Pod) => pod.getNs(),
|
||||||
[sortBy.containers]: (pod: Pod) => pod.getContainers().length,
|
[columnId.containers]: (pod: Pod) => pod.getContainers().length,
|
||||||
[sortBy.restarts]: (pod: Pod) => pod.getRestartsCount(),
|
[columnId.restarts]: (pod: Pod) => pod.getRestartsCount(),
|
||||||
[sortBy.owners]: (pod: Pod) => pod.getOwnerRefs().map(ref => ref.kind),
|
[columnId.owners]: (pod: Pod) => pod.getOwnerRefs().map(ref => ref.kind),
|
||||||
[sortBy.qos]: (pod: Pod) => pod.getQosClass(),
|
[columnId.qos]: (pod: Pod) => pod.getQosClass(),
|
||||||
[sortBy.node]: (pod: Pod) => pod.getNodeName(),
|
[columnId.node]: (pod: Pod) => pod.getNodeName(),
|
||||||
[sortBy.age]: (pod: Pod) => pod.metadata.creationTimestamp,
|
[columnId.age]: (pod: Pod) => pod.metadata.creationTimestamp,
|
||||||
[sortBy.status]: (pod: Pod) => pod.getStatusMessage(),
|
[columnId.status]: (pod: Pod) => pod.getStatusMessage(),
|
||||||
}}
|
}}
|
||||||
searchFilters={[
|
searchFilters={[
|
||||||
(pod: Pod) => pod.getSearchFields(),
|
(pod: Pod) => pod.getSearchFields(),
|
||||||
@ -95,16 +94,16 @@ export class Pods extends React.Component<Props> {
|
|||||||
]}
|
]}
|
||||||
renderHeaderTitle="Pods"
|
renderHeaderTitle="Pods"
|
||||||
renderTableHeader={[
|
renderTableHeader={[
|
||||||
{ title: "Name", className: "name", sortBy: sortBy.name },
|
{ title: "Name", className: "name", sortBy: columnId.name, id: columnId.name },
|
||||||
{ className: "warning", showWithColumn: "name" },
|
{ className: "warning", showWithColumn: columnId.name },
|
||||||
{ title: "Namespace", className: "namespace", sortBy: sortBy.namespace },
|
{ title: "Namespace", className: "namespace", sortBy: columnId.namespace, id: columnId.namespace },
|
||||||
{ title: "Containers", className: "containers", sortBy: sortBy.containers },
|
{ title: "Containers", className: "containers", sortBy: columnId.containers, id: columnId.containers },
|
||||||
{ title: "Restarts", className: "restarts", sortBy: sortBy.restarts },
|
{ title: "Restarts", className: "restarts", sortBy: columnId.restarts, id: columnId.restarts },
|
||||||
{ title: "Controlled By", className: "owners", sortBy: sortBy.owners },
|
{ title: "Controlled By", className: "owners", sortBy: columnId.owners, id: columnId.owners },
|
||||||
{ title: "Node", className: "node", sortBy: sortBy.node },
|
{ title: "Node", className: "node", sortBy: columnId.node, id: columnId.node },
|
||||||
{ title: "QoS", className: "qos", sortBy: sortBy.qos },
|
{ title: "QoS", className: "qos", sortBy: columnId.qos, id: columnId.qos },
|
||||||
{ title: "Age", className: "age", sortBy: sortBy.age },
|
{ title: "Age", className: "age", sortBy: columnId.age, id: columnId.age },
|
||||||
{ title: "Status", className: "status", sortBy: sortBy.status },
|
{ title: "Status", className: "status", sortBy: columnId.status, id: columnId.status },
|
||||||
]}
|
]}
|
||||||
renderTableContents={(pod: Pod) => [
|
renderTableContents={(pod: Pod) => [
|
||||||
<Badge flat key="name" label={pod.getName()} tooltip={pod.getName()} />,
|
<Badge flat key="name" label={pod.getName()} tooltip={pod.getName()} />,
|
||||||
|
|||||||
@ -36,3 +36,14 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.ItemListLayoutVisibilityMenu {
|
||||||
|
.MenuItem {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.Checkbox {
|
||||||
|
width: 100%;
|
||||||
|
padding: var(--spacing);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -1,12 +1,11 @@
|
|||||||
import "./item-list-layout.scss";
|
import "./item-list-layout.scss";
|
||||||
import "./table-menu.scss";
|
|
||||||
import groupBy from "lodash/groupBy";
|
import groupBy from "lodash/groupBy";
|
||||||
|
|
||||||
import React, { ReactNode } from "react";
|
import React, { ReactNode } from "react";
|
||||||
import { computed, observable, reaction, toJS, when } from "mobx";
|
import { computed, IReactionDisposer, observable, reaction, toJS } from "mobx";
|
||||||
import { disposeOnUnmount, observer } from "mobx-react";
|
import { disposeOnUnmount, observer } from "mobx-react";
|
||||||
import { ConfirmDialog, ConfirmDialogParams } from "../confirm-dialog";
|
import { ConfirmDialog, ConfirmDialogParams } from "../confirm-dialog";
|
||||||
import { TableSortCallback, Table, TableCell, TableCellProps, TableHead, TableProps, TableRow, TableRowProps } from "../table";
|
import { Table, TableCell, TableCellProps, TableHead, TableProps, TableRow, TableRowProps, TableSortCallback } from "../table";
|
||||||
import { autobind, createStorage, cssNames, IClassName, isReactNode, noop, prevDefault, stopPropagation } from "../../utils";
|
import { autobind, createStorage, cssNames, IClassName, isReactNode, noop, prevDefault, stopPropagation } from "../../utils";
|
||||||
import { AddRemoveButtons, AddRemoveButtonsProps } from "../add-remove-buttons";
|
import { AddRemoveButtons, AddRemoveButtonsProps } from "../add-remove-buttons";
|
||||||
import { NoItems } from "../no-items";
|
import { NoItems } from "../no-items";
|
||||||
@ -19,11 +18,10 @@ 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 { MenuActions} from "../menu/menu-actions";
|
import { MenuActions } from "../menu/menu-actions";
|
||||||
import { MenuItem } from "../menu";
|
import { MenuItem } from "../menu";
|
||||||
import { Checkbox } from "../checkbox";
|
import { Checkbox } from "../checkbox";
|
||||||
import { userStore } from "../../../common/user-store";
|
import { userStore } from "../../../common/user-store";
|
||||||
import logger from "../../../main/logger";
|
|
||||||
|
|
||||||
// todo: refactor, split to small re-usable components
|
// todo: refactor, split to small re-usable components
|
||||||
|
|
||||||
@ -98,10 +96,11 @@ interface ItemListLayoutUserSettings {
|
|||||||
@observer
|
@observer
|
||||||
export class ItemListLayout extends React.Component<ItemListLayoutProps> {
|
export class ItemListLayout extends React.Component<ItemListLayoutProps> {
|
||||||
static defaultProps = defaultProps as object;
|
static defaultProps = defaultProps as object;
|
||||||
@observable hiddenColumnNames = new Set<string>();
|
|
||||||
|
private watchDisposers: IReactionDisposer[] = [];
|
||||||
|
|
||||||
@observable isUnmounting = false;
|
@observable isUnmounting = false;
|
||||||
|
|
||||||
// default user settings (ui show-hide tweaks mostly)
|
|
||||||
@observable userSettings: ItemListLayoutUserSettings = {
|
@observable userSettings: ItemListLayoutUserSettings = {
|
||||||
showAppliedFilters: false,
|
showAppliedFilters: false,
|
||||||
};
|
};
|
||||||
@ -120,31 +119,54 @@ export class ItemListLayout extends React.Component<ItemListLayoutProps> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async componentDidMount() {
|
async componentDidMount() {
|
||||||
const { store, dependentStores, isClusterScoped, tableId } = this.props;
|
const { isClusterScoped, isConfigurable, tableId } = this.props;
|
||||||
|
|
||||||
if (this.canBeConfigured) this.hiddenColumnNames = new Set(userStore.preferences?.hiddenTableColumns?.[tableId]);
|
if (isConfigurable && !tableId) {
|
||||||
|
throw new Error("[ItemListLayout]: configurable list require props.tableId to be specified");
|
||||||
|
}
|
||||||
|
|
||||||
const stores = [store, ...dependentStores];
|
this.loadStores();
|
||||||
|
|
||||||
if (!isClusterScoped) stores.push(namespaceStore);
|
if (!isClusterScoped) {
|
||||||
|
disposeOnUnmount(this, [
|
||||||
try {
|
namespaceStore.onContextChange(() => this.loadStores())
|
||||||
stores.map(store => store.reset());
|
]);
|
||||||
await Promise.all(stores.map(store => store.loadAll()));
|
|
||||||
const subscriptions = stores.map(store => store.subscribe());
|
|
||||||
|
|
||||||
await when(() => this.isUnmounting);
|
|
||||||
subscriptions.forEach(dispose => dispose()); // unsubscribe all
|
|
||||||
} catch (error) {
|
|
||||||
console.log("catched", error);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount() {
|
async componentWillUnmount() {
|
||||||
this.isUnmounting = true;
|
this.isUnmounting = true;
|
||||||
const { store, isSelectable } = this.props;
|
this.unsubscribeStores();
|
||||||
|
}
|
||||||
|
|
||||||
if (isSelectable) store.resetSelection();
|
@computed get stores() {
|
||||||
|
const { store, dependentStores } = this.props;
|
||||||
|
|
||||||
|
return new Set([store, ...dependentStores]);
|
||||||
|
}
|
||||||
|
|
||||||
|
async loadStores() {
|
||||||
|
this.unsubscribeStores(); // reset first
|
||||||
|
|
||||||
|
// load
|
||||||
|
for (const store of this.stores) {
|
||||||
|
if (this.isUnmounting) {
|
||||||
|
this.unsubscribeStores();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await store.loadAll();
|
||||||
|
this.watchDisposers.push(store.subscribe());
|
||||||
|
} catch (error) {
|
||||||
|
console.error("loading store error", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
unsubscribeStores() {
|
||||||
|
this.watchDisposers.forEach(dispose => dispose());
|
||||||
|
this.watchDisposers.length = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
private filterCallbacks: { [type: string]: ItemsFilter } = {
|
private filterCallbacks: { [type: string]: ItemsFilter } = {
|
||||||
@ -180,9 +202,7 @@ export class ItemListLayout extends React.Component<ItemListLayoutProps> {
|
|||||||
};
|
};
|
||||||
|
|
||||||
@computed get isReady() {
|
@computed get isReady() {
|
||||||
const { isReady, store } = this.props;
|
return this.props.isReady ?? this.props.store.isLoaded;
|
||||||
|
|
||||||
return typeof isReady == "boolean" ? isReady : store.isLoaded;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@computed get filters() {
|
@computed get filters() {
|
||||||
@ -228,42 +248,6 @@ export class ItemListLayout extends React.Component<ItemListLayoutProps> {
|
|||||||
return this.applyFilters(filterItems, allItems);
|
return this.applyFilters(filterItems, allItems);
|
||||||
}
|
}
|
||||||
|
|
||||||
updateColumnFilter(checkboxValue: boolean, columnName: string) {
|
|
||||||
if (checkboxValue){
|
|
||||||
this.hiddenColumnNames.delete(columnName);
|
|
||||||
} else {
|
|
||||||
this.hiddenColumnNames.add(columnName);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.canBeConfigured) {
|
|
||||||
userStore.preferences.hiddenTableColumns[this.props.tableId] = Array.from(this.hiddenColumnNames);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
columnIsVisible(index: number): boolean {
|
|
||||||
const {renderTableHeader} = this.props;
|
|
||||||
|
|
||||||
if (!this.canBeConfigured) return true;
|
|
||||||
|
|
||||||
return !this.hiddenColumnNames.has(renderTableHeader[index].showWithColumn ?? renderTableHeader[index].className);
|
|
||||||
}
|
|
||||||
|
|
||||||
get canBeConfigured(): boolean {
|
|
||||||
const { isConfigurable, tableId, renderTableHeader } = this.props;
|
|
||||||
|
|
||||||
if (!isConfigurable || !tableId) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!renderTableHeader?.every(({ className }) => className)) {
|
|
||||||
logger.warning("[ItemObjectList]: cannot configure an object list without all headers being identifiable");
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
@autobind()
|
@autobind()
|
||||||
getRow(uid: string) {
|
getRow(uid: string) {
|
||||||
const {
|
const {
|
||||||
@ -295,20 +279,18 @@ export class ItemListLayout extends React.Component<ItemListLayoutProps> {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{
|
{
|
||||||
renderTableContents(item)
|
renderTableContents(item).map((content, index) => {
|
||||||
.map((content, index) => {
|
const cellProps: TableCellProps = isReactNode(content) ? { children: content } : content;
|
||||||
const cellProps: TableCellProps = isReactNode(content) ? { children: content } : content;
|
const headCell = renderTableHeader?.[index];
|
||||||
|
|
||||||
if (copyClassNameFromHeadCells && renderTableHeader) {
|
if (copyClassNameFromHeadCells && headCell) {
|
||||||
const headCell = renderTableHeader[index];
|
cellProps.className = cssNames(cellProps.className, headCell.className);
|
||||||
|
}
|
||||||
|
|
||||||
if (headCell) {
|
if (!headCell || !this.isHiddenColumn(headCell)) {
|
||||||
cellProps.className = cssNames(cellProps.className, headCell.className);
|
return <TableCell key={index} {...cellProps} />;
|
||||||
}
|
}
|
||||||
}
|
})
|
||||||
|
|
||||||
return this.columnIsVisible(index) ? <TableCell key={index} {...cellProps} /> : null;
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
{renderItemMenu && (
|
{renderItemMenu && (
|
||||||
<TableCell className="menu" onClick={stopPropagation}>
|
<TableCell className="menu" onClick={stopPropagation}>
|
||||||
@ -347,16 +329,11 @@ export class ItemListLayout extends React.Component<ItemListLayoutProps> {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
return <PageFiltersList filters={filters} />;
|
return <PageFiltersList filters={filters}/>;
|
||||||
}
|
}
|
||||||
|
|
||||||
renderNoItems() {
|
renderNoItems() {
|
||||||
const { allItems, items, filters } = this;
|
if (this.filters.length > 0) {
|
||||||
const allItemsCount = allItems.length;
|
|
||||||
const itemsCount = items.length;
|
|
||||||
const isFiltered = filters.length > 0 && allItemsCount > itemsCount;
|
|
||||||
|
|
||||||
if (isFiltered) {
|
|
||||||
return (
|
return (
|
||||||
<NoItems>
|
<NoItems>
|
||||||
No items found.
|
No items found.
|
||||||
@ -369,7 +346,7 @@ export class ItemListLayout extends React.Component<ItemListLayoutProps> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return <NoItems />;
|
return <NoItems/>;
|
||||||
}
|
}
|
||||||
|
|
||||||
renderHeaderContent(placeholders: IHeaderPlaceholders): ReactNode {
|
renderHeaderContent(placeholders: IHeaderPlaceholders): ReactNode {
|
||||||
@ -413,12 +390,12 @@ export class ItemListLayout extends React.Component<ItemListLayoutProps> {
|
|||||||
title: <h5 className="title">{title}</h5>,
|
title: <h5 className="title">{title}</h5>,
|
||||||
info: this.renderInfo(),
|
info: this.renderInfo(),
|
||||||
filters: <>
|
filters: <>
|
||||||
{!isClusterScoped && <NamespaceSelectFilter />}
|
{!isClusterScoped && <NamespaceSelectFilter/>}
|
||||||
<PageFiltersSelect allowEmpty disableFilters={{
|
<PageFiltersSelect allowEmpty disableFilters={{
|
||||||
[FilterType.NAMESPACE]: true, // namespace-select used instead
|
[FilterType.NAMESPACE]: true, // namespace-select used instead
|
||||||
}} />
|
}}/>
|
||||||
</>,
|
</>,
|
||||||
search: <SearchInputUrl />,
|
search: <SearchInputUrl/>,
|
||||||
};
|
};
|
||||||
let header = this.renderHeaderContent(placeholders);
|
let header = this.renderHeaderContent(placeholders);
|
||||||
|
|
||||||
@ -442,10 +419,40 @@ export class ItemListLayout extends React.Component<ItemListLayoutProps> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
renderTableHeader() {
|
||||||
|
const { renderTableHeader, isSelectable, isConfigurable, store } = this.props;
|
||||||
|
|
||||||
|
if (!renderTableHeader) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TableHead showTopLine nowrap>
|
||||||
|
{isSelectable && (
|
||||||
|
<TableCell
|
||||||
|
checkbox
|
||||||
|
isChecked={store.isSelectedAll(this.items)}
|
||||||
|
onClick={prevDefault(() => store.toggleSelectionAll(this.items))}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{renderTableHeader.map((cellProps, index) => {
|
||||||
|
if (!this.isHiddenColumn(cellProps)) {
|
||||||
|
return <TableCell key={cellProps.id ?? index} {...cellProps} />;
|
||||||
|
}
|
||||||
|
})}
|
||||||
|
{isConfigurable && (
|
||||||
|
<TableCell className="menu">
|
||||||
|
{this.renderColumnVisibilityMenu()}
|
||||||
|
</TableCell>
|
||||||
|
)}
|
||||||
|
</TableHead>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
renderList() {
|
renderList() {
|
||||||
const {
|
const {
|
||||||
isSelectable, tableProps = {}, renderTableHeader, renderItemMenu,
|
store, hasDetailsView, addRemoveButtons = {}, virtual, sortingCallbacks, detailsItem,
|
||||||
store, hasDetailsView, addRemoveButtons = {}, virtual, sortingCallbacks, detailsItem
|
tableProps = {},
|
||||||
} = this.props;
|
} = this.props;
|
||||||
const { isReady, removeItemsDialog, items } = this;
|
const { isReady, removeItemsDialog, items } = this;
|
||||||
const { selectedItems } = store;
|
const { selectedItems } = store;
|
||||||
@ -454,7 +461,7 @@ export class ItemListLayout extends React.Component<ItemListLayoutProps> {
|
|||||||
return (
|
return (
|
||||||
<div className="items box grow flex column">
|
<div className="items box grow flex column">
|
||||||
{!isReady && (
|
{!isReady && (
|
||||||
<Spinner center />
|
<Spinner center/>
|
||||||
)}
|
)}
|
||||||
{isReady && (
|
{isReady && (
|
||||||
<Table
|
<Table
|
||||||
@ -470,23 +477,7 @@ export class ItemListLayout extends React.Component<ItemListLayoutProps> {
|
|||||||
className: cssNames("box grow", tableProps.className, themeStore.activeTheme.type),
|
className: cssNames("box grow", tableProps.className, themeStore.activeTheme.type),
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
{renderTableHeader && (
|
{this.renderTableHeader()}
|
||||||
<TableHead showTopLine nowrap>
|
|
||||||
{isSelectable && (
|
|
||||||
<TableCell
|
|
||||||
checkbox
|
|
||||||
isChecked={store.isSelectedAll(items)}
|
|
||||||
onClick={prevDefault(() => store.toggleSelectionAll(items))}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{renderTableHeader.map((cellProps, index) => this.columnIsVisible(index) ? <TableCell key={index} {...cellProps} /> : null)}
|
|
||||||
{ renderItemMenu &&
|
|
||||||
<TableCell className="menu" >
|
|
||||||
{this.canBeConfigured && this.renderColumnMenu()}
|
|
||||||
</TableCell>
|
|
||||||
}
|
|
||||||
</TableHead>
|
|
||||||
)}
|
|
||||||
{
|
{
|
||||||
!virtual && items.map(item => this.getRow(item.getId()))
|
!virtual && items.map(item => this.getRow(item.getId()))
|
||||||
}
|
}
|
||||||
@ -502,24 +493,47 @@ export class ItemListLayout extends React.Component<ItemListLayoutProps> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
renderColumnMenu() {
|
@computed get hiddenColumns() {
|
||||||
const { renderTableHeader} = this.props;
|
return userStore.getHiddenTableColumns(this.props.tableId);
|
||||||
|
}
|
||||||
|
|
||||||
|
isHiddenColumn({ id: columnId, showWithColumn }: TableCellProps): boolean {
|
||||||
|
if (!this.props.isConfigurable) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.hiddenColumns.has(columnId) || (
|
||||||
|
showWithColumn && this.hiddenColumns.has(showWithColumn)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
updateColumnVisibility({ id: columnId }: TableCellProps, isVisible: boolean) {
|
||||||
|
const hiddenColumns = new Set(this.hiddenColumns);
|
||||||
|
|
||||||
|
if (!isVisible) {
|
||||||
|
hiddenColumns.add(columnId);
|
||||||
|
} else {
|
||||||
|
hiddenColumns.delete(columnId);
|
||||||
|
}
|
||||||
|
|
||||||
|
userStore.setHiddenTableColumns(this.props.tableId, hiddenColumns);
|
||||||
|
}
|
||||||
|
|
||||||
|
renderColumnVisibilityMenu() {
|
||||||
|
const { renderTableHeader } = this.props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<MenuActions
|
<MenuActions className="ItemListLayoutVisibilityMenu" toolbar={false} autoCloseOnSelect={false}>
|
||||||
toolbar = {false}
|
|
||||||
autoCloseOnSelect = {false}
|
|
||||||
className={cssNames("KubeObjectMenu")}
|
|
||||||
>
|
|
||||||
{renderTableHeader.map((cellProps, index) => (
|
{renderTableHeader.map((cellProps, index) => (
|
||||||
!cellProps.showWithColumn &&
|
!cellProps.showWithColumn && (
|
||||||
<MenuItem key={index} className="input">
|
<MenuItem key={index} className="input">
|
||||||
<Checkbox label = {cellProps.title ?? `<${cellProps.className}>`}
|
<Checkbox
|
||||||
className = "MenuCheckbox"
|
label={cellProps.title ?? `<${cellProps.className}>`}
|
||||||
value ={!this.hiddenColumnNames.has(cellProps.className)}
|
value={!this.isHiddenColumn(cellProps)}
|
||||||
onChange = {(v) => this.updateColumnFilter(v, cellProps.className)}
|
onChange={isVisible => this.updateColumnVisibility(cellProps, isVisible)}
|
||||||
/>
|
/>
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
|
)
|
||||||
))}
|
))}
|
||||||
</MenuActions>
|
</MenuActions>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -34,14 +34,14 @@ export class PageFiltersStore {
|
|||||||
namespaceStore.setContext(filteredNs);
|
namespaceStore.setContext(filteredNs);
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
reaction(() => namespaceStore.contextNs.toJS(), contextNs => {
|
namespaceStore.onContextChange(namespaces => {
|
||||||
const filteredNs = this.getValues(FilterType.NAMESPACE);
|
const filteredNs = this.getValues(FilterType.NAMESPACE);
|
||||||
const isChanged = contextNs.length !== filteredNs.length;
|
const isChanged = namespaces.length !== filteredNs.length;
|
||||||
|
|
||||||
if (isChanged) {
|
if (isChanged) {
|
||||||
this.filters.replace([
|
this.filters.replace([
|
||||||
...this.filters.filter(({ type }) => type !== FilterType.NAMESPACE),
|
...this.filters.filter(({ type }) => type !== FilterType.NAMESPACE),
|
||||||
...contextNs.map(ns => ({ type: FilterType.NAMESPACE, value: ns })),
|
...namespaces.map(ns => ({ type: FilterType.NAMESPACE, value: ns })),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
}, {
|
}, {
|
||||||
|
|||||||
@ -1,4 +0,0 @@
|
|||||||
.MenuCheckbox {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
@ -9,13 +9,14 @@ import { Checkbox } from "../checkbox";
|
|||||||
export type TableCellElem = React.ReactElement<TableCellProps>;
|
export type TableCellElem = React.ReactElement<TableCellProps>;
|
||||||
|
|
||||||
export interface TableCellProps extends React.DOMAttributes<HTMLDivElement> {
|
export interface TableCellProps extends React.DOMAttributes<HTMLDivElement> {
|
||||||
|
id?: string; // used for configuration visibility of columns
|
||||||
className?: string;
|
className?: string;
|
||||||
title?: ReactNode;
|
title?: ReactNode;
|
||||||
checkbox?: boolean; // render cell with a checkbox
|
checkbox?: boolean; // render cell with a checkbox
|
||||||
isChecked?: boolean; // mark checkbox as checked or not
|
isChecked?: boolean; // mark checkbox as checked or not
|
||||||
renderBoolean?: boolean; // show "true" or "false" for all of the children elements are "typeof boolean"
|
renderBoolean?: boolean; // show "true" or "false" for all of the children elements are "typeof boolean"
|
||||||
sortBy?: TableSortBy; // column name, must be same as key in sortable object <Table sortable={}/>
|
sortBy?: TableSortBy; // column name, must be same as key in sortable object <Table sortable={}/>
|
||||||
showWithColumn?: string // className of another column, if it is not empty the current column is not shown in the filter menu, visibility of this one is the same as a specified column, applicable to headers only
|
showWithColumn?: string // id of the column which follow same visibility rules
|
||||||
_sorting?: Partial<TableSortParams>; // <Table> sorting state, don't use this prop outside (!)
|
_sorting?: Partial<TableSortParams>; // <Table> sorting state, don't use this prop outside (!)
|
||||||
_sort?(sortBy: TableSortBy): void; // <Table> sort function, don't use this prop outside (!)
|
_sort?(sortBy: TableSortBy): void; // <Table> sort function, don't use this prop outside (!)
|
||||||
_nowrap?: boolean; // indicator, might come from parent <TableHead>, don't use this prop outside (!)
|
_nowrap?: boolean; // indicator, might come from parent <TableHead>, don't use this prop outside (!)
|
||||||
@ -73,7 +74,7 @@ export class TableCell extends React.Component<TableCellProps> {
|
|||||||
const content = displayBooleans(displayBoolean, title || children);
|
const content = displayBooleans(displayBoolean, title || children);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div {...cellProps} id={className} className={classNames} onClick={this.onClick}>
|
<div {...cellProps} className={classNames} onClick={this.onClick}>
|
||||||
{this.renderCheckbox()}
|
{this.renderCheckbox()}
|
||||||
{_nowrap ? <div className="content">{content}</div> : content}
|
{_nowrap ? <div className="content">{content}</div> : content}
|
||||||
{this.renderSortIcon()}
|
{this.renderSortIcon()}
|
||||||
|
|||||||
@ -9,7 +9,7 @@ export interface ItemObject {
|
|||||||
|
|
||||||
@autobind()
|
@autobind()
|
||||||
export abstract class ItemStore<T extends ItemObject = ItemObject> {
|
export abstract class ItemStore<T extends ItemObject = ItemObject> {
|
||||||
abstract loadAll(): Promise<void>;
|
abstract loadAll(...args: any[]): Promise<void>;
|
||||||
|
|
||||||
protected defaultSorting = (item: T) => item.getName();
|
protected defaultSorting = (item: T) => item.getName();
|
||||||
|
|
||||||
@ -40,8 +40,7 @@ export abstract class ItemStore<T extends ItemObject = ItemObject> {
|
|||||||
|
|
||||||
if (item) {
|
if (item) {
|
||||||
return item;
|
return item;
|
||||||
}
|
} else {
|
||||||
else {
|
|
||||||
const items = this.sortItems([...this.items, newItem]);
|
const items = this.sortItems([...this.items, newItem]);
|
||||||
|
|
||||||
this.items.replace(items);
|
this.items.replace(items);
|
||||||
@ -83,8 +82,7 @@ export abstract class ItemStore<T extends ItemObject = ItemObject> {
|
|||||||
const index = this.items.findIndex(item => item === existingItem);
|
const index = this.items.findIndex(item => item === existingItem);
|
||||||
|
|
||||||
this.items.splice(index, 1, item);
|
this.items.splice(index, 1, item);
|
||||||
}
|
} else {
|
||||||
else {
|
|
||||||
let items = [...this.items, item];
|
let items = [...this.items, item];
|
||||||
|
|
||||||
if (sortItems) items = this.sortItems(items);
|
if (sortItems) items = this.sortItems(items);
|
||||||
@ -130,8 +128,7 @@ export abstract class ItemStore<T extends ItemObject = ItemObject> {
|
|||||||
toggleSelection(item: T) {
|
toggleSelection(item: T) {
|
||||||
if (this.isSelected(item)) {
|
if (this.isSelected(item)) {
|
||||||
this.unselect(item);
|
this.unselect(item);
|
||||||
}
|
} else {
|
||||||
else {
|
|
||||||
this.select(item);
|
this.select(item);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -142,8 +139,7 @@ export abstract class ItemStore<T extends ItemObject = ItemObject> {
|
|||||||
|
|
||||||
if (allSelected) {
|
if (allSelected) {
|
||||||
visibleItems.forEach(this.unselect);
|
visibleItems.forEach(this.unselect);
|
||||||
}
|
} else {
|
||||||
else {
|
|
||||||
visibleItems.forEach(this.select);
|
visibleItems.forEach(this.select);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
import type { Cluster } from "../main/cluster";
|
||||||
import { action, observable, reaction } from "mobx";
|
import { action, observable, reaction } from "mobx";
|
||||||
import { autobind } from "./utils";
|
import { autobind } from "./utils";
|
||||||
import { KubeObject } from "./api/kube-object";
|
import { KubeObject } from "./api/kube-object";
|
||||||
@ -6,7 +7,11 @@ import { ItemStore } from "./item.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 { getHostedCluster } from "../common/cluster-store";
|
|
||||||
|
export interface KubeObjectStoreLoadingParams {
|
||||||
|
namespaces: string[];
|
||||||
|
api?: KubeApi;
|
||||||
|
}
|
||||||
|
|
||||||
@autobind()
|
@autobind()
|
||||||
export abstract class KubeObjectStore<T extends KubeObject = any> extends ItemStore<T> {
|
export abstract class KubeObjectStore<T extends KubeObject = any> extends ItemStore<T> {
|
||||||
@ -71,14 +76,26 @@ export abstract class KubeObjectStore<T extends KubeObject = any> extends ItemSt
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async loadItems(allowedNamespaces?: string[]): Promise<T[]> {
|
protected async resolveCluster(): Promise<Cluster> {
|
||||||
if (!this.api.isNamespaced || !allowedNamespaces) {
|
const { getHostedCluster } = await import("../common/cluster-store");
|
||||||
return this.api.list({}, this.query);
|
|
||||||
} else {
|
return getHostedCluster();
|
||||||
return Promise
|
}
|
||||||
.all(allowedNamespaces.map(namespace => this.api.list({ namespace })))
|
|
||||||
.then(items => items.flat());
|
protected async loadItems({ namespaces, api }: KubeObjectStoreLoadingParams): Promise<T[]> {
|
||||||
|
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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
protected filterItemsOnLoad(items: T[]) {
|
protected filterItemsOnLoad(items: T[]) {
|
||||||
@ -86,30 +103,35 @@ export abstract class KubeObjectStore<T extends KubeObject = any> extends ItemSt
|
|||||||
}
|
}
|
||||||
|
|
||||||
@action
|
@action
|
||||||
async loadAll() {
|
async loadAll({ namespaces: contextNamespaces }: { namespaces?: string[] } = {}) {
|
||||||
this.isLoading = true;
|
this.isLoading = true;
|
||||||
let items: T[];
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const { allowedNamespaces, accessibleNamespaces, isAdmin } = getHostedCluster();
|
if (!contextNamespaces) {
|
||||||
|
const { namespaceStore } = await import("./components/+namespaces/namespace.store");
|
||||||
|
|
||||||
if (isAdmin && accessibleNamespaces.length == 0) {
|
contextNamespaces = namespaceStore.getContextNamespaces();
|
||||||
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.filterItemsOnLoad(items);
|
||||||
} finally {
|
items = this.sortItems(items);
|
||||||
if (items) {
|
|
||||||
items = this.sortItems(items);
|
this.items.replace(items);
|
||||||
this.items.replace(items);
|
|
||||||
}
|
|
||||||
this.isLoading = false;
|
|
||||||
this.isLoaded = true;
|
this.isLoaded = true;
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Loading store items failed", { error, store: this });
|
||||||
|
this.resetOnError(error);
|
||||||
|
} finally {
|
||||||
|
this.isLoading = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected resetOnError(error: any) {
|
||||||
|
if (error) this.reset();
|
||||||
|
}
|
||||||
|
|
||||||
protected async loadItem(params: { name: string; namespace?: string }): Promise<T> {
|
protected async loadItem(params: { name: string; namespace?: string }): Promise<T> {
|
||||||
return this.api.get(params);
|
return this.api.get(params);
|
||||||
}
|
}
|
||||||
@ -194,7 +216,7 @@ export abstract class KubeObjectStore<T extends KubeObject = any> extends ItemSt
|
|||||||
// create latest non-observable copy of items to apply updates in one action (==single render)
|
// create latest non-observable copy of items to apply updates in one action (==single render)
|
||||||
const items = this.items.toJS();
|
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 index = items.findIndex(item => item.getId() === object.metadata?.uid);
|
||||||
const item = items[index];
|
const item = items[index];
|
||||||
const api = apiManager.getApiByKind(object.kind, object.apiVersion);
|
const api = apiManager.getApiByKind(object.kind, object.apiVersion);
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user