Arcadia wiring: - home: real Overview dashboard (tenants/users/audit/health probe) replacing the inherited Vibespace welcome tiles; skeleton loaders, refresh button, registers admin context - profile: split into Account (synced via getUser/updateUser of session user) and local Preferences; updateSessionUser keeps the appbar in sync after edits - session: drop unused signIn mock, add updateSessionUser, refresh tests - profile schema: drop redundant Profile.name/email (session is the source of truth) - routes: delete orphaned resources route + lib Auth flows that previously 404'd: - /signup, /login/forgot, /login/reset, /login/2fa wired via @crema/arcadia-auth-ui - shared AuthShell + AuthBrand wrapper Assistant tools (admin-tools.ts): - +10 tools: deactivate_tenant, set_user_status, delete_user, list_memberships, list_roles, revoke_api_key, create_user, update_user, assign_role, remove_role - list_memberships gains user_id filter for "tenants this user belongs to" queries - search_kb / read_chunk: new token resolution (window override → VITE_ARCADIA_SEARCH_TOKEN service token → operator session JWT → "dev"); on 401/403 emit a tailored hint based on which token was used UI consistency: - new PageHeader component - AppShell.title was unrendered — dropped; first-child padding on #main-content keeps the floating actions pill from colliding with header content - removed dead "Sign in required" fallback cards from 14 routes (AppShell already redirects) - stripped p-6 from outer wrappers across 14 routes (was double-padding under AppShell's own p-6) - migrated home + tenants to PageHeader arcadia-search ergonomics: - scripts/mint-search-token.mjs + `npm run mint:search-token` mints HS512 JWT with required tenant_id claim, upserts VITE_ARCADIA_SEARCH_TOKEN into .env.local - README/.env document the new VITE_ARCADIA_SEARCH_URL / VITE_ARCADIA_SEARCH_TOKEN knobs - .env.local now gitignored Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
156 lines
4.5 KiB
TypeScript
156 lines
4.5 KiB
TypeScript
// Run a saved script from public/scripts/ or paste DSL ad-hoc.
|
|
// Triggered by the Play icon in the appbar or Cmd/Ctrl+Shift+P.
|
|
|
|
import { useEffect, useState } from "react"
|
|
import { Play } from "lucide-react"
|
|
|
|
import { Button } from "~/components/ui/button"
|
|
import {
|
|
Dialog,
|
|
DialogContent,
|
|
DialogDescription,
|
|
DialogHeader,
|
|
DialogTitle,
|
|
} from "~/components/ui/dialog"
|
|
import { runScript, runScriptText } from "@crema/action-bus"
|
|
|
|
const KNOWN_SCRIPTS = [
|
|
{ name: "demo-tour", description: "Tour the rail" },
|
|
{ name: "demo-search", description: "Focus and fill search" },
|
|
]
|
|
|
|
export function useScriptsHotkey(open: () => void) {
|
|
useEffect(() => {
|
|
const onKey = (e: KeyboardEvent) => {
|
|
const mod = e.metaKey || e.ctrlKey
|
|
if (mod && e.shiftKey && (e.key === "p" || e.key === "P")) {
|
|
e.preventDefault()
|
|
open()
|
|
}
|
|
}
|
|
window.addEventListener("keydown", onKey)
|
|
return () => window.removeEventListener("keydown", onKey)
|
|
}, [open])
|
|
}
|
|
|
|
export function ScriptsDialog({
|
|
open,
|
|
onOpenChange,
|
|
}: {
|
|
open: boolean
|
|
onOpenChange: (v: boolean) => void
|
|
}) {
|
|
const [text, setText] = useState("")
|
|
const [status, setStatus] = useState<string | null>(null)
|
|
const [busy, setBusy] = useState(false)
|
|
|
|
const runByName = async (name: string) => {
|
|
setBusy(true)
|
|
setStatus(`Running ${name}…`)
|
|
try {
|
|
onOpenChange(false)
|
|
await runScript(name)
|
|
setStatus(`Ran ${name}.`)
|
|
} catch (e) {
|
|
setStatus(`Error: ${e instanceof Error ? e.message : String(e)}`)
|
|
} finally {
|
|
setBusy(false)
|
|
}
|
|
}
|
|
|
|
const runText = async () => {
|
|
if (!text.trim()) return
|
|
setBusy(true)
|
|
setStatus("Running…")
|
|
try {
|
|
onOpenChange(false)
|
|
await runScriptText(text)
|
|
setStatus("Done.")
|
|
} catch (e) {
|
|
setStatus(`Error: ${e instanceof Error ? e.message : String(e)}`)
|
|
} finally {
|
|
setBusy(false)
|
|
}
|
|
}
|
|
|
|
return (
|
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
|
<DialogContent className="sm:max-w-lg">
|
|
<DialogHeader>
|
|
<DialogTitle>Run a script</DialogTitle>
|
|
<DialogDescription>
|
|
Pick a saved script or paste DSL. Cmd/Ctrl + Shift + P toggles this.
|
|
</DialogDescription>
|
|
</DialogHeader>
|
|
|
|
<div className="flex flex-col gap-3">
|
|
<div>
|
|
<div className="mb-1.5 text-xs font-medium text-muted-foreground">
|
|
Saved
|
|
</div>
|
|
<div className="flex flex-col gap-1">
|
|
{KNOWN_SCRIPTS.map((s) => (
|
|
<button
|
|
key={s.name}
|
|
type="button"
|
|
data-action={`run-script-${s.name}`}
|
|
onClick={() => runByName(s.name)}
|
|
disabled={busy}
|
|
className="flex items-center justify-between rounded-md border px-3 py-2 text-left text-sm transition-colors hover:bg-accent disabled:opacity-50"
|
|
>
|
|
<span>
|
|
<span className="font-mono text-xs">{s.name}</span>
|
|
<span className="ml-2 text-muted-foreground">
|
|
{s.description}
|
|
</span>
|
|
</span>
|
|
<Play className="size-4 text-muted-foreground" />
|
|
</button>
|
|
))}
|
|
</div>
|
|
</div>
|
|
|
|
<div>
|
|
<div className="mb-1.5 text-xs font-medium text-muted-foreground">
|
|
Paste DSL
|
|
</div>
|
|
<textarea
|
|
data-action="scripts-dsl"
|
|
value={text}
|
|
onChange={(e) => setText(e.target.value)}
|
|
placeholder={"navigate /tenants\nclick nav-tenants"}
|
|
spellCheck={false}
|
|
rows={6}
|
|
className="w-full rounded-md border bg-background p-2 font-mono text-xs"
|
|
/>
|
|
</div>
|
|
|
|
{status && (
|
|
<div className="rounded-md border bg-muted px-3 py-2 text-xs text-muted-foreground">
|
|
{status}
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
<div className="flex justify-end gap-2">
|
|
<Button
|
|
data-action="scripts-cancel"
|
|
variant="outline"
|
|
onClick={() => onOpenChange(false)}
|
|
disabled={busy}
|
|
>
|
|
Close
|
|
</Button>
|
|
<Button
|
|
data-action="scripts-run"
|
|
onClick={runText}
|
|
disabled={busy || !text.trim()}
|
|
>
|
|
<Play className="size-4" /> Run
|
|
</Button>
|
|
</div>
|
|
</DialogContent>
|
|
</Dialog>
|
|
)
|
|
}
|