reopt designreopt design
DocsExploreToolsPricingBuilder
시작하기
개요
시작하기
Next.js 설치
Private install
핵심 개념
아키텍처
컴포지션 패턴
접근성
키보드 패턴
스타일링
테마 시스템
고급 패턴
구축·운영
Skills
AI 연동
CLI (opt surface add)
의존 그래프
도구
Canvas 카탈로그
Theme Builder
Form Builder
템플릿
템플릿
릴리즈
릴리즈 노트
Oopt-ui
reopt designreopt design

AI 시대를 위한 디자인 시스템

  • 문서
  • 가격
  • 릴리즈 노트
  • GitHub
  • 서비스 약관
  • 개인정보처리방침

© 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";