Line data Source code
1 : /*
2 : * Copyright (C) 2019 Red Hat, Inc.
3 : * SPDX-License-Identifier: LGPL-2.1-or-later
4 : */
5 94 : import React from 'react';
6 : import { Button } from "@patternfly/react-core/dist/esm/components/Button/index.js";
7 : import { Card, CardBody, CardFooter, CardTitle } from "@patternfly/react-core/dist/esm/components/Card/index.js";
8 : import { Progress, ProgressMeasureLocation, ProgressVariant } from "@patternfly/react-core/dist/esm/components/Progress/index.js";
9 :
10 : import * as machine_info from "machine-info.js";
11 94 : import cockpit from "cockpit";
12 :
13 : import "./usageCard.scss";
14 :
15 94 : const _ = cockpit.gettext;
16 :
17 94 : const METRICS_SPEC = {
18 94 : payload: "metrics1",
19 94 : source: "internal",
20 94 : interval: 3000,
21 94 : metrics: [
22 94 : { name: "cpu.basic.user", derive: "rate" },
23 94 : { name: "cpu.basic.system", derive: "rate" },
24 94 : { name: "cpu.basic.nice", derive: "rate" },
25 94 : { name: "memory.used" },
26 94 : ]
27 94 : };
28 :
29 94 : export class UsageCard extends React.Component {
30 94 : constructor(props) {
31 94 : super(props);
32 :
33 94 : this.metrics_channel = null;
34 94 : this.samples = [];
35 :
36 94 : this.state = {
37 94 : memTotal: 0, // bytes
38 94 : memUsed: 0, // bytes
39 94 : memUsedText: " ",
40 94 : numCpu: 1, // number
41 94 : cpuUsed: 0, // percentage
42 94 : };
43 :
44 94 : this.onVisibilityChange = this.onVisibilityChange.bind(this);
45 94 : this.onMetricsUpdate = this.onMetricsUpdate.bind(this);
46 94 : }
47 :
48 94 : componentDidMount() {
49 94 : machine_info.cpu_ram_info()
50 86 : .then(info => this.setState({
51 86 : memTotal: info.memory,
52 86 : numCpu: info.cpus,
53 86 : }));
54 94 : cockpit.addEventListener("visibilitychange", this.onVisibilityChange);
55 94 : this.onVisibilityChange();
56 94 : }
57 :
58 0 : componentWillUnmount() {
59 0 : cockpit.removeEventListener("visibilitychange", this.onVisibilityChange);
60 0 : }
61 :
62 117 : onVisibilityChange() {
63 74 : if (cockpit.hidden && this.metrics_channel !== null) {
64 74 : this.metrics_channel.removeEventListener("message", this.onMetricsUpdate);
65 74 : this.metrics_channel.close();
66 74 : this.metrics_channel = null;
67 74 : return;
68 74 : }
69 :
70 116 : if (!cockpit.hidden && this.metrics_channel === null) {
71 116 : this.metrics_channel = cockpit.channel(METRICS_SPEC);
72 0 : this.metrics_channel.addEventListener("closed", (ev, error) => console.error("metrics closed:", error));
73 116 : this.metrics_channel.addEventListener("message", this.onMetricsUpdate);
74 116 : }
75 117 : }
76 :
77 88 : onMetricsUpdate(event, message) {
78 88 : const data = JSON.parse(message);
79 :
80 : // reset state on meta messages
81 88 : if (!Array.isArray(data)) {
82 88 : this.samples = [];
83 88 : return;
84 88 : }
85 :
86 : // decompress
87 88 : data.forEach(samples => {
88 88 : samples.forEach((sample, i) => {
89 88 : if (sample !== null)
90 88 : this.samples[i] = sample;
91 88 : });
92 88 : });
93 :
94 : // CPU metrics are in ms/s; divide by 10 to get percentage
95 40 : if (this.samples[0] !== false) {
96 40 : const cpu = Math.round((this.samples[0] + this.samples[1] + this.samples[2]) / 10 / this.state.numCpu);
97 40 : this.setState({ cpuUsed: cpu });
98 40 : }
99 :
100 88 : let used_text;
101 44 : if (this.state.memTotal) {
102 44 : const [total_fmt, unit] = cockpit.format_bytes(this.state.memTotal, { base2: true, separate: true, precision: 2 });
103 : // FIXME: passing explicit unit is deprecated, redesign this
104 44 : const used_fmt = cockpit.format_bytes(this.samples[3], unit, { separate: true, precision: 2 })[0];
105 44 : used_text = cockpit.format("$0 / $1 $2", used_fmt, total_fmt, unit);
106 44 : } else {
107 88 : used_text = " ";
108 88 : }
109 :
110 88 : this.setState({ memUsed: this.samples[3], memUsedText: used_text });
111 88 : }
112 :
113 94 : render() {
114 86 : const fraction = this.state.memTotal ? this.state.memUsed / this.state.memTotal : 0;
115 94 : const cores_str = cockpit.format(cockpit.ngettext("of $0 CPU", "of $0 CPUs", this.state.numCpu), this.state.numCpu);
116 :
117 94 : return (
118 94 : <Card className="system-usage">
119 94 : <CardTitle>{_("Usage")}</CardTitle>
120 94 : <CardBody>
121 94 : <table className="pf-v6-c-table pf-m-grid-md pf-m-compact">
122 94 : <tbody className="pf-v6-c-table__tbody">
123 94 : <tr className="pf-v6-c-table__tr">
124 94 : <th className="pf-v6-c-table__th" id="system-usage-cpu-progress" scope="row">{_("CPU")}</th>
125 94 : <td className="pf-v6-c-table__td">
126 94 : <Progress value={this.state.cpuUsed}
127 94 : className="pf-m-sm"
128 94 : min={0} max={100}
129 19 : variant={ this.state.cpuUsed > 90 ? ProgressVariant.danger : undefined }
130 94 : label={ this.state.cpuUsed + '% ' + cores_str }
131 94 : aria-labelledby="system-usage-cpu-progress"
132 94 : measureLocation={ProgressMeasureLocation.outside} />
133 94 : </td>
134 94 : </tr>
135 94 : <tr className="pf-v6-c-table__tr">
136 94 : <th className="pf-v6-c-table__th" id="system-usage-memory-progress" scope="row">{_("Memory")}</th>
137 94 : <td className="pf-v6-c-table__td">
138 94 : <Progress value={this.state.memUsed}
139 94 : className="pf-m-sm"
140 94 : min={0} max={this.state.memTotal}
141 16 : variant={fraction > 0.9 ? ProgressVariant.danger : undefined}
142 94 : aria-labelledby="system-usage-memory-progress"
143 94 : label={this.state.memUsedText}
144 94 : measureLocation={ProgressMeasureLocation.outside} />
145 94 : </td>
146 94 : </tr>
147 94 : </tbody>
148 94 : </table>
149 94 : </CardBody>
150 94 : <CardFooter>
151 0 : <Button isInline variant="link" component="a" onClick={ev => { ev.preventDefault(); cockpit.jump("/metrics", cockpit.transport.host) }}>
152 94 : {_("View metrics and history")}
153 94 : </Button>
154 94 : </CardFooter>
155 94 : </Card>
156 : );
157 94 : }
158 94 : }
|