it0/it0_app/lib/features/servers/presentation/pages/servers_page.dart

450 lines
15 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:it0_app/l10n/app_localizations.dart';
import '../../../../core/config/api_endpoints.dart';
import '../../../../core/network/dio_client.dart';
import '../../../../core/theme/app_colors.dart';
import '../../../../core/widgets/empty_state.dart';
import '../../../../core/widgets/error_view.dart';
import '../../../../core/widgets/status_badge.dart';
// ---------------------------------------------------------------------------
// Provider fetch servers from inventory API
// ---------------------------------------------------------------------------
/// Fetches the full list of servers from the backend.
final serversProvider =
FutureProvider<List<Map<String, dynamic>>>((ref) async {
final dio = ref.watch(dioClientProvider);
final response = await dio.get(ApiEndpoints.servers);
final data = response.data;
if (data is List) {
return data.cast<Map<String, dynamic>>();
}
if (data is Map && data.containsKey('items')) {
return (data['items'] as List).cast<Map<String, dynamic>>();
}
return [];
});
// ---------------------------------------------------------------------------
// Servers page ConsumerStatefulWidget (needs local filter state)
// ---------------------------------------------------------------------------
class ServersPage extends ConsumerStatefulWidget {
const ServersPage({super.key});
@override
ConsumerState<ServersPage> createState() => _ServersPageState();
}
class _ServersPageState extends ConsumerState<ServersPage> {
String _envFilter = 'all';
static const _environments = ['all', 'dev', 'staging', 'prod'];
@override
Widget build(BuildContext context) {
final serversAsync = ref.watch(serversProvider);
return Scaffold(
appBar: AppBar(
title: Text(AppLocalizations.of(context).serversPageTitle),
),
body: RefreshIndicator(
onRefresh: () async => ref.invalidate(serversProvider),
child: Column(
children: [
// Environment filter chips
_buildFilterBar(),
// Server list
Expanded(
child: serversAsync.when(
data: (servers) {
final filtered = _filterServers(servers);
if (filtered.isEmpty) {
return EmptyState(
icon: Icons.dns_outlined,
title: AppLocalizations.of(context).noServersTitle,
subtitle: AppLocalizations.of(context).noServersFiltered,
);
}
return ListView.builder(
padding: const EdgeInsets.all(12),
itemCount: filtered.length,
itemBuilder: (context, index) {
final server = filtered[index];
return _ServerCard(
server: server,
onTap: () => _showServerDetails(context, server),
);
},
);
},
loading: () =>
const Center(child: CircularProgressIndicator()),
error: (e, _) => ErrorView(
error: e,
onRetry: () => ref.invalidate(serversProvider),
),
),
),
],
),
),
);
}
// ---- Environment filter bar -----------------------------------------------
Widget _buildFilterBar() {
return SingleChildScrollView(
scrollDirection: Axis.horizontal,
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
child: Row(
children: _environments.map((env) {
final selected = _envFilter == env;
return Padding(
padding: const EdgeInsets.only(right: 8),
child: FilterChip(
label: Text(env == 'all' ? AppLocalizations.of(context).allEnvironments : env),
selected: selected,
onSelected: (_) => setState(() => _envFilter = env),
selectedColor: _envColor(env).withOpacity(0.25),
checkmarkColor: _envColor(env),
labelStyle: TextStyle(
color: selected ? _envColor(env) : AppColors.textSecondary,
fontWeight: selected ? FontWeight.w600 : FontWeight.normal,
),
side: BorderSide(
color: selected
? _envColor(env).withOpacity(0.5)
: AppColors.surfaceLight,
),
backgroundColor: AppColors.surface,
),
);
}).toList(),
),
);
}
List<Map<String, dynamic>> _filterServers(
List<Map<String, dynamic>> servers) {
if (_envFilter == 'all') return servers;
return servers.where((s) {
final env = (s['environment'] as String? ?? '').toLowerCase();
return env == _envFilter;
}).toList();
}
// ---- Server details bottom sheet ------------------------------------------
void _showServerDetails(
BuildContext context, Map<String, dynamic> server) {
final hostname =
server['hostname'] as String? ?? server['name'] as String? ?? AppLocalizations.of(context).unknownLabel;
final ip = server['ip_address'] as String? ??
server['ipAddress'] as String? ??
server['ip'] as String? ??
'N/A';
final env = server['environment'] as String? ?? 'unknown';
final status = server['status'] as String? ?? 'unknown';
final role = server['role'] as String? ?? '';
final os = server['os'] as String? ??
server['operating_system'] as String? ??
'';
final cpu = server['cpu'] as String? ?? server['cpu_cores']?.toString() ?? '';
final memory =
server['memory'] as String? ?? server['memory_gb']?.toString() ?? '';
final region = server['region'] as String? ?? '';
final provider = server['provider'] as String? ??
server['cloud_provider'] as String? ??
'';
final createdAt =
server['created_at'] as String? ?? server['createdAt'] as String? ?? '';
showModalBottomSheet(
context: context,
isScrollControlled: true,
backgroundColor: AppColors.surface,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
),
builder: (context) => DraggableScrollableSheet(
initialChildSize: 0.55,
minChildSize: 0.3,
maxChildSize: 0.85,
expand: false,
builder: (context, scrollController) => SingleChildScrollView(
controller: scrollController,
padding: const EdgeInsets.fromLTRB(20, 12, 20, 32),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Handle bar
Center(
child: Container(
width: 40,
height: 4,
decoration: BoxDecoration(
color: AppColors.textMuted,
borderRadius: BorderRadius.circular(2),
),
),
),
const SizedBox(height: 16),
// Hostname + status
Row(
children: [
Expanded(
child: Text(
hostname,
style: const TextStyle(
fontSize: 20, fontWeight: FontWeight.bold),
),
),
StatusBadge(status: status),
],
),
const SizedBox(height: 16),
// Environment + Role row
Row(
children: [
_EnvironmentTag(environment: env),
if (role.isNotEmpty) ...[
const SizedBox(width: 8),
StatusBadge(status: role, color: AppColors.info),
],
],
),
const SizedBox(height: 20),
// Detail rows
_DetailRow(label: AppLocalizations.of(context).ipAddressLabel, value: ip),
if (os.isNotEmpty) _DetailRow(label: AppLocalizations.of(context).osLabel, value: os),
if (cpu.isNotEmpty) _DetailRow(label: 'CPU', value: cpu),
if (memory.isNotEmpty)
_DetailRow(label: AppLocalizations.of(context).memoryLabel, value: memory),
if (region.isNotEmpty)
_DetailRow(label: AppLocalizations.of(context).regionLabel, value: region),
if (provider.isNotEmpty)
_DetailRow(label: AppLocalizations.of(context).cloudProviderLabel, value: provider),
if (createdAt.isNotEmpty)
_DetailRow(label: AppLocalizations.of(context).createdAtLabel, value: createdAt),
],
),
),
),
);
}
// ---- Helpers --------------------------------------------------------------
static Color _envColor(String env) {
switch (env.toLowerCase()) {
case 'dev':
return AppColors.info;
case 'staging':
return AppColors.warning;
case 'prod':
return AppColors.error;
default:
return AppColors.primary;
}
}
}
// ---------------------------------------------------------------------------
// Server card widget
// ---------------------------------------------------------------------------
class _ServerCard extends StatelessWidget {
final Map<String, dynamic> server;
final VoidCallback onTap;
const _ServerCard({required this.server, required this.onTap});
@override
Widget build(BuildContext context) {
final hostname =
server['hostname'] as String? ?? server['name'] as String? ?? AppLocalizations.of(context).unknownLabel;
final ip = server['ip_address'] as String? ??
server['ipAddress'] as String? ??
server['ip'] as String? ??
'N/A';
final env = server['environment'] as String? ?? 'unknown';
final status = server['status'] as String? ?? 'unknown';
final role = server['role'] as String? ?? '';
final isOnline = _isOnline(status);
return Card(
color: AppColors.surface,
margin: const EdgeInsets.only(bottom: 10),
child: InkWell(
onTap: onTap,
borderRadius: BorderRadius.circular(12),
child: Padding(
padding: const EdgeInsets.all(14),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Hostname + status dot
Row(
children: [
// Status indicator dot
Container(
width: 10,
height: 10,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: isOnline ? AppColors.success : AppColors.error,
boxShadow: [
BoxShadow(
color: (isOnline ? AppColors.success : AppColors.error)
.withOpacity(0.4),
blurRadius: 6,
spreadRadius: 1,
),
],
),
),
const SizedBox(width: 10),
Expanded(
child: Text(
hostname,
style: const TextStyle(
fontSize: 15, fontWeight: FontWeight.w600),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
const SizedBox(width: 8),
_EnvironmentTag(environment: env),
],
),
const SizedBox(height: 8),
// IP + role row
Row(
children: [
const Icon(Icons.lan_outlined,
size: 14, color: AppColors.textMuted),
const SizedBox(width: 4),
Text(
ip,
style: const TextStyle(
color: AppColors.textSecondary, fontSize: 13),
),
if (role.isNotEmpty) ...[
const Spacer(),
Icon(Icons.workspaces_outline,
size: 14, color: AppColors.textMuted),
const SizedBox(width: 4),
Text(
role,
style: const TextStyle(
color: AppColors.textSecondary, fontSize: 13),
),
],
],
),
],
),
),
),
);
}
bool _isOnline(String status) {
final lower = status.toLowerCase();
return lower == 'online' ||
lower == 'running' ||
lower == 'active' ||
lower == 'healthy';
}
}
// ---------------------------------------------------------------------------
// Environment tag chip
// ---------------------------------------------------------------------------
class _EnvironmentTag extends StatelessWidget {
final String environment;
const _EnvironmentTag({required this.environment});
Color get _color {
switch (environment.toLowerCase()) {
case 'dev':
return AppColors.info;
case 'staging':
return AppColors.warning;
case 'prod':
return AppColors.error;
default:
return AppColors.textMuted;
}
}
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: _color.withOpacity(0.15),
borderRadius: BorderRadius.circular(12),
border: Border.all(color: _color.withOpacity(0.3)),
),
child: Text(
environment.toUpperCase(),
style: TextStyle(
color: _color,
fontSize: 11,
fontWeight: FontWeight.w600,
),
),
);
}
}
// ---------------------------------------------------------------------------
// Detail row for bottom sheet
// ---------------------------------------------------------------------------
class _DetailRow extends StatelessWidget {
final String label;
final String value;
const _DetailRow({required this.label, required this.value});
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.only(bottom: 12),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(
width: 100,
child: Text(
label,
style: const TextStyle(
color: AppColors.textMuted,
fontSize: 13,
fontWeight: FontWeight.w500),
),
),
Expanded(
child: Text(
value,
style: const TextStyle(
color: AppColors.textPrimary, fontSize: 13),
),
),
],
),
);
}
}