* Copyright (C) 2019 Red Hat, Inc.
* SPDX-License-Identifier: LGPL-2.1-or-later
*/
import React from 'react';
import { Button } from "@patternfly/react-core/dist/esm/components/Button/index.js";
import { Card, CardBody, CardFooter, CardTitle } from "@patternfly/react-core/dist/esm/components/Card/index.js";
import { Progress, ProgressMeasureLocation, ProgressVariant } from "@patternfly/react-core/dist/esm/components/Progress/index.js";
import * as machine_info from "machine-info.js";
import cockpit from "cockpit";
import "./usageCard.scss";
const _ = cockpit.gettext;
const METRICS_SPEC = {
payload: "metrics1",
source: "internal",
interval: 3000,
metrics: [
{ name: "cpu.basic.user", derive: "rate" },
{ name: "cpu.basic.system", derive: "rate" },
{ name: "cpu.basic.nice", derive: "rate" },
{ name: "memory.used" },
]
};
export class UsageCard extends React.Component {
constructor(props) {
super(props);
this.metrics_channel = null;
this.samples = [];
this.state = {
memTotal: 0,
memUsed: 0,
memUsedText: " ",
numCpu: 1,
cpuUsed: 0,
};
this.onVisibilityChange = this.onVisibilityChange.bind(this);
this.onMetricsUpdate = this.onMetricsUpdate.bind(this);
}
componentDidMount() {
machine_info.cpu_ram_info()
.then(info => this.setState({
memTotal: info.memory,
numCpu: info.cpus,
}));
cockpit.addEventListener("visibilitychange", this.onVisibilityChange);
this.onVisibilityChange();
}
componentWillUnmount() {
cockpit.removeEventListener("visibilitychange", this.onVisibilityChange);
}
onVisibilityChange() {
if (cockpit.hidden && this.metrics_channel !== null) {
this.metrics_channel.removeEventListener("message", this.onMetricsUpdate);
this.metrics_channel.close();
this.metrics_channel = null;
return;
}
if (!cockpit.hidden && this.metrics_channel === null) {
this.metrics_channel = cockpit.channel(METRICS_SPEC);
this.metrics_channel.addEventListener("closed", (ev, error) => console.error("metrics closed:", error));
this.metrics_channel.addEventListener("message", this.onMetricsUpdate);
}
}
onMetricsUpdate(event, message) {
const data = JSON.parse(message);
if (!Array.isArray(data)) {
this.samples = [];
return;
}
data.forEach(samples => {
samples.forEach((sample, i) => {
if (sample !== null)
this.samples[i] = sample;
});
});
if (this.samples[0] !== false) {
const cpu = Math.round((this.samples[0] + this.samples[1] + this.samples[2]) / 10 / this.state.numCpu);
this.setState({ cpuUsed: cpu });
}
let used_text;
if (this.state.memTotal) {
const [total_fmt, unit] = cockpit.format_bytes(this.state.memTotal, { base2: true, separate: true, precision: 2 });
const used_fmt = cockpit.format_bytes(this.samples[3], unit, { separate: true, precision: 2 })[0];
used_text = cockpit.format("$0 / $1 $2", used_fmt, total_fmt, unit);
} else {
used_text = " ";
}
this.setState({ memUsed: this.samples[3], memUsedText: used_text });
}
render() {
const fraction = this.state.memTotal ? this.state.memUsed / this.state.memTotal : 0;
const cores_str = cockpit.format(cockpit.ngettext("of $0 CPU", "of $0 CPUs", this.state.numCpu), this.state.numCpu);
return (
<Card className="system-usage">
<CardTitle>{_("Usage")}</CardTitle>
<CardBody>
<table className="pf-v6-c-table pf-m-grid-md pf-m-compact">
<tbody className="pf-v6-c-table__tbody">
<tr className="pf-v6-c-table__tr">
<th className="pf-v6-c-table__th" id="system-usage-cpu-progress" scope="row">{_("CPU")}</th>
<td className="pf-v6-c-table__td">
<Progress value={this.state.cpuUsed}
className="pf-m-sm"
min={0} max={100}
variant={ this.state.cpuUsed > 90 ? ProgressVariant.danger : undefined }
label={ this.state.cpuUsed + '% ' + cores_str }
aria-labelledby="system-usage-cpu-progress"
measureLocation={ProgressMeasureLocation.outside} />
</td>
</tr>
<tr className="pf-v6-c-table__tr">
<th className="pf-v6-c-table__th" id="system-usage-memory-progress" scope="row">{_("Memory")}</th>
<td className="pf-v6-c-table__td">
<Progress value={this.state.memUsed}
className="pf-m-sm"
min={0} max={this.state.memTotal}
variant={fraction > 0.9 ? ProgressVariant.danger : undefined}
aria-labelledby="system-usage-memory-progress"
label={this.state.memUsedText}
measureLocation={ProgressMeasureLocation.outside} />
</td>
</tr>
</tbody>
</table>
</CardBody>
<CardFooter>
<Button isInline variant="link" component="a" onClick={ev => { ev.preventDefault(); cockpit.jump("/metrics", cockpit.transport.host) }}>
{_("View metrics and history")}
</Button>
</CardFooter>
</Card>
);
}
}