Network map enhancements and fixes

This commit is contained in:
2026-06-05 16:42:56 +08:00
parent e2ddc9a3ae
commit d813ee45a2
14 changed files with 2473 additions and 395 deletions
+100 -83
View File
@@ -1,17 +1,12 @@
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.
///
/// 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].
/// 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,
@@ -19,6 +14,7 @@ class DeviceNode extends StatelessWidget {
this.portCount,
this.isSelected = false,
this.isHighlighted = false,
this.nodeSize = const Size(160, 110),
this.onTap,
});
@@ -26,29 +22,9 @@ class DeviceNode extends StatelessWidget {
final int? portCount;
final bool isSelected;
final bool isHighlighted;
final Size nodeSize;
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:
@@ -65,11 +41,25 @@ class DeviceNode extends StatelessWidget {
}
}
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
@@ -105,67 +95,94 @@ class DeviceNode extends StatelessWidget {
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(
child: Stack(
children: [
Container(
width: nodeSize.width,
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
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),
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,
),
),
],
),
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,
),
],
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,
),
),
),
],
],
),
),
],
),
),
),