From 7b72592174b8095119e0c4203fa619a72ace03de Mon Sep 17 00:00:00 2001 From: unknown Date: Tue, 16 Jun 2026 23:42:09 +0100 Subject: [PATCH 1/2] finished Implementing changes --- .../DemoAdminDashboard.tsx | 513 +++++++++++++++--- src/features/demo-admin-dashboard/README.md | 2 +- .../__tests__/localStorageAdapter.test.ts | 6 +- .../__tests__/presets.test.ts | 93 ++++ .../demo-admin-dashboard/fixtures/presets.ts | 237 ++++++++ src/features/demo-admin-dashboard/index.ts | 8 +- src/features/demo-admin-dashboard/types.ts | 73 +++ 7 files changed, 860 insertions(+), 72 deletions(-) create mode 100644 src/features/demo-admin-dashboard/__tests__/presets.test.ts create mode 100644 src/features/demo-admin-dashboard/fixtures/presets.ts diff --git a/src/features/demo-admin-dashboard/DemoAdminDashboard.tsx b/src/features/demo-admin-dashboard/DemoAdminDashboard.tsx index f3d2a006..bbc64817 100644 --- a/src/features/demo-admin-dashboard/DemoAdminDashboard.tsx +++ b/src/features/demo-admin-dashboard/DemoAdminDashboard.tsx @@ -1,15 +1,20 @@ import { useState, type ReactNode } from "react"; -import { Activity, BarChart3, FileText, LayoutDashboard, Mail, Shield, Users } from "lucide-react"; +import { Activity, BarChart3, FileText, LayoutDashboard, Mail, Shield, Users, X } from "lucide-react"; import { cn } from "@/lib/utils"; import type { DashboardNavItem, DashboardSection, DemoAdminDashboardProps, StatCard, + PresetId, + PresetAccount, + PresetMail, + PresetAuditEvent, } from "./types"; import { TemplatePicker } from "./templates"; +import { PRESET_SCENARIOS } from "./fixtures/presets"; -// ─── Deterministic fake data ────────────────────────────────────────────────── +// ─── Default Deterministic fake data ────────────────────────────────────────── const NAV_ITEMS: DashboardNavItem[] = [ { id: "overview", label: "Overview", description: "High-level demo system status" }, @@ -26,21 +31,69 @@ const OVERVIEW_STATS: StatCard[] = [ { label: "Total Postage (XLM)", value: "1,240.5", delta: "+45.2" }, ]; -const ACCOUNTS_FAKE: { name: string; address: string; balance: string; type: string }[] = [ +const ACCOUNTS_FAKE: PresetAccount[] = [ { name: "Alice Demo", address: "GABCD...1234", balance: "500.0 XLM", type: "User" }, { name: "Bob Demo", address: "GBCDE...2345", balance: "320.0 XLM", type: "User" }, { name: "Relay East", address: "GCDEF...3456", balance: "1,200.0 XLM", type: "Relay" }, { name: "Relay West", address: "GDEFG...4567", balance: "980.0 XLM", type: "Relay" }, ]; -const MAIL_FIXTURES: { subject: string; status: string; folder: string }[] = [ - { subject: "Welcome to Stealth", status: "delivered", folder: "inbox" }, - { subject: "Invoice #1042", status: "pending", folder: "requests" }, - { subject: "Meeting notes", status: "delivered", folder: "inbox" }, - { subject: "Newsletter #47", status: "held", folder: "spam" }, +const MAIL_FIXTURES: PresetMail[] = [ + { + subject: "Welcome to Stealth", + status: "delivered", + folder: "inbox", + from: "Stealth Team", + email: "welcome*stealth.demo", + body: "Hi there,\n\nYour Stealth mailbox is set up. You decide who can reach you: trusted contacts arrive instantly, everyone else follows the policy you choose.\n\nReply any time to start a conversation.\n\n— The Stealth demo team", + time: "9:42 AM", + unread: true, + starred: true, + labels: ["onboarding", "intro"], + avatarColor: "#5b6470", + }, + { + subject: "Invoice #1042", + status: "pending", + folder: "requests", + from: "Vendor Demo", + email: "billing*stealth.demo", + body: "Please find attached your invoice #1042.\n\nAmount: 120 XLM\nStatus: pending", + time: "9:18 AM", + unread: true, + starred: false, + labels: ["invoice"], + avatarColor: "#7a8290", + }, + { + subject: "Meeting notes", + status: "delivered", + folder: "inbox", + from: "Bob Demo", + email: "bob*stealth.demo", + body: "Here are the meeting notes from today's discussion.", + time: "8:57 AM", + unread: false, + starred: false, + labels: ["notes"], + avatarColor: "#4d5560", + }, + { + subject: "Newsletter #47", + status: "held", + folder: "spam", + from: "Newsletter System", + email: "digest*stealth.demo", + body: "Your weekly newsletter is ready to view.", + time: "Yesterday", + unread: false, + starred: false, + labels: ["digest"], + avatarColor: "#9098a4", + }, ]; -const AUDIT_EVENTS_FAKE: { action: string; actor: string; timestamp: string }[] = [ +const AUDIT_EVENTS_FAKE: PresetAuditEvent[] = [ { action: "Session started", actor: "demo-user-1", timestamp: "2026-06-16T09:00:00Z" }, { action: "Policy default changed to request", @@ -48,7 +101,7 @@ const AUDIT_EVENTS_FAKE: { action: string; actor: string; timestamp: string }[] timestamp: "2026-06-16T09:05:00Z", }, { - action: "Sender approved: alice*stealth.xyz", + action: "Sender approved: alice*stealth.demo", actor: "demo-user-1", timestamp: "2026-06-16T09:10:00Z", }, @@ -67,14 +120,22 @@ const SECTION_ICON: Record = { // ─── Content region components ──────────────────────────────────────────────── -function OverviewContent() { +function OverviewContent({ + activePresetId, + setActivePresetId, + stats, +}: { + activePresetId: PresetId; + setActivePresetId: (id: PresetId) => void; + stats: StatCard[]; +}) { return (

Summary of the demo environment. All data is synthetic and resets on each page load.

