feat: start/stop vm
This commit is contained in:
parent
fc1962defc
commit
658e4aa5f2
20
src/api.ts
20
src/api.ts
@ -122,6 +122,20 @@ export class LNVpsApi {
|
|||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async startVm(id: number) {
|
||||||
|
const { data } = await this.#handleResponse<ApiResponse<VmInstance>>(
|
||||||
|
await this.#req(`/api/v1/vm/${id}/start`, "PATCH"),
|
||||||
|
);
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
async stopVm(id: number) {
|
||||||
|
const { data } = await this.#handleResponse<ApiResponse<VmInstance>>(
|
||||||
|
await this.#req(`/api/v1/vm/${id}/stop`, "PATCH"),
|
||||||
|
);
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
async listOffers() {
|
async listOffers() {
|
||||||
const { data } = await this.#handleResponse<ApiResponse<Array<VmTemplate>>>(
|
const { data } = await this.#handleResponse<ApiResponse<Array<VmTemplate>>>(
|
||||||
await this.#req("/api/v1/vm/templates", "GET"),
|
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 = async (url: string, method: string) => {
|
||||||
const auth = await this.publisher?.generic((eb) => {
|
const auth = await this.publisher?.generic((eb) => {
|
||||||
return eb
|
return eb
|
||||||
|
@ -1,18 +1,28 @@
|
|||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
import { forwardRef, HTMLProps } from "react";
|
import { forwardRef, HTMLProps, useState } from "react";
|
||||||
|
import Spinner from "./spinner";
|
||||||
|
|
||||||
export type AsyncButtonProps = {
|
export type AsyncButtonProps = {
|
||||||
onClick?: (e: React.MouseEvent) => Promise<void> | void;
|
onClick?: (e: React.MouseEvent) => Promise<void> | void;
|
||||||
} & Omit<HTMLProps<HTMLButtonElement>, "type" | "ref" | "onClick">;
|
} & Omit<HTMLProps<HTMLButtonElement>, "type" | "ref" | "onClick">;
|
||||||
|
|
||||||
const AsyncButton = forwardRef<HTMLButtonElement, AsyncButtonProps>(
|
const AsyncButton = forwardRef<HTMLButtonElement, AsyncButtonProps>(
|
||||||
function AsyncButton({ className, ...props }, ref) {
|
function AsyncButton({ className, onClick, ...props }, ref) {
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
const hasBg = className?.includes("bg-");
|
const hasBg = className?.includes("bg-");
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
ref={ref}
|
ref={ref}
|
||||||
|
onClick={async (e) => {
|
||||||
|
setLoading(true);
|
||||||
|
try {
|
||||||
|
await onClick?.(e);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}}
|
||||||
className={classNames(
|
className={classNames(
|
||||||
"py-1 px-2 rounded-xl font-medium",
|
"py-1 px-2 rounded-xl font-medium relative",
|
||||||
{
|
{
|
||||||
"bg-neutral-800 cursor-not-allowed text-neutral-500":
|
"bg-neutral-800 cursor-not-allowed text-neutral-500":
|
||||||
!hasBg && props.disabled === true,
|
!hasBg && props.disabled === true,
|
||||||
@ -21,8 +31,18 @@ const AsyncButton = forwardRef<HTMLButtonElement, AsyncButtonProps>(
|
|||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
style={{ visibility: loading ? "hidden" : "visible" }}
|
||||||
|
className="whitespace-nowrap items-center justify-center"
|
||||||
>
|
>
|
||||||
{props.children}
|
{props.children}
|
||||||
|
</span>
|
||||||
|
{loading && (
|
||||||
|
<span className="absolute w-full h-full top-0 left-0 flex items-center justify-center">
|
||||||
|
<Spinner />
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
33
src/components/spinner.css
Normal file
33
src/components/spinner.css
Normal file
@ -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;
|
||||||
|
}
|
||||||
|
}
|
23
src/components/spinner.tsx
Normal file
23
src/components/spinner.tsx
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
import "./spinner.css";
|
||||||
|
|
||||||
|
export interface IconProps {
|
||||||
|
className?: string;
|
||||||
|
width?: number;
|
||||||
|
height?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Spinner = (props: IconProps) => (
|
||||||
|
<svg
|
||||||
|
width="20"
|
||||||
|
height="20"
|
||||||
|
stroke="currentColor"
|
||||||
|
viewBox="0 0 20 20"
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<g className="spinner_V8m1">
|
||||||
|
<circle cx="10" cy="10" r="7.5" fill="none" strokeWidth="3"></circle>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
|
||||||
|
export default Spinner;
|
@ -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 { 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;
|
const state = vm.status?.state;
|
||||||
if (!state) return;
|
if (!state) return;
|
||||||
|
|
||||||
|
const api = new LNVpsApi(
|
||||||
|
ApiUrl,
|
||||||
|
login?.signer ? new EventPublisher(login.signer, login.pubkey) : undefined,
|
||||||
|
);
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-1">
|
<div className="flex flex-col gap-1">
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<Icon
|
<AsyncButton
|
||||||
name={state === "running" ? "stop" : "start"}
|
onClick={async (e) => {
|
||||||
className="bg-neutral-700 p-2 rounded-lg hover:bg-neutral-600"
|
|
||||||
size={40}
|
|
||||||
onClick={(e) => {
|
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
|
|
||||||
|
if (state === "running") {
|
||||||
|
await api.stopVm(vm.id);
|
||||||
|
} else {
|
||||||
|
await api.startVm(vm.id);
|
||||||
|
}
|
||||||
|
onReload?.();
|
||||||
}}
|
}}
|
||||||
/>
|
className="bg-neutral-700 hover:bg-neutral-600"
|
||||||
<Icon
|
>
|
||||||
|
<Icon name={state === "running" ? "stop" : "start"} size={30} />
|
||||||
|
</AsyncButton>
|
||||||
|
|
||||||
|
{/*<Icon
|
||||||
name="delete"
|
name="delete"
|
||||||
className="bg-neutral-700 p-2 rounded-lg hover:bg-neutral-600"
|
className="bg-neutral-700 p-2 rounded-lg hover:bg-neutral-600"
|
||||||
size={40}
|
size={40}
|
||||||
@ -30,7 +54,7 @@ export default function VmActions({ vm }: { vm: VmInstance }) {
|
|||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
}}
|
}}
|
||||||
/>
|
/>*/}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -7,9 +7,11 @@ import VmActions from "./vps-actions";
|
|||||||
export default function VpsInstanceRow({
|
export default function VpsInstanceRow({
|
||||||
vm,
|
vm,
|
||||||
actions,
|
actions,
|
||||||
|
onReload,
|
||||||
}: {
|
}: {
|
||||||
vm: VmInstance;
|
vm: VmInstance;
|
||||||
actions?: boolean;
|
actions?: boolean;
|
||||||
|
onReload?: () => void;
|
||||||
}) {
|
}) {
|
||||||
const expires = new Date(vm.expires);
|
const expires = new Date(vm.expires);
|
||||||
const isExpired = expires <= new Date();
|
const isExpired = expires <= new Date();
|
||||||
@ -44,7 +46,9 @@ export default function VpsInstanceRow({
|
|||||||
</Link>
|
</Link>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
{!isExpired && (actions ?? true) && <VmActions vm={vm} />}
|
{!isExpired && (actions ?? true) && (
|
||||||
|
<VmActions vm={vm} onReload={onReload} />
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -9,13 +9,20 @@ export default function AccountPage() {
|
|||||||
const login = useLogin();
|
const login = useLogin();
|
||||||
const [vms, setVms] = useState<Array<VmInstance>>([]);
|
const [vms, setVms] = useState<Array<VmInstance>>([]);
|
||||||
|
|
||||||
useEffect(() => {
|
async function loadVms() {
|
||||||
if (!login?.signer) return;
|
if (!login?.signer) return;
|
||||||
const api = new LNVpsApi(
|
const api = new LNVpsApi(
|
||||||
ApiUrl,
|
ApiUrl,
|
||||||
new EventPublisher(login.signer, login.pubkey),
|
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]);
|
}, [login]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -23,7 +30,7 @@ export default function AccountPage() {
|
|||||||
<h3>My Resources</h3>
|
<h3>My Resources</h3>
|
||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2">
|
||||||
{vms.map((a) => (
|
{vms.map((a) => (
|
||||||
<VpsInstanceRow key={a.id} vm={a} actions={false} />
|
<VpsInstanceRow key={a.id} vm={a} onReload={loadVms} />
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
|
Loading…
x
Reference in New Issue
Block a user