diff --git a/SW.Bitween.Web/ClientApp/e2e/dashboard.spec.ts b/SW.Bitween.Web/ClientApp/e2e/dashboard.spec.ts index 55696b85..375789b9 100644 --- a/SW.Bitween.Web/ClientApp/e2e/dashboard.spec.ts +++ b/SW.Bitween.Web/ClientApp/e2e/dashboard.spec.ts @@ -41,3 +41,41 @@ test("dashboard loads with real aggregated data", async ({ page }) => { await expect(page).toHaveURL(/\/exchanges\?ids=/); } }); + +test("subscription health pages its rows instead of growing without bound", async ({ page }) => { + // Fourteen unhealthy subscriptions, built from a real row so the rest of the page still resolves: + // eleven failing, then three paused. + await page.route("**/api/subscriptions", async (route) => { + const res = await route.fetch(); + const body = await res.json(); + const template = body.result[0]; + body.result = Array.from({ length: 14 }, (_, i) => ({ + ...template, + id: 900000 + i, + name: `Health page ${i + 1}`, + consecutiveFailures: i < 11 ? i + 1 : 0, + pausedOn: i < 11 ? null : new Date().toISOString(), + })); + body.totalCount = body.result.length; + await route.fulfill({ response: res, json: body }); + }); + + await page.goto("login"); + await page.fill("#login-email", ADMIN_EMAIL); + await page.fill("#login-password", ADMIN_PASSWORD); + await page.getByRole("button", { name: "Sign in" }).click(); + await page.waitForURL((url) => !url.pathname.endsWith("/login"), { timeout: 15000 }); + + await page.goto("dashboard"); + const panel = page.locator("section").filter({ has: page.getByRole("heading", { name: "Subscription health" }) }); + await expect(panel.getByText("1–10 of 14")).toBeVisible({ timeout: 15000 }); + await expect(panel.getByRole("listitem")).toHaveCount(10); + await expect(panel.getByText("Health page 1", { exact: true })).toBeVisible(); + + await panel.getByRole("button", { name: "Next →" }).click(); + await expect(panel.getByText("11–14 of 14")).toBeVisible(); + await expect(panel.getByRole("listitem")).toHaveCount(4); + await expect(panel.getByText("Health page 11", { exact: true })).toBeVisible(); + await expect(panel.getByText("Paused")).toHaveCount(3); + await expect(panel.getByRole("button", { name: "Next →" })).toBeDisabled(); +}); diff --git a/SW.Bitween.Web/ClientApp/src/pages/dashboard/DashboardPage.tsx b/SW.Bitween.Web/ClientApp/src/pages/dashboard/DashboardPage.tsx index b87908d4..d4f7ae8d 100644 --- a/SW.Bitween.Web/ClientApp/src/pages/dashboard/DashboardPage.tsx +++ b/SW.Bitween.Web/ClientApp/src/pages/dashboard/DashboardPage.tsx @@ -1,4 +1,4 @@ -import type { ReactNode } from "react"; +import { useState, type ReactNode } from "react"; import { Link } from "react-router"; import { keepPreviousData, useQuery } from "@tanstack/react-query"; import { api } from "../../api"; @@ -11,6 +11,8 @@ import { StatusBadge, XchangeId } from "../exchanges/shared"; import { keys } from "../../api/queryKeys"; const CHART_HEIGHT = 140; +/** Single-line rows, so ten fill about the height of the six two-line "Latest failures" beside them. */ +const HEALTH_PAGE_SIZE = 10; const dayLabel = new Intl.DateTimeFormat("en", { day: "numeric", month: "short" }); @@ -57,6 +59,7 @@ function StatTile({ */ export function DashboardPage() { const rabbitMqConfigured = useRabbitMqManagementConfigured(); + const [healthOffset, setHealthOffset] = useState(0); const { data, isLoading, isError } = useQuery({ queryKey: keys.dashboard, queryFn: () => api.getDashboard(), @@ -81,8 +84,14 @@ export function DashboardPage() { : "live consumer health"; const delta = data.today.total - data.yesterdayTotal; const maxDay = Math.max(1, ...data.trafficByDay.map((d) => d.success + d.failed)); - const needsAttention = - data.attention.failingSubscriptions.length + data.attention.pausedSubscriptions.length; + const unhealthy = [ + ...data.attention.failingSubscriptions.map((s) => ({ ...s, paused: false })), + ...data.attention.pausedSubscriptions.map((s) => ({ ...s, consecutiveFailures: 0, paused: true })), + ]; + // The list can shrink on a refetch; don't leave the page pointing past its end. Reset the stored + // offset too, or a later refetch that grows the list again jumps back to the old page. + const healthStart = healthOffset < unhealthy.length ? healthOffset : 0; + if (healthStart !== healthOffset) setHealthOffset(0); return (
@@ -338,35 +347,56 @@ export function DashboardPage() { {/* — subscription health — */} - {needsAttention === 0 ? ( + {unhealthy.length === 0 ? ( No failures piling up, nothing paused. ) : ( - + <> + + {unhealthy.length > HEALTH_PAGE_SIZE && ( +
+ + {healthStart + 1}–{Math.min(healthStart + HEALTH_PAGE_SIZE, unhealthy.length)} of{" "} + {unhealthy.length} + + + + + +
+ )} + )}