diff --git a/src/main/cluster.ts b/src/main/cluster.ts index e2831e8c3f..c7749ebe77 100644 --- a/src/main/cluster.ts +++ b/src/main/cluster.ts @@ -42,7 +42,6 @@ export interface ClusterState { accessible: boolean; ready: boolean; failureReason: string; - eventCount: number; isAdmin: boolean; allowedNamespaces: string[] allowedResources: string[] @@ -74,7 +73,6 @@ export class Cluster implements ClusterModel, ClusterState { @observable disconnected = true; // false if user has selected to connect @observable failureReason: string; @observable isAdmin = false; - @observable eventCount = 0; @observable preferences: ClusterPreferences = {}; @observable metadata: ClusterMetadata = {}; @observable allowedNamespaces: string[] = []; @@ -209,10 +207,7 @@ export class Cluster implements ClusterModel, ClusterState { await this.refreshConnectionStatus(); if (this.accessible) { this.isAdmin = await this.isClusterAdmin(); - await Promise.all([ - this.refreshEvents(), - this.refreshAllowedResources(), - ]); + await this.refreshAllowedResources(); if (opts.refreshMetadata) { this.refreshMetadata(); } @@ -242,11 +237,6 @@ export class Cluster implements ClusterModel, ClusterState { this.allowedResources = await this.getAllowedResources(); } - @action - async refreshEvents() { - this.eventCount = await this.getEventCount(); - } - protected getKubeconfig(): KubeConfig { return loadConfig(this.kubeConfigPath); } @@ -332,40 +322,6 @@ export class Cluster implements ClusterModel, ClusterState { }); } - protected async getEventCount(): Promise { - if (!this.isAdmin) { - return 0; - } - const client = this.getProxyKubeconfig().makeApiClient(CoreV1Api); - try { - const response = await client.listEventForAllNamespaces(false, null, null, null, 1000); - const uniqEventSources = new Set(); - const warnings = response.body.items.filter(e => e.type !== 'Normal'); - for (const w of warnings) { - if (w.involvedObject.kind === 'Pod') { - try { - const { body: pod } = await client.readNamespacedPod(w.involvedObject.name, w.involvedObject.namespace); - logger.debug(`checking pod ${w.involvedObject.namespace}/${w.involvedObject.name}`); - if (podHasIssues(pod)) { - uniqEventSources.add(w.involvedObject.uid); - } - } catch (err) { - } - } else { - uniqEventSources.add(w.involvedObject.uid); - } - } - const nodes = (await client.listNode()).body.items; - const nodeNotificationCount = nodes - .map(getNodeWarningConditions) - .reduce((sum, conditions) => sum + conditions.length, 0); - return uniqEventSources.size + nodeNotificationCount; - } catch (error) { - logger.error("Failed to fetch event count: " + JSON.stringify(error)); - return 0; - } - } - toJSON(): ClusterModel { const model: ClusterModel = { id: this.id, @@ -393,7 +349,6 @@ export class Cluster implements ClusterModel, ClusterState { accessible: this.accessible, failureReason: this.failureReason, isAdmin: this.isAdmin, - eventCount: this.eventCount, allowedNamespaces: this.allowedNamespaces, allowedResources: this.allowedResources, }; diff --git a/src/renderer/components/app.tsx b/src/renderer/components/app.tsx index ca0cb92a73..59b95f776c 100755 --- a/src/renderer/components/app.tsx +++ b/src/renderer/components/app.tsx @@ -37,13 +37,17 @@ import { webFrame } from "electron"; import { clusterPageRegistry, getExtensionPageUrl, PageRegistration, RegisteredPage } from "../../extensions/registries/page-registry"; import { extensionLoader } from "../../extensions/extension-loader"; import { appEventBus } from "../../common/event-bus"; -import { requestMain } from "../../common/ipc"; +import { broadcastMessage, requestMain } from "../../common/ipc"; import whatInput from 'what-input'; import { clusterSetFrameIdHandler } from "../../common/cluster-ipc"; import { ClusterPageMenuRegistration, clusterPageMenuRegistry } from "../../extensions/registries"; import { TabLayoutRoute, TabLayout } from "./layout/tab-layout"; -import { Trans } from "@lingui/macro"; -import {StatefulSetScaleDialog} from "./+workloads-statefulsets/statefulset-scale-dialog"; +import { StatefulSetScaleDialog } from "./+workloads-statefulsets/statefulset-scale-dialog"; +import { eventStore } from "./+events/event.store"; +import { reaction, computed } from "mobx"; +import { nodesStore } from "./+nodes/nodes.store"; +import { podsStore } from "./+workloads-pods/pods.store"; +import { sum } from "lodash"; @observer export class App extends React.Component { @@ -69,6 +73,39 @@ export class App extends React.Component { whatInput.ask(); // Start to monitor user input device } + async componentDidMount() { + const cluster = getHostedCluster(); + const promises: Promise[] = []; + if (isAllowedResource("events") && isAllowedResource("pods")) { + promises.push(eventStore.loadAll()); + promises.push(podsStore.loadAll()); + } + if (isAllowedResource("nodes")) { + promises.push(nodesStore.loadAll()); + } + await Promise.all(promises); + if (eventStore.isLoaded && podsStore.isLoaded) { + eventStore.subscribe(); + podsStore.subscribe(); + } + if (nodesStore.isLoaded) { + nodesStore.subscribe(); + } + + reaction(() => this.warningsCount, (count) => { + broadcastMessage(`cluster-warning-event-count:${cluster.id}`, count); + }); + } + + @computed + get warningsCount() { + let warnings = sum(nodesStore.items + .map(node => node.getWarningConditions().length)); + warnings = warnings + eventStore.getWarnings().length; + + return warnings; + } + get startURL() { if (isAllowedResource(["events", "nodes", "pods"])) { return clusterURL(); diff --git a/src/renderer/components/cluster-icon/cluster-icon.tsx b/src/renderer/components/cluster-icon/cluster-icon.tsx index d3e7f05eee..8ee7c79aaf 100644 --- a/src/renderer/components/cluster-icon/cluster-icon.tsx +++ b/src/renderer/components/cluster-icon/cluster-icon.tsx @@ -1,13 +1,17 @@ import "./cluster-icon.scss"; import React, { DOMAttributes } from "react"; -import { observer } from "mobx-react"; +import { disposeOnUnmount, observer } from "mobx-react"; import { Params as HashiconParams } from "@emeraldpay/hashicon"; import { Hashicon } from "@emeraldpay/hashicon-react"; import { Cluster } from "../../../main/cluster"; import { cssNames, IClassName } from "../../utils"; import { Badge } from "../badge"; import { Tooltip } from "../tooltip"; +import { eventStore } from "../+events/event.store"; +import { forCluster } from "../../api/kube-api"; +import { subscribeToBroadcast, unsubscribeAllFromBroadcast } from "../../../common/ipc"; +import { observable, when } from "mobx"; interface Props extends DOMAttributes { cluster: Cluster; @@ -29,12 +33,29 @@ const defaultProps: Partial = { export class ClusterIcon extends React.Component { static defaultProps = defaultProps as object; + @observable eventCount = 0; + + get eventCountBroadcast() { + return `cluster-warning-event-count:${this.props.cluster.id}`; + } + + componentDidMount() { + const subscriber = subscribeToBroadcast(this.eventCountBroadcast, (ev, eventCount) => { + this.eventCount = eventCount; + }); + + disposeOnUnmount(this, [ + subscriber + ]); + } + render() { const { cluster, showErrors, showTooltip, errorClass, options, interactive, isActive, children, ...elemProps } = this.props; - const { isAdmin, name, eventCount, preferences, id: clusterId } = cluster; + const { name, preferences, id: clusterId } = cluster; + const eventCount = this.eventCount; const { icon } = preferences; const clusterIconId = `cluster-icon-${clusterId}`; const className = cssNames("ClusterIcon flex inline", this.props.className, { @@ -48,7 +69,7 @@ export class ClusterIcon extends React.Component { )} {icon && {name}/} {!icon && } - {showErrors && isAdmin && eventCount > 0 && ( + {showErrors && eventCount > 0 && !isActive && ( = 1000 ? Math.ceil(eventCount / 1000) + "k+" : eventCount}