feat(adminsite): licence delivery, paste instructions and relink
The blob is shown inline as well as offered as a file, because a licence is signed public data bound to one instance -- useless anywhere else -- and a blocked download must never leave a paying customer stuck. Admin now returns it to its owner for the same reason. Relink shows the remaining allowance from the backend's max_relinks rather than a hardcoded 3, and at zero it disables and says to contact support instead of failing at the API. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,104 @@
|
||||
"use client";
|
||||
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { useParams, useRouter } from "next/navigation";
|
||||
import { useState } from "react";
|
||||
import { API_BASE, ApiError, NotConnected, api } from "@/lib/api";
|
||||
import { NotConnectedPanel } from "@/components/NotConnected";
|
||||
import { LicenceDelivery } from "@/components/LicenceDelivery";
|
||||
import { RelinkPanel } from "@/components/RelinkPanel";
|
||||
import { StatePill } from "@/components/StatePill";
|
||||
import { formatDate, licenceState, limitLabel } from "@/lib/format";
|
||||
|
||||
export default function InstancePage() {
|
||||
const id = String(useParams().id);
|
||||
const router = useRouter();
|
||||
const qc = useQueryClient();
|
||||
const [relinkError, setRelinkError] = useState<string | undefined>();
|
||||
|
||||
const account = useQuery({ queryKey: ["account"], queryFn: api.account });
|
||||
const licence = useQuery({
|
||||
queryKey: ["license", id],
|
||||
queryFn: () => api.license(id),
|
||||
retry: false,
|
||||
});
|
||||
|
||||
const relink = useMutation({
|
||||
mutationFn: (newId: string) => api.relink(id, newId),
|
||||
onSuccess: (lic) => {
|
||||
qc.invalidateQueries({ queryKey: ["account"] });
|
||||
router.replace(`/instances/${lic.instance_id}`);
|
||||
},
|
||||
onError: (err) =>
|
||||
setRelinkError(err instanceof ApiError ? err.message : "Relink failed. Try again."),
|
||||
});
|
||||
|
||||
if (account.error instanceof NotConnected) return <NotConnectedPanel url={API_BASE} />;
|
||||
|
||||
const instance = account.data?.instances.find((i) => i.instance_id === id);
|
||||
if (account.isLoading) return <p className="text-ink-3">Loading…</p>;
|
||||
if (!instance) {
|
||||
// Says "not on your account" rather than "does not exist": the backend
|
||||
// answers 404 for another account's instance, and confirming existence
|
||||
// here would undo that.
|
||||
return <p className="text-ink-2">That instance is not on your account.</p>;
|
||||
}
|
||||
|
||||
const lic = licence.data;
|
||||
const state = licenceState(lic?.expires_at, Boolean(lic));
|
||||
|
||||
return (
|
||||
<div className="grid gap-8">
|
||||
<header className="grid gap-3">
|
||||
<div className="flex flex-wrap items-center gap-3">
|
||||
<h1 className="text-3xl">{instance.name}</h1>
|
||||
<StatePill state={state} />
|
||||
</div>
|
||||
<p className="font-mono text-[0.82rem] tabular-nums text-ink-3">
|
||||
{instance.instance_id}
|
||||
</p>
|
||||
</header>
|
||||
|
||||
{lic ? (
|
||||
<>
|
||||
<dl className="grid gap-4 sm:grid-cols-2 lg:grid-cols-4">
|
||||
<Fact label="Tier" value={lic.tier.replace("_", " ")} />
|
||||
<Fact label="Expires" value={formatDate(lic.expires_at)} />
|
||||
<Fact label="Servers" value={limitLabel(lic.limits.max_servers)} />
|
||||
<Fact label="Features" value={lic.features.join(", ") || "none"} />
|
||||
</dl>
|
||||
|
||||
{instance.deployment === "self_hosted" && (
|
||||
<>
|
||||
<LicenceDelivery
|
||||
instanceId={instance.instance_id}
|
||||
blob={lic.blob ?? ""}
|
||||
downloadUrl={api.licenseBlobUrl(instance.instance_id)}
|
||||
/>
|
||||
<RelinkPanel
|
||||
instanceId={instance.instance_id}
|
||||
used={instance.relink_count}
|
||||
max={account.data?.max_relinks ?? 3}
|
||||
error={relinkError}
|
||||
onRelink={(newId) => relink.mutate(newId)}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<p className="text-ink-2">No licence has been issued for this instance yet.</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function Fact({ label, value }: { label: string; value: string }) {
|
||||
return (
|
||||
<div className="grid gap-1">
|
||||
<dt className="font-mono text-[0.72rem] uppercase tracking-[0.1em] text-ink-3">
|
||||
{label}
|
||||
</dt>
|
||||
<dd className="font-mono tabular-nums">{value}</dd>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user