Files
orca/src/shared/automation-run-cursor.ts
T
Jinjing e3de6b2ce8 Add automation runs dashboard with pagination and filtering (#18226)
* Add automation runs dashboard with pagination and filtering

Adds a new Runs view in the Automations page that lets users browse all runs across automations with status/host filtering, search, and pagination support. Includes virtualized table rendering for efficient handling of large run histories and summary cards showing 24h/7d success/failure counts.

* Fix missing dependencies in useCallback hooks and imports

Missing dependencies in useCallback can cause stale closure bugs. This
adds missing state setters to dependency arrays and consolidates type
imports for consistency.

* Use keyset pagination for stable automation runs pages

Pagination now uses createdAt:id boundaries instead of offsets, so new
runs arriving between pages don't shift the window. Maintains backwards
compatibility with legacy offset cursors.

Move pagination to shared module, fix outcome counting for future-dated
runs, and improve hook state tracking on authority re-pairing or target
changes.

* Extract automation run details to top-level page view

Moves run display from detail pane to dedicated page, establishing
three-level navigation (Automations → Runs → Run Details) and simplifying
the detail pane component.

* Fix pagination stability when automation runs share createdAt

- Define a stable total order with createdAt and id tiebreaker to prevent runs tied on createdAt from being dropped when the boundary run is pruned between page requests
- Retain cursor on failed pagination so pages remain retryable
- Update ownerNotice type to AutomationActionNotice

* Extract automations list panel and worktree map logic

Split AutomationsPageSurface into smaller, focused modules for better maintainability and reusability. Move list panel UI rendering to AutomationsPageListPanel component and worktree map selection logic to a standalone utility function.

* Add i18n strings for automation runs dashboard

Adds localized strings for the automation runs dashboard view, including search, filtering by host and status, run counts for 24h/7d windows, and empty state messaging across all supported languages.

* fix missing translation

* fix missing translation
2026-09-02 13:42:14 -07:00

76 lines
2.7 KiB
TypeScript

import type { AutomationRun, AutomationRunsPage } from './automations-types'
const MAX_PAGE_SIZE = 100
/** A keyset boundary (`createdAt:id` of the previous page's last run), or the
* bare offset older hosts emitted — still read so a cursor issued before an
* upgrade keeps working. */
type AutomationRunCursor =
| { kind: 'key'; createdAt: number; id: string }
| { kind: 'offset'; offset: number }
function decodeAutomationRunCursor(cursor: string | undefined): AutomationRunCursor | null {
if (!cursor) {
return null
}
const separator = cursor.indexOf(':')
if (separator === -1) {
const offset = Number.parseInt(cursor, 10)
return Number.isFinite(offset) && offset > 0 ? { kind: 'offset', offset } : null
}
const createdAt = Number.parseInt(cursor.slice(0, separator), 10)
const id = cursor.slice(separator + 1)
return Number.isFinite(createdAt) && id ? { kind: 'key', createdAt, id } : null
}
/** The single total order pages and cursors agree on. Ties on `createdAt` fall
* back to `id`, so a pruned boundary cannot take the runs tied with it. */
export function compareAutomationRunsNewestFirst(
left: Pick<AutomationRun, 'createdAt' | 'id'>,
right: Pick<AutomationRun, 'createdAt' | 'id'>
): number {
return right.createdAt - left.createdAt || left.id.localeCompare(right.id)
}
function pageStartIndex(
runs: readonly AutomationRun[],
cursor: AutomationRunCursor | null
): number {
if (!cursor) {
return 0
}
if (cursor.kind === 'offset') {
return Math.min(cursor.offset, runs.length)
}
const boundary = runs.findIndex(
(run) => run.id === cursor.id && run.createdAt === cursor.createdAt
)
if (boundary !== -1) {
return boundary + 1
}
// Boundary run pruned between pages: resume at the first run the total order
// places after it, so runs tied on `createdAt` are not dropped with it.
const older = runs.findIndex((run) => compareAutomationRunsNewestFirst(cursor, run) < 0)
return older === -1 ? runs.length : older
}
/**
* Pages `runs`, which must already be sorted by `compareAutomationRunsNewestFirst`.
* The cursor names the previous page's last run rather than an index, so runs
* created between two page requests cannot shift the window and drop a run.
*/
export function paginateAutomationRuns(
runs: readonly AutomationRun[],
limit?: number,
cursor?: string
): AutomationRunsPage {
const start = pageStartIndex(runs, decodeAutomationRunCursor(cursor))
const boundedLimit = Math.min(Math.max(1, limit ?? 100), MAX_PAGE_SIZE)
const page = runs.slice(start, start + boundedLimit)
const last = page.at(-1)
return {
runs: page,
nextCursor: last && start + page.length < runs.length ? `${last.createdAt}:${last.id}` : null
}
}