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:
parent
961a38d52f
commit
a61e20965d
@ -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"
|
||||
}]
|
||||
},
|
||||
|
||||
@ -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%;
|
||||
|
||||
@ -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}
|
||||
>
|
||||
<Radio label={<Trans>Master</Trans>} value={MetricNodeRole.MASTER}/>
|
||||
<Radio label={<Trans>Worker</Trans>} 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}
|
||||
>
|
||||
<Radio label={<Trans>CPU</Trans>} value={MetricType.CPU}/>
|
||||
<Radio label={<Trans>Memory</Trans>} value={MetricType.MEMORY}/>
|
||||
|
||||
@ -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 <Spinner center/>;
|
||||
}
|
||||
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
.Cluster {
|
||||
.ClusterOverview {
|
||||
$gridGap: $margin * 2;
|
||||
|
||||
position: relative;
|
||||
@ -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<Cluster> {
|
||||
export class ClusterOverviewStore extends KubeObjectStore<Cluster> {
|
||||
api = clusterApi;
|
||||
|
||||
@observable metrics: Partial<IClusterMetrics> = {};
|
||||
@observable liveMetrics: Partial<IClusterMetrics> = {};
|
||||
@observable metricsLoaded = false;
|
||||
@observable metricType: MetricType;
|
||||
@observable metricNodeRole: MetricNodeRole;
|
||||
@ -46,9 +45,8 @@ export class ClusterStore extends KubeObjectStore<Cluster> {
|
||||
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<Cluster> {
|
||||
});
|
||||
}
|
||||
|
||||
@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<IClusterMetrics>): [number, string][] {
|
||||
switch (this.metricType) {
|
||||
case MetricType.CPU:
|
||||
@ -111,5 +92,5 @@ export class ClusterStore extends KubeObjectStore<Cluster> {
|
||||
}
|
||||
}
|
||||
|
||||
export const clusterStore = new ClusterStore();
|
||||
apiManager.registerStore(clusterStore);
|
||||
export const clusterOverviewStore = new ClusterOverviewStore();
|
||||
apiManager.registerStore(clusterOverviewStore);
|
||||
79
src/renderer/components/+cluster/cluster-overview.tsx
Normal file
79
src/renderer/components/+cluster/cluster-overview.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -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(() => {
|
||||
</div>
|
||||
);
|
||||
}
|
||||
const { memoryCapacity, cpuCapacity, podCapacity } = getMetricLastPoints(clusterStore.metrics);
|
||||
const { memoryCapacity, cpuCapacity, podCapacity } = getMetricLastPoints(clusterOverviewStore.metrics);
|
||||
|
||||
if (!memoryCapacity || !cpuCapacity || !podCapacity) {
|
||||
return <ClusterNoMetrics className="empty"/>;
|
||||
|
||||
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -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 {
|
||||
<ErrorBoundary>
|
||||
<MainLayout>
|
||||
<Switch>
|
||||
<Route component={Cluster} {...clusterRoute}/>
|
||||
<Route component={ClusterOverview} {...clusterRoute}/>
|
||||
<Route component={Nodes} {...nodesRoute}/>
|
||||
<Route component={Workloads} {...workloadsRoute}/>
|
||||
<Route component={Config} {...configRoute}/>
|
||||
|
||||
@ -41,9 +41,9 @@ describe("<MainLayoutHeader />", () => {
|
||||
expect(mockBroadcastIpc).toBeCalledWith("renderer:navigate", "/cluster/foo/settings");
|
||||
});
|
||||
|
||||
it("renders cluster name", async () => {
|
||||
it("renders cluster name", () => {
|
||||
const { getByText } = render(<MainLayoutHeader cluster={cluster} />);
|
||||
|
||||
expect(await getByText("minikube")).toBeTruthy();
|
||||
expect(getByText("minikube")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
Loading…
Reference in New Issue
Block a user