- New /api/dashboard/funnel (5-stage stack health, 30s TTL cache) and /api/dashboard/summary (containers, daily uptime jsonl, ops activity) - Token system (tokens.css + Tailwind sp-* aliases); legacy bg/card/accent remapped onto the tokens; Schibsted Grotesk bundled via fontsource - TopNav pill navigation + AppShell replace the sidebar layout (off-canvas drawer below 1024px); central display-weight page titles - Dashboard redesign: FunnelChart (gradient/hatch SVG waterfall), container count card with per-host bars + Insights chip, UptimeChart, OpsGrid, AiPromptBar; 30/7-day range selector; host sections retained below - Stacks page honours ?q= / ?filter= deep links + new status-filter select Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
98 lines
3.3 KiB
TypeScript
98 lines
3.3 KiB
TypeScript
import { useState } from "react";
|
|
import { useNavigate } from "react-router-dom";
|
|
import { ChevronDown, CornerDownLeft } from "lucide-react";
|
|
import { cn } from "@/lib/utils";
|
|
|
|
const CONTEXT_TAGS = [
|
|
{ tag: "/running", filter: "running" },
|
|
{ tag: "/stopped", filter: "stopped" },
|
|
{ tag: "/attention", filter: "attention" },
|
|
];
|
|
|
|
function SparkleIcon() {
|
|
return (
|
|
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" aria-hidden="true">
|
|
<path
|
|
d="M8 1.5 9.4 6 14 7.5 9.4 9 8 13.5 6.6 9 2 7.5 6.6 6 8 1.5Z"
|
|
fill="var(--sp-amber)"
|
|
/>
|
|
<path d="M13 11l.6 1.7L15.3 13l-1.7.6L13 15.3l-.6-1.7-1.7-.6 1.7-.6.6-1.7Z" fill="var(--sp-amber)" fillOpacity="0.7" />
|
|
</svg>
|
|
);
|
|
}
|
|
|
|
/** Inline explore-prompt: typed (or tag-clicked) queries jump to the Stacks
|
|
* page with a matching filter — no LLM behind it (yet). */
|
|
export function AiPromptBar({ onSubmit }: { onSubmit?: (query: string) => void }) {
|
|
const [open, setOpen] = useState(true);
|
|
const [value, setValue] = useState("");
|
|
const navigate = useNavigate();
|
|
|
|
const submit = (raw: string) => {
|
|
const query = raw.trim();
|
|
if (!query) return;
|
|
if (onSubmit) {
|
|
onSubmit(query);
|
|
return;
|
|
}
|
|
const tag = CONTEXT_TAGS.find((t) => query.startsWith(t.tag));
|
|
if (tag) navigate(`/stacks?filter=${tag.filter}`);
|
|
else navigate(`/stacks?q=${encodeURIComponent(query)}`);
|
|
};
|
|
|
|
return (
|
|
<div className="rounded-2xl border border-sp-border bg-sp-surface-2">
|
|
<button
|
|
onClick={() => setOpen((v) => !v)}
|
|
className="flex w-full items-center gap-2 px-4 py-3 text-left"
|
|
aria-expanded={open}
|
|
>
|
|
<SparkleIcon />
|
|
<span className="text-sm font-medium text-sp-text-1">
|
|
What would you like to explore next?
|
|
</span>
|
|
<ChevronDown
|
|
className={cn("ml-auto h-4 w-4 text-sp-text-3 transition-transform", open && "rotate-180")}
|
|
/>
|
|
</button>
|
|
{open && (
|
|
<div className="px-4 pb-3.5">
|
|
<form
|
|
onSubmit={(e) => {
|
|
e.preventDefault();
|
|
submit(value);
|
|
}}
|
|
className="flex items-center gap-2 rounded-xl border border-sp-border bg-sp-surface px-3 py-2"
|
|
>
|
|
<input
|
|
value={value}
|
|
onChange={(e) => setValue(e.target.value)}
|
|
placeholder="Show me stacks that are…"
|
|
className="min-w-0 flex-1 bg-transparent text-sm text-sp-text-1 placeholder:text-sp-text-3 focus:outline-none"
|
|
/>
|
|
<button
|
|
type="submit"
|
|
className="rounded-lg p-1.5 text-sp-text-3 hover:bg-sp-surface-2 hover:text-sp-text-1"
|
|
title="Go"
|
|
aria-label="Submit"
|
|
>
|
|
<CornerDownLeft className="h-4 w-4" />
|
|
</button>
|
|
</form>
|
|
<div className="mt-2 flex flex-wrap gap-1.5">
|
|
{CONTEXT_TAGS.map(({ tag }) => (
|
|
<button
|
|
key={tag}
|
|
onClick={() => submit(tag)}
|
|
className="rounded-chip bg-sp-amber/15 px-2 py-0.5 font-mono text-xs font-semibold text-sp-amber hover:bg-sp-amber/25"
|
|
>
|
|
{tag}
|
|
</button>
|
|
))}
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|