mirror of
https://github.com/moghtech/komodo.git
synced 2026-04-28 11:49:39 -05:00
server stats page
This commit is contained in:
@@ -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")
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
}
|
||||
64
frontend/src/components/shared/LightweightChart.tsx
Normal file
64
frontend/src/components/shared/LightweightChart.tsx
Normal 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;
|
||||
@@ -35,6 +35,10 @@
|
||||
background-color: c.$lightgrey;
|
||||
}
|
||||
|
||||
.card.dark {
|
||||
background-color: c.$darkgrey;
|
||||
}
|
||||
|
||||
.content {
|
||||
grid-template-columns: auto 1fr;
|
||||
background-color: c.$darkgrey;
|
||||
|
||||
@@ -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[]> {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user