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

ClusterOverview page refactorings (#1696)

* ClusterOverview page refactorings

Signed-off-by: Alex Andreev <alex.andreev.email@gmail.com>

* Minor test fix for MainLayoutHeader

Signed-off-by: Alex Andreev <alex.andreev.email@gmail.com>

* Replacing class name in tests

Signed-off-by: Alex Andreev <alex.andreev.email@gmail.com>

* Remove unnecessary parenthesis

Signed-off-by: Alex Andreev <alex.andreev.email@gmail.com>
This commit is contained in:
Alex Andreev 2020-12-11 08:36:47 +03:00 committed by GitHub
parent 961a38d52f
commit a61e20965d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 106 additions and 124 deletions

View File

@ -226,7 +226,7 @@ describe("Lens integration tests", () => {
pages: [{ pages: [{
name: "Cluster", name: "Cluster",
href: "cluster", href: "cluster",
expectedSelector: "div.Cluster div.label", expectedSelector: "div.ClusterOverview div.label",
expectedText: "Master" expectedText: "Master"
}] }]
}, },

View File

@ -1,17 +1,14 @@
.ClusterIssues { .ClusterIssues {
min-height: 350px; min-height: 350px;
position: relative; position: relative;
grid-column-start: 1;
grid-column-end: 3;
@include media("<1024px") { @include media("<1024px") {
grid-column-start: 1!important; grid-column-start: 1!important;
grid-column-end: 1!important; grid-column-end: 1!important;
} }
&.wide {
grid-column-start: 1;
grid-column-end: 3;
}
.SubHeader { .SubHeader {
.Icon { .Icon {
font-size: 130%; font-size: 130%;

View File

@ -6,10 +6,10 @@ import { observer } from "mobx-react";
import { nodesStore } from "../+nodes/nodes.store"; import { nodesStore } from "../+nodes/nodes.store";
import { cssNames } from "../../utils"; import { cssNames } from "../../utils";
import { Radio, RadioGroup } from "../radio"; import { Radio, RadioGroup } from "../radio";
import { clusterStore, MetricNodeRole, MetricType } from "./cluster.store"; import { clusterOverviewStore, MetricNodeRole, MetricType } from "./cluster-overview.store";
export const ClusterMetricSwitchers = observer(() => { export const ClusterMetricSwitchers = observer(() => {
const { metricType, metricNodeRole, getMetricsValues, metrics } = clusterStore; const { metricType, metricNodeRole, getMetricsValues, metrics } = clusterOverviewStore;
const { masterNodes, workerNodes } = nodesStore; const { masterNodes, workerNodes } = nodesStore;
const metricsValues = getMetricsValues(metrics); const metricsValues = getMetricsValues(metrics);
const disableRoles = !masterNodes.length || !workerNodes.length; const disableRoles = !masterNodes.length || !workerNodes.length;
@ -22,7 +22,7 @@ export const ClusterMetricSwitchers = observer(() => {
asButtons asButtons
className={cssNames("RadioGroup flex gaps", { disabled: disableRoles })} className={cssNames("RadioGroup flex gaps", { disabled: disableRoles })}
value={metricNodeRole} value={metricNodeRole}
onChange={(metric: MetricNodeRole) => clusterStore.metricNodeRole = metric} onChange={(metric: MetricNodeRole) => clusterOverviewStore.metricNodeRole = metric}
> >
<Radio label={<Trans>Master</Trans>} value={MetricNodeRole.MASTER}/> <Radio label={<Trans>Master</Trans>} value={MetricNodeRole.MASTER}/>
<Radio label={<Trans>Worker</Trans>} value={MetricNodeRole.WORKER}/> <Radio label={<Trans>Worker</Trans>} value={MetricNodeRole.WORKER}/>
@ -33,7 +33,7 @@ export const ClusterMetricSwitchers = observer(() => {
asButtons asButtons
className={cssNames("RadioGroup flex gaps", { disabled: disableMetrics })} className={cssNames("RadioGroup flex gaps", { disabled: disableMetrics })}
value={metricType} value={metricType}
onChange={(value: MetricType) => clusterStore.metricType = value} onChange={(value: MetricType) => clusterOverviewStore.metricType = value}
> >
<Radio label={<Trans>CPU</Trans>} value={MetricType.CPU}/> <Radio label={<Trans>CPU</Trans>} value={MetricType.CPU}/>
<Radio label={<Trans>Memory</Trans>} value={MetricType.MEMORY}/> <Radio label={<Trans>Memory</Trans>} value={MetricType.MEMORY}/>

View File

@ -3,7 +3,7 @@ import "./cluster-metrics.scss";
import React from "react"; import React from "react";
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import { ChartOptions, ChartPoint } from "chart.js"; import { ChartOptions, ChartPoint } from "chart.js";
import { clusterStore, MetricType } from "./cluster.store"; import { clusterOverviewStore, MetricType } from "./cluster-overview.store";
import { BarChart } from "../chart"; import { BarChart } from "../chart";
import { bytesToUnits } from "../../utils"; import { bytesToUnits } from "../../utils";
import { Spinner } from "../spinner"; import { Spinner } from "../spinner";
@ -13,10 +13,9 @@ import { ClusterMetricSwitchers } from "./cluster-metric-switchers";
import { getMetricLastPoints } from "../../api/endpoints/metrics.api"; import { getMetricLastPoints } from "../../api/endpoints/metrics.api";
export const ClusterMetrics = observer(() => { export const ClusterMetrics = observer(() => {
const { metricType, metricNodeRole, getMetricsValues, metricsLoaded, metrics, liveMetrics } = clusterStore; const { metricType, metricNodeRole, getMetricsValues, metricsLoaded, metrics } = clusterOverviewStore;
const { memoryCapacity, cpuCapacity } = getMetricLastPoints(clusterStore.metrics); const { memoryCapacity, cpuCapacity } = getMetricLastPoints(clusterOverviewStore.metrics);
const metricValues = getMetricsValues(metrics); const metricValues = getMetricsValues(metrics);
const liveMetricValues = getMetricsValues(liveMetrics);
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],
@ -70,7 +69,7 @@ export const ClusterMetrics = observer(() => {
const options = metricType === MetricType.CPU ? cpuOptions : memoryOptions; const options = metricType === MetricType.CPU ? cpuOptions : memoryOptions;
const renderMetrics = () => { const renderMetrics = () => {
if ((!metricValues.length || !liveMetricValues.length) && !metricsLoaded) { if (!metricValues.length && !metricsLoaded) {
return <Spinner center/>; return <Spinner center/>;
} }

View File

@ -1,4 +1,4 @@
.Cluster { .ClusterOverview {
$gridGap: $margin * 2; $gridGap: $margin * 2;
position: relative; position: relative;

View File

@ -1,4 +1,4 @@
import { observable, reaction, when } from "mobx"; import { action, observable, reaction, when } from "mobx";
import { KubeObjectStore } from "../../kube-object.store"; import { KubeObjectStore } from "../../kube-object.store";
import { Cluster, clusterApi, IClusterMetrics } from "../../api/endpoints"; import { Cluster, clusterApi, IClusterMetrics } from "../../api/endpoints";
import { autobind, createStorage } from "../../utils"; import { autobind, createStorage } from "../../utils";
@ -17,11 +17,10 @@ export enum MetricNodeRole {
} }
@autobind() @autobind()
export class ClusterStore extends KubeObjectStore<Cluster> { export class ClusterOverviewStore extends KubeObjectStore<Cluster> {
api = clusterApi; api = clusterApi;
@observable metrics: Partial<IClusterMetrics> = {}; @observable metrics: Partial<IClusterMetrics> = {};
@observable liveMetrics: Partial<IClusterMetrics> = {};
@observable metricsLoaded = false; @observable metricsLoaded = false;
@observable metricType: MetricType; @observable metricType: MetricType;
@observable metricNodeRole: MetricNodeRole; @observable metricNodeRole: MetricNodeRole;
@ -46,9 +45,8 @@ export class ClusterStore extends KubeObjectStore<Cluster> {
reaction(() => this.metricNodeRole, () => { reaction(() => this.metricNodeRole, () => {
if (!this.metricsLoaded) return; if (!this.metricsLoaded) return;
this.metrics = {}; this.metrics = {};
this.liveMetrics = {};
this.metricsLoaded = false; this.metricsLoaded = false;
this.getAllMetrics(); this.loadMetrics();
}); });
// check which node type to select // check which node type to select
@ -60,33 +58,16 @@ export class ClusterStore extends KubeObjectStore<Cluster> {
}); });
} }
@action
async loadMetrics(params?: IMetricsReqParams) { async loadMetrics(params?: IMetricsReqParams) {
await when(() => nodesStore.isLoaded); await when(() => nodesStore.isLoaded);
const { masterNodes, workerNodes } = nodesStore; const { masterNodes, workerNodes } = nodesStore;
const nodes = this.metricNodeRole === MetricNodeRole.MASTER && masterNodes.length ? masterNodes : workerNodes; const nodes = this.metricNodeRole === MetricNodeRole.MASTER && masterNodes.length ? masterNodes : workerNodes;
return clusterApi.getMetrics(nodes.map(node => node.getName()), params); this.metrics = await clusterApi.getMetrics(nodes.map(node => node.getName()), params);
}
async getAllMetrics() {
await this.getMetrics();
await this.getLiveMetrics();
this.metricsLoaded = true; 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<IClusterMetrics>): [number, string][] { getMetricsValues(source: Partial<IClusterMetrics>): [number, string][] {
switch (this.metricType) { switch (this.metricType) {
case MetricType.CPU: case MetricType.CPU:
@ -111,5 +92,5 @@ export class ClusterStore extends KubeObjectStore<Cluster> {
} }
} }
export const clusterStore = new ClusterStore(); export const clusterOverviewStore = new ClusterOverviewStore();
apiManager.registerStore(clusterStore); apiManager.registerStore(clusterOverviewStore);

View File

@ -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<any>[] = [];
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 (
<TabLayout>
<div className="ClusterOverview">
{!isLoaded ? <Spinner center/> : (
<>
<ClusterMetrics/>
<ClusterPieCharts/>
<ClusterIssues/>
</>
)}
</div>
</TabLayout>
);
}
}

View File

@ -4,7 +4,7 @@ import React from "react";
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import { t, Trans } from "@lingui/macro"; import { t, Trans } from "@lingui/macro";
import { useLingui } from "@lingui/react"; import { useLingui } from "@lingui/react";
import { clusterStore, MetricNodeRole } from "./cluster.store"; import { clusterOverviewStore, MetricNodeRole } from "./cluster-overview.store";
import { Spinner } from "../spinner"; import { Spinner } from "../spinner";
import { Icon } from "../icon"; import { Icon } from "../icon";
import { nodesStore } from "../+nodes/nodes.store"; import { nodesStore } from "../+nodes/nodes.store";
@ -27,7 +27,7 @@ export const ClusterPieCharts = observer(() => {
}; };
const renderCharts = () => { const renderCharts = () => {
const data = getMetricLastPoints(clusterStore.metrics); const data = getMetricLastPoints(clusterOverviewStore.metrics);
const { memoryUsage, memoryRequests, memoryCapacity, memoryLimits } = data; const { memoryUsage, memoryRequests, memoryCapacity, memoryLimits } = data;
const { cpuUsage, cpuRequests, cpuCapacity, cpuLimits } = data; const { cpuUsage, cpuRequests, cpuCapacity, cpuLimits } = data;
const { podUsage, podCapacity } = data; const { podUsage, podCapacity } = data;
@ -173,7 +173,7 @@ export const ClusterPieCharts = observer(() => {
const renderContent = () => { const renderContent = () => {
const { masterNodes, workerNodes } = nodesStore; const { masterNodes, workerNodes } = nodesStore;
const { metricNodeRole, metricsLoaded } = clusterStore; const { metricNodeRole, metricsLoaded } = clusterOverviewStore;
const nodes = metricNodeRole === MetricNodeRole.MASTER ? masterNodes : workerNodes; const nodes = metricNodeRole === MetricNodeRole.MASTER ? masterNodes : workerNodes;
if (!nodes.length) { if (!nodes.length) {
@ -192,7 +192,7 @@ export const ClusterPieCharts = observer(() => {
</div> </div>
); );
} }
const { memoryCapacity, cpuCapacity, podCapacity } = getMetricLastPoints(clusterStore.metrics); const { memoryCapacity, cpuCapacity, podCapacity } = getMetricLastPoints(clusterOverviewStore.metrics);
if (!memoryCapacity || !cpuCapacity || !podCapacity) { if (!memoryCapacity || !cpuCapacity || !podCapacity) {
return <ClusterNoMetrics className="empty"/>; return <ClusterNoMetrics className="empty"/>;

View File

@ -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 (
<TabLayout>
<div className="Cluster">
{!isLoaded && <Spinner center/>}
{isLoaded && (
<>
<ClusterMetrics/>
<ClusterPieCharts/>
<ClusterIssues className={cssNames({ wide: isElectron })}/>
</>
)}
</div>
</TabLayout>
);
}
}

View File

@ -16,7 +16,7 @@ import { Workloads, workloadsRoute, workloadsURL } from "./+workloads";
import { Namespaces, namespacesRoute } from "./+namespaces"; import { Namespaces, namespacesRoute } from "./+namespaces";
import { Network, networkRoute } from "./+network"; import { Network, networkRoute } from "./+network";
import { Storage, storageRoute } from "./+storage"; import { Storage, storageRoute } from "./+storage";
import { Cluster } from "./+cluster/cluster"; import { ClusterOverview } from "./+cluster/cluster-overview";
import { Config, configRoute } from "./+config"; import { Config, configRoute } from "./+config";
import { Events } from "./+events/events"; import { Events } from "./+events/events";
import { eventRoute } from "./+events"; import { eventRoute } from "./+events";
@ -181,7 +181,7 @@ export class App extends React.Component {
<ErrorBoundary> <ErrorBoundary>
<MainLayout> <MainLayout>
<Switch> <Switch>
<Route component={Cluster} {...clusterRoute}/> <Route component={ClusterOverview} {...clusterRoute}/>
<Route component={Nodes} {...nodesRoute}/> <Route component={Nodes} {...nodesRoute}/>
<Route component={Workloads} {...workloadsRoute}/> <Route component={Workloads} {...workloadsRoute}/>
<Route component={Config} {...configRoute}/> <Route component={Config} {...configRoute}/>

View File

@ -41,9 +41,9 @@ describe("<MainLayoutHeader />", () => {
expect(mockBroadcastIpc).toBeCalledWith("renderer:navigate", "/cluster/foo/settings"); expect(mockBroadcastIpc).toBeCalledWith("renderer:navigate", "/cluster/foo/settings");
}); });
it("renders cluster name", async () => { it("renders cluster name", () => {
const { getByText } = render(<MainLayoutHeader cluster={cluster} />); const { getByText } = render(<MainLayoutHeader cluster={cluster} />);
expect(await getByText("minikube")).toBeTruthy(); expect(getByText("minikube")).toBeInTheDocument();
}); });
}); });