feat: add dag_engine app — CLI + web frontend for DAG execution (0007e)
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>
This commit is contained in:
@@ -0,0 +1,105 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user