d9414e4cba
Full DAG engine app with CLI subcommands (run, list, status, validate, server) and React/Mantine web frontend. Uses net/http + embedded Vite build. SQLite store for run history. Scheduler with cron_ticker for automated execution. Compatible with existing dagu YAML format. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
106 lines
2.7 KiB
TypeScript
106 lines
2.7 KiB
TypeScript
import { useEffect, useState } from "react";
|
|
import { useParams, useNavigate } from "react-router-dom";
|
|
import {
|
|
Title,
|
|
Text,
|
|
Group,
|
|
Button,
|
|
Stack,
|
|
Paper,
|
|
Alert,
|
|
Loader,
|
|
} from "@mantine/core";
|
|
import { IconArrowLeft } from "@tabler/icons-react";
|
|
import { getRun } from "../api";
|
|
import { StatusBadge } from "../components/StatusBadge";
|
|
import { StepTimeline } from "../components/StepTimeline";
|
|
import type { RunDetail as RunDetailType } from "../types";
|
|
|
|
export function RunDetail() {
|
|
const { id } = useParams<{ id: string }>();
|
|
const navigate = useNavigate();
|
|
const [data, setData] = useState<RunDetailType | null>(null);
|
|
const [loading, setLoading] = useState(true);
|
|
const [error, setError] = useState<string | null>(null);
|
|
|
|
const load = async () => {
|
|
if (!id) return;
|
|
try {
|
|
setData(await getRun(id));
|
|
setError(null);
|
|
} catch (e) {
|
|
setError((e as Error).message);
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
useEffect(() => {
|
|
load();
|
|
// Auto-refresh while running.
|
|
const interval = setInterval(() => {
|
|
if (data?.run.Status === "running") {
|
|
load();
|
|
}
|
|
}, 2000);
|
|
return () => clearInterval(interval);
|
|
}, [id, data?.run.Status]);
|
|
|
|
if (loading) return <Loader />;
|
|
if (error) return <Alert color="red">{error}</Alert>;
|
|
if (!data) return <Text>Not found</Text>;
|
|
|
|
const { run, steps } = data;
|
|
const duration = run.FinishedAt
|
|
? `${Math.round((new Date(run.FinishedAt).getTime() - new Date(run.StartedAt).getTime()) / 1000)}s`
|
|
: "running...";
|
|
|
|
return (
|
|
<Stack gap="md">
|
|
<Group>
|
|
<Button
|
|
variant="subtle"
|
|
size="xs"
|
|
leftSection={<IconArrowLeft size={14} />}
|
|
onClick={() => navigate(`/dags/${run.DagName}`)}
|
|
>
|
|
Back to {run.DagName}
|
|
</Button>
|
|
</Group>
|
|
|
|
<Group justify="space-between">
|
|
<div>
|
|
<Title order={2}>Run {run.ID.substring(0, 16)}...</Title>
|
|
<Text size="sm" c="dimmed">
|
|
{run.DagName} · {run.Trigger} ·{" "}
|
|
{new Date(run.StartedAt).toLocaleString()}
|
|
</Text>
|
|
</div>
|
|
<Group gap="xs">
|
|
<StatusBadge status={run.Status} />
|
|
<Text size="sm">{duration}</Text>
|
|
</Group>
|
|
</Group>
|
|
|
|
{run.Error && (
|
|
<Alert color="red" title="Error">
|
|
{run.Error}
|
|
</Alert>
|
|
)}
|
|
|
|
<Paper p="md" withBorder>
|
|
<Title order={4} mb="md">
|
|
Steps ({steps?.length || 0})
|
|
</Title>
|
|
{steps?.length ? (
|
|
<StepTimeline steps={steps} />
|
|
) : (
|
|
<Text size="sm" c="dimmed">
|
|
No steps recorded
|
|
</Text>
|
|
)}
|
|
</Paper>
|
|
</Stack>
|
|
);
|
|
}
|