Share and embed The dashboard leaves the tool and has to keep meaning something. A shared link carrying a relative range means a different thing tomorrow, so the dialog has to make the absolute choice visible rather than defaulting quietly. npx shadcn@latest add tabs input button switch label npm i lucide-react Tokens this needs: --background, --card, --card-foreground, --muted-foreground, --border, --chart-1 ──────────────────────────────────────────────────────────────────────── // SharePanel.tsx ──────────────────────────────────────────────────────────────────────── import { useState, type ReactNode } from "react"; import { Copy } from "lucide-react"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { Switch } from "@/components/ui/switch"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; /** Four needs wearing one button, each described by what it is good for. */ export type Mechanism = "link" | "snapshot" | "embed" | "export"; const MECHANISMS: { id: Mechanism; label: string; blurb: string }[] = [ { id: "link", label: "Link", blurb: "Live state, cheapest, best when they have access. Hinges on the URL carrying all of it." }, { id: "snapshot", label: "Snapshot", blurb: "Frozen data, viewable without access. Solves the writeup that links to a healthy system." }, { id: "embed", label: "Embed", blurb: "Rendered inside a wiki or a portal, updating live, read by people who never open the tool." }, { id: "export", label: "Export", blurb: "A PDF arriving on a timetable, for people who will never open the tool at all." }, ]; export type ShareState = { title: string; /** The window as it stands right now, absolute. */ range: { from: Date; to: Date }; /** The relative form the page is actually on, if any: "last 1h". */ relative?: string; filters: string[]; /** Set when the viewer has isolated part of the chart. It travels or it lies. */ isolated?: { shown: number; of: number }; }; const hhmm = (d: Date) => d.toISOString().slice(11, 16); /** What gets written onto everything that leaves. */ export const stamp = (s: ShareState) => [`${hhmm(s.range.from)}-${hhmm(s.range.to)}`, ...s.filters].join(" · "); /** * One panel behind the share button, with the state it will carry shown * before anything is produced. The absolute range is the default and the * relative one is a switch, so a link that means something else tomorrow is * a choice rather than an accident. Public is a switch with its consequence * written beside it. */ export function SharePanel({ state, url, preview, onCopy, onSnapshot, onExport }: { state: ShareState; /** The live URL. It must carry the range, filters and selection already. */ url: string; /** The panel as it will leave, rendered small. */ preview?: ReactNode; onCopy: (text: string) => void; onSnapshot: (opts: { isPublic: boolean }) => void; onExport: (opts: { every: "day" | "week" }) => void; }) { const [absolute, setAbsolute] = useState(true); const [isPublic, setPublic] = useState(false); const href = absolute || !state.relative ? url : url.replace(/from=[^&]*&to=[^&]*/, `range=${encodeURIComponent(state.relative)}`); const embed = ``; const copyRow = (text: string, label: string) => (
{state.title}
{stamp(state)}
{state.isolated.shown} of {state.isolated.of} series isolated
)}