amalia/22-mei-26 #25
@@ -18,8 +18,14 @@ export const API_URLS = {
|
||||
if (orderDir) params.set('orderDir', orderDir)
|
||||
return `${DESA_PLUS_PROXY}/api/monitoring/user?${params}`
|
||||
},
|
||||
getLogsAllVillages: (page: number, search: string) =>
|
||||
`${DESA_PLUS_PROXY}/api/monitoring/log-all-villages?page=${page}&search=${encodeURIComponent(search)}`,
|
||||
getLogsAllVillages: (page: number, search: string, action?: string, idVillage?: string, dateFrom?: string, dateTo?: string) => {
|
||||
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`,
|
||||
getDailyActivity: () => `${DESA_PLUS_PROXY}/api/monitoring/daily-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 {
|
||||
ActionIcon,
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
Pagination,
|
||||
Paper,
|
||||
ScrollArea,
|
||||
Select,
|
||||
Stack,
|
||||
Table,
|
||||
Text,
|
||||
@@ -17,10 +18,12 @@ import {
|
||||
Title,
|
||||
Tooltip,
|
||||
} 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 {
|
||||
TbAlertCircle,
|
||||
TbCalendar,
|
||||
TbHistory,
|
||||
TbHome2,
|
||||
TbSearch,
|
||||
@@ -51,30 +54,75 @@ const ACTION_COLOR: Record<string, string> = {
|
||||
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) {
|
||||
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() {
|
||||
const { appId } = useParams({ from: '/apps/$appId/logs' })
|
||||
const [page, setPage] = useState(1)
|
||||
const [search, setSearch] = 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 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 logs: LogEntry[] = response?.data?.log || []
|
||||
|
||||
const handleSearchChange = (val: string) => {
|
||||
setSearch(val)
|
||||
if (val.length >= 3 || val.length === 0) {
|
||||
setSearchQuery(val)
|
||||
const { data: filterVillagesResp } = useSWR(
|
||||
isDesaPlus && filterVillageSearch.length >= 1 ? API_URLS.getVillages(1, filterVillageSearch) : null,
|
||||
fetcher
|
||||
)
|
||||
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)
|
||||
}
|
||||
}
|
||||
}, [debouncedSearch])
|
||||
|
||||
useEffect(() => {
|
||||
setPage(1)
|
||||
}, [filterAction, filterVillageId, dateFrom, dateTo])
|
||||
|
||||
const handleClearSearch = () => {
|
||||
setSearch('')
|
||||
@@ -108,23 +156,61 @@ function AppLogsPage() {
|
||||
</Group>
|
||||
|
||||
<Paper withBorder p="md" className="glass">
|
||||
<TextInput
|
||||
placeholder="Search by action or village... (min. 3 characters)"
|
||||
leftSection={<TbSearch size={16} />}
|
||||
size="sm"
|
||||
rightSection={
|
||||
search ? (
|
||||
<Tooltip label="Clear search" withArrow>
|
||||
<ActionIcon variant="transparent" color="gray" onClick={handleClearSearch} size="sm">
|
||||
<TbX size={16} />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
) : null
|
||||
}
|
||||
value={search}
|
||||
onChange={(e) => handleSearchChange(e.currentTarget.value)}
|
||||
radius="md"
|
||||
/>
|
||||
<Stack gap="sm">
|
||||
<TextInput
|
||||
placeholder="Search by user name or village..."
|
||||
leftSection={<TbSearch size={16} />}
|
||||
size="sm"
|
||||
rightSection={
|
||||
search ? (
|
||||
<Tooltip label="Clear search" withArrow>
|
||||
<ActionIcon variant="transparent" color="gray" onClick={handleClearSearch} size="sm">
|
||||
<TbX size={16} />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
) : null
|
||||
}
|
||||
value={search}
|
||||
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>
|
||||
|
||||
{isLoading ? (
|
||||
@@ -143,7 +229,7 @@ function AppLogsPage() {
|
||||
<Stack align="center" gap="xs" py="xl">
|
||||
<TbHistory size={32} style={{ opacity: 0.25 }} />
|
||||
<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>
|
||||
</Stack>
|
||||
</Paper>
|
||||
@@ -174,18 +260,7 @@ function AppLogsPage() {
|
||||
{logs.map((log) => (
|
||||
<Table.Tr key={log.id}>
|
||||
<Table.Td>
|
||||
{log.createdAt.endsWith('lalu') ? (
|
||||
<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>
|
||||
)}
|
||||
<LogTimestamp value={log.createdAt} />
|
||||
</Table.Td>
|
||||
<Table.Td>
|
||||
<Stack gap={4} style={{ overflow: 'hidden' }}>
|
||||
@@ -229,7 +304,7 @@ function AppLogsPage() {
|
||||
</Paper>
|
||||
)}
|
||||
|
||||
{!isLoading && !error && response?.data?.totalPage > 0 && (
|
||||
{!isLoading && !error && response?.data?.totalPage > 1 && (
|
||||
<Group justify="center">
|
||||
<Pagination
|
||||
value={page}
|
||||
|
||||
Reference in New Issue
Block a user