diff --git a/src/api.ts b/src/api.ts index 3fc3a8f..49b98f5 100644 --- a/src/api.ts +++ b/src/api.ts @@ -122,6 +122,20 @@ export class LNVpsApi { return data; } + async startVm(id: number) { + const { data } = await this.#handleResponse>( + await this.#req(`/api/v1/vm/${id}/start`, "PATCH"), + ); + return data; + } + + async stopVm(id: number) { + const { data } = await this.#handleResponse>( + await this.#req(`/api/v1/vm/${id}/stop`, "PATCH"), + ); + return data; + } + async listOffers() { const { data } = await this.#handleResponse>>( await this.#req("/api/v1/vm/templates", "GET"), @@ -192,7 +206,11 @@ export class LNVpsApi { } } - async #req(path: string, method: "GET" | "POST" | "DELETE", body?: object) { + async #req( + path: string, + method: "GET" | "POST" | "DELETE" | "PUT" | "PATCH", + body?: object, + ) { const auth = async (url: string, method: string) => { const auth = await this.publisher?.generic((eb) => { return eb diff --git a/src/components/button.tsx b/src/components/button.tsx index 21e5882..066c413 100644 --- a/src/components/button.tsx +++ b/src/components/button.tsx @@ -1,18 +1,28 @@ import classNames from "classnames"; -import { forwardRef, HTMLProps } from "react"; +import { forwardRef, HTMLProps, useState } from "react"; +import Spinner from "./spinner"; export type AsyncButtonProps = { onClick?: (e: React.MouseEvent) => Promise | void; } & Omit, "type" | "ref" | "onClick">; const AsyncButton = forwardRef( - function AsyncButton({ className, ...props }, ref) { + function AsyncButton({ className, onClick, ...props }, ref) { + const [loading, setLoading] = useState(false); const hasBg = className?.includes("bg-"); return ( ); }, diff --git a/src/components/spinner.css b/src/components/spinner.css new file mode 100644 index 0000000..4318a13 --- /dev/null +++ b/src/components/spinner.css @@ -0,0 +1,33 @@ +.spinner_V8m1 { + transform-origin: center; + animation: spinner_zKoa 2s linear infinite; +} + +.spinner_V8m1 circle { + stroke-linecap: round; + animation: spinner_YpZS 1.5s ease-in-out infinite; +} + +@keyframes spinner_zKoa { + 100% { + transform: rotate(360deg); + } +} + +@keyframes spinner_YpZS { + 0% { + stroke-dasharray: 0 150; + stroke-dashoffset: 0; + } + + 47.5% { + stroke-dasharray: 42 150; + stroke-dashoffset: -16; + } + + 95%, + 100% { + stroke-dasharray: 42 150; + stroke-dashoffset: -59; + } +} diff --git a/src/components/spinner.tsx b/src/components/spinner.tsx new file mode 100644 index 0000000..5d6e6d9 --- /dev/null +++ b/src/components/spinner.tsx @@ -0,0 +1,23 @@ +import "./spinner.css"; + +export interface IconProps { + className?: string; + width?: number; + height?: number; +} + +const Spinner = (props: IconProps) => ( + + + + + +); + +export default Spinner; diff --git a/src/components/vps-actions.tsx b/src/components/vps-actions.tsx index 081bb1c..7b67f1c 100644 --- a/src/components/vps-actions.tsx +++ b/src/components/vps-actions.tsx @@ -1,21 +1,45 @@ -import { VmInstance } from "../api"; +import { EventPublisher } from "@snort/system"; +import { LNVpsApi, VmInstance } from "../api"; +import { ApiUrl } from "../const"; +import useLogin from "../hooks/login"; import { Icon } from "./icon"; +import { AsyncButton } from "./button"; -export default function VmActions({ vm }: { vm: VmInstance }) { +export default function VmActions({ + vm, + onReload, +}: { + vm: VmInstance; + onReload?: () => void; +}) { + const login = useLogin(); const state = vm.status?.state; if (!state) return; + + const api = new LNVpsApi( + ApiUrl, + login?.signer ? new EventPublisher(login.signer, login.pubkey) : undefined, + ); return (
- { + { e.stopPropagation(); + + if (state === "running") { + await api.stopVm(vm.id); + } else { + await api.startVm(vm.id); + } + onReload?.(); }} - /> - + + + + {/* { e.stopPropagation(); }} - /> + />*/}
); diff --git a/src/components/vps-instance.tsx b/src/components/vps-instance.tsx index 791a608..e05a097 100644 --- a/src/components/vps-instance.tsx +++ b/src/components/vps-instance.tsx @@ -7,9 +7,11 @@ import VmActions from "./vps-actions"; export default function VpsInstanceRow({ vm, actions, + onReload, }: { vm: VmInstance; actions?: boolean; + onReload?: () => void; }) { const expires = new Date(vm.expires); const isExpired = expires <= new Date(); @@ -44,7 +46,9 @@ export default function VpsInstanceRow({ )} - {!isExpired && (actions ?? true) && } + {!isExpired && (actions ?? true) && ( + + )} ); diff --git a/src/pages/account.tsx b/src/pages/account.tsx index e74c522..4b2d59e 100644 --- a/src/pages/account.tsx +++ b/src/pages/account.tsx @@ -9,13 +9,20 @@ export default function AccountPage() { const login = useLogin(); const [vms, setVms] = useState>([]); - useEffect(() => { + async function loadVms() { if (!login?.signer) return; const api = new LNVpsApi( ApiUrl, new EventPublisher(login.signer, login.pubkey), ); - api.listVms().then(setVms); + const vms = await api.listVms(); + setVms(vms); + } + + useEffect(() => { + loadVms(); + const t = setInterval(() => loadVms(), 5_000); + return () => clearInterval(t); }, [login]); return ( @@ -23,7 +30,7 @@ export default function AccountPage() {

My Resources

{vms.map((a) => ( - + ))}