guardia-messenger/app/(tabs)/audit_log.tsx

78 lines
2.9 KiB
TypeScript

import React, { useState, useCallback } from 'react'
import { View, Text, FlatList, StyleSheet, RefreshControl, ActivityIndicator } from 'react-native'
import { COLORS } from '../../constants/Config'
import { getAuditLogs } from '../../services/api'
function maskIp(ip: string | undefined) {
if (!ip) return '-'
const parts = ip.split('.')
if (parts.length === 4) return `${parts[0]}.xxx.xxx.xxx`
return ip.replace(/[0-9]+/g, 'x')
}
export default function AuditLogScreen() {
const [items, setItems] = useState<any[]>([])
const [page, setPage] = useState(0)
const [loading, setLoading] = useState(false)
const [loadingMore, setLoadingMore] = useState(false)
const [hasMore, setHasMore] = useState(true)
const load = useCallback(async (pg = 0) => {
if (pg === 0) setLoading(true)
else setLoadingMore(true)
try {
const r = await getAuditLogs(pg)
const data = r.data?.items ?? r.data ?? []
if (pg === 0) setItems(data)
else setItems(prev => [...prev, ...data])
setHasMore(data.length >= 30)
setPage(pg)
} catch {}
finally { setLoading(false); setLoadingMore(false) }
}, [])
React.useEffect(() => { load(0) }, [])
const loadMore = () => { if (!loadingMore && hasMore) load(page + 1) }
const renderItem = ({ item }: { item: any }) => (
<View style={s.card}>
<View style={s.row}>
<Text style={s.actor}>{item.actor}</Text>
<Text style={s.time}>{item.created_at?.slice(0, 16).replace('T', ' ')}</Text>
</View>
<Text style={s.action}>{item.action}</Text>
<Text style={s.detail} numberOfLines={2}>{item.detail}</Text>
<Text style={s.ip}>IP: {maskIp(item.ip_hash ?? item.ip_addr)}</Text>
</View>
)
if (loading) return <ActivityIndicator style={{ flex: 1 }} color={COLORS.accent} />
return (
<FlatList
data={items}
keyExtractor={(_, i) => String(i)}
renderItem={renderItem}
refreshControl={<RefreshControl refreshing={loading} onRefresh={() => load(0)} />}
onEndReached={loadMore}
onEndReachedThreshold={0.3}
ListFooterComponent={loadingMore ? <ActivityIndicator color={COLORS.accent} style={{ padding: 16 }} /> : null}
ListEmptyComponent={<Text style={s.empty}> .</Text>}
contentContainerStyle={{ padding: 12 }}
style={{ backgroundColor: COLORS.bg }}
/>
)
}
const s = StyleSheet.create({
card: { backgroundColor: '#fff', borderRadius: 8, padding: 12, marginBottom: 6, elevation: 1 },
row: { flexDirection: 'row', justifyContent: 'space-between', marginBottom: 4 },
actor: { fontSize: 13, fontWeight: '700', color: COLORS.text },
time: { fontSize: 11, color: COLORS.muted },
action: { fontSize: 12, color: COLORS.accent, fontWeight: '600', marginBottom: 2 },
detail: { fontSize: 12, color: COLORS.text },
ip: { fontSize: 11, color: COLORS.muted, marginTop: 4 },
empty: { textAlign: 'center', color: COLORS.muted, marginTop: 40 },
})