feat: new billing page
This commit is contained in:
176
src/pages/vm.tsx
176
src/pages/vm.tsx
@ -1,12 +1,10 @@
|
||||
import "@xterm/xterm/css/xterm.css";
|
||||
|
||||
import { useLocation, useNavigate, useParams } from "react-router-dom";
|
||||
import { VmInstance, VmIpAssignment, VmPayment } from "../api";
|
||||
import { useLocation, useNavigate } from "react-router-dom";
|
||||
import { VmInstance, VmIpAssignment } from "../api";
|
||||
import VpsInstanceRow from "../components/vps-instance";
|
||||
import useLogin from "../hooks/login";
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import VpsPayment from "../components/vps-payment";
|
||||
import CostLabel from "../components/cost";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { AsyncButton } from "../components/button";
|
||||
import { Terminal } from "@xterm/xterm";
|
||||
import { FitAddon } from "@xterm/addon-fit";
|
||||
@ -19,11 +17,10 @@ const fit = new FitAddon();
|
||||
|
||||
export default function VmPage() {
|
||||
const location = useLocation() as { state?: VmInstance };
|
||||
const { action } = useParams();
|
||||
const login = useLogin();
|
||||
const navigate = useNavigate();
|
||||
const [state, setState] = useState<VmInstance | undefined>(location?.state);
|
||||
const [payment, setPayment] = useState<VmPayment>();
|
||||
|
||||
const [term] = useState<Terminal>();
|
||||
const termRef = useRef<HTMLDivElement | null>(null);
|
||||
const [editKey, setEditKey] = useState(false);
|
||||
@ -31,15 +28,6 @@ export default function VmPage() {
|
||||
const [error, setError] = useState<string>();
|
||||
const [key, setKey] = useState(state?.ssh_key?.id ?? -1);
|
||||
|
||||
const renew = useCallback(
|
||||
async function () {
|
||||
if (!login?.api || !state) return;
|
||||
const p = await login?.api.renewVm(state.id);
|
||||
setPayment(p);
|
||||
},
|
||||
[login?.api, state],
|
||||
);
|
||||
|
||||
async function reloadVmState() {
|
||||
if (!state) return;
|
||||
const newState = await login?.api.getVm(state.id);
|
||||
@ -47,35 +35,55 @@ export default function VmPage() {
|
||||
}
|
||||
|
||||
function ipRow(a: VmIpAssignment, reverse: boolean) {
|
||||
return <div
|
||||
key={a.id}
|
||||
className="bg-neutral-900 px-2 py-3 rounded-lg flex gap-2 flex-col justify-center"
|
||||
>
|
||||
<div>
|
||||
<span className="select-none">IP: </span>
|
||||
<span className="select-all">{a.ip.split("/")[0]}</span>
|
||||
return (
|
||||
<div
|
||||
key={a.id}
|
||||
className="bg-neutral-900 px-2 py-3 rounded-lg flex gap-2 flex-col justify-center"
|
||||
>
|
||||
<div>
|
||||
<span className="select-none">IP: </span>
|
||||
<span className="select-all">{a.ip.split("/")[0]}</span>
|
||||
</div>
|
||||
{a.forward_dns && (
|
||||
<div className="text-sm select-none">
|
||||
DNS: <span className="select-all">{a.forward_dns}</span>
|
||||
</div>
|
||||
)}
|
||||
{reverse && (
|
||||
<div className="text-sm select-none flex items-center gap-2">
|
||||
<div>
|
||||
PTR: <span className="select-all">{a.reverse_dns}</span>
|
||||
</div>
|
||||
<Icon
|
||||
name="pencil"
|
||||
className="inline"
|
||||
size={15}
|
||||
onClick={() => setEditReverse(a)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{a.forward_dns && <div className="text-sm select-none">DNS: <span className="select-all">{a.forward_dns}</span></div>}
|
||||
{reverse && <div className="text-sm select-none flex items-center gap-2">
|
||||
<div>PTR: <span className="select-all">{a.reverse_dns}</span></div>
|
||||
<Icon name="pencil" className="inline" size={15} onClick={() => setEditReverse(a)} />
|
||||
</div>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function networkInfo() {
|
||||
if (!state) return;
|
||||
if ((state.ip_assignments?.length ?? 0) === 0) {
|
||||
return <div className="text-sm text-red-500">No IP's assigned</div>
|
||||
return <div className="text-sm text-red-500">No IP's assigned</div>;
|
||||
}
|
||||
return <>
|
||||
{state.ip_assignments?.map(i => ipRow(i, true))}
|
||||
{ipRow({
|
||||
id: -1,
|
||||
ip: toEui64("2a13:2c0::", state.mac_address),
|
||||
gateway: ""
|
||||
}, false)}
|
||||
</>
|
||||
return (
|
||||
<>
|
||||
{state.ip_assignments?.map((i) => ipRow(i, true))}
|
||||
{ipRow(
|
||||
{
|
||||
id: -1,
|
||||
ip: toEui64("2a13:2c0::", state.mac_address),
|
||||
gateway: "",
|
||||
},
|
||||
false,
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
/*async function openTerminal() {
|
||||
@ -104,17 +112,10 @@ export default function VmPage() {
|
||||
}
|
||||
}, [termRef, term, fit]);
|
||||
|
||||
useEffect(() => {
|
||||
switch (action) {
|
||||
case "renew":
|
||||
renew();
|
||||
}
|
||||
}, [renew, action]);
|
||||
|
||||
useEffect(() => {
|
||||
const t = setInterval(() => reloadVmState(), 5000);
|
||||
return () => clearInterval(t);
|
||||
}, [])
|
||||
}, []);
|
||||
|
||||
if (!state) {
|
||||
return <h2>No VM selected</h2>;
|
||||
@ -123,60 +124,31 @@ export default function VmPage() {
|
||||
return (
|
||||
<div className="flex flex-col gap-4">
|
||||
<VpsInstanceRow vm={state} actions={true} />
|
||||
{action === undefined && (
|
||||
<>
|
||||
<div className="text-xl">Network:</div>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
{networkInfo()}
|
||||
</div>
|
||||
<div className="flex gap-2 items-center">
|
||||
<div className="text-xl">SSH Key:</div>
|
||||
<div className="text-sm bg-neutral-900 px-3 py-1 rounded-lg">
|
||||
{state.ssh_key?.name}
|
||||
</div>
|
||||
<Icon name="pencil" onClick={() => setEditKey(true)} />
|
||||
</div>
|
||||
<hr />
|
||||
<div className="text-xl">Renewal</div>
|
||||
<div className="flex justify-between items-center">
|
||||
<div>{new Date(state.expires).toDateString()}</div>
|
||||
{state.template?.cost_plan && (
|
||||
<div>
|
||||
<CostLabel cost={state.template?.cost_plan} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<AsyncButton onClick={() => navigate("/vm/renew", { state })}>
|
||||
Extend Now
|
||||
</AsyncButton>
|
||||
{/*
|
||||
|
||||
<div className="text-xl">Network:</div>
|
||||
<div className="grid grid-cols-2 gap-4">{networkInfo()}</div>
|
||||
<div className="flex gap-2 items-center">
|
||||
<div className="text-xl">SSH Key:</div>
|
||||
<div className="text-sm bg-neutral-900 px-3 py-1 rounded-lg">
|
||||
{state.ssh_key?.name}
|
||||
</div>
|
||||
<Icon name="pencil" onClick={() => setEditKey(true)} />
|
||||
</div>
|
||||
<hr />
|
||||
<div className="flex gap-4">
|
||||
<AsyncButton onClick={() => navigate("/vm/billing", { state })}>
|
||||
Billing
|
||||
</AsyncButton>
|
||||
</div>
|
||||
{/*
|
||||
{!term && <AsyncButton onClick={openTerminal}>Connect Terminal</AsyncButton>}
|
||||
{term && <div className="border p-2" ref={termRef}></div>}*/}
|
||||
</>
|
||||
)}
|
||||
{action === "renew" && (
|
||||
<>
|
||||
<h3>Renew VPS</h3>
|
||||
{payment && (
|
||||
<VpsPayment
|
||||
payment={payment}
|
||||
onPaid={async () => {
|
||||
setPayment(undefined);
|
||||
if (!login?.api || !state) return;
|
||||
navigate("/vm", {
|
||||
state
|
||||
})
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{editKey && (
|
||||
<Modal id="edit-ssh-key" onClose={() => setEditKey(false)}>
|
||||
<SSHKeySelector selectedKey={key} setSelectedKey={setKey} />
|
||||
<div className="flex flex-col gap-4 mt-8">
|
||||
<small>After selecting a new key, please restart the VM.</small>
|
||||
{error && <b className="text-red-700">{error}</b>}
|
||||
{error && <b className="text-red-500">{error}</b>}
|
||||
<AsyncButton
|
||||
onClick={async () => {
|
||||
setError(undefined);
|
||||
@ -201,15 +173,21 @@ export default function VmPage() {
|
||||
)}
|
||||
{editReverse && (
|
||||
<Modal id="edit-reverse" onClose={() => setEditReverse(undefined)}>
|
||||
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="text-lg">Reverse DNS:</div>
|
||||
<input type="text" placeholder="my-domain.com" value={editReverse.reverse_dns} onChange={(e) => setEditReverse({
|
||||
...editReverse,
|
||||
reverse_dns: e.target.value
|
||||
})} />
|
||||
<input
|
||||
type="text"
|
||||
placeholder="my-domain.com"
|
||||
value={editReverse.reverse_dns}
|
||||
onChange={(e) =>
|
||||
setEditReverse({
|
||||
...editReverse,
|
||||
reverse_dns: e.target.value,
|
||||
})
|
||||
}
|
||||
/>
|
||||
<small>DNS updates can take up to 48hrs to propagate.</small>
|
||||
{error && <b className="text-red-700">{error}</b>}
|
||||
{error && <b className="text-red-500">{error}</b>}
|
||||
<AsyncButton
|
||||
onClick={async () => {
|
||||
setError(undefined);
|
||||
|
Reference in New Issue
Block a user