import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../../../../core/config/api_endpoints.dart'; import '../../../../core/network/dio_client.dart'; import '../../../../core/theme/app_colors.dart'; import '../../../../core/utils/date_formatter.dart'; import '../../../../core/widgets/empty_state.dart'; import '../../../../core/widgets/status_badge.dart'; // --------------------------------------------------------------------------- // Phase 2C – Alerts page: list with severity/status filters + acknowledge // --------------------------------------------------------------------------- /// Fetches alert events from the backend. final alertEventsProvider = FutureProvider>>((ref) async { final dio = ref.watch(dioClientProvider); final response = await dio.get(ApiEndpoints.alertEvents); final data = response.data; if (data is List) { return data.cast>(); } if (data is Map && data.containsKey('items')) { return (data['items'] as List).cast>(); } return []; }); // --------------------------------------------------------------------------- // Alerts page – ConsumerStatefulWidget (needs local filter state) // --------------------------------------------------------------------------- class AlertsPage extends ConsumerStatefulWidget { const AlertsPage({super.key}); @override ConsumerState createState() => _AlertsPageState(); } class _AlertsPageState extends ConsumerState { String _severityFilter = 'all'; String _statusFilter = 'all'; static const _severities = ['all', 'info', 'warning', 'critical']; static const _statuses = ['all', 'firing', 'resolved', 'acknowledged']; List> _applyFilters(List> alerts) { return alerts.where((a) { final severity = (a['severity'] as String? ?? '').toLowerCase(); final status = (a['status'] as String? ?? '').toLowerCase(); if (_severityFilter != 'all' && severity != _severityFilter) return false; if (_statusFilter != 'all' && status != _statusFilter) return false; return true; }).toList(); } @override Widget build(BuildContext context) { final alertsAsync = ref.watch(alertEventsProvider); return Scaffold( appBar: AppBar( title: const Text('告警中心'), actions: [ IconButton( icon: const Icon(Icons.refresh), onPressed: () => ref.invalidate(alertEventsProvider), ), ], ), body: RefreshIndicator( onRefresh: () async => ref.invalidate(alertEventsProvider), child: Column( children: [ // -- Filter chips ------------------------------------------------ _buildFilterSection(), // -- Alert list -------------------------------------------------- Expanded( child: alertsAsync.when( data: (alerts) { final filtered = _applyFilters(alerts); if (filtered.isEmpty) { return const EmptyState( icon: Icons.notifications_off_outlined, title: '暂无告警', subtitle: '一切正常 — 没有匹配的告警', ); } return ListView.builder( padding: const EdgeInsets.all(12), itemCount: filtered.length, itemBuilder: (context, index) { final alert = filtered[index]; return _AlertCard( alert: alert, onAcknowledge: () => _acknowledgeAlert(alert), ); }, ); }, loading: () => const Center(child: CircularProgressIndicator()), error: (e, _) => Center( child: Padding( padding: const EdgeInsets.all(32), child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ const Icon(Icons.error_outline, color: AppColors.error, size: 48), const SizedBox(height: 12), const Text( '加载告警失败', style: TextStyle( fontSize: 16, fontWeight: FontWeight.w600), ), const SizedBox(height: 6), Text( e.toString(), style: const TextStyle( color: AppColors.textSecondary, fontSize: 13), textAlign: TextAlign.center, ), const SizedBox(height: 16), OutlinedButton.icon( onPressed: () => ref.invalidate(alertEventsProvider), icon: const Icon(Icons.refresh), label: const Text('重试'), ), ], ), ), ), ), ), ], ), ), ); } // ---------- Filter section ------------------------------------------------ Widget _buildFilterSection() { return Padding( padding: const EdgeInsets.fromLTRB(12, 8, 12, 0), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // Severity filter row SingleChildScrollView( scrollDirection: Axis.horizontal, child: Row( children: _severities.map((s) { final selected = _severityFilter == s; return Padding( padding: const EdgeInsets.only(right: 8), child: FilterChip( label: Text(s == 'all' ? '全部级别' : s), selected: selected, selectedColor: _severityChipColor(s).withOpacity(0.25), checkmarkColor: _severityChipColor(s), labelStyle: TextStyle( color: selected ? _severityChipColor(s) : AppColors.textSecondary, fontSize: 12, fontWeight: FontWeight.w500, ), backgroundColor: AppColors.surface, side: BorderSide( color: selected ? _severityChipColor(s).withOpacity(0.5) : AppColors.surfaceLight, ), onSelected: (_) => setState(() => _severityFilter = s), ), ); }).toList(), ), ), const SizedBox(height: 4), // Status filter row SingleChildScrollView( scrollDirection: Axis.horizontal, child: Row( children: _statuses.map((s) { final selected = _statusFilter == s; return Padding( padding: const EdgeInsets.only(right: 8), child: FilterChip( label: Text(s == 'all' ? '全部状态' : s), selected: selected, selectedColor: AppColors.primary.withOpacity(0.25), checkmarkColor: AppColors.primary, labelStyle: TextStyle( color: selected ? AppColors.primary : AppColors.textSecondary, fontSize: 12, fontWeight: FontWeight.w500, ), backgroundColor: AppColors.surface, side: BorderSide( color: selected ? AppColors.primary.withOpacity(0.5) : AppColors.surfaceLight, ), onSelected: (_) => setState(() => _statusFilter = s), ), ); }).toList(), ), ), const SizedBox(height: 4), ], ), ); } Color _severityChipColor(String severity) { switch (severity) { case 'info': return AppColors.info; case 'warning': return AppColors.warning; case 'critical': return AppColors.error; default: return AppColors.primary; } } // ---------- Acknowledge action -------------------------------------------- Future _acknowledgeAlert(Map alert) async { final id = alert['id']?.toString(); if (id == null) return; try { final dio = ref.read(dioClientProvider); await dio.patch( '${ApiEndpoints.alertEvents}/$id', data: {'status': 'acknowledged'}, ); ref.invalidate(alertEventsProvider); if (mounted) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text('告警已确认'), backgroundColor: AppColors.success, ), ); } } catch (e) { if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text('确认失败: $e'), backgroundColor: AppColors.error, ), ); } } } } // --------------------------------------------------------------------------- // Alert card widget // --------------------------------------------------------------------------- class _AlertCard extends StatelessWidget { final Map alert; final VoidCallback onAcknowledge; const _AlertCard({required this.alert, required this.onAcknowledge}); @override Widget build(BuildContext context) { final message = alert['message'] as String? ?? alert['name'] as String? ?? 'No message'; final severity = (alert['severity'] as String? ?? 'info').toLowerCase(); final status = (alert['status'] as String? ?? 'unknown').toLowerCase(); final serverName = alert['server_name'] as String? ?? alert['serverName'] as String? ?? alert['hostname'] as String? ?? ''; final createdAt = alert['created_at'] as String? ?? alert['createdAt'] as String?; final timeLabel = createdAt != null ? DateFormatter.timeAgo(DateTime.parse(createdAt)) : ''; // Only allow swipe-to-acknowledge if not already acknowledged final canAcknowledge = status != 'acknowledged'; final card = Card( color: AppColors.surface, margin: const EdgeInsets.only(bottom: 10), child: Padding( padding: const EdgeInsets.all(14), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // Severity icon + message + status badge Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ _SeverityIcon(severity: severity), const SizedBox(width: 10), Expanded( child: Text( message, style: const TextStyle( fontSize: 14, fontWeight: FontWeight.w600), maxLines: 2, overflow: TextOverflow.ellipsis, ), ), const SizedBox(width: 8), StatusBadge(status: status), ], ), const SizedBox(height: 8), // Server name + timestamp row Row( children: [ if (serverName.isNotEmpty) ...[ const Icon(Icons.dns_outlined, size: 14, color: AppColors.textMuted), const SizedBox(width: 4), Text( serverName, style: const TextStyle( color: AppColors.textSecondary, fontSize: 12), ), const SizedBox(width: 12), ], const Icon(Icons.access_time, size: 14, color: AppColors.textMuted), const SizedBox(width: 4), Text( timeLabel, style: const TextStyle( color: AppColors.textMuted, fontSize: 12), ), ], ), ], ), ), ); if (!canAcknowledge) return card; return Dismissible( key: ValueKey(alert['id']), direction: DismissDirection.endToStart, confirmDismiss: (_) async { onAcknowledge(); return false; // don't remove the widget; let the refresh handle it }, background: Container( alignment: Alignment.centerRight, margin: const EdgeInsets.only(bottom: 10), padding: const EdgeInsets.symmetric(horizontal: 20), decoration: BoxDecoration( color: AppColors.info.withOpacity(0.2), borderRadius: BorderRadius.circular(12), ), child: const Row( mainAxisAlignment: MainAxisAlignment.end, children: [ Text( '确认', style: TextStyle( color: AppColors.info, fontWeight: FontWeight.w600, fontSize: 13), ), SizedBox(width: 8), Icon(Icons.check_circle_outline, color: AppColors.info), ], ), ), child: card, ); } } // --------------------------------------------------------------------------- // Severity icon helper // --------------------------------------------------------------------------- class _SeverityIcon extends StatelessWidget { final String severity; const _SeverityIcon({required this.severity}); @override Widget build(BuildContext context) { final IconData icon; final Color color; switch (severity) { case 'critical': icon = Icons.error; color = AppColors.error; break; case 'warning': icon = Icons.warning; color = AppColors.warning; break; case 'info': default: icon = Icons.info; color = AppColors.info; break; } return Icon(icon, size: 22, color: color); } }