diff --git a/src/frontend/config/api.ts b/src/frontend/config/api.ts
index f65a3aa..1cb70e3 100644
--- a/src/frontend/config/api.ts
+++ b/src/frontend/config/api.ts
@@ -11,4 +11,6 @@ export const API_URLS = {
`${API_BASE_URL}/api/monitoring/graph-log-villages?id=${id}&time=${time}`,
getUsers: (page: number, search: string) =>
`${API_BASE_URL}/api/monitoring/user?page=${page}&search=${encodeURIComponent(search)}`,
+ getLogsAllVillages: (page: number, search: string) =>
+ `${API_BASE_URL}/api/monitoring/log-all-villages?page=${page}&search=${encodeURIComponent(search)}`,
}
diff --git a/src/frontend/routes/apps.$appId.logs.tsx b/src/frontend/routes/apps.$appId.logs.tsx
index 81b4bcf..16c9cf7 100644
--- a/src/frontend/routes/apps.$appId.logs.tsx
+++ b/src/frontend/routes/apps.$appId.logs.tsx
@@ -1,6 +1,7 @@
+import { useState } from 'react'
+import useSWR from 'swr'
import {
Badge,
- Container,
Group,
Stack,
Text,
@@ -8,116 +9,244 @@ import {
Paper,
Table,
TextInput,
- Select,
ActionIcon,
- Tooltip,
Avatar,
Code,
- Button
+ Button,
+ Box,
+ Pagination,
+ ThemeIcon,
+ ScrollArea,
+ Container,
} from '@mantine/core'
+import { useMediaQuery } from '@mantine/hooks'
import { createFileRoute, useParams } from '@tanstack/react-router'
-import { TbSearch, TbFilter, TbDownload, TbCalendar } from 'react-icons/tb'
+import {
+ TbSearch,
+ TbDownload,
+ TbX,
+ TbHistory,
+ TbCalendar,
+ TbUser,
+ TbHome2
+} from 'react-icons/tb'
+import { API_URLS } from '../config/api'
export const Route = createFileRoute('/apps/$appId/logs')({
component: AppLogsPage,
})
-const mockLogs = [
- { id: 1, type: 'DOCUMENT', village: 'Sukatani', activity: 'GENERATE_SURAT_DOMISILI', operator: 'Budi Santoso', time: '2 mins ago', status: 'SUCCESS' },
- { id: 2, type: 'FINANCE', village: 'Sukamaju', activity: 'UPLOAD_LAPORAN_REALISASI_Q1', operator: 'Siti Aminah', time: '15 mins ago', status: 'SUCCESS' },
- { id: 3, type: 'SYNC', village: 'Cikini', activity: 'SYNC_DATA_PENDUDUK_SIAK', operator: 'System', time: '1 hour ago', status: 'WARNING' },
- { id: 4, type: 'SECURITY', village: 'Bojong Gede', activity: 'LOGIN_ADMIN_DESA', operator: 'Rahmat Hidayat', time: '2 hours ago', status: 'SUCCESS' },
- { id: 5, type: 'DOCUMENT', village: 'Tapos', activity: 'VERIFIKASI_SURAT_KEMATIAN', operator: 'Agus Setiawan', time: '4 hours ago', status: 'SUCCESS' },
-]
+interface LogEntry {
+ id: string
+ createdAt: string
+ action: string
+ desc: string
+ username: string
+ village: string
+}
+
+const fetcher = (url: string) => fetch(url).then((res) => res.json())
function AppLogsPage() {
const { appId } = useParams({ from: '/apps/$appId/logs' })
+ const [page, setPage] = useState(1)
+ const [search, setSearch] = useState('')
+ const [searchQuery, setSearchQuery] = useState('')
+
const isDesaPlus = appId === 'desa-plus'
+ const isMobile = useMediaQuery('(max-width: 768px)')
+
+ const apiUrl = isDesaPlus ? API_URLS.getLogsAllVillages(page, searchQuery) : 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)
+ setPage(1)
+ }
+ }
+
+ const handleClearSearch = () => {
+ setSearch('')
+ setSearchQuery('')
+ setPage(1)
+ }
+
+ const getActionColor = (action: string) => {
+ const a = action.toUpperCase()
+ if (a === 'LOGIN') return 'blue'
+ if (a === 'LOGOUT') return 'gray'
+ if (a === 'CREATE') return 'teal'
+ if (a === 'UPDATE') return 'orange'
+ if (a === 'DELETE') return 'red'
+ return 'brand-blue'
+ }
+
+ if (!isDesaPlus) {
+ return (
+
+
+
+ Activity Logs
+ This feature is currently customized for Desa+. Other apps coming soon.
+
+
+ )
+ }
return (
-
-
-
- {isDesaPlus ? 'Desa+ Service Logs' : 'Application Activity Logs'}
- Detailed audit trail of all actions performed within the application instances.
-
-
- } radius="md">Export XLS
-
-
+
+
+
+
+
+
+
+
+
+ Activity Logs
+
+
+ {isLoading ? 'Loading logs...' : `Auditing ${response?.data?.total || 0} events across all villages`}
+
+
+ }
+ radius="md"
+ size="md"
+ >
+ Export
+
+
-
-
}
+ placeholder="Search action or village..."
+ leftSection={}
+ size="md"
+ rightSection={
+ search ? (
+
+
+
+ ) : null
+ }
+ value={search}
+ onChange={(e) => handleSearchChange(e.currentTarget.value)}
radius="md"
+ style={{ maxWidth: 500 }}
+ ml={40}
/>
- }
+
+
+
+ {isLoading ? (
+
+ Fetching activity logs...
+
+ ) : error ? (
+
+ Failed to load logs from API.
+
+ ) : logs.length === 0 ? (
+
+
+ No activity found for this search.
+
+ ) : (
+
+
+
+
+
+ Timestamp
+ User & Village
+ Action
+ Description
+
+
+
+ {logs.map((log) => (
+
+
+
+
+
+
+
+
+ {log.createdAt.split(' ').slice(1).join(' ')}
+
+
+ {log.createdAt.split(' ')[0]}
+
+
+
+
+
+
+
+
+ {log.username.charAt(0)}
+
+ {log.username}
+
+
+
+ {log.village}
+
+
+
+
+
+ {log.action}
+
+
+
+
+ {log.desc}
+
+
+
+ ))}
+
+
+
+
+ )}
+
+ {!isLoading && !error && response?.data?.totalPage > 0 && (
+
+
-
-
-
-
- Type
- Village / Instance
- Activity Name
- Operator
- Timestamp
- Status
-
-
-
- {mockLogs.map((log) => (
-
-
-
- {log.type}
-
-
-
- {log.village}
-
-
- {log.activity}
-
-
-
- {log.operator[0]}
- {log.operator}
-
-
-
- {log.time}
-
-
-
- {log.status}
-
-
-
- ))}
-
-
-
+ )}
)
}
diff --git a/src/frontend/routes/apps.$appId.users.index.tsx b/src/frontend/routes/apps.$appId.users.index.tsx
index 1b19141..3db42f5 100644
--- a/src/frontend/routes/apps.$appId.users.index.tsx
+++ b/src/frontend/routes/apps.$appId.users.index.tsx
@@ -289,6 +289,8 @@ function UsersIndexPage() {
total={response.data.totalPage}
radius="md"
withEdges={false}
+ siblings={1}
+ boundaries={1}
/>
)}