175 lines
5.8 KiB
Dart
175 lines
5.8 KiB
Dart
import 'package:flutter/material.dart';
|
|
|
|
import '../../../models/network/network_device.dart';
|
|
|
|
/// A styled node representing one network device on the topology canvas.
|
|
///
|
|
/// Designed to feel solid at ~120-160dp wide so it remains legible at the
|
|
/// "good for 200 nodes" zoom level called out in the V1 spec.
|
|
///
|
|
/// The node has three visual states beyond its default rest:
|
|
/// - [isSelected]: stronger primary-tinted background + 2dp primary border.
|
|
/// - [isHighlighted]: softer secondary tint + primary outline, used when an
|
|
/// edge touching this device is hovered on the canvas.
|
|
/// - [isSelected] takes precedence over [isHighlighted].
|
|
class DeviceNode extends StatelessWidget {
|
|
const DeviceNode({
|
|
super.key,
|
|
required this.device,
|
|
this.portCount,
|
|
this.isSelected = false,
|
|
this.isHighlighted = false,
|
|
this.onTap,
|
|
});
|
|
|
|
final NetworkDevice device;
|
|
final int? portCount;
|
|
final bool isSelected;
|
|
final bool isHighlighted;
|
|
final VoidCallback? onTap;
|
|
|
|
IconData _iconForKind(NetworkDeviceKind kind) {
|
|
switch (kind) {
|
|
case NetworkDeviceKind.router:
|
|
return Icons.router_outlined;
|
|
case NetworkDeviceKind.switchDevice:
|
|
return Icons.hub_outlined;
|
|
case NetworkDeviceKind.ap:
|
|
return Icons.wifi_outlined;
|
|
case NetworkDeviceKind.firewall:
|
|
return Icons.security_outlined;
|
|
case NetworkDeviceKind.server:
|
|
return Icons.dns_outlined;
|
|
case NetworkDeviceKind.endpoint:
|
|
return Icons.devices_outlined;
|
|
case NetworkDeviceKind.patchPanel:
|
|
return Icons.view_module_outlined;
|
|
case NetworkDeviceKind.other:
|
|
return Icons.device_unknown_outlined;
|
|
}
|
|
}
|
|
|
|
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;
|
|
}
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final cs = Theme.of(context).colorScheme;
|
|
final tt = Theme.of(context).textTheme;
|
|
final accent = _accentForRole(cs, device.role);
|
|
|
|
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: Container(
|
|
width: 160,
|
|
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
Row(
|
|
children: [
|
|
Icon(_iconForKind(device.kind), size: 18, color: accent),
|
|
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,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
],
|
|
],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|