Initial Commit : Network Map
This commit is contained in:
@@ -0,0 +1,174 @@
|
||||
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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user