feat: add user filter chips and resource types to audit log

- Replace user ID text input with checkbox-style user chips extracted from log entries
- Add sales_stats and landings to resource type filter
- Fix export not respecting status filter
- Remove dead auto-refresh interval code
- Add aria-pressed and focus-visible styles for accessibility
This commit is contained in:
Fringg
2026-03-07 06:17:25 +03:00
parent 5228b2dea6
commit 40722747e3
5 changed files with 82 additions and 16 deletions

View File

@@ -3097,6 +3097,7 @@
"filters": {
"title": "Filters",
"active": "Active",
"user": "User",
"action": "Action",
"actionPlaceholder": "Search by action...",
"resource": "Resource type",

View File

@@ -2826,6 +2826,7 @@
"filters": {
"title": "فیلترها",
"active": "فعال",
"user": "کاربر",
"action": "عملیات",
"actionPlaceholder": "جستجو بر اساس عملیات...",
"resource": "نوع منبع",

View File

@@ -3645,6 +3645,7 @@
"filters": {
"title": "Фильтры",
"active": "Активны",
"user": "Пользователь",
"action": "Действие",
"actionPlaceholder": "Поиск по действию...",
"resource": "Тип ресурса",

View File

@@ -2825,6 +2825,7 @@
"filters": {
"title": "筛选",
"active": "已激活",
"user": "用户",
"action": "操作",
"actionPlaceholder": "按操作搜索...",
"resource": "资源类型",

View File

@@ -1,4 +1,4 @@
import { useState, useCallback, useMemo, useEffect, useRef } from 'react';
import { useState, useCallback, useMemo, useEffect } from 'react';
import { useNavigate } from 'react-router';
import { useQuery } from '@tanstack/react-query';
import { useTranslation } from 'react-i18next';
@@ -84,6 +84,7 @@ const RESOURCE_TYPES = [
'users',
'tickets',
'stats',
'sales_stats',
'broadcasts',
'tariffs',
'promocodes',
@@ -106,6 +107,7 @@ const RESOURCE_TYPES = [
'apps',
'email_templates',
'pinned_messages',
'landings',
'updates',
] as const;
@@ -116,6 +118,7 @@ const PAGE_SIZE_OPTIONS = [20, 50, 100] as const;
const AUTO_REFRESH_INTERVAL = 30_000;
interface FiltersState {
userId: string;
action: string;
resource: string;
status: string;
@@ -124,6 +127,7 @@ interface FiltersState {
}
const INITIAL_FILTERS: FiltersState = {
userId: '',
action: '',
resource: '',
status: '',
@@ -411,6 +415,11 @@ function LogEntryCard({ entry, isExpanded, onToggle }: LogEntryCardProps) {
// === Main Page ===
interface KnownUser {
name: string;
email: string | null;
}
export default function AdminAuditLog() {
const { t } = useTranslation();
const navigate = useNavigate();
@@ -431,8 +440,8 @@ export default function AdminAuditLog() {
const [exporting, setExporting] = useState(false);
const [exportError, setExportError] = useState<string | null>(null);
// Auto-refresh interval ref
const intervalRef = useRef<ReturnType<typeof setInterval> | null>(null);
// Known users accumulated from audit log entries
const [knownUsers, setKnownUsers] = useState<Map<number, KnownUser>>(new Map());
// Build query params
const queryParams = useMemo((): AuditLogFilters => {
@@ -441,6 +450,10 @@ export default function AdminAuditLog() {
offset: page * pageSize,
};
const parsedUserId = parseInt(appliedFilters.userId, 10);
if (!isNaN(parsedUserId) && parsedUserId > 0) {
params.user_id = parsedUserId;
}
if (appliedFilters.action.trim()) {
params.action = appliedFilters.action.trim();
}
@@ -467,21 +480,23 @@ export default function AdminAuditLog() {
refetchInterval: autoRefresh ? AUTO_REFRESH_INTERVAL : false,
});
// Auto-refresh visual indicator
// Accumulate unique users from audit log entries
useEffect(() => {
if (autoRefresh) {
intervalRef.current = setInterval(() => {
// The visual indicator updates are driven by isFetching from react-query
}, AUTO_REFRESH_INTERVAL);
}
return () => {
if (intervalRef.current) {
clearInterval(intervalRef.current);
intervalRef.current = null;
if (!data?.items?.length) return;
setKnownUsers((prev) => {
const next = new Map(prev);
let changed = false;
for (const entry of data.items) {
const name = entry.user_first_name || entry.user_email || `#${entry.user_id}`;
const existing = next.get(entry.user_id);
if (!existing || existing.name !== name || existing.email !== entry.user_email) {
next.set(entry.user_id, { name, email: entry.user_email });
changed = true;
}
}
};
}, [autoRefresh]);
return changed ? next : prev;
});
}, [data?.items]);
const entries = data?.items ?? [];
const total = data?.total ?? 0;
@@ -517,8 +532,11 @@ export default function AdminAuditLog() {
setExporting(true);
try {
const exportParams: AuditLogFilters = {};
const exportUserId = parseInt(appliedFilters.userId, 10);
if (!isNaN(exportUserId) && exportUserId > 0) exportParams.user_id = exportUserId;
if (appliedFilters.action.trim()) exportParams.action = appliedFilters.action.trim();
if (appliedFilters.resource) exportParams.resource_type = appliedFilters.resource;
if (appliedFilters.status) exportParams.status = appliedFilters.status;
if (appliedFilters.dateFrom) exportParams.date_from = appliedFilters.dateFrom;
if (appliedFilters.dateTo) exportParams.date_to = appliedFilters.dateTo;
@@ -548,6 +566,7 @@ export default function AdminAuditLog() {
const hasActiveFilters = useMemo(() => {
return (
appliedFilters.userId.trim() !== '' ||
appliedFilters.action.trim() !== '' ||
appliedFilters.resource !== '' ||
appliedFilters.status !== '' ||
@@ -649,6 +668,49 @@ export default function AdminAuditLog() {
{filtersOpen && (
<div className="border-t border-dark-700 p-4">
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
{/* User filter */}
{knownUsers.size > 0 && (
<div className="sm:col-span-2 lg:col-span-3">
<label className="mb-1 block text-sm font-medium text-dark-300">
{t('admin.auditLog.filters.user')}
</label>
<div className="flex flex-wrap gap-2">
{Array.from(knownUsers.entries()).map(([uid, user]) => {
const isSelected = filters.userId === String(uid);
return (
<button
key={uid}
type="button"
aria-pressed={isSelected}
onClick={() =>
setFilters((prev) => ({
...prev,
userId: isSelected ? '' : String(uid),
}))
}
className={`flex items-center gap-1.5 rounded-lg border px-3 py-1.5 text-sm transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent-500 focus-visible:ring-offset-1 focus-visible:ring-offset-dark-900 ${
isSelected
? 'border-accent-500 bg-accent-500/20 text-accent-300'
: 'border-dark-600 bg-dark-900 text-dark-300 hover:border-dark-500 hover:text-dark-200'
}`}
>
<span
className={`flex h-4 w-4 items-center justify-center rounded border text-xs ${
isSelected
? 'border-accent-500 bg-accent-500 text-white'
: 'border-dark-500 bg-dark-800'
}`}
>
{isSelected && '✓'}
</span>
<span>{user.name}</span>
</button>
);
})}
</div>
</div>
)}
{/* Action search */}
<div>
<label