- {OVERVIEW_STATS.map((stat) => ( + {stats.map((stat) => (
))}
+ + {/* Preset Selector */} +
+
+

+ + Protocol Scenario Presets +

+

+ Select a preset to populate the dashboard tables with simulated ledger states, relay nodes, and pending proof mail flows. +

+
+
+ {[ + { id: "none" as const, name: "Default System", desc: "Standard demo system stats and static fixtures." }, + { id: "relay-verification" as const, name: "Relay Verification", desc: "Simulates registering and verifying a new relay node." }, + { id: "proof-pending" as const, name: "Proof Pending", desc: "Simulates an on-chain cryptographic proof generation delay." }, + { id: "receipt-settlement" as const, name: "Receipt Settlement", desc: "Simulates postage fees and read receipts confirming on-chain." }, + ].map((preset) => { + const active = activePresetId === preset.id; + return ( + + ); + })} +
+
); } -function AccountsContent() { +function AccountsContent({ + accounts, + selectedAccountAddress, + setSelectedAccountAddress, +}: { + accounts: PresetAccount[]; + selectedAccountAddress: string | null; + setSelectedAccountAddress: (addr: string | null) => void; +}) { return (

- Demo Stellar accounts used for populating the inbox UI. + Demo Stellar accounts used for populating the inbox UI. Rows with metadata can be clicked to inspect details.

@@ -108,16 +230,50 @@ function AccountsContent() { - {ACCOUNTS_FAKE.map((acct) => ( - - - - - - - ))} + {accounts.map((acct) => { + const hasMetadata = !!acct.relayMetadata; + const isSelected = selectedAccountAddress === acct.address; + return ( + { + if (hasMetadata) { + setSelectedAccountAddress(isSelected ? null : acct.address); + } + }} + className={cn( + "border-b border-white/[0.04] last:border-0 transition", + hasMetadata ? "cursor-pointer hover:bg-white/[0.02]" : "", + isSelected ? "bg-white/[0.04]" : "" + )} + > + + + + + + ); + })}
{acct.name} - {acct.address} - {acct.balance}{acct.type}
+
+ {acct.name} + {hasMetadata && ( + + Inspectable + + )} +
+
+ {acct.address} + {acct.balance} + + {acct.type} + +
@@ -125,11 +281,19 @@ function AccountsContent() { ); } -function MailContent() { +function MailContent({ + mail, + selectedMailSubject, + setSelectedMailSubject, +}: { + mail: PresetMail[]; + selectedMailSubject: string | null; + setSelectedMailSubject: (subject: string | null) => void; +}) { return (

- Mail fixtures available for populating the demo inbox. + Mail fixtures available for populating the demo inbox. Rows with cryptographic proofs can be clicked to inspect ledger details.

@@ -141,24 +305,54 @@ function MailContent() { - {MAIL_FIXTURES.map((mail, i) => ( - - - - - - ))} + {mail.map((item, i) => { + const hasMetadata = !!item.proofMetadata; + const isSelected = selectedMailSubject === item.subject; + return ( + { + if (hasMetadata) { + setSelectedMailSubject(isSelected ? null : item.subject); + } + }} + className={cn( + "border-b border-white/[0.04] last:border-0 transition", + hasMetadata ? "cursor-pointer hover:bg-white/[0.02]" : "", + isSelected ? "bg-white/[0.04]" : "" + )} + > + + + + + ); + })}
{mail.subject} - - {mail.status} - - {mail.folder}
+
+
+ {item.subject} + {hasMetadata && ( + + Has Proof + + )} +
+ + From: {item.from} ({item.email}) + +
+
+ + {item.status} + + {item.folder}
@@ -166,14 +360,18 @@ function MailContent() { ); } -function AuditContent() { +function AuditContent({ + auditEvents, +}: { + auditEvents: PresetAuditEvent[]; +}) { return (

Recent demo protocol events. No real user data or message body content is recorded.

- {AUDIT_EVENTS_FAKE.map((evt, i) => ( + {auditEvents.map((evt, i) => (
; } -const SECTION_CONTENT: Record ReactNode> = { - overview: OverviewContent, - accounts: AccountsContent, - mail: MailContent, - templates: TemplatesContent, - audit: AuditContent, -}; - // ─── Dashboard Shell ────────────────────────────────────────────────────────── export function DemoAdminDashboard({ className }: DemoAdminDashboardProps) { const [activeSection, setActiveSection] = useState("overview"); + const [activePresetId, setActivePresetId] = useState("none"); + const [selectedAccountAddress, setSelectedAccountAddress] = useState(null); + const [selectedMailSubject, setSelectedMailSubject] = useState(null); + + const activePreset = PRESET_SCENARIOS.find((p) => p.id === activePresetId); + + const stats = activePreset ? activePreset.stats : OVERVIEW_STATS; + const accounts = activePreset ? activePreset.accounts : ACCOUNTS_FAKE; + const mail = activePreset ? activePreset.mail : MAIL_FIXTURES; + const auditEvents = activePreset ? activePreset.auditEvents : AUDIT_EVENTS_FAKE; + + const selectedAccount = accounts.find((a) => a.address === selectedAccountAddress); + const selectedMail = mail.find((m) => m.subject === selectedMailSubject); + + const handleSectionChange = (section: DashboardSection) => { + setActiveSection(section); + setSelectedAccountAddress(null); + setSelectedMailSubject(null); + }; + const Icon = SECTION_ICON[activeSection]; - const ContentComponent = SECTION_CONTENT[activeSection]; return (
@@ -233,9 +442,16 @@ export function DemoAdminDashboard({ className }: DemoAdminDashboardProps) {

- - Demo - +
+ {activePresetId !== "none" && ( + + Preset: {activePreset?.name} + + )} + + Demo + +
{/* ── Navigation slots ── */} @@ -253,7 +469,7 @@ export function DemoAdminDashboard({ className }: DemoAdminDashboardProps) { role="tab" aria-selected={isActive} aria-label={item.description} - onClick={() => setActiveSection(item.id)} + onClick={() => handleSectionChange(item.id)} className={cn( "flex items-center gap-1.5 rounded-lg px-3 py-1.5 text-xs font-medium transition", isActive @@ -276,14 +492,183 @@ export function DemoAdminDashboard({ className }: DemoAdminDashboardProps) { >
{/* Section header */} -
- -

{activeSection}

+
+
+ +

{activeSection}

+
+ {activePresetId !== "none" && ( + + Simulated {activePreset?.name} flow active + + )}
- + {activeSection === "overview" && ( + { + setActivePresetId(id); + setSelectedAccountAddress(null); + setSelectedMailSubject(null); + }} + stats={stats} + /> + )} + + {activeSection === "accounts" && ( + + )} + + {activeSection === "mail" && ( + + )} + + {activeSection === "templates" && } + + {activeSection === "audit" && }
+ + {/* ── Slide-out Inspection Panel (Drawer for Account/Relay Metadata) ── */} + {selectedAccount && selectedAccount.relayMetadata && ( +
+
+
+
+

Relay Node Inspector

+

{selectedAccount.name}

+
+ +
+ +
+
+ Relay Registry Metadata +
+
+
+ Node Address: + {selectedAccount.relayMetadata.nodeUri} +
+
+ Stellar Account: + {selectedAccount.address} +
+
+ Routing Latency: + {selectedAccount.relayMetadata.latency} +
+
+ Signature Scheme: + {selectedAccount.relayMetadata.signatureScheme} +
+
+ Owner Account: + {selectedAccount.relayMetadata.owner} +
+
+ Verification Status: + + {selectedAccount.relayMetadata.status} + +
+
+
+
+ +
+ )} + + {/* ── Slide-out Inspection Panel (Drawer for Cryptographic Ledger Proof) ── */} + {selectedMail && selectedMail.proofMetadata && ( +
+
+
+
+

Ledger Proof Inspector

+

{selectedMail.subject}

+
+ +
+ +
+
+ Cryptographic Details +
+
+
+ Message Hash: + {selectedMail.proofMetadata.messageHash} +
+
+ Payment Preimage Hash: + {selectedMail.proofMetadata.paymentHash} +
+
+ Soroban Contract: + {selectedMail.proofMetadata.contractAddress} +
+
+ Relay Latency: + {selectedMail.proofMetadata.latency} +
+
+ Cryptographic Signature: + {selectedMail.proofMetadata.signature} +
+
+ Postage State: + + {selectedMail.proofMetadata.postageStatus} + +
+
+
+
+ +
+ )}
); } diff --git a/src/features/demo-admin-dashboard/README.md b/src/features/demo-admin-dashboard/README.md index 5239dfd1..1505a6dd 100644 --- a/src/features/demo-admin-dashboard/README.md +++ b/src/features/demo-admin-dashboard/README.md @@ -26,7 +26,7 @@ npx vitest run src/features/demo-admin-dashboard/__tests__/layout.test.ts ``` The fixture data in `fixtures/demoData.ts` is deterministic, fake, and safe for public repository review. -======= + This folder is the implementation boundary for the admin dashboard used to populate and manage demo data in the Stealth demo inbox UI. Contributors working on demo-admin issues should keep new dashboard code, local state helpers, fixtures, validators, UI components, test utilities, and documentation inside: diff --git a/src/features/demo-admin-dashboard/__tests__/localStorageAdapter.test.ts b/src/features/demo-admin-dashboard/__tests__/localStorageAdapter.test.ts index 498277a3..fa66e8e7 100644 --- a/src/features/demo-admin-dashboard/__tests__/localStorageAdapter.test.ts +++ b/src/features/demo-admin-dashboard/__tests__/localStorageAdapter.test.ts @@ -18,15 +18,19 @@ const mockStorage = (() => { }; })(); -// Helper to attach mock to global window +// Helper to attach mock to global window and globalThis function setWindowStorage() { // @ts-ignore global.window = { localStorage: mockStorage } as any; + // @ts-ignore + global.localStorage = mockStorage; } function clearWindowStorage() { // @ts-ignore delete (global as any).window; + // @ts-ignore + delete (global as any).localStorage; } describe('localStorageAdapter', () => { diff --git a/src/features/demo-admin-dashboard/__tests__/presets.test.ts b/src/features/demo-admin-dashboard/__tests__/presets.test.ts new file mode 100644 index 00000000..0cf9a408 --- /dev/null +++ b/src/features/demo-admin-dashboard/__tests__/presets.test.ts @@ -0,0 +1,93 @@ +import { describe, expect, it } from "vitest"; +import { PRESET_SCENARIOS } from "../fixtures/presets"; + +describe("demo admin dashboard presets", () => { + it("defines the three required scenario flows", () => { + const ids = PRESET_SCENARIOS.map((p) => p.id); + expect(ids).toContain("relay-verification"); + expect(ids).toContain("proof-pending"); + expect(ids).toContain("receipt-settlement"); + expect(PRESET_SCENARIOS.length).toBe(3); + }); + + it("contains deterministic and valid stats, accounts, mail, and audit logs for each scenario", () => { + for (const scenario of PRESET_SCENARIOS) { + expect(scenario.name.trim()).not.toBe(""); + expect(scenario.description.trim()).not.toBe(""); + expect(scenario.stats.length).toBeGreaterThan(0); + expect(scenario.accounts.length).toBeGreaterThan(0); + expect(scenario.mail.length).toBeGreaterThan(0); + expect(scenario.auditEvents.length).toBeGreaterThan(0); + + // Verify stats shape + for (const stat of scenario.stats) { + expect(stat.label.trim()).not.toBe(""); + expect(stat.value.trim()).not.toBe(""); + } + + // Verify accounts shape + for (const acct of scenario.accounts) { + expect(acct.name.trim()).not.toBe(""); + expect(acct.address.trim()).not.toBe(""); + expect(acct.balance.trim()).not.toBe(""); + expect(acct.type.trim()).not.toBe(""); + } + + // Verify audit events shape + for (const event of scenario.auditEvents) { + expect(event.action.trim()).not.toBe(""); + expect(event.actor.trim()).not.toBe(""); + expect(event.timestamp.trim()).not.toBe(""); + } + } + }); + + it("uses only safe, fake demo emails", () => { + for (const scenario of PRESET_SCENARIOS) { + for (const item of scenario.mail) { + expect(item.email).toMatch(/(\*stealth\.demo|@example\.(com|org))$/); + } + } + }); + + it("attaches required relay verification metadata in relay verification scenario", () => { + const relayVerification = PRESET_SCENARIOS.find((p) => p.id === "relay-verification"); + expect(relayVerification).toBeDefined(); + + const pendingRelay = relayVerification?.accounts.find((a) => a.name === "Relay Node 07"); + expect(pendingRelay).toBeDefined(); + expect(pendingRelay?.relayMetadata).toBeDefined(); + expect(pendingRelay?.relayMetadata?.status).toBe("pending"); + expect(pendingRelay?.relayMetadata?.nodeUri).toBe("relay07*stealth.demo"); + + const verificationMail = relayVerification?.mail.find((m) => m.subject === "Your relay verification code"); + expect(verificationMail).toBeDefined(); + expect(verificationMail?.status).toBe("pending"); + expect(verificationMail?.folder).toBe("pending"); + expect(verificationMail?.proofMetadata).toBeDefined(); + }); + + it("attaches required proof pending metadata in proof pending scenario", () => { + const proofPending = PRESET_SCENARIOS.find((p) => p.id === "proof-pending"); + expect(proofPending).toBeDefined(); + + const pendingMail = proofPending?.mail.find((m) => m.subject === "Soroban proof generation pending"); + expect(pendingMail).toBeDefined(); + expect(pendingMail?.status).toBe("pending"); + expect(pendingMail?.folder).toBe("pending"); + expect(pendingMail?.proofMetadata).toBeDefined(); + expect(pendingMail?.proofMetadata?.postageStatus).toBe("pending"); + }); + + it("attaches required receipt settlement metadata in receipt settlement scenario", () => { + const receiptSettlement = PRESET_SCENARIOS.find((p) => p.id === "receipt-settlement"); + expect(receiptSettlement).toBeDefined(); + + const deliveryReceiptMail = receiptSettlement?.mail.find((m) => m.subject === "Delivery receipt settled"); + expect(deliveryReceiptMail).toBeDefined(); + expect(deliveryReceiptMail?.status).toBe("delivered"); + expect(deliveryReceiptMail?.folder).toBe("receipts"); + expect(deliveryReceiptMail?.proofMetadata).toBeDefined(); + expect(deliveryReceiptMail?.proofMetadata?.postageStatus).toBe("settled"); + }); +}); diff --git a/src/features/demo-admin-dashboard/fixtures/presets.ts b/src/features/demo-admin-dashboard/fixtures/presets.ts new file mode 100644 index 00000000..c6651c79 --- /dev/null +++ b/src/features/demo-admin-dashboard/fixtures/presets.ts @@ -0,0 +1,237 @@ +import type { PresetScenario } from "../types"; + +export const PRESET_SCENARIOS: PresetScenario[] = [ + { + id: "relay-verification", + name: "Relay Verification", + description: "Simulates registration, OTP generation, and identity verification for a new message routing relay.", + stats: [ + { label: "Active Accounts", value: "13", delta: "+3" }, + { label: "Messages Sent", value: "847", delta: "+12%" }, + { label: "Pending Requests", value: "4", delta: "+1" }, + { label: "Total Postage (XLM)", value: "1,240.5", delta: "+45.2" }, + ], + accounts: [ + { name: "Alice Demo", address: "GABCD...1234", balance: "500.0 XLM", type: "User" }, + { name: "Bob Demo", address: "GBCDE...2345", balance: "320.0 XLM", type: "User" }, + { name: "Relay East", address: "GCDEF...3456", balance: "1,200.0 XLM", type: "Relay" }, + { name: "Relay West", address: "GDEFG...4567", balance: "980.0 XLM", type: "Relay" }, + { + name: "Relay Node 07", + address: "GCRLY...N007", + balance: "1,500.0 XLM", + type: "Relay (Pending)", + relayMetadata: { + nodeUri: "relay07*stealth.demo", + latency: "42ms", + signatureScheme: "Ed25519", + status: "pending", + owner: "GDREL...OWNR", + }, + }, + ], + mail: [ + { + subject: "Your relay verification code", + status: "pending", + folder: "pending", + from: "Relay Node 07", + email: "relay07*stealth.demo", + body: "Hi Eve,\n\nA new relay session is requesting authorization on Node 07. Use the one-time passkey below to confirm it's you.\n\nYour OTP code: 482 015\n\nThis code expires in 10 minutes. If you didn't initiate this, ignore the message and your session will stay locked.\n\n— Relay Node 07", + time: "Just now", + unread: true, + starred: false, + labels: ["Security", "OTP"], + avatarColor: "#4d5560", + verifiedSender: false, + proofMetadata: { + messageHash: "0x0303030303030303030303030303030303030303030303030303030303d8c7e9", + paymentHash: "0x03pay03pay03pay03pay03pay03pay03pay03pay03pay03pay03payf12a3d", + diagnosticId: "d1f038c7-4b1d-44a6-8968-3e5f49230503", + contractAddress: "CB3333333333333333333333333333333333333333333333339999", + latency: "42ms", + signature: "Ed25519 [0x0303030303030303f31b]", + postageStatus: "pending", + }, + }, + { + subject: "Welcome to Stealth", + status: "delivered", + folder: "inbox", + from: "Stealth Team", + email: "welcome*stealth.demo", + body: "Hi there,\n\nYour Stealth mailbox is set up. You decide who can reach you: trusted contacts arrive instantly, everyone else follows the policy you choose.\n\nReply any time to start a conversation.\n\n— The Stealth demo team", + time: "9:42 AM", + unread: false, + starred: true, + labels: ["Onboarding"], + avatarColor: "#5b6470", + }, + ], + auditEvents: [ + { action: "Relay Node 07 registration initiated", actor: "relay07*stealth.demo", timestamp: "2026-06-16T12:00:00Z" }, + { action: "Authorization OTP generated and dispatched", actor: "system", timestamp: "2026-06-16T12:01:00Z" }, + { action: "Relay verification check pending for GCRLY...N007", actor: "system", timestamp: "2026-06-16T12:02:00Z" }, + ], + }, + { + id: "proof-pending", + name: "Proof Pending", + description: "Simulates an incoming message bridge event holding for cryptographic proof generation on the Stellar ledger.", + stats: [ + { label: "Active Accounts", value: "13", delta: "+3" }, + { label: "Messages Sent", value: "847", delta: "+12%" }, + { label: "Pending Requests", value: "4", delta: "+1" }, + { label: "Total Postage (XLM)", value: "1,240.5", delta: "+45.2" }, + ], + accounts: [ + { name: "Alice Demo", address: "GABCD...1234", balance: "500.0 XLM", type: "User" }, + { name: "Bob Demo", address: "GBCDE...2345", balance: "320.0 XLM", type: "User" }, + { name: "Relay East", address: "GCDEF...3456", balance: "1,200.0 XLM", type: "Relay" }, + { name: "Relay West", address: "GDEFG...4567", balance: "980.0 XLM", type: "Relay" }, + { + name: "Stellar Bridge Relay", + address: "GCSBR...BREG", + balance: "10,000.0 XLM", + type: "Relay (Active)", + relayMetadata: { + nodeUri: "bridge*stealth.demo", + latency: "15ms", + signatureScheme: "Ed25519", + status: "verified", + owner: "GDBRG...OWNR", + }, + }, + ], + mail: [ + { + subject: "Soroban proof generation pending", + status: "pending", + folder: "pending", + from: "Legacy Bridge", + email: "bridge*stealth.demo", + body: "This message was bridged from SMTP and is currently waiting for a Soroban cryptographic validity proof to be generated and registered on-chain.\n\nTransaction: 0x5b39...aef2\nRelay: bridge*stealth.demo", + time: "5m ago", + unread: true, + starred: false, + labels: ["Bridge", "Pending"], + avatarColor: "#7a8290", + verifiedSender: false, + proofMetadata: { + messageHash: "0x0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0ed8c7e9", + paymentHash: "0x0epay0epay0epay0epay0epay0epay0epay0epay0epay0epay0epayf12a3d", + diagnosticId: "d1f038c7-4b1d-44a6-8968-3e5f4923050e", + contractAddress: "CBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB9999", + latency: "84ms", + signature: "Ed25519 [0x0e0e0e0e0e0e0e0e0e0e0ed8c7e9f31b]", + postageStatus: "pending", + }, + }, + { + subject: "Message request awaiting approval", + status: "pending", + folder: "requests", + from: "Anonymous Sender", + email: "gckn...n4xq@example.com", + body: "This sender paid postage but is not in your trusted contacts yet.\n\nPostage paid: 1.0 XLM\nPreimage status: pending", + time: "10m ago", + unread: true, + starred: false, + labels: ["Request", "Paid"], + avatarColor: "#3d434d", + postageAmount: "10000000", + verifiedSender: false, + proofMetadata: { + messageHash: "0x0505050505050505050505050505050505050505050505050505050505d8c7e9", + paymentHash: "0x05pay05pay05pay05pay05pay05pay05pay05pay05pay05pay05payf12a3d", + diagnosticId: "d1f038c7-4b1d-44a6-8968-3e5f49230505", + contractAddress: "CB5555555555555555555555555555555555555555555555559999", + latency: "62ms", + signature: "Ed25519 [0x0505050505050505f31b]", + postageStatus: "pending", + }, + }, + ], + auditEvents: [ + { action: "Incoming bridge message captured", actor: "bridge*stealth.demo", timestamp: "2026-06-16T12:10:00Z" }, + { action: "Postage preimage submitted to escrow contract", actor: "bridge*stealth.demo", timestamp: "2026-06-16T12:11:00Z" }, + { action: "On-chain Soroban ledger proof check scheduled", actor: "system", timestamp: "2026-06-16T12:12:00Z" }, + ], + }, + { + id: "receipt-settlement", + name: "Receipt Settlement", + description: "Simulates the successful settlement of postage fees and read confirmation receipts on the Soroban smart contract ledger.", + stats: [ + { label: "Active Accounts", value: "12", delta: "0" }, + { label: "Messages Sent", value: "849", delta: "+15%" }, + { label: "Pending Requests", value: "3", delta: "-2" }, + { label: "Total Postage (XLM)", value: "1,240.5001", delta: "+0.0001" }, + ], + accounts: [ + { name: "Alice Demo", address: "GABCD...1234", balance: "500.0 XLM", type: "User" }, + { name: "Bob Demo", address: "GBCDE...2345", balance: "320.0 XLM", type: "User" }, + { name: "Relay East", address: "GCDEF...3456", balance: "1,200.0001 XLM", type: "Relay" }, + { name: "Relay West", address: "GDEFG...4567", balance: "980.0 XLM", type: "Relay" }, + { + name: "Receipt Contract", + address: "CCLC7...RECP", + balance: "25,000.0 XLM", + type: "Soroban Contract", + }, + ], + mail: [ + { + subject: "Delivery receipt settled", + status: "delivered", + folder: "receipts", + from: "Receipt Contract", + email: "receipts*stealth.demo", + body: "Delivery receipt settled.\n\nMessage: 48fb...c29a\nContract: CCLC7...RECP\nEvent: read_proof\nFee: 0.00002 XLM\n\nPostage is now settled on-chain.", + time: "1h ago", + unread: false, + starred: false, + labels: ["Receipt", "Soroban"], + avatarColor: "#9098a4", + verifiedSender: true, + proofMetadata: { + messageHash: "0x0707070707070707070707070707070707070707070707070707070707d8c7e9", + paymentHash: "0x07pay07pay07pay07pay07pay07pay07pay07pay07pay07pay07payf12a3d", + diagnosticId: "d1f038c7-4b1d-44a6-8968-3e5f49230507", + contractAddress: "CB7777777777777777777777777777777777777777777777779999", + latency: "24ms", + signature: "Ed25519 [0x0707070707070707f31b]", + postageStatus: "settled", + }, + }, + { + subject: "Postage settled for your message", + status: "delivered", + folder: "receipts", + from: "Billing System", + email: "billing*stealth.demo", + body: "Your message postage has settled.\n\nAmount: 0.0001 XLM\nStatus: settled\nReference: demo-memo-0001\n\nThis is a demo receipt and carries no real value.", + time: "2h ago", + unread: false, + starred: false, + labels: ["Postage", "Billing"], + avatarColor: "#3d434d", + verifiedSender: true, + proofMetadata: { + messageHash: "0x0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0ad8c7e9", + paymentHash: "0x0apay0apay0apay0apay0apay0apay0apay0apay0apay0apay0apayf12a3d", + diagnosticId: "d1f038c7-4b1d-44a6-8968-3e5f4923050a", + contractAddress: "CBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA9999", + latency: "35ms", + signature: "Ed25519 [0x0a0a0a0a0a0a0a0a0a0a0af31b]", + postageStatus: "settled", + }, + }, + ], + auditEvents: [ + { action: "Soroban delivery receipt contract invoked: CCLC7...RECP", actor: "system", timestamp: "2026-06-16T11:00:00Z" }, + { action: "Cryptographic read proof signature verified on-chain", actor: "system", timestamp: "2026-06-16T11:02:00Z" }, + { action: "Postage fee of 0.0001 XLM released to Relay East", actor: "system", timestamp: "2026-06-16T11:05:00Z" }, + ], + }, +]; diff --git a/src/features/demo-admin-dashboard/index.ts b/src/features/demo-admin-dashboard/index.ts index f149e7f6..82e4d94b 100644 --- a/src/features/demo-admin-dashboard/index.ts +++ b/src/features/demo-admin-dashboard/index.ts @@ -1,5 +1,5 @@ - -export { DemoAdminDashboard } from "./components/DemoAdminDashboard"; +export { DemoAdminDashboard } from "./DemoAdminDashboard"; +export { DemoAdminDashboard as DemoAdminLayoutDashboard } from "./components/DemoAdminDashboard"; export { ADMIN_DASHBOARD_MIN_SUPPORTED_WIDTH, getAdminDashboardBreakpoint, @@ -16,14 +16,10 @@ export type { AdminDashboardLayoutCheck, AdminDashboardPanel, AdminDashboardWidthNote, -======= -export { DemoAdminDashboard } from "./DemoAdminDashboard"; -export type { DashboardNavItem, DashboardSection, DemoAdminDashboardProps, StatCard, - } from "./types"; export { diff --git a/src/features/demo-admin-dashboard/types.ts b/src/features/demo-admin-dashboard/types.ts index 5885c27d..b2ecfa03 100644 --- a/src/features/demo-admin-dashboard/types.ts +++ b/src/features/demo-admin-dashboard/types.ts @@ -59,3 +59,76 @@ export interface StatCard { delta?: string; } +export type PresetId = "none" | "relay-verification" | "proof-pending" | "receipt-settlement"; + +export interface PresetAccount { + name: string; + address: string; + balance: string; + type: string; + relayMetadata?: { + nodeUri: string; + latency: string; + signatureScheme: string; + status: "verified" | "pending" | "failed"; + owner: string; + }; +} + +export interface PresetMail { + subject: string; + status: string; + folder: string; + from: string; + email: string; + body: string; + time: string; + unread: boolean; + starred: boolean; + labels: string[]; + avatarColor: string; + postageAmount?: string; + verifiedSender?: boolean; + receiptState?: "none" | "pending" | "sent"; + proofMetadata?: { + messageHash: string; + paymentHash: string; + diagnosticId: string; + contractAddress: string; + latency: string; + signature: string; + postageStatus: "pending" | "settled" | "refunded"; + }; +} + +export interface PresetAuditEvent { + action: string; + actor: string; + timestamp: string; +} + +export interface PresetScenario { + id: PresetId; + name: string; + description: string; + stats: StatCard[]; + accounts: PresetAccount[]; + mail: PresetMail[]; + auditEvents: PresetAuditEvent[]; +} + +export interface DemoUser { + id: string; + name: string; + email: string; + role: string; +} + +export interface DemoItem { + id: string; + title: string; + description: string; +} + + + From 90d93361d184126fe65c7ba9abfb4c2024030625 Mon Sep 17 00:00:00 2001 From: unknown Date: Wed, 17 Jun 2026 00:01:56 +0100 Subject: [PATCH 2/2] Implemented Admin Data Table Component --- .../DemoAdminDashboard.tsx | 500 +++++++++++++----- .../__tests__/AdminDataTable.test.ts | 67 +++ .../__tests__/presets.test.ts | 33 ++ .../components/AdminDataTable.tsx | 154 ++++++ .../demo-admin-dashboard/fixtures/presets.ts | 71 +++ src/features/demo-admin-dashboard/index.ts | 2 + src/features/demo-admin-dashboard/types.ts | 30 +- 7 files changed, 718 insertions(+), 139 deletions(-) create mode 100644 src/features/demo-admin-dashboard/__tests__/AdminDataTable.test.ts create mode 100644 src/features/demo-admin-dashboard/components/AdminDataTable.tsx diff --git a/src/features/demo-admin-dashboard/DemoAdminDashboard.tsx b/src/features/demo-admin-dashboard/DemoAdminDashboard.tsx index bbc64817..62710ca5 100644 --- a/src/features/demo-admin-dashboard/DemoAdminDashboard.tsx +++ b/src/features/demo-admin-dashboard/DemoAdminDashboard.tsx @@ -1,5 +1,5 @@ import { useState, type ReactNode } from "react"; -import { Activity, BarChart3, FileText, LayoutDashboard, Mail, Shield, Users, X } from "lucide-react"; +import { Activity, BarChart3, FileText, LayoutDashboard, Mail, Shield, Users, X, Paperclip, Calendar } from "lucide-react"; import { cn } from "@/lib/utils"; import type { DashboardNavItem, @@ -10,9 +10,12 @@ import type { PresetAccount, PresetMail, PresetAuditEvent, + PresetAttachment, + PresetEvent, } from "./types"; import { TemplatePicker } from "./templates"; import { PRESET_SCENARIOS } from "./fixtures/presets"; +import { AdminDataTable, type Column } from "./components/AdminDataTable"; // ─── Default Deterministic fake data ────────────────────────────────────────── @@ -20,6 +23,8 @@ const NAV_ITEMS: DashboardNavItem[] = [ { id: "overview", label: "Overview", description: "High-level demo system status" }, { id: "accounts", label: "Accounts", description: "Demo Stellar accounts and balances" }, { id: "mail", label: "Mail", description: "Demo mail fixtures and delivery states" }, + { id: "attachments", label: "Attachments", description: "Demo mail attachment fixtures" }, + { id: "events", label: "Events", description: "Demo calendar and protocol events" }, { id: "templates", label: "Templates", description: "Pick message templates to populate drafts" }, { id: "audit", label: "Audit", description: "Demo protocol event log" }, ]; @@ -108,12 +113,54 @@ const AUDIT_EVENTS_FAKE: PresetAuditEvent[] = [ { action: "Postage refunded for msg_abc123", actor: "system", timestamp: "2026-06-16T09:12:00Z" }, ]; +const ATTACHMENTS_FAKE: PresetAttachment[] = [ + { + id: "att-inv-1042", + fileName: "invoice_1042.pdf", + fileSize: "120 KB", + fileType: "PDF Document", + messageSubject: "Invoice #1042", + sender: "Vendor Demo", + }, + { + id: "att-roundtable", + fileName: "roundtable_agenda.pdf", + fileSize: "85 KB", + fileType: "PDF Document", + messageSubject: "You're invited: Stealth demo roundtable", + sender: "events*stealth.demo", + }, +]; + +const EVENTS_FAKE: PresetEvent[] = [ + { + id: "evt-roundtable", + title: "Stealth demo roundtable", + date: "2026-07-09", + time: "3:00 PM", + location: "Demo room", + organizer: "events*stealth.demo", + status: "confirmed", + }, + { + id: "evt-sync", + title: "Weekly Sync", + date: "2026-06-18", + time: "10:00 AM", + location: "Virtual", + organizer: "bob*stealth.demo", + status: "confirmed", + }, +]; + // ─── Section icon map ───────────────────────────────────────────────────────── const SECTION_ICON: Record = { overview: LayoutDashboard, accounts: Users, mail: Mail, + attachments: Paperclip, + events: Calendar, templates: FileText, audit: Activity, }; @@ -214,69 +261,88 @@ function AccountsContent({ selectedAccountAddress: string | null; setSelectedAccountAddress: (addr: string | null) => void; }) { + const columns: Column[] = [ + { + key: "name", + header: "Name", + sortable: true, + render: (acct) => ( +
+ {acct.name} + {acct.relayMetadata && ( + + Inspectable + + )} +
+ ), + }, + { + key: "address", + header: "Address", + sortable: true, + render: (acct) => {acct.address}, + }, + { + key: "balance", + header: "Balance", + sortable: true, + sortValue: (acct) => parseFloat(acct.balance.replace(/[^0-9.]/g, "")), + render: (acct) => {acct.balance}, + }, + { + key: "type", + header: "Type", + sortable: true, + render: (acct) => ( + + {acct.type} + + ), + }, + { + key: "status", + header: "Status", + sortable: true, + sortValue: (acct) => acct.relayMetadata?.status ?? "none", + render: (acct) => { + const status = acct.relayMetadata?.status; + if (!status) return ; + return ( + + {status} + + ); + }, + }, + ]; + return (

Demo Stellar accounts used for populating the inbox UI. Rows with metadata can be clicked to inspect details.

-
- - - - - - - - - - - {accounts.map((acct) => { - const hasMetadata = !!acct.relayMetadata; - const isSelected = selectedAccountAddress === acct.address; - return ( - { - if (hasMetadata) { - setSelectedAccountAddress(isSelected ? null : acct.address); - } - }} - className={cn( - "border-b border-white/[0.04] last:border-0 transition", - hasMetadata ? "cursor-pointer hover:bg-white/[0.02]" : "", - isSelected ? "bg-white/[0.04]" : "" - )} - > - - - - - - ); - })} - -
NameAddressBalanceType
-
- {acct.name} - {hasMetadata && ( - - Inspectable - - )} -
-
- {acct.address} - {acct.balance} - - {acct.type} - -
-
+ { + if (acct.relayMetadata) { + setSelectedAccountAddress(selectedAccountAddress === acct.address ? null : acct.address); + } + }} + selectedRowKey={(acct) => selectedAccountAddress === acct.address} + defaultSortKey="name" + />
); } @@ -290,72 +356,201 @@ function MailContent({ selectedMailSubject: string | null; setSelectedMailSubject: (subject: string | null) => void; }) { + const columns: Column[] = [ + { + key: "subject", + header: "Subject", + sortable: true, + render: (item) => ( +
+
+ {item.subject} + {item.proofMetadata && ( + + Has Proof + + )} +
+ + From: {item.from} ({item.email}) + +
+ ), + }, + { + key: "status", + header: "Status", + sortable: true, + render: (item) => ( + + {item.status} + + ), + }, + { + key: "folder", + header: "Folder", + sortable: true, + render: (item) => {item.folder}, + }, + { + key: "time", + header: "Time", + sortable: true, + }, + ]; + return (

Mail fixtures available for populating the demo inbox. Rows with cryptographic proofs can be clicked to inspect ledger details.

-
- - - - - - - - - - {mail.map((item, i) => { - const hasMetadata = !!item.proofMetadata; - const isSelected = selectedMailSubject === item.subject; - return ( - { - if (hasMetadata) { - setSelectedMailSubject(isSelected ? null : item.subject); - } - }} - className={cn( - "border-b border-white/[0.04] last:border-0 transition", - hasMetadata ? "cursor-pointer hover:bg-white/[0.02]" : "", - isSelected ? "bg-white/[0.04]" : "" - )} - > - - - - - ); - })} - -
SubjectStatusFolder
-
-
- {item.subject} - {hasMetadata && ( - - Has Proof - - )} -
- - From: {item.from} ({item.email}) - -
-
- - {item.status} - - {item.folder}
-
+ { + if (item.proofMetadata) { + setSelectedMailSubject(selectedMailSubject === item.subject ? null : item.subject); + } + }} + selectedRowKey={(item) => selectedMailSubject === item.subject} + defaultSortKey="time" + defaultSortDirection="desc" + /> +
+ ); +} + +function AttachmentsContent({ + attachments, +}: { + attachments: PresetAttachment[]; +}) { + const columns: Column[] = [ + { + key: "fileName", + header: "File Name", + sortable: true, + render: (att) => ( +
+ + {att.fileName} +
+ ), + }, + { + key: "fileSize", + header: "Size", + sortable: true, + }, + { + key: "fileType", + header: "Type", + sortable: true, + render: (att) => ( + + {att.fileType} + + ), + }, + { + key: "messageSubject", + header: "Source Message", + sortable: true, + render: (att) => {att.messageSubject}, + }, + { + key: "sender", + header: "Sender", + sortable: true, + render: (att) => {att.sender}, + }, + ]; + + return ( +
+

+ Deterministic file attachments mock list, extracted from active mail fixtures. +

+ +
+ ); +} + +function EventsContent({ + events, +}: { + events: PresetEvent[]; +}) { + const columns: Column[] = [ + { + key: "title", + header: "Title", + sortable: true, + render: (evt) => ( +
+ + {evt.title} +
+ ), + }, + { + key: "date", + header: "Scheduled Time", + sortable: true, + sortValue: (evt) => new Date(`${evt.date}T${evt.time.replace(" PM", "").replace(" AM", "")}`).getTime(), + render: (evt) => {evt.date} · {evt.time}, + }, + { + key: "location", + header: "Location", + sortable: true, + render: (evt) => {evt.location}, + }, + { + key: "organizer", + header: "Organizer", + sortable: true, + render: (evt) => {evt.organizer}, + }, + { + key: "status", + header: "Status", + sortable: true, + render: (evt) => ( + + {evt.status} + + ), + }, + ]; + + return ( +
+

+ Stellar node registration and verification events. +

+
); } @@ -365,29 +560,48 @@ function AuditContent({ }: { auditEvents: PresetAuditEvent[]; }) { + const columns: Column[] = [ + { + key: "action", + header: "Action", + sortable: true, + render: (evt) => ( +
+ + {evt.action} +
+ ), + }, + { + key: "actor", + header: "Actor", + sortable: true, + render: (evt) => {evt.actor}, + }, + { + key: "timestamp", + header: "Timestamp", + sortable: true, + sortValue: (evt) => new Date(evt.timestamp).getTime(), + render: (evt) => ( + + {new Date(evt.timestamp).toLocaleString()} + + ), + }, + ]; + return (

Recent demo protocol events. No real user data or message body content is recorded.

-
- {auditEvents.map((evt, i) => ( -
-
- -
-
-

{evt.action}

-

- {evt.actor} · {new Date(evt.timestamp).toLocaleTimeString()} -

-
-
- ))} -
+
); } @@ -409,6 +623,8 @@ export function DemoAdminDashboard({ className }: DemoAdminDashboardProps) { const stats = activePreset ? activePreset.stats : OVERVIEW_STATS; const accounts = activePreset ? activePreset.accounts : ACCOUNTS_FAKE; const mail = activePreset ? activePreset.mail : MAIL_FIXTURES; + const attachments = activePreset ? activePreset.attachments : ATTACHMENTS_FAKE; + const events = activePreset ? activePreset.events : EVENTS_FAKE; const auditEvents = activePreset ? activePreset.auditEvents : AUDIT_EVENTS_FAKE; const selectedAccount = accounts.find((a) => a.address === selectedAccountAddress); @@ -532,6 +748,14 @@ export function DemoAdminDashboard({ className }: DemoAdminDashboardProps) { /> )} + {activeSection === "attachments" && ( + + )} + + {activeSection === "events" && ( + + )} + {activeSection === "templates" && } {activeSection === "audit" && } diff --git a/src/features/demo-admin-dashboard/__tests__/AdminDataTable.test.ts b/src/features/demo-admin-dashboard/__tests__/AdminDataTable.test.ts new file mode 100644 index 00000000..33af328c --- /dev/null +++ b/src/features/demo-admin-dashboard/__tests__/AdminDataTable.test.ts @@ -0,0 +1,67 @@ +import { describe, expect, it } from "vitest"; +import { sortData } from "../components/AdminDataTable"; + +interface TestItem { + id: number; + name: string; + count: number; + optional?: string | null; +} + +describe("AdminDataTable sortData helper", () => { + const items: TestItem[] = [ + { id: 1, name: "Charlie", count: 15, optional: "Yes" }, + { id: 2, name: "Alice", count: 42, optional: null }, + { id: 3, name: "Bob", count: 7, optional: "No" }, + ]; + + it("returns original data if sortKey is null", () => { + const result = sortData(items, null, "asc"); + expect(result).toEqual(items); + // Should be a reference match (or at least return the exact same array/elements) + expect(result).toBe(items); + }); + + it("sorts string fields in ascending order", () => { + const result = sortData(items, "name", "asc"); + expect(result.map((item) => item.name)).toEqual(["Alice", "Bob", "Charlie"]); + }); + + it("sorts string fields in descending order", () => { + const result = sortData(items, "name", "desc"); + expect(result.map((item) => item.name)).toEqual(["Charlie", "Bob", "Alice"]); + }); + + it("sorts numeric fields in ascending order", () => { + const result = sortData(items, "count", "asc"); + expect(result.map((item) => item.count)).toEqual([7, 15, 42]); + }); + + it("sorts numeric fields in descending order", () => { + const result = sortData(items, "count", "desc"); + expect(result.map((item) => item.count)).toEqual([42, 15, 7]); + }); + + it("handles null/undefined values by treating them as empty strings", () => { + const result = sortData(items, "optional", "asc"); + // null optional value (Alice) should sort first in ascending + expect(result[0].name).toBe("Alice"); + }); + + it("uses custom sortValue function if provided", () => { + const column = { + key: "name", + header: "Name", + sortValue: (row: TestItem) => row.name.length, + }; + // Charlie (7), Alice (5), Bob (3) + const result = sortData(items, "name", "asc", column); + expect(result.map((item) => item.name)).toEqual(["Bob", "Alice", "Charlie"]); + }); + + it("does not mutate the original array", () => { + const itemsCopy = [...items]; + sortData(items, "name", "asc"); + expect(items).toEqual(itemsCopy); + }); +}); diff --git a/src/features/demo-admin-dashboard/__tests__/presets.test.ts b/src/features/demo-admin-dashboard/__tests__/presets.test.ts index 0cf9a408..90bd0faa 100644 --- a/src/features/demo-admin-dashboard/__tests__/presets.test.ts +++ b/src/features/demo-admin-dashboard/__tests__/presets.test.ts @@ -17,6 +17,8 @@ describe("demo admin dashboard presets", () => { expect(scenario.stats.length).toBeGreaterThan(0); expect(scenario.accounts.length).toBeGreaterThan(0); expect(scenario.mail.length).toBeGreaterThan(0); + expect(scenario.attachments.length).toBeGreaterThan(0); + expect(scenario.events.length).toBeGreaterThan(0); expect(scenario.auditEvents.length).toBeGreaterThan(0); // Verify stats shape @@ -33,6 +35,27 @@ describe("demo admin dashboard presets", () => { expect(acct.type.trim()).not.toBe(""); } + // Verify attachments shape + for (const att of scenario.attachments) { + expect(att.id.trim()).not.toBe(""); + expect(att.fileName.trim()).not.toBe(""); + expect(att.fileSize.trim()).not.toBe(""); + expect(att.fileType.trim()).not.toBe(""); + expect(att.messageSubject.trim()).not.toBe(""); + expect(att.sender.trim()).not.toBe(""); + } + + // Verify events shape + for (const evt of scenario.events) { + expect(evt.id.trim()).not.toBe(""); + expect(evt.title.trim()).not.toBe(""); + expect(evt.date.trim()).not.toBe(""); + expect(evt.time.trim()).not.toBe(""); + expect(evt.location.trim()).not.toBe(""); + expect(evt.organizer.trim()).not.toBe(""); + expect(["confirmed", "tentative", "cancelled"]).toContain(evt.status); + } + // Verify audit events shape for (const event of scenario.auditEvents) { expect(event.action.trim()).not.toBe(""); @@ -47,6 +70,16 @@ describe("demo admin dashboard presets", () => { for (const item of scenario.mail) { expect(item.email).toMatch(/(\*stealth\.demo|@example\.(com|org))$/); } + for (const item of scenario.attachments) { + if (item.sender.includes("@") || item.sender.includes("*")) { + expect(item.sender).toMatch(/(\*stealth\.demo|@example\.(com|org))$/); + } + } + for (const item of scenario.events) { + if (item.organizer.includes("@") || item.organizer.includes("*")) { + expect(item.organizer).toMatch(/(\*stealth\.demo|@example\.(com|org))$/); + } + } } }); diff --git a/src/features/demo-admin-dashboard/components/AdminDataTable.tsx b/src/features/demo-admin-dashboard/components/AdminDataTable.tsx new file mode 100644 index 00000000..79d1fd74 --- /dev/null +++ b/src/features/demo-admin-dashboard/components/AdminDataTable.tsx @@ -0,0 +1,154 @@ +import React, { useState, useMemo } from "react"; +import { ArrowUpDown, ChevronUp, ChevronDown } from "lucide-react"; +import { cn } from "@/lib/utils"; + +export interface Column { + key: string; + header: string; + sortable?: boolean; + sortValue?: (row: T) => string | number | boolean; + render?: (row: T) => React.ReactNode; +} + +interface AdminDataTableProps { + data: T[]; + columns: Column[]; + onRowClick?: (row: T) => void; + selectedRowKey?: (row: T) => boolean; + defaultSortKey?: string; + defaultSortDirection?: "asc" | "desc"; + emptyMessage?: string; + className?: string; +} + +/** + * Pure helper function to sort data based on a key and direction. + */ +export function sortData( + data: T[], + sortKey: string | null, + sortDirection: "asc" | "desc", + column?: Column +): T[] { + if (!sortKey) return data; + + return [...data].sort((a, b) => { + let valA: any = column?.sortValue ? column.sortValue(a) : (a as any)[sortKey]; + let valB: any = column?.sortValue ? column.sortValue(b) : (b as any)[sortKey]; + + if (valA === undefined || valA === null) valA = ""; + if (valB === undefined || valB === null) valB = ""; + + if (typeof valA === "string" && typeof valB === "string") { + return sortDirection === "asc" + ? valA.localeCompare(valB) + : valB.localeCompare(valA); + } + + if (valA < valB) return sortDirection === "asc" ? -1 : 1; + if (valA > valB) return sortDirection === "asc" ? 1 : -1; + return 0; + }); +} + +/** + * Reusable, sortable table component for the Demo Admin Dashboard. + * Confined to displaying messages, senders, attachments, events, and audit entries. + */ +export function AdminDataTable({ + data, + columns, + onRowClick, + selectedRowKey, + defaultSortKey, + defaultSortDirection = "asc", + emptyMessage = "No records found.", + className, +}: AdminDataTableProps) { + const [sortKey, setSortKey] = useState(defaultSortKey || null); + const [sortDirection, setSortDirection] = useState<"asc" | "desc">(defaultSortDirection); + + const handleSort = (key: string) => { + if (sortKey === key) { + setSortDirection((prev) => (prev === "asc" ? "desc" : "asc")); + } else { + setSortKey(key); + setSortDirection("asc"); + } + }; + + const sortedData = useMemo(() => { + const col = columns.find((c) => c.key === sortKey); + return sortData(data, sortKey, sortDirection, col); + }, [data, sortKey, sortDirection, columns]); + + return ( +
+ + + + {columns.map((col) => { + const isSorted = sortKey === col.key; + return ( + + ); + })} + + + + {sortedData.length === 0 ? ( + + + + ) : ( + sortedData.map((row, i) => { + const isSelected = selectedRowKey ? selectedRowKey(row) : false; + const isClickable = !!onRowClick; + return ( + onRowClick && onRowClick(row)} + className={cn( + "border-b border-white/[0.04] last:border-0 transition-colors", + isClickable ? "cursor-pointer hover:bg-white/[0.02]" : "", + isSelected ? "bg-white/[0.04]" : "" + )} + > + {columns.map((col) => ( + + ))} + + ); + }) + )} + +
col.sortable && handleSort(col.key)} + className={cn( + "px-4 py-3 font-medium text-muted-foreground select-none", + col.sortable ? "cursor-pointer hover:text-foreground transition-colors" : "" + )} + > +
+ {col.header} + {col.sortable && ( + + {!isSorted ? ( + + ) : sortDirection === "asc" ? ( + + ) : ( + + )} + + )} +
+
+ {emptyMessage} +
+ {col.render ? col.render(row) : String((row as any)[col.key] ?? "")} +
+
+ ); +} diff --git a/src/features/demo-admin-dashboard/fixtures/presets.ts b/src/features/demo-admin-dashboard/fixtures/presets.ts index c6651c79..be9bae70 100644 --- a/src/features/demo-admin-dashboard/fixtures/presets.ts +++ b/src/features/demo-admin-dashboard/fixtures/presets.ts @@ -68,6 +68,27 @@ export const PRESET_SCENARIOS: PresetScenario[] = [ avatarColor: "#5b6470", }, ], + attachments: [ + { + id: "att-relay-spec", + fileName: "relay_specification.json", + fileSize: "4.2 KB", + fileType: "JSON", + messageSubject: "Your relay verification code", + sender: "relay07*stealth.demo", + }, + ], + events: [ + { + id: "evt-relay-register", + title: "Relay Node 07 Registration", + date: "2026-06-16", + time: "12:00 PM", + location: "Stellar Network", + organizer: "GDREL...OWNR", + status: "tentative", + }, + ], auditEvents: [ { action: "Relay Node 07 registration initiated", actor: "relay07*stealth.demo", timestamp: "2026-06-16T12:00:00Z" }, { action: "Authorization OTP generated and dispatched", actor: "system", timestamp: "2026-06-16T12:01:00Z" }, @@ -152,6 +173,35 @@ export const PRESET_SCENARIOS: PresetScenario[] = [ }, }, ], + attachments: [ + { + id: "att-soroban-tx", + fileName: "soroban_transaction.tx", + fileSize: "12.8 KB", + fileType: "Transaction Payload", + messageSubject: "Soroban proof generation pending", + sender: "bridge*stealth.demo", + }, + { + id: "att-invoice-1042", + fileName: "invoice_1042.pdf", + fileSize: "120 KB", + fileType: "PDF Document", + messageSubject: "Message request awaiting approval", + sender: "gckn...n4xq@example.com", + }, + ], + events: [ + { + id: "evt-proof-schedule", + title: "Soroban Proof Ledger Verification", + date: "2026-06-16", + time: "12:12 PM", + location: "Soroban VM", + organizer: "system", + status: "tentative", + }, + ], auditEvents: [ { action: "Incoming bridge message captured", actor: "bridge*stealth.demo", timestamp: "2026-06-16T12:10:00Z" }, { action: "Postage preimage submitted to escrow contract", actor: "bridge*stealth.demo", timestamp: "2026-06-16T12:11:00Z" }, @@ -228,6 +278,27 @@ export const PRESET_SCENARIOS: PresetScenario[] = [ }, }, ], + attachments: [ + { + id: "att-read-proof", + fileName: "read_receipt.proof", + fileSize: "2.1 KB", + fileType: "Cryptographic Proof", + messageSubject: "Delivery receipt settled", + sender: "receipts*stealth.demo", + }, + ], + events: [ + { + id: "evt-postage-release", + title: "Postage Release Settlement", + date: "2026-06-16", + time: "11:05 PM", + location: "Soroban Contract CCLC7...RECP", + organizer: "system", + status: "confirmed", + }, + ], auditEvents: [ { action: "Soroban delivery receipt contract invoked: CCLC7...RECP", actor: "system", timestamp: "2026-06-16T11:00:00Z" }, { action: "Cryptographic read proof signature verified on-chain", actor: "system", timestamp: "2026-06-16T11:02:00Z" }, diff --git a/src/features/demo-admin-dashboard/index.ts b/src/features/demo-admin-dashboard/index.ts index 82e4d94b..d4c59850 100644 --- a/src/features/demo-admin-dashboard/index.ts +++ b/src/features/demo-admin-dashboard/index.ts @@ -20,6 +20,8 @@ export type { DashboardSection, DemoAdminDashboardProps, StatCard, + PresetAttachment, + PresetEvent, } from "./types"; export { diff --git a/src/features/demo-admin-dashboard/types.ts b/src/features/demo-admin-dashboard/types.ts index b2ecfa03..173ee65c 100644 --- a/src/features/demo-admin-dashboard/types.ts +++ b/src/features/demo-admin-dashboard/types.ts @@ -43,7 +43,14 @@ export interface DashboardNavItem { } /** The available top-level sections in the admin dashboard. */ -export type DashboardSection = "overview" | "accounts" | "mail" | "templates" | "audit"; +export type DashboardSection = + | "overview" + | "accounts" + | "mail" + | "attachments" + | "events" + | "templates" + | "audit"; /** Props passed to the dashboard shell. */ export interface DemoAdminDashboardProps { @@ -107,6 +114,25 @@ export interface PresetAuditEvent { timestamp: string; } +export interface PresetAttachment { + id: string; + fileName: string; + fileSize: string; + fileType: string; + messageSubject: string; + sender: string; +} + +export interface PresetEvent { + id: string; + title: string; + date: string; + time: string; + location: string; + organizer: string; + status: "confirmed" | "tentative" | "cancelled"; +} + export interface PresetScenario { id: PresetId; name: string; @@ -114,6 +140,8 @@ export interface PresetScenario { stats: StatCard[]; accounts: PresetAccount[]; mail: PresetMail[]; + attachments: PresetAttachment[]; + events: PresetEvent[]; auditEvents: PresetAuditEvent[]; }