mirror of
https://github.com/lensapp/lens.git
synced 2025-05-20 05:10:56 +00:00
Display resource load errors seperately from list of items (#4228)
This commit is contained in:
parent
df230d2bec
commit
159c240a80
@ -37,6 +37,32 @@ export interface KubeObjectStoreLoadingParams<K extends KubeObject> {
|
|||||||
namespaces: string[];
|
namespaces: string[];
|
||||||
api?: KubeApi<K>;
|
api?: KubeApi<K>;
|
||||||
reqInit?: RequestInit;
|
reqInit?: RequestInit;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A function that is called when listing fails. If set then blocks errors
|
||||||
|
* being rejected with
|
||||||
|
*/
|
||||||
|
onLoadFailure?: (err: any) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface KubeObjectStoreLoadAllParams {
|
||||||
|
namespaces?: string[];
|
||||||
|
merge?: boolean;
|
||||||
|
reqInit?: RequestInit;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A function that is called when listing fails. If set then blocks errors
|
||||||
|
* being rejected with
|
||||||
|
*/
|
||||||
|
onLoadFailure?: (err: any) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface KubeObjectStoreSubscribeParams {
|
||||||
|
/**
|
||||||
|
* A function that is called when listing fails. If set then blocks errors
|
||||||
|
* being rejected with
|
||||||
|
*/
|
||||||
|
onLoadFailure?: (err: any) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export abstract class KubeObjectStore<T extends KubeObject> extends ItemStore<T> {
|
export abstract class KubeObjectStore<T extends KubeObject> extends ItemStore<T> {
|
||||||
@ -141,30 +167,63 @@ export abstract class KubeObjectStore<T extends KubeObject> extends ItemStore<T>
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async loadItems({ namespaces, api, reqInit }: KubeObjectStoreLoadingParams<T>): Promise<T[]> {
|
protected async loadItems({ namespaces, api, reqInit, onLoadFailure }: KubeObjectStoreLoadingParams<T>): Promise<T[]> {
|
||||||
if (this.context?.cluster.isAllowedResource(api.kind)) {
|
if (!this.context?.cluster.isAllowedResource(api.kind)) {
|
||||||
if (!api.isNamespaced) {
|
return [];
|
||||||
return api.list({ reqInit }, this.query);
|
}
|
||||||
|
|
||||||
|
const isLoadingAll = this.context.allNamespaces?.length > 1
|
||||||
|
&& this.context.cluster.accessibleNamespaces.length === 0
|
||||||
|
&& this.context.allNamespaces.every(ns => namespaces.includes(ns));
|
||||||
|
|
||||||
|
if (!api.isNamespaced || isLoadingAll) {
|
||||||
|
if (api.isNamespaced) {
|
||||||
|
this.loadedNamespaces = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
const isLoadingAll = this.context.allNamespaces?.length > 1
|
const res = api.list({ reqInit }, this.query);
|
||||||
&& this.context.cluster.accessibleNamespaces.length === 0
|
|
||||||
&& this.context.allNamespaces.every(ns => namespaces.includes(ns));
|
|
||||||
|
|
||||||
if (isLoadingAll) {
|
if (onLoadFailure) {
|
||||||
this.loadedNamespaces = [];
|
try {
|
||||||
|
return await res;
|
||||||
|
} catch (error) {
|
||||||
|
onLoadFailure(error?.message || error?.toString() || "Unknown error");
|
||||||
|
|
||||||
return api.list({ reqInit }, this.query);
|
// reset the store because we are loading all, so that nothing is displayed
|
||||||
} else {
|
this.items.clear();
|
||||||
this.loadedNamespaces = namespaces;
|
this.selectedItemsIds.clear();
|
||||||
|
|
||||||
return Promise // load resources per namespace
|
return [];
|
||||||
.all(namespaces.map(namespace => api.list({ namespace, reqInit }, this.query)))
|
}
|
||||||
.then(items => items.flat().filter(Boolean));
|
}
|
||||||
|
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.loadedNamespaces = namespaces;
|
||||||
|
|
||||||
|
const results = await Promise.allSettled(
|
||||||
|
namespaces.map(namespace => api.list({ namespace, reqInit }, this.query)),
|
||||||
|
);
|
||||||
|
const res: T[] = [];
|
||||||
|
|
||||||
|
for (const result of results) {
|
||||||
|
switch (result.status) {
|
||||||
|
case "fulfilled":
|
||||||
|
res.push(...result.value);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "rejected":
|
||||||
|
if (onLoadFailure) {
|
||||||
|
onLoadFailure(result.reason.message);
|
||||||
|
} else {
|
||||||
|
// if onLoadFailure is not provided then preserve old behaviour
|
||||||
|
throw result.reason;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return [];
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected filterItemsOnLoad(items: T[]) {
|
protected filterItemsOnLoad(items: T[]) {
|
||||||
@ -172,18 +231,18 @@ export abstract class KubeObjectStore<T extends KubeObject> extends ItemStore<T>
|
|||||||
}
|
}
|
||||||
|
|
||||||
@action
|
@action
|
||||||
async loadAll(options: { namespaces?: string[], merge?: boolean, reqInit?: RequestInit } = {}): Promise<void | T[]> {
|
async loadAll(options: KubeObjectStoreLoadAllParams = {}): Promise<void | T[]> {
|
||||||
await this.contextReady;
|
await this.contextReady;
|
||||||
this.isLoading = true;
|
this.isLoading = true;
|
||||||
|
const {
|
||||||
|
namespaces = this.context.allNamespaces, // load all namespaces by default
|
||||||
|
merge = true, // merge loaded items or return as result
|
||||||
|
reqInit,
|
||||||
|
onLoadFailure,
|
||||||
|
} = options;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const {
|
const items = await this.loadItems({ namespaces, api: this.api, reqInit, onLoadFailure });
|
||||||
namespaces = this.context.allNamespaces, // load all namespaces by default
|
|
||||||
merge = true, // merge loaded items or return as result
|
|
||||||
reqInit,
|
|
||||||
} = options;
|
|
||||||
|
|
||||||
const items = await this.loadItems({ namespaces, api: this.api, reqInit });
|
|
||||||
|
|
||||||
if (merge) {
|
if (merge) {
|
||||||
this.mergeItems(items, { replace: false });
|
this.mergeItems(items, { replace: false });
|
||||||
@ -297,7 +356,7 @@ export abstract class KubeObjectStore<T extends KubeObject> extends ItemStore<T>
|
|||||||
async patch(item: T, patch: Patch): Promise<T> {
|
async patch(item: T, patch: Patch): Promise<T> {
|
||||||
return this.postUpdate(
|
return this.postUpdate(
|
||||||
await this.api.patch(
|
await this.api.patch(
|
||||||
{
|
{
|
||||||
name: item.getName(), namespace: item.getNs(),
|
name: item.getName(), namespace: item.getNs(),
|
||||||
},
|
},
|
||||||
patch,
|
patch,
|
||||||
@ -309,7 +368,7 @@ export abstract class KubeObjectStore<T extends KubeObject> extends ItemStore<T>
|
|||||||
async update(item: T, data: Partial<T>): Promise<T> {
|
async update(item: T, data: Partial<T>): Promise<T> {
|
||||||
return this.postUpdate(
|
return this.postUpdate(
|
||||||
await this.api.update(
|
await this.api.update(
|
||||||
{
|
{
|
||||||
name: item.getName(), namespace: item.getNs(),
|
name: item.getName(), namespace: item.getNs(),
|
||||||
},
|
},
|
||||||
data,
|
data,
|
||||||
@ -335,29 +394,29 @@ export abstract class KubeObjectStore<T extends KubeObject> extends ItemStore<T>
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
subscribe() {
|
subscribe(opts: KubeObjectStoreSubscribeParams = {}) {
|
||||||
const abortController = new AbortController();
|
const abortController = new AbortController();
|
||||||
|
|
||||||
if (this.api.isNamespaced) {
|
if (this.api.isNamespaced) {
|
||||||
Promise.race([rejectPromiseBy(abortController.signal), Promise.all([this.contextReady, this.namespacesReady])])
|
Promise.race([rejectPromiseBy(abortController.signal), Promise.all([this.contextReady, this.namespacesReady])])
|
||||||
.then(() => {
|
.then(() => {
|
||||||
if (this.context.cluster.isGlobalWatchEnabled && this.loadedNamespaces.length === 0) {
|
if (this.context.cluster.isGlobalWatchEnabled && this.loadedNamespaces.length === 0) {
|
||||||
return this.watchNamespace("", abortController);
|
return this.watchNamespace("", abortController, opts);
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const namespace of this.loadedNamespaces) {
|
for (const namespace of this.loadedNamespaces) {
|
||||||
this.watchNamespace(namespace, abortController);
|
this.watchNamespace(namespace, abortController, opts);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(noop); // ignore DOMExceptions
|
.catch(noop); // ignore DOMExceptions
|
||||||
} else {
|
} else {
|
||||||
this.watchNamespace("", abortController);
|
this.watchNamespace("", abortController, opts);
|
||||||
}
|
}
|
||||||
|
|
||||||
return () => abortController.abort();
|
return () => abortController.abort();
|
||||||
}
|
}
|
||||||
|
|
||||||
private watchNamespace(namespace: string, abortController: AbortController) {
|
private watchNamespace(namespace: string, abortController: AbortController, opts: KubeObjectStoreSubscribeParams) {
|
||||||
if (!this.api.getResourceVersion(namespace)) {
|
if (!this.api.getResourceVersion(namespace)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -389,8 +448,8 @@ export abstract class KubeObjectStore<T extends KubeObject> extends ItemStore<T>
|
|||||||
timedRetry = setTimeout(() => {
|
timedRetry = setTimeout(() => {
|
||||||
(
|
(
|
||||||
namespace
|
namespace
|
||||||
? this.loadAll({ namespaces: [namespace], reqInit: { signal }})
|
? this.loadAll({ namespaces: [namespace], reqInit: { signal }, ...opts })
|
||||||
: this.loadAll({ merge: false, reqInit: { signal }})
|
: this.loadAll({ merge: false, reqInit: { signal }, ...opts })
|
||||||
).then(watch);
|
).then(watch);
|
||||||
}, 1000);
|
}, 1000);
|
||||||
} else if (error) { // not sure what to do, best to retry
|
} else if (error) { // not sure what to do, best to retry
|
||||||
|
|||||||
@ -27,7 +27,7 @@ import type { ClusterContext } from "./cluster-context";
|
|||||||
|
|
||||||
import plimit from "p-limit";
|
import plimit from "p-limit";
|
||||||
import { comparer, observable, reaction, makeObservable } from "mobx";
|
import { comparer, observable, reaction, makeObservable } from "mobx";
|
||||||
import { autoBind, Disposer, noop } from "../utils";
|
import { autoBind, disposer, Disposer, noop } from "../utils";
|
||||||
import type { KubeApi } from "./kube-api";
|
import type { KubeApi } from "./kube-api";
|
||||||
import type { KubeJsonApiData } from "./kube-json-api";
|
import type { KubeJsonApiData } from "./kube-json-api";
|
||||||
import { isDebugging, isProduction } from "../vars";
|
import { isDebugging, isProduction } from "../vars";
|
||||||
@ -38,11 +38,38 @@ export interface IKubeWatchEvent<T extends KubeJsonApiData> {
|
|||||||
object?: T;
|
object?: T;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IKubeWatchSubscribeStoreOptions {
|
interface KubeWatchPreloadOptions {
|
||||||
namespaces?: string[]; // default: all accessible namespaces
|
/**
|
||||||
preload?: boolean; // preload store items, default: true
|
* The namespaces to watch
|
||||||
waitUntilLoaded?: boolean; // subscribe only after loading all stores, default: true
|
* @default all-accessible
|
||||||
loadOnce?: boolean; // check store.isLoaded to skip loading if done already, default: false
|
*/
|
||||||
|
namespaces?: string[];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether to skip loading if the store is already loaded
|
||||||
|
* @default false
|
||||||
|
*/
|
||||||
|
loadOnce?: boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A function that is called when listing fails. If set then blocks errors
|
||||||
|
* being rejected with
|
||||||
|
*/
|
||||||
|
onLoadFailure?: (err: any) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface KubeWatchSubscribeStoreOptions extends KubeWatchPreloadOptions {
|
||||||
|
/**
|
||||||
|
* Whether to subscribe only after loading all stores
|
||||||
|
* @default true
|
||||||
|
*/
|
||||||
|
waitUntilLoaded?: boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether to preload the stores before watching
|
||||||
|
* @default true
|
||||||
|
*/
|
||||||
|
preload?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IKubeWatchLog {
|
export interface IKubeWatchLog {
|
||||||
@ -63,15 +90,15 @@ export class KubeWatchApi {
|
|||||||
return Boolean(this.context?.cluster.isAllowedResource(api.kind));
|
return Boolean(this.context?.cluster.isAllowedResource(api.kind));
|
||||||
}
|
}
|
||||||
|
|
||||||
preloadStores(stores: KubeObjectStore<KubeObject>[], opts: { namespaces?: string[], loadOnce?: boolean } = {}) {
|
preloadStores(stores: KubeObjectStore<KubeObject>[], { loadOnce, namespaces, onLoadFailure }: KubeWatchPreloadOptions = {}) {
|
||||||
const limitRequests = plimit(1); // load stores one by one to allow quick skipping when fast clicking btw pages
|
const limitRequests = plimit(1); // load stores one by one to allow quick skipping when fast clicking btw pages
|
||||||
const preloading: Promise<any>[] = [];
|
const preloading: Promise<any>[] = [];
|
||||||
|
|
||||||
for (const store of stores) {
|
for (const store of stores) {
|
||||||
preloading.push(limitRequests(async () => {
|
preloading.push(limitRequests(async () => {
|
||||||
if (store.isLoaded && opts.loadOnce) return; // skip
|
if (store.isLoaded && loadOnce) return; // skip
|
||||||
|
|
||||||
return store.loadAll({ namespaces: opts.namespaces });
|
return store.loadAll({ namespaces, onLoadFailure });
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -81,22 +108,22 @@ export class KubeWatchApi {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
subscribeStores(stores: KubeObjectStore<KubeObject>[], opts: IKubeWatchSubscribeStoreOptions = {}): Disposer {
|
subscribeStores(stores: KubeObjectStore<KubeObject>[], opts: KubeWatchSubscribeStoreOptions = {}): Disposer {
|
||||||
const { preload = true, waitUntilLoaded = true, loadOnce = false } = opts;
|
const { preload = true, waitUntilLoaded = true, loadOnce = false, onLoadFailure } = opts;
|
||||||
const subscribingNamespaces = opts.namespaces ?? this.context?.allNamespaces ?? [];
|
const subscribingNamespaces = opts.namespaces ?? this.context?.allNamespaces ?? [];
|
||||||
const unsubscribeList: Function[] = [];
|
const unsubscribeStores = disposer();
|
||||||
let isUnsubscribed = false;
|
let isUnsubscribed = false;
|
||||||
|
|
||||||
const load = (namespaces = subscribingNamespaces) => this.preloadStores(stores, { namespaces, loadOnce });
|
const load = (namespaces = subscribingNamespaces) => this.preloadStores(stores, { namespaces, loadOnce, onLoadFailure });
|
||||||
let preloading = preload && load();
|
let preloading = preload && load();
|
||||||
let cancelReloading: Disposer = noop;
|
let cancelReloading: Disposer = noop;
|
||||||
|
|
||||||
const subscribe = () => {
|
const subscribe = () => {
|
||||||
if (isUnsubscribed) return;
|
if (isUnsubscribed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
stores.forEach((store) => {
|
unsubscribeStores.push(...stores.map(store => store.subscribe({ onLoadFailure })));
|
||||||
unsubscribeList.push(store.subscribe());
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
if (preloading) {
|
if (preloading) {
|
||||||
@ -114,8 +141,7 @@ export class KubeWatchApi {
|
|||||||
// reload stores only for context namespaces change
|
// reload stores only for context namespaces change
|
||||||
cancelReloading = reaction(() => this.context?.contextNamespaces, namespaces => {
|
cancelReloading = reaction(() => this.context?.contextNamespaces, namespaces => {
|
||||||
preloading?.cancelLoading();
|
preloading?.cancelLoading();
|
||||||
unsubscribeList.forEach(unsubscribe => unsubscribe());
|
unsubscribeStores();
|
||||||
unsubscribeList.length = 0;
|
|
||||||
preloading = load(namespaces);
|
preloading = load(namespaces);
|
||||||
preloading.loading.then(subscribe);
|
preloading.loading.then(subscribe);
|
||||||
}, {
|
}, {
|
||||||
@ -129,8 +155,7 @@ export class KubeWatchApi {
|
|||||||
isUnsubscribed = true;
|
isUnsubscribed = true;
|
||||||
cancelReloading();
|
cancelReloading();
|
||||||
preloading?.cancelLoading();
|
preloading?.cancelLoading();
|
||||||
unsubscribeList.forEach(unsubscribe => unsubscribe());
|
unsubscribeStores();
|
||||||
unsubscribeList.length = 0;
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -0,0 +1,27 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2021 OpenLens Authors
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||||
|
* this software and associated documentation files (the "Software"), to deal in
|
||||||
|
* the Software without restriction, including without limitation the rights to
|
||||||
|
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||||
|
* the Software, and to permit persons to whom the Software is furnished to do so,
|
||||||
|
* subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||||
|
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||||
|
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||||
|
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||||
|
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
.KubeObjectListLayout {
|
||||||
|
.Icon.load-error {
|
||||||
|
color: var(--colorWarning);
|
||||||
|
float: right;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -19,10 +19,12 @@
|
|||||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import "./kube-object-list-layout.scss";
|
||||||
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { computed, makeObservable } from "mobx";
|
import { computed, makeObservable, observable, reaction } from "mobx";
|
||||||
import { disposeOnUnmount, observer } from "mobx-react";
|
import { disposeOnUnmount, observer } from "mobx-react";
|
||||||
import { cssNames } from "../../utils";
|
import { cssNames, Disposer } from "../../utils";
|
||||||
import type { KubeObject } from "../../../common/k8s-api/kube-object";
|
import type { KubeObject } from "../../../common/k8s-api/kube-object";
|
||||||
import { ItemListLayout, ItemListLayoutProps } from "../item-object-list/item-list-layout";
|
import { ItemListLayout, ItemListLayoutProps } from "../item-object-list/item-list-layout";
|
||||||
import type { KubeObjectStore } from "../../../common/k8s-api/kube-object.store";
|
import type { KubeObjectStore } from "../../../common/k8s-api/kube-object.store";
|
||||||
@ -32,6 +34,8 @@ import { clusterContext } from "../context";
|
|||||||
import { NamespaceSelectFilter } from "../+namespaces/namespace-select-filter";
|
import { NamespaceSelectFilter } from "../+namespaces/namespace-select-filter";
|
||||||
import { ResourceKindMap, ResourceNames } from "../../utils/rbac";
|
import { ResourceKindMap, ResourceNames } from "../../utils/rbac";
|
||||||
import { kubeSelectedUrlParam, toggleDetails } from "../kube-detail-params";
|
import { kubeSelectedUrlParam, toggleDetails } from "../kube-detail-params";
|
||||||
|
import { Icon } from "../icon";
|
||||||
|
import { TooltipPosition } from "../tooltip";
|
||||||
|
|
||||||
export interface KubeObjectListLayoutProps<K extends KubeObject> extends ItemListLayoutProps<K> {
|
export interface KubeObjectListLayoutProps<K extends KubeObject> extends ItemListLayoutProps<K> {
|
||||||
store: KubeObjectStore<K>;
|
store: KubeObjectStore<K>;
|
||||||
@ -53,6 +57,8 @@ export class KubeObjectListLayout<K extends KubeObject> extends React.Component<
|
|||||||
makeObservable(this);
|
makeObservable(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@observable loadErrors: string[] = [];
|
||||||
|
|
||||||
@computed get selectedItem() {
|
@computed get selectedItem() {
|
||||||
return this.props.store.getByPath(kubeSelectedUrlParam.get());
|
return this.props.store.getByPath(kubeSelectedUrlParam.get());
|
||||||
}
|
}
|
||||||
@ -60,15 +66,45 @@ export class KubeObjectListLayout<K extends KubeObject> extends React.Component<
|
|||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
const { store, dependentStores = [], subscribeStores } = this.props;
|
const { store, dependentStores = [], subscribeStores } = this.props;
|
||||||
const stores = Array.from(new Set([store, ...dependentStores]));
|
const stores = Array.from(new Set([store, ...dependentStores]));
|
||||||
|
const reactions: Disposer[] = [
|
||||||
|
reaction(() => clusterContext.contextNamespaces.length, () => {
|
||||||
|
// clear load errors
|
||||||
|
this.loadErrors.length = 0;
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
|
||||||
if (subscribeStores) {
|
if (subscribeStores) {
|
||||||
disposeOnUnmount(this, [
|
reactions.push(
|
||||||
kubeWatchApi.subscribeStores(stores, {
|
kubeWatchApi.subscribeStores(stores, {
|
||||||
preload: true,
|
preload: true,
|
||||||
namespaces: clusterContext.contextNamespaces,
|
namespaces: clusterContext.contextNamespaces,
|
||||||
|
onLoadFailure: error => this.loadErrors.push(String(error)),
|
||||||
}),
|
}),
|
||||||
]);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
disposeOnUnmount(this, reactions);
|
||||||
|
}
|
||||||
|
|
||||||
|
renderLoadErrors() {
|
||||||
|
if (this.loadErrors.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Icon
|
||||||
|
material="warning"
|
||||||
|
className="load-error"
|
||||||
|
tooltip={{
|
||||||
|
children: (
|
||||||
|
<>
|
||||||
|
{this.loadErrors.map((error, index) => <p key={index}>{error}</p>)}
|
||||||
|
</>
|
||||||
|
),
|
||||||
|
preferredPositions: TooltipPosition.BOTTOM,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
@ -84,7 +120,7 @@ export class KubeObjectListLayout<K extends KubeObject> extends React.Component<
|
|||||||
preloadStores={false} // loading handled in kubeWatchApi.subscribeStores()
|
preloadStores={false} // loading handled in kubeWatchApi.subscribeStores()
|
||||||
detailsItem={this.selectedItem}
|
detailsItem={this.selectedItem}
|
||||||
customizeHeader={[
|
customizeHeader={[
|
||||||
({ filters, searchProps, ...headerPlaceHolders }) => ({
|
({ filters, searchProps, info, ...headerPlaceHolders }) => ({
|
||||||
filters: (
|
filters: (
|
||||||
<>
|
<>
|
||||||
{filters}
|
{filters}
|
||||||
@ -95,6 +131,12 @@ export class KubeObjectListLayout<K extends KubeObject> extends React.Component<
|
|||||||
...searchProps,
|
...searchProps,
|
||||||
placeholder: `Search ${placeholderString}...`,
|
placeholder: `Search ${placeholderString}...`,
|
||||||
},
|
},
|
||||||
|
info: (
|
||||||
|
<>
|
||||||
|
{info}
|
||||||
|
{this.renderLoadErrors()}
|
||||||
|
</>
|
||||||
|
),
|
||||||
...headerPlaceHolders,
|
...headerPlaceHolders,
|
||||||
}),
|
}),
|
||||||
...[customizeHeader].flat(),
|
...[customizeHeader].flat(),
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user