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 (#4345)

* 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>

* Resolve PR comments

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Detect the adding of a namespace when selected namespaces change

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Update debug log

Signed-off-by: Sebastian Malton <sebastian@malton.name>
This commit is contained in:
Sebastian Malton 2021-11-29 17:25:35 -05:00 committed by GitHub
parent 8ff7c03993
commit 0286faf387
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
25 changed files with 338 additions and 252 deletions

View File

@ -25,4 +25,5 @@ export interface ClusterContext {
cluster?: Cluster;
allNamespaces: string[]; // available / allowed namespaces from cluster.ts
contextNamespaces: string[]; // selected by user (see: namespace-select.tsx)
hasSelectedAll: boolean;
}

View File

@ -33,9 +33,8 @@ import type { RequestInit } from "node-fetch";
import AbortController from "abort-controller";
import type { Patch } from "rfc6902";
export interface KubeObjectStoreLoadingParams<K extends KubeObject> {
export interface KubeObjectStoreLoadingParams {
namespaces: string[];
api?: KubeApi<K>;
reqInit?: RequestInit;
/**
@ -63,6 +62,11 @@ export interface KubeObjectStoreSubscribeParams {
* being rejected with
*/
onLoadFailure?: (err: any) => void;
/**
* An optional parent abort controller
*/
abortController?: AbortController;
}
export abstract class KubeObjectStore<T extends KubeObject> extends ItemStore<T> {
@ -167,8 +171,8 @@ export abstract class KubeObjectStore<T extends KubeObject> extends ItemStore<T>
}
}
protected async loadItems({ namespaces, api, reqInit, onLoadFailure }: KubeObjectStoreLoadingParams<T>): Promise<T[]> {
if (!this.context?.cluster.isAllowedResource(api.kind)) {
protected async loadItems({ namespaces, reqInit, onLoadFailure }: KubeObjectStoreLoadingParams): Promise<T[]> {
if (!this.context?.cluster.isAllowedResource(this.api.kind)) {
return [];
}
@ -176,12 +180,12 @@ export abstract class KubeObjectStore<T extends KubeObject> extends ItemStore<T>
&& this.context.cluster.accessibleNamespaces.length === 0
&& this.context.allNamespaces.every(ns => namespaces.includes(ns));
if (!api.isNamespaced || isLoadingAll) {
if (api.isNamespaced) {
if (!this.api.isNamespaced || isLoadingAll) {
if (this.api.isNamespaced) {
this.loadedNamespaces = [];
}
const res = api.list({ reqInit }, this.query);
const res = this.api.list({ reqInit }, this.query);
if (onLoadFailure) {
try {
@ -203,7 +207,7 @@ export abstract class KubeObjectStore<T extends KubeObject> extends ItemStore<T>
this.loadedNamespaces = namespaces;
const results = await Promise.allSettled(
namespaces.map(namespace => api.list({ namespace, reqInit }, this.query)),
namespaces.map(namespace => this.api.list({ namespace, reqInit }, this.query)),
);
const res: T[] = [];
@ -231,24 +235,14 @@ export abstract class KubeObjectStore<T extends KubeObject> extends ItemStore<T>
}
@action
async loadAll(options: KubeObjectStoreLoadAllParams = {}): Promise<void | T[]> {
async loadAll({ namespaces = this.context.contextNamespaces, merge = true, reqInit, onLoadFailure }: KubeObjectStoreLoadAllParams = {}): Promise<void | T[]> {
await this.contextReady;
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 {
const items = await this.loadItems({ namespaces, api: this.api, reqInit, onLoadFailure });
const items = await this.loadItems({ namespaces, reqInit, onLoadFailure });
if (merge) {
this.mergeItems(items, { replace: false });
} else {
this.mergeItems(items, { replace: true });
}
this.mergeItems(items, { merge });
this.isLoaded = true;
this.failedLoading = false;
@ -275,11 +269,11 @@ export abstract class KubeObjectStore<T extends KubeObject> extends ItemStore<T>
}
@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;
// update existing items
if (!replace) {
if (merge) {
const namespaces = partialItems.map(item => item.getNs());
items = [
@ -394,23 +388,21 @@ export abstract class KubeObjectStore<T extends KubeObject> extends ItemStore<T>
});
}
subscribe(opts: KubeObjectStoreSubscribeParams = {}) {
const abortController = new AbortController();
subscribe({ onLoadFailure, abortController = new AbortController() }: KubeObjectStoreSubscribeParams = {}) {
if (this.api.isNamespaced) {
Promise.race([rejectPromiseBy(abortController.signal), Promise.all([this.contextReady, this.namespacesReady])])
.then(() => {
if (this.context.cluster.isGlobalWatchEnabled && this.loadedNamespaces.length === 0) {
return this.watchNamespace("", abortController, opts);
return this.watchNamespace("", abortController, { onLoadFailure });
}
for (const namespace of this.loadedNamespaces) {
this.watchNamespace(namespace, abortController, opts);
this.watchNamespace(namespace, abortController, { onLoadFailure });
}
})
.catch(noop); // ignore DOMExceptions
} else {
this.watchNamespace("", abortController, opts);
this.watchNamespace("", abortController, { onLoadFailure });
}
return () => abortController.abort();

View File

@ -105,8 +105,9 @@ export class KubeCreationError extends Error {
}
export class KubeObject<Metadata extends KubeObjectMetadata = KubeObjectMetadata, Status = any, Spec = any> implements ItemObject {
static readonly kind: string;
static readonly namespaced: boolean;
static readonly kind?: string;
static readonly namespaced?: boolean;
static readonly apiBase?: string;
apiVersion: string;
kind: string;

View File

@ -25,32 +25,36 @@
import type { KubeObjectStore } from "./kube-object.store";
import type { ClusterContext } from "./cluster-context";
import plimit from "p-limit";
import { comparer, observable, reaction, makeObservable } from "mobx";
import { autoBind, disposer, Disposer, noop } from "../utils";
import type { KubeApi } from "./kube-api";
import { comparer, reaction } from "mobx";
import { disposer, Disposer, noop } from "../utils";
import type { KubeJsonApiData } from "./kube-json-api";
import { isDebugging, isProduction } from "../vars";
import type { KubeObject } from "./kube-object";
import AbortController from "abort-controller";
import { once } from "lodash";
import logger from "../logger";
class WrappedAbortController extends AbortController {
constructor(protected parent: AbortController) {
super();
parent.signal.addEventListener("abort", () => {
this.abort();
});
}
}
export interface IKubeWatchEvent<T extends KubeJsonApiData> {
type: "ADDED" | "MODIFIED" | "DELETED" | "ERROR";
object?: T;
}
interface KubeWatchPreloadOptions {
export interface KubeWatchSubscribeStoreOptions {
/**
* The namespaces to watch
* @default all-accessible
* @default all selected namespaces
*/
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
@ -58,123 +62,148 @@ interface KubeWatchPreloadOptions {
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 {
message: string | string[] | Error;
meta?: object;
cssStyle?: string;
}
interface SubscribeStoreParams {
store: KubeObjectStore<KubeObject>;
parent: AbortController;
watchChanges: boolean;
namespaces: string[];
onLoadFailure?: (err: any) => void;
}
class WatchCount {
#data = new Map<KubeObjectStore<KubeObject>, number>();
public inc(store: KubeObjectStore<KubeObject>): number {
if (!this.#data.has(store)) {
this.#data.set(store, 0);
}
const newCount = this.#data.get(store) + 1;
logger.info(`[KUBE-WATCH-API]: inc() count for ${store.api.objectConstructor.apiBase} is now ${newCount}`);
this.#data.set(store, newCount);
return newCount;
}
public dec(store: KubeObjectStore<KubeObject>): number {
if (!this.#data.has(store)) {
throw new Error(`Cannot dec count for store that has never been inc: ${store.api.objectConstructor.kind}`);
}
const newCount = this.#data.get(store) - 1;
if (newCount < 0) {
throw new Error(`Cannot dec count more times than it has been inc: ${store.api.objectConstructor.kind}`);
}
logger.debug(`[KUBE-WATCH-API]: dec() count for ${store.api.objectConstructor.apiBase} is now ${newCount}`);
this.#data.set(store, newCount);
return newCount;
}
}
export class KubeWatchApi {
@observable context: ClusterContext = null;
static context: ClusterContext = null;
constructor() {
makeObservable(this);
autoBind(this);
}
#watch = new WatchCount();
isAllowedApi(api: KubeApi<KubeObject>): boolean {
return Boolean(this.context?.cluster.isAllowedResource(api.kind));
}
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 preloading: Promise<any>[] = [];
for (const store of stores) {
preloading.push(limitRequests(async () => {
if (store.isLoaded && loadOnce) return; // skip
return store.loadAll({ namespaces, onLoadFailure });
}));
private subscribeStore({ store, parent, watchChanges, namespaces, onLoadFailure }: SubscribeStoreParams): Disposer {
if (this.#watch.inc(store) > 1) {
// don't load or subscribe to a store more than once
return () => this.#watch.dec(store);
}
return {
loading: Promise.allSettled(preloading),
cancelLoading: () => limitRequests.clearQueue(),
};
}
let childController = new WrappedAbortController(parent);
const unsubscribe = disposer();
subscribeStores(stores: KubeObjectStore<KubeObject>[], opts: KubeWatchSubscribeStoreOptions = {}): Disposer {
const { preload = true, waitUntilLoaded = true, loadOnce = false, onLoadFailure } = opts;
const subscribingNamespaces = opts.namespaces ?? this.context?.allNamespaces ?? [];
const unsubscribeStores = disposer();
let isUnsubscribed = false;
const load = (namespaces = subscribingNamespaces) => this.preloadStores(stores, { namespaces, loadOnce, onLoadFailure });
let preloading = preload && load();
let cancelReloading: Disposer = noop;
const subscribe = () => {
if (isUnsubscribed) {
return;
}
unsubscribeStores.push(...stores.map(store => store.subscribe({ onLoadFailure })));
};
if (preloading) {
if (waitUntilLoaded) {
preloading.loading.then(subscribe, error => {
this.log({
message: new Error("Loading stores has failed"),
meta: { stores, error, options: opts },
const loadThenSubscribe = async (namespaces: string[]) => {
try {
await store.loadAll({ namespaces, reqInit: { signal: childController.signal }, onLoadFailure });
unsubscribe.push(store.subscribe({ onLoadFailure, abortController: childController }));
} catch (error) {
if (!(error instanceof DOMException)) {
this.log(Object.assign(new Error("Loading stores has failed"), { cause: error }), {
meta: { store, namespaces },
});
});
} else {
subscribe();
}
}
};
// reload stores only for context namespaces change
cancelReloading = reaction(() => this.context?.contextNamespaces, namespaces => {
preloading?.cancelLoading();
unsubscribeStores();
preloading = load(namespaces);
preloading.loading.then(subscribe);
}, {
equals: comparer.shallow,
});
}
/**
* We don't want to wait because we want to start reacting to namespace
* selection changes ASAP
*/
loadThenSubscribe(namespaces).catch(noop);
const cancelReloading = watchChanges
? reaction(
// Note: must slice because reaction won't fire if it isn't there
() => [KubeWatchApi.context.contextNamespaces.slice(), KubeWatchApi.context.hasSelectedAll] as const,
([namespaces, curSelectedAll], [prevNamespaces, prevSelectedAll]) => {
if (curSelectedAll && prevSelectedAll) {
const action = namespaces.length > prevNamespaces.length ? "created" : "deleted";
return console.debug(`[KUBE-WATCH-API]: Not changing watch for ${store.api.apiBase} because a new namespace was ${action} but all namespaces are selected`);
}
console.log(`[KUBE-WATCH-API]: changing watch ${store.api.apiBase}`, namespaces);
childController.abort();
unsubscribe();
childController = new WrappedAbortController(parent);
loadThenSubscribe(namespaces).catch(noop);
},
{
equals: comparer.shallow,
},
)
: noop; // don't watch namespaces if namespaces were provided
return () => {
if (this.#watch.dec(store) === 0) {
// only stop the subcribe if this is the last one
cancelReloading();
childController.abort();
unsubscribe();
}
};
}
subscribeStores(stores: KubeObjectStore<KubeObject>[], { namespaces, onLoadFailure }: KubeWatchSubscribeStoreOptions = {}): Disposer {
const parent = new AbortController();
const unsubscribe = disposer(
...stores.map(store => this.subscribeStore({
store,
parent,
watchChanges: !namespaces && store.api.isNamespaced,
namespaces: namespaces ?? KubeWatchApi.context?.contextNamespaces ?? [],
onLoadFailure,
})),
);
// unsubscribe
return () => {
if (isUnsubscribed) return;
isUnsubscribed = true;
cancelReloading();
preloading?.cancelLoading();
unsubscribeStores();
};
return once(() => {
parent.abort();
unsubscribe();
});
}
protected log({ message, cssStyle = "", meta = {}}: IKubeWatchLog) {
if (isProduction && !isDebugging) {
return;
}
protected log(message: any, meta: any) {
const log = message instanceof Error
? console.error
: console.debug;
const logInfo = [`%c[KUBE-WATCH-API]:`, `font-weight: bold; ${cssStyle}`, message].flat().map(String);
const logMeta = {
log("[KUBE-WATCH-API]:", message, {
time: new Date().toLocaleString(),
...meta,
};
if (message instanceof Error) {
console.error(...logInfo, logMeta);
} else {
console.info(...logInfo, logMeta);
}
});
}
}

View File

@ -42,11 +42,11 @@ import whatInput from "what-input";
import { clusterSetFrameIdHandler } from "../common/cluster-ipc";
import { ClusterPageMenuRegistration, ClusterPageMenuRegistry } from "../extensions/registries";
import { StatefulSetScaleDialog } from "./components/+workloads-statefulsets/statefulset-scale-dialog";
import { kubeWatchApi } from "../common/k8s-api/kube-watch-api";
import { KubeWatchApi, kubeWatchApi } from "../common/k8s-api/kube-watch-api";
import { ReplicaSetScaleDialog } from "./components/+workloads-replicasets/replicaset-scale-dialog";
import { CommandContainer } from "./components/command-palette/command-container";
import { KubeObjectStore } from "../common/k8s-api/kube-object.store";
import { clusterContext } from "./components/context";
import { FrameContext } from "./components/context";
import * as routes from "../common/routes";
import { TabLayout, TabLayoutRoute } from "./components/layout/tab-layout";
import { ErrorBoundary } from "./components/error-boundary";
@ -73,6 +73,8 @@ import { watchHistoryState } from "./remote-helpers/history-updater";
import { unmountComponentAtNode } from "react-dom";
import { PortForwardDialog } from "./port-forward";
import { DeleteClusterDialog } from "./components/delete-cluster-dialog";
import { WorkloadsOverview } from "./components/+workloads-overview/overview";
import { KubeObjectListLayout } from "./components/kube-object-list-layout";
@observer
export class ClusterFrame extends React.Component {
@ -91,10 +93,12 @@ export class ClusterFrame extends React.Component {
ClusterFrame.clusterId = getHostedClusterId();
const cluster = ClusterStore.getInstance().getById(ClusterFrame.clusterId);
logger.info(`${ClusterFrame.logPrefix} Init dashboard, clusterId=${ClusterFrame.clusterId}, frameId=${frameId}`);
await Terminal.preloadFonts();
await requestMain(clusterSetFrameIdHandler, ClusterFrame.clusterId);
await ClusterStore.getInstance().getById(ClusterFrame.clusterId).whenReady; // cluster.activate() is done at this point
await cluster.whenReady; // cluster.activate() is done at this point
catalogEntityRegistry.activeEntity = ClusterFrame.clusterId;
@ -120,16 +124,21 @@ export class ClusterFrame extends React.Component {
whatInput.ask(); // Start to monitor user input device
const clusterContext = new FrameContext(cluster);
// Setup hosted cluster context
KubeObjectStore.defaultContext.set(clusterContext);
kubeWatchApi.context = clusterContext;
WorkloadsOverview.clusterContext
= KubeObjectListLayout.clusterContext
= KubeWatchApi.context
= clusterContext;
}
componentDidMount() {
disposeOnUnmount(this, [
kubeWatchApi.subscribeStores([namespaceStore], {
preload: true,
}),
kubeWatchApi.subscribeStores([
namespaceStore,
]),
watchHistoryState(),
]);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -23,7 +23,7 @@ import "./cronjob-details.scss";
import React from "react";
import kebabCase from "lodash/kebabCase";
import { observer } from "mobx-react";
import { disposeOnUnmount, observer } from "mobx-react";
import { DrawerItem, DrawerTitle } from "../drawer";
import { Badge } from "../badge/badge";
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 { KubeObjectMeta } from "../kube-object-meta";
import logger from "../../../common/logger";
import { kubeWatchApi } from "../../../common/k8s-api/kube-watch-api";
interface Props extends KubeObjectDetailsProps<CronJob> {
}
@observer
export class CronJobDetails extends React.Component<Props> {
async componentDidMount() {
jobStore.reloadAll();
componentDidMount() {
disposeOnUnmount(this, [
kubeWatchApi.subscribeStores([
jobStore,
]),
]);
}
render() {

View File

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

View File

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

View File

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

View File

@ -36,16 +36,18 @@ import { kubeWatchApi } from "../../../common/k8s-api/kube-watch-api";
import { WorkloadsOverviewDetailRegistry } from "../../../extensions/registries";
import type { WorkloadsOverviewRouteParams } from "../../../common/routes";
import { makeObservable, observable, reaction } from "mobx";
import { clusterContext } from "../context";
import { NamespaceSelectFilter } from "../+namespaces/namespace-select-filter";
import { Icon } from "../icon";
import { TooltipPosition } from "../tooltip";
import type { ClusterContext } from "../../../common/k8s-api/cluster-context";
interface Props extends RouteComponentProps<WorkloadsOverviewRouteParams> {
}
@observer
export class WorkloadsOverview extends React.Component<Props> {
static clusterContext: ClusterContext;
@observable loadErrors: string[] = [];
constructor(props: Props) {
@ -56,12 +58,18 @@ export class WorkloadsOverview extends React.Component<Props> {
componentDidMount() {
disposeOnUnmount(this, [
kubeWatchApi.subscribeStores([
podsStore, deploymentStore, daemonSetStore, statefulSetStore, replicaSetStore,
jobStore, cronJobStore, eventStore,
cronJobStore,
daemonSetStore,
deploymentStore,
eventStore,
jobStore,
podsStore,
replicaSetStore,
statefulSetStore,
], {
onLoadFailure: error => this.loadErrors.push(String(error)),
}),
reaction(() => clusterContext.contextNamespaces.slice(), () => {
reaction(() => WorkloadsOverview.clusterContext.contextNamespaces.slice(), () => {
// clear load errors
this.loadErrors.length = 0;
}),

View File

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

View File

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

View File

@ -19,24 +19,19 @@
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
import { ClusterStore } from "../../common/cluster-store";
import type { Cluster } from "../../main/cluster";
import { getHostedClusterId } from "../utils";
import { namespaceStore } from "./+namespaces/namespace.store";
import type { ClusterContext } from "../../common/k8s-api/cluster-context";
import { computed, makeObservable } from "mobx";
export const clusterContext: ClusterContext = {
get cluster(): Cluster | null {
return ClusterStore.getInstance().getById(getHostedClusterId());
},
get allNamespaces(): string[] {
if (!this.cluster) {
return [];
}
export class FrameContext implements ClusterContext {
constructor(public cluster: Cluster) {
makeObservable(this);
}
@computed get allNamespaces(): string[] {
// user given list of namespaces
if (this.cluster?.accessibleNamespaces.length) {
if (this.cluster.accessibleNamespaces.length) {
return this.cluster.accessibleNamespaces;
}
@ -47,9 +42,17 @@ export const clusterContext: ClusterContext = {
// fallback to cluster resolved namespaces because we could not load list
return this.cluster.allowedNamespaces || [];
}
},
}
get contextNamespaces(): string[] {
return namespaceStore.contextNamespaces ?? [];
},
};
@computed get contextNamespaces(): string[] {
return namespaceStore.contextNamespaces;
}
@computed get hasSelectedAll(): boolean {
const namespaces = new Set(this.contextNamespaces);
return this.allNamespaces?.length > 1
&& this.cluster.accessibleNamespaces.length === 0
&& this.allNamespaces.every(ns => namespaces.has(ns));
}
}

View File

@ -30,12 +30,12 @@ import { ItemListLayout, ItemListLayoutProps } from "../item-object-list/item-li
import type { KubeObjectStore } from "../../../common/k8s-api/kube-object.store";
import { KubeObjectMenu } from "../kube-object-menu";
import { kubeWatchApi } from "../../../common/k8s-api/kube-watch-api";
import { clusterContext } from "../context";
import { NamespaceSelectFilter } from "../+namespaces/namespace-select-filter";
import { ResourceKindMap, ResourceNames } from "../../utils/rbac";
import { kubeSelectedUrlParam, toggleDetails } from "../kube-detail-params";
import { Icon } from "../icon";
import { TooltipPosition } from "../tooltip";
import type { ClusterContext } from "../../../common/k8s-api/cluster-context";
export interface KubeObjectListLayoutProps<K extends KubeObject> extends ItemListLayoutProps<K> {
store: KubeObjectStore<K>;
@ -51,6 +51,7 @@ const defaultProps: Partial<KubeObjectListLayoutProps<KubeObject>> = {
@observer
export class KubeObjectListLayout<K extends KubeObject> extends React.Component<KubeObjectListLayoutProps<K>> {
static defaultProps = defaultProps as object;
static clusterContext: ClusterContext;
constructor(props: KubeObjectListLayoutProps<K>) {
super(props);
@ -67,7 +68,7 @@ export class KubeObjectListLayout<K extends KubeObject> extends React.Component<
const { store, dependentStores = [], subscribeStores } = this.props;
const stores = Array.from(new Set([store, ...dependentStores]));
const reactions: Disposer[] = [
reaction(() => clusterContext.contextNamespaces.slice(), () => {
reaction(() => KubeObjectListLayout.clusterContext.contextNamespaces.slice(), () => {
// clear load errors
this.loadErrors.length = 0;
}),
@ -76,8 +77,6 @@ export class KubeObjectListLayout<K extends KubeObject> extends React.Component<
if (subscribeStores) {
reactions.push(
kubeWatchApi.subscribeStores(stores, {
preload: true,
namespaces: clusterContext.contextNamespaces,
onLoadFailure: error => this.loadErrors.push(String(error)),
}),
);

View File

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