- Add Control Room page with stacks sidebar and containers list - Implement container actions (start/stop/restart) with snackbars - Add container logs viewer dialog - Add search/filter for both stacks and containers lists - Add external links for Portainer and Netdata (url_launcher) - Add VS Code launch configuration for Flutter web debugging 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
85 lines
2.7 KiB
Dart
85 lines
2.7 KiB
Dart
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
|
|
|
import '../../../containers/data/datasources/containers_datasource.dart';
|
|
import '../../../containers/domain/entities/container.dart';
|
|
import '../../domain/entities/stack.dart';
|
|
|
|
part 'stacks_datasource.g.dart';
|
|
|
|
/// Data source for stack operations.
|
|
///
|
|
/// Stacks are derived from container labels (Docker Compose convention).
|
|
/// The Core API doesn't have a dedicated stacks endpoint, so we aggregate
|
|
/// from containers.
|
|
class StacksDatasource {
|
|
StacksDatasource(this._containersDatasource);
|
|
|
|
final ContainersDatasource _containersDatasource;
|
|
|
|
/// Gets all stacks by aggregating container data.
|
|
Future<List<Stack>> getStacks() async {
|
|
final containers = await _containersDatasource.getContainers();
|
|
final containerEntities = containers.map((c) => c.toEntity()).toList();
|
|
|
|
// Group containers by stack name
|
|
final stackMap = <String, List<Container>>{};
|
|
|
|
for (final container in containerEntities) {
|
|
final stackName = container.stackName;
|
|
if (stackName != null) {
|
|
stackMap.putIfAbsent(stackName, () => []).add(container);
|
|
}
|
|
}
|
|
|
|
// Convert to Stack entities
|
|
return stackMap.entries.map((entry) {
|
|
final name = entry.key;
|
|
final stackContainers = entry.value;
|
|
final runningCount = stackContainers.where((c) => c.isRunning).length;
|
|
|
|
return Stack(
|
|
id: name, // Use name as ID for compose stacks
|
|
name: name,
|
|
type: StackType.compose,
|
|
status: runningCount == stackContainers.length
|
|
? StackStatus.active
|
|
: runningCount > 0
|
|
? StackStatus.active
|
|
: StackStatus.inactive,
|
|
containerCount: stackContainers.length,
|
|
runningCount: runningCount,
|
|
);
|
|
}).toList()
|
|
..sort((a, b) => a.name.compareTo(b.name));
|
|
}
|
|
|
|
/// Gets a single stack by ID.
|
|
Future<Stack> getStack(String id) async {
|
|
final stacks = await getStacks();
|
|
return stacks.firstWhere(
|
|
(s) => s.id == id,
|
|
orElse: () => throw Exception('Stack not found: $id'),
|
|
);
|
|
}
|
|
|
|
/// Performs an action on all containers in a stack.
|
|
Future<void> stackAction(String stackId, String action) async {
|
|
final containers = await _containersDatasource.getContainers();
|
|
final stackContainers = containers
|
|
.map((c) => c.toEntity())
|
|
.where((c) => c.stackId == stackId)
|
|
.toList();
|
|
|
|
for (final container in stackContainers) {
|
|
await _containersDatasource.containerAction(container.fullId, action);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Provides the stacks datasource.
|
|
@riverpod
|
|
StacksDatasource stacksDatasource(StacksDatasourceRef ref) {
|
|
final containersDatasource = ref.watch(containersDatasourceProvider);
|
|
return StacksDatasource(containersDatasource);
|
|
}
|