feat: improve logs page with debounce, action/village/date filters, and timestamp fix
This commit is contained in:
@@ -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`,
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
Reference in New Issue
Block a user