|
|
@@ -0,0 +1,1000 @@
|
|
|
+import React, { useState, useEffect, useCallback, useMemo } from 'react';
|
|
|
+import {
|
|
|
+ View,
|
|
|
+ Text,
|
|
|
+ StyleSheet,
|
|
|
+ TouchableOpacity,
|
|
|
+ Alert
|
|
|
+} from 'react-native';
|
|
|
+import { Ionicons } from '@expo/vector-icons';
|
|
|
+import { useFocusEffect } from '@react-navigation/native';
|
|
|
+import * as FileSystem from 'expo-file-system/legacy';
|
|
|
+import Share from 'react-native-share';
|
|
|
+import { Header, Input, PageWrapper } from 'src/components';
|
|
|
+import { Colors } from 'src/theme';
|
|
|
+import { getFontSize } from 'src/utils';
|
|
|
+import {
|
|
|
+ getTelemetryEvents,
|
|
|
+ clearTelemetryEvents,
|
|
|
+ getAmbientCacheSize,
|
|
|
+ sanitizeUrlForDisplay,
|
|
|
+ APP_OPEN_TIME,
|
|
|
+ TelemetryEvent
|
|
|
+} from 'src/utils/telemetry';
|
|
|
+import { FlashList } from '@shopify/flash-list';
|
|
|
+
|
|
|
+const formatBytes = (bytes: number, decimals = 2) => {
|
|
|
+ if (!bytes || bytes === 0) return '0 Bytes';
|
|
|
+ const k = 1024;
|
|
|
+ const dm = decimals < 0 ? 0 : decimals;
|
|
|
+ const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'];
|
|
|
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
|
+ return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i];
|
|
|
+};
|
|
|
+
|
|
|
+interface GroupedTelemetry {
|
|
|
+ urlKey: string;
|
|
|
+ method: string;
|
|
|
+ caller: string;
|
|
|
+ count: number;
|
|
|
+ totalSize: number;
|
|
|
+ avgDuration: number;
|
|
|
+ type: string;
|
|
|
+ status: number;
|
|
|
+}
|
|
|
+
|
|
|
+export default function DataReportScreen() {
|
|
|
+ const [events, setEvents] = useState<TelemetryEvent[]>([]);
|
|
|
+ const [ambientCacheSize, setAmbientCacheSize] = useState(0);
|
|
|
+ const [searchQuery, setSearchQuery] = useState('');
|
|
|
+ const [activeTab, setActiveTab] = useState<'all' | 'api_request' | 'file_download'>('all');
|
|
|
+ const [expandedId, setExpandedId] = useState<string | null>(null);
|
|
|
+ const [viewMode, setViewMode] = useState<'grouped' | 'detailed'>('grouped');
|
|
|
+ const [timeRange, setTimeRange] = useState<'app_open' | 'hour' | 'day' | 'week'>('app_open');
|
|
|
+ const [expandedModules, setExpandedModules] = useState<Record<string, boolean>>({});
|
|
|
+
|
|
|
+ const loadData = useCallback(() => {
|
|
|
+ const allEvents = getTelemetryEvents();
|
|
|
+ setEvents(allEvents);
|
|
|
+ getAmbientCacheSize().then((size) => setAmbientCacheSize(size));
|
|
|
+ }, []);
|
|
|
+
|
|
|
+ useFocusEffect(
|
|
|
+ useCallback(() => {
|
|
|
+ loadData();
|
|
|
+ }, [loadData])
|
|
|
+ );
|
|
|
+
|
|
|
+ useEffect(() => {
|
|
|
+ const timer = setInterval(() => {
|
|
|
+ loadData();
|
|
|
+ }, 3000);
|
|
|
+ return () => clearInterval(timer);
|
|
|
+ }, [loadData]);
|
|
|
+
|
|
|
+ const handleClear = () => {
|
|
|
+ Alert.alert(
|
|
|
+ 'Clear Logs',
|
|
|
+ 'This will delete all recorded traffic logs. Proceed?',
|
|
|
+ [
|
|
|
+ { text: 'Cancel', style: 'cancel' },
|
|
|
+ {
|
|
|
+ text: 'Clear',
|
|
|
+ style: 'destructive',
|
|
|
+ onPress: () => {
|
|
|
+ clearTelemetryEvents();
|
|
|
+ loadData();
|
|
|
+ }
|
|
|
+ }
|
|
|
+ ]
|
|
|
+ );
|
|
|
+ };
|
|
|
+
|
|
|
+ const handleExportCSV = async () => {
|
|
|
+ if (events.length === 0) {
|
|
|
+ Alert.alert('No Logs', 'There are no logs to export.');
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ let csv = 'Timestamp,Type,Caller,Method,URL,ReqSize,ResSize,Status,DurationMs\n';
|
|
|
+ events.forEach((e) => {
|
|
|
+ const timeStr = new Date(e.timestamp).toISOString();
|
|
|
+ const urlEscaped = e.url.replace(/"/g, '""');
|
|
|
+ csv += `"${timeStr}","${e.type}","${e.caller}","${e.method || ''}","${urlEscaped}",${e.reqSize},${e.resSize},${e.status},${e.duration}\n`;
|
|
|
+ });
|
|
|
+
|
|
|
+ try {
|
|
|
+ const filename = `nomadmania_traffic_report_${Date.now()}.csv`;
|
|
|
+ const fileUri = `${FileSystem.cacheDirectory}${filename}`;
|
|
|
+
|
|
|
+ await FileSystem.writeAsStringAsync(fileUri, csv, {
|
|
|
+ encoding: FileSystem.EncodingType.UTF8
|
|
|
+ });
|
|
|
+
|
|
|
+ await Share.open({
|
|
|
+ url: fileUri,
|
|
|
+ type: 'text/csv',
|
|
|
+ filename: 'nomadmania_traffic_report',
|
|
|
+ title: 'Share CSV Traffic Report'
|
|
|
+ });
|
|
|
+ } catch (error: any) {
|
|
|
+ if (error && error.message && error.message.includes('User cancelled')) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ console.error('Failed to export CSV file:', error);
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ const toggleExpand = (id: string) => {
|
|
|
+ setExpandedId(expandedId === id ? null : id);
|
|
|
+ };
|
|
|
+
|
|
|
+ const toggleModuleExpand = (moduleName: string) => {
|
|
|
+ setExpandedModules((prev) => ({
|
|
|
+ ...prev,
|
|
|
+ [moduleName]: !prev[moduleName]
|
|
|
+ }));
|
|
|
+ };
|
|
|
+
|
|
|
+ const timeFilteredEvents = useMemo(() => {
|
|
|
+ let cutoff = 0;
|
|
|
+ if (timeRange === 'app_open') {
|
|
|
+ cutoff = APP_OPEN_TIME;
|
|
|
+ } else if (timeRange === 'hour') {
|
|
|
+ cutoff = Date.now() - 3600 * 1000;
|
|
|
+ } else if (timeRange === 'day') {
|
|
|
+ cutoff = Date.now() - 24 * 3600 * 1000;
|
|
|
+ } else {
|
|
|
+ cutoff = 0;
|
|
|
+ }
|
|
|
+ return events.filter((e) => e.timestamp >= cutoff);
|
|
|
+ }, [events, timeRange]);
|
|
|
+
|
|
|
+ const dynamicSummary = useMemo(() => {
|
|
|
+ let totalDownloaded = 0;
|
|
|
+ let totalUploaded = 0;
|
|
|
+ const callerBreakdown: Record<string, { count: number; downloaded: number }> = {};
|
|
|
+
|
|
|
+ timeFilteredEvents.forEach((event) => {
|
|
|
+ totalDownloaded += event.resSize;
|
|
|
+ totalUploaded += event.reqSize;
|
|
|
+
|
|
|
+ const caller = event.caller || 'Other';
|
|
|
+ if (!callerBreakdown[caller]) {
|
|
|
+ callerBreakdown[caller] = { count: 0, downloaded: 0 };
|
|
|
+ }
|
|
|
+ callerBreakdown[caller].count += 1;
|
|
|
+ callerBreakdown[caller].downloaded += event.resSize;
|
|
|
+ });
|
|
|
+
|
|
|
+ const breakdown = Object.entries(callerBreakdown)
|
|
|
+ .map(([name, data]) => ({
|
|
|
+ name,
|
|
|
+ count: data.count,
|
|
|
+ downloaded: data.downloaded
|
|
|
+ }))
|
|
|
+ .sort((a, b) => b.downloaded - a.downloaded);
|
|
|
+
|
|
|
+ return {
|
|
|
+ totalDownloaded,
|
|
|
+ totalUploaded,
|
|
|
+ totalEvents: timeFilteredEvents.length,
|
|
|
+ callerBreakdown: breakdown
|
|
|
+ };
|
|
|
+ }, [timeFilteredEvents]);
|
|
|
+
|
|
|
+ const filteredEvents = useMemo(() => {
|
|
|
+ return timeFilteredEvents.filter((e) => {
|
|
|
+ const matchesTab = activeTab === 'all' || e.type === activeTab;
|
|
|
+ const matchesSearch =
|
|
|
+ e.url.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
|
|
+ e.caller.toLowerCase().includes(searchQuery.toLowerCase());
|
|
|
+ return matchesTab && matchesSearch;
|
|
|
+ });
|
|
|
+ }, [timeFilteredEvents, activeTab, searchQuery]);
|
|
|
+
|
|
|
+ const groupedEvents = useMemo(() => {
|
|
|
+ const groups: Record<string, GroupedTelemetry> = {};
|
|
|
+
|
|
|
+ filteredEvents.forEach((e) => {
|
|
|
+ const displayUrl = sanitizeUrlForDisplay(e.url);
|
|
|
+ const key = `${e.method || ''}:${displayUrl}`;
|
|
|
+
|
|
|
+ if (!groups[key]) {
|
|
|
+ groups[key] = {
|
|
|
+ urlKey: displayUrl,
|
|
|
+ method: e.method || 'GET',
|
|
|
+ caller: e.caller,
|
|
|
+ count: 0,
|
|
|
+ totalSize: 0,
|
|
|
+ avgDuration: 0,
|
|
|
+ type: e.type,
|
|
|
+ status: e.status
|
|
|
+ };
|
|
|
+ }
|
|
|
+
|
|
|
+ const g = groups[key];
|
|
|
+ g.count += 1;
|
|
|
+ g.totalSize += (e.resSize + e.reqSize);
|
|
|
+ g.avgDuration += e.duration;
|
|
|
+ g.status = e.status;
|
|
|
+ });
|
|
|
+
|
|
|
+ return Object.values(groups).map((g) => ({
|
|
|
+ ...g,
|
|
|
+ avgDuration: Math.round(g.avgDuration / g.count)
|
|
|
+ })).sort((a, b) => b.totalSize - a.totalSize);
|
|
|
+ }, [filteredEvents]);
|
|
|
+
|
|
|
+ const modulesBreakdown = useMemo(() => {
|
|
|
+ const groups: Record<string, { name: string; totalSize: number; count: number; items: GroupedTelemetry[] }> = {};
|
|
|
+
|
|
|
+ groupedEvents.forEach((item) => {
|
|
|
+ const moduleName = item.caller;
|
|
|
+ if (!groups[moduleName]) {
|
|
|
+ groups[moduleName] = {
|
|
|
+ name: moduleName,
|
|
|
+ totalSize: 0,
|
|
|
+ count: 0,
|
|
|
+ items: []
|
|
|
+ };
|
|
|
+ }
|
|
|
+ const g = groups[moduleName];
|
|
|
+ g.totalSize += item.totalSize;
|
|
|
+ g.count += item.count;
|
|
|
+ g.items.push(item);
|
|
|
+ });
|
|
|
+
|
|
|
+ return Object.values(groups).sort((a, b) => b.totalSize - a.totalSize);
|
|
|
+ }, [groupedEvents]);
|
|
|
+
|
|
|
+ const renderEventItem = ({ item }: { item: TelemetryEvent }) => {
|
|
|
+ const isExpanded = expandedId === item.id;
|
|
|
+ const dateStr = new Date(item.timestamp).toLocaleTimeString();
|
|
|
+ const totalSize = item.resSize + item.reqSize;
|
|
|
+ const isError = item.status >= 400 || item.status === 0;
|
|
|
+
|
|
|
+ return (
|
|
|
+ <TouchableOpacity
|
|
|
+ style={[styles.eventCard, isExpanded && styles.eventCardExpanded]}
|
|
|
+ onPress={() => toggleExpand(item.id)}
|
|
|
+ activeOpacity={0.7}
|
|
|
+ >
|
|
|
+ <View style={styles.eventHeader}>
|
|
|
+ <View style={styles.eventLeft}>
|
|
|
+ <View
|
|
|
+ style={[
|
|
|
+ styles.typeBadge,
|
|
|
+ item.type === 'file_download' ? styles.badgeDownload : styles.badgeApi
|
|
|
+ ]}
|
|
|
+ >
|
|
|
+ <Text style={styles.typeBadgeText}>
|
|
|
+ {item.type === 'file_download' ? 'FILE' : 'API'}
|
|
|
+ </Text>
|
|
|
+ </View>
|
|
|
+ <View style={styles.eventTitleContainer}>
|
|
|
+ <Text style={styles.eventCaller} numberOfLines={1}>
|
|
|
+ {item.caller}
|
|
|
+ </Text>
|
|
|
+ <Text style={styles.eventUrlShort} numberOfLines={1}>
|
|
|
+ {item.url.split('/').pop() || item.url}
|
|
|
+ </Text>
|
|
|
+ </View>
|
|
|
+ </View>
|
|
|
+ <View style={styles.eventRight}>
|
|
|
+ <Text style={styles.eventSize}>{formatBytes(totalSize)}</Text>
|
|
|
+ <Text style={styles.eventTime}>{dateStr}</Text>
|
|
|
+ </View>
|
|
|
+ </View>
|
|
|
+
|
|
|
+ {isExpanded && (
|
|
|
+ <View style={styles.expandedContent}>
|
|
|
+ <View style={styles.divider} />
|
|
|
+ <Text style={styles.detailLabel}>Full URL / Path:</Text>
|
|
|
+ <Text style={styles.detailValueSelectable} selectable>
|
|
|
+ {item.url}
|
|
|
+ </Text>
|
|
|
+
|
|
|
+ <View style={styles.detailGrid}>
|
|
|
+ <View style={styles.gridItem}>
|
|
|
+ <Text style={styles.detailLabel}>Method:</Text>
|
|
|
+ <Text style={styles.detailValue}>{item.method || 'GET'}</Text>
|
|
|
+ </View>
|
|
|
+ <View style={styles.gridItem}>
|
|
|
+ <Text style={styles.detailLabel}>Status:</Text>
|
|
|
+ <Text style={[styles.detailValue, isError ? styles.textRed : styles.textGreen]}>
|
|
|
+ {item.status}
|
|
|
+ </Text>
|
|
|
+ </View>
|
|
|
+ <View style={styles.gridItem}>
|
|
|
+ <Text style={styles.detailLabel}>Duration:</Text>
|
|
|
+ <Text style={styles.detailValue}>{item.duration}ms</Text>
|
|
|
+ </View>
|
|
|
+ </View>
|
|
|
+
|
|
|
+ <View style={styles.detailGrid}>
|
|
|
+ <View style={styles.gridItem}>
|
|
|
+ <Text style={styles.detailLabel}>Upload size:</Text>
|
|
|
+ <Text style={styles.detailValue}>{formatBytes(item.reqSize)}</Text>
|
|
|
+ </View>
|
|
|
+ <View style={styles.gridItem}>
|
|
|
+ <Text style={styles.detailLabel}>Download size:</Text>
|
|
|
+ <Text style={styles.detailValue}>{formatBytes(item.resSize)}</Text>
|
|
|
+ </View>
|
|
|
+ <View style={styles.gridItem}>
|
|
|
+ <Text style={styles.detailLabel}>Timestamp:</Text>
|
|
|
+ <Text style={styles.detailValue}>
|
|
|
+ {new Date(item.timestamp).toLocaleString()}
|
|
|
+ </Text>
|
|
|
+ </View>
|
|
|
+ </View>
|
|
|
+ </View>
|
|
|
+ )}
|
|
|
+ </TouchableOpacity>
|
|
|
+ );
|
|
|
+ };
|
|
|
+
|
|
|
+ const renderModuleItem = ({ item }: { item: any }) => {
|
|
|
+ const isExpanded = !!expandedModules[item.name];
|
|
|
+ const totalSize = item.totalSize;
|
|
|
+
|
|
|
+ return (
|
|
|
+ <View style={styles.moduleWrapper}>
|
|
|
+ <TouchableOpacity
|
|
|
+ style={[styles.moduleHeader, isExpanded && styles.moduleHeaderExpanded]}
|
|
|
+ onPress={() => toggleModuleExpand(item.name)}
|
|
|
+ activeOpacity={0.7}
|
|
|
+ >
|
|
|
+ <View style={styles.moduleHeaderLeft}>
|
|
|
+ <Ionicons
|
|
|
+ name={isExpanded ? 'chevron-down-outline' : 'chevron-forward-outline'}
|
|
|
+ size={18}
|
|
|
+ color={Colors.DARK_BLUE}
|
|
|
+ style={{ marginRight: 8 }}
|
|
|
+ />
|
|
|
+ <Text style={styles.moduleName} numberOfLines={1}>{item.name}</Text>
|
|
|
+ <View style={styles.countBadge}>
|
|
|
+ <Text style={styles.countBadgeText}>{item.count}</Text>
|
|
|
+ </View>
|
|
|
+ </View>
|
|
|
+ <Text style={styles.moduleSize}>{formatBytes(totalSize)}</Text>
|
|
|
+ </TouchableOpacity>
|
|
|
+
|
|
|
+ {isExpanded && (
|
|
|
+ <View style={styles.moduleItemsContainer}>
|
|
|
+ {item.items.map((subItem: GroupedTelemetry, index: number) => {
|
|
|
+ const isError = subItem.status >= 400 || subItem.status === 0;
|
|
|
+ return (
|
|
|
+ <View key={index} style={styles.subItemCard}>
|
|
|
+ <View style={styles.subItemHeader}>
|
|
|
+ <View style={{ flex: 1, marginRight: 8 }}>
|
|
|
+ <Text style={styles.subItemUrl} numberOfLines={2}>
|
|
|
+ {subItem.method} {subItem.urlKey}
|
|
|
+ </Text>
|
|
|
+ </View>
|
|
|
+ <View style={{ alignItems: 'flex-end' }}>
|
|
|
+ <Text style={styles.subItemSize}>{formatBytes(subItem.totalSize)}</Text>
|
|
|
+ <Text style={styles.subItemCount}>x{subItem.count}</Text>
|
|
|
+ </View>
|
|
|
+ </View>
|
|
|
+ <View style={styles.subItemFooter}>
|
|
|
+ <Text style={[styles.subItemStatus, isError ? styles.textRed : styles.textGreen]}>
|
|
|
+ Status: {subItem.status}
|
|
|
+ </Text>
|
|
|
+ <Text style={styles.subItemDuration}>
|
|
|
+ avg {subItem.avgDuration}ms
|
|
|
+ </Text>
|
|
|
+ </View>
|
|
|
+ </View>
|
|
|
+ );
|
|
|
+ })}
|
|
|
+ </View>
|
|
|
+ )}
|
|
|
+ </View>
|
|
|
+ );
|
|
|
+ };
|
|
|
+
|
|
|
+ const renderHeader = () => {
|
|
|
+ return (
|
|
|
+ <View style={styles.headerContainer}>
|
|
|
+ <View style={styles.summaryContainer}>
|
|
|
+ <View style={styles.summaryRow}>
|
|
|
+ <View style={styles.summaryCard}>
|
|
|
+ <Ionicons name="cloud-download-outline" size={20} color={Colors.ORANGE} />
|
|
|
+ <Text style={styles.summaryValue}>
|
|
|
+ {formatBytes(dynamicSummary.totalDownloaded)}
|
|
|
+ </Text>
|
|
|
+ <Text style={styles.summaryLabel}>Downloaded</Text>
|
|
|
+ </View>
|
|
|
+
|
|
|
+ <View style={styles.summaryCard}>
|
|
|
+ <Ionicons name="swap-vertical-outline" size={20} color={Colors.DARK_BLUE} />
|
|
|
+ <Text style={styles.summaryValue}>{dynamicSummary.totalEvents}</Text>
|
|
|
+ <Text style={styles.summaryLabel}>Requests</Text>
|
|
|
+ </View>
|
|
|
+
|
|
|
+ <View style={styles.summaryCard}>
|
|
|
+ <Ionicons name="map-outline" size={20} color="#28A745" />
|
|
|
+ <Text style={styles.summaryValue}>
|
|
|
+ {formatBytes(ambientCacheSize)}
|
|
|
+ </Text>
|
|
|
+ <Text style={styles.summaryLabel}>Map Cache</Text>
|
|
|
+ </View>
|
|
|
+ </View>
|
|
|
+
|
|
|
+ {dynamicSummary?.callerBreakdown?.length > 0 && (
|
|
|
+ <View style={styles.breakdownContainer}>
|
|
|
+ <Text style={styles.sectionTitle}>Breakdown by Module</Text>
|
|
|
+ {dynamicSummary.callerBreakdown.slice(0, 5).map((callerItem: any, index: number) => {
|
|
|
+ const percentage = dynamicSummary.totalDownloaded > 0
|
|
|
+ ? (callerItem.downloaded / dynamicSummary.totalDownloaded) * 100
|
|
|
+ : 0;
|
|
|
+ return (
|
|
|
+ <View key={callerItem.name} style={styles.breakdownRow}>
|
|
|
+ <View style={styles.breakdownTextRow}>
|
|
|
+ <Text style={styles.breakdownName} numberOfLines={1}>{callerItem.name}</Text>
|
|
|
+ <Text style={styles.breakdownValue}>
|
|
|
+ {formatBytes(callerItem.downloaded)} ({Math.round(percentage)}%)
|
|
|
+ </Text>
|
|
|
+ </View>
|
|
|
+ <View style={styles.progressBarBg}>
|
|
|
+ <View
|
|
|
+ style={[
|
|
|
+ styles.progressBarFill,
|
|
|
+ { width: `${percentage}%`, backgroundColor: index === 0 ? Colors.ORANGE : Colors.DARK_BLUE }
|
|
|
+ ]}
|
|
|
+ />
|
|
|
+ </View>
|
|
|
+ </View>
|
|
|
+ );
|
|
|
+ })}
|
|
|
+ </View>
|
|
|
+ )}
|
|
|
+ </View>
|
|
|
+
|
|
|
+ <View style={styles.timeRangeContainer}>
|
|
|
+ <Text style={styles.timeRangeLabel}>Report Period:</Text>
|
|
|
+ <View style={styles.timeRangeButtons}>
|
|
|
+ {(
|
|
|
+ [
|
|
|
+ { label: 'Session', value: 'app_open' },
|
|
|
+ { label: '1 Hour', value: 'hour' },
|
|
|
+ { label: '24 Hours', value: 'day' },
|
|
|
+ { label: '7 Days', value: 'week' }
|
|
|
+ ] as const
|
|
|
+ ).map((opt) => (
|
|
|
+ <TouchableOpacity
|
|
|
+ key={opt.value}
|
|
|
+ style={[styles.timeBtn, timeRange === opt.value && styles.timeBtnActive]}
|
|
|
+ onPress={() => setTimeRange(opt.value)}
|
|
|
+ >
|
|
|
+ <Text style={[styles.timeBtnText, timeRange === opt.value && styles.timeBtnTextActive]}>
|
|
|
+ {opt.label}
|
|
|
+ </Text>
|
|
|
+ </TouchableOpacity>
|
|
|
+ ))}
|
|
|
+ </View>
|
|
|
+ </View>
|
|
|
+
|
|
|
+ <View style={styles.actionRow}>
|
|
|
+ <TouchableOpacity style={[styles.actionBtn, styles.btnOutline]} onPress={handleExportCSV}>
|
|
|
+ <Ionicons name="share-outline" size={16} color={Colors.DARK_BLUE} style={{ marginRight: 6 }} />
|
|
|
+ <Text style={styles.btnOutlineText}>Share CSV</Text>
|
|
|
+ </TouchableOpacity>
|
|
|
+
|
|
|
+ <TouchableOpacity style={[styles.actionBtn, styles.btnDestructive]} onPress={handleClear}>
|
|
|
+ <Ionicons name="trash-outline" size={16} color={Colors.WHITE} style={{ marginRight: 6 }} />
|
|
|
+ <Text style={styles.btnDestructiveText}>Clear Logs</Text>
|
|
|
+ </TouchableOpacity>
|
|
|
+ </View>
|
|
|
+
|
|
|
+ <View style={styles.modeContainer}>
|
|
|
+ <TouchableOpacity
|
|
|
+ style={[styles.modeButton, viewMode === 'grouped' && styles.modeActive]}
|
|
|
+ onPress={() => setViewMode('grouped')}
|
|
|
+ >
|
|
|
+ <Text style={[styles.modeText, viewMode === 'grouped' && styles.modeTextActive]}>Grouped View</Text>
|
|
|
+ </TouchableOpacity>
|
|
|
+ <TouchableOpacity
|
|
|
+ style={[styles.modeButton, viewMode === 'detailed' && styles.modeActive]}
|
|
|
+ onPress={() => setViewMode('detailed')}
|
|
|
+ >
|
|
|
+ <Text style={[styles.modeText, viewMode === 'detailed' && styles.modeTextActive]}>Detailed Logs</Text>
|
|
|
+ </TouchableOpacity>
|
|
|
+ </View>
|
|
|
+
|
|
|
+ <View style={styles.tabContainer}>
|
|
|
+ <TouchableOpacity
|
|
|
+ style={[styles.tabButton, activeTab === 'all' && styles.tabActive]}
|
|
|
+ onPress={() => setActiveTab('all')}
|
|
|
+ >
|
|
|
+ <Text style={[styles.tabText, activeTab === 'all' && styles.tabTextActive]}>All</Text>
|
|
|
+ </TouchableOpacity>
|
|
|
+ <TouchableOpacity
|
|
|
+ style={[styles.tabButton, activeTab === 'api_request' && styles.tabActive]}
|
|
|
+ onPress={() => setActiveTab('api_request')}
|
|
|
+ >
|
|
|
+ <Text style={[styles.tabText, activeTab === 'api_request' && styles.tabTextActive]}>APIs</Text>
|
|
|
+ </TouchableOpacity>
|
|
|
+ <TouchableOpacity
|
|
|
+ style={[styles.tabButton, activeTab === 'file_download' && styles.tabActive]}
|
|
|
+ onPress={() => setActiveTab('file_download')}
|
|
|
+ >
|
|
|
+ <Text style={[styles.tabText, activeTab === 'file_download' && styles.tabTextActive]}>Downloads</Text>
|
|
|
+ </TouchableOpacity>
|
|
|
+ </View>
|
|
|
+
|
|
|
+ <View style={styles.searchContainer}>
|
|
|
+ <Input
|
|
|
+ value={searchQuery}
|
|
|
+ onChange={setSearchQuery}
|
|
|
+ placeholder="Search by URL or module..."
|
|
|
+ inputMode="text"
|
|
|
+ />
|
|
|
+ </View>
|
|
|
+ </View>
|
|
|
+ );
|
|
|
+ };
|
|
|
+
|
|
|
+ return (
|
|
|
+ <PageWrapper style={styles.wrapperStyle}>
|
|
|
+ <Header label="Data Traffic Report" />
|
|
|
+
|
|
|
+ <FlashList
|
|
|
+ estimatedItemSize={50}
|
|
|
+ style={styles.flexOne}
|
|
|
+ data={viewMode === 'grouped' ? modulesBreakdown : filteredEvents}
|
|
|
+ renderItem={viewMode === 'grouped' ? renderModuleItem : renderEventItem}
|
|
|
+ keyExtractor={(item) => viewMode === 'grouped' ? (item as any).name : (item as TelemetryEvent).id}
|
|
|
+ ListHeaderComponent={renderHeader}
|
|
|
+ contentContainerStyle={styles.listContainer}
|
|
|
+ ListEmptyComponent={
|
|
|
+ <View style={styles.emptyContainer}>
|
|
|
+ <Ionicons name="bar-chart-outline" size={40} color={Colors.LIGHT_GRAY} />
|
|
|
+ <Text style={styles.emptyText}>No data traffic recorded yet</Text>
|
|
|
+ <Text style={styles.emptySubText}>
|
|
|
+ Any API calls or database downloads will show up here in real-time.
|
|
|
+ </Text>
|
|
|
+ </View>
|
|
|
+ }
|
|
|
+ />
|
|
|
+ </PageWrapper>
|
|
|
+ );
|
|
|
+}
|
|
|
+
|
|
|
+const styles = StyleSheet.create({
|
|
|
+ flexOne: {
|
|
|
+ flex: 1
|
|
|
+ },
|
|
|
+ wrapperStyle: {
|
|
|
+ flex: 1,
|
|
|
+ },
|
|
|
+ headerContainer: {
|
|
|
+ paddingHorizontal: 2
|
|
|
+ },
|
|
|
+ summaryContainer: {
|
|
|
+ backgroundColor: Colors.FILL_LIGHT,
|
|
|
+ borderRadius: 8,
|
|
|
+ padding: 16,
|
|
|
+ marginBottom: 12
|
|
|
+ },
|
|
|
+ summaryRow: {
|
|
|
+ flexDirection: 'row',
|
|
|
+ justifyContent: 'space-between',
|
|
|
+ gap: 10
|
|
|
+ },
|
|
|
+ summaryCard: {
|
|
|
+ flex: 1,
|
|
|
+ backgroundColor: Colors.WHITE,
|
|
|
+ borderRadius: 6,
|
|
|
+ padding: 10,
|
|
|
+ alignItems: 'center',
|
|
|
+ borderColor: '#eef2f3',
|
|
|
+ borderWidth: 1
|
|
|
+ },
|
|
|
+ summaryValue: {
|
|
|
+ fontSize: getFontSize(13),
|
|
|
+ fontFamily: 'redhat-700',
|
|
|
+ color: Colors.DARK_BLUE,
|
|
|
+ marginTop: 4
|
|
|
+ },
|
|
|
+ summaryLabel: {
|
|
|
+ fontSize: getFontSize(10),
|
|
|
+ color: Colors.TEXT_GRAY,
|
|
|
+ marginTop: 2
|
|
|
+ },
|
|
|
+ breakdownContainer: {
|
|
|
+ marginTop: 16,
|
|
|
+ borderTopWidth: 1,
|
|
|
+ borderTopColor: 'rgba(0, 0, 0, 0.05)',
|
|
|
+ paddingTop: 12
|
|
|
+ },
|
|
|
+ sectionTitle: {
|
|
|
+ fontSize: getFontSize(12),
|
|
|
+ fontFamily: 'redhat-700',
|
|
|
+ color: Colors.DARK_BLUE,
|
|
|
+ marginBottom: 10
|
|
|
+ },
|
|
|
+ breakdownRow: {
|
|
|
+ marginBottom: 8
|
|
|
+ },
|
|
|
+ breakdownTextRow: {
|
|
|
+ flexDirection: 'row',
|
|
|
+ justifyContent: 'space-between',
|
|
|
+ marginBottom: 4
|
|
|
+ },
|
|
|
+ breakdownName: {
|
|
|
+ fontSize: getFontSize(11),
|
|
|
+ color: Colors.DARK_BLUE,
|
|
|
+ fontWeight: '600',
|
|
|
+ flex: 1,
|
|
|
+ marginRight: 8
|
|
|
+ },
|
|
|
+ breakdownValue: {
|
|
|
+ fontSize: getFontSize(11),
|
|
|
+ color: Colors.TEXT_GRAY
|
|
|
+ },
|
|
|
+ progressBarBg: {
|
|
|
+ height: 6,
|
|
|
+ backgroundColor: 'rgba(0,0,0,0.06)',
|
|
|
+ borderRadius: 3,
|
|
|
+ overflow: 'hidden'
|
|
|
+ },
|
|
|
+ progressBarFill: {
|
|
|
+ height: '100%',
|
|
|
+ borderRadius: 3
|
|
|
+ },
|
|
|
+ timeRangeContainer: {
|
|
|
+ marginBottom: 12,
|
|
|
+ backgroundColor: Colors.FILL_LIGHT,
|
|
|
+ borderRadius: 8,
|
|
|
+ padding: 12
|
|
|
+ },
|
|
|
+ timeRangeLabel: {
|
|
|
+ fontSize: getFontSize(11),
|
|
|
+ fontFamily: 'redhat-700',
|
|
|
+ color: Colors.DARK_BLUE,
|
|
|
+ marginBottom: 6
|
|
|
+ },
|
|
|
+ timeRangeButtons: {
|
|
|
+ flexDirection: 'row',
|
|
|
+ gap: 6
|
|
|
+ },
|
|
|
+ timeBtn: {
|
|
|
+ flex: 1,
|
|
|
+ paddingVertical: 6,
|
|
|
+ borderRadius: 4,
|
|
|
+ backgroundColor: Colors.WHITE,
|
|
|
+ borderWidth: 1,
|
|
|
+ borderColor: '#EAEEF0',
|
|
|
+ alignItems: 'center'
|
|
|
+ },
|
|
|
+ timeBtnActive: {
|
|
|
+ backgroundColor: Colors.DARK_BLUE,
|
|
|
+ borderColor: Colors.DARK_BLUE
|
|
|
+ },
|
|
|
+ timeBtnText: {
|
|
|
+ fontSize: getFontSize(10),
|
|
|
+ color: Colors.TEXT_GRAY,
|
|
|
+ fontFamily: 'montserrat-600'
|
|
|
+ },
|
|
|
+ timeBtnTextActive: {
|
|
|
+ color: Colors.WHITE,
|
|
|
+ fontFamily: 'montserrat-700'
|
|
|
+ },
|
|
|
+ actionRow: {
|
|
|
+ flexDirection: 'row',
|
|
|
+ justifyContent: 'space-between',
|
|
|
+ gap: 12,
|
|
|
+ marginBottom: 12
|
|
|
+ },
|
|
|
+ actionBtn: {
|
|
|
+ flex: 1,
|
|
|
+ flexDirection: 'row',
|
|
|
+ alignItems: 'center',
|
|
|
+ justifyContent: 'center',
|
|
|
+ paddingVertical: 10,
|
|
|
+ borderRadius: 4
|
|
|
+ },
|
|
|
+ btnOutline: {
|
|
|
+ borderColor: Colors.DARK_BLUE,
|
|
|
+ borderWidth: 1
|
|
|
+ },
|
|
|
+ btnOutlineText: {
|
|
|
+ color: Colors.DARK_BLUE,
|
|
|
+ fontSize: getFontSize(12),
|
|
|
+ fontFamily: 'redhat-700'
|
|
|
+ },
|
|
|
+ btnDestructive: {
|
|
|
+ backgroundColor: Colors.RED
|
|
|
+ },
|
|
|
+ btnDestructiveText: {
|
|
|
+ color: Colors.WHITE,
|
|
|
+ fontSize: getFontSize(12),
|
|
|
+ fontFamily: 'redhat-700'
|
|
|
+ },
|
|
|
+ modeContainer: {
|
|
|
+ flexDirection: 'row',
|
|
|
+ borderWidth: 1,
|
|
|
+ borderColor: Colors.BORDER_LIGHT,
|
|
|
+ borderRadius: 6,
|
|
|
+ overflow: 'hidden',
|
|
|
+ marginBottom: 12
|
|
|
+ },
|
|
|
+ modeButton: {
|
|
|
+ flex: 1,
|
|
|
+ paddingVertical: 10,
|
|
|
+ alignItems: 'center',
|
|
|
+ backgroundColor: Colors.WHITE
|
|
|
+ },
|
|
|
+ modeActive: {
|
|
|
+ backgroundColor: Colors.DARK_BLUE
|
|
|
+ },
|
|
|
+ modeText: {
|
|
|
+ fontSize: getFontSize(12),
|
|
|
+ color: Colors.DARK_BLUE,
|
|
|
+ fontFamily: 'redhat-700'
|
|
|
+ },
|
|
|
+ modeTextActive: {
|
|
|
+ color: Colors.WHITE
|
|
|
+ },
|
|
|
+ tabContainer: {
|
|
|
+ flexDirection: 'row',
|
|
|
+ backgroundColor: '#EAEEF0',
|
|
|
+ borderRadius: 6,
|
|
|
+ padding: 3,
|
|
|
+ marginBottom: 12
|
|
|
+ },
|
|
|
+ tabButton: {
|
|
|
+ flex: 1,
|
|
|
+ paddingVertical: 8,
|
|
|
+ alignItems: 'center',
|
|
|
+ borderRadius: 4
|
|
|
+ },
|
|
|
+ tabActive: {
|
|
|
+ backgroundColor: Colors.WHITE
|
|
|
+ },
|
|
|
+ tabText: {
|
|
|
+ fontSize: getFontSize(12),
|
|
|
+ color: Colors.TEXT_GRAY,
|
|
|
+ fontFamily: 'montserrat-600'
|
|
|
+ },
|
|
|
+ tabTextActive: {
|
|
|
+ color: Colors.DARK_BLUE,
|
|
|
+ fontFamily: 'montserrat-700'
|
|
|
+ },
|
|
|
+ searchContainer: {
|
|
|
+ marginBottom: 10
|
|
|
+ },
|
|
|
+ listContainer: {
|
|
|
+ paddingBottom: 24
|
|
|
+ },
|
|
|
+ eventCard: {
|
|
|
+ backgroundColor: Colors.FILL_LIGHT,
|
|
|
+ borderRadius: 6,
|
|
|
+ padding: 12,
|
|
|
+ marginBottom: 8,
|
|
|
+ borderWidth: 1,
|
|
|
+ borderColor: 'transparent'
|
|
|
+ },
|
|
|
+ eventCardExpanded: {
|
|
|
+ borderColor: Colors.BORDER_LIGHT,
|
|
|
+ backgroundColor: Colors.WHITE
|
|
|
+ },
|
|
|
+ eventHeader: {
|
|
|
+ flexDirection: 'row',
|
|
|
+ justifyContent: 'space-between',
|
|
|
+ alignItems: 'center'
|
|
|
+ },
|
|
|
+ eventLeft: {
|
|
|
+ flexDirection: 'row',
|
|
|
+ alignItems: 'center',
|
|
|
+ flex: 1,
|
|
|
+ marginRight: 8
|
|
|
+ },
|
|
|
+ typeBadge: {
|
|
|
+ paddingHorizontal: 6,
|
|
|
+ paddingVertical: 3,
|
|
|
+ borderRadius: 4,
|
|
|
+ marginRight: 8
|
|
|
+ },
|
|
|
+ badgeApi: {
|
|
|
+ backgroundColor: '#EBF4F6'
|
|
|
+ },
|
|
|
+ badgeDownload: {
|
|
|
+ backgroundColor: '#FDF3E7'
|
|
|
+ },
|
|
|
+ typeBadgeText: {
|
|
|
+ fontSize: getFontSize(8),
|
|
|
+ fontFamily: 'redhat-700',
|
|
|
+ color: Colors.DARK_BLUE
|
|
|
+ },
|
|
|
+ eventTitleContainer: {
|
|
|
+ flex: 1
|
|
|
+ },
|
|
|
+ eventCaller: {
|
|
|
+ fontSize: getFontSize(12),
|
|
|
+ fontFamily: 'redhat-700',
|
|
|
+ color: Colors.DARK_BLUE
|
|
|
+ },
|
|
|
+ eventUrlShort: {
|
|
|
+ fontSize: getFontSize(10),
|
|
|
+ color: Colors.TEXT_GRAY,
|
|
|
+ marginTop: 1
|
|
|
+ },
|
|
|
+ eventRight: {
|
|
|
+ alignItems: 'flex-end'
|
|
|
+ },
|
|
|
+ eventSize: {
|
|
|
+ fontSize: getFontSize(12),
|
|
|
+ fontFamily: 'redhat-700',
|
|
|
+ color: Colors.DARK_BLUE
|
|
|
+ },
|
|
|
+ eventTime: {
|
|
|
+ fontSize: getFontSize(10),
|
|
|
+ color: Colors.TEXT_GRAY,
|
|
|
+ marginTop: 1
|
|
|
+ },
|
|
|
+ expandedContent: {
|
|
|
+ marginTop: 10
|
|
|
+ },
|
|
|
+ divider: {
|
|
|
+ height: 1,
|
|
|
+ backgroundColor: '#EAEEF0',
|
|
|
+ marginBottom: 10
|
|
|
+ },
|
|
|
+ detailLabel: {
|
|
|
+ fontSize: getFontSize(10),
|
|
|
+ color: Colors.TEXT_GRAY,
|
|
|
+ fontWeight: '600',
|
|
|
+ marginBottom: 2
|
|
|
+ },
|
|
|
+ detailValue: {
|
|
|
+ fontSize: getFontSize(11),
|
|
|
+ color: Colors.DARK_BLUE,
|
|
|
+ fontWeight: '500'
|
|
|
+ },
|
|
|
+ detailValueSelectable: {
|
|
|
+ fontSize: getFontSize(11),
|
|
|
+ color: Colors.DARK_BLUE,
|
|
|
+ fontWeight: '500',
|
|
|
+ backgroundColor: Colors.FILL_LIGHT,
|
|
|
+ padding: 6,
|
|
|
+ borderRadius: 4,
|
|
|
+ marginBottom: 10
|
|
|
+ },
|
|
|
+ detailGrid: {
|
|
|
+ flexDirection: 'row',
|
|
|
+ justifyContent: 'space-between',
|
|
|
+ marginBottom: 10
|
|
|
+ },
|
|
|
+ gridItem: {
|
|
|
+ flex: 1
|
|
|
+ },
|
|
|
+ textRed: {
|
|
|
+ color: Colors.RED,
|
|
|
+ fontFamily: 'redhat-700'
|
|
|
+ },
|
|
|
+ textGreen: {
|
|
|
+ color: '#28A745',
|
|
|
+ fontFamily: 'redhat-700'
|
|
|
+ },
|
|
|
+ emptyContainer: {
|
|
|
+ alignItems: 'center',
|
|
|
+ justifyContent: 'center',
|
|
|
+ paddingVertical: 60,
|
|
|
+ gap: 8
|
|
|
+ },
|
|
|
+ emptyText: {
|
|
|
+ fontSize: getFontSize(13),
|
|
|
+ fontFamily: 'redhat-700',
|
|
|
+ color: Colors.DARK_BLUE,
|
|
|
+ marginTop: 10
|
|
|
+ },
|
|
|
+ emptySubText: {
|
|
|
+ fontSize: getFontSize(11),
|
|
|
+ color: Colors.TEXT_GRAY,
|
|
|
+ textAlign: 'center',
|
|
|
+ paddingHorizontal: 20
|
|
|
+ },
|
|
|
+ moduleWrapper: {
|
|
|
+ marginBottom: 10,
|
|
|
+ borderRadius: 8,
|
|
|
+ overflow: 'hidden',
|
|
|
+ backgroundColor: Colors.FILL_LIGHT,
|
|
|
+ borderWidth: 1,
|
|
|
+ borderColor: '#EAEEF0'
|
|
|
+ },
|
|
|
+ moduleHeader: {
|
|
|
+ flexDirection: 'row',
|
|
|
+ justifyContent: 'space-between',
|
|
|
+ alignItems: 'center',
|
|
|
+ padding: 14,
|
|
|
+ backgroundColor: Colors.FILL_LIGHT
|
|
|
+ },
|
|
|
+ moduleHeaderExpanded: {
|
|
|
+ borderBottomWidth: 1,
|
|
|
+ borderBottomColor: '#EAEEF0',
|
|
|
+ backgroundColor: Colors.WHITE
|
|
|
+ },
|
|
|
+ moduleHeaderLeft: {
|
|
|
+ flexDirection: 'row',
|
|
|
+ alignItems: 'center',
|
|
|
+ flex: 1,
|
|
|
+ marginRight: 8
|
|
|
+ },
|
|
|
+ moduleName: {
|
|
|
+ fontSize: getFontSize(13),
|
|
|
+ fontFamily: 'redhat-700',
|
|
|
+ color: Colors.DARK_BLUE,
|
|
|
+ marginRight: 8,
|
|
|
+ flexShrink: 1
|
|
|
+ },
|
|
|
+ moduleSize: {
|
|
|
+ fontSize: getFontSize(13),
|
|
|
+ fontFamily: 'redhat-700',
|
|
|
+ color: Colors.DARK_BLUE
|
|
|
+ },
|
|
|
+ moduleItemsContainer: {
|
|
|
+ padding: 10,
|
|
|
+ backgroundColor: Colors.WHITE,
|
|
|
+ gap: 8
|
|
|
+ },
|
|
|
+ subItemCard: {
|
|
|
+ backgroundColor: Colors.FILL_LIGHT,
|
|
|
+ borderRadius: 6,
|
|
|
+ padding: 10,
|
|
|
+ borderWidth: 1,
|
|
|
+ borderColor: '#EAEEF0'
|
|
|
+ },
|
|
|
+ subItemHeader: {
|
|
|
+ flexDirection: 'row',
|
|
|
+ justifyContent: 'space-between',
|
|
|
+ alignItems: 'flex-start',
|
|
|
+ marginBottom: 6
|
|
|
+ },
|
|
|
+ subItemUrl: {
|
|
|
+ fontSize: getFontSize(11),
|
|
|
+ color: Colors.DARK_BLUE,
|
|
|
+ fontFamily: 'montserrat-600'
|
|
|
+ },
|
|
|
+ subItemSize: {
|
|
|
+ fontSize: getFontSize(11),
|
|
|
+ fontFamily: 'redhat-700',
|
|
|
+ color: Colors.DARK_BLUE
|
|
|
+ },
|
|
|
+ subItemCount: {
|
|
|
+ fontSize: getFontSize(10),
|
|
|
+ color: Colors.TEXT_GRAY,
|
|
|
+ marginTop: 1
|
|
|
+ },
|
|
|
+ subItemFooter: {
|
|
|
+ flexDirection: 'row',
|
|
|
+ justifyContent: 'space-between',
|
|
|
+ alignItems: 'center',
|
|
|
+ borderTopWidth: 1,
|
|
|
+ borderTopColor: '#EAEEF0',
|
|
|
+ paddingTop: 6,
|
|
|
+ marginTop: 2
|
|
|
+ },
|
|
|
+ subItemStatus: {
|
|
|
+ fontSize: getFontSize(10),
|
|
|
+ fontWeight: '600'
|
|
|
+ },
|
|
|
+ subItemDuration: {
|
|
|
+ fontSize: getFontSize(10),
|
|
|
+ color: Colors.TEXT_GRAY,
|
|
|
+ fontWeight: '500'
|
|
|
+ },
|
|
|
+ countBadge: {
|
|
|
+ backgroundColor: Colors.DARK_BLUE,
|
|
|
+ borderRadius: 10,
|
|
|
+ paddingHorizontal: 6,
|
|
|
+ paddingVertical: 2
|
|
|
+ },
|
|
|
+ countBadgeText: {
|
|
|
+ color: Colors.WHITE,
|
|
|
+ fontSize: getFontSize(9),
|
|
|
+ fontWeight: '700'
|
|
|
+ }
|
|
|
+});
|