WorkflowRunHistory
surface워크플로우 실행 이력 화면. 검색/상태 필터 + DataTable + 상세 Dialog 탭 뷰 조합.
컴포넌트 의존 관계
깊이
100%
기본 사용
Workflow Run History
| Run ID | Workflow | Status | Duration | Started |
|---|---|---|---|---|
| run-001 | Nightly ETL | success | 1.8s | 2026. 5. 24. 오후 6:46:00 |
| run-002 | Webhook Handler | error | 430ms | 2026. 5. 24. 오후 6:26:00 |
| run-003 | Monthly Billing | running | 940ms | 2026. 5. 24. 오후 6:54:00 |
테스트 커버리지
2026년 2월 4일생성된 테스트 결과를 찾지 못했습니다.
WorkflowRunHistory 항목이 문서 메타에 연결되어 있지만 현재 생성 파일에는 없습니다.
테스트를 추가한 뒤 `bun run generate:test-results`를 실행하거나 `testDescribe` 매핑을 다시 확인하세요.
WorkflowRunHistory Props
| Prop | 타입 | 기본값 | 설명 |
|---|---|---|---|
runs* | WorkflowRunRecord[] | — | 실행 기록 배열. { id, workflowName, status, startedAt, durationMs?, steps?, logs? } |
statusOptions | SelectOption[] | — | 상태 필터 옵션 목록 |
onRunClick | (run: WorkflowRunRecord) => void | — | 행 클릭 핸들러 |
labels | WorkflowRunHistoryLabels | — | i18n 라벨 오버라이드 |
Surface 설치
CLI가 공식 배포 채널입니다. 필요한 Surface를 프로젝트로 복사한 뒤 직접 수정할 수 있습니다.
bash
npx @reopt-ai/opt-cli surface add workflow-run-historyConsumer target
복사된 파일은 components/surfaces 아래에 저장됩니다.
tsx
import { WorkflowRunHistory } from "@/components/surfaces/workflow-run-history";Registry metadata
- 설명
- 워크플로우 실행 이력 화면. 검색/상태 필터 + DataTable + 상세 Dialog 탭 뷰 조합.
- 파일 수
- 1개
- Registry dependencies
- 없음
- Package dependencies
- 없음
- 태그
- table
- Install notes
- 없음
포함 파일
workflow-run-history.tsxworkflow-run-history.tsx
Surface 소스 보기
workflow-run-history.tsx
"use client";
import * as React from "react";
import { Input, Badge, DialogRoot, DialogPanel, DialogHeading, DialogHeader, DialogBody, SurfaceLayout, StatusSelect, DataTable, type ColumnDef, ContentTabs, formatOptDateTime } from "@reopt-ai/opt-ui";
import type { SelectOption } from "@reopt-ai/opt-ui";
/** Defines the workflow run step shape. */
export interface WorkflowRunStep {
id: string;
name: string;
status: "success" | "running" | "error" | "pending";
durationMs?: number;
}
/** Defines the workflow run record shape. */
export interface WorkflowRunRecord {
id: string;
workflowName: string;
status: "success" | "running" | "error" | "cancelled";
trigger?: string;
startedAt: string;
durationMs?: number;
steps?: WorkflowRunStep[];
logs?: string[];
}
/** Localized labels for the workflow run history component. */
export interface WorkflowRunHistoryLabels {
title?: string;
searchPlaceholder?: string;
statusLabel?: string;
emptyMessage?: string;
detailsTitle?: string;
summaryTab?: string;
stepsTab?: string;
logsTab?: string;
workflowLabel?: string;
runStatusLabel?: string;
durationLabel?: string;
startedLabel?: string;
triggerLabel?: string;
noSteps?: string;
noLogs?: string;
}
/** Props for the workflow run history component. */
export interface WorkflowRunHistoryProps {
runs: WorkflowRunRecord[];
statusOptions?: SelectOption[];
loading?: boolean;
labels?: WorkflowRunHistoryLabels;
onRunClick?: (run: WorkflowRunRecord) => void;
header?: React.ReactNode;
actions?: React.ReactNode;
className?: string;
}
const defaultLabels: Required<WorkflowRunHistoryLabels> = {
title: "Workflow Run History",
searchPlaceholder: "Search by run ID, workflow, or trigger",
statusLabel: "Status",
emptyMessage: "No workflow runs found",
detailsTitle: "Run Details",
summaryTab: "Summary",
stepsTab: "Steps",
logsTab: "Logs",
workflowLabel: "Workflow",
runStatusLabel: "Status",
durationLabel: "Duration",
startedLabel: "Started",
triggerLabel: "Trigger",
noSteps: "No steps",
noLogs: "No logs",
};
const defaultStatusOptions: SelectOption[] = [
{ value: "all", label: "All" },
{ value: "success", label: "Success" },
{ value: "running", label: "Running" },
{ value: "error", label: "Error" },
{ value: "cancelled", label: "Cancelled" },
];
function formatDuration(durationMs?: number): string {
if (typeof durationMs !== "number") {
return "-";
}
if (durationMs >= 1000) {
return `${(durationMs / 1000).toFixed(1)}s`;
}
return `${durationMs}ms`;
}
function getRunStatusVariant(
status: WorkflowRunRecord["status"],
): "success" | "error" | "info" | "warning" {
if (status === "success") {
return "success";
}
if (status === "error") {
return "error";
}
if (status === "running") {
return "info";
}
return "warning";
}
/** Renders the workflow run history component. */
export function WorkflowRunHistory({
runs,
statusOptions = defaultStatusOptions,
loading = false,
labels: customLabels,
onRunClick,
header,
actions,
className,
}: WorkflowRunHistoryProps) {
const labels = { ...defaultLabels, ...customLabels };
const [search, setSearch] = React.useState("");
const [status, setStatus] = React.useState("all");
const [selectedRun, setSelectedRun] =
React.useState<WorkflowRunRecord | null>(null);
const filteredRuns = React.useMemo(() => {
const query = search.trim().toLowerCase();
return runs.filter((run) => {
const statusMatch = status === "all" || run.status === status;
const searchMatch =
query.length === 0 ||
run.id.toLowerCase().includes(query) ||
run.workflowName.toLowerCase().includes(query) ||
(run.trigger ?? "").toLowerCase().includes(query);
return statusMatch && searchMatch;
});
}, [runs, search, status]);
const columns: ColumnDef<WorkflowRunRecord>[] = React.useMemo(
() => [
{
id: "id",
header: "Run ID",
accessor: "id",
},
{
id: "workflowName",
header: "Workflow",
accessor: "workflowName",
},
{
id: "status",
header: "Status",
accessor: (run) => (
<Badge variant={getRunStatusVariant(run.status)} size="sm">
{run.status}
</Badge>
),
},
{
id: "durationMs",
header: "Duration",
accessor: (run) => formatDuration(run.durationMs),
},
{
id: "startedAt",
header: "Started",
accessor: (run) => formatOptDateTime(run.startedAt),
},
],
[],
);
return (
<SurfaceLayout
className={className}
loading={loading}
data-opt-id="6ZRK8"
data-opt-slug="workflow-run-history.WorkflowRunHistory"
>
<div className="gap-group flex flex-wrap items-end justify-between">
<div className="min-w-0">
{header ?? (
<h3 className="text-text-primary text-lg font-semibold">
{labels.title}
</h3>
)}
</div>
{actions && (
<div className="gap-element flex items-center">{actions}</div>
)}
</div>
<div className="border-border bg-bg-subtle gap-group grid rounded-lg border p-4 md:grid-cols-[minmax(0,1fr)_220px] md:items-start">
<div className="min-w-0">
<Input
label={labels.searchPlaceholder}
placeholder={labels.searchPlaceholder}
value={search}
onChange={(event) => setSearch(event.target.value)}
className="w-full"
/>
</div>
<div className="w-full md:w-[220px]">
<StatusSelect
label={labels.statusLabel}
options={statusOptions}
value={status}
onChange={setStatus}
/>
</div>
</div>
<DataTable
title=""
data={filteredRuns}
columns={columns}
keyExtractor={(run) => run.id}
onRowClick={(run) => {
setSelectedRun(run);
onRunClick?.(run);
}}
emptyMessage={labels.emptyMessage}
/>
<DialogRoot
open={selectedRun !== null}
onOpenChange={(open) => {
if (!open) {
setSelectedRun(null);
}
}}
>
<DialogPanel className="flex max-h-[85vh] w-full max-w-3xl flex-col overflow-hidden">
{selectedRun && (
<>
<DialogHeader>
<DialogHeading className="text-base">
{labels.detailsTitle}: {selectedRun.id}
</DialogHeading>
</DialogHeader>
<DialogBody>
<ContentTabs
tabs={[
{
id: "summary",
label: labels.summaryTab,
content: (
<div className="bg-bg-subtle border-border gap-group flex flex-col rounded-lg border p-4 text-sm">
<p className="leading-6">
<strong>{labels.workflowLabel}:</strong>{" "}
{selectedRun.workflowName}
</p>
<p className="leading-6">
<strong>{labels.runStatusLabel}:</strong>{" "}
{selectedRun.status}
</p>
<p className="leading-6">
<strong>{labels.durationLabel}:</strong>{" "}
{formatDuration(selectedRun.durationMs)}
</p>
<p className="leading-6">
<strong>{labels.startedLabel}:</strong>{" "}
{formatOptDateTime(selectedRun.startedAt)}
</p>
<p className="leading-6">
<strong>{labels.triggerLabel}:</strong>{" "}
{selectedRun.trigger ?? "-"}
</p>
</div>
),
},
{
id: "steps",
label: labels.stepsTab,
content: (
<div className="bg-bg-subtle border-border gap-group flex flex-col rounded-lg border p-4">
{(selectedRun.steps ?? []).length === 0 ? (
<p className="text-text-secondary text-sm">
{labels.noSteps}
</p>
) : (
(selectedRun.steps ?? []).map((step) => (
<div
key={step.id}
className="border-border bg-surface flex items-center justify-between rounded-lg border px-3 py-2.5"
>
<span className="text-sm font-medium">
{step.name}
</span>
<div className="gap-element flex items-center">
<Badge size="sm" variant="default">
{step.status}
</Badge>
<span className="text-text-secondary text-xs">
{formatDuration(step.durationMs)}
</span>
</div>
</div>
))
)}
</div>
),
},
{
id: "logs",
label: labels.logsTab,
content: (
<pre className="bg-bg-subtle border-border max-h-[320px] overflow-auto rounded-lg border p-4 text-xs leading-6 whitespace-pre-wrap">
{(selectedRun.logs ?? []).length === 0
? labels.noLogs
: (selectedRun.logs ?? []).join("\n")}
</pre>
),
},
]}
/>
</DialogBody>
</>
)}
</DialogPanel>
</DialogRoot>
</SurfaceLayout>
);
}
WorkflowRunHistory.displayName = "WorkflowRunHistory";