192 lines
6.4 KiB
Dart
192 lines
6.4 KiB
Dart
import 'package:flutter/material.dart';
|
|
|
|
import '../../../models/network/network_device.dart';
|
|
import 'network_device_icon.dart';
|
|
|
|
/// A styled node representing one network device on the topology canvas.
|
|
///
|
|
/// Accepts an explicit [nodeSize] so role-based sizing can be applied by the
|
|
/// canvas without this widget needing to know the layout strategy.
|
|
class DeviceNode extends StatelessWidget {
|
|
const DeviceNode({
|
|
super.key,
|
|
required this.device,
|
|
this.portCount,
|
|
this.isSelected = false,
|
|
this.isHighlighted = false,
|
|
this.nodeSize = const Size(160, 110),
|
|
this.onTap,
|
|
});
|
|
|
|
final NetworkDevice device;
|
|
final int? portCount;
|
|
final bool isSelected;
|
|
final bool isHighlighted;
|
|
final Size nodeSize;
|
|
final VoidCallback? onTap;
|
|
|
|
Color _accentForRole(ColorScheme cs, NetworkDeviceRole? role) {
|
|
switch (role) {
|
|
case NetworkDeviceRole.core:
|
|
return cs.error;
|
|
case NetworkDeviceRole.distribution:
|
|
return cs.tertiary;
|
|
case NetworkDeviceRole.access:
|
|
return cs.primary;
|
|
case NetworkDeviceRole.edge:
|
|
return cs.secondary;
|
|
case NetworkDeviceRole.endpoint:
|
|
case null:
|
|
return cs.outline;
|
|
}
|
|
}
|
|
|
|
Color? _statusColor(ColorScheme cs, NetworkDeviceStatus status) {
|
|
switch (status) {
|
|
case NetworkDeviceStatus.online:
|
|
return const Color(0xFF34A853); // Google green — universally understood
|
|
case NetworkDeviceStatus.offline:
|
|
return cs.error;
|
|
case NetworkDeviceStatus.warning:
|
|
return const Color(0xFFFBBC04); // Amber
|
|
case NetworkDeviceStatus.unknown:
|
|
return null; // No dot shown
|
|
}
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final cs = Theme.of(context).colorScheme;
|
|
final tt = Theme.of(context).textTheme;
|
|
final accent = _accentForRole(cs, device.role);
|
|
final statusColor = _statusColor(cs, device.status);
|
|
|
|
final bgColor = isSelected
|
|
? cs.primaryContainer
|
|
: isHighlighted
|
|
? cs.secondaryContainer.withValues(alpha: 0.6)
|
|
: cs.surfaceContainerHigh;
|
|
final borderColor = isSelected
|
|
? cs.primary
|
|
: isHighlighted
|
|
? cs.primary.withValues(alpha: 0.8)
|
|
: accent.withValues(alpha: 0.6);
|
|
final borderWidth = isSelected ? 2.0 : (isHighlighted ? 1.5 : 1.0);
|
|
|
|
return AnimatedContainer(
|
|
duration: const Duration(milliseconds: 180),
|
|
curve: Curves.easeOut,
|
|
decoration: BoxDecoration(
|
|
color: bgColor,
|
|
borderRadius: BorderRadius.circular(16),
|
|
border: Border.all(color: borderColor, width: borderWidth),
|
|
boxShadow: (isHighlighted || isSelected)
|
|
? [
|
|
BoxShadow(
|
|
color: cs.primary.withValues(alpha: 0.18),
|
|
blurRadius: 12,
|
|
offset: const Offset(0, 2),
|
|
),
|
|
]
|
|
: null,
|
|
),
|
|
child: Material(
|
|
color: Colors.transparent,
|
|
child: InkWell(
|
|
onTap: onTap,
|
|
borderRadius: BorderRadius.circular(16),
|
|
child: Stack(
|
|
children: [
|
|
Container(
|
|
width: nodeSize.width,
|
|
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
Row(
|
|
children: [
|
|
NetworkDeviceIcon(
|
|
kind: device.kind,
|
|
color: accent,
|
|
size: 20,
|
|
),
|
|
const SizedBox(width: 6),
|
|
Expanded(
|
|
child: Text(
|
|
device.kind.label,
|
|
style: tt.labelSmall?.copyWith(color: accent),
|
|
maxLines: 1,
|
|
overflow: TextOverflow.ellipsis,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 4),
|
|
Text(
|
|
device.name,
|
|
style: tt.bodyMedium?.copyWith(fontWeight: FontWeight.w600),
|
|
maxLines: 2,
|
|
overflow: TextOverflow.ellipsis,
|
|
),
|
|
if (device.vendor != null || device.model != null) ...[
|
|
const SizedBox(height: 2),
|
|
Text(
|
|
[
|
|
if (device.vendor != null) device.vendor,
|
|
if (device.model != null) device.model,
|
|
].whereType<String>().join(' • '),
|
|
style: tt.labelSmall?.copyWith(
|
|
color: cs.onSurfaceVariant),
|
|
maxLines: 1,
|
|
overflow: TextOverflow.ellipsis,
|
|
),
|
|
],
|
|
if (portCount != null) ...[
|
|
const SizedBox(height: 4),
|
|
Row(
|
|
children: [
|
|
Icon(
|
|
Icons.cable_outlined,
|
|
size: 12,
|
|
color: cs.onSurfaceVariant,
|
|
),
|
|
const SizedBox(width: 4),
|
|
Text(
|
|
'$portCount ports',
|
|
style: tt.labelSmall?.copyWith(
|
|
color: cs.onSurfaceVariant,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
],
|
|
],
|
|
),
|
|
),
|
|
// Status dot — top-right, only shown when status is not unknown
|
|
if (statusColor != null)
|
|
Positioned(
|
|
top: 8,
|
|
right: 8,
|
|
child: Container(
|
|
width: 8,
|
|
height: 8,
|
|
decoration: BoxDecoration(
|
|
color: statusColor,
|
|
shape: BoxShape.circle,
|
|
border: Border.all(
|
|
color: bgColor,
|
|
width: 1.5,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|