From a61e20965d2458974c261c75a177bb5924d6efe5 Mon Sep 17 00:00:00 2001 From: Alex Andreev Date: Fri, 11 Dec 2020 08:36:47 +0300 Subject: [PATCH] ClusterOverview page refactorings (#1696) * ClusterOverview page refactorings Signed-off-by: Alex Andreev * Minor test fix for MainLayoutHeader Signed-off-by: Alex Andreev * Replacing class name in tests Signed-off-by: Alex Andreev * Remove unnecessary parenthesis Signed-off-by: Alex Andreev --- integration/__tests__/app.tests.ts | 2 +- .../components/+cluster/cluster-issues.scss | 7 +- .../+cluster/cluster-metric-switchers.tsx | 8 +- .../components/+cluster/cluster-metrics.tsx | 9 +-- .../{cluster.scss => cluster-overview.scss} | 2 +- ...ter.store.ts => cluster-overview.store.ts} | 33 ++------ .../components/+cluster/cluster-overview.tsx | 79 +++++++++++++++++++ .../+cluster/cluster-pie-charts.tsx | 8 +- src/renderer/components/+cluster/cluster.tsx | 74 ----------------- src/renderer/components/app.tsx | 4 +- .../__test__/main-layout-header.test.tsx | 4 +- 11 files changed, 106 insertions(+), 124 deletions(-) rename src/renderer/components/+cluster/{cluster.scss => cluster-overview.scss} (95%) rename src/renderer/components/+cluster/{cluster.store.ts => cluster-overview.store.ts} (76%) create mode 100644 src/renderer/components/+cluster/cluster-overview.tsx delete mode 100644 src/renderer/components/+cluster/cluster.tsx diff --git a/integration/__tests__/app.tests.ts b/integration/__tests__/app.tests.ts index 285af70ceb..410712e4f0 100644 --- a/integration/__tests__/app.tests.ts +++ b/integration/__tests__/app.tests.ts @@ -226,7 +226,7 @@ describe("Lens integration tests", () => { pages: [{ name: "Cluster", href: "cluster", - expectedSelector: "div.Cluster div.label", + expectedSelector: "div.ClusterOverview div.label", expectedText: "Master" }] }, diff --git a/src/renderer/components/+cluster/cluster-issues.scss b/src/renderer/components/+cluster/cluster-issues.scss index b552cf6877..8886fa40e3 100644 --- a/src/renderer/components/+cluster/cluster-issues.scss +++ b/src/renderer/components/+cluster/cluster-issues.scss @@ -1,17 +1,14 @@ .ClusterIssues { min-height: 350px; position: relative; + grid-column-start: 1; + grid-column-end: 3; @include media("<1024px") { grid-column-start: 1!important; grid-column-end: 1!important; } - &.wide { - grid-column-start: 1; - grid-column-end: 3; - } - .SubHeader { .Icon { font-size: 130%; diff --git a/src/renderer/components/+cluster/cluster-metric-switchers.tsx b/src/renderer/components/+cluster/cluster-metric-switchers.tsx index f2e090cdbb..02ffbd8755 100644 --- a/src/renderer/components/+cluster/cluster-metric-switchers.tsx +++ b/src/renderer/components/+cluster/cluster-metric-switchers.tsx @@ -6,10 +6,10 @@ import { observer } from "mobx-react"; import { nodesStore } from "../+nodes/nodes.store"; import { cssNames } from "../../utils"; import { Radio, RadioGroup } from "../radio"; -import { clusterStore, MetricNodeRole, MetricType } from "./cluster.store"; +import { clusterOverviewStore, MetricNodeRole, MetricType } from "./cluster-overview.store"; export const ClusterMetricSwitchers = observer(() => { - const { metricType, metricNodeRole, getMetricsValues, metrics } = clusterStore; + const { metricType, metricNodeRole, getMetricsValues, metrics } = clusterOverviewStore; const { masterNodes, workerNodes } = nodesStore; const metricsValues = getMetricsValues(metrics); const disableRoles = !masterNodes.length || !workerNodes.length; @@ -22,7 +22,7 @@ export const ClusterMetricSwitchers = observer(() => { asButtons className={cssNames("RadioGroup flex gaps", { disabled: disableRoles })} value={metricNodeRole} - onChange={(metric: MetricNodeRole) => clusterStore.metricNodeRole = metric} + onChange={(metric: MetricNodeRole) => clusterOverviewStore.metricNodeRole = metric} > Master} value={MetricNodeRole.MASTER}/> Worker} value={MetricNodeRole.WORKER}/> @@ -33,7 +33,7 @@ export const ClusterMetricSwitchers = observer(() => { asButtons className={cssNames("RadioGroup flex gaps", { disabled: disableMetrics })} value={metricType} - onChange={(value: MetricType) => clusterStore.metricType = value} + onChange={(value: MetricType) => clusterOverviewStore.metricType = value} > CPU} value={MetricType.CPU}/> Memory} value={MetricType.MEMORY}/> diff --git a/src/renderer/components/+cluster/cluster-metrics.tsx b/src/renderer/components/+cluster/cluster-metrics.tsx index b049cfc2f4..6461bae7f3 100644 --- a/src/renderer/components/+cluster/cluster-metrics.tsx +++ b/src/renderer/components/+cluster/cluster-metrics.tsx @@ -3,7 +3,7 @@ import "./cluster-metrics.scss"; import React from "react"; import { observer } from "mobx-react"; import { ChartOptions, ChartPoint } from "chart.js"; -import { clusterStore, MetricType } from "./cluster.store"; +import { clusterOverviewStore, MetricType } from "./cluster-overview.store"; import { BarChart } from "../chart"; import { bytesToUnits } from "../../utils"; import { Spinner } from "../spinner"; @@ -13,10 +13,9 @@ import { ClusterMetricSwitchers } from "./cluster-metric-switchers"; import { getMetricLastPoints } from "../../api/endpoints/metrics.api"; export const ClusterMetrics = observer(() => { - const { metricType, metricNodeRole, getMetricsValues, metricsLoaded, metrics, liveMetrics } = clusterStore; - const { memoryCapacity, cpuCapacity } = getMetricLastPoints(clusterStore.metrics); + const { metricType, metricNodeRole, getMetricsValues, metricsLoaded, metrics } = clusterOverviewStore; + const { memoryCapacity, cpuCapacity } = getMetricLastPoints(clusterOverviewStore.metrics); const metricValues = getMetricsValues(metrics); - const liveMetricValues = getMetricsValues(liveMetrics); const colors = { cpu: "#3D90CE", memory: "#C93DCE" }; const data = metricValues.map(value => ({ x: value[0], @@ -70,7 +69,7 @@ export const ClusterMetrics = observer(() => { const options = metricType === MetricType.CPU ? cpuOptions : memoryOptions; const renderMetrics = () => { - if ((!metricValues.length || !liveMetricValues.length) && !metricsLoaded) { + if (!metricValues.length && !metricsLoaded) { return ; } diff --git a/src/renderer/components/+cluster/cluster.scss b/src/renderer/components/+cluster/cluster-overview.scss similarity index 95% rename from src/renderer/components/+cluster/cluster.scss rename to src/renderer/components/+cluster/cluster-overview.scss index 32739c378c..c0534f4fff 100644 --- a/src/renderer/components/+cluster/cluster.scss +++ b/src/renderer/components/+cluster/cluster-overview.scss @@ -1,4 +1,4 @@ -.Cluster { +.ClusterOverview { $gridGap: $margin * 2; position: relative; diff --git a/src/renderer/components/+cluster/cluster.store.ts b/src/renderer/components/+cluster/cluster-overview.store.ts similarity index 76% rename from src/renderer/components/+cluster/cluster.store.ts rename to src/renderer/components/+cluster/cluster-overview.store.ts index 04bc1d8658..64faa2394c 100644 --- a/src/renderer/components/+cluster/cluster.store.ts +++ b/src/renderer/components/+cluster/cluster-overview.store.ts @@ -1,4 +1,4 @@ -import { observable, reaction, when } from "mobx"; +import { action, observable, reaction, when } from "mobx"; import { KubeObjectStore } from "../../kube-object.store"; import { Cluster, clusterApi, IClusterMetrics } from "../../api/endpoints"; import { autobind, createStorage } from "../../utils"; @@ -17,11 +17,10 @@ export enum MetricNodeRole { } @autobind() -export class ClusterStore extends KubeObjectStore { +export class ClusterOverviewStore extends KubeObjectStore { api = clusterApi; @observable metrics: Partial = {}; - @observable liveMetrics: Partial = {}; @observable metricsLoaded = false; @observable metricType: MetricType; @observable metricNodeRole: MetricNodeRole; @@ -46,9 +45,8 @@ export class ClusterStore extends KubeObjectStore { reaction(() => this.metricNodeRole, () => { if (!this.metricsLoaded) return; this.metrics = {}; - this.liveMetrics = {}; this.metricsLoaded = false; - this.getAllMetrics(); + this.loadMetrics(); }); // check which node type to select @@ -60,33 +58,16 @@ export class ClusterStore extends KubeObjectStore { }); } + @action async loadMetrics(params?: IMetricsReqParams) { await when(() => nodesStore.isLoaded); const { masterNodes, workerNodes } = nodesStore; const nodes = this.metricNodeRole === MetricNodeRole.MASTER && masterNodes.length ? masterNodes : workerNodes; - return clusterApi.getMetrics(nodes.map(node => node.getName()), params); - } - - async getAllMetrics() { - await this.getMetrics(); - await this.getLiveMetrics(); + this.metrics = await clusterApi.getMetrics(nodes.map(node => node.getName()), params); this.metricsLoaded = true; } - async getMetrics() { - this.metrics = await this.loadMetrics(); - } - - async getLiveMetrics() { - const step = 3; - const range = 15; - const end = Date.now() / 1000; - const start = end - range; - - this.liveMetrics = await this.loadMetrics({ start, end, step, range }); - } - getMetricsValues(source: Partial): [number, string][] { switch (this.metricType) { case MetricType.CPU: @@ -111,5 +92,5 @@ export class ClusterStore extends KubeObjectStore { } } -export const clusterStore = new ClusterStore(); -apiManager.registerStore(clusterStore); +export const clusterOverviewStore = new ClusterOverviewStore(); +apiManager.registerStore(clusterOverviewStore); diff --git a/src/renderer/components/+cluster/cluster-overview.tsx b/src/renderer/components/+cluster/cluster-overview.tsx new file mode 100644 index 0000000000..104c6fd022 --- /dev/null +++ b/src/renderer/components/+cluster/cluster-overview.tsx @@ -0,0 +1,79 @@ +import "./cluster-overview.scss"; + +import React from "react"; +import { reaction } from "mobx"; +import { disposeOnUnmount, observer } from "mobx-react"; + +import { eventStore } from "../+events/event.store"; +import { nodesStore } from "../+nodes/nodes.store"; +import { podsStore } from "../+workloads-pods/pods.store"; +import { getHostedCluster } from "../../../common/cluster-store"; +import { isAllowedResource } from "../../../common/rbac"; +import { KubeObjectStore } from "../../kube-object.store"; +import { interval } from "../../utils"; +import { TabLayout } from "../layout/tab-layout"; +import { Spinner } from "../spinner"; +import { ClusterIssues } from "./cluster-issues"; +import { ClusterMetrics } from "./cluster-metrics"; +import { clusterOverviewStore } from "./cluster-overview.store"; +import { ClusterPieCharts } from "./cluster-pie-charts"; + +@observer +export class ClusterOverview extends React.Component { + private stores: KubeObjectStore[] = []; + private subscribers: Array<() => void> = []; + private metricPoller = interval(60, this.loadMetrics); + + @disposeOnUnmount + fetchMetrics = reaction( + () => clusterOverviewStore.metricNodeRole, // Toggle Master/Worker node switcher + () => this.metricPoller.restart(true) + ); + + loadMetrics() { + getHostedCluster().available && clusterOverviewStore.loadMetrics(); + } + + async componentDidMount() { + if (isAllowedResource("nodes")) { + this.stores.push(nodesStore); + } + + if (isAllowedResource("pods")) { + this.stores.push(podsStore); + } + + if (isAllowedResource("events")) { + this.stores.push(eventStore); + } + + await Promise.all(this.stores.map(store => store.loadAll())); + this.loadMetrics(); + + this.subscribers = this.stores.map(store => store.subscribe()); + this.metricPoller.start(); + } + + componentWillUnmount() { + this.subscribers.forEach(dispose => dispose()); // unsubscribe all + this.metricPoller.stop(); + } + + render() { + const isLoaded = nodesStore.isLoaded && podsStore.isLoaded; + + return ( + +
+ {!isLoaded ? : ( + <> + + + + + )} +
+
+ ); + } +} diff --git a/src/renderer/components/+cluster/cluster-pie-charts.tsx b/src/renderer/components/+cluster/cluster-pie-charts.tsx index 246af6a3ca..684233f8ca 100644 --- a/src/renderer/components/+cluster/cluster-pie-charts.tsx +++ b/src/renderer/components/+cluster/cluster-pie-charts.tsx @@ -4,7 +4,7 @@ import React from "react"; import { observer } from "mobx-react"; import { t, Trans } from "@lingui/macro"; import { useLingui } from "@lingui/react"; -import { clusterStore, MetricNodeRole } from "./cluster.store"; +import { clusterOverviewStore, MetricNodeRole } from "./cluster-overview.store"; import { Spinner } from "../spinner"; import { Icon } from "../icon"; import { nodesStore } from "../+nodes/nodes.store"; @@ -27,7 +27,7 @@ export const ClusterPieCharts = observer(() => { }; const renderCharts = () => { - const data = getMetricLastPoints(clusterStore.metrics); + const data = getMetricLastPoints(clusterOverviewStore.metrics); const { memoryUsage, memoryRequests, memoryCapacity, memoryLimits } = data; const { cpuUsage, cpuRequests, cpuCapacity, cpuLimits } = data; const { podUsage, podCapacity } = data; @@ -173,7 +173,7 @@ export const ClusterPieCharts = observer(() => { const renderContent = () => { const { masterNodes, workerNodes } = nodesStore; - const { metricNodeRole, metricsLoaded } = clusterStore; + const { metricNodeRole, metricsLoaded } = clusterOverviewStore; const nodes = metricNodeRole === MetricNodeRole.MASTER ? masterNodes : workerNodes; if (!nodes.length) { @@ -192,7 +192,7 @@ export const ClusterPieCharts = observer(() => { ); } - const { memoryCapacity, cpuCapacity, podCapacity } = getMetricLastPoints(clusterStore.metrics); + const { memoryCapacity, cpuCapacity, podCapacity } = getMetricLastPoints(clusterOverviewStore.metrics); if (!memoryCapacity || !cpuCapacity || !podCapacity) { return ; diff --git a/src/renderer/components/+cluster/cluster.tsx b/src/renderer/components/+cluster/cluster.tsx deleted file mode 100644 index f99f65c479..0000000000 --- a/src/renderer/components/+cluster/cluster.tsx +++ /dev/null @@ -1,74 +0,0 @@ -import "./cluster.scss"; - -import React from "react"; -import { computed, reaction } from "mobx"; -import { disposeOnUnmount, observer } from "mobx-react"; -import { TabLayout } from "../layout/tab-layout"; -import { ClusterIssues } from "./cluster-issues"; -import { Spinner } from "../spinner"; -import { cssNames, interval, isElectron } from "../../utils"; -import { ClusterPieCharts } from "./cluster-pie-charts"; -import { ClusterMetrics } from "./cluster-metrics"; -import { nodesStore } from "../+nodes/nodes.store"; -import { podsStore } from "../+workloads-pods/pods.store"; -import { clusterStore } from "./cluster.store"; -import { eventStore } from "../+events/event.store"; -import { isAllowedResource } from "../../../common/rbac"; -import { getHostedCluster } from "../../../common/cluster-store"; - -@observer -export class Cluster extends React.Component { - private dependentStores = [nodesStore, podsStore]; - - private watchers = [ - interval(60, () => { getHostedCluster().available && clusterStore.getMetrics();}), - interval(20, () => { getHostedCluster().available && eventStore.loadAll();}) - ]; - - @computed get isLoaded() { - return nodesStore.isLoaded && podsStore.isLoaded; - } - - // todo: refactor - async componentDidMount() { - const { dependentStores } = this; - - if (!isAllowedResource("nodes")) { - dependentStores.splice(dependentStores.indexOf(nodesStore), 1); - } - this.watchers.forEach(watcher => watcher.start(true)); - - await Promise.all([ - ...dependentStores.map(store => store.loadAll()), - clusterStore.getAllMetrics() - ]); - - disposeOnUnmount(this, [ - ...dependentStores.map(store => store.subscribe()), - () => this.watchers.forEach(watcher => watcher.stop()), - reaction( - () => clusterStore.metricNodeRole, - () => this.watchers.forEach(watcher => watcher.restart()) - ) - ]); - } - - render() { - const { isLoaded } = this; - - return ( - -
- {!isLoaded && } - {isLoaded && ( - <> - - - - - )} -
-
- ); - } -} diff --git a/src/renderer/components/app.tsx b/src/renderer/components/app.tsx index 1cc5f910b6..e6af7e11d2 100755 --- a/src/renderer/components/app.tsx +++ b/src/renderer/components/app.tsx @@ -16,7 +16,7 @@ import { Workloads, workloadsRoute, workloadsURL } from "./+workloads"; import { Namespaces, namespacesRoute } from "./+namespaces"; import { Network, networkRoute } from "./+network"; import { Storage, storageRoute } from "./+storage"; -import { Cluster } from "./+cluster/cluster"; +import { ClusterOverview } from "./+cluster/cluster-overview"; import { Config, configRoute } from "./+config"; import { Events } from "./+events/events"; import { eventRoute } from "./+events"; @@ -181,7 +181,7 @@ export class App extends React.Component { - + diff --git a/src/renderer/components/layout/__test__/main-layout-header.test.tsx b/src/renderer/components/layout/__test__/main-layout-header.test.tsx index 91fcc5dc7f..b2a7bb5d93 100644 --- a/src/renderer/components/layout/__test__/main-layout-header.test.tsx +++ b/src/renderer/components/layout/__test__/main-layout-header.test.tsx @@ -41,9 +41,9 @@ describe("", () => { expect(mockBroadcastIpc).toBeCalledWith("renderer:navigate", "/cluster/foo/settings"); }); - it("renders cluster name", async () => { + it("renders cluster name", () => { const { getByText } = render(); - expect(await getByText("minikube")).toBeTruthy(); + expect(getByText("minikube")).toBeInTheDocument(); }); }); \ No newline at end of file