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>
205 lines
5.4 KiB
TypeScript
205 lines
5.4 KiB
TypeScript
import { useEffect, useState } from "react";
|
|
import { useParams, useNavigate } from "react-router-dom";
|
|
import {
|
|
Title,
|
|
Text,
|
|
Group,
|
|
Button,
|
|
Badge,
|
|
Stack,
|
|
Paper,
|
|
Table,
|
|
Alert,
|
|
Loader,
|
|
Code,
|
|
} from "@mantine/core";
|
|
import { IconPlayerPlay, IconArrowLeft } from "@tabler/icons-react";
|
|
import { getDag, triggerDag } from "../api";
|
|
import { StatusBadge } from "../components/StatusBadge";
|
|
import type { DagDetail as DagDetailType } from "../types";
|
|
|
|
export function DagDetail() {
|
|
const { name } = useParams<{ name: string }>();
|
|
const navigate = useNavigate();
|
|
const [data, setData] = useState<DagDetailType | null>(null);
|
|
const [loading, setLoading] = useState(true);
|
|
const [error, setError] = useState<string | null>(null);
|
|
const [triggering, setTriggering] = useState(false);
|
|
|
|
const load = async () => {
|
|
if (!name) return;
|
|
setLoading(true);
|
|
try {
|
|
setData(await getDag(name));
|
|
setError(null);
|
|
} catch (e) {
|
|
setError((e as Error).message);
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
useEffect(() => {
|
|
load();
|
|
}, [name]);
|
|
|
|
const handleRun = async () => {
|
|
if (!name) return;
|
|
setTriggering(true);
|
|
try {
|
|
await triggerDag(name);
|
|
setTimeout(load, 1000);
|
|
} catch (e) {
|
|
setError((e as Error).message);
|
|
} finally {
|
|
setTriggering(false);
|
|
}
|
|
};
|
|
|
|
if (loading) return <Loader />;
|
|
if (error) return <Alert color="red">{error}</Alert>;
|
|
if (!data) return <Text>Not found</Text>;
|
|
|
|
const { dag, validation, runs } = data;
|
|
|
|
return (
|
|
<Stack gap="md">
|
|
<Group>
|
|
<Button
|
|
variant="subtle"
|
|
size="xs"
|
|
leftSection={<IconArrowLeft size={14} />}
|
|
onClick={() => navigate("/")}
|
|
>
|
|
Back
|
|
</Button>
|
|
</Group>
|
|
|
|
<Group justify="space-between">
|
|
<div>
|
|
<Title order={2}>{dag.Name}</Title>
|
|
{dag.Description && (
|
|
<Text size="sm" c="dimmed">
|
|
{dag.Description}
|
|
</Text>
|
|
)}
|
|
</div>
|
|
<Button
|
|
leftSection={<IconPlayerPlay size={16} />}
|
|
onClick={handleRun}
|
|
loading={triggering}
|
|
>
|
|
Run Now
|
|
</Button>
|
|
</Group>
|
|
|
|
<Group gap="xs">
|
|
{dag.Schedule?.map((s: string) => (
|
|
<Badge key={s} variant="light" ff="monospace">
|
|
{s}
|
|
</Badge>
|
|
))}
|
|
<Badge variant="light">{dag.Type || "chain"}</Badge>
|
|
{dag.Tags?.map((t: string) => (
|
|
<Badge key={t} variant="dot">
|
|
{t}
|
|
</Badge>
|
|
))}
|
|
</Group>
|
|
|
|
{!validation.Valid && (
|
|
<Alert color="red" title="Validation errors">
|
|
{validation.Errors.map((e: string, i: number) => (
|
|
<Text key={i} size="sm">
|
|
{e}
|
|
</Text>
|
|
))}
|
|
</Alert>
|
|
)}
|
|
|
|
<Paper p="md" withBorder>
|
|
<Title order={4} mb="sm">
|
|
Steps ({dag.Steps?.length || 0})
|
|
</Title>
|
|
{validation.Levels?.map((level: string[], i: number) => (
|
|
<Group key={i} gap="xs" mb="xs">
|
|
<Text size="xs" c="dimmed" w={60}>
|
|
Level {i}:
|
|
</Text>
|
|
{level.map((name: string) => {
|
|
const step = dag.Steps?.find(
|
|
(s) => s.Name === name || s.ID === name
|
|
);
|
|
return (
|
|
<Badge key={name} variant="outline" size="sm">
|
|
{name}
|
|
{step?.Depends?.length
|
|
? ` (after ${step.Depends.join(",")})`
|
|
: ""}
|
|
</Badge>
|
|
);
|
|
})}
|
|
</Group>
|
|
))}
|
|
|
|
{dag.Env && Object.keys(dag.Env).length > 0 && (
|
|
<>
|
|
<Title order={5} mt="md" mb="xs">
|
|
Environment
|
|
</Title>
|
|
<Code block>
|
|
{Object.entries(dag.Env)
|
|
.map(([k, v]) => `${k}=${v}`)
|
|
.join("\n")}
|
|
</Code>
|
|
</>
|
|
)}
|
|
</Paper>
|
|
|
|
<Paper p="md" withBorder>
|
|
<Title order={4} mb="sm">
|
|
Run History
|
|
</Title>
|
|
{runs?.length ? (
|
|
<Table striped>
|
|
<Table.Thead>
|
|
<Table.Tr>
|
|
<Table.Th>Status</Table.Th>
|
|
<Table.Th>Trigger</Table.Th>
|
|
<Table.Th>Started</Table.Th>
|
|
<Table.Th>Duration</Table.Th>
|
|
</Table.Tr>
|
|
</Table.Thead>
|
|
<Table.Tbody>
|
|
{runs.map((r) => (
|
|
<Table.Tr
|
|
key={r.ID}
|
|
style={{ cursor: "pointer" }}
|
|
onClick={() => navigate(`/runs/${r.ID}`)}
|
|
>
|
|
<Table.Td>
|
|
<StatusBadge status={r.Status} />
|
|
</Table.Td>
|
|
<Table.Td>{r.Trigger}</Table.Td>
|
|
<Table.Td>
|
|
{new Date(r.StartedAt).toLocaleString()}
|
|
</Table.Td>
|
|
<Table.Td>
|
|
{r.FinishedAt
|
|
? `${Math.round((new Date(r.FinishedAt).getTime() - new Date(r.StartedAt).getTime()) / 1000)}s`
|
|
: "running..."}
|
|
</Table.Td>
|
|
</Table.Tr>
|
|
))}
|
|
</Table.Tbody>
|
|
</Table>
|
|
) : (
|
|
<Text size="sm" c="dimmed">
|
|
No runs yet
|
|
</Text>
|
|
)}
|
|
</Paper>
|
|
</Stack>
|
|
);
|
|
}
|