Skip to content

Commit d13695e

Browse files
Merge pull request #339 from ipnet-mesh/feat/page-header-icons
Show section icons in page headers across all pages
2 parents e0c1a67 + 78fab35 commit d13695e

29 files changed

Lines changed: 115 additions & 38 deletions

src/meshcore_hub/web/static/js/spa-react/components/PageHeader.test.tsx

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,23 @@
11
import { render, screen } from "@testing-library/react";
2-
import type { ReactNode } from "react";
2+
import type { ComponentType, ReactNode } from "react";
33
import { describe, expect, it } from "vitest";
44

55
import { AppConfigProvider } from "@/context/AppConfigContext";
66
import { PageHeader } from "@/components/PageHeader";
7+
import { IconNodes } from "@/components/icons";
78
import { makeConfig } from "@/test/makeConfig";
89
import type { AppConfig } from "@/types/config";
910

10-
function renderHeader(config: AppConfig = makeConfig(), children?: ReactNode) {
11+
function renderHeader(
12+
config: AppConfig = makeConfig(),
13+
children?: ReactNode,
14+
icon?: ComponentType<{ className?: string }>,
15+
) {
1116
return render(
1217
<AppConfigProvider config={config}>
13-
<PageHeader title="Nodes">{children}</PageHeader>
18+
<PageHeader title="Nodes" icon={icon}>
19+
{children}
20+
</PageHeader>
1421
</AppConfigProvider>,
1522
);
1623
}
@@ -41,4 +48,21 @@ describe("PageHeader", () => {
4148
expect(screen.getByText("EST")).toBeInTheDocument();
4249
expect(screen.getByText("extra badge")).toBeInTheDocument();
4350
});
51+
52+
it("renders the icon inside the heading when provided", () => {
53+
renderHeader(makeConfig(), undefined, IconNodes);
54+
const svg = screen
55+
.getByRole("heading", { name: "Nodes" })
56+
.querySelector("svg");
57+
expect(svg).not.toBeNull();
58+
expect(svg?.getAttribute("class")).toContain("h-8");
59+
expect(svg?.getAttribute("class")).toContain("w-8");
60+
});
61+
62+
it("renders no icon when omitted", () => {
63+
renderHeader();
64+
expect(
65+
screen.getByRole("heading", { name: "Nodes" }).querySelector("svg"),
66+
).toBeNull();
67+
});
4468
});

src/meshcore_hub/web/static/js/spa-react/components/PageHeader.tsx

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,23 @@
1-
import type { ReactNode } from "react";
1+
import type { ComponentType, ReactNode } from "react";
22
import { useAppConfig } from "@/context/AppConfigContext";
33

