feat: Revamp instance page
This commit is contained in:
@@ -1,56 +1,86 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { Button } from "./Button";
|
||||
import { Panel } from "./Panel";
|
||||
|
||||
/*
|
||||
* A licence blob is signed public data, not a secret it is useless on any
|
||||
* A licence blob is signed public data, not a secret — it is useless on any
|
||||
* instance other than the one it names. So it is safe to show inline, and
|
||||
* showing it is what stops a blocked download from blocking a paying customer.
|
||||
* That is also why it is never collapsed behind a toggle: someone whose
|
||||
* clipboard and download are both blocked has to be able to select it by hand.
|
||||
*
|
||||
* It is evidence rather than content, so it is set in a well with a keyed strip
|
||||
* saying what it is and how much of it there is, and given a fixed height. It
|
||||
* used to run to 250px of base64 and was the largest thing on the page, which
|
||||
* is a strange amount of room to give a string nobody reads.
|
||||
*
|
||||
* The download lives in the page header beside Renew, not here — it was in both
|
||||
* places, which is one button too many for one file.
|
||||
*/
|
||||
export function LicenceDelivery({ instanceId, blob, downloadUrl }: { instanceId: string; blob: string; downloadUrl: string }) {
|
||||
export function LicenceDelivery({ blob }: { instanceId: string; blob: string; downloadUrl: string }) {
|
||||
const [copied, setCopied] = useState(false);
|
||||
|
||||
async function copy() {
|
||||
await navigator.clipboard.writeText(blob);
|
||||
setCopied(true);
|
||||
setTimeout(() => setCopied(false), 2000);
|
||||
try {
|
||||
await navigator.clipboard.writeText(blob);
|
||||
setCopied(true);
|
||||
setTimeout(() => setCopied(false), 2000);
|
||||
} catch {
|
||||
// Clipboard is refused without a secure context or a gesture the
|
||||
// browser trusts. The blob is on screen and selectable either way,
|
||||
// so this needs no error state.
|
||||
}
|
||||
}
|
||||
|
||||
const steps = [
|
||||
<>
|
||||
Open <code className="rounded-sm bg-accent-wash px-1">Settings → Licence</code> on your install.
|
||||
Open <Code>Settings → Licence</Code> on your install.
|
||||
</>,
|
||||
<>Paste the licence into the box and save.</>,
|
||||
<>
|
||||
The page reports <code className="rounded-sm bg-accent-wash px-1">Valid</code> straight away no restart.
|
||||
The page reports <Code>Valid</Code> straight away — no restart.
|
||||
</>,
|
||||
];
|
||||
|
||||
return (
|
||||
<section className="grid gap-3">
|
||||
<h2 className="text-xl">Your licence</h2>
|
||||
<div className="flex flex-wrap items-center gap-3">
|
||||
<a
|
||||
href={downloadUrl}
|
||||
download={`vantage-${instanceId}.lic`}
|
||||
className="inline-flex items-center gap-2 rounded border border-accent bg-accent px-4 py-2.5 text-[0.94rem] font-semibold text-accent-ink"
|
||||
>
|
||||
Download licence
|
||||
</a>
|
||||
<Button variant="line" type="button" onClick={copy}>
|
||||
{copied ? "Copied" : "Copy to clipboard"}
|
||||
</Button>
|
||||
<Panel title="Your licence" meta="Paste into your install">
|
||||
<div className="grid gap-2">
|
||||
<div className="flex flex-wrap items-baseline justify-between gap-3">
|
||||
<span className="font-mono text-[0.64rem] uppercase tracking-[0.14em] text-ink-3">Licence key</span>
|
||||
<span className="font-mono text-[0.64rem] uppercase tracking-[0.12em] text-ink-3">{blob.length.toLocaleString()} characters</span>
|
||||
</div>
|
||||
|
||||
<div className="relative">
|
||||
{/* Dashed, because this is data to be carried somewhere else
|
||||
rather than a surface to read. */}
|
||||
<pre className="max-h-32 overflow-y-auto whitespace-pre-wrap break-all rounded border border-dashed border-rule bg-panel-2 p-3 pr-24 font-mono text-[0.7rem] leading-relaxed text-ink-2">
|
||||
{blob}
|
||||
</pre>
|
||||
<button
|
||||
type="button"
|
||||
onClick={copy}
|
||||
className="absolute right-2 top-2 rounded border border-rule bg-panel px-2.5 py-1 font-mono text-[0.66rem] uppercase tracking-[0.1em] text-ink-2 hover:border-accent hover:text-accent"
|
||||
>
|
||||
{copied ? "Copied" : "Copy"}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<pre className="max-h-48 overflow-y-auto whitespace-pre-wrap break-all rounded border border-dashed border-rule bg-panel-2 p-3 font-mono text-[0.72rem] text-ink-2">{blob}</pre>
|
||||
|
||||
{/* Numbered because this is an actual sequence — each step is only
|
||||
possible once the one before it is done. */}
|
||||
<ol className="grid gap-2">
|
||||
{steps.map((body, i) => (
|
||||
<li key={i} className="grid grid-cols-[1.6rem_1fr] gap-3 text-[0.82rem] text-ink-2">
|
||||
<span className="h-6 rounded-sm border border-rule text-center font-mono text-[0.72rem] leading-6 text-accent">{i + 1}</span>
|
||||
<span>{body}</span>
|
||||
<li key={i} className="grid grid-cols-[1.5rem_1fr] items-start gap-3 text-[0.84rem] text-ink-2">
|
||||
<span className="grid h-[1.4rem] place-items-center rounded-sm border border-rule font-mono text-[0.68rem] text-accent">{i + 1}</span>
|
||||
<span className="leading-[1.4rem]">{body}</span>
|
||||
</li>
|
||||
))}
|
||||
</ol>
|
||||
</section>
|
||||
</Panel>
|
||||
);
|
||||
}
|
||||
|
||||
function Code({ children }: { children: React.ReactNode }) {
|
||||
return <code className="rounded-sm bg-accent-wash px-1 font-mono text-[0.8rem] text-ink">{children}</code>;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user