Files
Stirling-PDF/frontend/src/proprietary/components/shared/config/configSections/AdminAuditSection.tsx
T
James BruntonandGitHub 8ab060a4be Prettier 2: Electric Boogaloo (#6113)
# Description of Changes
When I added Prettier formatting in #6052, my aim was to use just the
default settings in Prettier. Turns out, Prettier looks _really hard_
for any config files if it's not explicitly given one, which means that
if a developer has some sort of Prettier config file lying around on
their system, Prettier might find it and use it. Also, Prettier changes
its defaults based on stuff in `.editorconfig` without any good way of
disabling that behaviour explicitly in its config file.

To solve both of these issues, I've introduced a `.prettierrc` file
which sets Prettier's defaults explicitly, and then reformatted all our
code _again_ in Prettier's actual default settings. This should achieve
the aim of #6052 and remove the possibility for it breaking on different
dev computers.
2026-04-17 09:50:16 +00:00

268 lines
8.6 KiB
TypeScript

import React, { useState, useEffect } from "react";
import { isAxiosError } from "axios";
import {
Tabs,
Loader,
Alert,
Stack,
Text,
Button,
Accordion,
} from "@mantine/core";
import { useTranslation } from "react-i18next";
import { useNavigate } from "react-router-dom";
import auditService, {
AuditSystemStatus as AuditStatus,
} from "@app/services/auditService";
import AuditSystemStatus from "@app/components/shared/config/configSections/audit/AuditSystemStatus";
import AuditStatsCards from "@app/components/shared/config/configSections/audit/AuditStatsCards";
import AuditChartsSection from "@app/components/shared/config/configSections/audit/AuditChartsSection";
import AuditEventsTable from "@app/components/shared/config/configSections/audit/AuditEventsTable";
import AuditExportSection from "@app/components/shared/config/configSections/audit/AuditExportSection";
import AuditClearDataSection from "@app/components/shared/config/configSections/audit/AuditClearDataSection";
import { useLoginRequired } from "@app/hooks/useLoginRequired";
import LoginRequiredBanner from "@app/components/shared/config/LoginRequiredBanner";
import { useAppConfig } from "@app/contexts/AppConfigContext";
import EnterpriseRequiredBanner from "@app/components/shared/config/EnterpriseRequiredBanner";
import LocalIcon from "@app/components/shared/LocalIcon";
const AdminAuditSection: React.FC = () => {
const { t } = useTranslation();
const navigate = useNavigate();
const { loginEnabled } = useLoginRequired();
const { config } = useAppConfig();
const licenseType = config?.license ?? "NORMAL";
const hasEnterpriseLicense = licenseType === "ENTERPRISE";
const showDemoData = !loginEnabled || !hasEnterpriseLicense;
const [systemStatus, setSystemStatus] = useState<AuditStatus | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [timePeriod, setTimePeriod] = useState<"day" | "week" | "month">(
"week",
);
useEffect(() => {
const fetchSystemStatus = async () => {
try {
setLoading(true);
setError(null);
const status = await auditService.getSystemStatus();
setSystemStatus(status);
} catch (err: unknown) {
// Check if this is a permission/license error (403/404)
const status = isAxiosError(err) ? err.response?.status : undefined;
if (status === 403 || status === 404) {
setError("enterprise-license-required");
} else {
setError(
err instanceof Error
? err.message
: "Failed to load audit system status",
);
}
} finally {
setLoading(false);
}
};
if (!showDemoData) {
fetchSystemStatus();
} else {
// Provide example audit system status when running in demo mode
setError(null);
setSystemStatus({
enabled: true,
level: "INFO",
retentionDays: 90,
totalEvents: 1234,
pdfMetadataEnabled: true,
captureFileHash: true,
capturePdfAuthor: true,
captureOperationResults: false,
});
setLoading(false);
}
}, [loginEnabled, showDemoData]);
// Override loading state when showing demo data
const actualLoading = showDemoData ? false : loading;
if (actualLoading) {
return (
<div
style={{
display: "flex",
justifyContent: "center",
alignItems: "center",
padding: "2rem 0",
}}
>
<Loader size="lg" />
</div>
);
}
if (error) {
if (error === "enterprise-license-required") {
return (
<Alert
color="blue"
title={t("audit.enterpriseRequired", "Enterprise License Required")}
>
{t(
"audit.enterpriseRequiredMessage",
"The audit logging system is an enterprise feature. Please upgrade to an enterprise license to access audit logs and analytics.",
)}
</Alert>
);
}
return (
<Alert
color="red"
title={t("audit.error.title", "Error loading audit system")}
>
{error}
</Alert>
);
}
if (!systemStatus) {
return (
<Alert
color="yellow"
title={t("audit.notAvailable", "Audit system not available")}
>
{t(
"audit.notAvailableMessage",
"The audit system is not configured or not available.",
)}
</Alert>
);
}
const isEnabled = loginEnabled && hasEnterpriseLicense;
return (
<Stack gap="lg">
<LoginRequiredBanner show={!loginEnabled} />
<EnterpriseRequiredBanner
show={!hasEnterpriseLicense}
featureName={t("settings.licensingAnalytics.audit", "Audit")}
/>
{/* Info banner about audit settings */}
{isEnabled && (
<Alert
icon={<LocalIcon icon="info" width="1.2rem" height="1.2rem" />}
title={t("audit.configureAudit", "Configure Audit Logging")}
color="blue"
variant="light"
>
<Stack gap="xs">
<Text size="sm">
{t(
"audit.configureAuditMessage",
"Adjust audit logging level, retention period, and other settings in the Security & Authentication section.",
)}
</Text>
<Button
variant="light"
size="xs"
onClick={() => navigate("/settings/adminSecurity#auditLogging")}
rightSection={
<LocalIcon
icon="arrow-forward"
width="0.9rem"
height="0.9rem"
/>
}
>
{t("audit.goToSettings", "Go to Audit Settings")}
</Button>
</Stack>
</Alert>
)}
<AuditSystemStatus status={systemStatus} />
{systemStatus?.enabled ? (
<Tabs defaultValue="dashboard">
<Tabs.List>
<Tabs.Tab value="dashboard" disabled={!isEnabled}>
{t("audit.tabs.dashboard", "Dashboard")}
</Tabs.Tab>
<Tabs.Tab value="events" disabled={!isEnabled}>
{t("audit.tabs.events", "Audit Events")}
</Tabs.Tab>
<Tabs.Tab value="export" disabled={!isEnabled}>
{t("audit.tabs.export", "Export")}
</Tabs.Tab>
<Tabs.Tab value="clearData" disabled={!isEnabled}>
{t("audit.tabs.clearData", "Clear Data")}
</Tabs.Tab>
</Tabs.List>
<Tabs.Panel value="dashboard" pt="md">
<Stack gap="lg">
{/* Stats Cards - Always Visible */}
<AuditStatsCards
loginEnabled={isEnabled}
timePeriod={timePeriod}
/>
{/* Charts in Accordion - Collapsible */}
<Accordion defaultValue={["events-over-time"]} multiple>
<Accordion.Item value="events-over-time">
<Accordion.Control>
{t("audit.charts.overTime", "Events Over Time")}
</Accordion.Control>
<Accordion.Panel>
<AuditChartsSection
loginEnabled={isEnabled}
timePeriod={timePeriod}
onTimePeriodChange={setTimePeriod}
/>
</Accordion.Panel>
</Accordion.Item>
</Accordion>
</Stack>
</Tabs.Panel>
<Tabs.Panel value="events" pt="md">
<AuditEventsTable
loginEnabled={isEnabled}
captureFileHash={systemStatus?.captureFileHash}
capturePdfAuthor={systemStatus?.capturePdfAuthor}
/>
</Tabs.Panel>
<Tabs.Panel value="export" pt="md">
<AuditExportSection
loginEnabled={isEnabled}
captureFileHash={systemStatus?.captureFileHash}
capturePdfAuthor={systemStatus?.capturePdfAuthor}
captureOperationResults={systemStatus?.captureOperationResults}
/>
</Tabs.Panel>
<Tabs.Panel value="clearData" pt="md">
<AuditClearDataSection loginEnabled={isEnabled} />
</Tabs.Panel>
</Tabs>
) : (
<Alert
color="blue"
title={t("audit.disabled", "Audit logging is disabled")}
>
{t(
"audit.disabledMessage",
"Enable audit logging in your application configuration to track system events.",
)}
</Alert>
)}
</Stack>
);
};
export default AdminAuditSection;