feat: Revamp instance page
This commit is contained in:
@@ -81,7 +81,7 @@ export default function BillingPage() {
|
||||
body="Cloud instances and self-hosted licences are both bought from the plan page, and each one bills separately."
|
||||
/>
|
||||
) : (
|
||||
<Table>
|
||||
<Table stack>
|
||||
<THead>
|
||||
<TR className="hover:bg-transparent">
|
||||
<TH>Instance</TH>
|
||||
@@ -97,14 +97,18 @@ export default function BillingPage() {
|
||||
const name = nameFor(s.instance_id);
|
||||
return (
|
||||
<TR key={s.subscription_id}>
|
||||
<TD>
|
||||
<TD label="Instance">
|
||||
{name ?? <span className="text-ink-3">Not linked yet</span>}
|
||||
{name && <Sub>{s.instance_id?.slice(0, 8)}</Sub>}
|
||||
</TD>
|
||||
<TD>{s.tier.replace("_", " ")}</TD>
|
||||
<TD className="text-ink-2">{s.term}</TD>
|
||||
<TD className="text-ink-2">{s.status}</TD>
|
||||
<TD>
|
||||
<TD label="Plan">{s.tier.replace("_", " ")}</TD>
|
||||
<TD label="Billing" className="text-ink-2">
|
||||
{s.term}
|
||||
</TD>
|
||||
<TD label="Status" className="text-ink-2">
|
||||
{s.status}
|
||||
</TD>
|
||||
<TD label="Renews">
|
||||
<div className="flex flex-wrap items-center gap-x-3 gap-y-1">
|
||||
{start && <TermSpark issuedAt={start} expiresAt={s.current_period_end} state={licenceState(s.current_period_end, true)} />}
|
||||
<span className="font-mono text-[0.78rem] tabular-nums text-ink-2">{formatDate(s.current_period_end)}</span>
|
||||
|
||||
@@ -184,16 +184,16 @@ export default function InstancePage() {
|
||||
</Panel>
|
||||
)}
|
||||
|
||||
{cloud ? (
|
||||
<MembersPanel instanceId={instance.instance_id} />
|
||||
) : (
|
||||
<Panel title="Who can sign in">
|
||||
<p className="text-[0.86rem] text-ink-2">Users for this install are managed inside it, in Settings → Instance. We have no access to your own deployment.</p>
|
||||
</Panel>
|
||||
)}
|
||||
|
||||
{/*
|
||||
* On a self-hosted instance the licence is the errand: someone
|
||||
* opens this page to fetch the blob and paste it. It sits
|
||||
* directly under the term, above the panels that only explain
|
||||
* things.
|
||||
*/}
|
||||
{lic && !cloud && <LicenceDelivery instanceId={instance.instance_id} blob={lic.blob ?? ""} downloadUrl={api.licenseBlobUrl(instance.instance_id)} />}
|
||||
|
||||
{cloud && <MembersPanel instanceId={instance.instance_id} />}
|
||||
|
||||
{/*
|
||||
* "Moves" rather than "Relinks": the count is rationed, so the
|
||||
* headline is how many are left, and the panel explains what
|
||||
@@ -201,12 +201,33 @@ export default function InstancePage() {
|
||||
* so the panel is absent rather than present and refusing.
|
||||
*/}
|
||||
{!cloud && (
|
||||
<Panel title="Moves" meta={`${instance.relink_count} of ${maxRelinks} used`}>
|
||||
<p className="text-[0.86rem] text-ink-2">A licence binds to one install. Moving it to different hardware is a relink, and each account gets {maxRelinks}.</p>
|
||||
<Panel title="Moves" meta={`${Math.max(0, maxRelinks - instance.relink_count)} of ${maxRelinks} left`}>
|
||||
<p className="text-[0.86rem] text-ink-2">
|
||||
A licence binds to one install. Rebuilding the host, or moving to different hardware, needs a replacement licence bound to the new ID —
|
||||
that is a move, and it covers the rest of your current term.
|
||||
</p>
|
||||
<RelinkPanel instanceId={instance.instance_id} used={instance.relink_count} max={maxRelinks} error={relinkError} onRelink={(newId) => relink.mutate(newId)} />
|
||||
</Panel>
|
||||
)}
|
||||
|
||||
{/*
|
||||
* A panel holding one sentence has not decided what it is for.
|
||||
* For a self-hosted install the useful content is not "we don't
|
||||
* do this" but where the thing they came looking for actually
|
||||
* lives — and why the people on their HQ account are not it.
|
||||
*/}
|
||||
{!cloud && (
|
||||
<Panel title="Who can sign in" meta="Managed in your install">
|
||||
<p className="text-[0.86rem] text-ink-2">
|
||||
You run this deployment, so its users live inside it rather than here. Add and remove them in the instance’s own settings.
|
||||
</p>
|
||||
<p className="text-[0.82rem] text-ink-3">
|
||||
People on your Vantage HQ account can see billing and this licence. That is separate from who can sign in to the instance, and granting
|
||||
one never grants the other.
|
||||
</p>
|
||||
</Panel>
|
||||
)}
|
||||
|
||||
{!lic && (
|
||||
<Panel bodyless>
|
||||
<EmptyState title="No licence issued yet." body="A licence binds to one install, so it is issued once this instance is linked to the ID its install reports." action={<LinkButton href="/purchase">Get a licence</LinkButton>} />
|
||||
|
||||
@@ -5,7 +5,7 @@ import { useState } from "react";
|
||||
import { API_BASE, ApiError, NotConnected, api, type AccountRole } from "@/lib/api";
|
||||
import { useSession } from "@/lib/session";
|
||||
import { NotConnectedPanel } from "@/components/NotConnected";
|
||||
import { Button } from "@/components/Button";
|
||||
import { Button, controlClass } from "@/components/Button";
|
||||
import { Field } from "@/components/Field";
|
||||
import { PageFrame, RailCard } from "@/components/PageFrame";
|
||||
import { formatDate } from "@/lib/format";
|
||||
@@ -89,11 +89,7 @@ export function InvitePanel() {
|
||||
<span className="font-mono text-[0.72rem] uppercase tracking-[0.1em] text-ink-3">
|
||||
Account role
|
||||
</span>
|
||||
<select
|
||||
value={role}
|
||||
onChange={(e) => setRole(e.target.value as AccountRole)}
|
||||
className="rounded border border-rule bg-panel-2 px-2.5 py-2 font-mono text-ink"
|
||||
>
|
||||
<select value={role} onChange={(e) => setRole(e.target.value as AccountRole)} className={controlClass()}>
|
||||
{assignable.map((r) => (
|
||||
<option key={r} value={r}>
|
||||
{r}
|
||||
|
||||
@@ -6,6 +6,7 @@ import { useState } from "react";
|
||||
import { api } from "@/lib/api";
|
||||
import { formatDate } from "@/lib/format";
|
||||
import { EmptyState, Panel } from "@/components/Panel";
|
||||
import { controlClass } from "@/components/Button";
|
||||
import { Sub, TBody, TD, TH, THead, TR, Table } from "@/components/Table";
|
||||
|
||||
export function AccountSearch() {
|
||||
@@ -27,7 +28,7 @@ export function AccountSearch() {
|
||||
value={q}
|
||||
onChange={(e) => setQ(e.target.value)}
|
||||
placeholder="Name, email, ctm_… or an instance UUID"
|
||||
className="w-full rounded border border-rule bg-panel-2 px-3 py-2 text-[0.9rem] text-ink focus:border-accent focus:outline-none"
|
||||
className={controlClass()}
|
||||
/>
|
||||
</label>
|
||||
</Panel>
|
||||
|
||||
@@ -5,6 +5,7 @@ import { useState } from "react";
|
||||
import { api } from "@/lib/api";
|
||||
import { formatDate, formatStamp } from "@/lib/format";
|
||||
import { PageHeader } from "@/components/PageHeader";
|
||||
import { controlClass } from "@/components/Button";
|
||||
import { EmptyState, Panel } from "@/components/Panel";
|
||||
import { Sub, TBody, TD, TH, THead, TR, Table } from "@/components/Table";
|
||||
|
||||
@@ -30,7 +31,7 @@ export default function AuditPage() {
|
||||
value={filter}
|
||||
onChange={(e) => setFilter(e.target.value)}
|
||||
placeholder="Action, actor or target"
|
||||
className="w-full rounded border border-rule bg-panel-2 px-3 py-2 text-[0.9rem] text-ink focus:border-accent focus:outline-none"
|
||||
className={controlClass()}
|
||||
/>
|
||||
</label>
|
||||
</Panel>
|
||||
|
||||
@@ -6,11 +6,12 @@ import { useState } from "react";
|
||||
import { api, type Tier } from "@/lib/api";
|
||||
import { formatDate, licenceState } from "@/lib/format";
|
||||
import { PageHeader } from "@/components/PageHeader";
|
||||
import { controlClass } from "@/components/Button";
|
||||
import { EmptyState, Panel } from "@/components/Panel";
|
||||
import { Sub, TBody, TD, TH, THead, TR, Table } from "@/components/Table";
|
||||
import { TermSpark } from "@/components/TermBar";
|
||||
|
||||
const SELECT = "rounded border border-rule bg-panel-2 px-2.5 py-2 text-[0.9rem] text-ink focus:border-accent focus:outline-none";
|
||||
const SELECT = controlClass("w-auto");
|
||||
|
||||
export default function LicensesPage() {
|
||||
const [tier, setTier] = useState<"" | Tier>("");
|
||||
|
||||
Reference in New Issue
Block a user