diff --git a/apps/fabro-web/app/components/runs-list/run-table-row.tsx b/apps/fabro-web/app/components/runs-list/run-table-row.tsx index 52efd1152..d1b328178 100644 --- a/apps/fabro-web/app/components/runs-list/run-table-row.tsx +++ b/apps/fabro-web/app/components/runs-list/run-table-row.tsx @@ -5,6 +5,7 @@ import type { RunWithStatus } from "../../data/runs"; import { formatRelativeTime } from "../../lib/format"; import { InlineMarkdown } from "../inline-markdown"; import { PullRequestChip } from "../pull-request-chip"; +import { SizeChip } from "../size-chip"; import { RowActionsMenu } from "./row-actions-menu"; import { SelectionCheckbox } from "./selection-checkbox"; import type { ToggleableColumn } from "./toggleable-column"; @@ -101,6 +102,11 @@ export function RunTableRow({ {run.elapsed} )} + {show("size") && ( + + {run.size != null && } + + )} {show("changes") && ( {run.additions != null && +{run.additions.toLocaleString()}} diff --git a/apps/fabro-web/app/components/runs-list/runs-list-view.tsx b/apps/fabro-web/app/components/runs-list/runs-list-view.tsx index 84780c578..6437c2cfb 100644 --- a/apps/fabro-web/app/components/runs-list/runs-list-view.tsx +++ b/apps/fabro-web/app/components/runs-list/runs-list-view.tsx @@ -150,6 +150,9 @@ export function RunsListView({ {show("elapsed") && ( )} + {show("size") && ( + + )} {show("changes") && ( )} diff --git a/apps/fabro-web/app/components/runs-list/toggleable-column.ts b/apps/fabro-web/app/components/runs-list/toggleable-column.ts index 8907f306e..3fcc3eb65 100644 --- a/apps/fabro-web/app/components/runs-list/toggleable-column.ts +++ b/apps/fabro-web/app/components/runs-list/toggleable-column.ts @@ -1,9 +1,10 @@ export const TOGGLEABLE_COLUMNS = [ - "elapsed", "repo", "workflow", "created", "updated", + "elapsed", + "size", "changes", "pr", ] as const; @@ -11,11 +12,12 @@ export const TOGGLEABLE_COLUMNS = [ export type ToggleableColumn = (typeof TOGGLEABLE_COLUMNS)[number]; export const toggleableColumnLabels: Record = { - elapsed: "Elapsed", repo: "Repo", workflow: "Workflow", created: "Created", updated: "Updated", + elapsed: "Elapsed", + size: "Size", changes: "Changes", pr: "PR", }; diff --git a/apps/fabro-web/app/components/size-chip.tsx b/apps/fabro-web/app/components/size-chip.tsx new file mode 100644 index 000000000..e3d07ed40 --- /dev/null +++ b/apps/fabro-web/app/components/size-chip.tsx @@ -0,0 +1,33 @@ +import type { RunSize } from "@qltysh/fabro-api-client"; + +import { formatUsdMicros } from "../lib/format"; +import { Tooltip } from "./ui"; + +const SIZE_TONE: Record = { + XS: { className: "bg-overlay text-fg-muted", note: null }, + S: { className: "bg-overlay text-fg-muted", note: null }, + M: { className: "bg-overlay text-fg-muted", note: null }, + L: { className: "bg-amber/15 text-amber", note: "risky" }, + XL: { className: "bg-coral/15 text-coral", note: "unhealthy" }, +}; + +export function SizeChip({ + size, + totalUsdMicros, +}: { + size: RunSize; + totalUsdMicros?: number | null; +}) { + const tone = SIZE_TONE[size]; + const billed = totalUsdMicros != null ? ` · ${formatUsdMicros(totalUsdMicros)} billed` : ""; + const tooltip = tone.note != null + ? `Size ${size} (${tone.note})${billed}` + : `Size ${size}${billed}`; + return ( + + + {size} + + + ); +} diff --git a/apps/fabro-web/app/data/runs.ts b/apps/fabro-web/app/data/runs.ts index 6f587846c..1ddcb14a1 100644 --- a/apps/fabro-web/app/data/runs.ts +++ b/apps/fabro-web/app/data/runs.ts @@ -2,6 +2,7 @@ import { formatDurationMs } from "../lib/format"; import { BoardColumn, type Run, + type RunSize, type RunStatus as ApiRunStatus, } from "@qltysh/fabro-api-client"; @@ -39,6 +40,7 @@ export interface RunItem { sourceDirectory?: string; createdAt?: string; lastEventAt?: string; + size?: RunSize; } export const columnStatuses = [ @@ -109,6 +111,7 @@ export function mapRunListItem(item: Run): RunItem { lastEventAt: item.timestamps.last_event_at ?? undefined, additions: item.diff?.additions, deletions: item.diff?.deletions, + size: item.size, }; } diff --git a/apps/fabro-web/app/routes/run-detail.tsx b/apps/fabro-web/app/routes/run-detail.tsx index 06ebbab7c..6deedbe6e 100644 --- a/apps/fabro-web/app/routes/run-detail.tsx +++ b/apps/fabro-web/app/routes/run-detail.tsx @@ -36,6 +36,7 @@ import { import { EditableRunTitle } from "../components/editable-run-title"; import { GitPullRequestIcon } from "../components/icons"; import { InterviewDock } from "../components/interview-dock"; +import { SizeChip } from "../components/size-chip"; import { SteerBar, type SteerBarHandle } from "../components/steer-bar"; import { ErrorState } from "../components/state"; import { useToast } from "../components/toast"; @@ -82,7 +83,6 @@ import { formatAbsoluteTs, formatDurationMs, formatRelativeTime, - formatUsdMicros, } from "../lib/format"; import { queryKeys } from "../lib/query-keys"; import { useRunEvents } from "../lib/run-events"; @@ -561,16 +561,8 @@ export default function RunDetail({ params }: { params: { id: string } }) { {run.workflow} ); - const totalUsdMicros = summary.billing?.total_usd_micros; - const sizeTooltip = totalUsdMicros != null - ? `Size ${summary.size} · ${formatUsdMicros(totalUsdMicros)} billed` - : `Size ${summary.size}`; const sizeChip = ( - - - {summary.size} - - + ); const visibility = lifecycleActionVisibility(run.lifecycleStatus); diff --git a/apps/fabro-web/app/routes/runs.test.tsx b/apps/fabro-web/app/routes/runs.test.tsx index 0cea3dca4..a77f36379 100644 --- a/apps/fabro-web/app/routes/runs.test.tsx +++ b/apps/fabro-web/app/routes/runs.test.tsx @@ -249,7 +249,7 @@ describe("runs route workspace preferences", () => { }), ); - expect(loadStoredRunsWorkspaceSearchParams(storage).toString()).toBe("hide=elapsed%2Crepo"); + expect(loadStoredRunsWorkspaceSearchParams(storage).toString()).toBe("hide=repo%2Celapsed"); }); test("valid stored preferences produce canonical URL params", () => { diff --git a/docs/public/api-reference/fabro-api.yaml b/docs/public/api-reference/fabro-api.yaml index 63ac4fc8c..0e6075427 100644 --- a/docs/public/api-reference/fabro-api.yaml +++ b/docs/public/api-reference/fabro-api.yaml @@ -4763,7 +4763,7 @@ components: description: Field to sort by. Defaults to `created_at`. schema: type: string - enum: [created_at, updated_at, status, elapsed, repo, title, workflow, changes] + enum: [created_at, updated_at, status, elapsed, repo, title, workflow, changes, size] default: created_at example: created_at diff --git a/lib/crates/fabro-server/src/server/handler/runs.rs b/lib/crates/fabro-server/src/server/handler/runs.rs index c5e892379..090d1ba5d 100644 --- a/lib/crates/fabro-server/src/server/handler/runs.rs +++ b/lib/crates/fabro-server/src/server/handler/runs.rs @@ -97,6 +97,7 @@ enum RunsSortKey { Title, Workflow, Changes, + Size, } #[derive(Debug, Clone, Copy, Default, serde::Deserialize)] @@ -194,6 +195,7 @@ fn sort_runs(runs: &mut [fabro_types::Run], key: RunsSortKey, direction: RunsSor RunsSortKey::Title => run_title_key(a).cmp(&run_title_key(b)), RunsSortKey::Workflow => run_workflow_key(a).cmp(&run_workflow_key(b)), RunsSortKey::Changes => run_changes_total(a).cmp(&run_changes_total(b)), + RunsSortKey::Size => a.size.cmp(&b.size), }; let primary = if asc { primary } else { primary.reverse() }; // Stable tiebreak: newer ULIDs (and thus newer runs) first. diff --git a/lib/crates/fabro-types/src/run_summary.rs b/lib/crates/fabro-types/src/run_summary.rs index fb5e7f576..1cde31614 100644 --- a/lib/crates/fabro-types/src/run_summary.rs +++ b/lib/crates/fabro-types/src/run_summary.rs @@ -211,6 +211,8 @@ pub struct RunBillingSummary { Default, PartialEq, Eq, + PartialOrd, + Ord, Hash, Serialize, Deserialize, diff --git a/lib/packages/fabro-api-client/src/api/runs-api.ts b/lib/packages/fabro-api-client/src/api/runs-api.ts index 9a074ce61..0977f14f1 100644 --- a/lib/packages/fabro-api-client/src/api/runs-api.ts +++ b/lib/packages/fabro-api-client/src/api/runs-api.ts @@ -1120,7 +1120,7 @@ export const RunsApiAxiosParamCreator = function (configuration?: Configuration) }; }, /** - * Creates a fresh run from the failed or dead source run\'s captured durable definition, records `retried_from` on the new run, and schedules it for execution. The source run is left unchanged. Cancelled, active, succeeded, and archived runs are not retryable. + * Creates a fresh run from the failed or dead source run\'s captured durable definition, records `retried_from` on the new run, and schedules it for execution. The source run is left unchanged. Active, succeeded, and archived runs are not retryable. * @summary Retry Run * @param {string} id Unique run identifier (ULID). * @param {*} [options] Override http request option. @@ -1868,7 +1868,7 @@ export const RunsApiFp = function(configuration?: Configuration) { return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath); }, /** - * Creates a fresh run from the failed or dead source run\'s captured durable definition, records `retried_from` on the new run, and schedules it for execution. The source run is left unchanged. Cancelled, active, succeeded, and archived runs are not retryable. + * Creates a fresh run from the failed or dead source run\'s captured durable definition, records `retried_from` on the new run, and schedules it for execution. The source run is left unchanged. Active, succeeded, and archived runs are not retryable. * @summary Retry Run * @param {string} id Unique run identifier (ULID). * @param {*} [options] Override http request option. @@ -2264,7 +2264,7 @@ export const RunsApiFactory = function (configuration?: Configuration, basePath? return localVarFp.retrieveRunGraphSource(id, options).then((request) => request(axios, basePath)); }, /** - * Creates a fresh run from the failed or dead source run\'s captured durable definition, records `retried_from` on the new run, and schedules it for execution. The source run is left unchanged. Cancelled, active, succeeded, and archived runs are not retryable. + * Creates a fresh run from the failed or dead source run\'s captured durable definition, records `retried_from` on the new run, and schedules it for execution. The source run is left unchanged. Active, succeeded, and archived runs are not retryable. * @summary Retry Run * @param {string} id Unique run identifier (ULID). * @param {*} [options] Override http request option. @@ -2652,7 +2652,7 @@ export class RunsApi extends BaseAPI { } /** - * Creates a fresh run from the failed or dead source run\'s captured durable definition, records `retried_from` on the new run, and schedules it for execution. The source run is left unchanged. Cancelled, active, succeeded, and archived runs are not retryable. + * Creates a fresh run from the failed or dead source run\'s captured durable definition, records `retried_from` on the new run, and schedules it for execution. The source run is left unchanged. Active, succeeded, and archived runs are not retryable. * @summary Retry Run * @param {string} id Unique run identifier (ULID). * @param {*} [options] Override http request option. @@ -2773,7 +2773,8 @@ export const ListRunsSortEnum = { REPO: 'repo', TITLE: 'title', WORKFLOW: 'workflow', - CHANGES: 'changes' + CHANGES: 'changes', + SIZE: 'size' } as const; export type ListRunsSortEnum = typeof ListRunsSortEnum[keyof typeof ListRunsSortEnum]; export const ListRunsDirectionEnum = {