fix(web): align topbar metrics heights (#1694)
This commit is contained in:
@@ -68,7 +68,7 @@ export function KanbanHeaderMobile({
|
||||
actionsClassName="gap-2"
|
||||
actions={
|
||||
<>
|
||||
<TopbarMetrics />
|
||||
<TopbarMetrics size="lg" />
|
||||
{onSearchChange && (
|
||||
<Button
|
||||
variant={isSearchOpen ? "secondary" : "outline"}
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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"),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user