1
0
mirror of https://github.com/lensapp/lens.git synced 2025-05-20 05:10:56 +00:00

loading k8s resources into stores per selected namespaces -- part 3

Signed-off-by: Roman <ixrock@gmail.com>
This commit is contained in:
Roman 2021-01-08 13:11:52 +02:00
parent f71829ff14
commit 51bdfc131b
5 changed files with 127 additions and 84 deletions

View File

@ -169,8 +169,7 @@ export class KubeWatchApi {
addListener(store: KubeObjectStore, callback: (evt: IKubeWatchEvent) => void) {
const listener = (evt: IKubeWatchEvent<KubeJsonApiData>) => {
if (evt.type === "ERROR") {
// console.error(evt.object);
return; // fixme: too old resource version (e.g. reproduce: quickly jump btw pages)
return; // e.g. evt.object.message == "too old resource version"
}
const { namespace, resourceVersion } = evt.object.metadata;

View File

@ -1,4 +1,4 @@
import { action, comparer, observable, reaction, when } from "mobx";
import { action, comparer, IReactionDisposer, IReactionOptions, observable, reaction, when } from "mobx";
import { autobind, createStorage } from "../../utils";
import { KubeObjectStore, KubeObjectStoreLoadingParams } from "../../kube-object.store";
import { Namespace, namespacesApi } from "../../api/endpoints/namespaces.api";
@ -32,30 +32,52 @@ export class NamespaceStore extends KubeObjectStore<Namespace> {
this.init();
}
onContextChange(callback: (contextNamespaces: string[]) => void, opts: IReactionOptions = {}): IReactionDisposer {
return reaction(() => this.contextNs.toJS(), callback, {
equals: comparer.identity,
...opts,
})
}
private async init() {
await clusterStore.whenLoaded;
if (!getHostedCluster()) return;
await getHostedCluster().whenReady; // wait for cluster-state from main
await this.loadAll(); // auto-load allowed namespaces
this.isReady = true;
this.setContext(this.initNamespaces);
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,
});
const disposers: IReactionDisposer[] = [];
// save selected namespaces to local-storage and update URL
disposers.push(
this.onContextChange(namespaces => {
storage.set(namespaces);
namespaceUrlParam.set(namespaces, { replaceHistory: true });
}, {
fireImmediately: true,
equals: comparer.identity,
})
);
// auto-load allowed namespaces
disposers.push(
reaction(() => this.allowedNamespaces, () => {
this.loadAll();
this.setContext(this.initNamespaces)
}, {
fireImmediately: true,
equals: comparer.identity,
})
);
return disposers;
}
get allowedNamespaces(): string[] {
return getHostedCluster().allowedNamespaces;
}
// FIXME: page/app reload doesn't restore previously selected namespaces
get initNamespaces() {
const allowedNamespaces = new Set(this.allowedNamespaces);
const lastUsedNamespaces = new Set(storage.get());
@ -118,6 +140,9 @@ export class NamespaceStore extends KubeObjectStore<Namespace> {
}
protected async loadItems({ isAdmin, namespaces }: KubeObjectStoreLoadingParams) {
if (isAdmin) {
return this.api.list();
}
if (!isAllowedResource("namespaces")) {
return namespaces.map(this.getDummyNamespace);
}

View File

@ -17,58 +17,56 @@ 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<IWorkloadsOverviewRouteParams> {
}
@observer
export class WorkloadsOverview extends React.Component<Props> {
@observable isLoading = false;
@observable isUnmounting = false;
async componentDidMount() {
const stores: KubeObjectStore[] = [];
const stores: KubeObjectStore[] = [
isAllowedResource("pods") && podsStore,
isAllowedResource("deployments") && deploymentStore,
isAllowedResource("daemonsets") && daemonSetStore,
isAllowedResource("statefulsets") && statefulSetStore,
isAllowedResource("statefulsets") && statefulSetStore,
isAllowedResource("replicasets") && replicaSetStore,
isAllowedResource("jobs") && jobStore,
isAllowedResource("cronjobs") && cronJobStore,
isAllowedResource("events") && eventStore,
].filter(Boolean);
if (isAllowedResource("pods")) {
stores.push(podsStore);
const unsubscribeMap = new Map<KubeObjectStore, Function>(
stores.map(store => [store, Function])
);
const loadStores = async () => {
this.isLoading = true;
for (const store of stores) {
if (this.isUnmounting) break;
try {
store.reset();
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("deployments")) {
stores.push(deploymentStore);
}
namespaceStore.onContextChange(loadStores, {
fireImmediately: true,
});
if (isAllowedResource("daemonsets")) {
stores.push(daemonSetStore);
}
if (isAllowedResource("statefulsets")) {
stores.push(statefulSetStore);
}
if (isAllowedResource("replicasets")) {
stores.push(replicaSetStore);
}
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());
await when(() => this.isUnmounting && !this.isLoading);
unsubscribeMap.forEach(dispose => dispose());
unsubscribeMap.clear();
}
componentWillUnmount() {
@ -79,11 +77,11 @@ export class WorkloadsOverview extends React.Component<Props> {
return (
<>
<OverviewStatuses/>
{ isAllowedResource("events") && <Events
{isAllowedResource("events") && <Events
compact
hideFilters
className="box grow"
/> }
/>}
</>
);
}

View File

@ -2,10 +2,10 @@ import "./item-list-layout.scss";
import groupBy from "lodash/groupBy";
import React, { ReactNode } from "react";
import { computed, observable, reaction, toJS, when } from "mobx";
import { computed, IReactionDisposer, observable, reaction, toJS, when } from "mobx";
import { disposeOnUnmount, observer } from "mobx-react";
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 { AddRemoveButtons, AddRemoveButtonsProps } from "../add-remove-buttons";
import { NoItems } from "../no-items";
@ -90,6 +90,9 @@ interface ItemListLayoutUserSettings {
export class ItemListLayout extends React.Component<ItemListLayoutProps> {
static defaultProps = defaultProps as object;
private watchDisposers: IReactionDisposer[] = [];
@observable isLoaded = false;
@observable isUnmounting = false;
// default user settings (ui show-hide tweaks mostly)
@ -110,30 +113,49 @@ export class ItemListLayout extends React.Component<ItemListLayoutProps> {
]);
}
// FIXME: reload and re-subscribe stores when context namespaces changed
async componentDidMount() {
const { store, dependentStores, isClusterScoped } = this.props;
const stores = [store, ...dependentStores];
if (!isClusterScoped) stores.push(namespaceStore);
try {
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);
}
disposeOnUnmount(this, [
namespaceStore.onContextChange(() => this.loadStores(), {
fireImmediately: true,
})
]);
}
componentWillUnmount() {
async componentWillUnmount() {
this.isUnmounting = true;
const { store, isSelectable } = this.props;
await when(() => this.isLoaded);
this.unsubscribeStores();
}
if (isSelectable) store.resetSelection();
async loadStores() {
const { store, dependentStores, isClusterScoped } = this.props;
const stores = new Set([store, ...dependentStores]);
if (!isClusterScoped) {
stores.add(namespaceStore);
}
// reset
this.isLoaded = false;
this.unsubscribeStores();
// load
for (let store of stores) {
if (this.isUnmounting) break;
try {
store.reset();
await store.loadAll();
this.watchDisposers.push(store.subscribe());
} catch (error) {
console.error("loading store error", error);
}
}
this.isLoaded = true;
}
unsubscribeStores() {
this.watchDisposers.forEach(dispose => dispose());
this.watchDisposers.length = 0;
}
private filterCallbacks: { [type: string]: ItemsFilter } = {
@ -300,7 +322,7 @@ export class ItemListLayout extends React.Component<ItemListLayoutProps> {
return;
}
return <PageFiltersList filters={filters} />;
return <PageFiltersList filters={filters}/>;
}
renderNoItems() {
@ -322,7 +344,7 @@ export class ItemListLayout extends React.Component<ItemListLayoutProps> {
);
}
return <NoItems />;
return <NoItems/>;
}
renderHeaderContent(placeholders: IHeaderPlaceholders): ReactNode {
@ -366,12 +388,12 @@ export class ItemListLayout extends React.Component<ItemListLayoutProps> {
title: <h5 className="title">{title}</h5>,
info: this.renderInfo(),
filters: <>
{!isClusterScoped && <NamespaceSelectFilter />}
{!isClusterScoped && <NamespaceSelectFilter/>}
<PageFiltersSelect allowEmpty disableFilters={{
[FilterType.NAMESPACE]: true, // namespace-select used instead
}} />
}}/>
</>,
search: <SearchInputUrl />,
search: <SearchInputUrl/>,
};
let header = this.renderHeaderContent(placeholders);
@ -407,7 +429,7 @@ export class ItemListLayout extends React.Component<ItemListLayoutProps> {
return (
<div className="items box grow flex column">
{!isReady && (
<Spinner center />
<Spinner center/>
)}
{isReady && (
<Table
@ -433,7 +455,7 @@ export class ItemListLayout extends React.Component<ItemListLayoutProps> {
/>
)}
{renderTableHeader.map((cellProps, index) => <TableCell key={index} {...cellProps} />)}
{renderItemMenu && <TableCell className="menu" />}
{renderItemMenu && <TableCell className="menu"/>}
</TableHead>
)}
{

View File

@ -9,8 +9,7 @@ export interface ItemObject {
@autobind()
export abstract class ItemStore<T extends ItemObject = ItemObject> {
abstract loadAll(...args: any[]): any;
abstract loadAll(): Promise<void>;
abstract loadAll(...args: any[]): Promise<any>;
protected defaultSorting = (item: T) => item.getName();