import 'package:flutter/material.dart' hide Stack; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; import 'package:tatlock_ui/features/control_room/containers/presentation/pages/containers_list_page.dart'; import 'package:tatlock_ui/features/control_room/npm/presentation/pages/proxy_hosts_page.dart'; import 'package:tatlock_ui/features/control_room/router.dart'; import 'package:tatlock_ui/features/control_room/stacks/data/repositories/stack_repository_impl.dart'; import 'package:tatlock_ui/features/control_room/stacks/domain/entities/stack.dart'; import 'package:tatlock_ui/features/control_room/stacks/presentation/pages/stack_detail_page.dart'; import 'package:tatlock_ui/features/control_room/stacks/presentation/providers/stacks_provider.dart'; import 'package:tatlock_ui/features/control_room/stacks/presentation/widgets/stack_list_tile.dart'; import 'package:tatlock_ui/shared/layouts/widgets/filter_panel.dart'; import 'package:tatlock_ui/shared/layouts/widgets/nav_panel.dart'; /// Main Control Room page with nav panel and section content. class ControlRoomPage extends ConsumerWidget { const ControlRoomPage({ super.key, this.nav = ControlRoomNav.containers, }); /// The current nav item to display. final ControlRoomNav nav; @override Widget build(BuildContext context, WidgetRef ref) { final colorScheme = Theme.of(context).colorScheme; return Scaffold( body: Row( children: [ // Nav Panel - section navigation with grouping NavPanel( title: 'Sections', icon: Icons.dns_outlined, items: ControlRoomNav.values.map((n) => n.toNavItem()).toList(), selectedId: nav.id, onItemSelected: (id) { final newNav = ControlRoomNav.values.firstWhere( (n) => n.id == id, ); // Navigate to section URL context.go(pathForNav(newNav)); // Clear stack selection when changing sections ref.read(selectedStackProvider.notifier).clear(); }, ), // Divider VerticalDivider( width: 1, thickness: 1, color: colorScheme.outlineVariant, ), // Section content Expanded( child: _SectionContent(nav: nav), ), ], ), ); } } /// Renders content for the selected nav item. class _SectionContent extends ConsumerWidget { const _SectionContent({required this.nav}); final ControlRoomNav nav; @override Widget build(BuildContext context, WidgetRef ref) { return switch (nav) { ControlRoomNav.containers => const _ContainersSection(), ControlRoomNav.proxyHosts => const ProxyHostsPage(), _ => _PlaceholderSection(nav: nav), }; } } /// Placeholder for nav items not yet implemented. class _PlaceholderSection extends StatelessWidget { const _PlaceholderSection({required this.nav}); final ControlRoomNav nav; @override Widget build(BuildContext context) { final colorScheme = Theme.of(context).colorScheme; final textTheme = Theme.of(context).textTheme; return Center( child: Column( mainAxisSize: MainAxisSize.min, children: [ Icon( nav.icon, size: 64, color: colorScheme.outline, ), const SizedBox(height: 16), Text( nav.label, style: textTheme.headlineSmall?.copyWith( color: colorScheme.onSurfaceVariant, ), ), const SizedBox(height: 8), Text( '${nav.section} • Coming soon', style: textTheme.bodyMedium?.copyWith( color: colorScheme.outline, ), ), ], ), ); } } /// Containers section with optional stack filter. class _ContainersSection extends ConsumerWidget { const _ContainersSection(); @override Widget build(BuildContext context, WidgetRef ref) { final selectedStack = ref.watch(selectedStackProvider); final colorScheme = Theme.of(context).colorScheme; return Row( children: [ // Stacks filter panel const _StacksFilterPanel(), // Divider VerticalDivider( width: 1, thickness: 1, color: colorScheme.outlineVariant, ), // Main content - containers list or stack detail Expanded( child: selectedStack == null ? const ContainersListPage() : StackDetailPage(stackId: selectedStack), ), ], ); } } /// Stacks filter panel for Containers section (includes "All Containers" option). class _StacksFilterPanel extends ConsumerWidget { const _StacksFilterPanel(); @override Widget build(BuildContext context, WidgetRef ref) { final stacksAsync = ref.watch(stacksProvider); final selectedStack = ref.watch(selectedStackProvider); final colorScheme = Theme.of(context).colorScheme; return FilterPanel( title: 'Stacks', icon: Icons.layers, onRefresh: () => ref.invalidate(stacksProvider), child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ // All containers option ListTile( selected: selectedStack == null, selectedTileColor: colorScheme.primaryContainer.withValues(alpha: 0.3), leading: Icon( Icons.all_inbox, color: selectedStack == null ? colorScheme.primary : colorScheme.onSurfaceVariant, ), title: const Text('All Containers'), onTap: () => ref.read(selectedStackProvider.notifier).clear(), ), const Divider(height: 1), // Stacks list Expanded( child: stacksAsync.when( loading: () => const Center( child: CircularProgressIndicator(), ), error: (error, _) => Center( child: Padding( padding: const EdgeInsets.all(16), child: Column( mainAxisSize: MainAxisSize.min, children: [ Icon( Icons.error_outline, color: colorScheme.error, ), const SizedBox(height: 8), Text( 'Failed to load stacks', style: TextStyle(color: colorScheme.error), ), ], ), ), ), data: (stacks) => _StacksList( stacks: stacks, selectedStackId: selectedStack, onStackSelected: (id) => ref.read(selectedStackProvider.notifier).select(id), onStackAction: (id, action) => _handleStackAction(context, ref, id, action), ), ), ), ], ), ); } Future _handleStackAction( BuildContext context, WidgetRef ref, String stackId, String action, ) async { final repository = ref.read(stackRepositoryProvider); final messenger = ScaffoldMessenger.of(context); final colorScheme = Theme.of(context).colorScheme; try { switch (action) { case 'start': await repository.startStack(stackId); case 'stop': await repository.stopStack(stackId); case 'restart': await repository.restartStack(stackId); } // Refresh data ref.invalidate(stacksProvider); // Show success snackbar messenger.showSnackBar( SnackBar( content: Text('Stack ${action}ed successfully'), backgroundColor: colorScheme.primaryContainer, behavior: SnackBarBehavior.floating, duration: const Duration(seconds: 2), ), ); } catch (e) { messenger.showSnackBar( SnackBar( content: Row( children: [ Icon(Icons.error_outline, color: colorScheme.onErrorContainer), const SizedBox(width: 8), Expanded( child: Text( 'Failed to $action stack: ${e.toString()}', style: TextStyle(color: colorScheme.onErrorContainer), ), ), ], ), backgroundColor: colorScheme.errorContainer, behavior: SnackBarBehavior.floating, duration: const Duration(seconds: 4), ), ); } } } class _StacksList extends StatefulWidget { const _StacksList({ required this.stacks, required this.selectedStackId, required this.onStackSelected, required this.onStackAction, }); final List stacks; final String? selectedStackId; final void Function(String) onStackSelected; final void Function(String, String) onStackAction; @override State<_StacksList> createState() => _StacksListState(); } class _StacksListState extends State<_StacksList> { final _searchController = TextEditingController(); String _searchQuery = ''; @override void dispose() { _searchController.dispose(); super.dispose(); } List get _filteredStacks { if (_searchQuery.isEmpty) return widget.stacks; final query = _searchQuery.toLowerCase(); return widget.stacks.where((s) { return s.name.toLowerCase().contains(query); }).toList(); } @override Widget build(BuildContext context) { final colorScheme = Theme.of(context).colorScheme; final filtered = _filteredStacks; return Column( children: [ // Search field Padding( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), child: TextField( controller: _searchController, decoration: InputDecoration( hintText: 'Search stacks...', prefixIcon: const Icon(Icons.search, size: 18), suffixIcon: _searchQuery.isNotEmpty ? IconButton( icon: const Icon(Icons.clear, size: 16), onPressed: () { _searchController.clear(); setState(() => _searchQuery = ''); }, ) : null, isDense: true, contentPadding: const EdgeInsets.symmetric( horizontal: 12, vertical: 8, ), border: OutlineInputBorder( borderRadius: BorderRadius.circular(8), ), ), style: const TextStyle(fontSize: 13), onChanged: (value) => setState(() => _searchQuery = value), ), ), // Stack list or empty state Expanded( child: filtered.isEmpty ? Center( child: Padding( padding: const EdgeInsets.all(16), child: Column( mainAxisSize: MainAxisSize.min, children: [ Icon( _searchQuery.isEmpty ? Icons.layers_clear : Icons.search_off, size: 32, color: colorScheme.outline, ), const SizedBox(height: 8), Text( _searchQuery.isEmpty ? 'No stacks found' : 'No stacks match "$_searchQuery"', style: TextStyle( color: colorScheme.outline, fontSize: 13, ), textAlign: TextAlign.center, ), ], ), ), ) : ListView.builder( itemCount: filtered.length, itemBuilder: (context, index) { final stack = filtered[index]; return StackListTile( stack: stack, isSelected: stack.id == widget.selectedStackId, onTap: () => widget.onStackSelected(stack.id), onStart: () => widget.onStackAction(stack.id, 'start'), onStop: () => widget.onStackAction(stack.id, 'stop'), onRestart: () => widget.onStackAction(stack.id, 'restart'), ); }, ), ), ], ); } }