Files
manager/lib/features/fleet/widgets/fleet_card_grid.dart
T
2026-07-27 14:42:23 +02:00

135 lines
4.4 KiB
Dart

import 'package:flutter/material.dart';
import '../../../core/models/models.dart';
import '../../../core/models/severity_mapping.dart';
import '../../../core/theme/theme_x.dart';
import '../../../core/widgets/empty_state.dart';
import '../../../core/widgets/status_pill.dart';
/// One card per registered node showing its live services (via
/// `/nodes/aggregated`), or an inline error card if this particular node
/// was unreachable — a per-entry condition the local API itself reports,
/// distinct from the aggregated fetch as a whole failing.
class FleetCardGrid extends StatelessWidget {
const FleetCardGrid({super.key, required this.entries});
final List<AggregatedNodeStatus> entries;
@override
Widget build(BuildContext context) {
if (entries.isEmpty) {
return const EmptyState(
icon: Icons.hub_outlined,
title: 'Nothing to show yet',
message: 'Register a node to see its live services here.',
);
}
return GridView.builder(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent(
maxCrossAxisExtent: 320,
mainAxisExtent: 180,
crossAxisSpacing: 12,
mainAxisSpacing: 12,
),
itemCount: entries.length,
itemBuilder: (context, index) => _NodeCard(entry: entries[index]),
);
}
}
class _NodeCard extends StatelessWidget {
const _NodeCard({required this.entry});
final AggregatedNodeStatus entry;
@override
Widget build(BuildContext context) {
final hasError = entry.error != null;
return Container(
padding: const EdgeInsets.all(14),
decoration: BoxDecoration(
color: context.colors.surface,
borderRadius: BorderRadius.circular(10),
border: Border.all(
color: hasError ? context.status.critical.withValues(alpha: 0.4) : context.colors.outlineVariant,
),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(entry.node.name, style: context.text.labelLarge),
const SizedBox(height: 10),
Expanded(
child: hasError
? _ErrorCallout(message: entry.error!)
: entry.services.isEmpty
? Text('No services reported.', style: context.text.bodySmall)
: ListView(
padding: EdgeInsets.zero,
children: [
for (final service in entry.services)
Padding(
padding: const EdgeInsets.symmetric(vertical: 3.5),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: Text(
service.name,
overflow: TextOverflow.ellipsis,
style: context.text.bodyMedium,
),
),
StatusPill(
label: service.status,
severity: severityForServiceStatus(service.status),
),
],
),
),
],
),
),
],
),
);
}
}
class _ErrorCallout extends StatelessWidget {
const _ErrorCallout({required this.message});
final String message;
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.all(10),
decoration: BoxDecoration(
color: Color.alphaBlend(context.status.critical.withValues(alpha: 0.08), context.colors.surface),
borderRadius: BorderRadius.circular(8),
border: Border.all(color: context.status.critical.withValues(alpha: 0.35)),
),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Icon(Icons.error_outline, size: 15, color: context.status.critical),
const SizedBox(width: 8),
Expanded(
child: Text(
message,
style: context.dataStyles.dataMonoSmall,
maxLines: 3,
overflow: TextOverflow.ellipsis,
),
),
],
),
);
}
}