192 lines
6.2 KiB
Dart
192 lines
6.2 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
|
|
import '../../../models/network/network_link.dart';
|
|
import '../../../models/network/network_port.dart';
|
|
import '../../../providers/network_map/network_devices_provider.dart';
|
|
|
|
/// Opens an M3 dialog to connect [portId] to a port on another device.
|
|
///
|
|
/// Returns true if a link was successfully created.
|
|
Future<bool> showLinkEditDialog({
|
|
required BuildContext context,
|
|
required WidgetRef ref,
|
|
required String portId,
|
|
required String currentDeviceId,
|
|
}) async {
|
|
final result = await showDialog<bool>(
|
|
context: context,
|
|
builder: (_) => _LinkEditDialog(
|
|
portId: portId,
|
|
currentDeviceId: currentDeviceId,
|
|
),
|
|
);
|
|
return result == true;
|
|
}
|
|
|
|
class _LinkEditDialog extends ConsumerStatefulWidget {
|
|
const _LinkEditDialog({
|
|
required this.portId,
|
|
required this.currentDeviceId,
|
|
});
|
|
|
|
final String portId;
|
|
final String currentDeviceId;
|
|
|
|
@override
|
|
ConsumerState<_LinkEditDialog> createState() => _LinkEditDialogState();
|
|
}
|
|
|
|
class _LinkEditDialogState extends ConsumerState<_LinkEditDialog> {
|
|
String? _selectedDeviceId;
|
|
String? _selectedPortId;
|
|
NetworkLinkKind? _linkKind;
|
|
final _cableLabelCtrl = TextEditingController();
|
|
bool _saving = false;
|
|
|
|
@override
|
|
void dispose() {
|
|
_cableLabelCtrl.dispose();
|
|
super.dispose();
|
|
}
|
|
|
|
Future<void> _save() async {
|
|
final portA = widget.portId;
|
|
final portB = _selectedPortId;
|
|
if (portB == null) return;
|
|
setState(() => _saving = true);
|
|
try {
|
|
ref.invalidate(networkLinksProvider);
|
|
final controller = ref.read(networkDevicesControllerProvider);
|
|
await controller.createLink(
|
|
portA: portA,
|
|
portB: portB,
|
|
linkKind: _linkKind,
|
|
cableLabel: _cableLabelCtrl.text.trim().isEmpty
|
|
? null
|
|
: _cableLabelCtrl.text.trim(),
|
|
);
|
|
if (mounted) Navigator.of(context).pop(true);
|
|
} catch (e) {
|
|
if (mounted) {
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
SnackBar(
|
|
content: Text('Failed to create link: $e'),
|
|
backgroundColor: Theme.of(context).colorScheme.error,
|
|
),
|
|
);
|
|
}
|
|
} finally {
|
|
if (mounted) setState(() => _saving = false);
|
|
}
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final devicesAsync = ref.watch(networkDevicesProvider);
|
|
final portsAsync = _selectedDeviceId != null
|
|
? ref.watch(networkPortsByDeviceProvider(_selectedDeviceId!))
|
|
: const AsyncValue<List<NetworkPort>>.data([]);
|
|
final allLinksAsync = ref.watch(networkLinksProvider);
|
|
|
|
final otherDevices = (devicesAsync.valueOrNull ?? const [])
|
|
.where((d) => d.id != widget.currentDeviceId)
|
|
.toList()
|
|
..sort((a, b) => a.name.compareTo(b.name));
|
|
|
|
final allLinks = allLinksAsync.valueOrNull ?? const [];
|
|
final usedPortIds = <String>{
|
|
for (final link in allLinks) ...[link.portA, link.portB],
|
|
};
|
|
|
|
final devicePorts = portsAsync.valueOrNull ?? const [];
|
|
final availablePorts =
|
|
devicePorts.where((p) => !usedPortIds.contains(p.id)).toList();
|
|
|
|
final canSave = _selectedPortId != null && !_saving;
|
|
|
|
return AlertDialog(
|
|
title: const Text('Connect port'),
|
|
content: ConstrainedBox(
|
|
constraints: const BoxConstraints(maxWidth: 420),
|
|
child: SingleChildScrollView(
|
|
child: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
children: [
|
|
DropdownButtonFormField<String>(
|
|
initialValue: _selectedDeviceId,
|
|
decoration:
|
|
const InputDecoration(labelText: 'Remote device *'),
|
|
items: [
|
|
for (final d in otherDevices)
|
|
DropdownMenuItem(value: d.id, child: Text(d.name)),
|
|
],
|
|
onChanged: (v) => setState(() {
|
|
_selectedDeviceId = v;
|
|
_selectedPortId = null;
|
|
}),
|
|
),
|
|
const SizedBox(height: 12),
|
|
DropdownButtonFormField<String>(
|
|
key: ValueKey(_selectedDeviceId),
|
|
initialValue: _selectedPortId,
|
|
decoration: InputDecoration(
|
|
labelText: 'Remote port *',
|
|
helperText: _selectedDeviceId == null
|
|
? 'Select a device first'
|
|
: availablePorts.isEmpty
|
|
? 'No available ports on this device'
|
|
: null,
|
|
),
|
|
items: [
|
|
for (final p in availablePorts)
|
|
DropdownMenuItem(
|
|
value: p.id,
|
|
child: Text(p.portNumber),
|
|
),
|
|
],
|
|
onChanged: _selectedDeviceId == null
|
|
? null
|
|
: (v) => setState(() => _selectedPortId = v),
|
|
),
|
|
const SizedBox(height: 12),
|
|
DropdownButtonFormField<NetworkLinkKind?>(
|
|
initialValue: _linkKind,
|
|
decoration: const InputDecoration(labelText: 'Link type'),
|
|
items: [
|
|
const DropdownMenuItem<NetworkLinkKind?>(
|
|
value: null,
|
|
child: Text('(unknown)'),
|
|
),
|
|
for (final k in NetworkLinkKind.values)
|
|
DropdownMenuItem(value: k, child: Text(k.label)),
|
|
],
|
|
onChanged: (v) => setState(() => _linkKind = v),
|
|
),
|
|
const SizedBox(height: 12),
|
|
TextField(
|
|
controller: _cableLabelCtrl,
|
|
decoration: const InputDecoration(
|
|
labelText: 'Cable label',
|
|
helperText: 'e.g. "CAT6-01", "SMF-A3" (optional)',
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
actions: [
|
|
TextButton(
|
|
onPressed: _saving ? null : () => Navigator.pop(context, false),
|
|
child: const Text('Cancel'),
|
|
),
|
|
FilledButton(
|
|
onPressed: canSave ? _save : null,
|
|
child: Text(_saving ? 'Saving…' : 'Connect'),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
}
|