create-ekka-desktop-app 0.4.9 → 0.4.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/template/src/demo/DemoApp.tsx +7 -1
- package/template/src/demo/pages/ExecutionPlansPage.tsx +85 -132
- package/template/src/demo/pages/ExecutionRunDetailPage.tsx +986 -0
- package/template/src/ekka/constants.ts +3 -0
- package/template/src/ekka/ops/admin.ts +63 -0
- package/template/src/ekka/ops/index.ts +1 -0
- package/template/src-tauri/crates/ekka-desktop-core/src/main.rs +175 -35
- package/template/src-tauri/src/commands.rs +6 -0
- package/template/src-tauri/src/main.rs +2 -0
- package/template/src-tauri/src/ops/auth.rs +130 -1
package/package.json
CHANGED
|
@@ -21,6 +21,7 @@ import { PathPermissionsPage } from './pages/PathPermissionsPage';
|
|
|
21
21
|
import { VaultPage } from './pages/VaultPage';
|
|
22
22
|
import { RunnerPage } from './pages/RunnerPage';
|
|
23
23
|
import { ExecutionPlansPage } from './pages/ExecutionPlansPage';
|
|
24
|
+
import { ExecutionRunDetailPage } from './pages/ExecutionRunDetailPage';
|
|
24
25
|
import { LoginPage } from './pages/LoginPage';
|
|
25
26
|
import { HomeSetupPage } from './pages/HomeSetupPage';
|
|
26
27
|
import { SetupWizard } from './components/SetupWizard';
|
|
@@ -37,6 +38,7 @@ interface DemoState {
|
|
|
37
38
|
|
|
38
39
|
export function DemoApp(): ReactElement {
|
|
39
40
|
const [selectedPage, setSelectedPage] = useState<Page>('path-permissions');
|
|
41
|
+
const [selectedRunId, setSelectedRunId] = useState<string | null>(null);
|
|
40
42
|
const [darkMode, setDarkMode] = useState<boolean>(() => {
|
|
41
43
|
if (typeof window !== 'undefined') {
|
|
42
44
|
return window.matchMedia('(prefers-color-scheme: dark)').matches;
|
|
@@ -252,7 +254,11 @@ export function DemoApp(): ReactElement {
|
|
|
252
254
|
{selectedPage === 'path-permissions' && <PathPermissionsPage darkMode={darkMode} />}
|
|
253
255
|
{selectedPage === 'vault' && <VaultPage darkMode={darkMode} />}
|
|
254
256
|
{selectedPage === 'runner' && <RunnerPage darkMode={darkMode} />}
|
|
255
|
-
{selectedPage === 'execution-plans' &&
|
|
257
|
+
{selectedPage === 'execution-plans' && (
|
|
258
|
+
selectedRunId
|
|
259
|
+
? <ExecutionRunDetailPage runId={selectedRunId} onBack={() => setSelectedRunId(null)} darkMode={darkMode} />
|
|
260
|
+
: <ExecutionPlansPage darkMode={darkMode} onViewRun={(id) => setSelectedRunId(id)} />
|
|
261
|
+
)}
|
|
256
262
|
{selectedPage === 'audit-log' && <AuditLogPage darkMode={darkMode} />}
|
|
257
263
|
{selectedPage === 'system' && <SystemPage darkMode={darkMode} />}
|
|
258
264
|
</Shell>
|
|
@@ -1,11 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Execution Plans Page
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* - Edit input JSON
|
|
7
|
-
* - Execute (start a run)
|
|
8
|
-
* - View runs grid + run details/events
|
|
4
|
+
* Plan selection, input editing, execution, and runs grid.
|
|
5
|
+
* Clicking "View" on a run navigates to ExecutionRunDetailPage (via onViewRun).
|
|
9
6
|
*/
|
|
10
7
|
|
|
11
8
|
import { useState, useEffect, useCallback, useRef, type CSSProperties, type ReactElement } from 'react';
|
|
@@ -13,7 +10,7 @@ import { _internal, makeRequest } from '../../ekka/internal';
|
|
|
13
10
|
import { InfoTooltip } from '../components';
|
|
14
11
|
|
|
15
12
|
// =============================================================================
|
|
16
|
-
// API
|
|
13
|
+
// API TYPES
|
|
17
14
|
// =============================================================================
|
|
18
15
|
|
|
19
16
|
interface Plan {
|
|
@@ -29,24 +26,25 @@ interface Plan {
|
|
|
29
26
|
interface Run {
|
|
30
27
|
id: string;
|
|
31
28
|
plan_id: string;
|
|
29
|
+
plan_code?: string;
|
|
32
30
|
status: string;
|
|
31
|
+
progress?: number;
|
|
32
|
+
correlation_id?: string;
|
|
33
33
|
inputs?: Record<string, unknown>;
|
|
34
|
+
context?: Record<string, unknown>;
|
|
34
35
|
outputs?: Record<string, unknown>;
|
|
36
|
+
result?: Record<string, unknown>;
|
|
35
37
|
error?: string;
|
|
38
|
+
duration_ms?: number;
|
|
36
39
|
created_at: string;
|
|
37
40
|
updated_at?: string;
|
|
38
41
|
started_at?: string;
|
|
39
42
|
completed_at?: string;
|
|
40
43
|
}
|
|
41
44
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
event_type: string;
|
|
46
|
-
step_key?: string;
|
|
47
|
-
payload?: unknown;
|
|
48
|
-
created_at: string;
|
|
49
|
-
}
|
|
45
|
+
// =============================================================================
|
|
46
|
+
// API HELPERS (use internal request wrapper, no direct fetch)
|
|
47
|
+
// =============================================================================
|
|
50
48
|
|
|
51
49
|
async function request<T>(op: string, payload: unknown = {}): Promise<T> {
|
|
52
50
|
const req = makeRequest(op, payload);
|
|
@@ -63,25 +61,23 @@ async function listPlans(): Promise<Plan[]> {
|
|
|
63
61
|
}
|
|
64
62
|
|
|
65
63
|
async function getPlan(id: string): Promise<Plan> {
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
async function listRuns(planId: string): Promise<Run[]> {
|
|
70
|
-
const result = await request<{ data?: Run[]; runs?: Run[] }>('execution.plans.runs.list', { planId, limit: 25 });
|
|
71
|
-
return result.data || result.runs || (Array.isArray(result) ? result as unknown as Run[] : []);
|
|
64
|
+
const result = await request<{ plan?: Plan }>('execution.plans.get', { id });
|
|
65
|
+
return result.plan || result as unknown as Plan;
|
|
72
66
|
}
|
|
73
67
|
|
|
74
|
-
|
|
75
|
-
|
|
68
|
+
interface RunsPage {
|
|
69
|
+
runs: Run[];
|
|
70
|
+
total: number;
|
|
76
71
|
}
|
|
77
72
|
|
|
78
|
-
async function
|
|
79
|
-
|
|
73
|
+
async function listRuns(planId: string, limit: number, offset: number): Promise<RunsPage> {
|
|
74
|
+
const result = await request<{ data?: Run[]; runs?: Run[]; total?: number }>('execution.plans.runs.list', { planId, limit, offset });
|
|
75
|
+
const runs = result.data || result.runs || (Array.isArray(result) ? result as unknown as Run[] : []);
|
|
76
|
+
return { runs, total: result.total ?? runs.length };
|
|
80
77
|
}
|
|
81
78
|
|
|
82
|
-
async function
|
|
83
|
-
|
|
84
|
-
return result.data || result.events || (Array.isArray(result) ? result as unknown as RunEvent[] : []);
|
|
79
|
+
async function startRun(plan_id: string, inputs: Record<string, unknown>): Promise<Run> {
|
|
80
|
+
return request<Run>('execution.runs.start', { plan_id, inputs });
|
|
85
81
|
}
|
|
86
82
|
|
|
87
83
|
// =============================================================================
|
|
@@ -90,17 +86,19 @@ async function getRunEvents(runId: string): Promise<RunEvent[]> {
|
|
|
90
86
|
|
|
91
87
|
interface ExecutionPlansPageProps {
|
|
92
88
|
darkMode: boolean;
|
|
89
|
+
onViewRun?: (runId: string) => void;
|
|
93
90
|
}
|
|
94
91
|
|
|
95
|
-
export function ExecutionPlansPage({ darkMode }: ExecutionPlansPageProps): ReactElement {
|
|
92
|
+
export function ExecutionPlansPage({ darkMode, onViewRun }: ExecutionPlansPageProps): ReactElement {
|
|
96
93
|
const [plans, setPlans] = useState<Plan[]>([]);
|
|
97
94
|
const [selectedPlanId, setSelectedPlanId] = useState<string>('');
|
|
98
95
|
const [selectedPlan, setSelectedPlan] = useState<Plan | null>(null);
|
|
99
96
|
const [inputJson, setInputJson] = useState<string>('{}');
|
|
100
97
|
const [jsonError, setJsonError] = useState<string | null>(null);
|
|
101
98
|
const [runs, setRuns] = useState<Run[]>([]);
|
|
102
|
-
const [
|
|
103
|
-
const [
|
|
99
|
+
const [runsTotal, setRunsTotal] = useState(0);
|
|
100
|
+
const [runsOffset, setRunsOffset] = useState(0);
|
|
101
|
+
const runsLimit = 10;
|
|
104
102
|
const [loading, setLoading] = useState(false);
|
|
105
103
|
const [executing, setExecuting] = useState(false);
|
|
106
104
|
const [error, setError] = useState<string | null>(null);
|
|
@@ -133,7 +131,6 @@ export function ExecutionPlansPage({ darkMode }: ExecutionPlansPageProps): React
|
|
|
133
131
|
button: { padding: '8px 20px', fontSize: '13px', fontWeight: 500, borderRadius: '6px', border: 'none', cursor: 'pointer', transition: 'opacity 0.15s' },
|
|
134
132
|
buttonPrimary: { background: colors.blue, color: '#ffffff' },
|
|
135
133
|
buttonDisabled: { opacity: 0.5, cursor: 'not-allowed' },
|
|
136
|
-
grid: { display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '16px' },
|
|
137
134
|
table: { width: '100%', borderCollapse: 'collapse' as const, fontSize: '12px' },
|
|
138
135
|
th: { textAlign: 'left' as const, padding: '8px 10px', borderBottom: `1px solid ${colors.border}`, fontWeight: 600, color: colors.textMuted, fontSize: '11px', textTransform: 'uppercase' as const, letterSpacing: '0.04em' },
|
|
139
136
|
td: { padding: '8px 10px', borderBottom: `1px solid ${colors.border}`, verticalAlign: 'top' as const },
|
|
@@ -141,9 +138,8 @@ export function ExecutionPlansPage({ darkMode }: ExecutionPlansPageProps): React
|
|
|
141
138
|
badge: { display: 'inline-block', padding: '2px 8px', borderRadius: '4px', fontSize: '11px', fontWeight: 500 },
|
|
142
139
|
sectionTitle: { fontSize: '14px', fontWeight: 600, marginBottom: '12px', display: 'flex', alignItems: 'center', gap: '6px' },
|
|
143
140
|
meta: { fontSize: '12px', color: colors.textMuted, marginTop: '8px' },
|
|
144
|
-
pre: { background: darkMode ? '#1c1c1e' : '#f5f5f7', border: `1px solid ${colors.border}`, borderRadius: '6px', padding: '12px', fontSize: '11px', fontFamily: 'SF Mono, Menlo, monospace', overflow: 'auto', maxHeight: '300px', whiteSpace: 'pre-wrap' as const, wordBreak: 'break-all' as const },
|
|
141
|
+
pre: { background: darkMode ? '#1c1c1e' : '#f5f5f7', border: `1px solid ${colors.border}`, borderRadius: '6px', padding: '12px', fontSize: '11px', fontFamily: 'SF Mono, Menlo, monospace', overflow: 'auto', maxHeight: '300px', whiteSpace: 'pre-wrap' as const, wordBreak: 'break-all' as const, margin: 0 },
|
|
145
142
|
link: { color: colors.blue, cursor: 'pointer', background: 'none', border: 'none', fontSize: '12px', fontFamily: 'SF Mono, Menlo, monospace', padding: 0 },
|
|
146
|
-
row: { display: 'flex', gap: '12px', alignItems: 'flex-end', marginBottom: '12px' },
|
|
147
143
|
};
|
|
148
144
|
|
|
149
145
|
// Load plans on mount
|
|
@@ -154,7 +150,6 @@ export function ExecutionPlansPage({ darkMode }: ExecutionPlansPageProps): React
|
|
|
154
150
|
listPlans()
|
|
155
151
|
.then((p) => {
|
|
156
152
|
setPlans(p);
|
|
157
|
-
// Restore last selected plan from localStorage
|
|
158
153
|
const lastPlan = localStorage.getItem('ekka_exec_plan_id');
|
|
159
154
|
if (lastPlan && p.some((x) => x.id === lastPlan)) {
|
|
160
155
|
setSelectedPlanId(lastPlan);
|
|
@@ -173,26 +168,28 @@ export function ExecutionPlansPage({ darkMode }: ExecutionPlansPageProps): React
|
|
|
173
168
|
}
|
|
174
169
|
localStorage.setItem('ekka_exec_plan_id', selectedPlanId);
|
|
175
170
|
setSelectedPlan(null);
|
|
176
|
-
|
|
177
|
-
setRunEvents([]);
|
|
171
|
+
setRunsOffset(0);
|
|
178
172
|
|
|
179
173
|
getPlan(selectedPlanId)
|
|
180
174
|
.then((p) => {
|
|
181
175
|
setSelectedPlan(p);
|
|
182
|
-
// Prefill input JSON from schema
|
|
183
176
|
const defaults = buildDefaultInputs(p);
|
|
184
177
|
const lastInputs = localStorage.getItem(`ekka_exec_inputs_${selectedPlanId}`);
|
|
185
178
|
setInputJson(lastInputs || JSON.stringify(defaults, null, 2));
|
|
186
179
|
})
|
|
187
180
|
.catch((e) => setError(e.message));
|
|
188
181
|
|
|
189
|
-
loadRuns(selectedPlanId);
|
|
182
|
+
loadRuns(selectedPlanId, 0);
|
|
190
183
|
}, [selectedPlanId]);
|
|
191
184
|
|
|
192
|
-
const loadRuns = useCallback((planId: string) => {
|
|
193
|
-
listRuns(planId)
|
|
194
|
-
.then(
|
|
195
|
-
|
|
185
|
+
const loadRuns = useCallback((planId: string, offset: number) => {
|
|
186
|
+
listRuns(planId, runsLimit, offset)
|
|
187
|
+
.then((page) => {
|
|
188
|
+
setRuns(page.runs);
|
|
189
|
+
setRunsTotal(page.total);
|
|
190
|
+
setRunsOffset(offset);
|
|
191
|
+
})
|
|
192
|
+
.catch(() => { setRuns([]); setRunsTotal(0); });
|
|
196
193
|
}, []);
|
|
197
194
|
|
|
198
195
|
// Validate JSON on change
|
|
@@ -205,7 +202,6 @@ export function ExecutionPlansPage({ darkMode }: ExecutionPlansPageProps): React
|
|
|
205
202
|
}
|
|
206
203
|
}, [inputJson]);
|
|
207
204
|
|
|
208
|
-
// Execute
|
|
209
205
|
async function handleExecute() {
|
|
210
206
|
if (!selectedPlanId || jsonError) return;
|
|
211
207
|
setExecuting(true);
|
|
@@ -214,7 +210,7 @@ export function ExecutionPlansPage({ darkMode }: ExecutionPlansPageProps): React
|
|
|
214
210
|
const inputs = JSON.parse(inputJson);
|
|
215
211
|
localStorage.setItem(`ekka_exec_inputs_${selectedPlanId}`, inputJson);
|
|
216
212
|
await startRun(selectedPlanId, inputs);
|
|
217
|
-
loadRuns(selectedPlanId);
|
|
213
|
+
loadRuns(selectedPlanId, 0);
|
|
218
214
|
} catch (e) {
|
|
219
215
|
setError((e as Error).message);
|
|
220
216
|
} finally {
|
|
@@ -222,14 +218,9 @@ export function ExecutionPlansPage({ darkMode }: ExecutionPlansPageProps): React
|
|
|
222
218
|
}
|
|
223
219
|
}
|
|
224
220
|
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
const [run, events] = await Promise.all([getRun(runId), getRunEvents(runId)]);
|
|
229
|
-
setSelectedRun(run);
|
|
230
|
-
setRunEvents(events);
|
|
231
|
-
} catch (e) {
|
|
232
|
-
setError((e as Error).message);
|
|
221
|
+
function handleViewRun(runId: string) {
|
|
222
|
+
if (onViewRun) {
|
|
223
|
+
onViewRun(runId);
|
|
233
224
|
}
|
|
234
225
|
}
|
|
235
226
|
|
|
@@ -242,10 +233,11 @@ export function ExecutionPlansPage({ darkMode }: ExecutionPlansPageProps): React
|
|
|
242
233
|
}
|
|
243
234
|
|
|
244
235
|
function shortId(id: string): string {
|
|
245
|
-
return id.length > 12 ? id.slice(0, 8) + '...' : id;
|
|
236
|
+
return id && id.length > 12 ? id.slice(0, 8) + '...' : (id || '—');
|
|
246
237
|
}
|
|
247
238
|
|
|
248
239
|
function timeAgo(ts: string): string {
|
|
240
|
+
if (!ts) return '—';
|
|
249
241
|
const diff = Date.now() - new Date(ts).getTime();
|
|
250
242
|
const secs = Math.floor(diff / 1000);
|
|
251
243
|
if (secs < 60) return `${secs}s ago`;
|
|
@@ -255,6 +247,13 @@ export function ExecutionPlansPage({ darkMode }: ExecutionPlansPageProps): React
|
|
|
255
247
|
return `${hrs}h ago`;
|
|
256
248
|
}
|
|
257
249
|
|
|
250
|
+
function formatDuration(ms?: number): string {
|
|
251
|
+
if (ms == null) return '—';
|
|
252
|
+
if (ms < 1000) return `${ms}ms`;
|
|
253
|
+
if (ms < 60000) return `${(ms / 1000).toFixed(1)}s`;
|
|
254
|
+
return `${(ms / 60000).toFixed(1)}min`;
|
|
255
|
+
}
|
|
256
|
+
|
|
258
257
|
return (
|
|
259
258
|
<div style={styles.container}>
|
|
260
259
|
<header style={styles.header}>
|
|
@@ -310,7 +309,7 @@ export function ExecutionPlansPage({ darkMode }: ExecutionPlansPageProps): React
|
|
|
310
309
|
</button>
|
|
311
310
|
<button
|
|
312
311
|
style={{ ...styles.button, border: `1px solid ${colors.border}`, background: 'transparent', color: colors.text }}
|
|
313
|
-
onClick={() => loadRuns(selectedPlanId)}
|
|
312
|
+
onClick={() => loadRuns(selectedPlanId, runsOffset)}
|
|
314
313
|
>
|
|
315
314
|
Refresh Runs
|
|
316
315
|
</button>
|
|
@@ -321,33 +320,61 @@ export function ExecutionPlansPage({ darkMode }: ExecutionPlansPageProps): React
|
|
|
321
320
|
{/* Runs Grid */}
|
|
322
321
|
{selectedPlanId && runs.length > 0 && (
|
|
323
322
|
<div style={styles.card}>
|
|
324
|
-
<div style={
|
|
323
|
+
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '12px' }}>
|
|
324
|
+
<div style={styles.sectionTitle}>Runs</div>
|
|
325
|
+
<span style={{ fontSize: '11px', color: colors.textMuted }}>
|
|
326
|
+
{runsTotal > 0
|
|
327
|
+
? `${runsOffset + 1}\u2013${runsOffset + runs.length} of ${runsTotal}`
|
|
328
|
+
: `Showing ${runs.length}`}
|
|
329
|
+
</span>
|
|
330
|
+
</div>
|
|
325
331
|
<table style={styles.table}>
|
|
326
332
|
<thead>
|
|
327
333
|
<tr>
|
|
328
334
|
<th style={styles.th}>Run ID</th>
|
|
329
335
|
<th style={styles.th}>Status</th>
|
|
336
|
+
<th style={styles.th}>Duration</th>
|
|
330
337
|
<th style={styles.th}>Created</th>
|
|
331
|
-
<th style={styles.th}
|
|
338
|
+
<th style={styles.th}></th>
|
|
332
339
|
</tr>
|
|
333
340
|
</thead>
|
|
334
341
|
<tbody>
|
|
335
342
|
{runs.map((run) => (
|
|
336
|
-
<tr key={run.id}>
|
|
343
|
+
<tr key={run.id} style={{ cursor: 'pointer' }} onClick={() => handleViewRun(run.id)}>
|
|
337
344
|
<td style={{ ...styles.td, ...styles.mono }}>{shortId(run.id)}</td>
|
|
338
345
|
<td style={styles.td}>
|
|
339
346
|
<span style={{ ...styles.badge, background: `${statusColor(run.status)}20`, color: statusColor(run.status) }}>
|
|
340
347
|
{run.status}
|
|
341
348
|
</span>
|
|
342
349
|
</td>
|
|
350
|
+
<td style={{ ...styles.td, ...styles.mono, color: colors.textMuted }}>{formatDuration(run.duration_ms)}</td>
|
|
343
351
|
<td style={{ ...styles.td, color: colors.textMuted }}>{timeAgo(run.created_at)}</td>
|
|
344
352
|
<td style={styles.td}>
|
|
345
|
-
<button style={styles.link} onClick={() => handleViewRun(run.id)}>View</button>
|
|
353
|
+
<button style={styles.link} onClick={(e) => { e.stopPropagation(); handleViewRun(run.id); }}>View</button>
|
|
346
354
|
</td>
|
|
347
355
|
</tr>
|
|
348
356
|
))}
|
|
349
357
|
</tbody>
|
|
350
358
|
</table>
|
|
359
|
+
{/* Pagination controls */}
|
|
360
|
+
{(runsOffset > 0 || runsOffset + runs.length < runsTotal) && (
|
|
361
|
+
<div style={{ display: 'flex', justifyContent: 'flex-end', alignItems: 'center', gap: '8px', marginTop: '12px', paddingTop: '8px', borderTop: `1px solid ${colors.border}` }}>
|
|
362
|
+
<button
|
|
363
|
+
style={{ ...styles.button, padding: '5px 14px', fontSize: '12px', border: `1px solid ${colors.border}`, background: 'transparent', color: colors.text, ...(runsOffset === 0 ? styles.buttonDisabled : {}) }}
|
|
364
|
+
disabled={runsOffset === 0}
|
|
365
|
+
onClick={() => loadRuns(selectedPlanId, Math.max(0, runsOffset - runsLimit))}
|
|
366
|
+
>
|
|
367
|
+
{'\u2190'} Prev
|
|
368
|
+
</button>
|
|
369
|
+
<button
|
|
370
|
+
style={{ ...styles.button, padding: '5px 14px', fontSize: '12px', border: `1px solid ${colors.border}`, background: 'transparent', color: colors.text, ...(runsOffset + runs.length >= runsTotal ? styles.buttonDisabled : {}) }}
|
|
371
|
+
disabled={runsOffset + runs.length >= runsTotal}
|
|
372
|
+
onClick={() => loadRuns(selectedPlanId, runsOffset + runsLimit)}
|
|
373
|
+
>
|
|
374
|
+
Next {'\u2192'}
|
|
375
|
+
</button>
|
|
376
|
+
</div>
|
|
377
|
+
)}
|
|
351
378
|
</div>
|
|
352
379
|
)}
|
|
353
380
|
|
|
@@ -357,80 +384,6 @@ export function ExecutionPlansPage({ darkMode }: ExecutionPlansPageProps): React
|
|
|
357
384
|
</div>
|
|
358
385
|
)}
|
|
359
386
|
|
|
360
|
-
{/* Run Detail Panel */}
|
|
361
|
-
{selectedRun && (
|
|
362
|
-
<div style={styles.card}>
|
|
363
|
-
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '12px' }}>
|
|
364
|
-
<div style={styles.sectionTitle}>
|
|
365
|
-
Run Detail
|
|
366
|
-
<span style={{ ...styles.badge, background: `${statusColor(selectedRun.status)}20`, color: statusColor(selectedRun.status) }}>
|
|
367
|
-
{selectedRun.status}
|
|
368
|
-
</span>
|
|
369
|
-
</div>
|
|
370
|
-
<button style={styles.link} onClick={() => { setSelectedRun(null); setRunEvents([]); }}>Close</button>
|
|
371
|
-
</div>
|
|
372
|
-
|
|
373
|
-
<div style={{ ...styles.grid, marginBottom: '16px' }}>
|
|
374
|
-
<div>
|
|
375
|
-
<span style={styles.label}>Run ID</span>
|
|
376
|
-
<div style={styles.mono}>{selectedRun.id}</div>
|
|
377
|
-
</div>
|
|
378
|
-
<div>
|
|
379
|
-
<span style={styles.label}>Created</span>
|
|
380
|
-
<div style={{ fontSize: '12px' }}>{new Date(selectedRun.created_at).toLocaleString()}</div>
|
|
381
|
-
</div>
|
|
382
|
-
</div>
|
|
383
|
-
|
|
384
|
-
{selectedRun.error && (
|
|
385
|
-
<div style={{ ...styles.error, marginBottom: '12px' }}>
|
|
386
|
-
<strong>Error:</strong> {selectedRun.error}
|
|
387
|
-
</div>
|
|
388
|
-
)}
|
|
389
|
-
|
|
390
|
-
{selectedRun.outputs && Object.keys(selectedRun.outputs).length > 0 && (
|
|
391
|
-
<>
|
|
392
|
-
<span style={styles.label}>Outputs</span>
|
|
393
|
-
<pre style={styles.pre}>{JSON.stringify(selectedRun.outputs, null, 2)}</pre>
|
|
394
|
-
</>
|
|
395
|
-
)}
|
|
396
|
-
|
|
397
|
-
{/* Events Timeline */}
|
|
398
|
-
<div style={{ marginTop: '16px' }}>
|
|
399
|
-
<div style={styles.sectionTitle}>
|
|
400
|
-
Events ({runEvents.length})
|
|
401
|
-
<button style={styles.link} onClick={() => handleViewRun(selectedRun.id)}>Refresh</button>
|
|
402
|
-
</div>
|
|
403
|
-
{runEvents.length === 0 ? (
|
|
404
|
-
<div style={{ color: colors.textMuted, fontSize: '12px' }}>No events yet.</div>
|
|
405
|
-
) : (
|
|
406
|
-
<table style={styles.table}>
|
|
407
|
-
<thead>
|
|
408
|
-
<tr>
|
|
409
|
-
<th style={styles.th}>Time</th>
|
|
410
|
-
<th style={styles.th}>Type</th>
|
|
411
|
-
<th style={styles.th}>Step</th>
|
|
412
|
-
<th style={styles.th}>Detail</th>
|
|
413
|
-
</tr>
|
|
414
|
-
</thead>
|
|
415
|
-
<tbody>
|
|
416
|
-
{runEvents.map((ev) => (
|
|
417
|
-
<tr key={ev.id}>
|
|
418
|
-
<td style={{ ...styles.td, ...styles.mono, whiteSpace: 'nowrap' }}>
|
|
419
|
-
{new Date(ev.created_at).toLocaleTimeString()}
|
|
420
|
-
</td>
|
|
421
|
-
<td style={{ ...styles.td, ...styles.mono }}>{ev.event_type}</td>
|
|
422
|
-
<td style={{ ...styles.td, ...styles.mono, color: colors.textMuted }}>{ev.step_key || '—'}</td>
|
|
423
|
-
<td style={{ ...styles.td, fontSize: '11px', maxWidth: '300px', overflow: 'hidden', textOverflow: 'ellipsis' }}>
|
|
424
|
-
{ev.payload ? (typeof ev.payload === 'string' ? ev.payload : JSON.stringify(ev.payload).slice(0, 120)) : '—'}
|
|
425
|
-
</td>
|
|
426
|
-
</tr>
|
|
427
|
-
))}
|
|
428
|
-
</tbody>
|
|
429
|
-
</table>
|
|
430
|
-
)}
|
|
431
|
-
</div>
|
|
432
|
-
</div>
|
|
433
|
-
)}
|
|
434
387
|
</div>
|
|
435
388
|
);
|
|
436
389
|
}
|