Files
CurrenciCombo/src/pages/TransactionsPage.tsx
nsatoshi b66ec0a78f
Some checks failed
CI / Frontend Lint (push) Failing after 6s
CI / Frontend Type Check (push) Failing after 6s
CI / Frontend Build (push) Failing after 6s
CI / Frontend E2E Tests (push) Failing after 9s
CI / Orchestrator Build (push) Failing after 6s
CI / Contracts Compile (push) Failing after 6s
CI / Contracts Test (push) Failing after 6s
Security Scan / Dependency Vulnerability Scan (push) Failing after 4s
Security Scan / OWASP ZAP Scan (push) Failing after 4s
PR G: portal /transactions page + 12-state machine view (#11)
2026-04-22 17:18:52 +00:00

244 lines
7.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useEffect, useState } from 'react';
import { useNavigate, useParams } from 'react-router-dom';
import { listPlans, getPlanState, getPlanEvents, type PlanSummary, type PlanStateDetail, type PlanEvent } from '../services/orchestrator';
import StateMachineView from '../components/portal/StateMachineView';
import { endpoints } from '../config/endpoints';
type Source = 'live' | 'mocked' | 'degraded';
function SourceBadge({ source }: { source: Source }) {
const label = source === 'live' ? 'LIVE' : source === 'degraded' ? 'DEGRADED' : 'DEMO';
return <span className={`source-badge source-badge--${source}`}>{label}</span>;
}
export default function TransactionsPage() {
const { planId } = useParams<{ planId?: string }>();
return planId ? <TransactionDetail planId={planId} /> : <TransactionsList />;
}
function TransactionsList() {
const navigate = useNavigate();
const [plans, setPlans] = useState<PlanSummary[] | null>(null);
const [source, setSource] = useState<Source>('mocked');
const [error, setError] = useState<string | null>(null);
useEffect(() => {
let cancelled = false;
setPlans(null);
setError(null);
listPlans()
.then((res) => {
if (cancelled) return;
setPlans(res.plans);
setSource(res.source);
})
.catch((err) => {
if (cancelled) return;
setError(err instanceof Error ? err.message : String(err));
});
return () => {
cancelled = true;
};
}, []);
return (
<div className="transactions-page">
<div className="page-header">
<h1>Transactions</h1>
<p className="page-subtitle">
Multi-layer atomic settlement plans. State machine per architecture note §8.
{!endpoints.orchestrator.deployed && (
<span className="muted">
{' '}Orchestrator not deployed showing demo plans.
</span>
)}
</p>
</div>
<div className="dashboard-card">
<div className="card-header">
<h3>Recent plans</h3>
<SourceBadge source={source} />
</div>
{error && <div className="error-banner">{error}</div>}
{plans === null ? (
<div className="loading-row">Loading</div>
) : plans.length === 0 ? (
<div className="empty-row">No plans yet.</div>
) : (
<table className="portal-table" data-testid="transactions-table">
<thead>
<tr>
<th>Plan ID</th>
<th>State</th>
<th>Instrument</th>
<th>Owner</th>
<th>Updated</th>
<th aria-label="open" />
</tr>
</thead>
<tbody>
{plans.map((p) => (
<tr
key={p.plan_id}
className="portal-table-row"
onClick={() => navigate(`/transactions/${encodeURIComponent(p.plan_id)}`)}
data-testid={`plan-row-${p.plan_id}`}
>
<td className="mono">{p.plan_id}</td>
<td>
<span className={`state-chip state-chip--${p.status.toLowerCase()}`}>
{p.status.replace(/_/g, ' ')}
</span>
</td>
<td>{p.instrument_hint ?? '—'}</td>
<td>{p.actor_id ?? '—'}</td>
<td>{new Date(p.updated_at).toLocaleString()}</td>
<td className="row-chevron"></td>
</tr>
))}
</tbody>
</table>
)}
</div>
</div>
);
}
function TransactionDetail({ planId }: { planId: string }) {
const navigate = useNavigate();
const [detail, setDetail] = useState<PlanStateDetail | null>(null);
const [events, setEvents] = useState<PlanEvent[] | null>(null);
const [source, setSource] = useState<Source>('mocked');
const [error, setError] = useState<string | null>(null);
useEffect(() => {
let cancelled = false;
setDetail(null);
setEvents(null);
setError(null);
Promise.all([getPlanState(planId), getPlanEvents(planId)])
.then(([s, e]) => {
if (cancelled) return;
setDetail(s.detail);
setEvents(e.events);
setSource(s.source === 'live' && e.source === 'live' ? 'live' : s.source);
})
.catch((err) => {
if (cancelled) return;
setError(err instanceof Error ? err.message : String(err));
});
return () => {
cancelled = true;
};
}, [planId]);
return (
<div className="transactions-page">
<div className="page-header">
<button className="back-button" onClick={() => navigate('/transactions')}>
All transactions
</button>
<h1>
Plan <span className="mono">{planId}</span>
</h1>
<p className="page-subtitle">
{detail ? (
<>Current state: <strong>{detail.current_state.replace(/_/g, ' ')}</strong></>
) : (
'Loading plan state…'
)}
</p>
</div>
{error && <div className="error-banner">{error}</div>}
<div className="dashboard-card">
<div className="card-header">
<h3>12-state machine</h3>
<SourceBadge source={source} />
</div>
{detail ? (
<StateMachineView current={detail.current_state} transitions={detail.transitions} />
) : (
<div className="loading-row">Loading</div>
)}
</div>
<div className="dashboard-card">
<div className="card-header">
<h3>Audit trail</h3>
</div>
{detail === null ? (
<div className="loading-row">Loading</div>
) : detail.transitions.length === 0 ? (
<div className="empty-row">No transitions recorded.</div>
) : (
<table className="portal-table" data-testid="audit-trail">
<thead>
<tr>
<th>#</th>
<th>From To</th>
<th>Actor</th>
<th>Role</th>
<th>Reason</th>
<th>At</th>
</tr>
</thead>
<tbody>
{detail.transitions.map((t, i) => (
<tr key={i}>
<td>{i + 1}</td>
<td className="mono">
{t.from_state ?? '∅'} {t.to_state}
</td>
<td>{t.actor_id}</td>
<td>
<span className={`role-chip role-chip--${t.actor_role}`}>{t.actor_role}</span>
</td>
<td>{t.reason ?? '—'}</td>
<td>{new Date(t.occurred_at).toLocaleString()}</td>
</tr>
))}
</tbody>
</table>
)}
</div>
<div className="dashboard-card">
<div className="card-header">
<h3>Signed event stream</h3>
</div>
{events === null ? (
<div className="loading-row">Loading</div>
) : events.length === 0 ? (
<div className="empty-row">No events.</div>
) : (
<table className="portal-table" data-testid="event-stream">
<thead>
<tr>
<th>#</th>
<th>Type</th>
<th>Signature</th>
<th>Prev hash</th>
<th>At</th>
</tr>
</thead>
<tbody>
{events.map((e) => (
<tr key={e.id}>
<td>{e.id}</td>
<td className="mono">{e.type}</td>
<td className="mono truncate">{e.signature}</td>
<td className="mono truncate">{e.prev_hash ?? '∅'}</td>
<td>{new Date(e.created_at).toLocaleString()}</td>
</tr>
))}
</tbody>
</table>
)}
</div>
</div>
);
}