diff --git a/frontend/src/components/dashboard/ActivityHistory.test.tsx b/frontend/src/components/dashboard/ActivityHistory.test.tsx index 1704199f..6abda6ca 100644 --- a/frontend/src/components/dashboard/ActivityHistory.test.tsx +++ b/frontend/src/components/dashboard/ActivityHistory.test.tsx @@ -141,3 +141,71 @@ describe("ActivityHistory malformed metadata", () => { ).toBeInTheDocument(); }); }); + +describe("ActivityHistory event type filter pills", () => { + const sampleEvents = [ + makeEvent({ id: "evt-created", streamId: 10, eventType: "CREATED" }), + makeEvent({ + id: "evt-withdrawn", + streamId: 11, + eventType: "WITHDRAWN", + amount: "250000000", + }), + makeEvent({ + id: "evt-topped", + streamId: 12, + eventType: "TOPPED_UP", + amount: "500000000", + }), + makeEvent({ id: "evt-paused", streamId: 13, eventType: "PAUSED" }), + ]; + + it("selects 'All' by default and displays all activity items", () => { + render(); + + const allButton = screen.getByRole("button", { name: /^All$/i }); + expect(allButton).toHaveAttribute("aria-pressed", "true"); + expect(screen.getAllByRole("listitem")).toHaveLength(4); + }); + + it("filters displayed items immediately when clicking Created, Withdrawn, Topped Up, or Paused", () => { + render(); + + fireEvent.click(screen.getByRole("button", { name: /^Withdrawn$/i })); + expect(screen.getByRole("button", { name: /^Withdrawn$/i })).toHaveAttribute( + "aria-pressed", + "true", + ); + expect(screen.getByRole("button", { name: /^All$/i })).toHaveAttribute( + "aria-pressed", + "false", + ); + expect(screen.getAllByRole("listitem")).toHaveLength(1); + expect(screen.getByText(/Withdrew 25 tokens from Stream/i)).toBeInTheDocument(); + + fireEvent.click(screen.getByRole("button", { name: /^Topped Up$/i })); + expect(screen.getAllByRole("listitem")).toHaveLength(1); + expect(screen.getByText(/Topped up Stream/i)).toBeInTheDocument(); + + fireEvent.click(screen.getByRole("button", { name: /^Paused$/i })); + expect(screen.getAllByRole("listitem")).toHaveLength(1); + expect(screen.getByText(/was paused/i)).toBeInTheDocument(); + + fireEvent.click(screen.getByRole("button", { name: /^Created$/i })); + expect(screen.getAllByRole("listitem")).toHaveLength(1); + expect(screen.getByText(/New stream created/i)).toBeInTheDocument(); + }); + + it("shows empty state message when no events match the selected filter", () => { + render(); + + fireEvent.click(screen.getByRole("button", { name: /^Cancelled$/i })); + expect(screen.queryAllByRole("listitem")).toHaveLength(0); + expect( + screen.getByText("No activity found for this filter."), + ).toBeInTheDocument(); + + fireEvent.click(screen.getByRole("button", { name: /^All$/i })); + expect(screen.getAllByRole("listitem")).toHaveLength(4); + }); +}); diff --git a/frontend/src/components/dashboard/ActivityHistory.tsx b/frontend/src/components/dashboard/ActivityHistory.tsx index 573163bb..021730ba 100644 --- a/frontend/src/components/dashboard/ActivityHistory.tsx +++ b/frontend/src/components/dashboard/ActivityHistory.tsx @@ -1,6 +1,6 @@ "use client"; -import React, { useRef, useState, useCallback } from "react"; +import React, { useRef, useState, useCallback, useMemo } from "react"; import Link from "next/link"; import { useVirtualizer } from "@tanstack/react-virtual"; import { BackendStreamEvent } from "@/lib/api-types"; @@ -13,6 +13,26 @@ import { Button } from "../ui/Button"; const VIRTUALIZATION_THRESHOLD = 50; +export type ActivityEventFilter = + | "ALL" + | "CREATED" + | "WITHDRAWN" + | "TOPPED_UP" + | "PAUSED" + | "CANCELLED"; + +export const ACTIVITY_FILTER_OPTIONS: ReadonlyArray<{ + id: ActivityEventFilter; + label: string; +}> = [ + { id: "ALL", label: "All" }, + { id: "CREATED", label: "Created" }, + { id: "WITHDRAWN", label: "Withdrawn" }, + { id: "TOPPED_UP", label: "Topped Up" }, + { id: "PAUSED", label: "Paused" }, + { id: "CANCELLED", label: "Cancelled" }, +]; + /** * Safely parse a StreamEvent metadata JSON string. Malformed metadata * (truncated/partial writes, migration artifacts) must never throw during @@ -38,10 +58,20 @@ export const ActivityHistory: React.FC = ({ isLoading, }) => { const parentRef = useRef(null); - const shouldVirtualize = events.length > VIRTUALIZATION_THRESHOLD; + const [selectedFilter, setSelectedFilter] = useState("ALL"); + + const filteredEvents = useMemo( + () => + selectedFilter === "ALL" + ? events + : events.filter((event) => event.eventType === selectedFilter), + [events, selectedFilter], + ); + + const shouldVirtualize = filteredEvents.length > VIRTUALIZATION_THRESHOLD; const virtualizer = useVirtualizer({ - count: events.length, + count: filteredEvents.length, getScrollElement: () => parentRef.current, estimateSize: () => 120, overscan: 5, @@ -157,7 +187,32 @@ export const ActivityHistory: React.FC = ({ {exportStatus === "complete" && "Export complete."} -
+
+
+ {ACTIVITY_FILTER_OPTIONS.map((option) => { + const isSelected = selectedFilter === option.id; + return ( + + ); + })} +
+