feat(reports): clamp report descriptions with a show more toggle

Report descriptions run to a paragraph. ReportDescription wraps Mantine's
Spoiler to clamp them to two lines, and the toggle only renders when the
text actually overflows.

PageHeader kept its subtitle on a hard truncate class, which would have
pinned the spoiler to one line, so a ReactNode subtitle now renders as-is
and owns its own layout. A string subtitle still truncates as before.
This commit is contained in:
Nathnael
2026-08-24 07:54:36 +00:00
parent baef14c847
commit de407c0014
4 changed files with 41 additions and 8 deletions

View File

@@ -58,9 +58,15 @@ export function PageHeader({
{meta} {meta}
</Group> </Group>
{subtitle ? ( {subtitle ? (
<Text c="dimmed" size="sm" mt={4} className="truncate"> typeof subtitle === "string" ? (
{subtitle} <Text c="dimmed" size="sm" mt={4} className="truncate">
</Text> {subtitle}
</Text>
) : (
// A component subtitle handles its own layout — truncating it
// to one line would defeat e.g. an expandable description.
<div style={{ marginTop: 4 }}>{subtitle}</div>
)
) : null} ) : null}
</div> </div>
</Group> </Group>

View File

@@ -0,0 +1,27 @@
import { Spoiler, Text } from "@mantine/core";
interface ReportDescriptionProps {
text: string;
}
/**
* Report descriptions run to a paragraph. Clamps to roughly two lines and adds
* a Show more toggle — Spoiler measures the content, so the toggle only appears
* when the text actually overflows.
*/
export function ReportDescription({ text }: ReportDescriptionProps) {
return (
<Spoiler
maxHeight={44}
showLabel="Show more"
hideLabel="Show less"
styles={{ control: { fontSize: "var(--mantine-font-size-xs)" } }}
>
<Text c="dimmed" size="sm">
{text}
</Text>
</Spoiler>
);
}
export default ReportDescription;

View File

@@ -1,8 +1,9 @@
import { Stack, Text, Title } from "@mantine/core"; import { Stack, Title } from "@mantine/core";
import { useQuery } from "@tanstack/react-query"; import { useQuery } from "@tanstack/react-query";
import { api } from "@/services/api"; import { api } from "@/services/api";
import { ReportDescription } from "./ReportDescription";
import { ReportView } from "./ReportView"; import { ReportView } from "./ReportView";
interface ReportSectionProps { interface ReportSectionProps {
@@ -29,9 +30,7 @@ export function ReportSection({ reportKey, idKeyValue, defaultView }: ReportSect
<Stack gap="xs"> <Stack gap="xs">
<div> <div>
<Title order={4}>{def.title}</Title> <Title order={4}>{def.title}</Title>
<Text size="sm" c="dimmed"> <ReportDescription text={def.description} />
{def.description}
</Text>
</div> </div>
<ReportView reportKey={reportKey} idKeyValue={idKeyValue} defaultView={defaultView} /> <ReportView reportKey={reportKey} idKeyValue={idKeyValue} defaultView={defaultView} />
</Stack> </Stack>

View File

@@ -13,6 +13,7 @@ import type { ReportFilterDef, ReportRunParams } from "@/types/reports";
import { DataTable, DataTableFooter, usePagination, type ColumnDef } from "@edr/ui-common"; import { DataTable, DataTableFooter, usePagination, type ColumnDef } from "@edr/ui-common";
import { ReportChart } from "./ReportChart"; import { ReportChart } from "./ReportChart";
import { ReportDescription } from "./ReportDescription";
import { ReportExportButton } from "./ReportExportButton"; import { ReportExportButton } from "./ReportExportButton";
import { formatKpiValue, formatReportCell } from "./report-format"; import { formatKpiValue, formatReportCell } from "./report-format";
@@ -229,7 +230,7 @@ export function ReportView({ reportKey, idKeyValue, pageHeader, defaultView }: R
{pageHeader ? ( {pageHeader ? (
<PageHeader <PageHeader
title={def.title} title={def.title}
subtitle={def.description} subtitle={<ReportDescription text={def.description} />}
action={ action={
<Group gap="xs"> <Group gap="xs">
{exportButton} {exportButton}