reopt designreopt design
DocsExploreToolsPricingBuilder
Start
Overview
Start
Next.js 설치
Private install
Core Concepts
아키텍처
Composition Patterns
Accessibility
Keyboard Patterns
Styling
Theme System
Advanced Patterns
Build & Operate
Skills
AI Integration
CLI (opt surface add)
Dependency Graph
Tools
Canvas Catalog
Theme Builder
Form Builder
Templates
Templates
Releases
Release Notes
Oopt-ui
reopt designreopt design

A design system for the AI era

  • Docs
  • Pricing
  • Releases
  • GitHub
  • Terms of Service
  • Privacy Policy

© 2026 reopt-ai. All rights reserved.

WorkflowRunHistory

surface

워크플로우 실행 이력 화면. 검색/상태 필터 + DataTable + 상세 Dialog 탭 뷰 조합.

컴포넌트 의존 관계

깊이
▼ USES (4)WorkflowRunHistoryinputstatus-selectdata-tablecontent-tabs
100%

기본 사용

Workflow Run History

Run IDWorkflowStatusDurationStarted
run-001Nightly ETLsuccess1.8s2026. 5. 24. 오후 6:46:00
run-002Webhook Handlererror430ms2026. 5. 24. 오후 6:26:00
run-003Monthly Billingrunning940ms2026. 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? }
statusOptionsSelectOption[]—상태 필터 옵션 목록
onRunClick(run: WorkflowRunRecord) => void—행 클릭 핸들러
labelsWorkflowRunHistoryLabels—i18n 라벨 오버라이드

Surface 설치

CLI가 공식 배포 채널입니다. 필요한 Surface를 프로젝트로 복사한 뒤 직접 수정할 수 있습니다.

bash
npx @reopt-ai/opt-cli surface add workflow-run-history

Consumer 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.tsx→workflow-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";