make standalone stats page

This commit is contained in:
mbecker20
2023-01-08 07:50:47 +00:00
parent e18cd2eebb
commit af948edbea
13 changed files with 480 additions and 322 deletions
+2
View File
@@ -1,5 +1,6 @@
import { Route, Routes } from "@solidjs/router";
import { Component, lazy, Show } from "solid-js";
import Stats from "./components/stats/Stats";
import Topbar from "./components/topbar/Topbar";
import { useUser } from "./state/UserProvider";
@@ -19,6 +20,7 @@ const App: Component = () => {
<Route path="/build/:id" component={Build} />
<Route path="/deployment/:id" component={Deployment} />
<Route path="/server/:id" component={Server} />
<Route path="/server/:id/stats" component={Stats} />
<Show when={user().admin}>
<Route path="/users" component={Users} />
</Show>
+4 -2
View File
@@ -10,7 +10,7 @@ import Deployment from "./Deployment";
import s from "../home.module.scss";
import { NewBuild, NewDeployment } from "./New";
import Loading from "../../shared/loading/Loading";
import { useNavigate } from "@solidjs/router";
import { A, useNavigate } from "@solidjs/router";
import { PermissionLevel, ServerStatus } from "../../../types";
import { useAppDimensions } from "../../../state/DimensionProvider";
import Build from "./Build";
@@ -101,7 +101,9 @@ const Server: Component<{ id: string }> = (p) => {
<Icon type="refresh" width="0.85rem" />
</button>
</Show>
{/* <StatGraphs id={p.id} /> */}
<A href={`/server/${p.id}/stats`} class="blue">
<Icon type="timeline-line-chart" />
</A>
</Flex>
</Show>
</Show>
@@ -7,7 +7,6 @@ import { Tab } from "../../shared/tabs/Tabs";
import Config from "./config/Config";
import { ConfigProvider } from "./config/Provider";
import Owners from "./Owners";
import Stats from "./stats/Stats";
const ServerTabs: Component<{}> = (p) => {
const { servers } = useAppState();
@@ -25,10 +24,6 @@ const ServerTabs: Component<{}> = (p) => {
title: "config",
element: () => <Config />,
},
{
title: "stats",
element: () => <Stats />,
},
user().admin && {
title: "collaborators",
element: () => <Owners />,
@@ -1,300 +0,0 @@
import { useParams } from "@solidjs/router";
import {
Accessor,
Component,
createEffect,
createSignal,
Show,
} from "solid-js";
import { client } from "../../../..";
import { SystemStats, SystemStatsRecord, Timelength } from "../../../../types";
import {
convertTsMsToLocalUnixTsInSecs,
get_to_one_sec_divisor,
} from "../../../../util/helpers";
import { useLocalStorage } from "../../../../util/hooks";
import Icon from "../../../shared/Icon";
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,
Timelength.FiveMinutes,
Timelength.FifteenMinutes,
Timelength.OneHour,
Timelength.SixHours,
Timelength.TwelveHours,
Timelength.OneDay,
];
const Stats: Component<{}> = (p) => {
const params = useParams();
const [timelength, setTimelength] = useLocalStorage(
Timelength.OneMinute,
"server-stats-timelength-v3"
);
const [currStats, setCurrStats] = createSignal<SystemStats>();
const [loadingCurr, setLoadingCurr] = createSignal(false);
const [stats, setStats] = createSignal<SystemStatsRecord[]>();
const [page, setPage] = createSignal(0);
const load_curr_stats = () => {
setLoadingCurr(true);
client.get_server_stats(params.id).then((stats) => {
setCurrStats(stats);
setLoadingCurr(false);
});
};
createEffect(() => {
client
.get_server_stats_history(params.id, {
interval: timelength(),
page: page(),
limit: 1000,
networks: true,
components: true,
})
.then(setStats);
});
createEffect(() => {
load_curr_stats();
})
// 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:
<div>{currStats()!.mem_total_gb.toFixed(1)} GB</div>
<h2>
{(
(100 * currStats()!.mem_used_gb) /
currStats()!.mem_total_gb
).toFixed(1)}
% full
</h2>
</Grid>
<Grid gap="0" placeItems="start center">
disk:
<div>{currStats()!.disk.total_gb.toFixed(1)} GB</div>
<h2>
{(
(100 * currStats()!.disk.used_gb) /
currStats()!.disk.total_gb
).toFixed(1)}
% full
</h2>
</Grid>
<button class="blue" onClick={load_curr_stats}>
<Show when={!loadingCurr()} fallback={<Loading />}>
<Icon type="refresh" />
</Show>
</button>
</Show>
</Flex>
<Flex class="card light shadow" alignItems="center">
<button class="darkgrey" onClick={() => {
setPage(page => page + 1);
}}>
<Icon type="chevron-left" />
</button>
<button class="darkgrey" onClick={() => {
setPage(page => page > 0 ? page - 1 : 0);
}}>
<Icon type="chevron-right" />
</button>
<button class="darkgrey" onClick={() => {
setPage(0)
}}>
<Icon type="double-chevron-right" />
</button>
<div>page: {page() + 1}</div>
</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} />
<NetworkChart 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={() => [{ title: "%", 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={() => [{ title: selected(), 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={() => [{ title: selected(), color: "#184e9f", line: line()! }]}
/>
</Grid>
</Show>
);
};
const NetworkChart: Component<{
stats: Accessor<SystemStatsRecord[] | undefined>;
}> = (p) => {
const recv_line = () => {
return p.stats()?.map((s) => {
return {
time: convertTsMsToLocalUnixTsInSecs(s.ts),
value:
s.networks?.length || 0 > 0
? s.networks!.map((n) => n.recieved_kb).reduce((p, c) => p + c) /
get_to_one_sec_divisor(s.polling_rate)!
: 0,
};
});
};
const trans_line = () => {
return p.stats()?.map((s) => {
return {
time: convertTsMsToLocalUnixTsInSecs(s.ts),
value:
s.networks?.length || 0 > 0
? s.networks!.map((n) => n.transmitted_kb).reduce((p, c) => p + c) /
get_to_one_sec_divisor(s.polling_rate)!
: 0,
};
});
};
return (
<Show when={recv_line()}>
<Grid gap="0" class="card dark shadow" style={{ height: "fit-content" }}>
<Flex alignItems="center" justifyContent="space-between">
<h2>network kb/s</h2>
</Flex>
<LightweightChart
class={s.LightweightChart}
style={{ height: "200px" }}
lines={() => [
{ title: "recv", color: "#41764c", line: recv_line()! },
{ title: "send", color: "#952E23", line: trans_line()! },
]}
/>
</Grid>
</Show>
);
};
@@ -1,9 +0,0 @@
@use "../../../../style/colors.scss" as c;
.Charts {
grid-template-columns: repeat(auto-fit, minmax(520px, 1fr));
}
.LightweightChart {
background-color: c.$darkgrey;
}
@@ -45,10 +45,12 @@ const LightweightChart: Component<{
textColor: "white",
},
grid: {
horzLines: { color: "transparent" },
vertLines: { color: "transparent" },
horzLines: { color: "#3f454d" },
vertLines: { color: "#3f454d" },
},
timeScale: { timeVisible: true },
// handleScroll: false,
// handleScale: false,
});
chart.timeScale().fitContent();
setChart(chart);
@@ -0,0 +1,67 @@
import { Params, useParams } from "@solidjs/router";
import ReconnectingWebSocket from "reconnecting-websocket";
import { Component, createEffect, createSignal, Setter } from "solid-js";
import { client, URL } from "../..";
import { SystemStats } from "../../types";
import { generateQuery } from "../../util/helpers";
import Flex from "../shared/layout/Flex";
import Grid from "../shared/layout/Grid";
import s from "./stats.module.scss";
const CurrentStats: Component<{}> = (p) => {
const params = useParams();
const [stats, setStats] = createSignal<SystemStats>();
const open = useStatsWs(params, setStats);
createEffect(() => {
client
.get_server_stats(params.id, {
networks: true,
components: true,
processes: true,
})
.then(setStats);
});
return (
<Grid class={s.Content}>
<Flex>
<div>cpu:</div>
<h2>{}</h2>
</Flex>
</Grid>
);
};
export default CurrentStats;
function useStatsWs(params: Params, setStats: Setter<SystemStats>) {
const ws = new ReconnectingWebSocket(
`${URL.replace("http", "ws")}/ws/stats/${params.id}${generateQuery({
networks: "true",
components: "true",
processes: "true",
})}`
);
const [open, setOpen] = createSignal(false);
ws.addEventListener("open", () => {
// console.log("connection opened");
ws.send(client.token!);
setOpen(true);
});
ws.addEventListener("message", ({ data }) => {
if (data === "LOGGED_IN") {
console.log("logged in to ws");
return;
}
const stats = JSON.parse(data) as SystemStats;
console.log(stats);
setStats(stats);
});
ws.addEventListener("close", () => {
console.log("stats connection closed");
// clearInterval(int);
setOpen(false);
});
return {
open,
};
}
@@ -0,0 +1,331 @@
import { useParams } from "@solidjs/router";
import { Accessor, Component, createEffect, createSignal, For, Match, Show, Switch } from "solid-js";
import { client } from "../..";
import { SystemStatsRecord, Timelength } from "../../types";
import { convertTsMsToLocalUnixTsInSecs, get_to_one_sec_divisor } from "../../util/helpers";
import { useLocalStorage } from "../../util/hooks";
import Icon from "../shared/Icon";
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,
Timelength.FiveMinutes,
Timelength.FifteenMinutes,
Timelength.OneHour,
Timelength.SixHours,
Timelength.TwelveHours,
Timelength.OneDay,
];
const COLORS = {
blue: "#184e9f",
orange: "#ac5c36",
purple: "#5A0B4D",
green: "#41764c",
red: "#952E23",
};
const VIEWS = [
"basic",
"i/o",
"temp"
];
const HistoricalStats: Component<{}> = (p) => {
const params = useParams();
const [timelength, setTimelength] = useLocalStorage(
Timelength.OneMinute,
"server-stats-timelength-v3"
);
const [view, setView] = useLocalStorage("basic", "historical-stats-view-v1")
const [stats, setStats] = createSignal<SystemStatsRecord[]>();
const [page, setPage] = createSignal(0);
createEffect(() => {
client
.get_server_stats_history(params.id, {
interval: timelength(),
page: page(),
limit: 1000,
networks: true,
components: true,
})
.then(setStats);
});
return (
<Grid class={s.Content}>
<Flex alignItems="center">
<Flex class="card light shadow" alignItems="center">
<button
class="darkgrey"
onClick={() => {
setPage((page) => page + 1);
}}
>
<Icon type="chevron-left" />
</button>
<button
class="darkgrey"
onClick={() => {
setPage((page) => (page > 0 ? page - 1 : 0));
}}
>
<Icon type="chevron-right" />
</button>
<button
class="darkgrey"
onClick={() => {
setPage(0);
}}
>
<Icon type="double-chevron-right" />
</button>
<div>page: {page() + 1}</div>
</Flex>
<Selector
targetClass="grey"
selected={timelength()}
items={TIMELENGTHS}
onSelect={(selected) => {
setPage(0);
setTimelength(selected as Timelength);
}}
/>
<Selector
targetClass="grey"
selected={view()}
items={VIEWS}
onSelect={(selected) => {
setView(selected);
}}
/>
</Flex>
<Show when={stats()} fallback={<Loading type="three-dot" />}>
<Switch>
<Match when={view() === "basic"}>
<Grid class={s.Charts}>
<CpuChart stats={stats} />
<MemChart stats={stats} />
<DiskChart stats={stats} />
</Grid>
</Match>
<Match when={view() === "i/o"}>
<Grid class={s.Charts}>
<NetworkChart stats={stats} />
</Grid>
</Match>
<Match when={view() === "temp"}>
<Grid class={s.Charts}>
<TempuratureChart stats={stats} />
</Grid>
</Match>
</Switch>
</Show>
</Grid>
);
};
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 shadow" style={{ height: "fit-content" }}>
<h2>cpu</h2>
<LightweightChart
class={s.LightweightChart}
style={{ height: "200px" }}
lines={() => [{ title: "%", color: COLORS.blue, 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 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={() => [{ title: selected(), color: COLORS.blue, 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 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={() => [{ title: selected(), color: "#184e9f", line: line()! }]}
/>
</Grid>
</Show>
);
};
const NetworkChart: Component<{
stats: Accessor<SystemStatsRecord[] | undefined>;
}> = (p) => {
const recv_line = () => {
return p.stats()?.map((s) => {
return {
time: convertTsMsToLocalUnixTsInSecs(s.ts),
value:
s.networks?.length || 0 > 0
? s.networks!.map((n) => n.recieved_kb).reduce((p, c) => p + c) /
get_to_one_sec_divisor(s.polling_rate)!
: 0,
};
});
};
const trans_line = () => {
return p.stats()?.map((s) => {
return {
time: convertTsMsToLocalUnixTsInSecs(s.ts),
value:
s.networks?.length || 0 > 0
? s.networks!.map((n) => n.transmitted_kb).reduce((p, c) => p + c) /
get_to_one_sec_divisor(s.polling_rate)!
: 0,
};
});
};
return (
<Show when={recv_line()}>
<Grid gap="0" class="card shadow" style={{ height: "fit-content" }}>
<Flex alignItems="center" justifyContent="space-between">
<h2>network sent kb/s</h2>
</Flex>
<LightweightChart
class={s.LightweightChart}
style={{ height: "200px" }}
lines={() => [
{ title: "kb/s", color: "#184e9f", line: trans_line()! },
]}
/>
</Grid>
<Grid gap="0" class="card shadow" style={{ height: "fit-content" }}>
<Flex alignItems="center" justifyContent="space-between">
<h2>network received kb/s</h2>
</Flex>
<LightweightChart
class={s.LightweightChart}
style={{ height: "200px" }}
lines={() => [
{ title: "kb/s", color: "#184e9f", line: recv_line()! },
]}
/>
</Grid>
</Show>
);
};
const TempuratureChart: Component<{
stats: Accessor<SystemStatsRecord[] | undefined>;
}> = (p) => {
// const [selected, setSelected] = createSignal(p.stats()![p.stats()!.length - 1].components![0].label);
const labels = () => {
return p.stats()![p.stats()!.length - 1].components!.map((c) => c.label);
};
const line = (component: string) => {
return p.stats()?.map((s) => {
const temp = s.components!.find((c) => c.label === component)?.temp;
return {
time: convertTsMsToLocalUnixTsInSecs(s.ts),
value: temp || 0,
};
});
};
return (
<For each={labels()}>
{(label) => (
<Grid
gap="0"
class="card shadow"
style={{ height: "fit-content" }}
>
<Flex alignItems="center" justifyContent="space-between">
<h2>{label}</h2>
{/* <Selector
selected={selected()}
items={labels()}
onSelect={setSelected}
/> */}
</Flex>
<LightweightChart
class={s.LightweightChart}
style={{ height: "200px" }}
lines={() => [
{ title: "temp", color: "#184e9f", line: line(label)! },
]}
/>
</Grid>
)}
</For>
);
};
export default HistoricalStats;
+53
View File
@@ -0,0 +1,53 @@
import { useParams } from "@solidjs/router";
import { Component, Match, Switch } from "solid-js";
import { useAppState } from "../../state/StateProvider";
import { useLocalStorage } from "../../util/hooks";
import Flex from "../shared/layout/Flex";
import Grid from "../shared/layout/Grid";
import Selector from "../shared/menu/Selector";
import CurrentStats from "./CurrentStats";
import HistoricalStats from "./HistoricalStats";
import s from "./stats.module.scss";
const VIEWS = [
"current",
"historical"
]
const Stats: Component<{}> = (p) => {
const [view, setView] = useLocalStorage("current", "stats-view-v1");
return (
<Grid class={s.Content}>
<Flex alignItems="center">
<Header />
<Selector
targetClass="grey"
selected={view()}
items={VIEWS}
onSelect={setView}
/>
</Flex>
<Switch>
<Match when={view() === "current"}>
<CurrentStats />
</Match>
<Match when={view() === "historical"}>
<HistoricalStats />
</Match>
</Switch>
</Grid>
);
};
export const Header = () => {
const { servers } = useAppState();
const params = useParams();
const server = () => servers.get(params.id);
return (
<Grid gap="0.1rem">
<h1>{server()?.server.name} - system stats</h1>
</Grid>
);
}
export default Stats;
@@ -0,0 +1,15 @@
@use "../../style/colors.scss" as c;
.Charts {
grid-template-columns: repeat(auto-fit, minmax(520px, 1fr));
}
.LightweightChart {
background-color: c.$grey;
}
.Content {
width: 100%;
height: fit-content;
box-sizing: border-box;
}
+1 -1
View File
@@ -19,7 +19,7 @@ export const URL =
? location.origin
: (import.meta.env.VITE_MONITOR_HOST as string) || "http://localhost:9000";
export const WS_URL = URL.replace("http", "ws") + "/ws/update";
export const UPDATE_WS_URL = URL.replace("http", "ws") + "/ws/update";
const token =
(import.meta.env.VITE_ACCESS_TOKEN as string) ||
+2 -2
View File
@@ -1,11 +1,11 @@
import { client, pushNotification, WS_URL } from "..";
import { client, pushNotification, UPDATE_WS_URL } from "..";
import { State } from "./StateProvider";
import { createSignal } from "solid-js";
import ReconnectingWebSocket from "reconnecting-websocket";
import { Operation, Update, UpdateStatus, UpdateTarget } from "../types";
function connectToWs(state: State) {
const ws = new ReconnectingWebSocket(WS_URL);
const ws = new ReconnectingWebSocket(UPDATE_WS_URL);
const [isOpen, setOpen] = createSignal(false);
+1 -1
View File
@@ -28,7 +28,7 @@
.card {
background-color: c.$grey;
padding: 0.5rem;
padding: 1rem;
}
.card.light {