Files
web/src/pages/vm.tsx
Kieran 0f8ee33279
All checks were successful
continuous-integration/drone/push Build is passing
feat: show lnurl as payment method
2025-05-02 13:59:37 +01:00

197 lines
6.3 KiB
TypeScript

import "@xterm/xterm/css/xterm.css";
import { Link, useLocation, useNavigate } from "react-router-dom";
import { VmInstance, VmIpAssignment } from "../api";
import VpsInstanceRow from "../components/vps-instance";
import useLogin from "../hooks/login";
import { useEffect, useState } from "react";
import { AsyncButton } from "../components/button";
import { Icon } from "../components/icon";
import Modal from "../components/modal";
import SSHKeySelector from "../components/ssh-keys";
export default function VmPage() {
const location = useLocation() as { state?: VmInstance };
const login = useLogin();
const navigate = useNavigate();
const [state, setState] = useState<VmInstance | undefined>(location?.state);
const [editKey, setEditKey] = useState(false);
const [editReverse, setEditReverse] = useState<VmIpAssignment>();
const [error, setError] = useState<string>();
const [key, setKey] = useState(state?.ssh_key?.id ?? -1);
async function reloadVmState() {
if (!state) return;
const newState = await login?.api.getVm(state.id);
setState(newState);
}
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>
</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>
);
}
const hasNoIps = (state?.ip_assignments?.length ?? 0) === 0;
function networkInfo() {
if (!state) return;
if (hasNoIps) {
return <div className="text-sm text-red-500">No IP's assigned</div>;
}
return <>{state.ip_assignments?.map((i) => ipRow(i, true))}</>;
}
useEffect(() => {
const t = setInterval(() => reloadVmState(), 5000);
return () => clearInterval(t);
}, []);
function bestHost() {
if (!state) return;
if (state.ip_assignments.length > 0) {
const ip = state.ip_assignments.at(0)!;
return ip.forward_dns ? ip.forward_dns : ip.ip.split("/")[0];
}
}
if (!state) {
return <h2>No VM selected</h2>;
}
return (
<div className="flex flex-col gap-4">
<Link to={"/account"}>&lt; Back</Link>
<VpsInstanceRow vm={state} actions={true} />
<div className="text-xl">Network:</div>
<div className="grid grid-cols-2 gap-4">{networkInfo()}</div>
<div className="text-xl">SSH:</div>
<div className="grid grid-cols-2 gap-4">
<div className="bg-neutral-900 px-2 py-3 rounded-lg flex gap-2 items-center">
<div>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>
{!hasNoIps && (
<div className="bg-neutral-900 px-2 py-3 rounded-lg flex gap-2 items-center">
<div>Login:</div>
<pre className="select-all bg-neutral-800 px-3 py-1 rounded-full">
ssh {state.image.default_username}@{bestHost()}
</pre>
</div>
)}
</div>
<hr />
<div className="flex gap-4">
{/*<AsyncButton onClick={() => navigate("/vm/console", { state })}>
Console
</AsyncButton>*/}
<AsyncButton onClick={() => navigate("/vm/billing", { state })}>
Billing
</AsyncButton>
<AsyncButton onClick={() => navigate("/vm/graphs", { state })}>
Graphs
</AsyncButton>
</div>
{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-500">{error}</b>}
<AsyncButton
onClick={async () => {
setError(undefined);
if (!login?.api) return;
try {
await login.api.patchVm(state.id, {
ssh_key_id: key,
});
await reloadVmState();
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-500">{error}</b>}
<AsyncButton
onClick={async () => {
setError(undefined);
if (!login?.api) return;
try {
await login.api.patchVm(state.id, {
reverse_dns: editReverse.reverse_dns,
});
await reloadVmState();
setEditReverse(undefined);
} catch (e) {
if (e instanceof Error) {
setError(e.message);
}
}
}}
>
Save
</AsyncButton>
</div>
</Modal>
)}
</div>
);
}