mirror of
https://github.com/fabro-sh/fabro.git
synced 2026-08-28 05:27:41 +00:00
feat(web): add sortable Size column to runs list
Surfaces the run t-shirt size (XS/S/M/L/XL) in both the main runs list and the Children sub-tab, visible by default. L renders in amber and XL in coral to flag risky and unhealthy runs at a glance. Extracts a shared SizeChip component used by the run header and the table cell, derives Ord on RunSize so the new sort key (server-side ListRuns sort) orders by bucket, and reorders TOGGLEABLE_COLUMNS so the column picker mirrors the visible table order. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
d69198bc30
commit
acf8caa351
11 changed files with 63 additions and 19 deletions
|
|
@ -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}
|
||||
</td>
|
||||
)}
|
||||
{show("size") && (
|
||||
<td className="whitespace-nowrap px-3 py-2.5 text-right">
|
||||
{run.size != null && <SizeChip size={run.size} />}
|
||||
</td>
|
||||
)}
|
||||
{show("changes") && (
|
||||
<td className="whitespace-nowrap px-3 py-2.5 text-right font-mono text-xs tabular-nums">
|
||||
{run.additions != null && <span className="text-mint">+{run.additions.toLocaleString()}</span>}
|
||||
|
|
|
|||
|
|
@ -150,6 +150,9 @@ export function RunsListView({
|
|||
{show("elapsed") && (
|
||||
<SortHeader label="Elapsed" sortKey="elapsed" activeSort={sort} direction={direction} align="right" onClick={onSortClick} />
|
||||
)}
|
||||
{show("size") && (
|
||||
<SortHeader label="Size" sortKey="size" activeSort={sort} direction={direction} align="right" onClick={onSortClick} />
|
||||
)}
|
||||
{show("changes") && (
|
||||
<SortHeader label="Changes" sortKey="changes" activeSort={sort} direction={direction} align="right" onClick={onSortClick} />
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -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<ToggleableColumn, string> = {
|
||||
elapsed: "Elapsed",
|
||||
repo: "Repo",
|
||||
workflow: "Workflow",
|
||||
created: "Created",
|
||||
updated: "Updated",
|
||||
elapsed: "Elapsed",
|
||||
size: "Size",
|
||||
changes: "Changes",
|
||||
pr: "PR",
|
||||
};
|
||||
|
|
|
|||
33
apps/fabro-web/app/components/size-chip.tsx
Normal file
33
apps/fabro-web/app/components/size-chip.tsx
Normal file
|
|
@ -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<RunSize, { className: string; note: string | null }> = {
|
||||
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 (
|
||||
<Tooltip label={tooltip}>
|
||||
<span className={`rounded px-1.5 py-0.5 font-mono text-xs font-bold tabular-nums ${tone.className}`}>
|
||||
{size}
|
||||
</span>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
</span>
|
||||
);
|
||||
const totalUsdMicros = summary.billing?.total_usd_micros;
|
||||
const sizeTooltip = totalUsdMicros != null
|
||||
? `Size ${summary.size} · ${formatUsdMicros(totalUsdMicros)} billed`
|
||||
: `Size ${summary.size}`;
|
||||
const sizeChip = (
|
||||
<Tooltip label={sizeTooltip}>
|
||||
<span className="rounded bg-overlay px-1.5 py-0.5 font-mono text-xs font-bold text-fg-muted tabular-nums">
|
||||
{summary.size}
|
||||
</span>
|
||||
</Tooltip>
|
||||
<SizeChip size={summary.size} totalUsdMicros={summary.billing?.total_usd_micros} />
|
||||
);
|
||||
|
||||
const visibility = lifecycleActionVisibility(run.lifecycleStatus);
|
||||
|
|
|
|||
|
|
@ -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", () => {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -211,6 +211,8 @@ pub struct RunBillingSummary {
|
|||
Default,
|
||||
PartialEq,
|
||||
Eq,
|
||||
PartialOrd,
|
||||
Ord,
|
||||
Hash,
|
||||
Serialize,
|
||||
Deserialize,
|
||||
|
|
|
|||
11
lib/packages/fabro-api-client/src/api/runs-api.ts
generated
11
lib/packages/fabro-api-client/src/api/runs-api.ts
generated
|
|
@ -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 = {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue