feat: improve logs page with debounce, action/village/date filters, and timestamp fix

This commit is contained in:
2026-05-22 11:37:37 +08:00
parent 603a0a04b7
commit 0afc2e271a
2 changed files with 122 additions and 41 deletions

View File

@@ -18,8 +18,14 @@ export const API_URLS = {
if (orderDir) params.set('orderDir', orderDir) if (orderDir) params.set('orderDir', orderDir)
return `${DESA_PLUS_PROXY}/api/monitoring/user?${params}` return `${DESA_PLUS_PROXY}/api/monitoring/user?${params}`
}, },
getLogsAllVillages: (page: number, search: string) => getLogsAllVillages: (page: number, search: string, action?: string, idVillage?: string, dateFrom?: string, dateTo?: string) => {
`${DESA_PLUS_PROXY}/api/monitoring/log-all-villages?page=${page}&search=${encodeURIComponent(search)}`, const params = new URLSearchParams({ page: String(page), search })
if (action) params.set('action', action)
if (idVillage) params.set('idVillage', idVillage)
if (dateFrom) params.set('dateFrom', dateFrom)
if (dateTo) params.set('dateTo', dateTo)
return `${DESA_PLUS_PROXY}/api/monitoring/log-all-villages?${params}`
},
getGridOverview: () => `${DESA_PLUS_PROXY}/api/monitoring/grid-overview`, getGridOverview: () => `${DESA_PLUS_PROXY}/api/monitoring/grid-overview`,
getDailyActivity: () => `${DESA_PLUS_PROXY}/api/monitoring/daily-activity`, getDailyActivity: () => `${DESA_PLUS_PROXY}/api/monitoring/daily-activity`,
getComparisonActivity: () => `${DESA_PLUS_PROXY}/api/monitoring/comparison-activity`, getComparisonActivity: () => `${DESA_PLUS_PROXY}/api/monitoring/comparison-activity`,

View File

