"use client";
import { useActionState } from "react";
import { FormField, Select } from "@unlocalhosted/metalui";
import type { FormState } from "@/app/actions/types";
import { PlusIcon, ServerIcon, TagIcon } from "@/components/icons";
import { CopyIconButton, TextField } from "@/components/settings-fields";
import { PlateBody, PlateFooter } from "@/components/settings-plate";
import { Alert, Button } from "@/components/ui";
const expiries = [
{ value: "30", label: "30 days" },
{ value: "90", label: "90 days" },
{ value: "180", label: "6 months" },
{ value: "365", label: "1 year" },
{ value: "never", label: "No expiration" },
];
function Command({ children }: { children: string }) {
return (
<div className="flex items-start gap-2 rounded-row well py-1.5 pr-1.5 pl-3.5">
<pre className="min-w-0 flex-1 overflow-x-auto py-1.5 font-mono text-[12px] leading-5 text-ink">{children}</pre>
<CopyIconButton text={children} label="Copy command" toast="Copied command" />
</div>
);
}
/** Registers a runner, then shows its token once with the commands that start it. */
export function NewRunnerForm({
action,
origin,
scopeText,
}: {
action: (prev: FormState, formData: FormData) => Promise<FormState>;
origin: string;
scopeText: string;
}) {
const [state, formAction, pending] = useActionState(action, undefined);
if (state?.token) {
return (
<>
<PlateBody className="max-w-3xl">
<Alert kind="success">Runner registered. Copy its token now — you won’t be able to see it again.</Alert>
<div className="flex items-center gap-2 rounded-row recipe-well-field py-1.5 pr-1.5 pl-3.5">
<code className="flex-1 font-mono text-[12.5px] leading-5 break-all text-ink">{state.token}</code>
<CopyIconButton text={state.token} label="Copy token" toast="Copied token" />
</div>
<div className="flex flex-col gap-2">
<p className="type-title text-ink">Start it on your machine</p>
<p className="type-body text-ink2">
The runner is one file and needs Node.js 18+ and git. Download it, then start it with the token. Jobs run as the user
that starts it, so use an account (or a VM or container) set aside for it.
</p>
<Command>{`curl -fsSLO ${origin}/vectrum-runner.mjs`}</Command>
<Command>{`node vectrum-runner.mjs --url ${origin} --token ${state.token}`}</Command>
<p className="type-body text-ink2">
It shows up as online here within a few seconds. Jobs with <code className="font-mono text-[12px]">runs-on: self-hosted</code>{" "}
run on any runner; use labels to pick particular ones.
</p>
</div>
</PlateBody>
<PlateFooter>
<Button type="button" onClick={() => window.location.reload()}>
Done
</Button>
</PlateFooter>
</>
);
}
return (
<form action={formAction}>
<PlateBody className="max-w-2xl gap-6">
<p className="type-body text-ink2">{scopeText}</p>
<TextField label="Name" icon={<ServerIcon />} name="name" required maxLength={100} description="e.g. the machine it runs on." />
<TextField
label="Labels"
icon={<TagIcon />}
name="labels"
optional
placeholder="docker, gpu"
description={
<>
Jobs pick runners by label with <code className="font-mono text-[12px]">runs-on</code>. Every runner also has{" "}
<code className="font-mono text-[12px]">self-hosted</code> and its OS and architecture (e.g.{" "}
<code className="font-mono text-[12px]">linux</code>, <code className="font-mono text-[12px]">x64</code>).
</>
}
/>
<FormField>
<FormField.Label>Token expiration</FormField.Label>
<Select name="expiry" defaultValue="180" options={expiries} aria-label="Token expiration" className="w-44" />
<FormField.Description>Once it expires, the runner stops picking up jobs and has to be registered again.</FormField.Description>
</FormField>
{state?.error && <Alert>{state.error}</Alert>}
</PlateBody>
<PlateFooter>
<Button variant="primary" icon={<PlusIcon />} disabled={pending}>
{pending ? "Registering…" : "Register Runner"}
</Button>
</PlateFooter>
</form>
);
}