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

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 <sebastian@malton.name>
This commit is contained in:
Sebastian Malton 2023-05-16 11:52:38 -04:00
parent 4a7eff1841
commit 4f8e4707f9
12 changed files with 567 additions and 532 deletions

View File

@ -5,49 +5,39 @@
import React from "react"; import React from "react";
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import type { NodeStore } from "../nodes/store";
import { Radio, RadioGroup } from "../radio"; 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 { withInjectables } from "@ogre-tools/injectable-react";
import nodeStoreInjectable from "../nodes/store.injectable"; import type { SelectedMetricsType } from "./overview/selected-metrics-type.injectable";
import { normalizeMetrics } from "../../../common/k8s-api/endpoints/metrics.api"; 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 { interface Dependencies {
clusterOverviewStore: ClusterOverviewStore; selectedMetricsType: SelectedMetricsType;
nodeStore: NodeStore; selectedNodeRoleForMetrics: SelectedNodeRoleForMetrics;
} }
const NonInjectedClusterMetricSwitchers = observer(({ const NonInjectedClusterMetricSwitchers = observer(({
clusterOverviewStore, selectedMetricsType,
nodeStore, selectedNodeRoleForMetrics,
}: Dependencies) => { }: 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 (
<div className="flex gaps" style={{ marginBottom: "calc(var(--margin) * 2)" }}> <div className="flex gaps" style={{ marginBottom: "calc(var(--margin) * 2)" }}>
<div className="box grow"> <div className="box grow">
<RadioGroup <RadioGroup
asButtons asButtons
className="RadioGroup flex gaps" className="RadioGroup flex gaps"
value={clusterOverviewStore.metricNodeRole} value={selectedNodeRoleForMetrics.value.get()}
onChange={metric => clusterOverviewStore.metricNodeRole = metric} onChange={selectedNodeRoleForMetrics.set}
> >
<Radio <Radio
label="Master" label="Master"
value={MetricNodeRole.MASTER} value="master"
disabled={!hasMasterNodes} disabled={!selectedNodeRoleForMetrics.hasMasterNodes.get()}
/> />
<Radio <Radio
label="Worker" label="Worker"
value={MetricNodeRole.WORKER} value="worker"
disabled={!hasWorkerNodes} disabled={!selectedNodeRoleForMetrics.hasWorkerNodes.get()}
/> />
</RadioGroup> </RadioGroup>
</div> </div>
@ -55,29 +45,28 @@ const NonInjectedClusterMetricSwitchers = observer(({
<RadioGroup <RadioGroup
asButtons asButtons
className="RadioGroup flex gaps" className="RadioGroup flex gaps"
value={clusterOverviewStore.metricType} value={selectedMetricsType.value.get()}
onChange={value => clusterOverviewStore.metricType = value} onChange={selectedMetricsType.set}
> >
<Radio <Radio
label="CPU" label="CPU"
value={MetricType.CPU} value="cpu"
disabled={!hasCpuMetrics} disabled={!selectedMetricsType.hasCPUMetrics.get()}
/> />
<Radio <Radio
label="Memory" label="Memory"
value={MetricType.MEMORY} value="memory"
disabled={!hasMemoryMetrics} disabled={!selectedMetricsType.hasMemoryMetrics.get()}
/> />
</RadioGroup> </RadioGroup>
</div> </div>
</div> </div>
); ));
});
export const ClusterMetricSwitchers = withInjectables<Dependencies>(NonInjectedClusterMetricSwitchers, { export const ClusterMetricSwitchers = withInjectables<Dependencies>(NonInjectedClusterMetricSwitchers, {
getProps: (di) => ({ getProps: (di) => ({
clusterOverviewStore: di.inject(clusterOverviewStoreInjectable), selectedMetricsType: di.inject(selectedMetricsTypeInjectable),
nodeStore: di.inject(nodeStoreInjectable), selectedNodeRoleForMetrics: di.inject(selectedNodeRoleForMetricsInjectable),
}), }),
}); });

View File

@ -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<ClusterMetricData | undefined>({
getValueFromObservedPromise: async () => {
now(everyMinute);
const nodeNames = selectedNodeRoleForMetrics
.nodes
.get()
.map(node => node.getName());
return requestClusterMetricsByNodeNames(nodeNames);
},
betweenUpdates: "show-latest-value",
});
},
});
export default clusterOverviewMetricsInjectable;

