server stats page

This commit is contained in:
mbecker20
2023-01-08 02:54:59 +00:00
parent 4bc8fc4b25
commit 2906eaf5f5
16 changed files with 292 additions and 397 deletions

View File

@@ -425,7 +425,7 @@ impl State {
limit,
query.page as u64 * limit as u64,
doc! { "ts": { "$mod": [ts_mod, 0] } },
None,
projection,
)
.await
.context("failed at mongo query to get stats")

View File

@@ -20,6 +20,7 @@
"apexcharts": "^3.36.3",
"axios": "^1.2.1",
"js-file-download": "^0.4.12",
"lightweight-charts": "^3.8.0",
"reconnecting-websocket": "^4.4.0",
"solid-js": "^1.6.6"
}

View File

@@ -29,7 +29,7 @@ const Updates: Component<{}> = (p) => {
return (
<Grid
class={combineClasses("card shadow")}
style={{ "min-width": "350px" }}
style={{ "width": "400px" }}
>
<h1>updates</h1>
<Grid class="updates-container scroller">

View File

@@ -58,7 +58,7 @@ export const ConfigProvider: ParentComponent<{}> = (p) => {
const [networks, setNetworks] = createSignal<any[]>([]);
const loadNetworks = () => {
console.log("load networks");
// console.log("load networks");
client.get_docker_networks(params.id).then(setNetworks);
};
createEffect(loadNetworks);

View File

@@ -1,72 +0,0 @@
import { Component, createEffect, createSignal, Show, For } from "solid-js";
import { pushNotification } from "../../../..";
import { useAppState } from "../../../../state/StateProvider";
import { combineClasses } from "../../../../util/helpers";
import Icon from "../../../shared/Icon";
import Flex from "../../../shared/layout/Flex";
import Grid from "../../../shared/layout/Grid";
import Loading from "../../../shared/loading/Loading";
import s from "./stats.module.scss";
const DockerStats: Component<{}> = (p) => {
// const [stats, setStats] = createSignal<DockerStat[]>();
const [refreshing, setRefreshing] = createSignal(false);
// const load = () => {
// if (selected.id()) {
// getServerStats(selected.id()).then(setStats);
// }
// };
// createEffect(load);
// const { themeClass } = useTheme();
return (
<Show
when={true}
fallback={
<Loading
type="three-dot"
scale={0.8}
style={{ "place-self": "center" }}
/>
}
>
{/* <Grid class={combineClasses(s.StatsContainer, themeClass())}>
<Flex justifyContent="space-between">
<h1>container stats</h1>
<Button
class="blue"
onClick={async () => {
setRefreshing(true);
const stats = await getServerStats(selected.id());
setStats(stats);
setRefreshing(false);
pushNotification("good", "stats refreshed");
}}
>
<Show when={!refreshing()} fallback={<Loading />}>
<Icon type="refresh" />
</Show>
</Button>
</Flex>
<Grid
class="scroller"
gap="0.5rem"
style={{ padding: "0.5rem", "max-height": "30vh" }}
>
<For each={stats()}>
{(stat) => (
<Flex alignItems="center" justifyContent="space-between">
<h2>{stat.Name}</h2>
<Flex alignItems="center">
<div>cpu: {stat.CPUPerc}</div>
<div>mem: {stat.MemPerc}</div>
</Flex>
</Flex>
)}
</For>
</Grid>
</Grid> */}
</Show>
);
};
export default DockerStats;

View File

@@ -1,10 +1,22 @@
import { useParams } from "@solidjs/router";
import { Component, createEffect, createSignal } from "solid-js";
import {
Accessor,
Component,
createEffect,
createMemo,
createSignal,
Show,
} from "solid-js";
import { client } from "../../../..";
import { useAppState } from "../../../../state/StateProvider";
import { Timelength } from "../../../../types";
import { SystemStats, SystemStatsRecord, Timelength } from "../../../../types";
import { convertTsMsToLocalUnixTsInSecs } from "../../../../util/helpers";
import { useLocalStorage } from "../../../../util/hooks";
import Flex from "../../../shared/layout/Flex";
import Grid from "../../../shared/layout/Grid";
import LightweightChart from "../../../shared/LightweightChart";
import Loading from "../../../shared/loading/Loading";
import Selector from "../../../shared/menu/Selector";
import s from "./stats.module.scss";
const TIMELENGTHS = [
Timelength.OneMinute,
@@ -16,17 +28,184 @@ const TIMELENGTHS = [
];
const Stats: Component<{}> = (p) => {
const { servers } = useAppState();
const params = useParams();
const [timelength, setTimelength] = useLocalStorage(
Timelength.OneHour,
"server-stats-timelength-v1"
Timelength.OneMinute,
"server-stats-timelength-v3"
);
const [stats, setStats] = createSignal();
const [currStats, setCurrStats] = createSignal<SystemStats>();
const [stats, setStats] = createSignal<SystemStatsRecord[]>();
createEffect(() => {
client.get_server_stats_history(params.id, { interval: timelength(), networks: true, components: true });
client.get_server_stats(params.id).then(setCurrStats);
client
.get_server_stats_history(params.id, {
interval: timelength(),
networks: true,
components: true,
})
.then(setStats);
});
return <Grid style={{ width: "100%" }}></Grid>;
// createEffect(() => console.log(stats()))
return (
<Grid
style={{
width: "100%",
height: "fit-content",
padding: "1rem 3rem",
"box-sizing": "border-box",
}}
>
<Flex
style={{ width: "100%" }}
alignItems="center"
justifyContent="space-between"
>
<Flex class="card light shadow" alignItems="center">
<Show when={currStats()} fallback={<Loading type="three-dot" />}>
<Grid gap="0" placeItems="start center">
cpu: <h2>{currStats()!.cpu_perc.toFixed(1)}%</h2>
</Grid>
<Grid gap="0" placeItems="start center">
mem:{" "}
<h2>
{(
(100 * currStats()!.mem_used_gb) /
currStats()!.mem_total_gb
).toFixed(1)}
%
</h2>
</Grid>
<Grid gap="0" placeItems="start center">
disk:{" "}
<h2>
{(
(100 * currStats()!.disk.used_gb) /
currStats()!.disk.total_gb
).toFixed(1)}
%
</h2>
</Grid>
</Show>
</Flex>
<Selector
selected={timelength()}
items={TIMELENGTHS}
onSelect={(selected) => setTimelength(selected as Timelength)}
/>
</Flex>
<Show
when={stats()}
fallback={
<div style={{ "place-self": "center" }}>
<Loading type="three-dot" />
</div>
}
>
<Grid class={s.Charts}>
<CpuChart stats={stats} />
<MemChart stats={stats} />
<DiskChart stats={stats} />
</Grid>
</Show>
</Grid>
);
};
export default Stats;
const CpuChart: Component<{
stats: Accessor<SystemStatsRecord[] | undefined>;
}> = (p) => {
const line = () => {
return p.stats()?.map((s) => {
return {
time: convertTsMsToLocalUnixTsInSecs(s.ts),
value: s.cpu_perc,
};
});
};
return (
<Show when={line()}>
<Grid gap="0" class="card dark shadow" style={{ height: "fit-content" }}>
<h2>cpu %</h2>
<LightweightChart
class={s.LightweightChart}
style={{ height: "200px" }}
lines={() => [{ color: "#184e9f", line: line()! }]}
/>
</Grid>
</Show>
);
};
const MemChart: Component<{
stats: Accessor<SystemStatsRecord[] | undefined>;
}> = (p) => {
const [selected, setSelected] = createSignal("%");
const line = () => {
return p.stats()?.map((s) => {
return {
time: convertTsMsToLocalUnixTsInSecs(s.ts),
value:
selected() === "%"
? (100 * s.mem_used_gb) / s.mem_total_gb
: s.mem_used_gb,
};
});
};
return (
<Show when={line()}>
<Grid gap="0" class="card dark shadow" style={{ height: "fit-content" }}>
<Flex alignItems="center" justifyContent="space-between">
<h2>memory %</h2>
<Selector
selected={selected()}
items={["%", "GB"]}
onSelect={setSelected}
/>
</Flex>
<LightweightChart
class={s.LightweightChart}
style={{ height: "200px" }}
lines={() => [{ color: "#184e9f", line: line()! }]}
/>
</Grid>
</Show>
);
};
const DiskChart: Component<{
stats: Accessor<SystemStatsRecord[] | undefined>;
}> = (p) => {
const [selected, setSelected] = createSignal("%");
const line = () => {
return p.stats()?.map((s) => {
return {
time: convertTsMsToLocalUnixTsInSecs(s.ts),
value:
selected() === "%"
? (100 * s.disk.used_gb) / s.disk.total_gb
: s.disk.used_gb,
};
});
};
return (
<Show when={line()}>
<Grid gap="0" class="card dark shadow" style={{ height: "fit-content" }}>
<Flex alignItems="center" justifyContent="space-between">
<h2>disk %</h2>
<Selector
selected={selected()}
items={["%", "GB"]}
onSelect={setSelected}
/>
</Flex>
<LightweightChart
class={s.LightweightChart}
style={{ height: "200px" }}
lines={() => [{ color: "#184e9f", line: line()! }]}
/>
</Grid>
</Show>
);
};

View File

@@ -1,63 +0,0 @@
import { Component, createSignal, Show } from "solid-js";
import { pushNotification } from "../../../..";
import { useAppState } from "../../../../state/StateProvider";
import { combineClasses } from "../../../../util/helpers";
import Icon from "../../../shared/Icon";
import Flex from "../../../shared/layout/Flex";
import Grid from "../../../shared/layout/Grid";
import Loading from "../../../shared/loading/Loading";
import s from "./stats.module.scss";
const SystemStats: Component<{}> = (p) => {
const { servers, serverStats } = useAppState();
const [refreshingStats, setRefreshingStats] = createSignal(false);
// const sysStats = () => serverStats.get(selected.id(), servers.get(selected.id()));
// const loadStats = async () => {
// if (selected.id() && servers.get(selected.id())?.status === "OK") {
// setRefreshingStats(true);
// await serverStats.load(selected.id());
// setRefreshingStats(false);
// pushNotification("good", "system stats refreshed");
// }
// };
return (
<Show when={true}>
{/* <Grid class={combineClasses(s.StatsContainer, themeClass())}>
<Flex justifyContent="space-between">
<h1>system stats</h1>
<Button
class="blue"
style={{ "justify-self": "end" }}
onClick={loadStats}
>
<Show when={!refreshingStats()} fallback={<Loading />}>
<Icon type="refresh" />
</Show>
</Button>
</Flex>
<Flex alignItems="center">
<h2>cpu: </h2>
<div>{sysStats()!.cpu}%</div>
</Flex>
<Flex alignItems="center">
<h2>mem: </h2>
<div>{sysStats()!.mem.usedMemPercentage}%</div>
<div>
(using {sysStats()!.mem.usedMemMb} mb of{" "}
{sysStats()!.mem.totalMemMb} mb)
</div>
</Flex>
<Flex>
<h2>disk: </h2>
<div>{sysStats()!.disk.usedPercentage}%</div>
<div>
(using {sysStats()!.disk.usedGb} gb of {sysStats()!.disk.totalGb}{" "}
gb)
</div>
</Flex>
</Grid> */}
</Show>
);
};
export default SystemStats;

View File

@@ -1,94 +0,0 @@
// import { Log as LogType } from "@monitor/types";
// import {
// Accessor,
// Component,
// createEffect,
// createSignal,
// } from "solid-js";
// import { pushNotification } from "../../../../..";
// import { useAppState } from "../../../../../state/StateProvider";
// import { useTheme } from "../../../../../state/ThemeProvider";
// import { combineClasses } from "../../../../../util/helpers";
// import { useToggle } from "../../../../../util/hooks";
// import { getPm2Log } from "../../../../../util/query";
// import Button from "../../../../util/Button";
// import Icon from "../../../../util/Icon";
// import Grid from "../../../../util/layout/Grid";
// import CenterMenu from "../../../../util/menu/CenterMenu";
// import Selector from "../../../../util/menu/Selector";
// import s from "../stats.module.scss";
// const LogButton: Component<{ name: string }> = (p) => {
// const { selected } = useAppState();
// const [show, toggleShow] = useToggle();
// const [log, setLog] = createSignal<LogType>();
// const [lines, setLines] = createSignal(50);
// const load = () => {
// getPm2Log(selected.id(), p.name, lines()).then((cle) => setLog(cle.log));
// };
// return (
// <CenterMenu
// show={show}
// toggleShow={toggleShow}
// title={`${p.name} log`}
// target="show log"
// targetClass="blue"
// leftOfX={
// <>
// lines:
// <Selector
// targetClass="lightgrey"
// targetStyle={{ padding: "0.35rem" }}
// selected={lines().toString()}
// items={["50", "100", "500", "1000"]}
// onSelect={(lines) => setLines(Number(lines))}
// position="bottom right"
// itemStyle={{ width: "4rem" }}
// />
// <Button class="blue" onClick={async () => {
// const cle = await getPm2Log(selected.id(), p.name, lines());
// setLog(cle.log);
// pushNotification("good", "log reloaded");
// }}>
// <Icon type="refresh" />
// </Button>
// </>
// }
// content={<Log name={p.name} log={log} setLog={setLog} load={load} />}
// />
// );
// };
// const Log: Component<{
// name: string;
// log: Accessor<LogType | undefined>;
// setLog: (log: LogType) => void;
// load: () => void;
// }> = (p) => {
// createEffect(p.load);
// const { themeClass } = useTheme();
// return (
// <Grid
// gap="0.2rem"
// style={{ padding: "0.5rem", width: "80vw", height: "90vh" }}
// >
// <pre class={combineClasses(s.Pm2Log, "scroller", themeClass())}>
// {p.log()?.stdout}
// </pre>
// </Grid>
// );
// };
// export default LogButton;
import { Component } from "solid-js";
const LogButton: Component<{}> = (p) => {
return (
<div>
</div>
);
}
export default LogButton;

View File

@@ -1,127 +0,0 @@
// import { PM2Process } from "@monitor/types";
// import {
// Component,
// createEffect,
// createSignal,
// For,
// Match,
// Show,
// Switch,
// } from "solid-js";
// import { pushNotification } from "../../../../..";
// import { useAppState } from "../../../../../state/StateProvider";
// import { useTheme } from "../../../../../state/ThemeProvider";
// import { combineClasses } from "../../../../../util/helpers";
// import {
// getPm2Processes,
// startPm2Process,
// stopPm2Process,
// } from "../../../../../util/query";
// import Button from "../../../../util/Button";
// import Icon from "../../../../util/Icon";
// import Flex from "../../../../util/layout/Flex";
// import Grid from "../../../../util/layout/Grid";
// import Loading from "../../../../util/loading/Loading";
// import LogButton from "./Log";
// import s from "../stats.module.scss";
// import ConfirmButton from "../../../../util/ConfirmButton";
// const Pm2Processes: Component<{}> = (p) => {
// const { selected } = useAppState();
// const [pm2Proc, setPm2Proc] = createSignal<PM2Process[]>();
// const [refreshing, setRefreshing] = createSignal(false);
// const loadPm2 = () => {
// if (selected.id()) {
// try {
// getPm2Processes(selected.id()).then(setPm2Proc);
// } catch {}
// }
// };
// createEffect(loadPm2);
// const { themeClass } = useTheme();
// return (
// <Show when={pm2Proc() && pm2Proc()!.length > 0}>
// <Grid class={combineClasses(s.StatsContainer, themeClass())}>
// <Flex justifyContent="space-between" alignItems="center">
// <h1>pm2 processes</h1>
// <Button
// class="blue"
// onClick={async () => {
// setRefreshing(true);
// const processes = await getPm2Processes(selected.id());
// setPm2Proc(processes);
// setRefreshing(false);
// pushNotification("good", "processes refreshed");
// }}
// >
// <Show when={!refreshing()} fallback={<Loading />}>
// <Icon type="refresh" />
// </Show>
// </Button>
// </Flex>
// <Grid style={{ padding: "0.5rem" }}>
// <For each={pm2Proc()}>
// {(process) => (
// <Flex justifyContent="space-between" alignItems="center">
// <h2>{process.name}</h2>
// <Flex alignItems="center">
// <div>{process.status}</div>
// <div>cpu: {process.cpu}%</div>
// <div>
// mem:{" "}
// {process.memory
// ? `${process.memory / 1024000} mb`
// : "unknown"}
// </div>
// <Switch>
// <Match when={process.status === "online"}>
// <ConfirmButton
// color="orange"
// onConfirm={async () => {
// pushNotification("ok", `stopping ${process.name}`);
// await stopPm2Process(selected.id(), process.name!);
// pushNotification("good", `${process.name} stopped`);
// setTimeout(() => loadPm2(), 1000);
// }}
// >
// <Icon type="pause" />
// </ConfirmButton>
// </Match>
// <Match when={process.status === "stopped"}>
// <ConfirmButton
// color="green"
// onConfirm={async () => {
// pushNotification("ok", `starting ${process.name}`);
// await startPm2Process(selected.id(), process.name!);
// pushNotification("good", `${process.name} started`);
// setTimeout(() => loadPm2(), 1000);
// }}
// >
// <Icon type="play" />
// </ConfirmButton>
// </Match>
// </Switch>
// <LogButton name={process.name!} />
// </Flex>
// </Flex>
// )}
// </For>
// </Grid>
// </Grid>
// </Show>
// );
// };
// export default Pm2Processes;
import { Component } from "solid-js";
const Pm2Processes: Component<{}> = (p) => {
return (
<div>
</div>
);
}
export default Pm2Processes;

View File

@@ -1,28 +1,9 @@
@use "../../../../style/colors.scss" as c;
.StatsContainer {
max-height: 75vh;
height: fit-content;
margin: 0.5rem;
margin-top: 0rem;
background-color: rgba(c.$darkgrey, 0.6);
box-sizing: border-box;
position: relative;
padding: 0.5rem;
.Charts {
grid-template-columns: repeat(auto-fit, minmax(520px, 1fr));
}
.Stats {
padding: 0rem 1rem;
}
.Pm2Log {
white-space: pre-wrap;
overflow-wrap: anywhere;
word-wrap: break-word;
tab-size: 2;
width: 80vw;
box-sizing: border-box;
height: 87vh;
background-color: rgba(c.$darkgrey, 0.6);
padding: 1rem;
.LightweightChart {
background-color: c.$darkgrey;
}

View File

@@ -0,0 +1,64 @@
import { ColorType, createChart, IChartApi, ISeriesApi } from "lightweight-charts";
import { Component, createEffect, createSignal, JSX, onCleanup, onMount } from "solid-js";
type LinesData = {
color: string,
line: LineDataPoint[]
}
type LineDataPoint = {
time: number;
value: number;
};
const LightweightChart: Component<{ style?: JSX.CSSProperties, class?: string, lines?: () => LinesData[] }> = (p) => {
let el: HTMLDivElement;
const [chart, setChart] = createSignal<IChartApi>();
let lineSeries: ISeriesApi<"Line">[] = [];
const [loaded, setLoaded] = createSignal(false);
onMount(() => {
if (loaded()) return;
setLoaded(true);
const chart = createChart(el!, {
width: el!.clientWidth,
height: el!.clientHeight,
layout: {
background: { type: ColorType.Solid, color: "transparent" },
textColor: "white",
},
grid: {
horzLines: { color: "transparent" },
vertLines: { color: "transparent" },
},
timeScale: { timeVisible: true },
});
chart.timeScale().fitContent();
setChart(chart);
});
createEffect(() => {
if (chart() && p.lines) {
for (const series of lineSeries) {
chart()!.removeSeries(series);
}
const series = p.lines().map((line) => {
const series = chart()!.addLineSeries({ color: line.color });
series.setData(line.line as any);
return series;
});
lineSeries = series;
}
})
const handleResize = () => {
if (el && chart()) {
chart()!.applyOptions({ width: el.clientWidth });
}
};
addEventListener("resize", handleResize);
onCleanup(() => {
chart()?.remove();
removeEventListener("resize", handleResize);
})
return <div ref={el!} class={p.class} style={{ width: "100%", height: "100%", ...p.style }} />;
};
export default LightweightChart;

View File

@@ -35,6 +35,10 @@
background-color: c.$lightgrey;
}
.card.dark {
background-color: c.$darkgrey;
}
.content {
grid-template-columns: auto 1fr;
background-color: c.$darkgrey;

View File

@@ -17,6 +17,7 @@ import {
ServerActionState,
ServerWithStatus,
SystemStats,
SystemStatsQuery,
SystemStatsRecord,
Update,
UpdateTarget,
@@ -240,8 +241,8 @@ export class Client {
return this.patch("/api/server/update", server);
}
get_server_stats(server_id: string): Promise<SystemStats> {
return this.get(`/api/server/${server_id}/stats`);
get_server_stats(server_id: string, query?: SystemStatsQuery): Promise<SystemStats> {
return this.get(`/api/server/${server_id}/stats${generateQuery(query as any)}`);
}
get_server_stats_history(
@@ -249,7 +250,7 @@ export class Client {
query?: HistoricalStatsQuery
): Promise<SystemStatsRecord[]> {
return this.get(
`/api/server/${server_id}/history${generateQuery(query as any)}`
`/api/server/${server_id}/stats/history${generateQuery(query as any)}`
);
}
@@ -257,7 +258,7 @@ export class Client {
server_id: string,
ts: number
): Promise<SystemStatsRecord> {
return this.get(`/api/server/${server_id}/at_ts?ts=${ts}`);
return this.get(`/api/server/${server_id}/stats/at_ts?ts=${ts}`);
}
get_docker_networks(server_id: string): Promise<any[]> {

View File

@@ -20,8 +20,8 @@ export function generateQuery(query?: QueryObject) {
} else return "";
}
export function readableTimestamp(unixTimeInSecs: number) {
const date = new Date(unixTimeInSecs * 1000);
export function readableTimestamp(unix_time_ms: number) {
const date = new Date(unix_time_ms);
const hours24 = date.getHours();
let hours = hours24 % 12;
if (hours === 0) hours = 12;
@@ -52,6 +52,12 @@ export function readableDuration(start_ts: string, end_ts: string) {
return `${seconds} seconds`;
}
const tzOffset = new Date().getTimezoneOffset() * 60;
export function convertTsMsToLocalUnixTsInSecs(ts: number) {
return ts / 1000 - tzOffset;
}
export function validatePercentage(perc: string) {
// validates that a string represents a percentage
const percNum = Number(perc);

View File

@@ -628,6 +628,11 @@ escape-string-regexp@^1.0.5:
resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4"
integrity sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==
fancy-canvas@0.2.2:
version "0.2.2"
resolved "https://registry.yarnpkg.com/fancy-canvas/-/fancy-canvas-0.2.2.tgz#33fd4976724169a1eda5015f515a2a1302d1ec91"
integrity sha512-50qi8xA0QkHbjmb8h7XQ6k2fvD7y/yMfiUw9YTarJ7rWrq6o5/3CCXPouYk+XSLASvvxtjyiQLRBFt3qkE3oyA==
fill-range@^7.0.1:
version "7.0.1"
resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.0.1.tgz#1919a6a7c75fe38b2c7c77e5198535da9acdda40"
@@ -754,6 +759,13 @@ json5@^2.2.1:
resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.2.tgz#64471c5bdcc564c18f7c1d4df2e2297f2457c5ab"
integrity sha512-46Tk9JiOL2z7ytNQWFLpj99RZkVgeHf87yGQKsIkaPz1qSH9UczKH1rO7K3wgRselo0tYMUNfecYpm/p1vC7tQ==
lightweight-charts@^3.8.0:
version "3.8.0"
resolved "https://registry.yarnpkg.com/lightweight-charts/-/lightweight-charts-3.8.0.tgz#8c41ad7c1c083f18621f11ece7fc1096e131a0d3"
integrity sha512-7yFGnYuE1RjRJG9RwUTBz5wvF1QtjBOSW4FFlikr8Dh+/TDNt4ci+HsWSYmStgQUpawpvkCJ3j5/W25GppGj9Q==
dependencies:
fancy-canvas "0.2.2"
lru-cache@^5.1.1:
version "5.1.1"
resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-5.1.1.tgz#1da27e6710271947695daf6848e847f01d84b920"

View File

@@ -246,8 +246,11 @@ pub struct SystemStatsRecord {
pub mem_used_gb: f64, // in GB
pub mem_total_gb: f64, // in GB
pub disk: DiskUsage,
#[serde(default)]
pub networks: Vec<SystemNetwork>,
#[serde(default)]
pub components: Vec<SystemComponent>,
#[serde(default)]
pub processes: Vec<SystemProcess>,
pub polling_rate: Timelength,
}