44
export function PageHeader({
55
title,
6+
icon: Icon,
67
children,
78
}: {
89
title: ReactNode;
10+
icon?: ComponentType<{ className?: string }>;
911
children?: ReactNode;
1012
}) {
1113
const config = useAppConfig();
1214
const tz = config.timezone || "";
1315
return (
1416
<div className="flex items-center justify-between mb-6">
15-
<h1 className="text-3xl font-bold">{title}</h1>
17+
<h1 className="text-3xl font-bold flex items-center gap-2">
18+
{Icon && <Icon className="h-8 w-8" />}
19+
{title}
20+
</h1>
1621
<div className="flex items-center gap-2">
1722
{tz && tz !== "UTC" && (
1823
<span className="text-sm opacity-60">{tz}</span>

src/meshcore_hub/web/static/js/spa-react/pages/Advertisements.test.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,11 @@ describe("Advertisements", () => {
4040
await waitFor(() => {
4141
expect(screen.getAllByText("AdNode").length).toBeGreaterThanOrEqual(1);
4242
});
43+
expect(
44+
screen
45+
.getByRole("heading", { name: "entities.advertisements" })
46+
.querySelector("svg"),
47+
).not.toBeNull();
4348
});
4449

4550
it("shows an error alert on fetch failure", async () => {

src/meshcore_hub/web/static/js/spa-react/pages/Advertisements.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { qk } from "@/utils/queryKeys";
88
import { useFormatDateTime } from "@/utils/format";
99
import { usePageTitle } from "@/hooks/usePageTitle";
1010
import { useAutoRefresh } from "@/hooks/useAutoRefresh";
11+
import { IconAdvertisements } from "@/components/icons";
1112
import { Pagination } from "@/components/Pagination";
1213
import {
1314
FilterForm,
@@ -235,7 +236,7 @@ export function Advertisements() {
235236

236237
return (
237238
<>
238-
<PageHeader title={t("entities.advertisements")} />
239+
<PageHeader title={t("entities.advertisements")} icon={IconAdvertisements} />
239240

240241
<ListToolbar
241242
total={total}

src/meshcore_hub/web/static/js/spa-react/pages/Channels.test.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,9 @@ describe("Channels", () => {
5252
expect(screen.getByText("Public")).toBeInTheDocument();
5353
expect(screen.getByText("Ops")).toBeInTheDocument();
5454
});
55+
expect(
56+
screen.getByRole("heading", { name: "entities.channels" }).querySelector("svg"),
57+
).not.toBeNull();
5558
});
5659

5760
it("shows an error alert on fetch failure", async () => {

src/meshcore_hub/web/static/js/spa-react/pages/Channels.tsx

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -283,7 +283,7 @@ export function Channels() {
283283
const config = useAppConfig();
284284
const oidcEnabled = config.oidc_enabled;
285285
const isAdmin = hasRole("admin");
286-
usePageTitle("channels.title");
286+
usePageTitle("entities.channels");
287287

288288
const queryClient = useQueryClient();
289289

@@ -364,14 +364,7 @@ export function Channels() {
364364

365365
return (
366366
<div>
367-
<PageHeader
368-
title={
369-
<span className="flex items-center gap-2">
370-
<IconChannel className="h-8 w-8" />
371-
{t("channels.title")}
372-
</span>
373-
}
374-
/>
367+
<PageHeader title={t("entities.channels")} icon={IconChannel} />
375368

376369
{error && <ErrorAlert message={error} />}
377370

src/meshcore_hub/web/static/js/spa-react/pages/Dashboard.test.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,9 @@ describe("Dashboard", () => {
4545
await waitFor(() => {
4646
expect(document.querySelector(".loading-spinner")).toBeNull();
4747
});
48+
expect(
49+
screen.getByRole("heading", { name: "entities.dashboard" }).querySelector("svg"),
50+
).not.toBeNull();
4851
});
4952

5053
it("shows an error on fetch failure", async () => {

src/meshcore_hub/web/static/js/spa-react/pages/Dashboard.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import { RouteTypeBadge } from "@/components/RouteTypeBadge";
1515
import {
1616
IconAdvertisements,
1717
IconChannel,
18+
IconDashboard,
1819
IconMessages,
1920
IconNodes,
2021
IconPackets,
@@ -428,7 +429,7 @@ export function DashboardPage() {
428429

429430
return (
430431
<>
431-
<PageHeader title={t("entities.dashboard")} />
432+
<PageHeader title={t("entities.dashboard")} icon={IconDashboard} />
432433

433434
{visibleChartCount > 0 && (
434435
<>

src/meshcore_hub/web/static/js/spa-react/pages/MapPage.test.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,9 @@ describe("MapPage", () => {
5555
await waitFor(() => {
5656
expect(screen.getByTestId("mock-map")).toBeInTheDocument();
5757
});
58+
expect(
59+
screen.getByRole("heading", { name: "entities.map" }).querySelector("svg"),
60+
).not.toBeNull();
5861
});
5962

6063
it("shows an error on fetch failure", async () => {

src/meshcore_hub/web/static/js/spa-react/pages/MapPage.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import {
2626
import { FilterToggle, OperatorSelect } from "@/components/FilterForm";
2727
import { ErrorAlert, Loading } from "@/components/Alerts";
2828
import { PageHeader } from "@/components/PageHeader";
29+
import { IconMap } from "@/components/icons";
2930

3031
const MAX_BOUNDS_RADIUS_KM = 20;
3132

@@ -386,7 +387,7 @@ export function MapPage() {
386387

387388
return (
388389
<div>
389-
<PageHeader title={t("entities.map")}>
390+
<PageHeader title={t("entities.map")} icon={IconMap}>
390391
<span className="badge badge-lg">{countBadgeText}</span>
391392
{showFilteredBadge && (
392393
<span className="badge badge-lg badge-ghost">

0 commit comments

Comments
 (0)