fix(web): align topbar metrics heights (#1694)

This commit is contained in:
Carlos Florêncio
2026-07-14 23:13:56 +01:00
committed by GitHub
parent 32b390c617
commit 0cb8f38464
6 changed files with 115 additions and 9 deletions
@@ -68,7 +68,7 @@ export function KanbanHeaderMobile({
actionsClassName="gap-2"
actions={
<>
<TopbarMetrics />
<TopbarMetrics size="lg" />
{onSearchChange && (
<Button
variant={isSearchOpen ? "secondary" : "outline"}
+2 -2
View File
@@ -173,7 +173,7 @@ function TabletHeader({
className="hidden md:flex w-48 lg:w-56 [&_input]:h-8"
/>
)}
<TopbarMetrics />
<TopbarMetrics size="lg" />
<TooltipProvider>
<ViewToggleGroup toggleValue={toggleValue} onValueChange={handleViewChange} size="lg" />
</TooltipProvider>
@@ -249,7 +249,7 @@ function DesktopHeader({
actions={
<>
{actionsSearch}
<TopbarMetrics />
<TopbarMetrics size="lg" />
<TooltipProvider>
<ViewToggleGroup toggleValue={toggleValue} onValueChange={handleViewChange} size="lg" />
</TooltipProvider>
@@ -19,9 +19,10 @@ import type { SystemMetricSample, SystemMetricsSource } from "@/lib/types/system
type TopbarMetricsProps = {
activeSessionId?: string | null;
size?: "sm" | "lg";
};
export function TopbarMetrics({ activeSessionId }: TopbarMetricsProps) {
export function TopbarMetrics({ activeSessionId, size = "lg" }: TopbarMetricsProps) {
const enabled = useAppStore((s) => s.userSettings.systemMetricsDisplay.showInTopbar);
const snapshot = useAppStore((s) => s.system.metrics);
const { usesDesktopWorkbench } = useResponsiveBreakpoint();
@@ -32,7 +33,10 @@ export function TopbarMetrics({ activeSessionId }: TopbarMetricsProps) {
const sources = selectSources(snapshot?.sources ?? [], activeSessionId);
if (sources.length === 0) {
return (
<div className="flex h-8 items-center gap-1 rounded px-2 text-xs text-muted-foreground md:border md:border-border">
<div
className={`flex ${metricsHeight(size)} items-center gap-1 rounded px-2 text-xs text-muted-foreground md:border md:border-border`}
data-testid="topbar-metrics"
>
<IconActivity className="h-3.5 w-3.5" />
<span className="hidden sm:inline">Metrics</span>
</div>
@@ -43,17 +47,22 @@ export function TopbarMetrics({ activeSessionId }: TopbarMetricsProps) {
<CompactSourceMetrics
source={selectCompactSource(sources, activeSessionId)}
updatedAt={snapshot?.timestamp}
size={size}
/>
);
}
return (
<div className="hidden md:flex max-w-[42vw] items-center gap-1 overflow-hidden">
<div
className="hidden max-w-[42vw] items-center gap-1 overflow-hidden md:flex"
data-testid="topbar-metrics"
>
{sources.map((source) => (
<SourceMetrics
key={source.id}
source={source}
updatedAt={snapshot?.timestamp}
showSource={sources.length > 1}
size={size}
/>
))}
</div>
@@ -75,15 +84,19 @@ function SourceMetrics({
source,
updatedAt,
showSource,
size,
}: {
source: SystemMetricsSource;
updatedAt?: string;
showSource: boolean;
size: NonNullable<TopbarMetricsProps["size"]>;
}) {
const metrics = source.metrics.slice(0, 4);
return (
<div className="flex h-8 max-w-[220px] items-center gap-1 overflow-hidden rounded border border-border px-1.5 text-xs">
<div
className={`flex ${metricsHeight(size)} max-w-[220px] items-center gap-1 overflow-hidden rounded border border-border px-1.5 text-xs`}
>
{showSource ? <SourceBadge source={source} updatedAt={updatedAt} /> : null}
{metrics.length > 0 ? (
metrics.map((metric) => (
@@ -99,15 +112,17 @@ function SourceMetrics({
function CompactSourceMetrics({
source,
updatedAt,
size,
}: {
source: SystemMetricsSource;
updatedAt?: string;
size: NonNullable<TopbarMetricsProps["size"]>;
}) {
const metrics = source.metrics.slice(0, 2);
return (
<div
className="flex h-9 max-w-[34vw] items-center gap-1 overflow-hidden rounded px-1.5 text-xs"
className={`flex ${metricsHeight(size)} max-w-[34vw] items-center gap-1 overflow-hidden rounded px-1.5 text-xs`}
data-testid="mobile-topbar-metrics"
>
<SourceBadge source={source} updatedAt={updatedAt} />
@@ -122,6 +137,10 @@ function CompactSourceMetrics({
);
}
function metricsHeight(size: NonNullable<TopbarMetricsProps["size"]>) {
return size === "sm" ? "h-7" : "h-8";
}
function SourceBadge({ source, updatedAt }: { source: SystemMetricsSource; updatedAt?: string }) {
const isHost = source.kind === "backend";
const label = isHost ? "Host" : "Executor";
+1 -1
View File
@@ -365,7 +365,7 @@ function TopBarRight({
}) {
return (
<div className="flex items-center justify-self-end gap-2 [&_button]:whitespace-nowrap">
<TopbarMetrics activeSessionId={activeSessionId} />
<TopbarMetrics activeSessionId={activeSessionId} size="sm" />
{isArchived && (
<TopbarCluster label="Unarchive task" className="[&_button]:h-7 [&_button]:text-xs">
<TaskUnarchiveButton taskId={taskId} />
@@ -3,6 +3,29 @@ import { MobileKanbanPage } from "../../pages/mobile-kanban-page";
import { missingGitHealth } from "./health-fixtures";
test.describe("Mobile kanban view", () => {
test.afterEach(async ({ apiClient }) => {
await apiClient.rawRequest("PATCH", "/api/v1/user/settings", {
system_metrics_display: { show_in_topbar: false },
});
});
test("metrics match the height of the mobile topbar actions", async ({ testPage, apiClient }) => {
await apiClient.rawRequest("PATCH", "/api/v1/user/settings", {
system_metrics_display: { show_in_topbar: true },
});
const mobile = new MobileKanbanPage(testPage);
await mobile.goto();
const metrics = testPage.getByTestId("mobile-topbar-metrics");
await expect(metrics).toBeVisible();
await expect(mobile.mobileSearchToggle).toBeVisible();
const metricsBox = await metrics.boundingBox();
const actionBox = await mobile.mobileSearchToggle.boundingBox();
if (!metricsBox || !actionBox) throw new Error("topbar action has no bounding box");
expect(metricsBox.height).toBe(actionBox.height);
});
test("renders mobile layout with column tabs and swipeable columns", async ({
testPage,
apiClient,
@@ -2,6 +2,7 @@ import { type Page } from "@playwright/test";
import { test, expect } from "../../fixtures/test-base";
import type { SeedData } from "../../fixtures/test-base";
import type { ApiClient } from "../../helpers/api-client";
import { KanbanPage } from "../../pages/kanban-page";
import { SessionPage } from "../../pages/session-page";
/**
@@ -16,9 +17,27 @@ async function bottomOf(page: Page, testId: string): Promise<number> {
return box.y + box.height;
}
async function heightOf(page: Page, testId: string): Promise<number> {
const box = await page.getByTestId(testId).boundingBox();
if (!box) throw new Error(`no bounding box for ${testId}`);
return box.height;
}
async function enableTopbarMetrics(apiClient: ApiClient): Promise<void> {
await apiClient.rawRequest("PATCH", "/api/v1/user/settings", {
system_metrics_display: { show_in_topbar: true },
});
}
test.describe("Sidebar header / top bar alignment", () => {
test.describe.configure({ retries: 1 });
test.afterEach(async ({ apiClient }) => {
await apiClient.rawRequest("PATCH", "/api/v1/user/settings", {
system_metrics_display: { show_in_topbar: false },
});
});
test("task top bar bottom edge aligns with the sidebar header", async ({
testPage,
apiClient,
@@ -65,4 +84,49 @@ test.describe("Sidebar header / top bar alignment", () => {
// Allow 1px for sub-pixel rounding of the shared border line.
expect(Math.abs((headerBottom as number) - topbarBottom)).toBeLessThanOrEqual(1);
});
test("task metrics match the height of the task action controls", async ({
testPage,
apiClient,
seedData,
}) => {
await enableTopbarMetrics(apiClient);
const task = await apiClient.createTaskWithAgent(
seedData.workspaceId,
"Metrics Alignment Task",
seedData.agentProfileId,
{
description: "/e2e:simple-message",
workflow_id: seedData.workflowId,
workflow_step_id: seedData.startStepId,
repository_ids: [seedData.repositoryId],
},
);
if (!task.session_id) throw new Error("createTaskWithAgent did not return a session_id");
await testPage.goto(`/t/${task.id}`);
const session = new SessionPage(testPage);
await session.waitForLoad();
await expect(testPage.getByTestId("topbar-metrics")).toBeVisible();
await expect(testPage.getByTestId("layout-preset-trigger")).toBeVisible();
expect(await heightOf(testPage, "topbar-metrics")).toBe(
await heightOf(testPage, "layout-preset-trigger"),
);
});
test("Kanban metrics match the height of the Kanban action controls", async ({
testPage,
apiClient,
}) => {
await enableTopbarMetrics(apiClient);
const kanban = new KanbanPage(testPage);
await kanban.goto();
await expect(testPage.getByTestId("topbar-metrics")).toBeVisible();
await expect(testPage.getByTestId("view-toggle-kanban")).toBeVisible();
expect(await heightOf(testPage, "topbar-metrics")).toBe(
await heightOf(testPage, "view-toggle-kanban"),
);
});
});