This commit is contained in:
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -61,7 +61,7 @@ const router = createBrowserRouter([
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "/vm/graphs",
|
path: "/vm/graphs",
|
||||||
element: <VmGraphsPage />
|
element: <VmGraphsPage />,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "/tos",
|
path: "/tos",
|
||||||
|
@ -32,7 +32,7 @@ export function VmBillingPage() {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (params["action"] === "renew" && login && state) {
|
if (params["action"] === "renew" && login && state) {
|
||||||
renew()
|
renew();
|
||||||
}
|
}
|
||||||
}, [login, state, params, renew]);
|
}, [login, state, params, renew]);
|
||||||
|
|
||||||
@ -54,8 +54,9 @@ export function VmBillingPage() {
|
|||||||
Expires: {expireDate.toDateString()} ({Math.floor(days)} days)
|
Expires: {expireDate.toDateString()} ({Math.floor(days)} days)
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{days < 0 && params["action"] !== "renew"
|
{days < 0 && params["action"] !== "renew" && (
|
||||||
&& <div className="text-red-500 text-xl">Expired</div>}
|
<div className="text-red-500 text-xl">Expired</div>
|
||||||
|
)}
|
||||||
{!payment && (
|
{!payment && (
|
||||||
<div>
|
<div>
|
||||||
<AsyncButton onClick={renew}>Extend Now</AsyncButton>
|
<AsyncButton onClick={renew}>Extend Now</AsyncButton>
|
||||||
|
@ -2,7 +2,15 @@ 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 };
|
||||||
@ -14,7 +22,8 @@ export function VmGraphsPage() {
|
|||||||
login?.api.getVmTimeSeries(state.id).then(setData);
|
login?.api.getVmTimeSeries(state.id).then(setData);
|
||||||
}, [login]);
|
}, [login]);
|
||||||
|
|
||||||
const maxRam = data?.reduce((acc, v) => {
|
const maxRam =
|
||||||
|
data?.reduce((acc, v) => {
|
||||||
const mb = v.memory_size / 1024 / 1024;
|
const mb = v.memory_size / 1024 / 1024;
|
||||||
return acc < mb ? mb : acc;
|
return acc < mb ? mb : acc;
|
||||||
}, 0) ?? 0;
|
}, 0) ?? 0;
|
||||||
@ -23,12 +32,15 @@ export function VmGraphsPage() {
|
|||||||
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 "KiB";
|
||||||
}
|
}
|
||||||
return "B";
|
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);
|
const b = Math.max(v.net_in, v.net_out);
|
||||||
if (b > MB && b > acc) {
|
if (b > MB && b > acc) {
|
||||||
return MB;
|
return MB;
|
||||||
@ -39,7 +51,8 @@ export function VmGraphsPage() {
|
|||||||
}
|
}
|
||||||
}, 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 =
|
||||||
|
data?.reduce((acc, v) => {
|
||||||
const b = Math.max(v.disk_read, v.disk_write);
|
const b = Math.max(v.disk_read, v.disk_write);
|
||||||
if (b > MB && b > acc) {
|
if (b > MB && b > acc) {
|
||||||
return MB;
|
return MB;
|
||||||
@ -61,24 +74,39 @@ export function VmGraphsPage() {
|
|||||||
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 = (
|
||||||
|
<Tooltip
|
||||||
|
cursor={{ fill: "rgba(200,200,200,0.5)" }}
|
||||||
content={({ active, payload }) => {
|
content={({ active, payload }) => {
|
||||||
if (active && payload && payload.length) {
|
if (active && payload && payload.length) {
|
||||||
const data = payload[0].payload as TimeSeriesData;
|
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>
|
<div>{data.timestamp}</div>
|
||||||
{payload.map((p) => <div>{p.name}: {Number(p.value).toFixed(2)}{p.unit}</div>)}
|
{payload.map((p) => (
|
||||||
|
<div>
|
||||||
</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}>
|
<Link to={"/vm"} state={state}>
|
||||||
< Back
|
< Back
|
||||||
</Link>
|
</Link>
|
||||||
<h2>CPU</h2>
|
<h2>CPU</h2>
|
||||||
<ResponsiveContainer height={200}>
|
<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" />
|
<XAxis dataKey="timestamp" />
|
||||||
<YAxis unit="%" domain={[0, 100]} />
|
<YAxis unit="%" domain={[0, 100]} />
|
||||||
<Line type="monotone" dataKey="CPU" unit="%" dot={false} />
|
<Line type="monotone" dataKey="CPU" unit="%" dot={false} />
|
||||||
@ -87,7 +115,11 @@ export function VmGraphsPage() {
|
|||||||
</ResponsiveContainer>
|
</ResponsiveContainer>
|
||||||
<h2>Memory</h2>
|
<h2>Memory</h2>
|
||||||
<ResponsiveContainer height={200}>
|
<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" />
|
<XAxis dataKey="timestamp" />
|
||||||
<YAxis unit="MB" domain={[0, maxRam]} />
|
<YAxis unit="MB" domain={[0, maxRam]} />
|
||||||
<Line type="monotone" dataKey="RAM" unit="MB" dot={false} />
|
<Line type="monotone" dataKey="RAM" unit="MB" dot={false} />
|
||||||
@ -96,26 +128,58 @@ export function VmGraphsPage() {
|
|||||||
</ResponsiveContainer>
|
</ResponsiveContainer>
|
||||||
<h2>Network</h2>
|
<h2>Network</h2>
|
||||||
<ResponsiveContainer height={200}>
|
<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" />
|
<XAxis dataKey="timestamp" />
|
||||||
<YAxis unit={`${net_scale_label}/s`} domain={[0, "auto"]} />
|
<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
|
||||||
<Line type="monotone" dataKey="NET_OUT" unit={`${net_scale_label}/s`} stroke="green" dot={false} />
|
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}
|
{toolTip}
|
||||||
<Legend />
|
<Legend />
|
||||||
</LineChart>
|
</LineChart>
|
||||||
</ResponsiveContainer>
|
</ResponsiveContainer>
|
||||||
<h2>Disk</h2>
|
<h2>Disk</h2>
|
||||||
<ResponsiveContainer height={200}>
|
<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" />
|
<XAxis dataKey="timestamp" />
|
||||||
<YAxis unit={`${disk_scale_label}/s`} domain={[0, "auto"]} />
|
<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
|
||||||
<Line type="monotone" dataKey="DISK_WRITE" unit={`${disk_scale_label}/s`} stroke="green" dot={false} />
|
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}
|
{toolTip}
|
||||||
<Legend />
|
<Legend />
|
||||||
</LineChart>
|
</LineChart>
|
||||||
</ResponsiveContainer>
|
</ResponsiveContainer>
|
||||||
</div>
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
Reference in New Issue
Block a user