mirror of
https://github.com/lensapp/lens.git
synced 2025-05-20 05:10:56 +00:00
1 parent
e810eab39f
commit
b7974827d2
786 files changed
+14567
-24066
No files matched your search
@@ -0,0 +1,58 @@
|
||||
.ClusterIssues {
|
||||
min-height: 350px;
|
||||
position: relative;
|
||||
|
||||
@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%;
|
||||
color: $colorError;
|
||||
}
|
||||
}
|
||||
|
||||
.Table {
|
||||
.TableHead {
|
||||
background-color: transparent;
|
||||
border-bottom: 1px solid $borderFaintColor;
|
||||
|
||||
.TableCell {
|
||||
padding-top: 0;
|
||||
padding-bottom: floor($padding / 1.33);
|
||||
}
|
||||
}
|
||||
|
||||
.TableCell {
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
|
||||
&.message {
|
||||
flex-grow: 3;
|
||||
}
|
||||
|
||||
&.object {
|
||||
flex-grow: 2;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.no-issues {
|
||||
.Icon {
|
||||
color: white;
|
||||
}
|
||||
|
||||
.ok-title {
|
||||
font-size: large;
|
||||
color: $textColorAccent;
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,149 @@
|
||||
import "./cluster-issues.scss"
|
||||
|
||||
import React from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { computed } from "mobx";
|
||||
import { Trans } from "@lingui/macro";
|
||||
import { Icon } from "../icon";
|
||||
import { SubHeader } from "../layout/sub-header";
|
||||
import { Table, TableCell, TableHead, TableRow } from "../table";
|
||||
import { nodesStore } from "../+nodes/nodes.store";
|
||||
import { eventStore } from "../+events/event.store";
|
||||
import { autobind, cssNames, prevDefault } from "../../utils";
|
||||
import { getSelectedDetails, showDetails } from "../../navigation";
|
||||
import { ItemObject } from "../../item.store";
|
||||
import { Spinner } from "../spinner";
|
||||
import { themeStore } from "../../theme.store";
|
||||
import { lookupApiLink } from "../../api/kube-api";
|
||||
|
||||
interface Props {
|
||||
className?: string;
|
||||
}
|
||||
|
||||
interface IWarning extends ItemObject {
|
||||
kind: string;
|
||||
message: string;
|
||||
selfLink: string;
|
||||
}
|
||||
|
||||
enum sortBy {
|
||||
type = "type",
|
||||
object = "object"
|
||||
}
|
||||
|
||||
@observer
|
||||
export class ClusterIssues extends React.Component<Props> {
|
||||
private sortCallbacks = {
|
||||
[sortBy.type]: (warning: IWarning) => warning.kind,
|
||||
[sortBy.object]: (warning: IWarning) => warning.getName(),
|
||||
};
|
||||
|
||||
@computed get warnings() {
|
||||
const warnings: IWarning[] = [];
|
||||
|
||||
// Node bad conditions
|
||||
nodesStore.items.forEach(node => {
|
||||
const { kind, selfLink, getId, getName } = node
|
||||
node.getWarningConditions().forEach(({ message }) => {
|
||||
warnings.push({
|
||||
kind,
|
||||
getId,
|
||||
getName,
|
||||
selfLink,
|
||||
message,
|
||||
})
|
||||
})
|
||||
});
|
||||
|
||||
// Warning events for Workloads
|
||||
const events = eventStore.getWarnings();
|
||||
events.forEach(error => {
|
||||
const { message, involvedObject } = error;
|
||||
const { uid, name, kind } = involvedObject;
|
||||
warnings.push({
|
||||
getId: () => uid,
|
||||
getName: () => name,
|
||||
message,
|
||||
kind,
|
||||
selfLink: lookupApiLink(involvedObject, error),
|
||||
});
|
||||
})
|
||||
|
||||
return warnings;
|
||||
}
|
||||
|
||||
@autobind()
|
||||
getTableRow(uid: string) {
|
||||
const { warnings } = this;
|
||||
const warning = warnings.find(warn => warn.getId() == uid);
|
||||
const { getId, getName, message, kind, selfLink } = warning;
|
||||
return (
|
||||
<TableRow
|
||||
key={getId()}
|
||||
sortItem={warning}
|
||||
selected={selfLink === getSelectedDetails()}
|
||||
onClick={prevDefault(() => showDetails(selfLink))}
|
||||
>
|
||||
<TableCell className="message">
|
||||
{message}
|
||||
</TableCell>
|
||||
<TableCell className="object">
|
||||
{getName()}
|
||||
</TableCell>
|
||||
<TableCell className="kind">
|
||||
{kind}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
}
|
||||
|
||||
renderContent() {
|
||||
const { warnings } = this;
|
||||
if (!eventStore.isLoaded) {
|
||||
return (
|
||||
<Spinner center/>
|
||||
);
|
||||
}
|
||||
if (!warnings.length) {
|
||||
return (
|
||||
<div className="no-issues flex column box grow gaps align-center justify-center">
|
||||
<div><Icon material="check" big sticker/></div>
|
||||
<div className="ok-title"><Trans>No issues found</Trans></div>
|
||||
<span><Trans>Everything is fine in the Cluster</Trans></span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<SubHeader>
|
||||
<Icon material="error_outline"/>{" "}
|
||||
<Trans>Warnings: {warnings.length}</Trans>
|
||||
</SubHeader>
|
||||
<Table
|
||||
items={warnings}
|
||||
virtual
|
||||
selectable
|
||||
sortable={this.sortCallbacks}
|
||||
sortByDefault={{ sortBy: sortBy.object, orderBy: "asc" }}
|
||||
sortSyncWithUrl={false}
|
||||
getTableRow={this.getTableRow}
|
||||
className={cssNames("box grow", themeStore.activeTheme.type)}
|
||||
>
|
||||
<TableHead nowrap>
|
||||
<TableCell className="message"><Trans>Message</Trans></TableCell>
|
||||
<TableCell className="object" sortBy={sortBy.object}><Trans>Object</Trans></TableCell>
|
||||
<TableCell className="kind" sortBy={sortBy.type}><Trans>Type</Trans></TableCell>
|
||||
</TableHead>
|
||||
</Table>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div className={cssNames("ClusterIssues flex column", this.props.className)}>
|
||||
{this.renderContent()}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
.ClusterMetricSwitchers {
|
||||
margin-bottom: $margin * 2;
|
||||
|
||||
.metric-switch {
|
||||
text-align: right;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
import "./cluster-metric-switchers.scss";
|
||||
|
||||
import React from "react";
|
||||
import { Trans } from "@lingui/macro";
|
||||
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";
|
||||
|
||||
export const ClusterMetricSwitchers = observer(() => {
|
||||
const { metricType, metricNodeRole, getMetricsValues, metrics } = clusterStore;
|
||||
const { masterNodes, workerNodes } = nodesStore;
|
||||
const metricsValues = getMetricsValues(metrics);
|
||||
const disableRoles = !masterNodes.length || !workerNodes.length;
|
||||
const disableMetrics = !metricsValues.length;
|
||||
return (
|
||||
<div className="ClusterMetricSwitchers flex gaps">
|
||||
<div className="box grow">
|
||||
<RadioGroup
|
||||
asButtons
|
||||
className={cssNames("RadioGroup flex gaps", { disabled: disableRoles })}
|
||||
value={metricNodeRole}
|
||||
onChange={(metric: MetricNodeRole) => clusterStore.metricNodeRole = metric}
|
||||
>
|
||||
<Radio label={<Trans>Master</Trans>} value={MetricNodeRole.MASTER}/>
|
||||
<Radio label={<Trans>Worker</Trans>} value={MetricNodeRole.WORKER}/>
|
||||
</RadioGroup>
|
||||
</div>
|
||||
<div className="box grow metric-switch">
|
||||
<RadioGroup
|
||||
asButtons
|
||||
className={cssNames("RadioGroup flex gaps", { disabled: disableMetrics })}
|
||||
value={metricType}
|
||||
onChange={(value: MetricType) => clusterStore.metricType = value}
|
||||
>
|
||||
<Radio label={<Trans>CPU</Trans>} value={MetricType.CPU}/>
|
||||
<Radio label={<Trans>Memory</Trans>} value={MetricType.MEMORY}/>
|
||||
</RadioGroup>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,16 @@
|
||||
.ClusterMetrics {
|
||||
position: relative;
|
||||
min-height: 280px;
|
||||
|
||||
.Chart {
|
||||
.chart-container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.empty {
|
||||
margin-top: -45px;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
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 { BarChart } from "../chart";
|
||||
import { bytesToUnits } from "../../utils";
|
||||
import { Spinner } from "../spinner";
|
||||
import { ZebraStripes } from "../chart/zebra-stripes.plugin";
|
||||
import { ClusterNoMetrics } from "./cluster-no-metrics";
|
||||
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 metricValues = getMetricsValues(metrics);
|
||||
const liveMetricValues = getMetricsValues(liveMetrics);
|
||||
const colors = { cpu: "#3D90CE", memory: "#C93DCE" };
|
||||
const data = metricValues.map(value => ({
|
||||
x: value[0],
|
||||
y: parseFloat(value[1]).toFixed(3)
|
||||
}));
|
||||
|
||||
const datasets = [{
|
||||
id: metricType + metricNodeRole,
|
||||
label: metricType.toUpperCase() + " usage",
|
||||
borderColor: colors[metricType],
|
||||
data: data
|
||||
}];
|
||||
const cpuOptions: ChartOptions = {
|
||||
scales: {
|
||||
yAxes: [{
|
||||
ticks: {
|
||||
suggestedMax: cpuCapacity,
|
||||
callback: (value) => value
|
||||
}
|
||||
}]
|
||||
},
|
||||
tooltips: {
|
||||
callbacks: {
|
||||
label: ({ index }, data) => {
|
||||
const value = data.datasets[0].data[index] as ChartPoint;
|
||||
return value.y.toString();
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
const memoryOptions: ChartOptions = {
|
||||
scales: {
|
||||
yAxes: [{
|
||||
ticks: {
|
||||
suggestedMax: memoryCapacity,
|
||||
callback: (value: string) => !value ? 0 : bytesToUnits(parseInt(value))
|
||||
}
|
||||
}]
|
||||
},
|
||||
tooltips: {
|
||||
callbacks: {
|
||||
label: ({ index }, data) => {
|
||||
const value = data.datasets[0].data[index] as ChartPoint;
|
||||
return bytesToUnits(parseInt(value.y as string), 3);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
const options = metricType === MetricType.CPU ? cpuOptions : memoryOptions;
|
||||
|
||||
const renderMetrics = () => {
|
||||
if ((!metricValues.length || !liveMetricValues.length) && !metricsLoaded) {
|
||||
return <Spinner center/>;
|
||||
}
|
||||
if (!memoryCapacity || !cpuCapacity) {
|
||||
return <ClusterNoMetrics className="empty"/>
|
||||
}
|
||||
return (
|
||||
<BarChart
|
||||
name={`${metricNodeRole}-${metricType}`}
|
||||
options={options}
|
||||
data={{ datasets }}
|
||||
timeLabelStep={5}
|
||||
showLegend={false}
|
||||
plugins={[ZebraStripes]}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="ClusterMetrics flex column">
|
||||
<ClusterMetricSwitchers/>
|
||||
{renderMetrics()}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,18 @@
|
||||
import React from "react";
|
||||
import { Icon } from "../icon";
|
||||
import { Trans } from "@lingui/macro";
|
||||
import { cssNames } from "../../utils";
|
||||
|
||||
interface Props {
|
||||
className: string;
|
||||
}
|
||||
|
||||
export function ClusterNoMetrics({ className }: Props) {
|
||||
return (
|
||||
<div className={cssNames("ClusterNoMetrics flex column box grow justify-center align-center", className)}>
|
||||
<Icon material="info"/>
|
||||
<p><Trans>Metrics are not available due to missing or invalid Prometheus configuration.</Trans></p>
|
||||
<p><Trans>Right click cluster icon to open cluster settings.</Trans></p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
.ClusterPieCharts {
|
||||
background: transparent!important;
|
||||
padding: 0!important;
|
||||
|
||||
.empty {
|
||||
background: $contentColor;
|
||||
min-height: 280px;
|
||||
text-align: center;
|
||||
padding: $padding * 2;
|
||||
}
|
||||
|
||||
.NodeCharts {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.chart {
|
||||
--flex-gap: #{$padding * 2};
|
||||
background: $contentColor;
|
||||
padding: $padding * 2 $padding;
|
||||
|
||||
.chart-title {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.legend {
|
||||
--flex-gap: #{$padding};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,204 @@
|
||||
import "./cluster-pie-charts.scss";
|
||||
|
||||
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 { Spinner } from "../spinner";
|
||||
import { Icon } from "../icon";
|
||||
import { nodesStore } from "../+nodes/nodes.store";
|
||||
import { ChartData, PieChart } from "../chart";
|
||||
import { ClusterNoMetrics } from "./cluster-no-metrics";
|
||||
import { bytesToUnits } from "../../utils";
|
||||
import { themeStore } from "../../theme.store";
|
||||
import { getMetricLastPoints } from "../../api/endpoints/metrics.api";
|
||||
|
||||
export const ClusterPieCharts = observer(() => {
|
||||
const { i18n } = useLingui();
|
||||
|
||||
const renderLimitWarning = () => {
|
||||
return (
|
||||
<div className="node-warning flex gaps align-center">
|
||||
<Icon material="info"/>
|
||||
<p><Trans>Specified limits are higher than node capacity!</Trans></p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const renderCharts = () => {
|
||||
const data = getMetricLastPoints(clusterStore.metrics);
|
||||
const { memoryUsage, memoryRequests, memoryCapacity, memoryLimits } = data;
|
||||
const { cpuUsage, cpuRequests, cpuCapacity, cpuLimits } = data;
|
||||
const { podUsage, podCapacity } = data;
|
||||
const cpuLimitsOverload = cpuLimits > cpuCapacity;
|
||||
const memoryLimitsOverload = memoryLimits > memoryCapacity;
|
||||
const defaultColor = themeStore.activeTheme.colors.pieChartDefaultColor;
|
||||
|
||||
if (!memoryCapacity || !cpuCapacity || !podCapacity) return null;
|
||||
const cpuData: ChartData = {
|
||||
datasets: [
|
||||
{
|
||||
data: [
|
||||
cpuUsage,
|
||||
cpuUsage ? cpuCapacity - cpuUsage : 1,
|
||||
],
|
||||
backgroundColor: [
|
||||
"#c93dce",
|
||||
defaultColor,
|
||||
],
|
||||
id: "cpuUsage"
|
||||
},
|
||||
{
|
||||
data: [
|
||||
cpuRequests,
|
||||
cpuRequests ? cpuCapacity - cpuRequests : 1,
|
||||
],
|
||||
backgroundColor: [
|
||||
"#4caf50",
|
||||
defaultColor,
|
||||
],
|
||||
id: "cpuRequests"
|
||||
},
|
||||
{
|
||||
data: [
|
||||
cpuLimits,
|
||||
cpuLimitsOverload ? 0 : cpuCapacity - cpuLimits,
|
||||
],
|
||||
backgroundColor: [
|
||||
"#3d90ce",
|
||||
defaultColor,
|
||||
],
|
||||
id: "cpuLimits"
|
||||
},
|
||||
],
|
||||
labels: [
|
||||
i18n._(t`Usage`) + `: ${cpuUsage ? cpuUsage.toFixed(2) : "N/A"}`,
|
||||
i18n._(t`Requests`) + `: ${cpuRequests ? cpuRequests.toFixed(2) : "N/A"}`,
|
||||
i18n._(t`Limits`) + `: ${cpuLimits ? cpuLimits.toFixed(2) : "N/A"}`,
|
||||
i18n._(t`Capacity`) + `: ${cpuCapacity || "N/A"}`
|
||||
]
|
||||
};
|
||||
const memoryData: ChartData = {
|
||||
datasets: [
|
||||
{
|
||||
data: [
|
||||
memoryUsage,
|
||||
memoryUsage ? memoryCapacity - memoryUsage : 1,
|
||||
],
|
||||
backgroundColor: [
|
||||
"#c93dce",
|
||||
defaultColor,
|
||||
],
|
||||
id: "memoryUsage"
|
||||
},
|
||||
{
|
||||
data: [
|
||||
memoryRequests,
|
||||
memoryRequests ? memoryCapacity - memoryRequests : 1,
|
||||
],
|
||||
backgroundColor: [
|
||||
"#4caf50",
|
||||
defaultColor,
|
||||
],
|
||||
id: "memoryRequests"
|
||||
},
|
||||
{
|
||||
data: [
|
||||
memoryLimits,
|
||||
memoryLimitsOverload ? 0 : memoryCapacity - memoryLimits,
|
||||
],
|
||||
backgroundColor: [
|
||||
"#3d90ce",
|
||||
defaultColor,
|
||||
],
|
||||
id: "memoryLimits"
|
||||
},
|
||||
],
|
||||
labels: [
|
||||
i18n._(t`Usage`) + `: ${bytesToUnits(memoryUsage)}`,
|
||||
i18n._(t`Requests`) + `: ${bytesToUnits(memoryRequests)}`,
|
||||
i18n._(t`Limits`) + `: ${bytesToUnits(memoryLimits)}`,
|
||||
i18n._(t`Capacity`) + `: ${bytesToUnits(memoryCapacity)}`,
|
||||
]
|
||||
};
|
||||
const podsData: ChartData = {
|
||||
datasets: [
|
||||
{
|
||||
data: [
|
||||
podUsage,
|
||||
podUsage ? podCapacity - podUsage : 1,
|
||||
],
|
||||
backgroundColor: [
|
||||
"#4caf50",
|
||||
defaultColor,
|
||||
],
|
||||
id: "podUsage"
|
||||
},
|
||||
],
|
||||
labels: [
|
||||
i18n._(t`Usage`) + `: ${podUsage || 0}`,
|
||||
i18n._(t`Capacity`) + `: ${podCapacity}`,
|
||||
]
|
||||
};
|
||||
return (
|
||||
<div className="NodeCharts flex justify-center box grow gaps">
|
||||
<div className="chart flex column align-center box grow">
|
||||
<PieChart
|
||||
data={cpuData}
|
||||
title={i18n._(t`CPU`)}
|
||||
legendColors={["#c93dce", "#4caf50", "#3d90ce", defaultColor]}
|
||||
/>
|
||||
{cpuLimitsOverload && renderLimitWarning()}
|
||||
</div>
|
||||
<div className="chart flex column align-center box grow">
|
||||
<PieChart
|
||||
data={memoryData}
|
||||
title={i18n._(t`Memory`)}
|
||||
legendColors={["#c93dce", "#4caf50", "#3d90ce", defaultColor]}
|
||||
/>
|
||||
{memoryLimitsOverload && renderLimitWarning()}
|
||||
</div>
|
||||
<div className="chart flex column align-center box grow">
|
||||
<PieChart
|
||||
data={podsData}
|
||||
title={i18n._(t`Pods`)}
|
||||
legendColors={["#4caf50", defaultColor]}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const renderContent = () => {
|
||||
const { masterNodes, workerNodes } = nodesStore;
|
||||
const { metricNodeRole, metricsLoaded } = clusterStore;
|
||||
const nodes = metricNodeRole === MetricNodeRole.MASTER ? masterNodes : workerNodes;
|
||||
if (!nodes.length) {
|
||||
return (
|
||||
<div className="empty flex column box grow align-center justify-center">
|
||||
<Icon material="info"/>
|
||||
<Trans>No Nodes Available.</Trans>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
if (!metricsLoaded) {
|
||||
return (
|
||||
<div className="flex justify-center align-center box grow empty">
|
||||
<Spinner/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
const { memoryCapacity, cpuCapacity, podCapacity } = getMetricLastPoints(clusterStore.metrics);
|
||||
if (!memoryCapacity || !cpuCapacity || !podCapacity) {
|
||||
return <ClusterNoMetrics className="empty"/>;
|
||||
}
|
||||
return renderCharts();
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="ClusterPieCharts flex">
|
||||
{renderContent()}
|
||||
</div>
|
||||
);
|
||||
})
|
||||
@@ -0,0 +1,8 @@
|
||||
import { RouteProps } from "react-router";
|
||||
import { buildURL } from "../../navigation";
|
||||
|
||||
export const clusterRoute: RouteProps = {
|
||||
path: "/cluster"
|
||||
}
|
||||
|
||||
export const clusterURL = buildURL(clusterRoute.path)
|
||||
@@ -0,0 +1,24 @@
|
||||
.Cluster {
|
||||
$gridGap: $margin * 2;
|
||||
|
||||
position: relative;
|
||||
height: 100%;
|
||||
min-height: 650px;
|
||||
display: grid;
|
||||
grid-gap: $gridGap;
|
||||
grid-template-columns: repeat(auto-fit, minmax(500px, 1fr));
|
||||
grid-template-rows: 1fr 1fr;
|
||||
|
||||
@include media(">1600px") {
|
||||
grid-template-columns: 1fr 1fr;
|
||||
}
|
||||
|
||||
> *:not(.Spinner) {
|
||||
padding: $gridGap;
|
||||
background: $contentColor;
|
||||
|
||||
> .SubHeader {
|
||||
padding-top: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
import { observable, reaction, when } from "mobx";
|
||||
import { KubeObjectStore } from "../../kube-object.store";
|
||||
import { Cluster, clusterApi, IClusterMetrics } from "../../api/endpoints";
|
||||
import { autobind, createStorage } from "../../utils";
|
||||
import { IMetricsReqParams, normalizeMetrics } from "../../api/endpoints/metrics.api";
|
||||
import { nodesStore } from "../+nodes/nodes.store";
|
||||
import { apiManager } from "../../api/api-manager";
|
||||
|
||||
export enum MetricType {
|
||||
MEMORY = "memory",
|
||||
CPU = "cpu"
|
||||
}
|
||||
|
||||
export enum MetricNodeRole {
|
||||
MASTER = "master",
|
||||
WORKER = "worker"
|
||||
}
|
||||
|
||||
@autobind()
|
||||
export class ClusterStore extends KubeObjectStore<Cluster> {
|
||||
api = clusterApi
|
||||
|
||||
@observable metrics: Partial<IClusterMetrics> = {};
|
||||
@observable liveMetrics: Partial<IClusterMetrics> = {};
|
||||
@observable metricsLoaded = false;
|
||||
@observable metricType: MetricType;
|
||||
@observable metricNodeRole: MetricNodeRole;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.resetMetrics();
|
||||
|
||||
// sync user setting with local storage
|
||||
const storage = createStorage("cluster_metric_switchers", {});
|
||||
Object.assign(this, storage.get());
|
||||
reaction(() => {
|
||||
const { metricType, metricNodeRole } = this;
|
||||
return { metricType, metricNodeRole }
|
||||
},
|
||||
settings => storage.set(settings)
|
||||
);
|
||||
|
||||
// auto-update metrics
|
||||
reaction(() => this.metricNodeRole, () => {
|
||||
if (!this.metricsLoaded) return;
|
||||
this.metrics = {};
|
||||
this.liveMetrics = {};
|
||||
this.metricsLoaded = false;
|
||||
this.getAllMetrics();
|
||||
});
|
||||
|
||||
// check which node type to select
|
||||
reaction(() => nodesStore.items.length, () => {
|
||||
const { masterNodes, workerNodes } = nodesStore;
|
||||
if (!masterNodes.length) this.metricNodeRole = MetricNodeRole.WORKER;
|
||||
if (!workerNodes.length) this.metricNodeRole = MetricNodeRole.MASTER;
|
||||
});
|
||||
}
|
||||
|
||||
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.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:
|
||||
return normalizeMetrics(source.cpuUsage).data.result[0].values
|
||||
case MetricType.MEMORY:
|
||||
return normalizeMetrics(source.memoryUsage).data.result[0].values
|
||||
default:
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
resetMetrics() {
|
||||
this.metrics = {};
|
||||
this.metricsLoaded = false;
|
||||
this.metricType = MetricType.CPU;
|
||||
this.metricNodeRole = MetricNodeRole.WORKER;
|
||||
}
|
||||
|
||||
reset() {
|
||||
super.reset();
|
||||
this.resetMetrics();
|
||||
}
|
||||
}
|
||||
|
||||
export const clusterStore = new ClusterStore();
|
||||
apiManager.registerStore(clusterApi, clusterStore);
|
||||
@@ -0,0 +1,73 @@
|
||||
import "./cluster.scss"
|
||||
|
||||
import React from "react";
|
||||
import { computed, reaction } from "mobx";
|
||||
import { disposeOnUnmount, observer } from "mobx-react";
|
||||
import { MainLayout } from "../layout/main-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 "../../api/rbac";
|
||||
|
||||
@observer
|
||||
export class Cluster extends React.Component {
|
||||
private watchers = [
|
||||
interval(60, () => clusterStore.getMetrics()),
|
||||
interval(20, () => eventStore.loadAll())
|
||||
];
|
||||
|
||||
private dependentStores = [nodesStore, podsStore];
|
||||
|
||||
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())
|
||||
)
|
||||
])
|
||||
}
|
||||
|
||||
@computed get isLoaded() {
|
||||
return (
|
||||
nodesStore.isLoaded &&
|
||||
podsStore.isLoaded
|
||||
)
|
||||
}
|
||||
|
||||
render() {
|
||||
const { isLoaded } = this;
|
||||
return (
|
||||
<MainLayout>
|
||||
<div className="Cluster">
|
||||
{!isLoaded && <Spinner center/>}
|
||||
{isLoaded && (
|
||||
<>
|
||||
<ClusterMetrics/>
|
||||
<ClusterPieCharts/>
|
||||
<ClusterIssues className={cssNames({ wide: isElectron })}/>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</MainLayout>
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from "./cluster.routes"
|
||||
|
||||
Reference in new issue
Block a user