From 4f8e4707f929eee04ff1345c9e93f00d50ac0a92 Mon Sep 17 00:00:00 2001 From: Sebastian Malton Date: Tue, 16 May 2023 11:52:38 -0400 Subject: [PATCH] fix: Switch to using IAsyncComputed to resolve bad setState error within ClusterOverview from react - The ClusterOverviewStore (now just ClusterStore) is not an exported type, so removing the legacy and wrong-abstraction code from it is not a breaking change Signed-off-by: Sebastian Malton --- .../cluster/cluster-metric-switchers.tsx | 113 ++--- .../cluster/cluster-metrics.injectable.ts | 36 ++ .../components/cluster/cluster-metrics.tsx | 47 +- .../cluster-overview-store.injectable.ts | 44 -- .../cluster-overview-store.ts | 122 ----- .../components/cluster/cluster-overview.tsx | 70 +-- .../components/cluster/cluster-pie-charts.tsx | 474 +++++++++--------- .../selected-metrics-type.injectable.ts | 58 +++ ...lected-node-role-for-metrics.injectable.ts | 63 +++ .../cluster/overview/storage.injectable.ts | 31 ++ .../components/cluster/store.injectable.ts | 30 ++ .../src/renderer/components/cluster/store.ts | 11 + 12 files changed, 567 insertions(+), 532 deletions(-) create mode 100644 packages/core/src/renderer/components/cluster/cluster-metrics.injectable.ts delete mode 100644 packages/core/src/renderer/components/cluster/cluster-overview-store/cluster-overview-store.injectable.ts delete mode 100644 packages/core/src/renderer/components/cluster/cluster-overview-store/cluster-overview-store.ts create mode 100644 packages/core/src/renderer/components/cluster/overview/selected-metrics-type.injectable.ts create mode 100644 packages/core/src/renderer/components/cluster/overview/selected-node-role-for-metrics.injectable.ts create mode 100644 packages/core/src/renderer/components/cluster/overview/storage.injectable.ts create mode 100644 packages/core/src/renderer/components/cluster/store.injectable.ts create mode 100644 packages/core/src/renderer/components/cluster/store.ts diff --git a/packages/core/src/renderer/components/cluster/cluster-metric-switchers.tsx b/packages/core/src/renderer/components/cluster/cluster-metric-switchers.tsx index 347d0db37b..d786308c4f 100644 --- a/packages/core/src/renderer/components/cluster/cluster-metric-switchers.tsx +++ b/packages/core/src/renderer/components/cluster/cluster-metric-switchers.tsx @@ -5,79 +5,68 @@ import React from "react"; import { observer } from "mobx-react"; -import type { NodeStore } from "../nodes/store"; import { Radio, RadioGroup } from "../radio"; -import type { ClusterOverviewStore } from "./cluster-overview-store/cluster-overview-store"; -import { MetricNodeRole, MetricType } from "./cluster-overview-store/cluster-overview-store"; -import clusterOverviewStoreInjectable from "./cluster-overview-store/cluster-overview-store.injectable"; import { withInjectables } from "@ogre-tools/injectable-react"; -import nodeStoreInjectable from "../nodes/store.injectable"; -import { normalizeMetrics } from "../../../common/k8s-api/endpoints/metrics.api"; +import type { SelectedMetricsType } from "./overview/selected-metrics-type.injectable"; +import type { SelectedNodeRoleForMetrics } from "./overview/selected-node-role-for-metrics.injectable"; +import selectedMetricsTypeInjectable from "./overview/selected-metrics-type.injectable"; +import selectedNodeRoleForMetricsInjectable from "./overview/selected-node-role-for-metrics.injectable"; interface Dependencies { - clusterOverviewStore: ClusterOverviewStore; - nodeStore: NodeStore; + selectedMetricsType: SelectedMetricsType; + selectedNodeRoleForMetrics: SelectedNodeRoleForMetrics; } const NonInjectedClusterMetricSwitchers = observer(({ - clusterOverviewStore, - nodeStore, -}: Dependencies) => { - const { masterNodes, workerNodes } = nodeStore; - const { cpuUsage, memoryUsage } = clusterOverviewStore.metrics ?? {}; - const hasMasterNodes = masterNodes.length > 0; - const hasWorkerNodes = workerNodes.length > 0; - const hasCpuMetrics = normalizeMetrics(cpuUsage).data.result[0].values.length > 0; - const hasMemoryMetrics = normalizeMetrics(memoryUsage).data.result[0].values.length > 0; - - return ( -
-
- clusterOverviewStore.metricNodeRole = metric} - > - - - -
-
- clusterOverviewStore.metricType = value} - > - - - -
+ selectedMetricsType, + selectedNodeRoleForMetrics, +}: Dependencies) => ( +
+
+ + + +
- ); -}); +
+ + + + +
+
+)); export const ClusterMetricSwitchers = withInjectables(NonInjectedClusterMetricSwitchers, { getProps: (di) => ({ - clusterOverviewStore: di.inject(clusterOverviewStoreInjectable), - nodeStore: di.inject(nodeStoreInjectable), + selectedMetricsType: di.inject(selectedMetricsTypeInjectable), + selectedNodeRoleForMetrics: di.inject(selectedNodeRoleForMetricsInjectable), }), }); diff --git a/packages/core/src/renderer/components/cluster/cluster-metrics.injectable.ts b/packages/core/src/renderer/components/cluster/cluster-metrics.injectable.ts new file mode 100644 index 0000000000..f18978becb --- /dev/null +++ b/packages/core/src/renderer/components/cluster/cluster-metrics.injectable.ts @@ -0,0 +1,36 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable } from "@ogre-tools/injectable"; +import { asyncComputed } from "@ogre-tools/injectable-react"; +import { now } from "mobx-utils"; +import type { ClusterMetricData } from "../../../common/k8s-api/endpoints/metrics.api/request-cluster-metrics-by-node-names.injectable"; +import requestClusterMetricsByNodeNamesInjectable from "../../../common/k8s-api/endpoints/metrics.api/request-cluster-metrics-by-node-names.injectable"; +import selectedNodeRoleForMetricsInjectable from "./overview/selected-node-role-for-metrics.injectable"; + +const everyMinute = 60 * 1000; + +const clusterOverviewMetricsInjectable = getInjectable({ + id: "cluster-overview-metrics", + instantiate: (di) => { + const requestClusterMetricsByNodeNames = di.inject(requestClusterMetricsByNodeNamesInjectable); + const selectedNodeRoleForMetrics = di.inject(selectedNodeRoleForMetricsInjectable); + + return asyncComputed({ + getValueFromObservedPromise: async () => { + now(everyMinute); + + const nodeNames = selectedNodeRoleForMetrics + .nodes + .get() + .map(node => node.getName()); + + return requestClusterMetricsByNodeNames(nodeNames); + }, + betweenUpdates: "show-latest-value", + }); + }, +}); + +export default clusterOverviewMetricsInjectable; diff --git a/packages/core/src/renderer/components/cluster/cluster-metrics.tsx b/packages/core/src/renderer/components/cluster/cluster-metrics.tsx index 726e37d76e..9f837e4306 100644 --- a/packages/core/src/renderer/components/cluster/cluster-metrics.tsx +++ b/packages/core/src/renderer/components/cluster/cluster-metrics.tsx @@ -8,8 +8,6 @@ import styles from "./cluster-metrics.module.scss"; import React, { useState } from "react"; import { observer } from "mobx-react"; import type { ChartOptions, ChartPoint } from "chart.js"; -import type { ClusterOverviewStore } from "./cluster-overview-store/cluster-overview-store"; -import { MetricType } from "./cluster-overview-store/cluster-overview-store"; import { BarChart } from "../chart"; import { bytesToUnits, cssNames } from "@k8slens/utilities"; import { Spinner } from "../spinner"; @@ -17,17 +15,34 @@ import { ZebraStripesPlugin } from "../chart/zebra-stripes.plugin"; import { ClusterNoMetrics } from "./cluster-no-metrics"; import { ClusterMetricSwitchers } from "./cluster-metric-switchers"; import { getMetricLastPoints } from "../../../common/k8s-api/endpoints/metrics.api"; +import type { IAsyncComputed } from "@ogre-tools/injectable-react"; import { withInjectables } from "@ogre-tools/injectable-react"; -import clusterOverviewStoreInjectable from "./cluster-overview-store/cluster-overview-store.injectable"; +import type { ClusterMetricData } from "../../../common/k8s-api/endpoints/metrics.api/request-cluster-metrics-by-node-names.injectable"; +import type { SelectedMetricsType } from "./overview/selected-metrics-type.injectable"; +import type { SelectedNodeRoleForMetrics } from "./overview/selected-node-role-for-metrics.injectable"; +import clusterOverviewMetricsInjectable from "./cluster-metrics.injectable"; +import selectedMetricsTypeInjectable from "./overview/selected-metrics-type.injectable"; +import selectedNodeRoleForMetricsInjectable from "./overview/selected-node-role-for-metrics.injectable"; interface Dependencies { - clusterOverviewStore: ClusterOverviewStore; + clusterOverviewMetrics: IAsyncComputed; + selectedMetricsType: SelectedMetricsType; + selectedNodeRoleForMetrics: SelectedNodeRoleForMetrics; } -const NonInjectedClusterMetrics = observer(({ clusterOverviewStore: { metricType, metricNodeRole, getMetricsValues, metricsLoaded, metrics }}: Dependencies) => { +const NonInjectedClusterMetrics = observer((props: Dependencies) => { + const { + clusterOverviewMetrics, + selectedMetricsType, + selectedNodeRoleForMetrics, + } = props; + + const metrics = clusterOverviewMetrics.value.get(); const [plugins] = useState([new ZebraStripesPlugin()]); const { memoryCapacity, cpuCapacity } = getMetricLastPoints(metrics ?? {}); - const metricValues = getMetricsValues(metrics ?? {}); + const metricValues = selectedMetricsType.metrics.get(); + const metricType = selectedMetricsType.value.get(); + const metricNodeRole = selectedNodeRoleForMetrics.value.get(); const colors = { cpu: "#3D90CE", memory: "#C93DCE" }; const data = metricValues.map(value => ({ x: value[0], @@ -86,10 +101,10 @@ const NonInjectedClusterMetrics = observer(({ clusterOverviewStore: { metricType }, }, }; - const options = metricType === MetricType.CPU ? cpuOptions : memoryOptions; + const options = metricType === "cpu" ? cpuOptions : memoryOptions; const renderMetrics = () => { - if (!metricValues.length && !metricsLoaded) { + if (!metricValues.length && !metrics) { return ; } @@ -118,12 +133,10 @@ const NonInjectedClusterMetrics = observer(({ clusterOverviewStore: { metricType ); }); -export const ClusterMetrics = withInjectables( - NonInjectedClusterMetrics, - - { - getProps: (di) => ({ - clusterOverviewStore: di.inject(clusterOverviewStoreInjectable), - }), - }, -); +export const ClusterMetrics = withInjectables(NonInjectedClusterMetrics, { + getProps: (di) => ({ + clusterOverviewMetrics: di.inject(clusterOverviewMetricsInjectable), + selectedMetricsType: di.inject(selectedMetricsTypeInjectable), + selectedNodeRoleForMetrics: di.inject(selectedNodeRoleForMetricsInjectable), + }), +}); diff --git a/packages/core/src/renderer/components/cluster/cluster-overview-store/cluster-overview-store.injectable.ts b/packages/core/src/renderer/components/cluster/cluster-overview-store/cluster-overview-store.injectable.ts deleted file mode 100644 index 929b24792b..0000000000 --- a/packages/core/src/renderer/components/cluster/cluster-overview-store/cluster-overview-store.injectable.ts +++ /dev/null @@ -1,44 +0,0 @@ -/** - * Copyright (c) OpenLens Authors. All rights reserved. - * Licensed under MIT License. See LICENSE in root directory for more information. - */ -import { getInjectable } from "@ogre-tools/injectable"; - -import type { ClusterOverviewStorageState } from "./cluster-overview-store"; -import { ClusterOverviewStore, MetricNodeRole, MetricType } from "./cluster-overview-store"; -import createStorageInjectable from "../../../utils/create-storage/create-storage.injectable"; -import { kubeObjectStoreInjectionToken } from "../../../../common/k8s-api/api-manager/kube-object-store-token"; -import clusterApiInjectable from "../../../../common/k8s-api/endpoints/cluster.api.injectable"; -import storesAndApisCanBeCreatedInjectable from "../../../stores-apis-can-be-created.injectable"; -import assert from "assert"; -import nodeStoreInjectable from "../../nodes/store.injectable"; -import requestClusterMetricsByNodeNamesInjectable from "../../../../common/k8s-api/endpoints/metrics.api/request-cluster-metrics-by-node-names.injectable"; -import clusterFrameContextForNamespacedResourcesInjectable from "../../../cluster-frame-context/for-namespaced-resources.injectable"; -import { loggerInjectionToken } from "@k8slens/logger"; - -const clusterOverviewStoreInjectable = getInjectable({ - id: "cluster-overview-store", - - instantiate: (di) => { - assert(di.inject(storesAndApisCanBeCreatedInjectable), "clusterOverviewStore is only available in certain environments"); - const createStorage = di.inject(createStorageInjectable); - const clusterApi = di.inject(clusterApiInjectable); - - return new ClusterOverviewStore({ - storage: createStorage( - "cluster_overview", - { - metricType: MetricType.CPU, // setup defaults - metricNodeRole: MetricNodeRole.WORKER, - }, - ), - nodeStore: di.inject(nodeStoreInjectable), - requestClusterMetricsByNodeNames: di.inject(requestClusterMetricsByNodeNamesInjectable), - context: di.inject(clusterFrameContextForNamespacedResourcesInjectable), - logger: di.inject(loggerInjectionToken), - }, clusterApi); - }, - injectionToken: kubeObjectStoreInjectionToken, -}); - -export default clusterOverviewStoreInjectable; diff --git a/packages/core/src/renderer/components/cluster/cluster-overview-store/cluster-overview-store.ts b/packages/core/src/renderer/components/cluster/cluster-overview-store/cluster-overview-store.ts deleted file mode 100644 index 0966e71b06..0000000000 --- a/packages/core/src/renderer/components/cluster/cluster-overview-store/cluster-overview-store.ts +++ /dev/null @@ -1,122 +0,0 @@ -/** - * Copyright (c) OpenLens Authors. All rights reserved. - * Licensed under MIT License. See LICENSE in root directory for more information. - */ - -import { action, observable, reaction, when, makeObservable, runInAction } from "mobx"; -import type { KubeObjectStoreDependencies } from "../../../../common/k8s-api/kube-object.store"; -import { KubeObjectStore } from "../../../../common/k8s-api/kube-object.store"; -import type { ClusterApi } from "../../../../common/k8s-api/endpoints"; -import type { Cluster } from "@k8slens/kube-object"; -import type { StorageLayer } from "../../../utils/storage-helper"; -import type { NodeStore } from "../../nodes/store"; -import type { ClusterMetricData, RequestClusterMetricsByNodeNames } from "../../../../common/k8s-api/endpoints/metrics.api/request-cluster-metrics-by-node-names.injectable"; -import type { RequestMetricsParams } from "../../../../common/k8s-api/endpoints/metrics.api/request-metrics.injectable"; -import { normalizeMetrics } from "../../../../common/k8s-api/endpoints/metrics.api"; -import autoBind from "auto-bind"; - -export enum MetricType { - MEMORY = "memory", - CPU = "cpu", -} - -export enum MetricNodeRole { - MASTER = "master", - WORKER = "worker", -} - -export interface ClusterOverviewStorageState { - metricType: MetricType; - metricNodeRole: MetricNodeRole; -} - -interface ClusterOverviewStoreDependencies extends KubeObjectStoreDependencies { - readonly storage: StorageLayer; - readonly nodeStore: NodeStore; - requestClusterMetricsByNodeNames: RequestClusterMetricsByNodeNames; -} - -export class ClusterOverviewStore extends KubeObjectStore implements ClusterOverviewStorageState { - @observable metrics: ClusterMetricData | undefined = undefined; - - get metricsLoaded() { - return !!this.metrics; - } - - get metricType(): MetricType { - return this.dependencies.storage.get().metricType; - } - - set metricType(value: MetricType) { - this.dependencies.storage.merge({ metricType: value }); - } - - get metricNodeRole(): MetricNodeRole { - return this.dependencies.storage.get().metricNodeRole; - } - - set metricNodeRole(value: MetricNodeRole) { - this.dependencies.storage.merge({ metricNodeRole: value }); - } - - constructor(protected readonly dependencies: ClusterOverviewStoreDependencies, api: ClusterApi) { - super(dependencies, api); - makeObservable(this); - autoBind(this); - - this.init(); - } - - private init() { - // TODO: refactor, seems not a correct place to be - // auto-refresh metrics on user-action - reaction(() => this.metricNodeRole, () => { - if (this.metrics) { - this.resetMetrics(); - void this.loadMetrics(); - } - }); - - // check which node type to select - reaction(() => this.dependencies.nodeStore.items.length, () => { - const { masterNodes, workerNodes } = this.dependencies.nodeStore; - - if (!masterNodes.length) this.metricNodeRole = MetricNodeRole.WORKER; - if (!workerNodes.length) this.metricNodeRole = MetricNodeRole.MASTER; - }); - } - - async loadMetrics(params?: RequestMetricsParams) { - await when(() => this.dependencies.nodeStore.isLoaded); - const { masterNodes, workerNodes } = this.dependencies.nodeStore; - const nodes = this.metricNodeRole === MetricNodeRole.MASTER && masterNodes.length ? masterNodes : workerNodes; - - const metrics = await this.dependencies.requestClusterMetricsByNodeNames(nodes.map(node => node.getName()), params); - - runInAction(() => { - this.metrics = metrics; - }); - } - - getMetricsValues(source: Partial): [number, string][] { - switch (this.metricType) { - case MetricType.CPU: - return normalizeMetrics(source.cpuUsage).data.result[0].values; - case MetricType.MEMORY: - return normalizeMetrics(source.memoryUsage).data.result[0].values; - default: - return []; - } - } - - @action - resetMetrics() { - this.metrics = undefined; - } - - reset() { - super.reset(); - this.resetMetrics(); - this.dependencies.storage?.reset(); - } -} diff --git a/packages/core/src/renderer/components/cluster/cluster-overview.tsx b/packages/core/src/renderer/components/cluster/cluster-overview.tsx index 9d84bf9eef..f14c8fea2e 100644 --- a/packages/core/src/renderer/components/cluster/cluster-overview.tsx +++ b/packages/core/src/renderer/components/cluster/cluster-overview.tsx @@ -7,19 +7,16 @@ import styles from "./cluster-overview.module.scss"; import React from "react"; import type { IComputedValue } from "mobx"; -import { reaction } from "mobx"; import { disposeOnUnmount, observer } from "mobx-react"; import type { NodeStore } from "../nodes/store"; import type { PodStore } from "../workloads-pods/store"; -import { interval } from "@k8slens/utilities"; import { TabLayout } from "../layout/tab-layout"; import { Spinner } from "../spinner"; import { ClusterIssues } from "./cluster-issues"; -import type { ClusterOverviewStore } from "./cluster-overview-store/cluster-overview-store"; import { ClusterMetricsResourceType } from "../../../common/cluster-types"; import type { EventStore } from "../events/store"; +import type { IAsyncComputed } from "@ogre-tools/injectable-react"; import { withInjectables } from "@ogre-tools/injectable-react"; -import clusterOverviewStoreInjectable from "./cluster-overview-store/cluster-overview-store.injectable"; import type { SubscribeStores } from "../../kube-watch-api/kube-watch-api"; import subscribeStoresInjectable from "../../kube-watch-api/subscribe-stores.injectable"; import podStoreInjectable from "../workloads-pods/store.injectable"; @@ -30,84 +27,61 @@ import type { ClusterOverviewUIBlock } from "@k8slens/metrics"; import { clusterOverviewUIBlockInjectionToken } from "@k8slens/metrics"; import { orderByOrderNumber } from "../../../common/utils/composable-responsibilities/orderable/orderable"; import { computedInjectManyInjectable } from "@ogre-tools/injectable-extension-for-mobx"; +import type { ClusterMetricData } from "../../../common/k8s-api/endpoints/metrics.api/request-cluster-metrics-by-node-names.injectable"; +import clusterOverviewMetricsInjectable from "./cluster-metrics.injectable"; interface Dependencies { subscribeStores: SubscribeStores; - clusterOverviewStore: ClusterOverviewStore; podStore: PodStore; eventStore: EventStore; nodeStore: NodeStore; clusterMetricsAreVisible: IComputedValue; uiBlocks: IComputedValue; + clusterOverviewMetrics: IAsyncComputed; } @observer class NonInjectedClusterOverview extends React.Component { - private readonly metricPoller = interval(60, async () => { - try { - await this.props.clusterOverviewStore.loadMetrics(); - } catch { - // ignore - } - }); - componentDidMount() { - this.metricPoller.start(true); - disposeOnUnmount(this, [ this.props.subscribeStores([ this.props.podStore, this.props.eventStore, this.props.nodeStore, ]), - - reaction( - () => this.props.clusterOverviewStore.metricNodeRole, // Toggle Master/Worker node switcher - () => this.metricPoller.restart(true), - ), ]); } - componentWillUnmount() { - this.metricPoller.stop(); - } - - renderMetrics(isMetricsHidden: boolean) { - if (isMetricsHidden) { - return null; - } - + renderWithMetrics() { return ( <> - {orderByOrderNumber(this.props.uiBlocks.get()).map((block) => ( - - ))} - - ); - } - - renderClusterOverview(isLoaded: boolean, isMetricsHidden: boolean) { - if (!isLoaded) { - return ; - } - - return ( - <> - {this.renderMetrics(isMetricsHidden)} - + { + orderByOrderNumber(this.props.uiBlocks.get()) + .map((block) => ( + + )) + } + ); } render() { const { eventStore, nodeStore, clusterMetricsAreVisible } = this.props; - const isLoaded = nodeStore.isLoaded && eventStore.isLoaded; const isMetricsHidden = !clusterMetricsAreVisible.get(); return (
- {this.renderClusterOverview(isLoaded, isMetricsHidden)} + { + (!nodeStore.isLoaded || !eventStore.isLoaded) + ? + : ( + isMetricsHidden + ? + : this.renderWithMetrics() + ) + }
); @@ -117,11 +91,11 @@ class NonInjectedClusterOverview extends React.Component { export const ClusterOverview = withInjectables(NonInjectedClusterOverview, { getProps: (di) => ({ subscribeStores: di.inject(subscribeStoresInjectable), - clusterOverviewStore: di.inject(clusterOverviewStoreInjectable), clusterMetricsAreVisible: di.inject(enabledMetricsInjectable, ClusterMetricsResourceType.Cluster), podStore: di.inject(podStoreInjectable), eventStore: di.inject(eventStoreInjectable), nodeStore: di.inject(nodeStoreInjectable), uiBlocks: di.inject(computedInjectManyInjectable)(clusterOverviewUIBlockInjectionToken), + clusterOverviewMetrics: di.inject(clusterOverviewMetricsInjectable), }), }); diff --git a/packages/core/src/renderer/components/cluster/cluster-pie-charts.tsx b/packages/core/src/renderer/components/cluster/cluster-pie-charts.tsx index 5a2afe22b2..f14229fe03 100644 --- a/packages/core/src/renderer/components/cluster/cluster-pie-charts.tsx +++ b/packages/core/src/renderer/components/cluster/cluster-pie-charts.tsx @@ -7,23 +7,24 @@ import styles from "./cluster-pie-charts.module.scss"; import React from "react"; import { observer } from "mobx-react"; -import type { ClusterOverviewStore } from "./cluster-overview-store/cluster-overview-store"; -import { MetricNodeRole } from "./cluster-overview-store/cluster-overview-store"; import { Spinner } from "../spinner"; import { Icon } from "../icon"; -import type { NodeStore } from "../nodes/store"; import type { PieChartData } from "../chart"; import { PieChart } from "../chart"; import { ClusterNoMetrics } from "./cluster-no-metrics"; import { bytesToUnits, cssNames } from "@k8slens/utilities"; import type { LensTheme } from "../../themes/lens-theme"; import { getMetricLastPoints } from "../../../common/k8s-api/endpoints/metrics.api"; +import type { IAsyncComputed } from "@ogre-tools/injectable-react"; import { withInjectables } from "@ogre-tools/injectable-react"; -import clusterOverviewStoreInjectable from "./cluster-overview-store/cluster-overview-store.injectable"; -import nodeStoreInjectable from "../nodes/store.injectable"; import type { IComputedValue } from "mobx"; import activeThemeInjectable from "../../themes/active.injectable"; import type { ClusterMetricData } from "../../../common/k8s-api/endpoints/metrics.api/request-cluster-metrics-by-node-names.injectable"; +import { isNumber } from "lodash"; +import type { SelectedNodeRoleForMetrics } from "./overview/selected-node-role-for-metrics.injectable"; +import type { Node } from "@k8slens/kube-object"; +import clusterOverviewMetricsInjectable from "./cluster-metrics.injectable"; +import selectedNodeRoleForMetricsInjectable from "./overview/selected-node-role-for-metrics.injectable"; function createLabels(rawLabelData: [string, number | undefined][]): string[] { return rawLabelData.map(([key, value]) => `${key}: ${value?.toFixed(2) || "N/A"}`); @@ -36,253 +37,248 @@ const checkedBytesToUnits = (value: number | undefined) => ( ); interface Dependencies { - clusterOverviewStore: ClusterOverviewStore; - nodeStore: NodeStore; + selectedNodeRoleForMetrics: SelectedNodeRoleForMetrics; + clusterOverviewMetrics: IAsyncComputed; activeTheme: IComputedValue; } -const NonInjectedClusterPieCharts = observer(({ - clusterOverviewStore, - nodeStore, - activeTheme, -}: Dependencies) => { - const renderLimitWarning = () => { - return ( -
- -

Specified limits are higher than node capacity!

-
- ); +const renderLimitWarning = () => ( +
+ +

Specified limits are higher than node capacity!

+
+); + +const renderCharts = (defaultColor: string, lastPoints: Partial>) => { + const { + memoryUsage, memoryRequests, memoryAllocatableCapacity, memoryCapacity, memoryLimits, + cpuUsage, cpuRequests, cpuAllocatableCapacity, cpuCapacity, cpuLimits, + podUsage, podAllocatableCapacity, podCapacity, + } = lastPoints; + + if ( + !isNumber(cpuCapacity) || + !isNumber(cpuAllocatableCapacity) || + !isNumber(podCapacity) || + !isNumber(podAllocatableCapacity) || + !isNumber(memoryAllocatableCapacity) || + !isNumber(memoryCapacity) || + !isNumber(memoryUsage) || + !isNumber(memoryRequests) + ) { + return null; + } + + const cpuData: PieChartData = { + datasets: [ + { + data: [ + cpuUsage, + cpuUsage ? cpuAllocatableCapacity - cpuUsage : 1, + ], + backgroundColor: [ + "#c93dce", + defaultColor, + ], + id: "cpuUsage", + label: "Usage", + }, + { + data: [ + cpuRequests, + cpuRequests ? cpuAllocatableCapacity - cpuRequests : 1, + ], + backgroundColor: [ + "#4caf50", + defaultColor, + ], + id: "cpuRequests", + label: "Requests", + }, + { + data: [ + cpuLimits, + Math.max(0, cpuAllocatableCapacity - (cpuLimits ?? cpuAllocatableCapacity)), + ], + backgroundColor: [ + "#3d90ce", + defaultColor, + ], + id: "cpuLimits", + label: "Limits", + }, + ], + labels: createLabels([ + ["Usage", cpuUsage], + ["Requests", cpuRequests], + ["Limits", cpuLimits], + ["Allocatable Capacity", cpuAllocatableCapacity], + ["Capacity", cpuCapacity], + ]), }; - - const renderCharts = (lastPoints: Partial>) => { - const { - memoryUsage, memoryRequests, memoryAllocatableCapacity, memoryCapacity, memoryLimits, - cpuUsage, cpuRequests, cpuAllocatableCapacity, cpuCapacity, cpuLimits, - podUsage, podAllocatableCapacity, podCapacity, - } = lastPoints; - - if ( - typeof cpuCapacity !== "number" || - typeof cpuAllocatableCapacity !== "number" || - typeof podCapacity !== "number" || - typeof podAllocatableCapacity !== "number" || - typeof memoryAllocatableCapacity !== "number" || - typeof memoryCapacity !== "number" || - typeof memoryUsage !== "number" || - typeof memoryRequests !== "number" - ) { - return null; - } - - const defaultColor = activeTheme.get().colors.pieChartDefaultColor; - - const cpuData: PieChartData = { - datasets: [ - { - data: [ - cpuUsage, - cpuUsage ? cpuAllocatableCapacity - cpuUsage : 1, - ], - backgroundColor: [ - "#c93dce", - defaultColor, - ], - id: "cpuUsage", - label: "Usage", - }, - { - data: [ - cpuRequests, - cpuRequests ? cpuAllocatableCapacity - cpuRequests : 1, - ], - backgroundColor: [ - "#4caf50", - defaultColor, - ], - id: "cpuRequests", - label: "Requests", - }, - { - data: [ - cpuLimits, - Math.max(0, cpuAllocatableCapacity - (cpuLimits ?? cpuAllocatableCapacity)), - ], - backgroundColor: [ - "#3d90ce", - defaultColor, - ], - id: "cpuLimits", - label: "Limits", - }, - ], - labels: createLabels([ - ["Usage", cpuUsage], - ["Requests", cpuRequests], - ["Limits", cpuLimits], - ["Allocatable Capacity", cpuAllocatableCapacity], - ["Capacity", cpuCapacity], - ]), - }; - const memoryData: PieChartData = { - datasets: [ - { - data: [ - memoryUsage, - memoryUsage ? memoryAllocatableCapacity - memoryUsage : 1, - ], - backgroundColor: [ - "#c93dce", - defaultColor, - ], - id: "memoryUsage", - label: "Usage", - }, - { - data: [ - memoryRequests, - memoryRequests ? memoryAllocatableCapacity - memoryRequests : 1, - ], - backgroundColor: [ - "#4caf50", - defaultColor, - ], - id: "memoryRequests", - label: "Requests", - }, - { - data: [ - memoryLimits, - Math.max(0, memoryAllocatableCapacity - (memoryLimits ?? memoryAllocatableCapacity)), - ], - backgroundColor: [ - "#3d90ce", - defaultColor, - ], - id: "memoryLimits", - label: "Limits", - }, - ], - labels: [ - `Usage: ${bytesToUnits(memoryUsage)}`, - `Requests: ${bytesToUnits(memoryRequests)}`, - `Limits: ${checkedBytesToUnits(memoryLimits)}`, - `Allocatable Capacity: ${bytesToUnits(memoryAllocatableCapacity)}`, - `Capacity: ${bytesToUnits(memoryCapacity)}`, - ], - }; - const podsData: PieChartData = { - datasets: [ - { - data: [ - podUsage, - podUsage ? podAllocatableCapacity - podUsage : 1, - ], - backgroundColor: [ - "#4caf50", - defaultColor, - ], - id: "podUsage", - label: "Usage", - tooltipLabels: [ - (percent) => `Usage: ${percent}`, - (percent) => `Available: ${percent}`, - ], - }, - ], - labels: [ - `Usage: ${podUsage || 0}`, - `Capacity: ${podAllocatableCapacity}`, - ], - }; - - return ( -
-
- - {((cpuLimits ?? cpuAllocatableCapacity) > cpuAllocatableCapacity) && renderLimitWarning()} -
-
- - {((memoryLimits ?? memoryAllocatableCapacity) > memoryAllocatableCapacity) && renderLimitWarning()} -
-
- -
-
- ); + const memoryData: PieChartData = { + datasets: [ + { + data: [ + memoryUsage, + memoryUsage ? memoryAllocatableCapacity - memoryUsage : 1, + ], + backgroundColor: [ + "#c93dce", + defaultColor, + ], + id: "memoryUsage", + label: "Usage", + }, + { + data: [ + memoryRequests, + memoryRequests ? memoryAllocatableCapacity - memoryRequests : 1, + ], + backgroundColor: [ + "#4caf50", + defaultColor, + ], + id: "memoryRequests", + label: "Requests", + }, + { + data: [ + memoryLimits, + Math.max(0, memoryAllocatableCapacity - (memoryLimits ?? memoryAllocatableCapacity)), + ], + backgroundColor: [ + "#3d90ce", + defaultColor, + ], + id: "memoryLimits", + label: "Limits", + }, + ], + labels: [ + `Usage: ${bytesToUnits(memoryUsage)}`, + `Requests: ${bytesToUnits(memoryRequests)}`, + `Limits: ${checkedBytesToUnits(memoryLimits)}`, + `Allocatable Capacity: ${bytesToUnits(memoryAllocatableCapacity)}`, + `Capacity: ${bytesToUnits(memoryCapacity)}`, + ], }; - - const renderContent = ({ metricNodeRole, metrics }: ClusterOverviewStore) => { - const { masterNodes, workerNodes } = nodeStore; - const nodes = metricNodeRole === MetricNodeRole.MASTER ? masterNodes : workerNodes; - - if (!nodes.length) { - return ( -
- - No Nodes Available. -
- ); - } - - if (!metrics) { - return ( -
- -
- ); - } - - const lastPoints = getMetricLastPoints(metrics); - const { memoryCapacity, cpuCapacity, podCapacity } = lastPoints; - - if (!memoryCapacity || !cpuCapacity || !podCapacity) { - return ( -
- -
- ); - } - - return renderCharts(lastPoints); + const podsData: PieChartData = { + datasets: [ + { + data: [ + podUsage, + podUsage ? podAllocatableCapacity - podUsage : 1, + ], + backgroundColor: [ + "#4caf50", + defaultColor, + ], + id: "podUsage", + label: "Usage", + tooltipLabels: [ + (percent) => `Usage: ${percent}`, + (percent) => `Available: ${percent}`, + ], + }, + ], + labels: [ + `Usage: ${podUsage || 0}`, + `Capacity: ${podAllocatableCapacity}`, + ], }; return ( -
- {renderContent(clusterOverviewStore)} +
+
+ + {((cpuLimits ?? cpuAllocatableCapacity) > cpuAllocatableCapacity) && renderLimitWarning()} +
+
+ + {((memoryLimits ?? memoryAllocatableCapacity) > memoryAllocatableCapacity) && renderLimitWarning()} +
+
+ +
); -}); +}; + +const renderContent = (defaultColor: string, nodes: Node[], metrics: ClusterMetricData | undefined) => { + if (!nodes.length) { + return ( +
+ + No Nodes Available. +
+ ); + } + + if (!metrics) { + return ( +
+ +
+ ); + } + + const lastPoints = getMetricLastPoints(metrics); + const { memoryCapacity, cpuCapacity, podCapacity } = lastPoints; + + if (!memoryCapacity || !cpuCapacity || !podCapacity) { + return ( +
+ +
+ ); + } + + return renderCharts(defaultColor, lastPoints); +}; + +const NonInjectedClusterPieCharts = observer(({ + selectedNodeRoleForMetrics, + clusterOverviewMetrics, + activeTheme, +}: Dependencies) => ( +
+ {renderContent( + activeTheme.get().colors.pieChartDefaultColor, + selectedNodeRoleForMetrics.nodes.get(), + clusterOverviewMetrics.value.get(), + )} +
+)); export const ClusterPieCharts = withInjectables(NonInjectedClusterPieCharts, { getProps: (di) => ({ - clusterOverviewStore: di.inject(clusterOverviewStoreInjectable), - nodeStore: di.inject(nodeStoreInjectable), activeTheme: di.inject(activeThemeInjectable), + clusterOverviewMetrics: di.inject(clusterOverviewMetricsInjectable), + selectedNodeRoleForMetrics: di.inject(selectedNodeRoleForMetricsInjectable), }), }); diff --git a/packages/core/src/renderer/components/cluster/overview/selected-metrics-type.injectable.ts b/packages/core/src/renderer/components/cluster/overview/selected-metrics-type.injectable.ts new file mode 100644 index 0000000000..2eb768dec4 --- /dev/null +++ b/packages/core/src/renderer/components/cluster/overview/selected-metrics-type.injectable.ts @@ -0,0 +1,58 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable } from "@ogre-tools/injectable"; +import { action, computed } from "mobx"; +import { normalizeMetrics } from "../../../../common/k8s-api/endpoints/metrics.api"; +import clusterOverviewMetricsInjectable from "../cluster-metrics.injectable"; +import type { MetricType } from "./storage.injectable"; +import clusterOverviewStorageInjectable from "./storage.injectable"; + +export type SelectedMetricsType = ReturnType; + +const selectedMetricsTypeInjectable = getInjectable({ + id: "selected-metrics-type", + instantiate: (di) => { + const storage = di.inject(clusterOverviewStorageInjectable); + const overviewMetrics = di.inject(clusterOverviewMetricsInjectable); + + const value = computed(() => storage.get().metricType); + const metrics = computed((): [number, string][] => { + const rawValue = overviewMetrics.value.get(); + + if (!rawValue) { + return []; + } + + const type = value.get(); + + switch (type) { + case "cpu": + return normalizeMetrics(rawValue.cpuUsage).data.result[0].values; + case "memory": + return normalizeMetrics(rawValue.memoryUsage).data.result[0].values; + default: + return []; + } + }); + const hasCPUMetrics = computed(() => ( + normalizeMetrics(overviewMetrics.value.get()?.cpuUsage).data.result[0].values.length > 0 + )); + const hasMemoryMetrics = computed(() => ( + normalizeMetrics(overviewMetrics.value.get()?.memoryUsage).data.result[0].values.length > 0 + )); + + return { + value, + metrics, + hasCPUMetrics, + hasMemoryMetrics, + set: action((value: MetricType) => { + storage.merge({ metricType: value }); + }), + }; + }, +}); + +export default selectedMetricsTypeInjectable; diff --git a/packages/core/src/renderer/components/cluster/overview/selected-node-role-for-metrics.injectable.ts b/packages/core/src/renderer/components/cluster/overview/selected-node-role-for-metrics.injectable.ts new file mode 100644 index 0000000000..50026be985 --- /dev/null +++ b/packages/core/src/renderer/components/cluster/overview/selected-node-role-for-metrics.injectable.ts @@ -0,0 +1,63 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable } from "@ogre-tools/injectable"; +import { action, computed } from "mobx"; +import nodeStoreInjectable from "../../nodes/store.injectable"; +import type { MetricNodeRole } from "./storage.injectable"; +import clusterOverviewStorageInjectable from "./storage.injectable"; + +export type SelectedNodeRoleForMetrics = ReturnType; + +const selectedNodeRoleForMetricsInjectable = getInjectable({ + id: "selected-node-role-for-metrics", + instantiate: (di) => { + const storage = di.inject(clusterOverviewStorageInjectable); + const nodeStore = di.inject(nodeStoreInjectable); + + const value = computed(() => { + const { masterNodes, workerNodes } = nodeStore; + const rawValue = storage.get().metricNodeRole; + + const hasMasterNodes = masterNodes.length > 0; + const hasWorkerNodes = workerNodes.length > 0; + + if (hasMasterNodes && !hasWorkerNodes && rawValue === "worker") { + return "master"; + } + + if (!hasMasterNodes && hasWorkerNodes && rawValue === "master") { + return "worker"; + } + + return rawValue; + }); + + const nodes = computed(() => { + const { masterNodes, workerNodes } = nodeStore; + const role = value.get(); + + if (role === "master") { + return masterNodes.slice(); + } + + return workerNodes.slice(); + }); + + const hasMasterNodes = computed(() => nodeStore.masterNodes.length > 0); + const hasWorkerNodes = computed(() => nodeStore.workerNodes.length > 0); + + return { + value, + nodes, + hasMasterNodes, + hasWorkerNodes, + set: action((value: MetricNodeRole) => { + storage.merge({ metricNodeRole: value }); + }), + }; + }, +}); + +export default selectedNodeRoleForMetricsInjectable; diff --git a/packages/core/src/renderer/components/cluster/overview/storage.injectable.ts b/packages/core/src/renderer/components/cluster/overview/storage.injectable.ts new file mode 100644 index 0000000000..91a5d6420e --- /dev/null +++ b/packages/core/src/renderer/components/cluster/overview/storage.injectable.ts @@ -0,0 +1,31 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable } from "@ogre-tools/injectable"; +import createStorageInjectable from "../../../utils/create-storage/create-storage.injectable"; + +export type MetricType = "memory" | "cpu"; +export type MetricNodeRole = "master" | "worker"; + +export interface ClusterOverviewStorageState { + metricType: MetricType; + metricNodeRole: MetricNodeRole; +} + +const clusterOverviewStorageInjectable = getInjectable({ + id: "cluster-overview-storage", + instantiate: (di) => { + const createStorage = di.inject(createStorageInjectable); + + return createStorage( + "cluster_overview", + { + metricType: "cpu", // setup defaults + metricNodeRole: "worker", + }, + ); + }, +}); + +export default clusterOverviewStorageInjectable; diff --git a/packages/core/src/renderer/components/cluster/store.injectable.ts b/packages/core/src/renderer/components/cluster/store.injectable.ts new file mode 100644 index 0000000000..cf0ce6888b --- /dev/null +++ b/packages/core/src/renderer/components/cluster/store.injectable.ts @@ -0,0 +1,30 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable } from "@ogre-tools/injectable"; + +import { ClusterStore } from "./store"; +import { kubeObjectStoreInjectionToken } from "../../../common/k8s-api/api-manager/kube-object-store-token"; +import clusterApiInjectable from "../../../common/k8s-api/endpoints/cluster.api.injectable"; +import storesAndApisCanBeCreatedInjectable from "../../stores-apis-can-be-created.injectable"; +import assert from "assert"; +import clusterFrameContextForNamespacedResourcesInjectable from "../../cluster-frame-context/for-namespaced-resources.injectable"; +import { loggerInjectionToken } from "@k8slens/logger"; + +const clusterStoreInjectable = getInjectable({ + id: "cluster-store", + + instantiate: (di) => { + assert(di.inject(storesAndApisCanBeCreatedInjectable), "clusterStore is only available in certain environments"); + const clusterApi = di.inject(clusterApiInjectable); + + return new ClusterStore({ + context: di.inject(clusterFrameContextForNamespacedResourcesInjectable), + logger: di.inject(loggerInjectionToken), + }, clusterApi); + }, + injectionToken: kubeObjectStoreInjectionToken, +}); + +export default clusterStoreInjectable; diff --git a/packages/core/src/renderer/components/cluster/store.ts b/packages/core/src/renderer/components/cluster/store.ts new file mode 100644 index 0000000000..748cbf82ea --- /dev/null +++ b/packages/core/src/renderer/components/cluster/store.ts @@ -0,0 +1,11 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ + +import { KubeObjectStore } from "../../../common/k8s-api/kube-object.store"; +import type { ClusterApi } from "../../../../common/k8s-api/endpoints"; +import type { Cluster } from "@k8slens/kube-object"; + +export class ClusterStore extends KubeObjectStore { +}