Files
web/src/pages/vm.tsx
kieran 669b852106
All checks were successful
continuous-integration/drone/push Build is passing
feat: edit reverse dns
2025-03-04 14:29:09 +00:00

237 lines
7.5 KiB
TypeScript

import "@xterm/xterm/css/xterm.css";
import { useLocation, useNavigate, useParams } from "react-router-dom";
import { VmInstance, VmIpAssignment, VmPayment } 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 { AsyncButton } from "../components/button";
import { Terminal } from "@xterm/xterm";
import { FitAddon } from "@xterm/addon-fit";
import { toEui64 } from "../utils";
import { Icon } from "../components/icon";
import Modal from "../components/modal";
import SSHKeySelector from "../components/ssh-keys";
const fit = new FitAddon();
export default function VmPage() {
const { state } = useLocation() as { state?: VmInstance };
const { action } = useParams();
const login = useLogin();
const navigate = useNavigate();
const [payment, setPayment] = useState<VmPayment>();
const [term] = useState<Terminal>();
const termRef = useRef<HTMLDivElement | null>(null);
const [editKey, setEditKey] = useState(false);
const [editReverse, setEditReverse] = useState<VmIpAssignment>();
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 openTerminal() {
if (!login?.api || !state) return;
const ws = await login.api.connect_terminal(state.id);
const te = new Terminal();
const webgl = new WebglAddon();
webgl.onContextLoss(() => {
webgl.dispose();
});
te.loadAddon(webgl);
te.loadAddon(fit);
te.onResize(({ cols, rows }) => {
ws.send(`${cols}:${rows}`);
});
const attach = new AttachAddon(ws);
te.loadAddon(attach);
setTerm(te);
}*/
useEffect(() => {
if (term && termRef.current) {
term.open(termRef.current);
term.focus();
fit.fit();
}
}, [termRef, term, fit]);
useEffect(() => {
switch (action) {
case "renew":
renew();
}
}, [renew, action]);
if (!state) {
return <h2>No VM selected</h2>;
}
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>IP: </span>
<span className="select-all">{a.ip.split("/")[0]}</span>
{a.forward_dns && <span> ({a.forward_dns})</span>}
</div>
{reverse && <div className="text-sm flex items-center gap-2">
<div>PTR: {a.reverse_dns}</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 <>
{state.ip_assignments?.map(i => ipRow(i, true))}
{ipRow({
id: -1,
ip: toEui64("2a13:2c0::", state.mac_address),
gateway: ""
}, false)}
</>
}
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>
{/*
{!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 () => {
if (!login?.api || !state) return;
const newState = await login?.api.getVm(state.id);
navigate("/vm", {
state: newState,
});
setPayment(undefined);
}}
/>
)}
</>
)}
{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>}
<AsyncButton
onClick={async () => {
setError(undefined);
if (!login?.api) return;
try {
await login.api.patchVm(state.id, {
ssh_key_id: key,
});
const ns = await login.api.getVm(state?.id);
navigate(".", {
state: ns,
replace: true,
});
setEditKey(false);
} catch (e) {
if (e instanceof Error) {
setError(e.message);
}
}
}}
>
Save
</AsyncButton>
</div>
</Modal>
)}
{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
})} />
<small>DNS updates can take up to 48hrs to propagate.</small>
{error && <b className="text-red-700">{error}</b>}
<AsyncButton
onClick={async () => {
setError(undefined);
if (!login?.api) return;
try {
await login.api.patchVm(state.id, {
reverse_dns: editReverse.reverse_dns,
});
const ns = await login.api.getVm(state?.id);
navigate(".", {
state: ns,
replace: true,
});
setEditReverse(undefined);
} catch (e) {
if (e instanceof Error) {
setError(e.message);
}
}
}}
>
Save
</AsyncButton>
</div>
</Modal>
)}
</div>
);
}