mirror of
https://github.com/fabro-sh/fabro.git
synced 2026-10-09 03:20:56 +00:00
feat(web): add "Created by" avatar column to runs list view
Visible by default to the right of Status; toggleable via the column picker. Extracts the principal avatar/label helper out of the run summary panel so both surfaces share one renderer. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
7c69807f1d
commit
71c06c1bc4
6 changed files with 100 additions and 74 deletions
|
|
@ -1,14 +1,5 @@
|
|||
import type { ReactNode } from "react";
|
||||
import {
|
||||
BoltIcon,
|
||||
ChatBubbleLeftEllipsisIcon,
|
||||
Cog6ToothIcon,
|
||||
CpuChipIcon,
|
||||
QuestionMarkCircleIcon,
|
||||
ServerIcon,
|
||||
} from "@heroicons/react/20/solid";
|
||||
import type {
|
||||
Principal,
|
||||
Run,
|
||||
SandboxResources,
|
||||
SandboxState,
|
||||
|
|
@ -20,6 +11,7 @@ import {
|
|||
formatCpuCores,
|
||||
formatUsdMicros,
|
||||
} from "../lib/format";
|
||||
import { principalDisplay } from "../lib/principal-display";
|
||||
import { useRun, useRunArtifacts, useRunSandboxDetails } from "../lib/queries";
|
||||
import { SANDBOX_STATE_DISPLAY } from "../lib/sandbox-state";
|
||||
import { Tooltip } from "./ui";
|
||||
|
|
@ -53,62 +45,6 @@ function Cell({ label, children }: { label: string; children: ReactNode }) {
|
|||
);
|
||||
}
|
||||
|
||||
interface CreatedByDisplay {
|
||||
glyph: ReactNode;
|
||||
label: string;
|
||||
}
|
||||
|
||||
function principalGlyph(icon: ReactNode) {
|
||||
return (
|
||||
<span className="grid size-5 place-items-center rounded-full bg-teal-500/20 text-teal-500">
|
||||
{icon}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
function createdByDisplay(actor: Principal): CreatedByDisplay {
|
||||
switch (actor.kind) {
|
||||
case "user": {
|
||||
let glyph: ReactNode;
|
||||
if (actor.avatar_url) {
|
||||
glyph = (
|
||||
<img
|
||||
alt=""
|
||||
src={actor.avatar_url}
|
||||
className="size-5 rounded-full outline -outline-offset-1 outline-line-strong"
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
const initial = actor.login.charAt(0).toUpperCase() || "?";
|
||||
glyph = (
|
||||
<span className="grid size-5 place-items-center rounded-full bg-teal-500/20 font-mono text-[10px] font-medium text-teal-500">
|
||||
{initial}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
return { glyph, label: actor.login };
|
||||
}
|
||||
case "agent":
|
||||
return { glyph: principalGlyph(<CpuChipIcon className="size-3" />), label: "agent" };
|
||||
case "system":
|
||||
return { glyph: principalGlyph(<Cog6ToothIcon className="size-3" />), label: "system" };
|
||||
case "slack":
|
||||
return {
|
||||
glyph: principalGlyph(<ChatBubbleLeftEllipsisIcon className="size-3" />),
|
||||
label: "slack",
|
||||
};
|
||||
case "webhook":
|
||||
return { glyph: principalGlyph(<BoltIcon className="size-3" />), label: "webhook" };
|
||||
case "worker":
|
||||
return { glyph: principalGlyph(<ServerIcon className="size-3" />), label: "worker" };
|
||||
case "anonymous":
|
||||
return {
|
||||
glyph: principalGlyph(<QuestionMarkCircleIcon className="size-3" />),
|
||||
label: "anonymous",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export interface RunSummaryPanelViewProps {
|
||||
run: Run | null;
|
||||
runLoading: boolean;
|
||||
|
|
@ -156,7 +92,7 @@ export function RunSummaryPanelView({
|
|||
artifactsCount,
|
||||
artifactsLoading,
|
||||
}: RunSummaryPanelViewProps) {
|
||||
const created = run?.created_by ? createdByDisplay(run.created_by) : null;
|
||||
const created = run?.created_by ? principalDisplay(run.created_by) : null;
|
||||
const diff = run?.diff ?? null;
|
||||
const cost = formatUsdMicros(run?.billing?.total_usd_micros);
|
||||
|
||||
|
|
|
|||
|
|
@ -3,9 +3,11 @@ import { Link } from "react-router";
|
|||
import { ciConfig, columnStatusDisplay, deriveCiStatus } from "../../data/runs";
|
||||
import type { RunWithStatus } from "../../data/runs";
|
||||
import { formatRelativeTime } from "../../lib/format";
|
||||
import { principalDisplay } from "../../lib/principal-display";
|
||||
import { InlineMarkdown } from "../inline-markdown";
|
||||
import { PullRequestChip } from "../pull-request-chip";
|
||||
import { SizeChip } from "../size-chip";
|
||||
import { Tooltip } from "../ui";
|
||||
import { RowActionsMenu } from "./row-actions-menu";
|
||||
import { SelectionCheckbox } from "./selection-checkbox";
|
||||
import type { ToggleableColumn } from "./toggleable-column";
|
||||
|
|
@ -50,6 +52,18 @@ export function RunTableRow({
|
|||
<span className={`font-mono text-xs ${statusDisplay.text}`}>{run.statusLabel}</span>
|
||||
</span>
|
||||
</td>
|
||||
{show("created_by") && (
|
||||
<td className="relative z-10 w-8 whitespace-nowrap px-3 py-2.5">
|
||||
{run.createdBy && (() => {
|
||||
const display = principalDisplay(run.createdBy);
|
||||
return (
|
||||
<Tooltip label={display.label}>
|
||||
<span aria-label={`Created by ${display.label}`}>{display.glyph}</span>
|
||||
</Tooltip>
|
||||
);
|
||||
})()}
|
||||
</td>
|
||||
)}
|
||||
{show("repo") && (
|
||||
<td className="whitespace-nowrap px-3 py-2.5 font-mono text-xs font-medium text-teal-500">
|
||||
{run.repo}
|
||||
|
|
|
|||
|
|
@ -152,6 +152,11 @@ export function RunsListView({
|
|||
/>
|
||||
</th>
|
||||
<SortHeader label="Status" sortKey="status" activeSort={sort} direction={direction} onClick={onSortClick} />
|
||||
{show("created_by") && (
|
||||
<th scope="col" className="w-8 whitespace-nowrap px-3 py-2.5 font-medium">
|
||||
By
|
||||
</th>
|
||||
)}
|
||||
{show("repo") && (
|
||||
<SortHeader label="Repo" sortKey="repo" activeSort={sort} direction={direction} onClick={onSortClick} />
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
export const TOGGLEABLE_COLUMNS = [
|
||||
"created_by",
|
||||
"repo",
|
||||
"workflow",
|
||||
"created",
|
||||
|
|
@ -14,14 +15,15 @@ const TOGGLEABLE_COLUMN_SET = new Set<string>(TOGGLEABLE_COLUMNS);
|
|||
export type ToggleableColumn = (typeof TOGGLEABLE_COLUMNS)[number];
|
||||
|
||||
export const toggleableColumnLabels: Record<ToggleableColumn, string> = {
|
||||
repo: "Repo",
|
||||
workflow: "Workflow",
|
||||
created: "Created",
|
||||
updated: "Updated",
|
||||
elapsed: "Elapsed",
|
||||
size: "Size",
|
||||
changes: "Changes",
|
||||
pr: "PR",
|
||||
repo: "Repo",
|
||||
workflow: "Workflow",
|
||||
created_by: "Created by",
|
||||
created: "Created",
|
||||
updated: "Updated",
|
||||
elapsed: "Elapsed",
|
||||
size: "Size",
|
||||
changes: "Changes",
|
||||
pr: "PR",
|
||||
};
|
||||
|
||||
export function parseHiddenColumns(raw: string | null): Set<ToggleableColumn> {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import { formatDurationMs } from "../lib/format";
|
||||
import {
|
||||
BoardColumn,
|
||||
type Principal,
|
||||
type Run,
|
||||
type RunSize,
|
||||
type RunStatus as ApiRunStatus,
|
||||
|
|
@ -39,6 +40,7 @@ export interface RunItem {
|
|||
sandboxWorkingDirectory?: string;
|
||||
sourceDirectory?: string;
|
||||
createdAt?: string;
|
||||
createdBy?: Principal | null;
|
||||
lastEventAt?: string;
|
||||
size?: RunSize;
|
||||
}
|
||||
|
|
@ -108,6 +110,7 @@ export function mapRunListItem(item: Run): RunItem {
|
|||
sandboxWorkingDirectory: runtime?.working_directory ?? undefined,
|
||||
sourceDirectory: item.source_directory ?? undefined,
|
||||
createdAt: item.timestamps.created_at,
|
||||
createdBy: item.created_by,
|
||||
lastEventAt: item.timestamps.last_event_at ?? undefined,
|
||||
additions: item.diff?.additions,
|
||||
deletions: item.diff?.deletions,
|
||||
|
|
|
|||
66
apps/fabro-web/app/lib/principal-display.tsx
Normal file
66
apps/fabro-web/app/lib/principal-display.tsx
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
import type { ReactNode } from "react";
|
||||
import {
|
||||
BoltIcon,
|
||||
ChatBubbleLeftEllipsisIcon,
|
||||
Cog6ToothIcon,
|
||||
CpuChipIcon,
|
||||
QuestionMarkCircleIcon,
|
||||
ServerIcon,
|
||||
} from "@heroicons/react/20/solid";
|
||||
import type { Principal } from "@qltysh/fabro-api-client";
|
||||
|
||||
export interface PrincipalDisplay {
|
||||
glyph: ReactNode;
|
||||
label: string;
|
||||
}
|
||||
|
||||
function principalIconGlyph(icon: ReactNode) {
|
||||
return (
|
||||
<span className="grid size-5 place-items-center rounded-full bg-teal-500/20 text-teal-500">
|
||||
{icon}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
export function principalDisplay(actor: Principal): PrincipalDisplay {
|
||||
switch (actor.kind) {
|
||||
case "user": {
|
||||
let glyph: ReactNode;
|
||||
if (actor.avatar_url) {
|
||||
glyph = (
|
||||
<img
|
||||
alt=""
|
||||
src={actor.avatar_url}
|
||||
className="size-5 rounded-full outline -outline-offset-1 outline-line-strong"
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
const initial = actor.login.charAt(0).toUpperCase() || "?";
|
||||
glyph = (
|
||||
<span className="grid size-5 place-items-center rounded-full bg-teal-500/20 font-mono text-[10px] font-medium text-teal-500">
|
||||
{initial}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
return { glyph, label: actor.login };
|
||||
}
|
||||
case "agent":
|
||||
return { glyph: principalIconGlyph(<CpuChipIcon className="size-3" />), label: "agent" };
|
||||
case "system":
|
||||
return { glyph: principalIconGlyph(<Cog6ToothIcon className="size-3" />), label: "system" };
|
||||
case "slack":
|
||||
return {
|
||||
glyph: principalIconGlyph(<ChatBubbleLeftEllipsisIcon className="size-3" />),
|
||||
label: "slack",
|
||||
};
|
||||
case "webhook":
|
||||
return { glyph: principalIconGlyph(<BoltIcon className="size-3" />), label: "webhook" };
|
||||
case "worker":
|
||||
return { glyph: principalIconGlyph(<ServerIcon className="size-3" />), label: "worker" };
|
||||
case "anonymous":
|
||||
return {
|
||||
glyph: principalIconGlyph(<QuestionMarkCircleIcon className="size-3" />),
|
||||
label: "anonymous",
|
||||
};
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue