← Files NGS Analysis WorkbenchARCHIVED FILE

src/components/RunHistorySidebar.tsx

13.5 KB · Sep 30, 2026 · 23:20 UTC

↓ Download file

import { Tabs } from "@base-ui/react/tabs";
import { Icon, StatusIcon } from "../design-system/Icon";
import { useDialogMotion } from "../design-system/useDialogMotion";
import { useEffect, useRef, useState, type RefObject } from "react";

import type { RunDetail, RunObservation, RunSummary } from "../model";
import { isActiveRunStatus, runStatusPresentation } from "../runStatus";
import { RunStatus } from "./RunStatus";
import styles from "./RunHistorySidebar.module.css";

const TAB_EDGE_FADE = 24;

function revealTab(list: HTMLElement, tab: HTMLElement) {
  const left = tab.offsetLeft;
  const right = left + tab.offsetWidth;
  if (left < list.scrollLeft + TAB_EDGE_FADE) {
    list.scrollLeft = Math.max(0, left - TAB_EDGE_FADE);
  } else if (right > list.scrollLeft + list.clientWidth - TAB_EDGE_FADE) {
    list.scrollLeft = right - list.clientWidth + TAB_EDGE_FADE;
  }
}

export function AttemptsToggle({
  attemptCount,
  buttonRef,
  modal = false,
  onClick,
  open,
}: {
  attemptCount: number;
  buttonRef?: RefObject<HTMLButtonElement | null>;
  modal?: boolean;
  onClick: () => void;
  open: boolean;
}) {
  return (
    <button
      aria-label={open ? "Close run history" : "Open run history"}
      aria-controls="run-attempts-panel"
      aria-expanded={open}
      aria-haspopup={modal && !open ? "dialog" : undefined}
      className={styles.historyToggle}
      onClick={onClick}
      ref={buttonRef}
      type="button"
    >
      <span>Run history · {attemptCount}</span>
      <Icon name="menu" size={16} />
    </button>
  );
}

export function RunHistorySidebar({
  analysisLoading,
  analysisObservation,
  analysisRun,
  executionInMain = false,
  lineageHead,
  loadRunDetail,
  loadRunLineage,
  loadRunObservation,
  observationError,
  open,
  modal,
  onClose,
  toggleRef,
  onRefreshAnalysis,
  refreshingAnalysis,
}: {
  analysisLoading: boolean;
  analysisObservation?: RunObservation;
  analysisRun?: RunDetail;
  executionInMain?: boolean;
  lineageHead: RunSummary;
  loadRunDetail: (run: RunSummary) => Promise<RunDetail>;
  loadRunLineage: (run: RunSummary) => Promise<RunSummary[]>;
  loadRunObservation: (run: RunSummary) => Promise<RunObservation>;
  observationError?: { registryRunId: string; message: string };
  open: boolean;
  modal: boolean;
  onClose: () => void;
  toggleRef: RefObject<HTMLButtonElement | null>;
  onRefreshAnalysis: () => void;
  refreshingAnalysis: boolean;
}) {
  const [attempts, setAttempts] = useState<RunSummary[]>([lineageHead]);
  const multipleAttempts = attempts.length > 1;
  const [selectedAttemptId, setSelectedAttemptId] = useState(
    lineageHead.registry_run_id,
  );
  const [detailCache, setDetailCache] = useState<Record<string, RunDetail>>({});
  const [observationCache, setObservationCache] = useState<
    Record<string, RunObservation>
  >({});
  const [detailLoading, setDetailLoading] = useState(false);
  const [detailError, setDetailError] = useState<string>();
  const detailRequestId = useRef(0);
  const dialogRef = useRef<HTMLDialogElement>(null);
  const scrollRef = useRef<HTMLDivElement>(null);
  const closeDialog = useDialogMotion(dialogRef);
  const tabListRef = useRef<HTMLDivElement>(null);
  const [tabOverflow, setTabOverflow] = useState({ left: false, right: false });

  useEffect(() => {
    const dialog = dialogRef.current;
    if (!modal || !dialog) return;
    if (open && !dialog.open) dialog.showModal();
    else if (!open && dialog.open) dialog.close();
  }, [modal, open]);

  useEffect(() => {
    scrollRef.current?.scrollTo({ top: 0 });
  }, [selectedAttemptId]);

  useEffect(() => {
    if (!open) return;
    const list = tabListRef.current;
    if (!list) return;
    const updateOverflow = () => {
      const left = list.scrollLeft > 1;
      const right = list.scrollLeft + list.clientWidth < list.scrollWidth - 1;
      setTabOverflow((current) =>
        current.left === left && current.right === right
          ? current
          : { left, right },
      );
    };
    const revealSelectedTab = () => {
      const tab = list.querySelector<HTMLElement>('[role="tab"][data-active]');
      if (!tab || !list.clientWidth) return;
      revealTab(list, tab);
      updateOverflow();
    };
    revealSelectedTab();
    // Reopening or resizing the inspector can change the available tab width.
    // Scroll only this strip, without moving the report or drawer vertically.
    const observer = new ResizeObserver(revealSelectedTab);
    observer.observe(list);
    list
      .querySelectorAll('[role="tab"]')
      .forEach((tab) => observer.observe(tab));
    list.addEventListener("scroll", updateOverflow, { passive: true });
    return () => {
      observer.disconnect();
      list.removeEventListener("scroll", updateOverflow);
    };
  }, [attempts.length, selectedAttemptId, open, modal]);

  useEffect(() => {
    let disposed = false;
    setAttempts([lineageHead]);
    setSelectedAttemptId(lineageHead.registry_run_id);
    setDetailCache({});
    setObservationCache({});
    setDetailError(undefined);
    void loadRunLineage(lineageHead)
      .then((loaded) => {
        if (!disposed && loaded.length) {
          const ordered = [...loaded].sort(
            (left, right) => left.attempt_number - right.attempt_number,
          );
          setAttempts(ordered);
          setSelectedAttemptId((current) =>
            ordered.some((attempt) => attempt.registry_run_id === current)
              ? current
              : lineageHead.registry_run_id,
          );
        }
      })
      .catch(() => {
        // The selected execution remains usable if lineage history cannot be loaded.
      });
    return () => {
      disposed = true;
    };
  }, [lineageHead.first_run_id, lineageHead.registry_run_id, loadRunLineage]);

  useEffect(() => {
    setAttempts((current) =>
      current.map((attempt) =>
        attempt.registry_run_id === lineageHead.registry_run_id
          ? lineageHead
          : attempt,
      ),
    );
  }, [lineageHead]);

  useEffect(() => {
    if (
      selectedAttemptId === lineageHead.registry_run_id &&
      analysisRun?.registry_run_id === lineageHead.registry_run_id
    ) {
      setDetailLoading(false);
      setDetailError(undefined);
    }
  }, [analysisRun, lineageHead.registry_run_id, selectedAttemptId]);

  const selectedAttempt =
    attempts.find((attempt) => attempt.registry_run_id === selectedAttemptId) ??
    lineageHead;

  const refreshSelectedObservation = () => {
    const requestId = ++detailRequestId.current;
    setDetailLoading(true);
    setDetailError(undefined);
    void loadRunObservation(selectedAttempt)
      .then((observation) => {
        if (requestId !== detailRequestId.current) return;
        setObservationCache((current) => ({
          ...current,
          [selectedAttemptId]: observation,
        }));
      })
      .catch((error: unknown) => {
        if (requestId !== detailRequestId.current) return;
        setDetailError(
          error instanceof Error
            ? error.message
            : "Could not observe this workflow attempt.",
        );
      })
      .finally(() => {
        if (requestId === detailRequestId.current) setDetailLoading(false);
      });
  };

  const selectAttempt = (attempt: RunSummary) => {
    const requestId = ++detailRequestId.current;
    setSelectedAttemptId(attempt.registry_run_id);
    setDetailError(undefined);
    if (attempt.registry_run_id === lineageHead.registry_run_id) {
      setDetailLoading(false);
      return;
    }
    if (detailCache[attempt.registry_run_id]) {
      setDetailLoading(false);
      return;
    }
    setDetailLoading(true);
    void loadRunDetail(attempt)
      .then((detail) => {
        if (requestId !== detailRequestId.current) return;
        setDetailCache((current) => ({
          ...current,
          [attempt.registry_run_id]: detail,
        }));
      })
      .catch((error: unknown) => {
        if (requestId !== detailRequestId.current) return;
        setDetailError(
          error instanceof Error
            ? error.message
            : "Could not load this workflow attempt.",
        );
      })
      .finally(() => {
        if (requestId === detailRequestId.current) setDetailLoading(false);
      });
  };

  const contents = (
    <Tabs.Root
      className={styles.attemptTabs}
      value={selectedAttemptId}
      onValueChange={(value) => {
        const attempt = attempts.find((item) => item.registry_run_id === value);
        if (attempt) selectAttempt(attempt);
      }}
    >
      <header className={styles.runHistoryPaneHeader}>
        <AttemptsToggle
          attemptCount={lineageHead.attempt_number}
          buttonRef={modal || !open ? undefined : toggleRef}
          onClick={modal ? closeDialog : onClose}
          open
        />
      </header>

      {multipleAttempts ? (
        <div className={styles.tabStrip}>
          <Tabs.List
            className={styles.attemptSelector}
            aria-label="Workflow attempts"
            activateOnFocus={false}
            data-overflow-left={tabOverflow.left}
            data-overflow-right={tabOverflow.right}
            onFocusCapture={(event) => {
              if (
                event.target instanceof HTMLElement &&
                event.target.getAttribute("role") === "tab"
              ) {
                revealTab(event.currentTarget, event.target);
              }
            }}
            ref={tabListRef}
          >
            {attempts.map((attempt) => (
              <Tabs.Tab
                className={styles.attemptTab}
                key={attempt.registry_run_id}
                value={attempt.registry_run_id}
                aria-label={`Attempt ${attempt.attempt_number}: ${
                  runStatusPresentation(attempt.status).label
                }`}
              >
                <span
                  className={styles.attemptStatus}
                  data-tone={runStatusPresentation(attempt.status).tone}
                >
                  <StatusIcon
                    tone={runStatusPresentation(attempt.status).tone}
                    active={isActiveRunStatus(attempt.status)}
                  />
                </span>
                Attempt {attempt.attempt_number}
              </Tabs.Tab>
            ))}
            <Tabs.Indicator className={styles.tabIndicator} />
          </Tabs.List>
        </div>
      ) : null}

      <div className={styles.panelsViewport}>
        {attempts.map((attempt) => {
          const isSelected = attempt.registry_run_id === selectedAttemptId;
          const isAnalysis =
            attempt.registry_run_id === analysisRun?.registry_run_id;
          const detail = isAnalysis
            ? analysisRun
            : detailCache[attempt.registry_run_id];
          const observation = isAnalysis
            ? analysisObservation
            : observationCache[attempt.registry_run_id];
          const error = isSelected
            ? detailError ??
              (observationError?.registryRunId === attempt.registry_run_id
                ? observationError.message
                : undefined)
            : undefined;
          return (
            <Tabs.Panel
              className={styles.paneBody}
              key={attempt.registry_run_id}
              value={attempt.registry_run_id}
              ref={isSelected ? scrollRef : undefined}
              role={multipleAttempts ? "tabpanel" : "region"}
              aria-label={
                multipleAttempts
                  ? undefined
                  : `Attempt ${attempt.attempt_number}: ${
                      runStatusPresentation(attempt.status).label
                    }`
              }
              aria-busy={isSelected && detailLoading}
            >
              {error ? (
                <p className={styles.runHistoryError} role="alert">
                  {error}
                </p>
              ) : null}
              {detail ? (
                <RunStatus
                  busy={
                    isAnalysis
                      ? refreshingAnalysis
                      : isSelected && detailLoading
                  }
                  compact
                  showExecution={!executionInMain || !isAnalysis}
                  observation={observation}
                  onRefresh={
                    isAnalysis ? onRefreshAnalysis : refreshSelectedObservation
                  }
                  run={detail}
                  summary={attempt}
                />
              ) : (
                <section className={styles.attemptLoading} aria-live="polite">
                  <h3>
                    {(isSelected && detailLoading) ||
                    (isAnalysis && analysisLoading)
                      ? `Loading attempt ${attempt.attempt_number}…`
                      : "Attempt details unavailable"}
                  </h3>
                </section>
              )}
            </Tabs.Panel>
          );
        })}
      </div>
    </Tabs.Root>
  );

  return modal ? (
    <dialog
      aria-label="Run history"
      className={styles.drawer}
      id="run-attempts-panel"
      onCancel={(event) => {
        event.preventDefault();
        closeDialog();
      }}
      onKeyDown={(event) => {
        if (event.key === "Escape") {
          event.preventDefault();
          closeDialog();
        }
      }}
      onClick={(event) => {
        if (event.target === event.currentTarget) closeDialog();
      }}
      onClose={onClose}
      ref={dialogRef}
    >
      {contents}
    </dialog>
  ) : (
    <aside
      aria-label="Run history"
      className={styles.runHistoryPane}
      hidden={!open}
      id="run-attempts-panel"
    >
      {contents}
    </aside>
  );
}

SHA-256: 9db1386c372f76569ff14289502a7d5471452d8902a80509e2551c8ab3d646f6