feat(web): add global metadata visibility toggle

Introduce MetadataContext + useMetadata hook backed by localStorage and
wire a Braces button into the sidebar footer that toggles raw metadata
visibility across the app. Replace per-page collapsible metadata in
PeerDetail and WorkspaceDetail with a global animated reveal styled
distinctly (warning-colored card) to signal raw payload. Also adopts
the shared Breadcrumb in both detail pages.
This commit is contained in:
Offending Commit
2026-05-15 14:22:42 -05:00
parent 62cae68d05
commit e490d911fc
6 changed files with 110 additions and 94 deletions

View File

@@ -2,6 +2,7 @@ import { Link, useMatchRoute } from "@tanstack/react-router";
import { AnimatePresence, motion } from "framer-motion";
import {
Boxes,
Braces,
Check,
ChevronRight,
ChevronsUpDown,
@@ -21,6 +22,7 @@ import { HealthDot } from "@/components/shared/HealthDot";
import { useDemo } from "@/hooks/useDemo";
import { useHealthStatus } from "@/hooks/useHealthStatus";
import { useInstances } from "@/hooks/useInstances";
import { useMetadata } from "@/hooks/useMetadata";
import { useTheme } from "@/hooks/useTheme";
import { COLOR } from "@/lib/constants";
@@ -42,6 +44,7 @@ export function Sidebar() {
const { instances, active, activate } = useInstances();
const { theme, toggle } = useTheme();
const { demo, toggle: toggleDemo, mask } = useDemo();
const { showMetadata, toggle: toggleMeta } = useMetadata();
const { data: health } = useHealthStatus();
const [switcherOpen, setSwitcherOpen] = useState(false);
const switcherRef = useRef<HTMLDivElement | null>(null);
@@ -285,7 +288,7 @@ export function Sidebar() {
</AnimatePresence>
</nav>
{/* Theme toggle + footer */}
{/* Footer — version, demo, metadata, theme */}
<div
className="px-3 sm:px-5 py-3 flex items-center justify-between"
style={{ borderTop: "1px solid var(--border)" }}
@@ -311,6 +314,19 @@ export function Sidebar() {
<Eye className="w-3.5 h-3.5" strokeWidth={1.5} />
)}
</button>
<button
type="button"
onClick={toggleMeta}
className="w-7 h-7 rounded-md flex items-center justify-center transition-colors"
style={{
background: showMetadata ? "rgba(245,158,11,0.1)" : "var(--surface)",
border: `1px solid ${showMetadata ? "rgba(245,158,11,0.3)" : "var(--border)"}`,
color: showMetadata ? COLOR.warning : "var(--text-3)",
}}
title={showMetadata ? "Hide raw metadata" : "Show raw metadata"}
>
<Braces className="w-3.5 h-3.5" strokeWidth={1.5} />
</button>
<button
type="button"
onClick={toggle}

View File

@@ -1,16 +1,6 @@
import { Link, useNavigate, useParams } from "@tanstack/react-router";
import { useNavigate, useParams } from "@tanstack/react-router";
import { AnimatePresence, motion } from "framer-motion";
import {
ChevronDown,
Eye,
EyeOff,
MessageCircle,
Save,
Search,
User,
Users,
X,
} from "lucide-react";
import { Eye, EyeOff, MessageCircle, Save, Search, User, Users, X } from "lucide-react";
import { useState } from "react";
import {
usePeer,
@@ -20,6 +10,7 @@ import {
useSearchPeer,
useSetPeerCard,
} from "@/api/queries";
import { Breadcrumb } from "@/components/layout/Breadcrumb";
import { Badge } from "@/components/shared/Badge";
import { ErrorAlert } from "@/components/shared/ErrorAlert";
import { JsonViewer } from "@/components/shared/JsonViewer";
@@ -38,10 +29,12 @@ import {
SectionHeading,
} from "@/components/ui/typography";
import { useDemo } from "@/hooks/useDemo";
import { useMetadata } from "@/hooks/useMetadata";
import { COLOR } from "@/lib/constants";
export function PeerDetail() {
const { mask } = useDemo();
const { showMetadata } = useMetadata();
const { workspaceId, peerId } = useParams({ strict: false }) as {
workspaceId: string;
peerId: string;
@@ -65,7 +58,6 @@ export function PeerDetail() {
const [cardDraft, setCardDraft] = useState<string | null>(null);
const [searchQuery, setSearchQuery] = useState("");
const [metaExpanded, setMetaExpanded] = useState(false);
const observeMe = (peer as { configuration?: { observe_me?: boolean } } | undefined)
?.configuration?.observe_me;
@@ -79,27 +71,7 @@ export function PeerDetail() {
return (
<div className="page-container page-container--xl">
<motion.div initial={{ opacity: 0, y: -8 }} animate={{ opacity: 1, y: 0 }}>
<div className="flex items-center gap-2 text-xs mb-4" style={{ color: "var(--text-3)" }}>
<Link to="/workspaces" className="hover:underline">
Workspaces
</Link>
<span>/</span>
<Link
to="/workspaces/$workspaceId"
params={{ workspaceId } as never}
className="hover:underline font-mono"
>
{mask(workspaceId)}
</Link>
<span>/</span>
<Link
to="/workspaces/$workspaceId/peers"
params={{ workspaceId } as never}
className="hover:underline"
>
Peers
</Link>
</div>
<Breadcrumb />
<div className="flex items-start justify-between gap-4">
<div>
@@ -377,47 +349,29 @@ export function PeerDetail() {
)}
</motion.div>
{/* Metadata — collapsible */}
<motion.div
initial={{ opacity: 0, y: 8 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.25 }}
className="rounded-xl theme-card overflow-hidden"
>
<button
type="button"
onClick={() => setMetaExpanded((v) => !v)}
className="w-full flex items-center justify-between px-5 py-4"
style={{ color: "var(--text-3)" }}
>
<SectionHeading className="mb-0">Metadata</SectionHeading>
{/* Metadata — global toggle */}
<AnimatePresence>
{showMetadata && (
<motion.div
animate={{ rotate: metaExpanded ? 0 : -90 }}
transition={{ duration: 0.15 }}
initial={{ opacity: 0, height: 0 }}
animate={{ opacity: 1, height: "auto" }}
exit={{ opacity: 0, height: 0 }}
transition={{ duration: 0.2 }}
className="overflow-hidden"
>
<ChevronDown
className="w-4 h-4"
strokeWidth={2}
style={{ color: COLOR.dimText }}
/>
</motion.div>
</button>
<AnimatePresence initial={false}>
{metaExpanded && (
<motion.div
initial={{ height: 0, opacity: 0 }}
animate={{ height: "auto", opacity: 1 }}
exit={{ height: 0, opacity: 0 }}
transition={{ duration: 0.2 }}
className="overflow-hidden"
<div
className="rounded-xl p-5"
style={{
background: "rgba(245,158,11,0.04)",
border: "1px solid rgba(245,158,11,0.2)",
}}
>
<div className="px-5 pb-5">
<JsonViewer data={peer.metadata} maxHeight="300px" />
</div>
</motion.div>
)}
</AnimatePresence>
</motion.div>
<SectionHeading style={{ color: COLOR.warning }}>Metadata</SectionHeading>
<JsonViewer data={peer.metadata} maxHeight="300px" />
</div>
</motion.div>
)}
</AnimatePresence>
</>
)}
</div>

View File

@@ -1,7 +1,6 @@
import { Link, useNavigate, useParams } from "@tanstack/react-router";
import { AnimatePresence, motion } from "framer-motion";
import {
ArrowLeft,
Boxes,
ChevronDown,
CircleDot,
@@ -14,6 +13,7 @@ import {
} from "lucide-react";
import { useState } from "react";
import { useDeleteWorkspace, useQueueStatus, useScheduleDream, useWorkspace } from "@/api/queries";
import { Breadcrumb } from "@/components/layout/Breadcrumb";
import { ConfirmDialog } from "@/components/shared/ConfirmDialog";
import { ErrorAlert } from "@/components/shared/ErrorAlert";
import { JsonViewer } from "@/components/shared/JsonViewer";
@@ -22,6 +22,7 @@ import { Button } from "@/components/ui/button";
import { Body, Caption, PageTitle, SectionHeading } from "@/components/ui/typography";
import { ScheduleDreamModal } from "@/components/workspaces/ScheduleDreamModal";
import { useDemo } from "@/hooks/useDemo";
import { useMetadata } from "@/hooks/useMetadata";
import { COLOR } from "@/lib/constants";
const NAV_SECTIONS = [
@@ -53,6 +54,7 @@ const NAV_SECTIONS = [
export function WorkspaceDetail() {
const { mask } = useDemo();
const { showMetadata } = useMetadata();
const { workspaceId } = useParams({ strict: false }) as { workspaceId: string };
const navigate = useNavigate();
@@ -74,14 +76,7 @@ export function WorkspaceDetail() {
return (
<div className="page-container page-container--wide">
<motion.div initial={{ opacity: 0, y: -8 }} animate={{ opacity: 1, y: 0 }}>
<Link
to="/workspaces"
className="inline-flex items-center gap-1.5 text-xs mb-4 transition-colors"
style={{ color: "var(--text-3)" }}
>
<ArrowLeft className="w-3 h-3" strokeWidth={1.5} />
Workspaces
</Link>
<Breadcrumb />
<div className="flex items-start justify-between gap-4 mb-1">
<div className="flex items-center gap-2 min-w-0">
<Boxes
@@ -125,7 +120,7 @@ export function WorkspaceDetail() {
<Link
to={`/workspaces/$workspaceId/${s.to}` as never}
params={{ workspaceId } as never}
className="block rounded-xl p-5 group transition-all theme-card"
className="block h-full rounded-xl p-5 group transition-all theme-card"
>
<Icon
className="w-5 h-5 mb-3"
@@ -309,16 +304,29 @@ export function WorkspaceDetail() {
</motion.div>
)}
{/* Metadata */}
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ delay: 0.38 }}
className="rounded-xl p-5 theme-card"
>
<SectionHeading>Metadata</SectionHeading>
<JsonViewer data={workspace.metadata} />
</motion.div>
{/* Metadata — global toggle */}
<AnimatePresence>
{showMetadata && (
<motion.div
initial={{ opacity: 0, height: 0 }}
animate={{ opacity: 1, height: "auto" }}
exit={{ opacity: 0, height: 0 }}
transition={{ duration: 0.2 }}
className="overflow-hidden"
>
<div
className="rounded-xl p-5"
style={{
background: "rgba(245,158,11,0.04)",
border: "1px solid rgba(245,158,11,0.2)",
}}
>
<SectionHeading style={{ color: COLOR.warning }}>Metadata</SectionHeading>
<JsonViewer data={workspace.metadata} />
</div>
</motion.div>
)}
</AnimatePresence>
</div>
)}
</div>

View File

@@ -0,0 +1,34 @@
import { createContext, type ReactNode, useContext, useState } from "react";
const STORAGE_KEY = "openconcho:show-metadata";
interface MetadataContextValue {
showMetadata: boolean;
toggle: () => void;
}
const MetadataContext = createContext<MetadataContextValue | null>(null);
export function MetadataProvider({ children }: { children: ReactNode }) {
const [showMetadata, setShowMetadata] = useState<boolean>(
() => localStorage.getItem(STORAGE_KEY) === "true",
);
function toggle() {
setShowMetadata((v) => {
const next = !v;
localStorage.setItem(STORAGE_KEY, String(next));
return next;
});
}
return (
<MetadataContext.Provider value={{ showMetadata, toggle }}>{children}</MetadataContext.Provider>
);
}
export function useMetadataContext(): MetadataContextValue {
const ctx = useContext(MetadataContext);
if (!ctx) throw new Error("useMetadataContext must be used within MetadataProvider");
return ctx;
}

View File

@@ -0,0 +1 @@
export { useMetadataContext as useMetadata } from "@/context/MetadataContext";

View File

@@ -3,6 +3,7 @@ import { createRouter, RouterProvider } from "@tanstack/react-router";
import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import { DemoProvider } from "./context/DemoContext";
import { MetadataProvider } from "./context/MetadataContext";
import { initDeepLinks } from "./lib/deep-link";
import { routeTree } from "./routeTree.gen";
import "./index.css";
@@ -37,7 +38,9 @@ createRoot(root).render(
<StrictMode>
<QueryClientProvider client={queryClient}>
<DemoProvider>
<RouterProvider router={router} />
<MetadataProvider>
<RouterProvider router={router} />
</MetadataProvider>
</DemoProvider>
</QueryClientProvider>
</StrictMode>,