This commit is contained in:
@ -147,9 +147,9 @@ export class LNVpsApi {
|
||||
}
|
||||
|
||||
async getVmTimeSeries(id: number) {
|
||||
const { data } = await this.#handleResponse<ApiResponse<Array<TimeSeriesData>>>(
|
||||
await this.#req(`/api/v1/vm/${id}/time-series`, "GET"),
|
||||
);
|
||||
const { data } = await this.#handleResponse<
|
||||
ApiResponse<Array<TimeSeriesData>>
|
||||
>(await this.#req(`/api/v1/vm/${id}/time-series`, "GET"));
|
||||
return data;
|
||||
}
|
||||
|
||||
|
@ -61,7 +61,7 @@ const router = createBrowserRouter([
|
||||
},
|
||||
{
|
||||
path: "/vm/graphs",
|
||||
element: <VmGraphsPage />
|
||||
element: <VmGraphsPage />,
|
||||
},
|
||||
{
|
||||
path: "/tos",
|
||||
|
@ -32,7 +32,7 @@ export function VmBillingPage() {
|
||||
|
||||
useEffect(() => {
|
||||
if (params["action"] === "renew" && login && state) {
|
||||
renew()
|
||||
renew();
|
||||
}
|
||||
}, [login, state, params, renew]);
|
||||
|
||||
@ -54,8 +54,9 @@ export function VmBillingPage() {
|
||||
Expires: {expireDate.toDateString()} ({Math.floor(days)} days)
|
||||
</div>
|
||||
)}
|
||||
{days < 0 && params["action"] !== "renew"
|
||||
&& <div className="text-red-500 text-xl">Expired</div>}
|
||||
{days < 0 && params["action"] !== "renew" && (
|
||||
<div className="text-red-500 text-xl">Expired</div>
|
||||
)}
|
||||
{!payment && (
|
||||
<div>
|
||||
<AsyncButton onClick={renew}>Extend Now</AsyncButton>
|
||||
|
@ -2,7 +2,15 @@ import { Link, useLocation } from "react-router-dom";
|
||||
import { TimeSeriesData, VmInstance } from "../api";
|
||||
import useLogin from "../hooks/login";
|
||||
import { useEffect, useState } from "react";
|
||||
import { ResponsiveContainer, XAxis, YAxis, Tooltip, LineChart, Line, Legend } from "recharts";
|
||||
import {
|
||||
ResponsiveContainer,
|
||||
XAxis,
|
||||
YAxis,
|
||||
Tooltip,
|
||||
LineChart,
|
||||
Line,
|
||||
Legend,
|
||||
} from "recharts";
|
||||
|
||||
export function VmGraphsPage() {
|
||||
const { state } = useLocation() as { state?: VmInstance };
|
||||
@ -14,7 +22,8 @@ export function VmGraphsPage() {
|
||||
login?.api.getVmTimeSeries(state.id).then(setData);
|
||||
}, [login]);
|
||||
|
||||
const maxRam = data?.reduce((acc, v) => {
|
||||
const maxRam =
|
||||
data?.reduce((acc, v) => {
|
||||
const mb = v.memory_size / 1024 / 1024;
|
||||
return acc < mb ? mb : acc;
|
||||
}, 0) ?? 0;
|
||||
@ -23,12 +32,15 @@ export function VmGraphsPage() {
|
||||
const MB = 1024 * 1024;
|
||||
function scaleLabel(v: number) {
|
||||
switch (net_scale) {
|
||||
case MB: return "MiB";
|
||||
case KB: return "KiB";
|
||||
case MB:
|
||||
return "MiB";
|
||||
case KB:
|
||||
return "KiB";
|
||||
}
|
||||
return "B";
|
||||
}
|
||||
const net_scale = data?.reduce((acc, v) => {
|
||||
const net_scale =
|
||||
data?.reduce((acc, v) => {
|
||||
const b = Math.max(v.net_in, v.net_out);
|
||||
if (b > MB && b > acc) {
|
||||
return MB;
|
||||
@ -39,7 +51,8 @@ export function VmGraphsPage() {
|
||||
}
|
||||
}, 0) ?? 0;
|
||||
const net_scale_label = scaleLabel(net_scale);
|
||||
const disk_scale = data?.reduce((acc, v) => {
|
||||
const disk_scale =
|
||||
data?.reduce((acc, v) => {
|
||||
const b = Math.max(v.disk_read, v.disk_write);
|
||||
if (b > MB && b > acc) {
|
||||
return MB;
|
||||
@ -61,24 +74,39 @@ export function VmGraphsPage() {
|
||||
DISK_READ: v.disk_read / disk_scale,
|
||||
DISK_WRITE: v.disk_write / disk_scale,
|
||||
}));
|
||||
const toolTip = <Tooltip cursor={{ fill: "rgba(200,200,200,0.5)" }}
|
||||
const toolTip = (
|
||||
<Tooltip
|
||||
cursor={{ fill: "rgba(200,200,200,0.5)" }}
|
||||
content={({ active, payload }) => {
|
||||
if (active && payload && payload.length) {
|
||||
const data = payload[0].payload as TimeSeriesData;
|
||||
return <div className="flex flex-col gap-2 bg-neutral-700 rounded-xl px-2 py-3">
|
||||
return (
|
||||
<div className="flex flex-col gap-2 bg-neutral-700 rounded-xl px-2 py-3">
|
||||
<div>{data.timestamp}</div>
|
||||
{payload.map((p) => <div>{p.name}: {Number(p.value).toFixed(2)}{p.unit}</div>)}
|
||||
|
||||
</div>;
|
||||
{payload.map((p) => (
|
||||
<div>
|
||||
{p.name}: {Number(p.value).toFixed(2)}
|
||||
{p.unit}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}} />;
|
||||
return <div className="flex flex-col gap-4">
|
||||
}}
|
||||
/>
|
||||
);
|
||||
return (
|
||||
<div className="flex flex-col gap-4">
|
||||
<Link to={"/vm"} state={state}>
|
||||
< Back
|
||||
</Link>
|
||||
<h2>CPU</h2>
|
||||
<ResponsiveContainer height={200}>
|
||||
<LineChart data={sortedData} margin={{ left: 0, right: 0 }} style={{ userSelect: "none" }}>
|
||||
<LineChart
|
||||
data={sortedData}
|
||||
margin={{ left: 0, right: 0 }}
|
||||
style={{ userSelect: "none" }}
|
||||
>
|
||||
<XAxis dataKey="timestamp" />
|
||||
<YAxis unit="%" domain={[0, 100]} />
|
||||
<Line type="monotone" dataKey="CPU" unit="%" dot={false} />
|
||||
@ -87,7 +115,11 @@ export function VmGraphsPage() {
|
||||
</ResponsiveContainer>
|
||||
<h2>Memory</h2>
|
||||
<ResponsiveContainer height={200}>
|
||||
<LineChart data={sortedData} margin={{ left: 0, right: 0 }} style={{ userSelect: "none" }}>
|
||||
<LineChart
|
||||
data={sortedData}
|
||||
margin={{ left: 0, right: 0 }}
|
||||
style={{ userSelect: "none" }}
|
||||
>
|
||||
<XAxis dataKey="timestamp" />
|
||||
<YAxis unit="MB" domain={[0, maxRam]} />
|
||||
<Line type="monotone" dataKey="RAM" unit="MB" dot={false} />
|
||||
@ -96,26 +128,58 @@ export function VmGraphsPage() {
|
||||
</ResponsiveContainer>
|
||||
<h2>Network</h2>
|
||||
<ResponsiveContainer height={200}>
|
||||
<LineChart data={sortedData} margin={{ left: 20, right: 0 }} style={{ userSelect: "none" }}>
|
||||
<LineChart
|
||||
data={sortedData}
|
||||
margin={{ left: 20, right: 0 }}
|
||||
style={{ userSelect: "none" }}
|
||||
>
|
||||
<XAxis dataKey="timestamp" />
|
||||
<YAxis unit={`${net_scale_label}/s`} domain={[0, "auto"]} />
|
||||
<Line type="monotone" dataKey="NET_IN" unit={`${net_scale_label}/s`} stroke="red" dot={false} />
|
||||
<Line type="monotone" dataKey="NET_OUT" unit={`${net_scale_label}/s`} stroke="green" dot={false} />
|
||||
<Line
|
||||
type="monotone"
|
||||
dataKey="NET_IN"
|
||||
unit={`${net_scale_label}/s`}
|
||||
stroke="red"
|
||||
dot={false}
|
||||
/>
|
||||
<Line
|
||||
type="monotone"
|
||||
dataKey="NET_OUT"
|
||||
unit={`${net_scale_label}/s`}
|
||||
stroke="green"
|
||||
dot={false}
|
||||
/>
|
||||
{toolTip}
|
||||
<Legend />
|
||||
</LineChart>
|
||||
</ResponsiveContainer>
|
||||
<h2>Disk</h2>
|
||||
<ResponsiveContainer height={200}>
|
||||
<LineChart data={sortedData} margin={{ left: 20, right: 0 }} style={{ userSelect: "none" }}>
|
||||
<LineChart
|
||||
data={sortedData}
|
||||
margin={{ left: 20, right: 0 }}
|
||||
style={{ userSelect: "none" }}
|
||||
>
|
||||
<XAxis dataKey="timestamp" />
|
||||
<YAxis unit={`${disk_scale_label}/s`} domain={[0, "auto"]} />
|
||||
<Line type="monotone" dataKey="DISK_READ" unit={`${disk_scale_label}/s`} stroke="red" dot={false} />
|
||||
<Line type="monotone" dataKey="DISK_WRITE" unit={`${disk_scale_label}/s`} stroke="green" dot={false} />
|
||||
<Line
|
||||
type="monotone"
|
||||
dataKey="DISK_READ"
|
||||
unit={`${disk_scale_label}/s`}
|
||||
stroke="red"
|
||||
dot={false}
|
||||
/>
|
||||
<Line
|
||||
type="monotone"
|
||||
dataKey="DISK_WRITE"
|
||||
unit={`${disk_scale_label}/s`}
|
||||
stroke="green"
|
||||
dot={false}
|
||||
/>
|
||||
{toolTip}
|
||||
<Legend />
|
||||
</LineChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
|
||||
);
|
||||
}
|
Reference in New Issue
Block a user