View File

@ -8,8 +8,6 @@ import styles from "./cluster-metrics.module.scss";
import React, { useState } from "react"; import React, { useState } from "react";
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import type { ChartOptions, ChartPoint } from "chart.js"; 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 { BarChart } from "../chart";
import { bytesToUnits, cssNames } from "@k8slens/utilities"; import { bytesToUnits, cssNames } from "@k8slens/utilities";
import { Spinner } from "../spinner"; import { Spinner } from "../spinner";
@ -17,17 +15,34 @@ import { ZebraStripesPlugin } from "../chart/zebra-stripes.plugin";
import { ClusterNoMetrics } from "./cluster-no-metrics"; import { ClusterNoMetrics } from "./cluster-no-metrics";
import { ClusterMetricSwitchers } from "./cluster-metric-switchers"; import { ClusterMetricSwitchers } from "./cluster-metric-switchers";
import { getMetricLastPoints } from "../../../common/k8s-api/endpoints/metrics.api"; 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 { 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 { interface Dependencies {
clusterOverviewStore: ClusterOverviewStore; clusterOverviewMetrics: IAsyncComputed<ClusterMetricData | undefined>;
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 [plugins] = useState([new ZebraStripesPlugin()]);
const { memoryCapacity, cpuCapacity } = getMetricLastPoints(metrics ?? {}); 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 colors = { cpu: "#3D90CE", memory: "#C93DCE" };
const data = metricValues.map(value => ({ const data = metricValues.map(value => ({
x: value[0], 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 = () => { const renderMetrics = () => {
if (!metricValues.length && !metricsLoaded) { if (!metricValues.length && !metrics) {
return <Spinner center/>; return <Spinner center/>;
} }
@ -118,12 +133,10 @@ const NonInjectedClusterMetrics = observer(({ clusterOverviewStore: { metricType
); );
}); });
export const ClusterMetrics = withInjectables<Dependencies>( export const ClusterMetrics = withInjectables<Dependencies>(NonInjectedClusterMetrics, {
NonInjectedClusterMetrics,
{
getProps: (di) => ({ getProps: (di) => ({
clusterOverviewStore: di.inject(clusterOverviewStoreInjectable), clusterOverviewMetrics: di.inject(clusterOverviewMetricsInjectable),
selectedMetricsType: di.inject(selectedMetricsTypeInjectable),
selectedNodeRoleForMetrics: di.inject(selectedNodeRoleForMetricsInjectable),
}), }),
}, });
);

View File

@ -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<ClusterOverviewStorageState>(
"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;

View File

@ -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<ClusterOverviewStorageState>;
readonly nodeStore: NodeStore;
requestClusterMetricsByNodeNames: RequestClusterMetricsByNodeNames;
}
export class ClusterOverviewStore extends KubeObjectStore<Cluster, ClusterApi> 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<ClusterMetricData>): [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();
}
}

View File

@ -7,19 +7,16 @@ import styles from "./cluster-overview.module.scss";
import React from "react"; import React from "react";
import type { IComputedValue } from "mobx"; import type { IComputedValue } from "mobx";
import { reaction } from "mobx";
import { disposeOnUnmount, observer } from "mobx-react"; import { disposeOnUnmount, observer } from "mobx-react";
import type { NodeStore } from "../nodes/store"; import type { NodeStore } from "../nodes/store";
import type { PodStore } from "../workloads-pods/store"; import type { PodStore } from "../workloads-pods/store";
import { interval } from "@k8slens/utilities";
import { TabLayout } from "../layout/tab-layout"; import { TabLayout } from "../layout/tab-layout";
import { Spinner } from "../spinner"; import { Spinner } from "../spinner";
import { ClusterIssues } from "./cluster-issues"; import { ClusterIssues } from "./cluster-issues";
import type { ClusterOverviewStore } from "./cluster-overview-store/cluster-overview-store";
import { ClusterMetricsResourceType } from "../../../common/cluster-types"; import { ClusterMetricsResourceType } from "../../../common/cluster-types";
import type { EventStore } from "../events/store"; import type { EventStore } from "../events/store";
import type { IAsyncComputed } from "@ogre-tools/injectable-react";
import { withInjectables } 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 type { SubscribeStores } from "../../kube-watch-api/kube-watch-api";
import subscribeStoresInjectable from "../../kube-watch-api/subscribe-stores.injectable"; import subscribeStoresInjectable from "../../kube-watch-api/subscribe-stores.injectable";
import podStoreInjectable from "../workloads-pods/store.injectable"; import podStoreInjectable from "../workloads-pods/store.injectable";
@ -30,84 +27,61 @@ import type { ClusterOverviewUIBlock } from "@k8slens/metrics";
import { clusterOverviewUIBlockInjectionToken } from "@k8slens/metrics"; import { clusterOverviewUIBlockInjectionToken } from "@k8slens/metrics";
import { orderByOrderNumber } from "../../../common/utils/composable-responsibilities/orderable/orderable"; import { orderByOrderNumber } from "../../../common/utils/composable-responsibilities/orderable/orderable";
import { computedInjectManyInjectable } from "@ogre-tools/injectable-extension-for-mobx"; 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 { interface Dependencies {
subscribeStores: SubscribeStores; subscribeStores: SubscribeStores;
clusterOverviewStore: ClusterOverviewStore;
podStore: PodStore; podStore: PodStore;
eventStore: EventStore; eventStore: EventStore;
nodeStore: NodeStore; nodeStore: NodeStore;
clusterMetricsAreVisible: IComputedValue<boolean>; clusterMetricsAreVisible: IComputedValue<boolean>;
uiBlocks: IComputedValue<ClusterOverviewUIBlock[]>; uiBlocks: IComputedValue<ClusterOverviewUIBlock[]>;
clusterOverviewMetrics: IAsyncComputed<ClusterMetricData | undefined>;
} }
@observer @observer
class NonInjectedClusterOverview extends React.Component<Dependencies> { class NonInjectedClusterOverview extends React.Component<Dependencies> {
private readonly metricPoller = interval(60, async () => {
try {
await this.props.clusterOverviewStore.loadMetrics();
} catch {
// ignore
}
});
componentDidMount() { componentDidMount() {
this.metricPoller.start(true);
disposeOnUnmount(this, [ disposeOnUnmount(this, [
this.props.subscribeStores([ this.props.subscribeStores([
this.props.podStore, this.props.podStore,
this.props.eventStore, this.props.eventStore,
this.props.nodeStore, this.props.nodeStore,
]), ]),
reaction(
() => this.props.clusterOverviewStore.metricNodeRole, // Toggle Master/Worker node switcher
() => this.metricPoller.restart(true),
),
]); ]);
} }
componentWillUnmount() { renderWithMetrics() {
this.metricPoller.stop();
}
renderMetrics(isMetricsHidden: boolean) {
if (isMetricsHidden) {
return null;
}
return ( return (
<> <>
{orderByOrderNumber(this.props.uiBlocks.get()).map((block) => ( {
orderByOrderNumber(this.props.uiBlocks.get())
.map((block) => (
<block.Component key={block.id} /> <block.Component key={block.id} />
))} ))
</>
);
} }
<ClusterIssues />
renderClusterOverview(isLoaded: boolean, isMetricsHidden: boolean) {
if (!isLoaded) {
return <Spinner center/>;
}
return (
<>
{this.renderMetrics(isMetricsHidden)}
<ClusterIssues className={isMetricsHidden ? "OnlyClusterIssues" : ""}/>
</> </>
); );
} }
render() { render() {
const { eventStore, nodeStore, clusterMetricsAreVisible } = this.props; const { eventStore, nodeStore, clusterMetricsAreVisible } = this.props;
const isLoaded = nodeStore.isLoaded && eventStore.isLoaded;
const isMetricsHidden = !clusterMetricsAreVisible.get(); const isMetricsHidden = !clusterMetricsAreVisible.get();
return ( return (
<TabLayout scrollable> <TabLayout scrollable>
<div className={styles.ClusterOverview} data-testid="cluster-overview-page"> <div className={styles.ClusterOverview} data-testid="cluster-overview-page">
{this.renderClusterOverview(isLoaded, isMetricsHidden)} {
(!nodeStore.isLoaded || !eventStore.isLoaded)
? <Spinner center/>
: (
isMetricsHidden
? <ClusterIssues className="OnlyClusterIssues"/>
: this.renderWithMetrics()
)
}
</div> </div>
</TabLayout> </TabLayout>
); );
@ -117,11 +91,11 @@ class NonInjectedClusterOverview extends React.Component<Dependencies> {
export const ClusterOverview = withInjectables<Dependencies>(NonInjectedClusterOverview, { export const ClusterOverview = withInjectables<Dependencies>(NonInjectedClusterOverview, {
getProps: (di) => ({ getProps: (di) => ({
subscribeStores: di.inject(subscribeStoresInjectable), subscribeStores: di.inject(subscribeStoresInjectable),
clusterOverviewStore: di.inject(clusterOverviewStoreInjectable),
clusterMetricsAreVisible: di.inject(enabledMetricsInjectable, ClusterMetricsResourceType.Cluster), clusterMetricsAreVisible: di.inject(enabledMetricsInjectable, ClusterMetricsResourceType.Cluster),
podStore: di.inject(podStoreInjectable), podStore: di.inject(podStoreInjectable),
eventStore: di.inject(eventStoreInjectable), eventStore: di.inject(eventStoreInjectable),
nodeStore: di.inject(nodeStoreInjectable), nodeStore: di.inject(nodeStoreInjectable),
uiBlocks: di.inject(computedInjectManyInjectable)(clusterOverviewUIBlockInjectionToken), uiBlocks: di.inject(computedInjectManyInjectable)(clusterOverviewUIBlockInjectionToken),
clusterOverviewMetrics: di.inject(clusterOverviewMetricsInjectable),
}), }),
}); });

View File

@ -7,23 +7,24 @@ import styles from "./cluster-pie-charts.module.scss";
import React from "react"; import React from "react";
import { observer } from "mobx-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 { Spinner } from "../spinner";
import { Icon } from "../icon"; import { Icon } from "../icon";
import type { NodeStore } from "../nodes/store";
import type { PieChartData } from "../chart"; import type { PieChartData } from "../chart";
import { PieChart } from "../chart"; import { PieChart } from "../chart";
import { ClusterNoMetrics } from "./cluster-no-metrics"; import { ClusterNoMetrics } from "./cluster-no-metrics";
import { bytesToUnits, cssNames } from "@k8slens/utilities"; import { bytesToUnits, cssNames } from "@k8slens/utilities";
import type { LensTheme } from "../../themes/lens-theme"; import type { LensTheme } from "../../themes/lens-theme";
import { getMetricLastPoints } from "../../../common/k8s-api/endpoints/metrics.api"; 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 { 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 type { IComputedValue } from "mobx";
import activeThemeInjectable from "../../themes/active.injectable"; import activeThemeInjectable from "../../themes/active.injectable";
import type { ClusterMetricData } from "../../../common/k8s-api/endpoints/metrics.api/request-cluster-metrics-by-node-names.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[] { function createLabels(rawLabelData: [string, number | undefined][]): string[] {
return rawLabelData.map(([key, value]) => `${key}: ${value?.toFixed(2) || "N/A"}`); return rawLabelData.map(([key, value]) => `${key}: ${value?.toFixed(2) || "N/A"}`);
@ -36,26 +37,19 @@ const checkedBytesToUnits = (value: number | undefined) => (
); );
interface Dependencies { interface Dependencies {
clusterOverviewStore: ClusterOverviewStore; selectedNodeRoleForMetrics: SelectedNodeRoleForMetrics;
nodeStore: NodeStore; clusterOverviewMetrics: IAsyncComputed<ClusterMetricData | undefined>;
activeTheme: IComputedValue<LensTheme>; activeTheme: IComputedValue<LensTheme>;
} }
const NonInjectedClusterPieCharts = observer(({ const renderLimitWarning = () => (
clusterOverviewStore,
nodeStore,
activeTheme,
}: Dependencies) => {
const renderLimitWarning = () => {
return (
<div className="node-warning flex gaps align-center"> <div className="node-warning flex gaps align-center">
<Icon material="info" /> <Icon material="info" />
<p>Specified limits are higher than node capacity!</p> <p>Specified limits are higher than node capacity!</p>
</div> </div>
); );
};
const renderCharts = (lastPoints: Partial<Record<keyof ClusterMetricData, number>>) => { const renderCharts = (defaultColor: string, lastPoints: Partial<Record<keyof ClusterMetricData, number>>) => {
const { const {
memoryUsage, memoryRequests, memoryAllocatableCapacity, memoryCapacity, memoryLimits, memoryUsage, memoryRequests, memoryAllocatableCapacity, memoryCapacity, memoryLimits,
cpuUsage, cpuRequests, cpuAllocatableCapacity, cpuCapacity, cpuLimits, cpuUsage, cpuRequests, cpuAllocatableCapacity, cpuCapacity, cpuLimits,
@ -63,20 +57,18 @@ const NonInjectedClusterPieCharts = observer(({
} = lastPoints; } = lastPoints;
if ( if (
typeof cpuCapacity !== "number" || !isNumber(cpuCapacity) ||
typeof cpuAllocatableCapacity !== "number" || !isNumber(cpuAllocatableCapacity) ||
typeof podCapacity !== "number" || !isNumber(podCapacity) ||
typeof podAllocatableCapacity !== "number" || !isNumber(podAllocatableCapacity) ||
typeof memoryAllocatableCapacity !== "number" || !isNumber(memoryAllocatableCapacity) ||
typeof memoryCapacity !== "number" || !isNumber(memoryCapacity) ||
typeof memoryUsage !== "number" || !isNumber(memoryUsage) ||
typeof memoryRequests !== "number" !isNumber(memoryRequests)
) { ) {
return null; return null;
} }
const defaultColor = activeTheme.get().colors.pieChartDefaultColor;
const cpuData: PieChartData = { const cpuData: PieChartData = {
datasets: [ datasets: [
{ {
@ -237,10 +229,7 @@ const NonInjectedClusterPieCharts = observer(({
); );
}; };
const renderContent = ({ metricNodeRole, metrics }: ClusterOverviewStore) => { const renderContent = (defaultColor: string, nodes: Node[], metrics: ClusterMetricData | undefined) => {
const { masterNodes, workerNodes } = nodeStore;
const nodes = metricNodeRole === MetricNodeRole.MASTER ? masterNodes : workerNodes;
if (!nodes.length) { if (!nodes.length) {
return ( return (
<div className={cssNames(styles.empty, "flex column box grow align-center justify-center")}> <div className={cssNames(styles.empty, "flex column box grow align-center justify-center")}>
@ -269,20 +258,27 @@ const NonInjectedClusterPieCharts = observer(({
); );
} }
return renderCharts(lastPoints); return renderCharts(defaultColor, lastPoints);
}; };
return ( const NonInjectedClusterPieCharts = observer(({
selectedNodeRoleForMetrics,
clusterOverviewMetrics,
activeTheme,
}: Dependencies) => (
<div className="flex"> <div className="flex">
{renderContent(clusterOverviewStore)} {renderContent(
activeTheme.get().colors.pieChartDefaultColor,
selectedNodeRoleForMetrics.nodes.get(),
clusterOverviewMetrics.value.get(),
)}
</div> </div>
); ));
});
export const ClusterPieCharts = withInjectables<Dependencies>(NonInjectedClusterPieCharts, { export const ClusterPieCharts = withInjectables<Dependencies>(NonInjectedClusterPieCharts, {
getProps: (di) => ({ getProps: (di) => ({
clusterOverviewStore: di.inject(clusterOverviewStoreInjectable),
nodeStore: di.inject(nodeStoreInjectable),
activeTheme: di.inject(activeThemeInjectable), activeTheme: di.inject(activeThemeInjectable),
clusterOverviewMetrics: di.inject(clusterOverviewMetricsInjectable),
selectedNodeRoleForMetrics: di.inject(selectedNodeRoleForMetricsInjectable),
}), }),
}); });

View File

@ -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<typeof selectedMetricsTypeInjectable["instantiate"]>;
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;

View File

@ -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<typeof selectedNodeRoleForMetricsInjectable["instantiate"]>;
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;

View File

@ -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<ClusterOverviewStorageState>(
"cluster_overview",
{
metricType: "cpu", // setup defaults
metricNodeRole: "worker",
},
);
},
});
export default clusterOverviewStorageInjectable;

View File

@ -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;

View File

@ -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<Cluster, ClusterApi> {
}