This commit is contained in:
debu-2 2026-10-02 22:39:04 +05:30
parent d272ae0bd3
commit ff01efe4bb
17 changed files with 1378 additions and 21 deletions

View file

@ -14,6 +14,7 @@ function TestModal({
}) {
const [to, setTo] = useState('')
const [customerName, setCustomerName] = useState('')
const [reportGroup, setReportGroup] = useState('')
const [reportId, setReportId] = useState('')
const [error, setError] = useState('')
const [sending, setSending] = useState(false)
@ -26,6 +27,7 @@ function TestModal({
await agentTemplatesApi.test(template.slug, {
to: to.trim(),
customer_name: customerName.trim(),
report_group: reportGroup.trim(),
report_id: reportId.trim() || undefined,
})
onSent()
@ -56,10 +58,15 @@ function TestModal({
<span>Customer name</span>
<input value={customerName} onChange={(e) => setCustomerName(e.target.value)} placeholder="Suyash" required />
</label>
<label>
<span>Report group</span>
<input value={reportGroup} onChange={(e) => setReportGroup(e.target.value)} placeholder="acme-agent-runs" required />
<small>Urmiport group slug used in urmiai.com/reports/{'{group}'}/{'{id}'}.</small>
</label>
<label>
<span>Report ID (optional)</span>
<input value={reportId} onChange={(e) => setReportId(e.target.value)} placeholder="run-abc123" />
<small>Used for the dynamic View Report button URL suffix.</small>
<small>Report postfix; auto-generated when omitted.</small>
</label>
{error && <p className="auth-error">{error}</p>}
</div>

View file

@ -0,0 +1,284 @@
import { ArrowRight, Code2, ExternalLink, FileText, Globe, Lock, Plus, X } from 'lucide-react'
import { useEffect, useState, type FormEvent } from 'react'
import { Link } from 'react-router-dom'
import { urmiportApi, type UrmiportApiDoc, type UrmiportGroup, type UrmiportReport } from '../../lib/api'
const GROUP_SLUG_RE = /^[a-z][a-z0-9-]{1,48}[a-z0-9]$/
function GroupModal({
onClose,
onCreated,
}: {
onClose: () => void
onCreated: (group: UrmiportGroup) => void
}) {
const [reportGroup, setReportGroup] = useState('')
const [displayName, setDisplayName] = useState('')
const [defaultAccess, setDefaultAccess] = useState<'public' | 'password'>('public')
const [password, setPassword] = useState('')
const [idMode, setIdMode] = useState<'auto' | 'manual'>('auto')
const [error, setError] = useState('')
const [saving, setSaving] = useState(false)
async function submit(event: FormEvent) {
event.preventDefault()
setError('')
const slug = reportGroup.trim().toLowerCase()
if (!GROUP_SLUG_RE.test(slug) || slug.includes('--')) {
setError('Report group must be 3–50 chars: lowercase letters, numbers, hyphens')
return
}
if (defaultAccess === 'password' && !password.trim()) {
setError('Password is required for password-protected groups')
return
}
setSaving(true)
try {
const group = await urmiportApi.createGroup({
report_group: slug,
display_name: displayName.trim() || undefined,
default_access: defaultAccess,
password: defaultAccess === 'password' ? password.trim() : undefined,
id_mode: idMode,
})
onCreated(group)
onClose()
} catch (reason) {
setError(reason instanceof Error ? reason.message : 'Unable to create report group')
} finally {
setSaving(false)
}
}
return (
<div className="modal-overlay" onClick={onClose}>
<form className="modal-card otp-project-modal" onSubmit={submit} onClick={(event) => event.stopPropagation()}>
<div className="modal-header">
<h2>Create report group</h2>
<button type="button" className="icon-button" onClick={onClose} aria-label="Close"><X size={18} /></button>
</div>
<div className="otp-project-form">
<label>
<span>Report group</span>
<input
value={reportGroup}
onChange={(event) => setReportGroup(event.target.value.toLowerCase().replace(/[^a-z0-9-]/g, ''))}
placeholder="acme-agent-runs"
required
/>
<small>Used in URLs: urmiai.com/reports/{'{report_group}'}/{'{report_id}'}</small>
</label>
<label>
<span>Display name (optional)</span>
<input value={displayName} onChange={(event) => setDisplayName(event.target.value)} placeholder="Acme agent runs" />
</label>
<fieldset>
<legend>Default access</legend>
<label><input type="radio" checked={defaultAccess === 'public'} onChange={() => setDefaultAccess('public')} /> Public</label>
<label><input type="radio" checked={defaultAccess === 'password'} onChange={() => setDefaultAccess('password')} /> Password</label>
</fieldset>
{defaultAccess === 'password' && (
<label>
<span>Default password</span>
<input type="password" value={password} onChange={(event) => setPassword(event.target.value)} required />
</label>
)}
<fieldset>
<legend>Report ID mode</legend>
<label><input type="radio" checked={idMode === 'auto'} onChange={() => setIdMode('auto')} /> Auto-generate postfix</label>
<label><input type="radio" checked={idMode === 'manual'} onChange={() => setIdMode('manual')} /> Require report_id in API</label>
</fieldset>
{error && <p className="auth-error">{error}</p>}
</div>
<div className="modal-actions">
<button className="primary-button" disabled={saving}>{saving ? 'Creating…' : 'Create group'}</button>
<button type="button" className="secondary-button" onClick={onClose}>Cancel</button>
</div>
</form>
</div>
)
}
export function UrmiportPage() {
const [groups, setGroups] = useState<UrmiportGroup[]>([])
const [selectedGroup, setSelectedGroup] = useState<string | null>(null)
const [reports, setReports] = useState<UrmiportReport[]>([])
const [docs, setDocs] = useState<UrmiportApiDoc | null>(null)
const [loading, setLoading] = useState(true)
const [reportsLoading, setReportsLoading] = useState(false)
const [error, setError] = useState('')
const [showModal, setShowModal] = useState(false)
useEffect(() => {
Promise.all([urmiportApi.listGroups(), urmiportApi.docs()])
.then(([groupList, apiDocs]) => {
setGroups(groupList)
setDocs(apiDocs)
if (groupList[0]) setSelectedGroup(groupList[0].report_group)
})
.catch((reason) => setError(reason instanceof Error ? reason.message : 'Unable to load Urmiport'))
.finally(() => setLoading(false))
}, [])
useEffect(() => {
if (!selectedGroup) {
setReports([])
return
}
setReportsLoading(true)
urmiportApi.listReports(selectedGroup)
.then(setReports)
.catch((reason) => setError(reason instanceof Error ? reason.message : 'Unable to load reports'))
.finally(() => setReportsLoading(false))
}, [selectedGroup])
return (
<div className="urmiport-page">
<header className="page-header">
<div>
<span className="eyebrow">Agent</span>
<h1><FileText size={24} /> Urmiport</h1>
<p>Host HTML reports at urmiai.com/reports/{'{report_group}'}/{'{report_id}'} — no Urmi branding on the public page.</p>
</div>
<button className="primary-button" onClick={() => setShowModal(true)}>
<Plus size={16} /> New report group
</button>
</header>
{error && <p className="auth-error">{error}</p>}
<section className="panel urmiport-groups-panel">
<div className="panel-heading">
<div>
<h2>Report groups</h2>
<p>Each group is a namespace for hosted reports. Publish HTML via the API below.</p>
</div>
</div>
{loading ? (
<p className="panel-empty">Loading…</p>
) : !groups.length ? (
<p className="panel-empty">Create a report group to start hosting reports.</p>
) : (
<div className="urmiport-group-table">
<div className="urmiport-group-table-head">
<span>Group</span>
<span>Access</span>
<span>ID mode</span>
<span>Reports</span>
<span>URL pattern</span>
</div>
{groups.map((group) => (
<button
type="button"
key={group.id}
className={`urmiport-group-row ${selectedGroup === group.report_group ? 'is-selected' : ''}`}
onClick={() => setSelectedGroup(group.report_group)}
>
<div>
<strong>{group.display_name || group.report_group}</strong>
<small>{group.report_group}</small>
</div>
<span>{group.default_access === 'password' ? <><Lock size={13} /> Password</> : <><Globe size={13} /> Public</>}</span>
<span>{group.id_mode === 'auto' ? 'Auto' : 'Manual'}</span>
<span>{group.report_count}</span>
<code>{group.url_pattern}</code>
</button>
))}
</div>
)}
</section>
{selectedGroup && (
<section className="panel urmiport-reports-panel">
<div className="panel-heading">
<div>
<h2>Reports in {selectedGroup}</h2>
<p>Published via API. Same report_group + report_id updates the HTML body.</p>
</div>
</div>
{reportsLoading ? (
<p className="panel-empty">Loading reports…</p>
) : !reports.length ? (
<p className="panel-empty">No reports yet. Publish one using the API docs below.</p>
) : (
<div className="urmiport-report-table">
<div className="urmiport-report-table-head">
<span>Report ID</span>
<span>Access</span>
<span>Updated</span>
<span>URL</span>
</div>
{reports.map((report) => (
<div className="urmiport-report-row" key={report.id}>
<strong>{report.report_id}</strong>
<span>{report.access === 'password' ? 'Password' : 'Public'}</span>
<span>{new Date(report.updated_at).toLocaleString()}</span>
<a href={report.url} target="_blank" rel="noreferrer">
<ExternalLink size={13} /> Open
</a>
</div>
))}
</div>
)}
</section>
)}
{docs && (
<section className="panel urmiport-docs-panel">
<div className="panel-heading">
<div>
<h2><Code2 size={18} /> API documentation</h2>
<p>{docs.summary}</p>
</div>
<span className="explorer-method">{docs.method}</span>
</div>
<div className="urmiport-docs-grid">
<div>
<h3>Endpoint</h3>
<code>POST /api/v1/urmiport/reports</code>
<h3>Authentication</h3>
<p><code>{docs.authentication.header}: {docs.authentication.format}</code></p>
<h3>Request body</h3>
<ul className="agent-template-notes">
{Object.entries(docs.request_body).map(([key, field]) => (
<li key={key}>
<strong>{key}</strong>{field.required ? ' (required)' : ' (optional)'} — {field.description}
{field.example ? <> Example: <code>{field.example}</code></> : null}
</li>
))}
</ul>
<h3>Notes</h3>
<ul className="agent-template-notes">
{docs.notes?.map((note) => <li key={note}>{note}</li>)}
</ul>
</div>
<div>
<h3>Example request</h3>
<pre className="explorer-code">{JSON.stringify({
report_group: 'acme-agent-runs',
report_id: 'run-abc123',
body: '<html><body><h1>Agent run complete</h1></body></html>',
access: 'public',
}, null, 2)}</pre>
<h3>Example response</h3>
<pre className="explorer-code">{JSON.stringify(docs.responses['200']?.example ?? {}, null, 2)}</pre>
<Link to="/console/api-explorer" className="text-button">
Try in API Explorer <ArrowRight size={14} />
</Link>
</div>
</div>
</section>
)}
{showModal && (
<GroupModal
onClose={() => setShowModal(false)}
onCreated={(group) => {
setGroups((current) => [group, ...current])
setSelectedGroup(group.report_group)
}}
/>
)}
</div>
)
}