Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,16 +1,23 @@
import { render, screen } from "@testing-library/react";
import type { ReactNode } from "react";
import type { ComponentType, ReactNode } from "react";
import { describe, expect, it } from "vitest";

import { AppConfigProvider } from "@/context/AppConfigContext";
import { PageHeader } from "@/components/PageHeader";
import { IconNodes } from "@/components/icons";
import { makeConfig } from "@/test/makeConfig";
import type { AppConfig } from "@/types/config";

function renderHeader(config: AppConfig = makeConfig(), children?: ReactNode) {
function renderHeader(
config: AppConfig = makeConfig(),
children?: ReactNode,
icon?: ComponentType<{ className?: string }>,
) {
return render(
<AppConfigProvider config={config}>
<PageHeader title="Nodes">{children}</PageHeader>
<PageHeader title="Nodes" icon={icon}>
{children}
</PageHeader>
</AppConfigProvider>,
);
}
Expand Down Expand Up @@ -41,4 +48,21 @@ describe("PageHeader", () => {
expect(screen.getByText("EST")).toBeInTheDocument();
expect(screen.getByText("extra badge")).toBeInTheDocument();
});

it("renders the icon inside the heading when provided", () => {
renderHeader(makeConfig(), undefined, IconNodes);
const svg = screen
.getByRole("heading", { name: "Nodes" })
.querySelector("svg");
expect(svg).not.toBeNull();
expect(svg?.getAttribute("class")).toContain("h-8");
expect(svg?.getAttribute("class")).toContain("w-8");
});

it("renders no icon when omitted", () => {
renderHeader();
expect(
screen.getByRole("heading", { name: "Nodes" }).querySelector("svg"),
).toBeNull();
});
});
Original file line number Diff line number Diff line change
@@ -1,18 +1,23 @@
import type { ReactNode } from "react";
import type { ComponentType, ReactNode } from "react";
import { useAppConfig } from "@/context/AppConfigContext";

