Search function added to audit logs for admins
This commit is contained in:
@@ -62,6 +62,7 @@
|
||||
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.
|
||||
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
|
||||
- **Real-time Notifications:** Explore WebSockets for task assignments.
|
||||
|
||||
+77
-14
@@ -636,33 +636,96 @@ function Field({ label, children }) {
|
||||
}
|
||||
|
||||
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 filtered = filter === 'all'
|
||||
? entries
|
||||
: filter === 'system'
|
||||
? entries.filter(e => e.actor === 'system')
|
||||
: entries.filter(e => e.actor === filter);
|
||||
let filtered = entries;
|
||||
|
||||
// 1. Actor Filter
|
||||
if (actorFilter !== 'all') {
|
||||
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 = {};
|
||||
filtered.forEach(e => {
|
||||
const day = new Date(e.at).toDateString();
|
||||
(out[day] = out[day] || []).push(e);
|
||||
});
|
||||
return out;
|
||||
}, [filter, entries]);
|
||||
}, [actorFilter, eventFilter, auditSearch, entries]);
|
||||
|
||||
return (
|
||||
<div className="audit">
|
||||
<header className="audit__head">
|
||||
<div>
|
||||
<header className="audit__head" style={{ gap: '20px' }}>
|
||||
<div style={{ flex: 1, minWidth: '300px' }}>
|
||||
<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 className="audit__filter">
|
||||
<FilterChip on={filter==='all'} onClick={() => setFilter('all')}>All</FilterChip>
|
||||
<FilterChip on={filter==='system'} onClick={() => setFilter('system')}>System</FilterChip>
|
||||
|
||||
<select
|
||||
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 => (
|
||||
<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}
|
||||
</FilterChip>
|
||||
))}
|
||||
|
||||
Reference in New Issue
Block a user