From a8a849e848139d93aa99edd303b1610e49e8ea88 Mon Sep 17 00:00:00 2001 From: Anthony Stirling <77850077+Frooodle@users.noreply.github.com> Date: Wed, 5 Nov 2025 18:12:58 +0000 Subject: [PATCH] addressing comments --- .../core/components/shared/AppConfigModal.tsx | 10 +- .../configSections/AdminMailSection.tsx | 15 +- .../configSections/audit/AuditEventsTable.tsx | 242 ++++++------------ .../audit/AuditExportSection.tsx | 103 +------- .../configSections/audit/AuditFiltersForm.tsx | 76 ++++++ .../usage/UsageAnalyticsTable.tsx | 164 +++++------- .../core/components/shared/config/types.ts | 62 ++--- frontend/src/core/hooks/useAuditFilters.ts | 59 +++++ .../shared/config/configNavSections.tsx | 42 +-- 9 files changed, 362 insertions(+), 411 deletions(-) create mode 100644 frontend/src/core/components/shared/config/configSections/audit/AuditFiltersForm.tsx create mode 100644 frontend/src/core/hooks/useAuditFilters.ts diff --git a/frontend/src/core/components/shared/AppConfigModal.tsx b/frontend/src/core/components/shared/AppConfigModal.tsx index 3faa92baa..3e0c6cdb3 100644 --- a/frontend/src/core/components/shared/AppConfigModal.tsx +++ b/frontend/src/core/components/shared/AppConfigModal.tsx @@ -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 = ({ 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; }; diff --git a/frontend/src/core/components/shared/config/configSections/AdminMailSection.tsx b/frontend/src/core/components/shared/config/configSections/AdminMailSection.tsx index 360d3eade..c30bceada 100644 --- a/frontend/src/core/components/shared/config/configSections/AdminMailSection.tsx +++ b/frontend/src/core/components/shared/config/configSections/AdminMailSection.tsx @@ -20,6 +20,13 @@ interface MailSettingsData { frontendUrl?: string; } +interface ApiResponseWithPending { + _pending?: Partial; +} + +type MailApiResponse = MailSettingsData & ApiResponseWithPending; +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('/api/v1/admin/settings/section/mail'), + apiClient.get('/api/v1/admin/settings/section/system') ]); const mail = mailResponse.data || {}; const system = systemResponse.data || {}; - const result: any = { + const result: MailSettingsData & ApiResponseWithPending = { ...mail, frontendUrl: system.frontendUrl || '' }; // Merge pending blocks from both endpoints - const pendingBlock: any = {}; + const pendingBlock: Partial = {}; if (mail._pending) { Object.assign(pendingBlock, mail._pending); } diff --git a/frontend/src/core/components/shared/config/configSections/audit/AuditEventsTable.tsx b/frontend/src/core/components/shared/config/configSections/audit/AuditEventsTable.tsx index f4e112e5d..9deeaed53 100644 --- a/frontend/src/core/components/shared/config/configSections/audit/AuditEventsTable.tsx +++ b/frontend/src/core/components/shared/config/configSections/audit/AuditEventsTable.tsx @@ -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(null); const [selectedEvent, setSelectedEvent] = useState(null); - const [eventTypes, setEventTypes] = useState([]); - const [users, setUsers] = useState([]); - // Filters - const [filters, setFilters] = useState({ - 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 = () => { {/* Filters */} - - ({ 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 }} - /> - - handleFilterChange('startDate', value ?? undefined) - } - clearable - style={{ flex: 1, minWidth: 150 }} - popoverProps={{ withinPortal: true, zIndex: Z_INDEX_OVER_CONFIG_MODAL }} - /> - - handleFilterChange('endDate', value ?? undefined) - } - clearable - style={{ flex: 1, minWidth: 150 }} - popoverProps={{ withinPortal: true, zIndex: Z_INDEX_OVER_CONFIG_MODAL }} - /> - - + {/* Table */} {loading ? ( @@ -161,81 +96,72 @@ const AuditEventsTable: React.FC = () => { ) : ( <> -
- - - - - - - - - - - - {events.length === 0 ? ( - - - - ) : ( - events.map((event) => ( - { - e.currentTarget.style.backgroundColor = - 'var(--mantine-color-gray-0)'; - }} - onMouseLeave={(e) => { - e.currentTarget.style.backgroundColor = 'transparent'; - }} - > - - - - - - - )) - )} - -
- {t('audit.events.timestamp', 'Timestamp')} - - {t('audit.events.type', 'Type')} - - {t('audit.events.user', 'User')} - - {t('audit.events.ipAddress', 'IP Address')} - - {t('audit.events.actions', 'Actions')} -
- {t('audit.events.noEvents', 'No events found')} -
- {formatDate(event.timestamp)} - - {event.eventType} - - {event.username} - - {event.ipAddress} - - -
-
+ + + + + {t('audit.events.timestamp', 'Timestamp')} + + + {t('audit.events.type', 'Type')} + + + {t('audit.events.user', 'User')} + + + {t('audit.events.ipAddress', 'IP Address')} + + + {t('audit.events.actions', 'Actions')} + + + + + {events.length === 0 ? ( + + + + {t('audit.events.noEvents', 'No events found')} + + + + ) : ( + events.map((event) => ( + + + {formatDate(event.timestamp)} + + + {event.eventType} + + + {event.username} + + + {event.ipAddress} + + + + + + )) + )} + +
{/* Pagination */} {totalPages > 1 && ( diff --git a/frontend/src/core/components/shared/config/configSections/audit/AuditExportSection.tsx b/frontend/src/core/components/shared/config/configSections/audit/AuditExportSection.tsx index 5908e2900..b37b89a15 100644 --- a/frontend/src/core/components/shared/config/configSections/audit/AuditExportSection.tsx +++ b/frontend/src/core/components/shared/config/configSections/audit/AuditExportSection.tsx @@ -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([]); - const [users, setUsers] = useState([]); - // Filters for export - const [filters, setFilters] = useState({ - 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 ( @@ -116,54 +78,13 @@ const AuditExportSection: React.FC = () => { {t('audit.export.filters', 'Filters (Optional)')} - - - ({ 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 }} - /> - - - - handleFilterChange('startDate', value ?? undefined) - } - clearable - style={{ flex: 1, minWidth: 200 }} - popoverProps={{ withinPortal: true, zIndex: Z_INDEX_OVER_CONFIG_MODAL }} - /> - - handleFilterChange('endDate', value ?? undefined) - } - clearable - style={{ flex: 1, minWidth: 200 }} - popoverProps={{ withinPortal: true, zIndex: Z_INDEX_OVER_CONFIG_MODAL }} - /> - - - + {/* Export Button */} diff --git a/frontend/src/core/components/shared/config/configSections/audit/AuditFiltersForm.tsx b/frontend/src/core/components/shared/config/configSections/audit/AuditFiltersForm.tsx new file mode 100644 index 000000000..28f0ed4d3 --- /dev/null +++ b/frontend/src/core/components/shared/config/configSections/audit/AuditFiltersForm.tsx @@ -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 = ({ + filters, + eventTypes, + users, + onFilterChange, + onClearFilters, +}) => { + const { t } = useTranslation(); + + return ( + + ({ 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 }} + /> + + onFilterChange('startDate', value ?? undefined) + } + clearable + style={{ flex: 1, minWidth: 150 }} + popoverProps={{ withinPortal: true, zIndex: Z_INDEX_OVER_CONFIG_MODAL }} + /> + + onFilterChange('endDate', value ?? undefined) + } + clearable + style={{ flex: 1, minWidth: 150 }} + popoverProps={{ withinPortal: true, zIndex: Z_INDEX_OVER_CONFIG_MODAL }} + /> + + + ); +}; + +export default AuditFiltersForm; diff --git a/frontend/src/core/components/shared/config/configSections/usage/UsageAnalyticsTable.tsx b/frontend/src/core/components/shared/config/configSections/usage/UsageAnalyticsTable.tsx index 8a38ac439..da323be80 100644 --- a/frontend/src/core/components/shared/config/configSections/usage/UsageAnalyticsTable.tsx +++ b/frontend/src/core/components/shared/config/configSections/usage/UsageAnalyticsTable.tsx @@ -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 = ({ data }) => { {t('usage.table.title', 'Detailed Statistics')} -
- - - - - - - - - - - {data.length === 0 ? ( - - - - ) : ( - data.map((stat, index) => ( - { - e.currentTarget.style.backgroundColor = 'var(--mantine-color-gray-0)'; - }} - onMouseLeave={(e) => { - e.currentTarget.style.backgroundColor = 'transparent'; - }} - > - - - - - - )) - )} - -
- # - - {t('usage.table.endpoint', 'Endpoint')} - - {t('usage.table.visits', 'Visits')} - - {t('usage.table.percentage', 'Percentage')} -
- {t('usage.table.noData', 'No data available')} -
- - {index + 1} - - - - {stat.endpoint} - - - - {stat.visits.toLocaleString()} - - - - {stat.percentage.toFixed(2)}% - -
-
+ + + + + # + + + {t('usage.table.endpoint', 'Endpoint')} + + + {t('usage.table.visits', 'Visits')} + + + {t('usage.table.percentage', 'Percentage')} + + + + + {data.length === 0 ? ( + + + + {t('usage.table.noData', 'No data available')} + + + + ) : ( + data.map((stat, index) => ( + + + + {index + 1} + + + + + {stat.endpoint} + + + + + {stat.visits.toLocaleString()} + + + + + {stat.percentage.toFixed(2)}% + + + + )) + )} + +
); diff --git a/frontend/src/core/components/shared/config/types.ts b/frontend/src/core/components/shared/config/types.ts index 6e7c3cf59..d4bd9a83c 100644 --- a/frontend/src/core/components/shared/config/types.ts +++ b/frontend/src/core/components/shared/config/types.ts @@ -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 \ No newline at end of file +// some of these are not used yet, but appear in figma designs \ No newline at end of file diff --git a/frontend/src/core/hooks/useAuditFilters.ts b/frontend/src/core/hooks/useAuditFilters.ts new file mode 100644 index 000000000..ef8f0c7df --- /dev/null +++ b/frontend/src/core/hooks/useAuditFilters.ts @@ -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 = {}) { + const [eventTypes, setEventTypes] = useState([]); + const [users, setUsers] = useState([]); + const [filters, setFilters] = useState({ + 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, + }; +} diff --git a/frontend/src/proprietary/components/shared/config/configNavSections.tsx b/frontend/src/proprietary/components/shared/config/configNavSections.tsx index eb8068cbd..6e6d39b3a 100644 --- a/frontend/src/proprietary/components/shared/config/configNavSections.tsx +++ b/frontend/src/proprietary/components/shared/config/configNavSections.tsx @@ -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: - }, - { - key: 'teams', - label: 'Teams', - icon: 'groups-rounded', - component: - }, - ], - }; + // Add Workspace section if user is admin + if (isAdmin) { + const workspaceSection: ConfigNavSection = { + title: 'Workspace', + items: [ + { + key: 'people', + label: 'People', + icon: 'group-rounded', + component: + }, + { + key: 'teams', + label: 'Teams', + icon: 'groups-rounded', + component: + }, + ], + }; - // 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; };