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:
parent
f71829ff14
commit
51bdfc131b
@ -169,8 +169,7 @@ 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") {
|
if (evt.type === "ERROR") {
|
||||||
// console.error(evt.object);
|
return; // e.g. evt.object.message == "too old resource version"
|
||||||
return; // fixme: too old resource version (e.g. reproduce: quickly jump btw pages)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const { namespace, resourceVersion } = evt.object.metadata;
|
const { namespace, resourceVersion } = evt.object.metadata;
|
||||||
|
|||||||
@ -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 { autobind, createStorage } from "../../utils";
|
||||||
import { KubeObjectStore, KubeObjectStoreLoadingParams } from "../../kube-object.store";
|
import { KubeObjectStore, KubeObjectStoreLoadingParams } from "../../kube-object.store";
|
||||||
import { Namespace, namespacesApi } from "../../api/endpoints/namespaces.api";
|
import { Namespace, namespacesApi } from "../../api/endpoints/namespaces.api";
|
||||||
@ -32,30 +32,52 @@ export class NamespaceStore extends KubeObjectStore<Namespace> {
|
|||||||
this.init();
|
this.init();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onContextChange(callback: (contextNamespaces: string[]) => void, opts: IReactionOptions = {}): IReactionDisposer {
|
||||||
|
return reaction(() => this.contextNs.toJS(), callback, {
|
||||||
|
equals: comparer.identity,
|
||||||
|
...opts,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
private async init() {
|
private async init() {
|
||||||
await clusterStore.whenLoaded;
|
await clusterStore.whenLoaded;
|
||||||
if (!getHostedCluster()) return;
|
if (!getHostedCluster()) return;
|
||||||
|
|
||||||
await getHostedCluster().whenReady; // wait for cluster-state from main
|
await getHostedCluster().whenReady; // wait for cluster-state from main
|
||||||
await this.loadAll(); // auto-load allowed namespaces
|
|
||||||
this.isReady = true;
|
this.isReady = true;
|
||||||
|
|
||||||
this.setContext(this.initNamespaces);
|
this.setContext(this.initNamespaces);
|
||||||
|
|
||||||
return reaction(() => this.contextNs.toJS(), namespaces => {
|
const disposers: IReactionDisposer[] = [];
|
||||||
storage.set(namespaces); // save to local-storage
|
|
||||||
namespaceUrlParam.set(namespaces, { replaceHistory: true }); // update url
|
// save selected namespaces to local-storage and update URL
|
||||||
}, {
|
disposers.push(
|
||||||
fireImmediately: true,
|
this.onContextChange(namespaces => {
|
||||||
equals: comparer.identity,
|
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[] {
|
get allowedNamespaces(): string[] {
|
||||||
return getHostedCluster().allowedNamespaces;
|
return getHostedCluster().allowedNamespaces;
|
||||||
}
|
}
|
||||||
|
|
||||||
// FIXME: page/app reload doesn't restore previously selected namespaces
|
|
||||||
get initNamespaces() {
|
get initNamespaces() {
|
||||||
const allowedNamespaces = new Set(this.allowedNamespaces);
|
const allowedNamespaces = new Set(this.allowedNamespaces);
|
||||||
const lastUsedNamespaces = new Set(storage.get());
|
const lastUsedNamespaces = new Set(storage.get());
|
||||||
@ -118,6 +140,9 @@ export class NamespaceStore extends KubeObjectStore<Namespace> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected async loadItems({ isAdmin, namespaces }: KubeObjectStoreLoadingParams) {
|
protected async loadItems({ isAdmin, namespaces }: KubeObjectStoreLoadingParams) {
|
||||||
|
if (isAdmin) {
|
||||||
|
return this.api.list();
|
||||||
|
}
|
||||||
if (!isAllowedResource("namespaces")) {
|
if (!isAllowedResource("namespaces")) {
|
||||||
return namespaces.map(this.getDummyNamespace);
|
return namespaces.map(this.getDummyNamespace);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -17,58 +17,56 @@ 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("statefulsets") && statefulSetStore,
|
||||||
|
isAllowedResource("replicasets") && replicaSetStore,
|
||||||
|
isAllowedResource("jobs") && jobStore,
|
||||||
|
isAllowedResource("cronjobs") && cronJobStore,
|
||||||
|
isAllowedResource("events") && eventStore,
|
||||||
|
].filter(Boolean);
|
||||||
|
|
||||||
if (isAllowedResource("pods")) {
|
const unsubscribeMap = new Map<KubeObjectStore, Function>(
|
||||||
stores.push(podsStore);
|
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")) {
|
namespaceStore.onContextChange(loadStores, {
|
||||||
stores.push(deploymentStore);
|
fireImmediately: true,
|
||||||
}
|
});
|
||||||
|
|
||||||
if (isAllowedResource("daemonsets")) {
|
await when(() => this.isUnmounting && !this.isLoading);
|
||||||
stores.push(daemonSetStore);
|
unsubscribeMap.forEach(dispose => dispose());
|
||||||
}
|
unsubscribeMap.clear();
|
||||||
|
|
||||||
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());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount() {
|
componentWillUnmount() {
|
||||||
@ -79,11 +77,11 @@ export class WorkloadsOverview extends React.Component<Props> {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<OverviewStatuses/>
|
<OverviewStatuses/>
|
||||||
{ isAllowedResource("events") && <Events
|
{isAllowedResource("events") && <Events
|
||||||
compact
|
compact
|
||||||
hideFilters
|
hideFilters
|
||||||
className="box grow"
|
className="box grow"
|
||||||
/> }
|
/>}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,10 +2,10 @@ import "./item-list-layout.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, when } 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";
|
||||||
@ -90,6 +90,9 @@ interface ItemListLayoutUserSettings {
|
|||||||
export class ItemListLayout extends React.Component<ItemListLayoutProps> {
|
export class ItemListLayout extends React.Component<ItemListLayoutProps> {
|
||||||
static defaultProps = defaultProps as object;
|
static defaultProps = defaultProps as object;
|
||||||
|
|
||||||
|
private watchDisposers: IReactionDisposer[] = [];
|
||||||
|
|
||||||
|
@observable isLoaded = false;
|
||||||
@observable isUnmounting = false;
|
@observable isUnmounting = false;
|
||||||
|
|
||||||
// default user settings (ui show-hide tweaks mostly)
|
// 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() {
|
async componentDidMount() {
|
||||||
const { store, dependentStores, isClusterScoped } = this.props;
|
disposeOnUnmount(this, [
|
||||||
const stores = [store, ...dependentStores];
|
namespaceStore.onContextChange(() => this.loadStores(), {
|
||||||
|
fireImmediately: true,
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount() {
|
async componentWillUnmount() {
|
||||||
this.isUnmounting = true;
|
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 } = {
|
private filterCallbacks: { [type: string]: ItemsFilter } = {
|
||||||
@ -300,7 +322,7 @@ export class ItemListLayout extends React.Component<ItemListLayoutProps> {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
return <PageFiltersList filters={filters} />;
|
return <PageFiltersList filters={filters}/>;
|
||||||
}
|
}
|
||||||
|
|
||||||
renderNoItems() {
|
renderNoItems() {
|
||||||
@ -322,7 +344,7 @@ export class ItemListLayout extends React.Component<ItemListLayoutProps> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return <NoItems />;
|
return <NoItems/>;
|
||||||
}
|
}
|
||||||
|
|
||||||
renderHeaderContent(placeholders: IHeaderPlaceholders): ReactNode {
|
renderHeaderContent(placeholders: IHeaderPlaceholders): ReactNode {
|
||||||
@ -366,12 +388,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);
|
||||||
|
|
||||||
@ -407,7 +429,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
|
||||||
@ -433,7 +455,7 @@ export class ItemListLayout extends React.Component<ItemListLayoutProps> {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{renderTableHeader.map((cellProps, index) => <TableCell key={index} {...cellProps} />)}
|
{renderTableHeader.map((cellProps, index) => <TableCell key={index} {...cellProps} />)}
|
||||||
{renderItemMenu && <TableCell className="menu" />}
|
{renderItemMenu && <TableCell className="menu"/>}
|
||||||
</TableHead>
|
</TableHead>
|
||||||
)}
|
)}
|
||||||
{
|
{
|
||||||
|
|||||||
@ -9,8 +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(...args: any[]): any;
|
abstract loadAll(...args: any[]): Promise<any>;
|
||||||
abstract loadAll(): Promise<void>;
|
|
||||||
|
|
||||||
protected defaultSorting = (item: T) => item.getName();
|
protected defaultSorting = (item: T) => item.getName();
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user