chore: formatting
Some checks failed
continuous-integration/drone/push Build is failing

This commit is contained in:
2025-03-05 16:26:47 +00:00
parent 7ba2659fbf
commit c6e4a9e3c9
4 changed files with 246 additions and 181 deletions

View File

@ -116,7 +116,7 @@ export class LNVpsApi {
constructor( constructor(
readonly url: string, readonly url: string,
readonly publisher: EventPublisher | undefined, readonly publisher: EventPublisher | undefined,
) { } ) {}
async getAccount() { async getAccount() {
const { data } = await this.#handleResponse<ApiResponse<AccountDetail>>( const { data } = await this.#handleResponse<ApiResponse<AccountDetail>>(
@ -147,9 +147,9 @@ export class LNVpsApi {
} }
async getVmTimeSeries(id: number) { async getVmTimeSeries(id: number) {
const { data } = await this.#handleResponse<ApiResponse<Array<TimeSeriesData>>>( const { data } = await this.#handleResponse<
await this.#req(`/api/v1/vm/${id}/time-series`, "GET"), ApiResponse<Array<TimeSeriesData>>
); >(await this.#req(`/api/v1/vm/${id}/time-series`, "GET"));
return data; return data;
} }

View File

@ -61,7 +61,7 @@ const router = createBrowserRouter([
}, },
{ {
path: "/vm/graphs", path: "/vm/graphs",
element: <VmGraphsPage /> element: <VmGraphsPage />,
}, },
{ {
path: "/tos", path: "/tos",

View File

@ -7,76 +7,77 @@ import { AsyncButton } from "../components/button";
import CostLabel from "../components/cost"; import CostLabel from "../components/cost";
export function VmBillingPage() { export function VmBillingPage() {
const location = useLocation() as { state?: VmInstance }; const location = useLocation() as { state?: VmInstance };
const params = useParams(); const params = useParams();
const login = useLogin(); const login = useLogin();
const navigate = useNavigate(); const navigate = useNavigate();
const [payment, setPayment] = useState<VmPayment>(); const [payment, setPayment] = useState<VmPayment>();
const [state, setState] = useState<VmInstance | undefined>(location?.state); const [state, setState] = useState<VmInstance | undefined>(location?.state);
async function reloadVmState() {
if (!state) return;
const newState = await login?.api.getVm(state.id);
setState(newState);
return newState;
}
const renew = useCallback(
async function () {
if (!login?.api || !state) return;
const p = await login?.api.renewVm(state.id);
setPayment(p);
},
[login?.api, state],
);
useEffect(() => {
if (params["action"] === "renew" && login && state) {
renew()
}
}, [login, state, params, renew]);
async function reloadVmState() {
if (!state) return; if (!state) return;
const expireDate = new Date(state.expires); const newState = await login?.api.getVm(state.id);
const days = setState(newState);
(expireDate.getTime() - new Date().getTime()) / 1000 / 24 / 60 / 60; return newState;
return ( }
<div className="flex flex-col gap-4">
<Link to={"/vm"} state={state}> const renew = useCallback(
&lt; Back async function () {
</Link> if (!login?.api || !state) return;
<div className="text-xl bg-neutral-900 rounded-xl px-3 py-4 flex justify-between items-center"> const p = await login?.api.renewVm(state.id);
<div>Renewal for #{state.id}</div> setPayment(p);
<CostLabel cost={state.template.cost_plan} /> },
</div> [login?.api, state],
{days > 0 && ( );
<div>
Expires: {expireDate.toDateString()} ({Math.floor(days)} days) useEffect(() => {
</div> if (params["action"] === "renew" && login && state) {
)} renew();
{days < 0 && params["action"] !== "renew" }
&& <div className="text-red-500 text-xl">Expired</div>} }, [login, state, params, renew]);
{!payment && (
<div> if (!state) return;
<AsyncButton onClick={renew}>Extend Now</AsyncButton> const expireDate = new Date(state.expires);
</div> const days =
)} (expireDate.getTime() - new Date().getTime()) / 1000 / 24 / 60 / 60;
{payment && ( return (
<> <div className="flex flex-col gap-4">
<h3>Renew VPS</h3> <Link to={"/vm"} state={state}>
<VpsPayment &lt; Back
payment={payment} </Link>
onPaid={async () => { <div className="text-xl bg-neutral-900 rounded-xl px-3 py-4 flex justify-between items-center">
setPayment(undefined); <div>Renewal for #{state.id}</div>
if (!login?.api || !state) return; <CostLabel cost={state.template.cost_plan} />
const s = await reloadVmState(); </div>
if (params["action"] === "renew") { {days > 0 && (
navigate("/vm", { state: s }); <div>
} Expires: {expireDate.toDateString()} ({Math.floor(days)} days)
}}
/>
</>
)}
</div> </div>
); )}
{days < 0 && params["action"] !== "renew" && (
<div className="text-red-500 text-xl">Expired</div>
)}
{!payment && (
<div>
<AsyncButton onClick={renew}>Extend Now</AsyncButton>
</div>
)}
{payment && (
<>
<h3>Renew VPS</h3>
<VpsPayment
payment={payment}
onPaid={async () => {
setPayment(undefined);
if (!login?.api || !state) return;
const s = await reloadVmState();
if (params["action"] === "renew") {
navigate("/vm", { state: s });
}
}}
/>
</>
)}
</div>
);
} }

View File

@ -2,120 +2,184 @@ import { Link, useLocation } from "react-router-dom";
import { TimeSeriesData, VmInstance } from "../api"; import { TimeSeriesData, VmInstance } from "../api";
import useLogin from "../hooks/login"; import useLogin from "../hooks/login";
import { useEffect, useState } from "react"; 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() { export function VmGraphsPage() {
const { state } = useLocation() as { state?: VmInstance }; const { state } = useLocation() as { state?: VmInstance };
const login = useLogin(); const login = useLogin();
const [data, setData] = useState<Array<TimeSeriesData>>(); const [data, setData] = useState<Array<TimeSeriesData>>();
useEffect(() => { useEffect(() => {
if (!state) return; if (!state) return;
login?.api.getVmTimeSeries(state.id).then(setData); login?.api.getVmTimeSeries(state.id).then(setData);
}, [login]); }, [login]);
const maxRam = data?.reduce((acc, v) => { const maxRam =
const mb = v.memory_size / 1024 / 1024; data?.reduce((acc, v) => {
return acc < mb ? mb : acc; const mb = v.memory_size / 1024 / 1024;
return acc < mb ? mb : acc;
}, 0) ?? 0; }, 0) ?? 0;
const KB = 1024; const KB = 1024;
const MB = 1024 * 1024; const MB = 1024 * 1024;
function scaleLabel(v: number) { function scaleLabel(v: number) {
switch (net_scale) { switch (net_scale) {
case MB: return "MiB"; case MB:
case KB: return "KiB"; return "MiB";
} case KB:
return "B"; return "KiB";
} }
const net_scale = data?.reduce((acc, v) => { return "B";
const b = Math.max(v.net_in, v.net_out); }
if (b > MB && b > acc) { const net_scale =
return MB; data?.reduce((acc, v) => {
} else if (b > KB && b > acc) { const b = Math.max(v.net_in, v.net_out);
return KB; if (b > MB && b > acc) {
} else { return MB;
return acc; } else if (b > KB && b > acc) {
} return KB;
} else {
return acc;
}
}, 0) ?? 0; }, 0) ?? 0;
const net_scale_label = scaleLabel(net_scale); const net_scale_label = scaleLabel(net_scale);
const disk_scale = data?.reduce((acc, v) => { const disk_scale =
const b = Math.max(v.disk_read, v.disk_write); data?.reduce((acc, v) => {
if (b > MB && b > acc) { const b = Math.max(v.disk_read, v.disk_write);
return MB; if (b > MB && b > acc) {
} else if (b > KB && b > acc) { return MB;
return KB; } else if (b > KB && b > acc) {
} else { return KB;
return acc; } else {
} return acc;
}
}, 0) ?? 0; }, 0) ?? 0;
const disk_scale_label = scaleLabel(disk_scale); const disk_scale_label = scaleLabel(disk_scale);
const sortedData = (data ?? []) const sortedData = (data ?? [])
.sort((a, b) => a.timestamp - b.timestamp) .sort((a, b) => a.timestamp - b.timestamp)
.map((v) => ({ .map((v) => ({
timestamp: new Date(v.timestamp * 1000).toLocaleTimeString(), timestamp: new Date(v.timestamp * 1000).toLocaleTimeString(),
CPU: 100 * v.cpu, CPU: 100 * v.cpu,
RAM: v.memory / 1024 / 1024, RAM: v.memory / 1024 / 1024,
NET_IN: v.net_in / net_scale, NET_IN: v.net_in / net_scale,
NET_OUT: v.net_out / net_scale, NET_OUT: v.net_out / net_scale,
DISK_READ: v.disk_read / disk_scale, DISK_READ: v.disk_read / disk_scale,
DISK_WRITE: v.disk_write / disk_scale, DISK_WRITE: v.disk_write / disk_scale,
})); }));
const toolTip = <Tooltip cursor={{ fill: "rgba(200,200,200,0.5)" }} const toolTip = (
content={({ active, payload }) => { <Tooltip
if (active && payload && payload.length) { cursor={{ fill: "rgba(200,200,200,0.5)" }}
const data = payload[0].payload as TimeSeriesData; content={({ active, payload }) => {
return <div className="flex flex-col gap-2 bg-neutral-700 rounded-xl px-2 py-3"> if (active && payload && payload.length) {
<div>{data.timestamp}</div> const data = payload[0].payload as TimeSeriesData;
{payload.map((p) => <div>{p.name}: {Number(p.value).toFixed(2)}{p.unit}</div>)} return (
<div className="flex flex-col gap-2 bg-neutral-700 rounded-xl px-2 py-3">
</div>; <div>{data.timestamp}</div>
} {payload.map((p) => (
}} />; <div>
return <div className="flex flex-col gap-4"> {p.name}: {Number(p.value).toFixed(2)}
<Link to={"/vm"} state={state}> {p.unit}
&lt; Back </div>
</Link> ))}
<h2>CPU</h2> </div>
<ResponsiveContainer height={200}> );
<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} /> );
{toolTip} return (
</LineChart> <div className="flex flex-col gap-4">
</ResponsiveContainer> <Link to={"/vm"} state={state}>
<h2>Memory</h2> &lt; Back
<ResponsiveContainer height={200}> </Link>
<LineChart data={sortedData} margin={{ left: 0, right: 0 }} style={{ userSelect: "none" }}> <h2>CPU</h2>
<XAxis dataKey="timestamp" /> <ResponsiveContainer height={200}>
<YAxis unit="MB" domain={[0, maxRam]} /> <LineChart
<Line type="monotone" dataKey="RAM" unit="MB" dot={false} /> data={sortedData}
{toolTip} margin={{ left: 0, right: 0 }}
</LineChart> style={{ userSelect: "none" }}
</ResponsiveContainer> >
<h2>Network</h2> <XAxis dataKey="timestamp" />
<ResponsiveContainer height={200}> <YAxis unit="%" domain={[0, 100]} />
<LineChart data={sortedData} margin={{ left: 20, right: 0 }} style={{ userSelect: "none" }}> <Line type="monotone" dataKey="CPU" unit="%" dot={false} />
<XAxis dataKey="timestamp" /> {toolTip}
<YAxis unit={`${net_scale_label}/s`} domain={[0, "auto"]} /> </LineChart>
<Line type="monotone" dataKey="NET_IN" unit={`${net_scale_label}/s`} stroke="red" dot={false} /> </ResponsiveContainer>
<Line type="monotone" dataKey="NET_OUT" unit={`${net_scale_label}/s`} stroke="green" dot={false} /> <h2>Memory</h2>
{toolTip} <ResponsiveContainer height={200}>
<Legend /> <LineChart
</LineChart> data={sortedData}
</ResponsiveContainer> margin={{ left: 0, right: 0 }}
<h2>Disk</h2> style={{ userSelect: "none" }}
<ResponsiveContainer height={200}> >
<LineChart data={sortedData} margin={{ left: 20, right: 0 }} style={{ userSelect: "none" }}> <XAxis dataKey="timestamp" />
<XAxis dataKey="timestamp" /> <YAxis unit="MB" domain={[0, maxRam]} />
<YAxis unit={`${disk_scale_label}/s`} domain={[0, "auto"]} /> <Line type="monotone" dataKey="RAM" unit="MB" dot={false} />
<Line type="monotone" dataKey="DISK_READ" unit={`${disk_scale_label}/s`} stroke="red" dot={false} /> {toolTip}
<Line type="monotone" dataKey="DISK_WRITE" unit={`${disk_scale_label}/s`} stroke="green" dot={false} /> </LineChart>
{toolTip} </ResponsiveContainer>
<Legend /> <h2>Network</h2>
</LineChart> <ResponsiveContainer height={200}>
</ResponsiveContainer> <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}
/>
{toolTip}
<Legend />
</LineChart>
</ResponsiveContainer>
<h2>Disk</h2>
<ResponsiveContainer height={200}>
<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}
/>
{toolTip}
<Legend />
</LineChart>
</ResponsiveContainer>
</div> </div>
);
} }