addressing comments

This commit is contained in:
Anthony Stirling 2025-11-05 18:12:58 +00:00
parent f1fec4e0d4
commit a8a849e848
9 changed files with 362 additions and 411 deletions

View File

@ -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;
};

View File

@ -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);
}

View File

@ -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 && (

View File

@ -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 */}

View File

@ -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;

View File

@ -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>
);

View File

@ -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

View 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,
};
}

View File

@ -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;
};