@@ -1,4 +1,4 @@
import { useState } from 'react' import { useEffect, useState } from 'react'
import useSWR from 'swr' import useSWR from 'swr'
import { import {
ActionIcon, ActionIcon,
@@ -10,6 +10,7 @@ import {
Pagination, Pagination,
Paper, Paper,
ScrollArea, ScrollArea,
Select,
Stack, Stack,
Table, Table,
Text, Text,
@@ -17,10 +18,12 @@ import {
Title, Title,
Tooltip, Tooltip,
} from '@mantine/core' } from '@mantine/core'
import { useMediaQuery } from '@mantine/hooks' import { useDebouncedValue, useMediaQuery } from '@mantine/hooks'
import { DatePickerInput } from '@mantine/dates'
import { createFileRoute, useParams } from '@tanstack/react-router' import { createFileRoute, useParams } from '@tanstack/react-router'
import { import {
TbAlertCircle, TbAlertCircle,
TbCalendar,
TbHistory, TbHistory,
TbHome2, TbHome2,
TbSearch, TbSearch,
@@ -51,30 +54,75 @@ const ACTION_COLOR: Record<string, string> = {
DELETE: 'red', DELETE: 'red',
} }
const ACTION_OPTIONS = [
{ value: 'LOGIN', label: 'Login' },
{ value: 'LOGOUT', label: 'Logout' },
{ value: 'CREATE', label: 'Create' },
{ value: 'UPDATE', label: 'Update' },
{ value: 'DELETE', label: 'Delete' },
]
function getActionColor(action: string) { function getActionColor(action: string) {
return ACTION_COLOR[action.toUpperCase()] ?? 'brand-blue' return ACTION_COLOR[action.toUpperCase()] ?? 'brand-blue'
} }
function LogTimestamp({ value }: { value: string }) {
if (value.endsWith('lalu')) {
return <Text size="xs" fw={600}>{value}</Text>
}
const [time, ...dateParts] = value.split(' ')
return (
<Stack gap={0}>
<Text size="xs" fw={600}>{dateParts.join(' ')}</Text>
<Text size="xs" c="dimmed">{time}</Text>
</Stack>
)
}
function AppLogsPage() { function AppLogsPage() {
const { appId } = useParams({ from: '/apps/$appId/logs' }) const { appId } = useParams({ from: '/apps/$appId/logs' })
const [page, setPage] = useState(1) const [page, setPage] = useState(1)
const [search, setSearch] = useState('') const [search, setSearch] = useState('')
const [searchQuery, setSearchQuery] = useState('') const [searchQuery, setSearchQuery] = useState('')
const [debouncedSearch] = useDebouncedValue(search, 400)
const [filterAction, setFilterAction] = useState<string | null>(null)
const [filterVillageSearch, setFilterVillageSearch] = useState('')
const [filterVillageId, setFilterVillageId] = useState<string | null>(null)
const [dateRange, setDateRange] = useState<[string | null, string | null]>([null, null])
const isDesaPlus = appId === 'desa-plus' const isDesaPlus = appId === 'desa-plus'
const isMobile = useMediaQuery('(max-width: 768px)') const isMobile = useMediaQuery('(max-width: 768px)')
const apiUrl = isDesaPlus ? API_URLS.getLogsAllVillages(page, searchQuery) : null const [dateFrom, dateTo] = dateRange
const apiUrl = isDesaPlus
? API_URLS.getLogsAllVillages(
page,
searchQuery,
filterAction ?? undefined,
filterVillageId ?? undefined,
dateFrom ?? undefined,
dateTo ?? undefined,
)
: null
const { data: response, error, isLoading } = useSWR(apiUrl, fetcher) const { data: response, error, isLoading } = useSWR(apiUrl, fetcher)
const logs: LogEntry[] = response?.data?.log || [] const logs: LogEntry[] = response?.data?.log || []
const handleSearchChange = (val: string) => { const { data: filterVillagesResp } = useSWR(
setSearch(val) isDesaPlus && filterVillageSearch.length >= 1 ? API_URLS.getVillages(1, filterVillageSearch) : null,
if (val.length >= 3 || val.length === 0) { fetcher
setSearchQuery(val) )
const filterVillagesOptions = (filterVillagesResp?.data || []).map((v: any) => ({ value: v.id, label: v.name }))
useEffect(() => {
if (debouncedSearch.length >= 3 || debouncedSearch.length === 0) {
setSearchQuery(debouncedSearch)
setPage(1) setPage(1)
} }
} }, [debouncedSearch])
useEffect(() => {
setPage(1)
}, [filterAction, filterVillageId, dateFrom, dateTo])
const handleClearSearch = () => { const handleClearSearch = () => {
setSearch('') setSearch('')
@@ -108,23 +156,61 @@ function AppLogsPage() {
</Group> </Group>
<Paper withBorder p="md" className="glass"> <Paper withBorder p="md" className="glass">
<TextInput <Stack gap="sm">
placeholder="Search by action or village... (min. 3 characters)" <TextInput
leftSection={<TbSearch size={16} />} placeholder="Search by user name or village..."
size="sm" leftSection={<TbSearch size={16} />}
rightSection={ size="sm"
search ? ( rightSection={
<Tooltip label="Clear search" withArrow> search ? (
<ActionIcon variant="transparent" color="gray" onClick={handleClearSearch} size="sm"> <Tooltip label="Clear search" withArrow>
<TbX size={16} /> <ActionIcon variant="transparent" color="gray" onClick={handleClearSearch} size="sm">
</ActionIcon> <TbX size={16} />
</Tooltip> </ActionIcon>
) : null </Tooltip>
} ) : null
value={search} }
onChange={(e) => handleSearchChange(e.currentTarget.value)} value={search}
radius="md" onChange={(e) => setSearch(e.currentTarget.value)}
/> radius="md"
/>
<Group gap="sm" wrap="nowrap">
<Select
size="sm"
placeholder="All actions"
data={ACTION_OPTIONS}
value={filterAction}
onChange={setFilterAction}
radius="md"
clearable
style={{ flex: 1 }}
/>
<Select
size="sm"
placeholder="Search village..."
searchable
onSearchChange={setFilterVillageSearch}
data={filterVillagesOptions}
value={filterVillageId}
onChange={setFilterVillageId}
radius="md"
clearable
style={{ flex: 1 }}
/>
<DatePickerInput
type="range"
size="sm"
placeholder="Date range"
leftSection={<TbCalendar size={16} />}
value={dateRange}
onChange={setDateRange}
radius="md"
clearable
style={{ flex: 1 }}
maxDate={new Date()}
/>
</Group>
</Stack>
</Paper> </Paper>
{isLoading ? ( {isLoading ? (
@@ -143,7 +229,7 @@ function AppLogsPage() {
<Stack align="center" gap="xs" py="xl"> <Stack align="center" gap="xs" py="xl">
<TbHistory size={32} style={{ opacity: 0.25 }} /> <TbHistory size={32} style={{ opacity: 0.25 }} />
<Text size="sm" c="dimmed"> <Text size="sm" c="dimmed">
{searchQuery ? 'No activity found for this search.' : 'No activity logs yet.'} {searchQuery || filterAction || filterVillageId || dateFrom ? 'No activity found for this filter.' : 'No activity logs yet.'}
</Text> </Text>
</Stack> </Stack>
</Paper> </Paper>
@@ -174,18 +260,7 @@ function AppLogsPage() {
{logs.map((log) => ( {logs.map((log) => (
<Table.Tr key={log.id}> <Table.Tr key={log.id}>
<Table.Td> <Table.Td>
{log.createdAt.endsWith('lalu') ? ( <LogTimestamp value={log.createdAt} />
<Text size="xs" fw={600}>{log.createdAt}</Text>
) : (
<Stack gap={0}>
<Text size="xs" fw={600}>
{log.createdAt.split(' ').slice(1).join(' ')}
</Text>
<Text size="xs" c="dimmed">
{log.createdAt.split(' ')[0]}
</Text>
</Stack>
)}
</Table.Td> </Table.Td>
<Table.Td> <Table.Td>
<Stack gap={4} style={{ overflow: 'hidden' }}> <Stack gap={4} style={{ overflow: 'hidden' }}>
@@ -229,7 +304,7 @@ function AppLogsPage() {
</Paper> </Paper>
)} )}
{!isLoading && !error && response?.data?.totalPage > 0 && ( {!isLoading && !error && response?.data?.totalPage > 1 && (
<Group justify="center"> <Group justify="center">
<Pagination <Pagination
value={page} value={page}