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(
readonly url: string,
readonly publisher: EventPublisher | undefined,
) { }
) {}
async getAccount() {
const { data } = await this.#handleResponse<ApiResponse<AccountDetail>>(
@ -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;
}

View File

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

View File

@ -7,76 +7,77 @@ import { AsyncButton } from "../components/button";
import CostLabel from "../components/cost";
export function VmBillingPage() {
const location = useLocation() as { state?: VmInstance };
const params = useParams();
const login = useLogin();
const navigate = useNavigate();
const [payment, setPayment] = useState<VmPayment>();
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]);
const location = useLocation() as { state?: VmInstance };
const params = useParams();
const login = useLogin();
const navigate = useNavigate();
const [payment, setPayment] = useState<VmPayment>();
const [state, setState] = useState<VmInstance | undefined>(location?.state);
async function reloadVmState() {
if (!state) return;
const expireDate = new Date(state.expires);
const days =
(expireDate.getTime() - new Date().getTime()) / 1000 / 24 / 60 / 60;
return (
<div className="flex flex-col gap-4">
<Link to={"/vm"} state={state}>
&lt; Back
</Link>
<div className="text-xl bg-neutral-900 rounded-xl px-3 py-4 flex justify-between items-center">
<div>Renewal for #{state.id}</div>
<CostLabel cost={state.template.cost_plan} />
</div>
{days > 0 && (
<div>
Expires: {expireDate.toDateString()} ({Math.floor(days)} days)
</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 });
}
}}
/>
</>
)}
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]);
if (!state) return;
const expireDate = new Date(state.expires);
const days =
(expireDate.getTime() - new Date().getTime()) / 1000 / 24 / 60 / 60;
return (
<div className="flex flex-col gap-4">
<Link to={"/vm"} state={state}>
&lt; Back
</Link>
<div className="text-xl bg-neutral-900 rounded-xl px-3 py-4 flex justify-between items-center">
<div>Renewal for #{state.id}</div>
<CostLabel cost={state.template.cost_plan} />
</div>
{days > 0 && (
<div>
Expires: {expireDate.toDateString()} ({Math.floor(days)} days)
</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 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 };
const login = useLogin();
const [data, setData] = useState<Array<TimeSeriesData>>();
const { state } = useLocation() as { state?: VmInstance };
const login = useLogin();
const [data, setData] = useState<Array<TimeSeriesData>>();
useEffect(() => {
if (!state) return;
login?.api.getVmTimeSeries(state.id).then(setData);
}, [login]);
useEffect(() => {
if (!state) return;
login?.api.getVmTimeSeries(state.id).then(setData);
}, [login]);
const maxRam = data?.reduce((acc, v) => {
const mb = v.memory_size / 1024 / 1024;
return acc < mb ? mb : acc;
const maxRam =
data?.reduce((acc, v) => {
const mb = v.memory_size / 1024 / 1024;
return acc < mb ? mb : acc;
}, 0) ?? 0;
const KB = 1024;
const MB = 1024 * 1024;
function scaleLabel(v: number) {
switch (net_scale) {
case MB: return "MiB";
case KB: return "KiB";
}
return "B";
const KB = 1024;
const MB = 1024 * 1024;
function scaleLabel(v: number) {
switch (net_scale) {
case MB:
return "MiB";
case KB:
return "KiB";
}
const net_scale = data?.reduce((acc, v) => {
const b = Math.max(v.net_in, v.net_out);
if (b > MB && b > acc) {
return MB;
} else if (b > KB && b > acc) {
return KB;
} else {
return acc;
}
return "B";
}
const net_scale =
data?.reduce((acc, v) => {
const b = Math.max(v.net_in, v.net_out);
if (b > MB && b > acc) {
return MB;
} else if (b > KB && b > acc) {
return KB;
} else {
return acc;
}
}, 0) ?? 0;
const net_scale_label = scaleLabel(net_scale);
const disk_scale = data?.reduce((acc, v) => {
const b = Math.max(v.disk_read, v.disk_write);
if (b > MB && b > acc) {
return MB;
} else if (b > KB && b > acc) {
return KB;
} else {
return acc;
}
const net_scale_label = scaleLabel(net_scale);
const disk_scale =
data?.reduce((acc, v) => {
const b = Math.max(v.disk_read, v.disk_write);
if (b > MB && b > acc) {
return MB;
} else if (b > KB && b > acc) {
return KB;
} else {
return acc;
}
}, 0) ?? 0;
const disk_scale_label = scaleLabel(disk_scale);
const sortedData = (data ?? [])
.sort((a, b) => a.timestamp - b.timestamp)
.map((v) => ({
timestamp: new Date(v.timestamp * 1000).toLocaleTimeString(),
CPU: 100 * v.cpu,
RAM: v.memory / 1024 / 1024,
NET_IN: v.net_in / net_scale,
NET_OUT: v.net_out / net_scale,
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)" }}
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">
<div>{data.timestamp}</div>
{payload.map((p) => <div>{p.name}: {Number(p.value).toFixed(2)}{p.unit}</div>)}
</div>;
}
}} />;
return <div className="flex flex-col gap-4">
<Link to={"/vm"} state={state}>
&lt; Back
</Link>
<h2>CPU</h2>
<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}
</LineChart>
</ResponsiveContainer>
<h2>Memory</h2>
<ResponsiveContainer height={200}>
<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} />
{toolTip}
</LineChart>
</ResponsiveContainer>
<h2>Network</h2>
<ResponsiveContainer height={200}>
<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>
const disk_scale_label = scaleLabel(disk_scale);
const sortedData = (data ?? [])
.sort((a, b) => a.timestamp - b.timestamp)
.map((v) => ({
timestamp: new Date(v.timestamp * 1000).toLocaleTimeString(),
CPU: 100 * v.cpu,
RAM: v.memory / 1024 / 1024,
NET_IN: v.net_in / net_scale,
NET_OUT: v.net_out / net_scale,
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)" }}
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">
<div>{data.timestamp}</div>
{payload.map((p) => (
<div>
{p.name}: {Number(p.value).toFixed(2)}
{p.unit}
</div>
))}
</div>
);
}
}}
/>
);
return (
<div className="flex flex-col gap-4">
<Link to={"/vm"} state={state}>
&lt; Back
</Link>
<h2>CPU</h2>
<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}
</LineChart>
</ResponsiveContainer>
<h2>Memory</h2>
<ResponsiveContainer height={200}>
<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} />
{toolTip}
</LineChart>
</ResponsiveContainer>
<h2>Network</h2>
<ResponsiveContainer height={200}>
<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>
}
);
}