Search function added to audit logs for admins

This commit is contained in:
NPS Agent
2026-05-12 10:19:19 +09:30
parent 5968294081
commit 7545d1da47
2 changed files with 78 additions and 14 deletions
+1
View File
@@ -62,6 +62,7 @@
18. **Persistent Workspace Settings:** Added a `Workspace` database model to persist global dashboard settings like Name and Timezone. 18. **Persistent Workspace Settings:** Added a `Workspace` database model to persist global dashboard settings like Name and Timezone.
19. **Dynamic UI Integration:** Completely refactored the navigation and boards to build columns and tabs dynamically from the live database user list. 19. **Dynamic UI Integration:** Completely refactored the navigation and boards to build columns and tabs dynamically from the live database user list.
20. **Functional Search:** Implemented a real-time task search feature. Clicking the search icon now reveals an inline search bar that filters tasks by title, description, or tags across all views. 20. **Functional Search:** Implemented a real-time task search feature. Clicking the search icon now reveals an inline search bar that filters tasks by title, description, or tags across all views.
21. **Advanced Audit Filtering:** Upgraded the Audit Log for Admins with a search bar and a granular event type filter. Admins can now filter the history by specific actions (e.g., "deleted tasks", "moved tasks", "user management") and search through summaries in real-time.
### Phase 3: Advanced Features ### Phase 3: Advanced Features
- **Real-time Notifications:** Explore WebSockets for task assignments. - **Real-time Notifications:** Explore WebSockets for task assignments.
+77 -14
View File
@@ -636,33 +636,96 @@ function Field({ label, children }) {
} }
function AuditScreen({ entries, onOpen }) { function AuditScreen({ entries, onOpen }) {
const [filter, setFilter] = React.useState('all'); const [actorFilter, setActorFilter] = React.useState('all');
const [eventFilter, setEventFilter] = React.useState('all');
const [auditSearch, setAuditSearch] = React.useState('');
const EVENT_TYPES = [
{ value: 'all', label: 'All events' },
{ value: 'task', label: 'Tasks (all)' },
{ value: 'task_created', label: 'Created' },
{ value: 'task_moved', label: 'Moved' },
{ value: 'task_completed', label: 'Completed' },
{ value: 'task_deleted', label: 'Deleted' },
{ value: 'task_restored', label: 'Restored' },
{ value: 'user', label: 'User management' },
{ value: 'workspace_updated', label: 'Workspace' },
];
const groups = React.useMemo(() => { const groups = React.useMemo(() => {
const filtered = filter === 'all' let filtered = entries;
? entries
: filter === 'system' // 1. Actor Filter
? entries.filter(e => e.actor === 'system') if (actorFilter !== 'all') {
: entries.filter(e => e.actor === filter); filtered = actorFilter === 'system'
? filtered.filter(e => e.actor === 'system')
: filtered.filter(e => e.actor === actorFilter);
}
// 2. Event Type Filter
if (eventFilter !== 'all') {
if (eventFilter === 'task') {
filtered = filtered.filter(e => e.action.startsWith('task_'));
} else if (eventFilter === 'user') {
filtered = filtered.filter(e => e.action.startsWith('user_') || e.action === 'password_changed');
} else {
filtered = filtered.filter(e => e.action === eventFilter);
}
}
// 3. Search Query
if (auditSearch.trim()) {
const q = auditSearch.toLowerCase();
filtered = filtered.filter(e =>
e.summary.toLowerCase().includes(q) ||
e.action.toLowerCase().includes(q) ||
(e.actor !== 'system' && (findUser(e.actor) || {}).name?.toLowerCase().includes(q))
);
}
const out = {}; const out = {};
filtered.forEach(e => { filtered.forEach(e => {
const day = new Date(e.at).toDateString(); const day = new Date(e.at).toDateString();
(out[day] = out[day] || []).push(e); (out[day] = out[day] || []).push(e);
}); });
return out; return out;
}, [filter, entries]); }, [actorFilter, eventFilter, auditSearch, entries]);
return ( return (
<div className="audit"> <div className="audit">
<header className="audit__head"> <header className="audit__head" style={{ gap: '20px' }}>
<div> <div style={{ flex: 1, minWidth: '300px' }}>
<h1 className="audit__title">Audit log</h1> <h1 className="audit__title">Audit log</h1>
<p className="audit__sub">Everything that's happened in the workspace · <span className="mono">last 7 days</span></p> <p className="audit__sub">History of the workspace · <span className="mono">last 7 days</span></p>
<div style={{ marginTop: '1rem', display: 'flex', gap: '8px' }}>
<div className="topbar__search" style={{ maxWidth: 'none', flex: 1 }}>
<Icon.Search />
<input
className="topbar__search-input"
placeholder="Search audit trail…"
value={auditSearch}
onChange={e => setAuditSearch(e.target.value)}
/>
{auditSearch && <IconBtn label="Clear" onClick={() => setAuditSearch('')}><Icon.Close /></IconBtn>}
</div> </div>
<div className="audit__filter">
<FilterChip on={filter==='all'} onClick={() => setFilter('all')}>All</FilterChip> <select
<FilterChip on={filter==='system'} onClick={() => setFilter('system')}>System</FilterChip> className="field__input"
style={{ width: '180px', padding: '0 8px', height: '32px', fontSize: '12.5px' }}
value={eventFilter}
onChange={e => setEventFilter(e.target.value)}
>
{EVENT_TYPES.map(t => <option key={t.value} value={t.value}>{t.label}</option>)}
</select>
</div>
</div>
<div className="audit__filter" style={{ alignSelf: 'flex-end' }}>
<FilterChip on={actorFilter==='all'} onClick={() => setActorFilter('all')}>All actors</FilterChip>
<FilterChip on={actorFilter==='system'} onClick={() => setActorFilter('system')}>System</FilterChip>
{dbUsers.map(u => ( {dbUsers.map(u => (
<FilterChip key={u.id} on={filter===u.id} onClick={() => setFilter(u.id)}> <FilterChip key={u.id} on={actorFilter===u.id} onClick={() => setActorFilter(u.id)}>
<Avatar user={u} size={16} /> {u.name} <Avatar user={u} size={16} /> {u.name}
</FilterChip> </FilterChip>
))} ))}