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

Fix detail views not watching child components

- Add subscribeStores calls to all relavent details

- Add support for tracking overlapping subscribes as an optimization

Signed-off-by: Sebastian Malton <sebastian@malton.name>
This commit is contained in:
Sebastian Malton 2021-11-15 14:34:38 -05:00 committed by Jim Ehrismann
parent df230d2bec
commit 624ac4680d
22 changed files with 242 additions and 207 deletions

View File

@ -33,9 +33,14 @@ import type { RequestInit } from "node-fetch";
import AbortController from "abort-controller"; import AbortController from "abort-controller";
import type { Patch } from "rfc6902"; import type { Patch } from "rfc6902";
export interface KubeObjectStoreLoadingParams<K extends KubeObject> { export interface KubeObjectStoreLoadingParams {
namespaces: string[]; namespaces: string[];
api?: KubeApi<K>; reqInit?: RequestInit;
}
export interface KubeObjectStoreLoadAllParams {
namespaces?: string[];
merge?: boolean;
reqInit?: RequestInit; reqInit?: RequestInit;
} }
@ -141,10 +146,10 @@ export abstract class KubeObjectStore<T extends KubeObject> extends ItemStore<T>
} }
} }
protected async loadItems({ namespaces, api, reqInit }: KubeObjectStoreLoadingParams<T>): Promise<T[]> { protected async loadItems({ namespaces, reqInit }: KubeObjectStoreLoadingParams): Promise<T[]> {
if (this.context?.cluster.isAllowedResource(api.kind)) { if (this.context?.cluster.isAllowedResource(this.api.kind)) {
if (!api.isNamespaced) { if (!this.api.isNamespaced) {
return api.list({ reqInit }, this.query); return this.api.list({ reqInit }, this.query);
} }
const isLoadingAll = this.context.allNamespaces?.length > 1 const isLoadingAll = this.context.allNamespaces?.length > 1
@ -154,12 +159,12 @@ export abstract class KubeObjectStore<T extends KubeObject> extends ItemStore<T>
if (isLoadingAll) { if (isLoadingAll) {
this.loadedNamespaces = []; this.loadedNamespaces = [];
return api.list({ reqInit }, this.query); return this.api.list({ reqInit }, this.query);
} else { } else {
this.loadedNamespaces = namespaces; this.loadedNamespaces = namespaces;
return Promise // load resources per namespace return Promise // load resources per namespace
.all(namespaces.map(namespace => api.list({ namespace, reqInit }, this.query))) .all(namespaces.map(namespace => this.api.list({ namespace, reqInit }, this.query)))
.then(items => items.flat().filter(Boolean)); .then(items => items.flat().filter(Boolean));
} }
} }
@ -172,24 +177,14 @@ 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({ namespaces = this.context.contextNamespaces, merge = true, reqInit }: KubeObjectStoreLoadAllParams = {}): Promise<void | T[]> {
await this.contextReady; await this.contextReady;
this.isLoading = true; this.isLoading = true;
try { try {
const { const items = await this.loadItems({ namespaces, reqInit });
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 }); this.mergeItems(items, { merge });
if (merge) {
this.mergeItems(items, { replace: false });
} else {
this.mergeItems(items, { replace: true });
}
this.isLoaded = true; this.isLoaded = true;
this.failedLoading = false; this.failedLoading = false;
@ -216,11 +211,11 @@ export abstract class KubeObjectStore<T extends KubeObject> extends ItemStore<T>
} }
@action @action
protected mergeItems(partialItems: T[], { replace = false, updateStore = true, sort = true, filter = true } = {}): T[] { protected mergeItems(partialItems: T[], { merge = true, updateStore = true, sort = true, filter = true } = {}): T[] {
let items = partialItems; let items = partialItems;
// update existing items // update existing items
if (!replace) { if (merge) {
const namespaces = partialItems.map(item => item.getNs()); const namespaces = partialItems.map(item => item.getNs());
items = [ items = [
@ -335,9 +330,7 @@ export abstract class KubeObjectStore<T extends KubeObject> extends ItemStore<T>
}); });
} }
subscribe() { subscribe(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(() => {

View File

@ -25,131 +25,131 @@
import type { KubeObjectStore } from "./kube-object.store"; import type { KubeObjectStore } from "./kube-object.store";
import type { ClusterContext } from "./cluster-context"; import type { ClusterContext } from "./cluster-context";
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, ExtendedMap, noop } from "../utils";
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 { isProduction } from "../vars";
import type { KubeObject } from "./kube-object"; import type { KubeObject } from "./kube-object";
import AbortController from "abort-controller";
import { once } from "lodash";
class WrappedAbortController extends AbortController {
constructor(protected parent: AbortController) {
super();
parent.signal.addEventListener("abort", () => {
this.abort();
});
}
}
export interface IKubeWatchEvent<T extends KubeJsonApiData> { export interface IKubeWatchEvent<T extends KubeJsonApiData> {
type: "ADDED" | "MODIFIED" | "DELETED" | "ERROR"; type: "ADDED" | "MODIFIED" | "DELETED" | "ERROR";
object?: T; object?: T;
} }
export interface IKubeWatchSubscribeStoreOptions { export interface KubeWatchSubscribeStoreOptions {
namespaces?: string[]; // default: all accessible namespaces /**
preload?: boolean; // preload store items, default: true * The namespaces to watch, if not specified then changes to the set of
waitUntilLoaded?: boolean; // subscribe only after loading all stores, default: true * selected namespaces will be watched as well
loadOnce?: boolean; // check store.isLoaded to skip loading if done already, default: false *
} * @default all selected namespaces
*/
export interface IKubeWatchLog { namespaces?: string[];
message: string | string[] | Error;
meta?: object;
cssStyle?: string;
} }
export class KubeWatchApi { export class KubeWatchApi {
@observable context: ClusterContext = null; @observable context: ClusterContext = null;
duplicateWatchSet = new ExtendedMap<KubeObjectStore<KubeObject>, number>();
constructor() { constructor() {
makeObservable(this); makeObservable(this);
autoBind(this); autoBind(this);
} }
isAllowedApi(api: KubeApi<KubeObject>): boolean { private subscribeStore(store: KubeObjectStore<KubeObject>, parent: AbortController, watchChanges: boolean, namespaces: string[]): Disposer {
return Boolean(this.context?.cluster.isAllowedResource(api.kind)); const count = this.duplicateWatchSet.getOrInsert(store, () => 1);
}
preloadStores(stores: KubeObjectStore<KubeObject>[], opts: { namespaces?: string[], loadOnce?: boolean } = {}) { if (count > 1) {
const limitRequests = plimit(1); // load stores one by one to allow quick skipping when fast clicking btw pages // don't load or subscribe to a store more than once
const preloading: Promise<any>[] = []; return () => {
this.duplicateWatchSet.set(store, this.duplicateWatchSet.get(store) - 1);
for (const store of stores) {
preloading.push(limitRequests(async () => {
if (store.isLoaded && opts.loadOnce) return; // skip
return store.loadAll({ namespaces: opts.namespaces });
}));
}
return {
loading: Promise.allSettled(preloading),
cancelLoading: () => limitRequests.clearQueue(),
}; };
} }
subscribeStores(stores: KubeObjectStore<KubeObject>[], opts: IKubeWatchSubscribeStoreOptions = {}): Disposer { let childController = new WrappedAbortController(parent);
const { preload = true, waitUntilLoaded = true, loadOnce = false } = opts; const unsubscribe = disposer();
const subscribingNamespaces = opts.namespaces ?? this.context?.allNamespaces ?? [];
const unsubscribeList: Function[] = [];
let isUnsubscribed = false;
const load = (namespaces = subscribingNamespaces) => this.preloadStores(stores, { namespaces, loadOnce }); const loadThenSubscribe = async (namespaces: string[]) => {
let preloading = preload && load(); try {
let cancelReloading: Disposer = noop; await store.loadAll({ namespaces, reqInit: { signal: childController.signal }});
unsubscribe.push(store.subscribe(childController));
const subscribe = () => { } catch (error) {
if (isUnsubscribed) return; if (!(error instanceof DOMException)) {
this.log(Object.assign(new Error("Loading stores has failed"), { cause: error }), {
stores.forEach((store) => { meta: { store, namespaces },
unsubscribeList.push(store.subscribe());
}); });
}
}
}; };
if (preloading) { loadThenSubscribe(namespaces);
if (waitUntilLoaded) {
preloading.loading.then(subscribe, error => {
this.log({
message: new Error("Loading stores has failed"),
meta: { stores, error, options: opts },
});
});
} else {
subscribe();
}
// reload stores only for context namespaces change const cancelReloading = watchChanges
cancelReloading = reaction(() => this.context?.contextNamespaces, namespaces => { ? noop // don't watch namespaces if namespaces were provided
preloading?.cancelLoading(); : reaction(() => this.context.contextNamespaces, namespaces => {
unsubscribeList.forEach(unsubscribe => unsubscribe()); childController.abort();
unsubscribeList.length = 0; unsubscribe();
preloading = load(namespaces); childController = new WrappedAbortController(parent);
preloading.loading.then(subscribe); loadThenSubscribe(namespaces);
}, { }, {
equals: comparer.shallow, equals: comparer.shallow,
}); });
}
// unsubscribe
return () => { return () => {
if (isUnsubscribed) return; const newCount = this.duplicateWatchSet.get(store) - 1;
isUnsubscribed = true;
this.duplicateWatchSet.set(store, newCount);
if (newCount === 0) {
cancelReloading(); cancelReloading();
preloading?.cancelLoading(); childController.abort();
unsubscribeList.forEach(unsubscribe => unsubscribe()); unsubscribe();
unsubscribeList.length = 0; }
}; };
} }
protected log({ message, cssStyle = "", meta = {}}: IKubeWatchLog) { subscribeStores(stores: KubeObjectStore<KubeObject>[], options: KubeWatchSubscribeStoreOptions = {}): Disposer {
if (isProduction && !isDebugging) { const parent = new AbortController();
const unsubscribe = disposer(
...stores.map(store => this.subscribeStore(
store,
parent,
!options.namespaces,
options.namespaces ?? this.context?.contextNamespaces ?? [],
)),
);
// unsubscribe
return once(() => {
parent.abort();
unsubscribe();
});
}
protected log(message: any, meta: any) {
if (isProduction) {
return; return;
} }
const logInfo = [`%c[KUBE-WATCH-API]:`, `font-weight: bold; ${cssStyle}`, message].flat().map(String); const log = message instanceof Error
const logMeta = { ? console.error
: console.debug;
log("[KUBE-WATCH-API]:", message, {
time: new Date().toLocaleString(), time: new Date().toLocaleString(),
...meta, ...meta,
}; });
if (message instanceof Error) {
console.error(...logInfo, logMeta);
} else {
console.info(...logInfo, logMeta);
}
} }
} }

View File

@ -55,9 +55,11 @@ export class ClusterOverview extends React.Component {
this.metricPoller.start(true); this.metricPoller.start(true);
disposeOnUnmount(this, [ disposeOnUnmount(this, [
kubeWatchApi.subscribeStores([podsStore, eventStore, nodesStore], { kubeWatchApi.subscribeStores([
preload: true, podsStore,
}), eventStore,
nodesStore,
]),
reaction( reaction(
() => clusterOverviewStore.metricNodeRole, // Toggle Master/Worker node switcher () => clusterOverviewStore.metricNodeRole, // Toggle Master/Worker node switcher
() => this.metricPoller.restart(true), () => this.metricPoller.restart(true),

View File

@ -22,13 +22,14 @@
import "./kube-event-details.scss"; import "./kube-event-details.scss";
import React from "react"; import React from "react";
import { observer } from "mobx-react"; import { disposeOnUnmount, observer } from "mobx-react";
import { KubeObject } from "../../../common/k8s-api/kube-object"; import { KubeObject } from "../../../common/k8s-api/kube-object";
import { DrawerItem, DrawerTitle } from "../drawer"; import { DrawerItem, DrawerTitle } from "../drawer";
import { cssNames } from "../../utils"; import { cssNames } from "../../utils";
import { LocaleDate } from "../locale-date"; import { LocaleDate } from "../locale-date";
import { eventStore } from "./event.store"; import { eventStore } from "./event.store";
import logger from "../../../common/logger"; import logger from "../../../common/logger";
import { kubeWatchApi } from "../../../common/k8s-api/kube-watch-api";
export interface KubeEventDetailsProps { export interface KubeEventDetailsProps {
object: KubeObject; object: KubeObject;
@ -36,8 +37,12 @@ export interface KubeEventDetailsProps {
@observer @observer
export class KubeEventDetails extends React.Component<KubeEventDetailsProps> { export class KubeEventDetails extends React.Component<KubeEventDetailsProps> {
async componentDidMount() { componentDidMount() {
eventStore.reloadAll(); disposeOnUnmount(this, [
kubeWatchApi.subscribeStores([
eventStore,
]),
]);
} }
render() { render() {

View File

@ -39,6 +39,7 @@ import { ClusterMetricsResourceType } from "../../../common/cluster-types";
import { getActiveClusterEntity } from "../../api/catalog-entity-registry"; import { getActiveClusterEntity } from "../../api/catalog-entity-registry";
import { getDetailsUrl } from "../kube-detail-params"; import { getDetailsUrl } from "../kube-detail-params";
import logger from "../../../common/logger"; import logger from "../../../common/logger";
import { kubeWatchApi } from "../../../common/k8s-api/kube-watch-api";
interface Props extends KubeObjectDetailsProps<Namespace> { interface Props extends KubeObjectDetailsProps<Namespace> {
} }
@ -52,14 +53,16 @@ export class NamespaceDetails extends React.Component<Props> {
makeObservable(this); makeObservable(this);
} }
@disposeOnUnmount
clean = reaction(() => this.props.object, () => {
this.metrics = null;
});
componentDidMount() { componentDidMount() {
resourceQuotaStore.reloadAll(); disposeOnUnmount(this, [
limitRangeStore.reloadAll(); reaction(() => this.props.object, () => {
this.metrics = null;
}),
kubeWatchApi.subscribeStores([
resourceQuotaStore,
limitRangeStore,
]),
]);
} }
@computed get quotas() { @computed get quotas() {

View File

@ -23,12 +23,11 @@ import "./namespace-select.scss";
import React from "react"; import React from "react";
import { computed, makeObservable } from "mobx"; import { computed, makeObservable } from "mobx";
import { disposeOnUnmount, observer } from "mobx-react"; import { observer } from "mobx-react";
import { Select, SelectOption, SelectProps } from "../select"; import { Select, SelectOption, SelectProps } from "../select";
import { cssNames } from "../../utils"; import { cssNames } from "../../utils";
import { Icon } from "../icon"; import { Icon } from "../icon";
import { namespaceStore } from "./namespace.store"; import { namespaceStore } from "./namespace.store";
import { kubeWatchApi } from "../../../common/k8s-api/kube-watch-api";
interface Props extends SelectProps { interface Props extends SelectProps {
showIcons?: boolean; showIcons?: boolean;
@ -50,14 +49,7 @@ export class NamespaceSelect extends React.Component<Props> {
makeObservable(this); makeObservable(this);
} }
componentDidMount() { // No subscribe here because the subscribe is in <App /> (the cluster frame root component)
disposeOnUnmount(this, [
kubeWatchApi.subscribeStores([namespaceStore], {
preload: true,
loadOnce: true, // skip reloading namespaces on every render / page visit
}),
]);
}
@computed.struct get options(): SelectOption[] { @computed.struct get options(): SelectOption[] {
const { customizeOptions, showAllNamespacesOption, sort } = this.props; const { customizeOptions, showAllNamespacesOption, sort } = this.props;

View File

@ -133,7 +133,7 @@ export class NamespaceStore extends KubeObjectStore<Namespace> {
return super.subscribe(); return super.subscribe();
} }
protected async loadItems(params: KubeObjectStoreLoadingParams<Namespace>): Promise<Namespace[]> { protected async loadItems(params: KubeObjectStoreLoadingParams): Promise<Namespace[]> {
const { allowedNamespaces } = this; const { allowedNamespaces } = this;
let namespaces = await super.loadItems(params).catch(() => []); let namespaces = await super.loadItems(params).catch(() => []);

View File

@ -49,10 +49,13 @@ export class IngressDetails extends React.Component<Props> {
makeObservable(this); makeObservable(this);
} }
@disposeOnUnmount componentDidMount() {
clean = reaction(() => this.props.object, () => { disposeOnUnmount(this, [
reaction(() => this.props.object, () => {
this.metrics = null; this.metrics = null;
}); }),
]);
}
@boundMethod @boundMethod
async loadMetrics() { async loadMetrics() {

View File

@ -44,8 +44,9 @@ export class ServiceDetails extends React.Component<Props> {
const { object: service } = this.props; const { object: service } = this.props;
disposeOnUnmount(this, [ disposeOnUnmount(this, [
kubeWatchApi.subscribeStores([endpointStore], { kubeWatchApi.subscribeStores([
preload: true, endpointStore,
], {
namespaces: [service.getNs()], namespaces: [service.getNs()],
}), }),
portForwardStore.watch(), portForwardStore.watch(),

View File

@ -41,6 +41,7 @@ import { NodeDetailsResources } from "./node-details-resources";
import { DrawerTitle } from "../drawer/drawer-title"; import { DrawerTitle } from "../drawer/drawer-title";
import { boundMethod } from "../../utils"; import { boundMethod } from "../../utils";
import logger from "../../../common/logger"; import logger from "../../../common/logger";
import { kubeWatchApi } from "../../../common/k8s-api/kube-watch-api";
interface Props extends KubeObjectDetailsProps<Node> { interface Props extends KubeObjectDetailsProps<Node> {
} }
@ -54,13 +55,15 @@ export class NodeDetails extends React.Component<Props> {
makeObservable(this); makeObservable(this);
} }
@disposeOnUnmount componentDidMount() {
clean = reaction(() => this.props.object.getName(), () => { disposeOnUnmount(this, [
reaction(() => this.props.object.getName(), () => {
this.metrics = null; this.metrics = null;
}); }),
kubeWatchApi.subscribeStores([
async componentDidMount() { podsStore,
podsStore.reloadAll(); ]),
]);
} }
@boundMethod @boundMethod

View File

@ -25,7 +25,7 @@ import React from "react";
import startCase from "lodash/startCase"; import startCase from "lodash/startCase";
import { DrawerItem, DrawerTitle } from "../drawer"; import { DrawerItem, DrawerTitle } from "../drawer";
import { Badge } from "../badge"; import { Badge } from "../badge";
import { observer } from "mobx-react"; import { disposeOnUnmount, observer } from "mobx-react";
import type { KubeObjectDetailsProps } from "../kube-object-details"; import type { KubeObjectDetailsProps } from "../kube-object-details";
import { StorageClass } from "../../../common/k8s-api/endpoints"; import { StorageClass } from "../../../common/k8s-api/endpoints";
import { KubeObjectMeta } from "../kube-object-meta"; import { KubeObjectMeta } from "../kube-object-meta";
@ -33,14 +33,19 @@ import { storageClassStore } from "./storage-class.store";
import { VolumeDetailsList } from "../+storage-volumes/volume-details-list"; import { VolumeDetailsList } from "../+storage-volumes/volume-details-list";
import { volumesStore } from "../+storage-volumes/volumes.store"; import { volumesStore } from "../+storage-volumes/volumes.store";
import logger from "../../../common/logger"; import logger from "../../../common/logger";
import { kubeWatchApi } from "../../../common/k8s-api/kube-watch-api";
interface Props extends KubeObjectDetailsProps<StorageClass> { interface Props extends KubeObjectDetailsProps<StorageClass> {
} }
@observer @observer
export class StorageClassDetails extends React.Component<Props> { export class StorageClassDetails extends React.Component<Props> {
async componentDidMount() { componentDidMount() {
volumesStore.reloadAll(); disposeOnUnmount(this, [
kubeWatchApi.subscribeStores([
volumesStore,
]),
]);
} }
render() { render() {

View File

@ -51,10 +51,13 @@ export class PersistentVolumeClaimDetails extends React.Component<Props> {
makeObservable(this); makeObservable(this);
} }
@disposeOnUnmount componentDidMount() {
clean = reaction(() => this.props.object, () => { disposeOnUnmount(this, [
reaction(() => this.props.object, () => {
this.metrics = null; this.metrics = null;
}); }),
]);
}
@boundMethod @boundMethod
async loadMetrics() { async loadMetrics() {

View File

@ -23,7 +23,7 @@ import "./cronjob-details.scss";
import React from "react"; import React from "react";
import kebabCase from "lodash/kebabCase"; import kebabCase from "lodash/kebabCase";
import { observer } from "mobx-react"; import { disposeOnUnmount, observer } from "mobx-react";
import { DrawerItem, DrawerTitle } from "../drawer"; import { DrawerItem, DrawerTitle } from "../drawer";
import { Badge } from "../badge/badge"; import { Badge } from "../badge/badge";
import { jobStore } from "../+workloads-jobs/job.store"; import { jobStore } from "../+workloads-jobs/job.store";
@ -34,14 +34,19 @@ import { getDetailsUrl } from "../kube-detail-params";
import { CronJob, Job } from "../../../common/k8s-api/endpoints"; import { CronJob, Job } from "../../../common/k8s-api/endpoints";
import { KubeObjectMeta } from "../kube-object-meta"; import { KubeObjectMeta } from "../kube-object-meta";
import logger from "../../../common/logger"; import logger from "../../../common/logger";
import { kubeWatchApi } from "../../../common/k8s-api/kube-watch-api";
interface Props extends KubeObjectDetailsProps<CronJob> { interface Props extends KubeObjectDetailsProps<CronJob> {
} }
@observer @observer
export class CronJobDetails extends React.Component<Props> { export class CronJobDetails extends React.Component<Props> {
async componentDidMount() { componentDidMount() {
jobStore.reloadAll(); disposeOnUnmount(this, [
kubeWatchApi.subscribeStores([
jobStore,
]),
]);
} }
render() { render() {

View File

@ -41,6 +41,7 @@ import { getActiveClusterEntity } from "../../api/catalog-entity-registry";
import { ClusterMetricsResourceType } from "../../../common/cluster-types"; import { ClusterMetricsResourceType } from "../../../common/cluster-types";
import { boundMethod } from "../../utils"; import { boundMethod } from "../../utils";
import logger from "../../../common/logger"; import logger from "../../../common/logger";
import { kubeWatchApi } from "../../../common/k8s-api/kube-watch-api";
interface Props extends KubeObjectDetailsProps<DaemonSet> { interface Props extends KubeObjectDetailsProps<DaemonSet> {
} }
@ -54,13 +55,15 @@ export class DaemonSetDetails extends React.Component<Props> {
makeObservable(this); makeObservable(this);
} }
@disposeOnUnmount
clean = reaction(() => this.props.object, () => {
this.metrics = null;
});
componentDidMount() { componentDidMount() {
podsStore.reloadAll(); disposeOnUnmount(this, [
reaction(() => this.props.object, () => {
this.metrics = null;
}),
kubeWatchApi.subscribeStores([
podsStore,
]),
]);
} }
@boundMethod @boundMethod

View File

@ -43,6 +43,7 @@ import { getActiveClusterEntity } from "../../api/catalog-entity-registry";
import { ClusterMetricsResourceType } from "../../../common/cluster-types"; import { ClusterMetricsResourceType } from "../../../common/cluster-types";
import { boundMethod } from "../../utils"; import { boundMethod } from "../../utils";
import logger from "../../../common/logger"; import logger from "../../../common/logger";
import { kubeWatchApi } from "../../../common/k8s-api/kube-watch-api";
interface Props extends KubeObjectDetailsProps<Deployment> { interface Props extends KubeObjectDetailsProps<Deployment> {
} }
@ -56,14 +57,16 @@ export class DeploymentDetails extends React.Component<Props> {
makeObservable(this); makeObservable(this);
} }
@disposeOnUnmount
clean = reaction(() => this.props.object, () => {
this.metrics = null;
});
componentDidMount() { componentDidMount() {
podsStore.reloadAll(); disposeOnUnmount(this, [
replicaSetStore.reloadAll(); reaction(() => this.props.object, () => {
this.metrics = null;
}),
kubeWatchApi.subscribeStores([
podsStore,
replicaSetStore,
]),
]);
} }
@boundMethod @boundMethod

View File

@ -23,7 +23,7 @@ import "./job-details.scss";
import React from "react"; import React from "react";
import kebabCase from "lodash/kebabCase"; import kebabCase from "lodash/kebabCase";
import { observer } from "mobx-react"; import { disposeOnUnmount, observer } from "mobx-react";
import { DrawerItem } from "../drawer"; import { DrawerItem } from "../drawer";
import { Badge } from "../badge"; import { Badge } from "../badge";
import { PodDetailsStatuses } from "../+workloads-pods/pod-details-statuses"; import { PodDetailsStatuses } from "../+workloads-pods/pod-details-statuses";
@ -36,7 +36,7 @@ import type { KubeObjectDetailsProps } from "../kube-object-details";
import { getMetricsForJobs, IPodMetrics, Job } from "../../../common/k8s-api/endpoints"; import { getMetricsForJobs, IPodMetrics, Job } from "../../../common/k8s-api/endpoints";
import { PodDetailsList } from "../+workloads-pods/pod-details-list"; import { PodDetailsList } from "../+workloads-pods/pod-details-list";
import { KubeObjectMeta } from "../kube-object-meta"; import { KubeObjectMeta } from "../kube-object-meta";
import { makeObservable, observable } from "mobx"; import { makeObservable, observable, reaction } from "mobx";
import { podMetricTabs, PodCharts } from "../+workloads-pods/pod-charts"; import { podMetricTabs, PodCharts } from "../+workloads-pods/pod-charts";
import { ClusterMetricsResourceType } from "../../../common/cluster-types"; import { ClusterMetricsResourceType } from "../../../common/cluster-types";
import { getActiveClusterEntity } from "../../api/catalog-entity-registry"; import { getActiveClusterEntity } from "../../api/catalog-entity-registry";
@ -45,6 +45,7 @@ import { boundMethod } from "autobind-decorator";
import { getDetailsUrl } from "../kube-detail-params"; import { getDetailsUrl } from "../kube-detail-params";
import { apiManager } from "../../../common/k8s-api/api-manager"; import { apiManager } from "../../../common/k8s-api/api-manager";
import logger from "../../../common/logger"; import logger from "../../../common/logger";
import { kubeWatchApi } from "../../../common/k8s-api/kube-watch-api";
interface Props extends KubeObjectDetailsProps<Job> { interface Props extends KubeObjectDetailsProps<Job> {
} }
@ -58,8 +59,15 @@ export class JobDetails extends React.Component<Props> {
makeObservable(this); makeObservable(this);
} }
async componentDidMount() { componentDidMount() {
podsStore.reloadAll(); disposeOnUnmount(this, [
reaction(() => this.props.object, () => {
this.metrics = null;
}),
kubeWatchApi.subscribeStores([
podsStore,
]),
]);
} }
@boundMethod @boundMethod

View File

@ -33,7 +33,6 @@ import { replicaSetStore } from "../+workloads-replicasets/replicasets.store";
import { jobStore } from "../+workloads-jobs/job.store"; import { jobStore } from "../+workloads-jobs/job.store";
import { cronJobStore } from "../+workloads-cronjobs/cronjob.store"; import { cronJobStore } from "../+workloads-cronjobs/cronjob.store";
import { kubeWatchApi } from "../../../common/k8s-api/kube-watch-api"; import { kubeWatchApi } from "../../../common/k8s-api/kube-watch-api";
import { clusterContext } from "../context";
import { WorkloadsOverviewDetailRegistry } from "../../../extensions/registries"; import { WorkloadsOverviewDetailRegistry } from "../../../extensions/registries";
import type { WorkloadsOverviewRouteParams } from "../../../common/routes"; import type { WorkloadsOverviewRouteParams } from "../../../common/routes";
@ -45,12 +44,15 @@ export class WorkloadsOverview extends React.Component<Props> {
componentDidMount() { componentDidMount() {
disposeOnUnmount(this, [ disposeOnUnmount(this, [
kubeWatchApi.subscribeStores([ kubeWatchApi.subscribeStores([
podsStore, deploymentStore, daemonSetStore, statefulSetStore, replicaSetStore, cronJobStore,
jobStore, cronJobStore, eventStore, daemonSetStore,
], { deploymentStore,
preload: true, eventStore,
namespaces: clusterContext.contextNamespaces, jobStore,
}), podsStore,
replicaSetStore,
statefulSetStore,
]),
]); ]);
} }

View File

@ -40,6 +40,7 @@ import { getActiveClusterEntity } from "../../api/catalog-entity-registry";
import { ClusterMetricsResourceType } from "../../../common/cluster-types"; import { ClusterMetricsResourceType } from "../../../common/cluster-types";
import { boundMethod } from "../../utils"; import { boundMethod } from "../../utils";
import logger from "../../../common/logger"; import logger from "../../../common/logger";
import { kubeWatchApi } from "../../../common/k8s-api/kube-watch-api";
interface Props extends KubeObjectDetailsProps<ReplicaSet> { interface Props extends KubeObjectDetailsProps<ReplicaSet> {
} }
@ -53,13 +54,15 @@ export class ReplicaSetDetails extends React.Component<Props> {
makeObservable(this); makeObservable(this);
} }
@disposeOnUnmount componentDidMount() {
clean = reaction(() => this.props.object, () => { disposeOnUnmount(this, [
reaction(() => this.props.object, () => {
this.metrics = null; this.metrics = null;
}); }),
kubeWatchApi.subscribeStores([
async componentDidMount() { podsStore,
podsStore.reloadAll(); ]),
]);
} }
@boundMethod @boundMethod

View File

@ -41,6 +41,7 @@ import { getActiveClusterEntity } from "../../api/catalog-entity-registry";
import { ClusterMetricsResourceType } from "../../../common/cluster-types"; import { ClusterMetricsResourceType } from "../../../common/cluster-types";
import { boundMethod } from "../../utils"; import { boundMethod } from "../../utils";
import logger from "../../../common/logger"; import logger from "../../../common/logger";
import { kubeWatchApi } from "../../../common/k8s-api/kube-watch-api";
interface Props extends KubeObjectDetailsProps<StatefulSet> { interface Props extends KubeObjectDetailsProps<StatefulSet> {
} }
@ -54,13 +55,15 @@ export class StatefulSetDetails extends React.Component<Props> {
makeObservable(this); makeObservable(this);
} }
@disposeOnUnmount
clean = reaction(() => this.props.object, () => {
this.metrics = null;
});
componentDidMount() { componentDidMount() {
podsStore.reloadAll(); disposeOnUnmount(this, [
reaction(() => this.props.object, () => {
this.metrics = null;
}),
kubeWatchApi.subscribeStores([
podsStore,
]),
]);
} }
@boundMethod @boundMethod

View File

@ -128,9 +128,9 @@ export class App extends React.Component {
componentDidMount() { componentDidMount() {
disposeOnUnmount(this, [ disposeOnUnmount(this, [
kubeWatchApi.subscribeStores([namespaceStore], { kubeWatchApi.subscribeStores([
preload: true, namespaceStore,
}), ]),
watchHistoryState(), watchHistoryState(),
]); ]);

View File

@ -28,7 +28,6 @@ import { ItemListLayout, ItemListLayoutProps } from "../item-object-list/item-li
import type { KubeObjectStore } from "../../../common/k8s-api/kube-object.store"; import type { KubeObjectStore } from "../../../common/k8s-api/kube-object.store";
import { KubeObjectMenu } from "../kube-object-menu"; import { KubeObjectMenu } from "../kube-object-menu";
import { kubeWatchApi } from "../../../common/k8s-api/kube-watch-api"; import { kubeWatchApi } from "../../../common/k8s-api/kube-watch-api";
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";
@ -63,10 +62,7 @@ export class KubeObjectListLayout<K extends KubeObject> extends React.Component<
if (subscribeStores) { if (subscribeStores) {
disposeOnUnmount(this, [ disposeOnUnmount(this, [
kubeWatchApi.subscribeStores(stores, { kubeWatchApi.subscribeStores(stores),
preload: true,
namespaces: clusterContext.contextNamespaces,
}),
]); ]);
} }
} }

View File

@ -54,7 +54,9 @@ export class Sidebar extends React.Component<Props> {
componentDidMount() { componentDidMount() {
disposeOnUnmount(this, [ disposeOnUnmount(this, [
kubeWatchApi.subscribeStores([crdStore]), kubeWatchApi.subscribeStores([
crdStore,
]),
]); ]);
} }