fix(admin): consolidate route headings and descriptions
This commit is contained in:
@@ -161,6 +161,10 @@ function titleFor(path: string, t: (k: any) => string): string {
|
||||
}
|
||||
|
||||
function subtitleFor(path: string, t: (k: any) => string): string | undefined {
|
||||
if (path.startsWith("/admin/audit")) return t("adminAuditSubtitle");
|
||||
if (path.startsWith("/admin/users")) return t("adminUsersSubtitle");
|
||||
if (path.startsWith("/admin/system")) return t("adminSystemSubtitle");
|
||||
if (path.startsWith("/admin/processing")) return t("processingSubtitle");
|
||||
if (/^\/settings\/?$/.test(path)) return t("settingsPageDescription");
|
||||
if (path === "/dashboard") return t("dashboardPageSubtitle");
|
||||
if (path.startsWith("/analytics")) return t("analyticsPageSubtitle");
|
||||
|
||||
@@ -37,8 +37,8 @@ test("[P7-10 audit] retains shared heading hierarchy and a named keyboard scroll
|
||||
mockedApi.get.mockResolvedValue({ data: [row] } as any);
|
||||
renderPage();
|
||||
await screen.findByRole("button", { name: "Undo" });
|
||||
expect(screen.getByRole("heading", { name: "Audit log", level: 2 })).toBeInTheDocument();
|
||||
expect(screen.getByRole("heading", { name: translations.en.adminAuditEvents, level: 3 })).toBeInTheDocument();
|
||||
expect(screen.queryByRole("heading", { name: /^Audit log$/ })).not.toBeInTheDocument();
|
||||
expect(screen.getByRole("heading", { name: translations.en.adminAuditEvents, level: 2 })).toBeInTheDocument();
|
||||
const region = screen.getByRole("region", { name: "Audit log" });
|
||||
expect(region).toHaveAttribute("tabindex", "0");
|
||||
expect(within(region).getByRole("table")).toHaveAccessibleName("Audit log");
|
||||
@@ -104,7 +104,7 @@ test("distinguishes an unavailable audit log from an empty audit log and retries
|
||||
mockedApi.get.mockRejectedValueOnce(new Error("offline"));
|
||||
renderPage();
|
||||
|
||||
expect(screen.getByRole("heading", { name: "Audit log", level: 2 })).toBeInTheDocument();
|
||||
expect(screen.getByRole("region", { name: /^Audit log$/ })).toBeInTheDocument();
|
||||
|
||||
expect(await screen.findByText("Unable to load audit events")).toBeInTheDocument();
|
||||
expect(screen.queryByText("No events.")).not.toBeInTheDocument();
|
||||
|
||||
@@ -176,7 +176,8 @@ describe('AdminSystemPage', () => {
|
||||
it('uses contextual system headings and keeps metric values out of heading navigation', async () => {
|
||||
render(<I18nProvider><AdminSystemPage /></I18nProvider>);
|
||||
await screen.findByText('Production');
|
||||
expect(screen.getByRole('heading', { name: 'System status', level: 2 })).toBeInTheDocument();
|
||||
expect(screen.getByRole('region', { name: 'System status' })).toBeInTheDocument();
|
||||
expect(screen.queryByRole('heading', { name: 'System status' })).not.toBeInTheDocument();
|
||||
expect(screen.queryByRole('heading', { name: 'Production' })).not.toBeInTheDocument();
|
||||
const levels = screen.getAllByRole('heading').map(heading => Number(heading.tagName.slice(1)));
|
||||
let previous = 1;
|
||||
|
||||
@@ -96,7 +96,7 @@ test('disposed create acknowledgement cannot start a new private list read', asy
|
||||
test("requires explicit confirmation before removing your own admin role", async () => {
|
||||
renderPage([{ id: "me", email: "me@example.com", userName: "me", roles: ["Admin"], emailConfirmed: true, isCurrentUser: true, canRemoveAdmin: true }]);
|
||||
|
||||
expect(screen.getByRole("heading", { name: "Users", level: 2 })).toBeInTheDocument();
|
||||
expect(screen.getByRole("heading", { name: "Create user", level: 2 })).toBeInTheDocument();
|
||||
|
||||
fireEvent.click(await screen.findByRole("button", { name: "Remove admin" }));
|
||||
expect(await screen.findByText(/you will immediately lose access/i)).toBeInTheDocument();
|
||||
|
||||
@@ -18,7 +18,7 @@ import { useToast } from "../toast";
|
||||
import { useI18n } from "../i18n/I18nProvider";
|
||||
import { useViewResource } from "../hooks/useViewResource";
|
||||
import ViewStateNotice from "../components/ViewStateNotice";
|
||||
import { PageHeader, PageSection } from "../layout/PageLayout";
|
||||
import { PageSection } from "../layout/PageLayout";
|
||||
import { checkSessionCookie, getSessionContext, isSessionInvalidated } from "../sessionBoundary";
|
||||
|
||||
type AuditItem = {
|
||||
@@ -86,8 +86,7 @@ export default function AdminAuditPage() {
|
||||
|
||||
return (
|
||||
<Box sx={{ display: "grid", gap: "var(--app-section-gap)", minWidth: 0 }}>
|
||||
<PageHeader level="h2" title={t("adminAuditTitle")} description={t("adminAuditSubtitle")} />
|
||||
<PageSection level="h3" title={t("adminAuditEvents")}>
|
||||
<PageSection title={t("adminAuditEvents")}>
|
||||
<Box ref={body} role="region" aria-label={t("adminAuditTitle")} tabIndex={0} aria-busy={busyId!==null || auditResource.loading || auditResource.refreshing} sx={{ minWidth: 0, overflow: "auto" }}>
|
||||
|
||||
<ViewStateNotice
|
||||
|
||||
@@ -6,7 +6,7 @@ import { AUTH_USER_CHANGED_EVENT, getAuthUserKey } from '../auth';
|
||||
import { isSessionInvalidated, SESSION_INVALIDATED_EVENT } from '../sessionBoundary';
|
||||
import { useI18n } from '../i18n/I18nProvider';
|
||||
import type { TranslationKey } from '../i18n/translations';
|
||||
import { PageHeader, PageSection } from '../layout/PageLayout';
|
||||
import { PageSection } from '../layout/PageLayout';
|
||||
|
||||
type Item = {
|
||||
id: string; owner: { id: string; name: string | null; email: string | null }; taskType: string; status: string; stage: string | null;
|
||||
@@ -139,7 +139,6 @@ export default function AdminProcessingPage() {
|
||||
const resume = () => { pausedRef.current = false; setPaused(false); setBefore(null); setRefresh(value => value + 1); };
|
||||
const visibleListing = identity === getAuthUserKey() ? listing : null;
|
||||
return <Box sx={{ display: 'grid', gap: 3, minWidth: 0, '& p, & td, & th': { overflowWrap: 'anywhere' }, '& button': { minHeight: 44 } }}>
|
||||
<PageHeader level="h2" title={t('processingTitle')} description={t('processingSubtitle')} />
|
||||
<Stack direction="row" useFlexGap flexWrap="wrap" spacing={1} alignItems="center">
|
||||
<Button onClick={() => { if (paused) resume(); else { pausedRef.current = true; setPaused(true); } }}>{t(paused ? 'processingResume' : 'processingPause')}</Button>
|
||||
<Button onClick={() => setRefresh(value => value + 1)} disabled={busy}>{t('refresh')}</Button>
|
||||
|
||||
@@ -17,7 +17,7 @@ import {
|
||||
|
||||
import { api, getApiErrorMessage } from "../api";
|
||||
import { useI18n } from "../i18n/I18nProvider";
|
||||
import { PageHeader, PageSection } from "../layout/PageLayout";
|
||||
import { PageSection } from "../layout/PageLayout";
|
||||
import type { TranslationKey } from "../i18n/translations";
|
||||
import { checkSessionCookie, getSessionContext, isSessionInvalidated } from "../sessionBoundary";
|
||||
|
||||
@@ -250,7 +250,6 @@ export default function AdminSystemPage() {
|
||||
const passwordRevision = useRef(0);
|
||||
const savedRevision = useRef(0);
|
||||
const bodyRef = useRef<HTMLDivElement>(null);
|
||||
const headingRef = useRef<HTMLHeadingElement>(null);
|
||||
const restoreFocus = useRef<HTMLElement | null>(null);
|
||||
const navigationId = useId();
|
||||
|
||||
@@ -302,7 +301,6 @@ export default function AdminSystemPage() {
|
||||
restoreFocus.current = null;
|
||||
if (document.activeElement !== bodyRef.current) return;
|
||||
if (target.isConnected && !target.matches(":disabled")) target.focus();
|
||||
else headingRef.current?.focus();
|
||||
}, [loading, action]);
|
||||
|
||||
const rememberFocus = () => {
|
||||
@@ -422,21 +420,20 @@ export default function AdminSystemPage() {
|
||||
|
||||
return (
|
||||
<Box ref={bodyRef} role="region" aria-label={t("adminSystemTitle")} aria-busy={locked} tabIndex={-1} sx={{ display: "flex", flexDirection: "column", gap: "var(--app-section-gap)", minWidth: 0, overflowWrap: "anywhere", "& .MuiButton-root": { whiteSpace: "normal", overflowWrap: "anywhere", maxWidth: "100%", minHeight: { xs: 44, sm: 36 }, "@media (pointer: coarse)": { minHeight: 44 } }, "& .MuiChip-root": { maxWidth: "100%", height: "auto" }, "& .MuiChip-label": { whiteSpace: "normal", overflowWrap: "anywhere", py: 0.5 } }}>
|
||||
<PageHeader level="h2" titleRef={headingRef} title={t("adminSystemTitle")} description={t("adminSystemSubtitle")} actions={
|
||||
<Box sx={{ display: "flex", gap: 1, flexWrap: "wrap" }}>
|
||||
<Button
|
||||
variant="outlined"
|
||||
onClick={async () => {
|
||||
await runAction("probe", async (active) => { await api.post("/admin/system/ai/probe"); if (active()) await load("status", true); }, "adminSystemProbeCompleted");
|
||||
}}
|
||||
disabled={locked || !status || !!readErrors.status}
|
||||
>
|
||||
{action === "probe" ? t("adminSystemRunningProbe") : t("adminSystemRunProbe")}
|
||||
</Button>
|
||||
<Button variant="contained" onClick={() => refresh()} disabled={locked}>
|
||||
{loading ? t("adminSystemRefreshing") : t("adminSystemRefresh")}
|
||||
</Button>
|
||||
</Box> } />
|
||||
<Box sx={{ display: "flex", gap: 1, flexWrap: "wrap" }}>
|
||||
<Button
|
||||
variant="outlined"
|
||||
onClick={async () => {
|
||||
await runAction("probe", async (active) => { await api.post("/admin/system/ai/probe"); if (active()) await load("status", true); }, "adminSystemProbeCompleted");
|
||||
}}
|
||||
disabled={locked || !status || !!readErrors.status}
|
||||
>
|
||||
{action === "probe" ? t("adminSystemRunningProbe") : t("adminSystemRunProbe")}
|
||||
</Button>
|
||||
<Button variant="contained" onClick={() => refresh()} disabled={locked}>
|
||||
{loading ? t("adminSystemRefreshing") : t("adminSystemRefresh")}
|
||||
</Button>
|
||||
</Box>
|
||||
|
||||
<Tabs value={tab} aria-label={t("adminSystemTitle")} onChange={(_, value) => setTab(value)} variant="scrollable" scrollButtons="auto" allowScrollButtonsMobile>
|
||||
<Tab id={`${navigationId}-tab-0`} aria-controls={`${navigationId}-panel-0`} label={t("adminSystemStatusTab")} />
|
||||
@@ -484,7 +481,7 @@ export default function AdminSystemPage() {
|
||||
</Box>
|
||||
|
||||
<Box sx={{ display: "grid", gridTemplateColumns: { xs: "1fr", lg: "1.2fr 1fr" }, gap: 2 }}>
|
||||
<PageSection level="h3" title={t("adminSystemDatabaseStorage")}>
|
||||
<PageSection title={t("adminSystemDatabaseStorage")}>
|
||||
<Stack spacing={0.75}>
|
||||
<DetailRow label={t("adminSystemProvider")} value={status?.database.provider || "-"} />
|
||||
<DetailRow label={t("adminSystemTarget")} value={status?.database.target || "-"} />
|
||||
@@ -501,7 +498,7 @@ export default function AdminSystemPage() {
|
||||
</Stack>
|
||||
</PageSection>
|
||||
|
||||
<PageSection level="h3" title={t("adminSystemRuntimeAuth")}>
|
||||
<PageSection title={t("adminSystemRuntimeAuth")}>
|
||||
<Stack spacing={0.75}>
|
||||
<DetailRow label={t("adminSystemFramework")} value={status?.runtime.framework || "-"} />
|
||||
<DetailRow label={t("adminSystemOs")} value={status?.runtime.osDescription || "-"} />
|
||||
@@ -518,7 +515,7 @@ export default function AdminSystemPage() {
|
||||
</Box>
|
||||
|
||||
<Box sx={{ display: "grid", gridTemplateColumns: { xs: "1fr", lg: "1fr 1fr" }, gap: 2 }}>
|
||||
<PageSection level="h3" title={t("adminSystemEmailConfig")}>
|
||||
<PageSection title={t("adminSystemEmailConfig")}>
|
||||
<Stack spacing={0.75}>
|
||||
<DetailRow label={t("adminSystemEnabled")} value={status?.email.enabled ? t("yes") : t("noWord")} />
|
||||
<DetailRow label={t("adminSystemFrom")} value={status?.email.from || "-"} />
|
||||
@@ -529,7 +526,7 @@ export default function AdminSystemPage() {
|
||||
</Stack>
|
||||
</PageSection>
|
||||
|
||||
<PageSection level="h3" title={t("adminSystemSummarizerRuntime")}>
|
||||
<PageSection title={t("adminSystemSummarizerRuntime")}>
|
||||
<Stack spacing={0.75}>
|
||||
<DetailRow label={t("adminSystemModel")} value={status?.ai.model || "-"} />
|
||||
<DetailRow label={t("adminSystemDevice")} value={status?.ai.device || "-"} />
|
||||
@@ -550,7 +547,7 @@ export default function AdminSystemPage() {
|
||||
</PageSection>
|
||||
</Box>
|
||||
|
||||
<PageSection level="h3" title={t("adminSystemSummarizerTelemetry")}>
|
||||
<PageSection title={t("adminSystemSummarizerTelemetry")}>
|
||||
<Box sx={{ display: "grid", gridTemplateColumns: { xs: "1fr 1fr", md: "repeat(8, 1fr)" }, gap: 2 }}>
|
||||
<Box><Typography variant="overline" sx={{ color: "text.secondary" }}>{t("adminSystemRequests")}</Typography><Typography component="p" variant="h6" sx={{ fontWeight: 900 }}>{status?.ai.requests ?? 0}</Typography></Box>
|
||||
<Box><Typography variant="overline" sx={{ color: "text.secondary" }}>{t("adminSystemCacheHits")}</Typography><Typography component="p" variant="h6" sx={{ fontWeight: 900 }}>{status?.ai.cacheHits ?? 0}</Typography></Box>
|
||||
@@ -577,7 +574,7 @@ export default function AdminSystemPage() {
|
||||
</Box>
|
||||
</PageSection>
|
||||
|
||||
<PageSection level="h3" title={t("adminBenchmarkTitle")}>
|
||||
<PageSection title={t("adminBenchmarkTitle")}>
|
||||
<Box sx={{ display: "flex", alignItems: "flex-start", justifyContent: "space-between", gap: 2, flexWrap: "wrap", mb: 1 }}>
|
||||
<Box>
|
||||
<Typography variant="body2" sx={{ color: "text.secondary", mt: 0.25 }}>
|
||||
@@ -619,7 +616,7 @@ export default function AdminSystemPage() {
|
||||
|
||||
<Box sx={{ mt: 2.5, display: "grid", gridTemplateColumns: { xs: "1fr", xl: "1.1fr 0.9fr" }, gap: 2 }}>
|
||||
<Box sx={{ minWidth: 0, pt: 1.5, borderTop: 1, borderColor: "divider" }}>
|
||||
<Typography component="h4" variant="subtitle1" sx={{ fontWeight: 900, mb: 1 }}>{t("adminBenchmarkTopFindings")}</Typography>
|
||||
<Typography component="h3" variant="subtitle1" sx={{ fontWeight: 900, mb: 1 }}>{t("adminBenchmarkTopFindings")}</Typography>
|
||||
<Stack spacing={1}>
|
||||
{benchmarkFindings.length > 0 ? benchmarkFindings.map((finding) => (
|
||||
<Box key={`${finding.file}:${finding.issue}`} sx={{ py: 1, overflowWrap: "anywhere", "& + &": { borderTop: 1, borderColor: "divider" } }}>
|
||||
@@ -631,7 +628,7 @@ export default function AdminSystemPage() {
|
||||
</Box>
|
||||
|
||||
<Box sx={{ minWidth: 0, pt: 1.5, borderTop: 1, borderColor: "divider" }}>
|
||||
<Typography component="h4" variant="subtitle1" sx={{ fontWeight: 900, mb: 1 }}>{t("adminBenchmarkWeakest")}</Typography>
|
||||
<Typography component="h3" variant="subtitle1" sx={{ fontWeight: 900, mb: 1 }}>{t("adminBenchmarkWeakest")}</Typography>
|
||||
<Stack spacing={1}>
|
||||
{weakestEntries.map((entry) => (
|
||||
<Box key={entry.Slug} sx={{ py: 1, overflowWrap: "anywhere", "& + &": { borderTop: 1, borderColor: "divider" } }}>
|
||||
@@ -649,7 +646,7 @@ export default function AdminSystemPage() {
|
||||
</Box>
|
||||
|
||||
<Box sx={{ mt: 2, pt: 1.5, borderTop: 1, borderColor: "divider", maxHeight: 280, overflow: "auto" }}>
|
||||
<Typography component="h4" variant="subtitle2" sx={{ fontWeight: 800, mb: 1 }}>{t("adminBenchmarkLatestSummary")}</Typography>
|
||||
<Typography component="h3" variant="subtitle2" sx={{ fontWeight: 800, mb: 1 }}>{t("adminBenchmarkLatestSummary")}</Typography>
|
||||
<Typography variant="body2" sx={{ whiteSpace: "pre-wrap", fontFamily: "ui-monospace, SFMono-Regular, monospace" }}>
|
||||
{benchmarkStatus?.reportMarkdown || "-"}
|
||||
</Typography>
|
||||
@@ -668,7 +665,7 @@ export default function AdminSystemPage() {
|
||||
<Box role="tabpanel" id={`${navigationId}-panel-1`} aria-labelledby={`${navigationId}-tab-1`} hidden={tab !== 1} sx={{ minWidth: 0 }}>
|
||||
<Box sx={{ display: "grid", gap: 2, minWidth: 0 }}>
|
||||
{readNotice("email")}
|
||||
{emailSettings ? <PageSection surface level="h3" title={t("adminSystemEmailSettingsTitle")}>
|
||||
{emailSettings ? <PageSection surface title={t("adminSystemEmailSettingsTitle")}>
|
||||
<Typography variant="body2" sx={{ color: "text.secondary", mb: 1.5 }}>{t("adminSystemEmailSettingsBody")}</Typography>
|
||||
<Box sx={{ display: "grid", gridTemplateColumns: { xs: "1fr", md: "1fr 1fr" }, gap: 1.5 }}>
|
||||
<FormControlLabel
|
||||
@@ -694,7 +691,7 @@ export default function AdminSystemPage() {
|
||||
</Box>
|
||||
</PageSection> : null}
|
||||
|
||||
<PageSection surface level="h3" title={t("adminSystemSmtpTest")}>
|
||||
<PageSection surface title={t("adminSystemSmtpTest")}>
|
||||
<Typography variant="body2" sx={{ color: "text.secondary", mb: 1.5 }}>
|
||||
{t("adminSystemSmtpTestBody")}
|
||||
</Typography>
|
||||
|
||||
@@ -23,7 +23,7 @@ import { useDialogActions } from "../dialogs";
|
||||
import { useI18n } from "../i18n/I18nProvider";
|
||||
import { useViewResource } from "../hooks/useViewResource";
|
||||
import ViewStateNotice from "../components/ViewStateNotice";
|
||||
import { PageHeader, PageSection } from "../layout/PageLayout";
|
||||
import { PageSection } from "../layout/PageLayout";
|
||||
import { checkSessionCookie, getSessionContext, isSessionInvalidated } from "../sessionBoundary";
|
||||
|
||||
type UserDto = {
|
||||
@@ -222,10 +222,9 @@ export default function AdminUsersPage() {
|
||||
|
||||
return (
|
||||
<Box ref={body} role="group" aria-label={t("adminUsersTitle")} tabIndex={-1} aria-busy={busy} sx={{ display: "grid", gap: "var(--app-section-gap)", minWidth: 0, "& button": { whiteSpace: "normal", overflowWrap: "anywhere", "@media (pointer: coarse)": { minHeight: 44 } } }}>
|
||||
<PageHeader level="h2" title={t("adminUsersTitle")} description={t("adminUsersSubtitle")} />
|
||||
<Alert severity="info" sx={{ mb: 2 }}>{t("adminUsersSafetyNotice")}</Alert>
|
||||
|
||||
<PageSection level="h3" surface title={t("adminUsersCreateUser")}>
|
||||
<PageSection surface title={t("adminUsersCreateUser")}>
|
||||
<Box component="form" onSubmit={event => { event.preventDefault(); void createUser(); }}>
|
||||
<Box sx={{ display: "grid", gridTemplateColumns: { xs: "1fr", md: "1fr 1fr" }, gap: 1.5 }}>
|
||||
<TextField label={t("profileEmail")} value={newEmail} onChange={(e) => setNewEmail(e.target.value)} fullWidth />
|
||||
|
||||
Reference in New Issue
Block a user