export function PageHeader({
title,
icon: Icon,
children,
}: {
title: ReactNode;
icon?: ComponentType<{ className?: string }>;
children?: ReactNode;
}) {
const config = useAppConfig();
const tz = config.timezone || "";
return (
<div className="flex items-center justify-between mb-6">
<h1 className="text-3xl font-bold">{title}</h1>
<h1 className="text-3xl font-bold flex items-center gap-2">
{Icon && <Icon className="h-8 w-8" />}
{title}
</h1>
<div className="flex items-center gap-2">
{tz && tz !== "UTC" && (
<span className="text-sm opacity-60">{tz}</span>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,11 @@ describe("Advertisements", () => {
await waitFor(() => {
expect(screen.getAllByText("AdNode").length).toBeGreaterThanOrEqual(1);
});
expect(
screen
.getByRole("heading", { name: "entities.advertisements" })
.querySelector("svg"),
).not.toBeNull();
});

it("shows an error alert on fetch failure", async () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { qk } from "@/utils/queryKeys";
import { useFormatDateTime } from "@/utils/format";
import { usePageTitle } from "@/hooks/usePageTitle";
import { useAutoRefresh } from "@/hooks/useAutoRefresh";
import { IconAdvertisements } from "@/components/icons";
import { Pagination } from "@/components/Pagination";
import {
FilterForm,
Expand Down Expand Up @@ -235,7 +236,7 @@ export function Advertisements() {

return (
<>
<PageHeader title={t("entities.advertisements")} />
<PageHeader title={t("entities.advertisements")} icon={IconAdvertisements} />

<ListToolbar
total={total}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,9 @@ describe("Channels", () => {
expect(screen.getByText("Public")).toBeInTheDocument();
expect(screen.getByText("Ops")).toBeInTheDocument();
});
expect(
screen.getByRole("heading", { name: "entities.channels" }).querySelector("svg"),
).not.toBeNull();
});

it("shows an error alert on fetch failure", async () => {
Expand Down
11 changes: 2 additions & 9 deletions src/meshcore_hub/web/static/js/spa-react/pages/Channels.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -283,7 +283,7 @@ export function Channels() {
const config = useAppConfig();
const oidcEnabled = config.oidc_enabled;
const isAdmin = hasRole("admin");
usePageTitle("channels.title");
usePageTitle("entities.channels");

const queryClient = useQueryClient();

Expand Down Expand Up @@ -364,14 +364,7 @@ export function Channels() {

return (
<div>
<PageHeader
title={
<span className="flex items-center gap-2">
<IconChannel className="h-8 w-8" />
{t("channels.title")}
</span>
}
/>
<PageHeader title={t("entities.channels")} icon={IconChannel} />

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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,9 @@ describe("Dashboard", () => {
await waitFor(() => {
expect(document.querySelector(".loading-spinner")).toBeNull();
});
expect(
screen.getByRole("heading", { name: "entities.dashboard" }).querySelector("svg"),
).not.toBeNull();
});

it("shows an error on fetch failure", async () => {
Expand Down
3 changes: 2 additions & 1 deletion src/meshcore_hub/web/static/js/spa-react/pages/Dashboard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { RouteTypeBadge } from "@/components/RouteTypeBadge";
import {
IconAdvertisements,
IconChannel,
IconDashboard,
IconMessages,
IconNodes,
IconPackets,
Expand Down Expand Up @@ -428,7 +429,7 @@ export function DashboardPage() {

return (
<>
<PageHeader title={t("entities.dashboard")} />
<PageHeader title={t("entities.dashboard")} icon={IconDashboard} />

{visibleChartCount > 0 && (
<>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,9 @@ describe("MapPage", () => {
await waitFor(() => {
expect(screen.getByTestId("mock-map")).toBeInTheDocument();
});
expect(
screen.getByRole("heading", { name: "entities.map" }).querySelector("svg"),
).not.toBeNull();
});

it("shows an error on fetch failure", async () => {
Expand Down
3 changes: 2 additions & 1 deletion src/meshcore_hub/web/static/js/spa-react/pages/MapPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import {
import { FilterToggle, OperatorSelect } from "@/components/FilterForm";
import { ErrorAlert, Loading } from "@/components/Alerts";
import { PageHeader } from "@/components/PageHeader";
import { IconMap } from "@/components/icons";

const MAX_BOUNDS_RADIUS_KM = 20;

Expand Down Expand Up @@ -386,7 +387,7 @@ export function MapPage() {

return (
<div>
<PageHeader title={t("entities.map")}>
<PageHeader title={t("entities.map")} icon={IconMap}>
<span className="badge badge-lg">{countBadgeText}</span>
{showFilteredBadge && (
<span className="badge badge-lg badge-ghost">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,9 @@ describe("Members", () => {
});
expect(screen.getByText("Bob")).toBeInTheDocument();
expect(screen.queryByText("TestUser")).not.toBeInTheDocument();
expect(
screen.getByRole("heading", { name: "entities.members" }).querySelector("svg"),
).not.toBeNull();
});

it("shows an empty state when no visible profiles exist", async () => {
Expand Down
6 changes: 3 additions & 3 deletions src/meshcore_hub/web/static/js/spa-react/pages/Members.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import { Loading, ErrorAlert } from "@/components/Alerts";
import { CallsignBadge, RoleBadge } from "@/components/Badges";
import { EmptyState } from "@/components/EmptyState";
import { PageHeader } from "@/components/PageHeader";
import { IconAntenna, IconUsers } from "@/components/icons";
import { IconAntenna, IconMembers, IconUsers } from "@/components/icons";
import { usePageTitle } from "@/hooks/usePageTitle";

interface MemberNode {
Expand Down Expand Up @@ -184,7 +184,7 @@ export function Members() {
if (visible.length === 0) {
return (
<>
<PageHeader title={t("entities.members")} />
<PageHeader title={t("entities.members")} icon={IconMembers} />
<EmptyState>
<p className="text-lg">{t("members_page.empty_state")}</p>
<p className="text-sm mt-2">{t("members_page.empty_description")}</p>
Expand All @@ -207,7 +207,7 @@ export function Members() {

return (
<>
<PageHeader title={t("entities.members")}>
<PageHeader title={t("entities.members")} icon={IconMembers}>
<span className="badge badge-lg">
{t("common.count_entity", {
count: formatNumber(operators.length + members.length),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,9 @@ describe("Messages", () => {
await waitFor(() => {
expect(screen.getAllByText("Hello world").length).toBeGreaterThanOrEqual(1);
});
expect(
screen.getByRole("heading", { name: "entities.messages" }).querySelector("svg"),
).not.toBeNull();
});

it("shows an error alert on fetch failure", async () => {
Expand Down
3 changes: 2 additions & 1 deletion src/meshcore_hub/web/static/js/spa-react/pages/Messages.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {
} from "@/utils/messageHelpers";
import { usePageTitle } from "@/hooks/usePageTitle";
import { useAutoRefresh } from "@/hooks/useAutoRefresh";
import { IconMessages } from "@/components/icons";
import { Pagination } from "@/components/Pagination";
import {
FilterForm,
Expand Down Expand Up @@ -278,7 +279,7 @@ export function Messages() {

return (
<>
<PageHeader title={t("entities.messages")} />
<PageHeader title={t("entities.messages")} icon={IconMessages} />

<ListToolbar
total={total}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,9 @@ describe("NodeDetail", () => {
await waitFor(() => {
expect(document.querySelector(".loading-spinner")).toBeNull();
});
expect(
screen.getByRole("heading", { name: "DetailNode" }).querySelector("svg"),
).not.toBeNull();
});

it("shows an error on fetch failure", async () => {
Expand Down
7 changes: 5 additions & 2 deletions src/meshcore_hub/web/static/js/spa-react/pages/NodeDetail.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import { ErrorAlert, Loading, SuccessAlert } from "@/components/Alerts";
import { Breadcrumbs } from "@/components/Breadcrumbs";
import { ConfirmDialog } from "@/components/ConfirmDialog";
import { CopyableValue } from "@/components/CopyableValue";
import { IconEdit, IconPlus, IconTrash } from "@/components/icons";
import { IconEdit, IconNodes, IconPlus, IconTrash } from "@/components/icons";
import { MeshQrCode } from "@/components/MeshQrCode";
import { Modal } from "@/components/Modal";
import { NotFoundState } from "@/components/NotFoundState";
Expand Down Expand Up @@ -606,7 +606,10 @@ export function NodeDetailPage() {
{emoji}
</span>
<div className="flex-1 min-w-0">
<h1 className="text-3xl font-bold">{displayName}</h1>
<h1 className="text-3xl font-bold flex items-center gap-2">
<IconNodes className="h-8 w-8" />
{displayName}
</h1>
{tagDescription && (
<p className="opacity-70 mt-2">{tagDescription}</p>
)}
Expand Down
3 changes: 3 additions & 0 deletions src/meshcore_hub/web/static/js/spa-react/pages/Nodes.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,9 @@ describe("Nodes", () => {
await waitFor(() => {
expect(screen.getAllByText("TestNode").length).toBeGreaterThanOrEqual(1);
});
expect(
screen.getByRole("heading", { name: "entities.nodes" }).querySelector("svg"),
).not.toBeNull();
});

it("shows an error alert on fetch failure", async () => {
Expand Down
3 changes: 2 additions & 1 deletion src/meshcore_hub/web/static/js/spa-react/pages/Nodes.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { useFormatDateTime } from "@/utils/format";
import { usePageTitle } from "@/hooks/usePageTitle";
import { useAutoRefresh } from "@/hooks/useAutoRefresh";
import { Pagination } from "@/components/Pagination";
import { IconNodes } from "@/components/icons";
import {
FilterForm,
FilterField,
Expand Down Expand Up @@ -249,7 +250,7 @@ export function Nodes() {

return (
<div>
<PageHeader title={t("entities.nodes")} />
<PageHeader title={t("entities.nodes")} icon={IconNodes} />

<ListToolbar
total={total}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,9 @@ describe("PacketDetail", () => {
expect(screen.getAllByText("abc123").length).toBeGreaterThanOrEqual(1);
});
expect(screen.getByText("Observer1")).toBeInTheDocument();
expect(
screen.getByRole("heading", { name: "abc123" }).querySelector("svg"),
).not.toBeNull();
});

it("shows not-found state on a 404 error", async () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { useFormatDateTime } from "@/utils/format";
import { Loading, WarningBadge } from "@/components/Alerts";
import { Breadcrumbs } from "@/components/Breadcrumbs";
import { NotFoundState } from "@/components/NotFoundState";
import { IconPackets } from "@/components/icons";
import { DefinitionGrid } from "@/components/Definition";
import {
buildChannelNames,
Expand Down Expand Up @@ -90,6 +91,11 @@ export function PacketDetail() {
]}
/>

<h1 className="text-3xl font-bold flex items-center gap-2 mb-6">
<IconPackets className="h-8 w-8" />
{leaf || t("packets.detail_title")}
</h1>

{notFound && (
<NotFoundState
message={t("common.entity_not_found_details", {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,9 @@ describe("PacketGroupDetail", () => {
await waitFor(() => {
expect(screen.getAllByText("grouphash").length).toBeGreaterThanOrEqual(1);
});
expect(
screen.getByRole("heading", { name: "grouphash" }).querySelector("svg"),
).not.toBeNull();
});

it("shows an error on fetch failure", async () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import {
import { groupByObserver } from "@/utils/packetGroupHelpers";
import { Loading, WarningBadge } from "@/components/Alerts";
import { Breadcrumbs } from "@/components/Breadcrumbs";
import { IconSatelliteDish } from "@/components/icons";
import { IconPackets, IconSatelliteDish } from "@/components/icons";
import { NotFoundState } from "@/components/NotFoundState";
import { TimeAgo } from "@/components/TimeAgo";
import { DefinitionGrid } from "@/components/Definition";
Expand Down Expand Up @@ -356,6 +356,11 @@ export function PacketGroupDetail() {
]}
/>

<h1 className="text-3xl font-bold flex items-center gap-2 mb-6">
<IconPackets className="h-8 w-8" />
{leaf || t("packets.detail_title")}
</h1>

{notFound && (
<NotFoundState tone="warning" message={t("packets.not_found_retention")} />
)}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,9 @@ describe("Packets", () => {
await waitFor(() => {
expect(screen.getByText("hash1")).toBeInTheDocument();
});
expect(
screen.getByRole("heading", { name: "entities.packets" }).querySelector("svg"),
).not.toBeNull();
});

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