mirror of
https://github.com/Frooodle/Stirling-PDF.git
synced 2026-01-14 20:11:17 +01:00
addressing comments
This commit is contained in:
parent
f1fec4e0d4
commit
a8a849e848
@ -4,7 +4,7 @@ import { useMediaQuery } from '@mantine/hooks';
|
||||
import { useNavigate, useLocation } from 'react-router-dom';
|
||||
import LocalIcon from '@app/components/shared/LocalIcon';
|
||||
import { createConfigNavSections } from '@app/components/shared/config/configNavSections';
|
||||
import { NavKey } from '@app/components/shared/config/types';
|
||||
import { NavKey, VALID_NAV_KEYS } from '@app/components/shared/config/types';
|
||||
import { useAppConfig } from '@app/contexts/AppConfigContext';
|
||||
import '@app/components/shared/AppConfigModal.css';
|
||||
import { Z_INDEX_OVER_FULLSCREEN_SURFACE, Z_INDEX_OVER_CONFIG_MODAL } from '@app/styles/zIndex';
|
||||
@ -25,14 +25,8 @@ const AppConfigModal: React.FC<AppConfigModalProps> = ({ opened, onClose }) => {
|
||||
const getSectionFromPath = (pathname: string): NavKey | null => {
|
||||
const match = pathname.match(/\/settings\/([^/]+)/);
|
||||
if (match && match[1]) {
|
||||
const validSections: NavKey[] = [
|
||||
'people', 'teams', 'general', 'hotkeys',
|
||||
'adminGeneral', 'adminSecurity', 'adminConnections', 'adminLegal',
|
||||
'adminPrivacy', 'adminDatabase', 'adminPremium', 'adminFeatures',
|
||||
'adminPlan', 'adminAudit', 'adminUsage', 'adminEndpoints', 'adminAdvanced'
|
||||
];
|
||||
const section = match[1] as NavKey;
|
||||
return validSections.includes(section) ? section : null;
|
||||
return VALID_NAV_KEYS.includes(section as any) ? section : null;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
@ -20,6 +20,13 @@ interface MailSettingsData {
|
||||
frontendUrl?: string;
|
||||
}
|
||||
|
||||
interface ApiResponseWithPending<T> {
|
||||
_pending?: Partial<T>;
|
||||
}
|
||||
|
||||
type MailApiResponse = MailSettingsData & ApiResponseWithPending<MailSettingsData>;
|
||||
type SystemApiResponse = { frontendUrl?: string } & ApiResponseWithPending<{ frontendUrl?: string }>;
|
||||
|
||||
export default function AdminMailSection() {
|
||||
const { t } = useTranslation();
|
||||
const { restartModalOpened, showRestartModal, closeRestartModal, restartServer } = useRestartServer();
|
||||
@ -36,20 +43,20 @@ export default function AdminMailSection() {
|
||||
sectionName: 'mail',
|
||||
fetchTransformer: async () => {
|
||||
const [mailResponse, systemResponse] = await Promise.all([
|
||||
apiClient.get('/api/v1/admin/settings/section/mail'),
|
||||
apiClient.get('/api/v1/admin/settings/section/system')
|
||||
apiClient.get<MailApiResponse>('/api/v1/admin/settings/section/mail'),
|
||||
apiClient.get<SystemApiResponse>('/api/v1/admin/settings/section/system')
|
||||
]);
|
||||
|
||||
const mail = mailResponse.data || {};
|
||||
const system = systemResponse.data || {};
|
||||
|
||||
const result: any = {
|
||||
const result: MailSettingsData & ApiResponseWithPending<MailSettingsData> = {
|
||||
...mail,
|
||||
frontendUrl: system.frontendUrl || ''
|
||||
};
|
||||
|
||||
// Merge pending blocks from both endpoints
|
||||
const pendingBlock: any = {};
|
||||
const pendingBlock: Partial<MailSettingsData> = {};
|
||||
if (mail._pending) {
|
||||
Object.assign(pendingBlock, mail._pending);
|
||||
}
|
||||
|
||||
@ -4,18 +4,19 @@ import {
|
||||
Text,
|
||||
Group,
|
||||
Stack,
|
||||
Select,
|
||||
Button,
|
||||
Pagination,
|
||||
Modal,
|
||||
Code,
|
||||
Loader,
|
||||
Alert,
|
||||
Table,
|
||||
} from '@mantine/core';
|
||||
import { DateInput } from '@mantine/dates';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import auditService, { AuditEvent, AuditFilters } from '@app/services/auditService';
|
||||
import auditService, { AuditEvent } from '@app/services/auditService';
|
||||
import { Z_INDEX_OVER_CONFIG_MODAL } from '@app/styles/zIndex';
|
||||
import { useAuditFilters } from '@app/hooks/useAuditFilters';
|
||||
import AuditFiltersForm from '@app/components/shared/config/configSections/audit/AuditFiltersForm';
|
||||
|
||||
const AuditEventsTable: React.FC = () => {
|
||||
const { t } = useTranslation();
|
||||
@ -25,36 +26,13 @@ const AuditEventsTable: React.FC = () => {
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [selectedEvent, setSelectedEvent] = useState<AuditEvent | null>(null);
|
||||
const [eventTypes, setEventTypes] = useState<string[]>([]);
|
||||
const [users, setUsers] = useState<string[]>([]);
|
||||
|
||||
// Filters
|
||||
const [filters, setFilters] = useState<AuditFilters>({
|
||||
eventType: undefined,
|
||||
username: undefined,
|
||||
startDate: undefined,
|
||||
endDate: undefined,
|
||||
// Use shared filters hook
|
||||
const { filters, eventTypes, users, handleFilterChange, handleClearFilters } = useAuditFilters({
|
||||
page: 0,
|
||||
pageSize: 20,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
const fetchMetadata = async () => {
|
||||
try {
|
||||
const [types, usersList] = await Promise.all([
|
||||
auditService.getEventTypes(),
|
||||
auditService.getUsers(),
|
||||
]);
|
||||
setEventTypes(types);
|
||||
setUsers(usersList);
|
||||
} catch (err) {
|
||||
console.error('Failed to fetch metadata:', err);
|
||||
}
|
||||
};
|
||||
|
||||
fetchMetadata();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchEvents = async () => {
|
||||
try {
|
||||
@ -76,20 +54,14 @@ const AuditEventsTable: React.FC = () => {
|
||||
fetchEvents();
|
||||
}, [filters, currentPage]);
|
||||
|
||||
const handleFilterChange = (key: keyof AuditFilters, value: any) => {
|
||||
setFilters((prev) => ({ ...prev, [key]: value }));
|
||||
// Wrap filter handlers to reset pagination
|
||||
const handleFilterChangeWithReset = (key: keyof typeof filters, value: any) => {
|
||||
handleFilterChange(key, value);
|
||||
setCurrentPage(1);
|
||||
};
|
||||
|
||||
const handleClearFilters = () => {
|
||||
setFilters({
|
||||
eventType: undefined,
|
||||
username: undefined,
|
||||
startDate: undefined,
|
||||
endDate: undefined,
|
||||
page: 0,
|
||||
pageSize: 20,
|
||||
});
|
||||
const handleClearFiltersWithReset = () => {
|
||||
handleClearFilters();
|
||||
setCurrentPage(1);
|
||||
};
|
||||
|
||||
@ -105,50 +77,13 @@ const AuditEventsTable: React.FC = () => {
|
||||
</Text>
|
||||
|
||||
{/* Filters */}
|
||||
<Group>
|
||||
<Select
|
||||
placeholder={t('audit.events.filterByType', 'Filter by type')}
|
||||
data={eventTypes.map((type) => ({ value: type, label: type }))}
|
||||
value={filters.eventType}
|
||||
onChange={(value) => handleFilterChange('eventType', value || undefined)}
|
||||
clearable
|
||||
style={{ flex: 1, minWidth: 200 }}
|
||||
comboboxProps={{ withinPortal: true, zIndex: Z_INDEX_OVER_CONFIG_MODAL }}
|
||||
/>
|
||||
<Select
|
||||
placeholder={t('audit.events.filterByUser', 'Filter by user')}
|
||||
data={users.map((user) => ({ value: user, label: user }))}
|
||||
value={filters.username}
|
||||
onChange={(value) => handleFilterChange('username', value || undefined)}
|
||||
clearable
|
||||
searchable
|
||||
style={{ flex: 1, minWidth: 200 }}
|
||||
comboboxProps={{ withinPortal: true, zIndex: Z_INDEX_OVER_CONFIG_MODAL }}
|
||||
/>
|
||||
<DateInput
|
||||
placeholder={t('audit.events.startDate', 'Start date')}
|
||||
value={filters.startDate ? new Date(filters.startDate) : null}
|
||||
onChange={(value: string | null) =>
|
||||
handleFilterChange('startDate', value ?? undefined)
|
||||
}
|
||||
clearable
|
||||
style={{ flex: 1, minWidth: 150 }}
|
||||
popoverProps={{ withinPortal: true, zIndex: Z_INDEX_OVER_CONFIG_MODAL }}
|
||||
/>
|
||||
<DateInput
|
||||
placeholder={t('audit.events.endDate', 'End date')}
|
||||
value={filters.endDate ? new Date(filters.endDate) : null}
|
||||
onChange={(value: string | null) =>
|
||||
handleFilterChange('endDate', value ?? undefined)
|
||||
}
|
||||
clearable
|
||||
style={{ flex: 1, minWidth: 150 }}
|
||||
popoverProps={{ withinPortal: true, zIndex: Z_INDEX_OVER_CONFIG_MODAL }}
|
||||
/>
|
||||
<Button variant="outline" onClick={handleClearFilters}>
|
||||
{t('audit.events.clearFilters', 'Clear')}
|
||||
</Button>
|
||||
</Group>
|
||||
<AuditFiltersForm
|
||||
filters={filters}
|
||||
eventTypes={eventTypes}
|
||||
users={users}
|
||||
onFilterChange={handleFilterChangeWithReset}
|
||||
onClearFilters={handleClearFiltersWithReset}
|
||||
/>
|
||||
|
||||
{/* Table */}
|
||||
{loading ? (
|
||||
@ -161,81 +96,72 @@ const AuditEventsTable: React.FC = () => {
|
||||
</Alert>
|
||||
) : (
|
||||
<>
|
||||
<div style={{ overflowX: 'auto' }}>
|
||||
<table style={{ width: '100%', borderCollapse: 'collapse' }}>
|
||||
<thead>
|
||||
<tr
|
||||
style={{
|
||||
borderBottom: '2px solid var(--mantine-color-gray-3)',
|
||||
}}
|
||||
>
|
||||
<th style={{ textAlign: 'left' }}>
|
||||
{t('audit.events.timestamp', 'Timestamp')}
|
||||
</th>
|
||||
<th style={{ textAlign: 'left' }}>
|
||||
{t('audit.events.type', 'Type')}
|
||||
</th>
|
||||
<th style={{ textAlign: 'left' }}>
|
||||
{t('audit.events.user', 'User')}
|
||||
</th>
|
||||
<th style={{ textAlign: 'left' }}>
|
||||
{t('audit.events.ipAddress', 'IP Address')}
|
||||
</th>
|
||||
<th style={{ textAlign: 'center' }}>
|
||||
{t('audit.events.actions', 'Actions')}
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{events.length === 0 ? (
|
||||
<tr>
|
||||
<td colSpan={5} style={{ textAlign: 'center', padding: '2rem' }}>
|
||||
<Text c="dimmed">{t('audit.events.noEvents', 'No events found')}</Text>
|
||||
</td>
|
||||
</tr>
|
||||
) : (
|
||||
events.map((event) => (
|
||||
<tr
|
||||
key={event.id}
|
||||
style={{
|
||||
borderBottom: '1px solid var(--mantine-color-gray-2)',
|
||||
cursor: 'pointer',
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
e.currentTarget.style.backgroundColor =
|
||||
'var(--mantine-color-gray-0)';
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
e.currentTarget.style.backgroundColor = 'transparent';
|
||||
}}
|
||||
>
|
||||
<td>
|
||||
<Text size="sm">{formatDate(event.timestamp)}</Text>
|
||||
</td>
|
||||
<td>
|
||||
<Text size="sm">{event.eventType}</Text>
|
||||
</td>
|
||||
<td>
|
||||
<Text size="sm">{event.username}</Text>
|
||||
</td>
|
||||
<td>
|
||||
<Text size="sm">{event.ipAddress}</Text>
|
||||
</td>
|
||||
<td style={{ textAlign: 'center' }}>
|
||||
<Button
|
||||
variant="subtle"
|
||||
size="xs"
|
||||
onClick={() => setSelectedEvent(event)}
|
||||
>
|
||||
{t('audit.events.viewDetails', 'View Details')}
|
||||
</Button>
|
||||
</td>
|
||||
</tr>
|
||||
))
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<Table
|
||||
horizontalSpacing="md"
|
||||
verticalSpacing="sm"
|
||||
withRowBorders
|
||||
highlightOnHover
|
||||
style={{
|
||||
'--table-border-color': 'var(--mantine-color-gray-3)',
|
||||
} as React.CSSProperties}
|
||||
>
|
||||
<Table.Thead>
|
||||
<Table.Tr style={{ backgroundColor: 'var(--mantine-color-gray-0)' }}>
|
||||
<Table.Th style={{ fontWeight: 600, color: 'var(--mantine-color-gray-7)' }} fz="sm">
|
||||
{t('audit.events.timestamp', 'Timestamp')}
|
||||
</Table.Th>
|
||||
<Table.Th style={{ fontWeight: 600, color: 'var(--mantine-color-gray-7)' }} fz="sm">
|
||||
{t('audit.events.type', 'Type')}
|
||||
</Table.Th>
|
||||
<Table.Th style={{ fontWeight: 600, color: 'var(--mantine-color-gray-7)' }} fz="sm">
|
||||
{t('audit.events.user', 'User')}
|
||||
</Table.Th>
|
||||
<Table.Th style={{ fontWeight: 600, color: 'var(--mantine-color-gray-7)' }} fz="sm">
|
||||
{t('audit.events.ipAddress', 'IP Address')}
|
||||
</Table.Th>
|
||||
<Table.Th style={{ fontWeight: 600, color: 'var(--mantine-color-gray-7)' }} fz="sm" ta="center">
|
||||
{t('audit.events.actions', 'Actions')}
|
||||
</Table.Th>
|
||||
</Table.Tr>
|
||||
</Table.Thead>
|
||||
<Table.Tbody>
|
||||
{events.length === 0 ? (
|
||||
<Table.Tr>
|
||||
<Table.Td colSpan={5}>
|
||||
<Text ta="center" c="dimmed" py="xl">
|
||||
{t('audit.events.noEvents', 'No events found')}
|
||||
</Text>
|
||||
</Table.Td>
|
||||
</Table.Tr>
|
||||
) : (
|
||||
events.map((event) => (
|
||||
<Table.Tr key={event.id}>
|
||||
<Table.Td>
|
||||
<Text size="sm">{formatDate(event.timestamp)}</Text>
|
||||
</Table.Td>
|
||||
<Table.Td>
|
||||
<Text size="sm">{event.eventType}</Text>
|
||||
</Table.Td>
|
||||
<Table.Td>
|
||||
<Text size="sm">{event.username}</Text>
|
||||
</Table.Td>
|
||||
<Table.Td>
|
||||
<Text size="sm">{event.ipAddress}</Text>
|
||||
</Table.Td>
|
||||
<Table.Td ta="center">
|
||||
<Button
|
||||
variant="subtle"
|
||||
size="xs"
|
||||
onClick={() => setSelectedEvent(event)}
|
||||
>
|
||||
{t('audit.events.viewDetails', 'View Details')}
|
||||
</Button>
|
||||
</Table.Td>
|
||||
</Table.Tr>
|
||||
))
|
||||
)}
|
||||
</Table.Tbody>
|
||||
</Table>
|
||||
|
||||
{/* Pagination */}
|
||||
{totalPages > 1 && (
|
||||
|
||||
@ -4,47 +4,22 @@ import {
|
||||
Text,
|
||||
Group,
|
||||
Stack,
|
||||
Select,
|
||||
Button,
|
||||
SegmentedControl,
|
||||
} from '@mantine/core';
|
||||
import { DateInput } from '@mantine/dates';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import auditService, { AuditFilters } from '@app/services/auditService';
|
||||
import auditService from '@app/services/auditService';
|
||||
import LocalIcon from '@app/components/shared/LocalIcon';
|
||||
import { Z_INDEX_OVER_CONFIG_MODAL } from '@app/styles/zIndex';
|
||||
import { useAuditFilters } from '@app/hooks/useAuditFilters';
|
||||
import AuditFiltersForm from '@app/components/shared/config/configSections/audit/AuditFiltersForm';
|
||||
|
||||
const AuditExportSection: React.FC = () => {
|
||||
const { t } = useTranslation();
|
||||
const [exportFormat, setExportFormat] = useState<'csv' | 'json'>('csv');
|
||||
const [exporting, setExporting] = useState(false);
|
||||
const [eventTypes, setEventTypes] = useState<string[]>([]);
|
||||
const [users, setUsers] = useState<string[]>([]);
|
||||
|
||||
// Filters for export
|
||||
const [filters, setFilters] = useState<AuditFilters>({
|
||||
eventType: undefined,
|
||||
username: undefined,
|
||||
startDate: undefined,
|
||||
endDate: undefined,
|
||||
});
|
||||
|
||||
React.useEffect(() => {
|
||||
const fetchMetadata = async () => {
|
||||
try {
|
||||
const [types, usersList] = await Promise.all([
|
||||
auditService.getEventTypes(),
|
||||
auditService.getUsers(),
|
||||
]);
|
||||
setEventTypes(types);
|
||||
setUsers(usersList);
|
||||
} catch (err) {
|
||||
console.error('Failed to fetch metadata:', err);
|
||||
}
|
||||
};
|
||||
|
||||
fetchMetadata();
|
||||
}, []);
|
||||
// Use shared filters hook
|
||||
const { filters, eventTypes, users, handleFilterChange, handleClearFilters } = useAuditFilters();
|
||||
|
||||
const handleExport = async () => {
|
||||
try {
|
||||
@ -69,19 +44,6 @@ const AuditExportSection: React.FC = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const handleFilterChange = (key: keyof AuditFilters, value: any) => {
|
||||
setFilters((prev) => ({ ...prev, [key]: value }));
|
||||
};
|
||||
|
||||
const handleClearFilters = () => {
|
||||
setFilters({
|
||||
eventType: undefined,
|
||||
username: undefined,
|
||||
startDate: undefined,
|
||||
endDate: undefined,
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Card padding="lg" radius="md" withBorder>
|
||||
<Stack gap="md">
|
||||
@ -116,54 +78,13 @@ const AuditExportSection: React.FC = () => {
|
||||
<Text size="sm" fw={600} mb="xs">
|
||||
{t('audit.export.filters', 'Filters (Optional)')}
|
||||
</Text>
|
||||
<Stack gap="sm">
|
||||
<Group>
|
||||
<Select
|
||||
placeholder={t('audit.export.filterByType', 'Filter by type')}
|
||||
data={eventTypes.map((type) => ({ value: type, label: type }))}
|
||||
value={filters.eventType}
|
||||
onChange={(value) => handleFilterChange('eventType', value || undefined)}
|
||||
clearable
|
||||
style={{ flex: 1, minWidth: 200 }}
|
||||
comboboxProps={{ withinPortal: true, zIndex: Z_INDEX_OVER_CONFIG_MODAL }}
|
||||
/>
|
||||
<Select
|
||||
placeholder={t('audit.export.filterByUser', 'Filter by user')}
|
||||
data={users.map((user) => ({ value: user, label: user }))}
|
||||
value={filters.username}
|
||||
onChange={(value) => handleFilterChange('username', value || undefined)}
|
||||
clearable
|
||||
searchable
|
||||
style={{ flex: 1, minWidth: 200 }}
|
||||
comboboxProps={{ withinPortal: true, zIndex: Z_INDEX_OVER_CONFIG_MODAL }}
|
||||
/>
|
||||
</Group>
|
||||
<Group>
|
||||
<DateInput
|
||||
placeholder={t('audit.export.startDate', 'Start date')}
|
||||
value={filters.startDate ? new Date(filters.startDate) : null}
|
||||
onChange={(value: string | null) =>
|
||||
handleFilterChange('startDate', value ?? undefined)
|
||||
}
|
||||
clearable
|
||||
style={{ flex: 1, minWidth: 200 }}
|
||||
popoverProps={{ withinPortal: true, zIndex: Z_INDEX_OVER_CONFIG_MODAL }}
|
||||
/>
|
||||
<DateInput
|
||||
placeholder={t('audit.export.endDate', 'End date')}
|
||||
value={filters.endDate ? new Date(filters.endDate) : null}
|
||||
onChange={(value: string | null) =>
|
||||
handleFilterChange('endDate', value ?? undefined)
|
||||
}
|
||||
clearable
|
||||
style={{ flex: 1, minWidth: 200 }}
|
||||
popoverProps={{ withinPortal: true, zIndex: Z_INDEX_OVER_CONFIG_MODAL }}
|
||||
/>
|
||||
<Button variant="outline" onClick={handleClearFilters}>
|
||||
{t('audit.export.clearFilters', 'Clear')}
|
||||
</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
<AuditFiltersForm
|
||||
filters={filters}
|
||||
eventTypes={eventTypes}
|
||||
users={users}
|
||||
onFilterChange={handleFilterChange}
|
||||
onClearFilters={handleClearFilters}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Export Button */}
|
||||
|
||||
@ -0,0 +1,76 @@
|
||||
import React from 'react';
|
||||
import { Group, Select, Button } from '@mantine/core';
|
||||
import { DateInput } from '@mantine/dates';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { AuditFilters } from '@app/services/auditService';
|
||||
import { Z_INDEX_OVER_CONFIG_MODAL } from '@app/styles/zIndex';
|
||||
|
||||
interface AuditFiltersFormProps {
|
||||
filters: AuditFilters;
|
||||
eventTypes: string[];
|
||||
users: string[];
|
||||
onFilterChange: (key: keyof AuditFilters, value: any) => void;
|
||||
onClearFilters: () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Shared filter form for audit components
|
||||
*/
|
||||
const AuditFiltersForm: React.FC<AuditFiltersFormProps> = ({
|
||||
filters,
|
||||
eventTypes,
|
||||
users,
|
||||
onFilterChange,
|
||||
onClearFilters,
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Group>
|
||||
<Select
|
||||
placeholder={t('audit.events.filterByType', 'Filter by type')}
|
||||
data={eventTypes.map((type) => ({ value: type, label: type }))}
|
||||
value={filters.eventType}
|
||||
onChange={(value) => onFilterChange('eventType', value || undefined)}
|
||||
clearable
|
||||
style={{ flex: 1, minWidth: 200 }}
|
||||
comboboxProps={{ withinPortal: true, zIndex: Z_INDEX_OVER_CONFIG_MODAL }}
|
||||
/>
|
||||
<Select
|
||||
placeholder={t('audit.events.filterByUser', 'Filter by user')}
|
||||
data={users.map((user) => ({ value: user, label: user }))}
|
||||
value={filters.username}
|
||||
onChange={(value) => onFilterChange('username', value || undefined)}
|
||||
clearable
|
||||
searchable
|
||||
style={{ flex: 1, minWidth: 200 }}
|
||||
comboboxProps={{ withinPortal: true, zIndex: Z_INDEX_OVER_CONFIG_MODAL }}
|
||||
/>
|
||||
<DateInput
|
||||
placeholder={t('audit.events.startDate', 'Start date')}
|
||||
value={filters.startDate ? new Date(filters.startDate) : null}
|
||||
onChange={(value: string | null) =>
|
||||
onFilterChange('startDate', value ?? undefined)
|
||||
}
|
||||
clearable
|
||||
style={{ flex: 1, minWidth: 150 }}
|
||||
popoverProps={{ withinPortal: true, zIndex: Z_INDEX_OVER_CONFIG_MODAL }}
|
||||
/>
|
||||
<DateInput
|
||||
placeholder={t('audit.events.endDate', 'End date')}
|
||||
value={filters.endDate ? new Date(filters.endDate) : null}
|
||||
onChange={(value: string | null) =>
|
||||
onFilterChange('endDate', value ?? undefined)
|
||||
}
|
||||
clearable
|
||||
style={{ flex: 1, minWidth: 150 }}
|
||||
popoverProps={{ withinPortal: true, zIndex: Z_INDEX_OVER_CONFIG_MODAL }}
|
||||
/>
|
||||
<Button variant="outline" onClick={onClearFilters}>
|
||||
{t('audit.events.clearFilters', 'Clear')}
|
||||
</Button>
|
||||
</Group>
|
||||
);
|
||||
};
|
||||
|
||||
export default AuditFiltersForm;
|
||||
@ -1,5 +1,5 @@
|
||||
import React from 'react';
|
||||
import { Card, Text, Stack } from '@mantine/core';
|
||||
import { Card, Text, Stack, Table } from '@mantine/core';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { EndpointStatistic } from '@app/services/usageAnalyticsService';
|
||||
|
||||
@ -17,106 +17,68 @@ const UsageAnalyticsTable: React.FC<UsageAnalyticsTableProps> = ({ data }) => {
|
||||
{t('usage.table.title', 'Detailed Statistics')}
|
||||
</Text>
|
||||
|
||||
<div style={{ overflowX: 'auto' }}>
|
||||
<table style={{ width: '100%', borderCollapse: 'collapse' }}>
|
||||
<thead>
|
||||
<tr
|
||||
style={{
|
||||
borderBottom: '2px solid var(--mantine-color-gray-3)',
|
||||
}}
|
||||
>
|
||||
<th
|
||||
style={{
|
||||
textAlign: 'left',
|
||||
fontSize: '0.875rem',
|
||||
width: '5%',
|
||||
}}
|
||||
>
|
||||
#
|
||||
</th>
|
||||
<th
|
||||
style={{
|
||||
textAlign: 'left',
|
||||
fontSize: '0.875rem',
|
||||
width: '55%',
|
||||
}}
|
||||
>
|
||||
{t('usage.table.endpoint', 'Endpoint')}
|
||||
</th>
|
||||
<th
|
||||
style={{
|
||||
textAlign: 'right',
|
||||
fontSize: '0.875rem',
|
||||
width: '20%',
|
||||
}}
|
||||
>
|
||||
{t('usage.table.visits', 'Visits')}
|
||||
</th>
|
||||
<th
|
||||
style={{
|
||||
textAlign: 'right',
|
||||
fontSize: '0.875rem',
|
||||
width: '20%',
|
||||
}}
|
||||
>
|
||||
{t('usage.table.percentage', 'Percentage')}
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{data.length === 0 ? (
|
||||
<tr>
|
||||
<td colSpan={4} style={{ textAlign: 'center', padding: '2rem' }}>
|
||||
<Text c="dimmed">{t('usage.table.noData', 'No data available')}</Text>
|
||||
</td>
|
||||
</tr>
|
||||
) : (
|
||||
data.map((stat, index) => (
|
||||
<tr
|
||||
key={index}
|
||||
style={{
|
||||
borderBottom: '1px solid var(--mantine-color-gray-2)',
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
e.currentTarget.style.backgroundColor = 'var(--mantine-color-gray-0)';
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
e.currentTarget.style.backgroundColor = 'transparent';
|
||||
}}
|
||||
>
|
||||
<td>
|
||||
<Text size="sm" c="dimmed">
|
||||
{index + 1}
|
||||
</Text>
|
||||
</td>
|
||||
<td>
|
||||
<Text
|
||||
size="sm"
|
||||
style={{
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap',
|
||||
}}
|
||||
>
|
||||
{stat.endpoint}
|
||||
</Text>
|
||||
</td>
|
||||
<td style={{ textAlign: 'right' }}>
|
||||
<Text size="sm" fw={600}>
|
||||
{stat.visits.toLocaleString()}
|
||||
</Text>
|
||||
</td>
|
||||
<td style={{ textAlign: 'right' }}>
|
||||
<Text size="sm" c="dimmed">
|
||||
{stat.percentage.toFixed(2)}%
|
||||
</Text>
|
||||
</td>
|
||||
</tr>
|
||||
))
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<Table
|
||||
horizontalSpacing="md"
|
||||
verticalSpacing="sm"
|
||||
withRowBorders
|
||||
highlightOnHover
|
||||
style={{
|
||||
'--table-border-color': 'var(--mantine-color-gray-3)',
|
||||
} as React.CSSProperties}
|
||||
>
|
||||
<Table.Thead>
|
||||
<Table.Tr style={{ backgroundColor: 'var(--mantine-color-gray-0)' }}>
|
||||
<Table.Th style={{ fontWeight: 600, color: 'var(--mantine-color-gray-7)' }} fz="sm" w="5%">
|
||||
#
|
||||
</Table.Th>
|
||||
<Table.Th style={{ fontWeight: 600, color: 'var(--mantine-color-gray-7)' }} fz="sm" w="55%">
|
||||
{t('usage.table.endpoint', 'Endpoint')}
|
||||
</Table.Th>
|
||||
<Table.Th style={{ fontWeight: 600, color: 'var(--mantine-color-gray-7)' }} fz="sm" w="20%" ta="right">
|
||||
{t('usage.table.visits', 'Visits')}
|
||||
</Table.Th>
|
||||
<Table.Th style={{ fontWeight: 600, color: 'var(--mantine-color-gray-7)' }} fz="sm" w="20%" ta="right">
|
||||
{t('usage.table.percentage', 'Percentage')}
|
||||
</Table.Th>
|
||||
</Table.Tr>
|
||||
</Table.Thead>
|
||||
<Table.Tbody>
|
||||
{data.length === 0 ? (
|
||||
<Table.Tr>
|
||||
<Table.Td colSpan={4}>
|
||||
<Text ta="center" c="dimmed" py="xl">
|
||||
{t('usage.table.noData', 'No data available')}
|
||||
</Text>
|
||||
</Table.Td>
|
||||
</Table.Tr>
|
||||
) : (
|
||||
data.map((stat, index) => (
|
||||
<Table.Tr key={index}>
|
||||
<Table.Td>
|
||||
<Text size="sm" c="dimmed">
|
||||
{index + 1}
|
||||
</Text>
|
||||
</Table.Td>
|
||||
<Table.Td>
|
||||
<Text size="sm" truncate>
|
||||
{stat.endpoint}
|
||||
</Text>
|
||||
</Table.Td>
|
||||
<Table.Td ta="right">
|
||||
<Text size="sm" fw={600}>
|
||||
{stat.visits.toLocaleString()}
|
||||
</Text>
|
||||
</Table.Td>
|
||||
<Table.Td ta="right">
|
||||
<Text size="sm" c="dimmed">
|
||||
{stat.percentage.toFixed(2)}%
|
||||
</Text>
|
||||
</Table.Td>
|
||||
</Table.Tr>
|
||||
))
|
||||
)}
|
||||
</Table.Tbody>
|
||||
</Table>
|
||||
</Stack>
|
||||
</Card>
|
||||
);
|
||||
|
||||
@ -1,31 +1,35 @@
|
||||
export type NavKey =
|
||||
| 'preferences'
|
||||
| 'notifications'
|
||||
| 'connections'
|
||||
| 'general'
|
||||
| 'people'
|
||||
| 'teams'
|
||||
| 'security'
|
||||
| 'identity'
|
||||
| 'plan'
|
||||
| 'payments'
|
||||
| 'requests'
|
||||
| 'developer'
|
||||
| 'api-keys'
|
||||
| 'hotkeys'
|
||||
| 'adminGeneral'
|
||||
| 'adminSecurity'
|
||||
| 'adminConnections'
|
||||
| 'adminPrivacy'
|
||||
| 'adminDatabase'
|
||||
| 'adminAdvanced'
|
||||
| 'adminLegal'
|
||||
| 'adminPremium'
|
||||
| 'adminFeatures'
|
||||
| 'adminPlan'
|
||||
| 'adminAudit'
|
||||
| 'adminUsage'
|
||||
| 'adminEndpoints';
|
||||
// Single source of truth for all valid nav keys
|
||||
export const VALID_NAV_KEYS = [
|
||||
'preferences',
|
||||
'notifications',
|
||||
'connections',
|
||||
'general',
|
||||
'people',
|
||||
'teams',
|
||||
'security',
|
||||
'identity',
|
||||
'plan',
|
||||
'payments',
|
||||
'requests',
|
||||
'developer',
|
||||
'api-keys',
|
||||
'hotkeys',
|
||||
'adminGeneral',
|
||||
'adminSecurity',
|
||||
'adminConnections',
|
||||
'adminPrivacy',
|
||||
'adminDatabase',
|
||||
'adminAdvanced',
|
||||
'adminLegal',
|
||||
'adminPremium',
|
||||
'adminFeatures',
|
||||
'adminPlan',
|
||||
'adminAudit',
|
||||
'adminUsage',
|
||||
'adminEndpoints',
|
||||
] as const;
|
||||
|
||||
// Derive the type from the array
|
||||
export type NavKey = typeof VALID_NAV_KEYS[number];
|
||||
|
||||
// some of these are not used yet, but appear in figma designs
|
||||
// some of these are not used yet, but appear in figma designs
|
||||
59
frontend/src/core/hooks/useAuditFilters.ts
Normal file
59
frontend/src/core/hooks/useAuditFilters.ts
Normal file
@ -0,0 +1,59 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import auditService, { AuditFilters } from '@app/services/auditService';
|
||||
|
||||
/**
|
||||
* Shared hook for managing audit filters across components
|
||||
*/
|
||||
export function useAuditFilters(initialFilters: Partial<AuditFilters> = {}) {
|
||||
const [eventTypes, setEventTypes] = useState<string[]>([]);
|
||||
const [users, setUsers] = useState<string[]>([]);
|
||||
const [filters, setFilters] = useState<AuditFilters>({
|
||||
eventType: undefined,
|
||||
username: undefined,
|
||||
startDate: undefined,
|
||||
endDate: undefined,
|
||||
...initialFilters,
|
||||
});
|
||||
|
||||
// Fetch metadata on mount
|
||||
useEffect(() => {
|
||||
const fetchMetadata = async () => {
|
||||
try {
|
||||
const [types, usersList] = await Promise.all([
|
||||
auditService.getEventTypes(),
|
||||
auditService.getUsers(),
|
||||
]);
|
||||
setEventTypes(types);
|
||||
setUsers(usersList);
|
||||
} catch (err) {
|
||||
console.error('Failed to fetch audit metadata:', err);
|
||||
}
|
||||
};
|
||||
|
||||
fetchMetadata();
|
||||
}, []);
|
||||
|
||||
const handleFilterChange = (key: keyof AuditFilters, value: any) => {
|
||||
setFilters((prev) => ({ ...prev, [key]: value }));
|
||||
};
|
||||
|
||||
const handleClearFilters = () => {
|
||||
setFilters({
|
||||
eventType: undefined,
|
||||
username: undefined,
|
||||
startDate: undefined,
|
||||
endDate: undefined,
|
||||
page: initialFilters.page,
|
||||
pageSize: initialFilters.pageSize,
|
||||
});
|
||||
};
|
||||
|
||||
return {
|
||||
filters,
|
||||
setFilters,
|
||||
eventTypes,
|
||||
users,
|
||||
handleFilterChange,
|
||||
handleClearFilters,
|
||||
};
|
||||
}
|
||||
@ -13,27 +13,29 @@ export const createConfigNavSections = (
|
||||
// Get the core sections
|
||||
const sections = createCoreConfigNavSections(isAdmin, runningEE);
|
||||
|
||||
// Add Workspace section after Preferences (index 1)
|
||||
const workspaceSection: ConfigNavSection = {
|
||||
title: 'Workspace',
|
||||
items: [
|
||||
{
|
||||
key: 'people',
|
||||
label: 'People',
|
||||
icon: 'group-rounded',
|
||||
component: <PeopleSection />
|
||||
},
|
||||
{
|
||||
key: 'teams',
|
||||
label: 'Teams',
|
||||
icon: 'groups-rounded',
|
||||
component: <TeamsSection />
|
||||
},
|
||||
],
|
||||
};
|
||||
// Add Workspace section if user is admin
|
||||
if (isAdmin) {
|
||||
const workspaceSection: ConfigNavSection = {
|
||||
title: 'Workspace',
|
||||
items: [
|
||||
{
|
||||
key: 'people',
|
||||
label: 'People',
|
||||
icon: 'group-rounded',
|
||||
component: <PeopleSection />
|
||||
},
|
||||
{
|
||||
key: 'teams',
|
||||
label: 'Teams',
|
||||
icon: 'groups-rounded',
|
||||
component: <TeamsSection />
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
// Insert workspace section after Preferences (at index 1)
|
||||
sections.splice(1, 0, workspaceSection);
|
||||
// Insert workspace section after Preferences (at index 1)
|
||||
sections.splice(1, 0, workspaceSection);
|
||||
}
|
||||
|
||||
return sections;
|
||||
};
|
||||
|
||||
Loading…
Reference in New Issue
Block a user