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.
|
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
@@ -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>
|
||||||
|
|
||||||
|
<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>
|
||||||
<div className="audit__filter">
|
|
||||||
<FilterChip on={filter==='all'} onClick={() => setFilter('all')}>All</FilterChip>
|
<div className="audit__filter" style={{ alignSelf: 'flex-end' }}>
|
||||||
<FilterChip on={filter==='system'} onClick={() => setFilter('system')}>System</FilterChip>
|
<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>
|
||||||
))}
|
))}
|
||||||
|
|||||||
Reference in New Issue
Block a user