Initial Commit : Network Map
This commit is contained in:
@@ -0,0 +1,246 @@
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
import '../../models/network/network_device.dart';
|
||||
import '../../models/network/network_link.dart';
|
||||
import '../../models/network/network_port.dart';
|
||||
import '../../models/network/network_vlan.dart';
|
||||
import '../supabase_provider.dart';
|
||||
|
||||
final networkDevicesProvider = FutureProvider<List<NetworkDevice>>((ref) async {
|
||||
final client = ref.watch(supabaseClientProvider);
|
||||
final rows = await client.from('network_devices').select().order('name');
|
||||
return (rows as List)
|
||||
.map((r) => NetworkDevice.fromMap(r as Map<String, dynamic>))
|
||||
.toList();
|
||||
});
|
||||
|
||||
final networkPortsProvider = FutureProvider<List<NetworkPort>>((ref) async {
|
||||
final client = ref.watch(supabaseClientProvider);
|
||||
final rows = await client.from('network_ports').select().order('port_number');
|
||||
return (rows as List)
|
||||
.map((r) => NetworkPort.fromMap(r as Map<String, dynamic>))
|
||||
.toList();
|
||||
});
|
||||
|
||||
final networkLinksProvider = FutureProvider<List<NetworkLink>>((ref) async {
|
||||
final client = ref.watch(supabaseClientProvider);
|
||||
final rows = await client.from('network_links').select();
|
||||
return (rows as List)
|
||||
.map((r) => NetworkLink.fromMap(r as Map<String, dynamic>))
|
||||
.toList();
|
||||
});
|
||||
|
||||
final networkVlansProvider = FutureProvider<List<NetworkVlan>>((ref) async {
|
||||
final client = ref.watch(supabaseClientProvider);
|
||||
final rows = await client.from('network_vlans').select().order('vlan_id');
|
||||
return (rows as List)
|
||||
.map((r) => NetworkVlan.fromMap(r as Map<String, dynamic>))
|
||||
.toList();
|
||||
});
|
||||
|
||||
final networkDeviceByIdProvider =
|
||||
FutureProvider.family<NetworkDevice?, String>((ref, id) async {
|
||||
final client = ref.watch(supabaseClientProvider);
|
||||
final row = await client
|
||||
.from('network_devices')
|
||||
.select()
|
||||
.eq('id', id)
|
||||
.maybeSingle();
|
||||
if (row == null) return null;
|
||||
return NetworkDevice.fromMap(row);
|
||||
});
|
||||
|
||||
final networkPortsByDeviceProvider =
|
||||
FutureProvider.family<List<NetworkPort>, String>((ref, deviceId) async {
|
||||
final client = ref.watch(supabaseClientProvider);
|
||||
final rows = await client
|
||||
.from('network_ports')
|
||||
.select()
|
||||
.eq('device_id', deviceId)
|
||||
.order('port_number');
|
||||
return (rows as List)
|
||||
.map((r) => NetworkPort.fromMap(r as Map<String, dynamic>))
|
||||
.toList();
|
||||
});
|
||||
|
||||
class NetworkDevicesController {
|
||||
NetworkDevicesController(this.ref);
|
||||
final Ref ref;
|
||||
|
||||
Future<NetworkDevice> createDevice({
|
||||
required String name,
|
||||
required NetworkDeviceKind kind,
|
||||
NetworkDeviceRole? role,
|
||||
String? vendor,
|
||||
String? model,
|
||||
String? serial,
|
||||
String? mgmtIp,
|
||||
String? mac,
|
||||
String? locationId,
|
||||
NetworkImportSource importSource = NetworkImportSource.manual,
|
||||
String? notes,
|
||||
}) async {
|
||||
final client = ref.read(supabaseClientProvider);
|
||||
final inserted = await client
|
||||
.from('network_devices')
|
||||
.insert({
|
||||
'name': name,
|
||||
'kind': kind.wire,
|
||||
'role': ?role?.wire,
|
||||
'vendor': ?vendor,
|
||||
'model': ?model,
|
||||
'serial': ?serial,
|
||||
'mgmt_ip': ?mgmtIp,
|
||||
'mac': ?mac,
|
||||
'location_id': ?locationId,
|
||||
'import_source': importSource.wire,
|
||||
'notes': ?notes,
|
||||
})
|
||||
.select()
|
||||
.single();
|
||||
ref.invalidate(networkDevicesProvider);
|
||||
return NetworkDevice.fromMap(inserted);
|
||||
}
|
||||
|
||||
Future<void> updateDevice(NetworkDevice device) async {
|
||||
final client = ref.read(supabaseClientProvider);
|
||||
await client
|
||||
.from('network_devices')
|
||||
.update(device.toUpdateMap())
|
||||
.eq('id', device.id);
|
||||
ref.invalidate(networkDevicesProvider);
|
||||
ref.invalidate(networkDeviceByIdProvider(device.id));
|
||||
}
|
||||
|
||||
Future<void> deleteDevice(String id) async {
|
||||
final client = ref.read(supabaseClientProvider);
|
||||
await client.from('network_devices').delete().eq('id', id);
|
||||
ref.invalidate(networkDevicesProvider);
|
||||
ref.invalidate(networkPortsProvider);
|
||||
ref.invalidate(networkLinksProvider);
|
||||
}
|
||||
|
||||
Future<NetworkPort> createPort({
|
||||
required String deviceId,
|
||||
required String portNumber,
|
||||
NetworkPortKind? portKind,
|
||||
int? speedMbps,
|
||||
int? accessVlan,
|
||||
bool isTrunk = false,
|
||||
List<int> trunkVlans = const [],
|
||||
String? notes,
|
||||
}) async {
|
||||
final client = ref.read(supabaseClientProvider);
|
||||
final inserted = await client
|
||||
.from('network_ports')
|
||||
.insert({
|
||||
'device_id': deviceId,
|
||||
'port_number': portNumber,
|
||||
'port_kind': ?portKind?.wire,
|
||||
'speed_mbps': ?speedMbps,
|
||||
'access_vlan': ?accessVlan,
|
||||
'is_trunk': isTrunk,
|
||||
if (trunkVlans.isNotEmpty) 'trunk_vlans': trunkVlans,
|
||||
'notes': ?notes,
|
||||
})
|
||||
.select()
|
||||
.single();
|
||||
ref.invalidate(networkPortsProvider);
|
||||
ref.invalidate(networkPortsByDeviceProvider(deviceId));
|
||||
return NetworkPort.fromMap(inserted);
|
||||
}
|
||||
|
||||
Future<void> deletePort(String id, {String? deviceId}) async {
|
||||
final client = ref.read(supabaseClientProvider);
|
||||
await client.from('network_ports').delete().eq('id', id);
|
||||
ref.invalidate(networkPortsProvider);
|
||||
ref.invalidate(networkLinksProvider);
|
||||
if (deviceId != null) {
|
||||
ref.invalidate(networkPortsByDeviceProvider(deviceId));
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> updatePort({
|
||||
required String id,
|
||||
required String deviceId,
|
||||
required String portNumber,
|
||||
NetworkPortKind? portKind,
|
||||
int? speedMbps,
|
||||
int? accessVlan,
|
||||
bool isTrunk = false,
|
||||
List<int> trunkVlans = const [],
|
||||
String? notes,
|
||||
}) async {
|
||||
final client = ref.read(supabaseClientProvider);
|
||||
await client.from('network_ports').update({
|
||||
'port_number': portNumber,
|
||||
'port_kind': portKind?.wire,
|
||||
'speed_mbps': speedMbps,
|
||||
'access_vlan': accessVlan,
|
||||
'is_trunk': isTrunk,
|
||||
'trunk_vlans': trunkVlans.isEmpty ? null : trunkVlans,
|
||||
'notes': notes,
|
||||
}).eq('id', id);
|
||||
ref.invalidate(networkPortsProvider);
|
||||
ref.invalidate(networkPortsByDeviceProvider(deviceId));
|
||||
}
|
||||
|
||||
Future<NetworkLink> createLink({
|
||||
required String portA,
|
||||
required String portB,
|
||||
NetworkLinkKind? linkKind,
|
||||
String? cableLabel,
|
||||
String? notes,
|
||||
}) async {
|
||||
final client = ref.read(supabaseClientProvider);
|
||||
final inserted = await client
|
||||
.from('network_links')
|
||||
.insert(NetworkLink.insertMapFor(
|
||||
portA: portA,
|
||||
portB: portB,
|
||||
linkKind: linkKind,
|
||||
cableLabel: cableLabel,
|
||||
notes: notes,
|
||||
))
|
||||
.select()
|
||||
.single();
|
||||
ref.invalidate(networkLinksProvider);
|
||||
return NetworkLink.fromMap(inserted);
|
||||
}
|
||||
|
||||
Future<void> deleteLink(String id) async {
|
||||
final client = ref.read(supabaseClientProvider);
|
||||
await client.from('network_links').delete().eq('id', id);
|
||||
ref.invalidate(networkLinksProvider);
|
||||
}
|
||||
|
||||
Future<NetworkVlan> createVlan({
|
||||
required int vlanId,
|
||||
required String name,
|
||||
String? description,
|
||||
String? color,
|
||||
}) async {
|
||||
final client = ref.read(supabaseClientProvider);
|
||||
final inserted = await client
|
||||
.from('network_vlans')
|
||||
.insert({
|
||||
'vlan_id': vlanId,
|
||||
'name': name,
|
||||
'description': ?description,
|
||||
'color': ?color,
|
||||
})
|
||||
.select()
|
||||
.single();
|
||||
ref.invalidate(networkVlansProvider);
|
||||
return NetworkVlan.fromMap(inserted);
|
||||
}
|
||||
|
||||
Future<void> deleteVlan(String id) async {
|
||||
final client = ref.read(supabaseClientProvider);
|
||||
await client.from('network_vlans').delete().eq('id', id);
|
||||
ref.invalidate(networkVlansProvider);
|
||||
}
|
||||
}
|
||||
|
||||
final networkDevicesControllerProvider =
|
||||
Provider<NetworkDevicesController>((ref) => NetworkDevicesController(ref));
|
||||
@@ -0,0 +1,155 @@
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:supabase_flutter/supabase_flutter.dart';
|
||||
|
||||
import '../../models/network/network_import.dart';
|
||||
import '../supabase_provider.dart';
|
||||
|
||||
final networkImportsProvider = FutureProvider<List<NetworkImport>>((ref) async {
|
||||
final client = ref.watch(supabaseClientProvider);
|
||||
final rows = await client
|
||||
.from('network_imports')
|
||||
.select()
|
||||
.order('created_at', ascending: false);
|
||||
return (rows as List)
|
||||
.map((r) => NetworkImport.fromMap(r as Map<String, dynamic>))
|
||||
.toList();
|
||||
});
|
||||
|
||||
final networkImportByIdProvider =
|
||||
FutureProvider.family<NetworkImport?, String>((ref, id) async {
|
||||
final client = ref.watch(supabaseClientProvider);
|
||||
final row = await client
|
||||
.from('network_imports')
|
||||
.select()
|
||||
.eq('id', id)
|
||||
.maybeSingle();
|
||||
if (row == null) return null;
|
||||
return NetworkImport.fromMap(row);
|
||||
});
|
||||
|
||||
class NetworkImportResult {
|
||||
NetworkImportResult({required this.status, this.aiResult, this.errorMessage});
|
||||
|
||||
final NetworkImportStatus status;
|
||||
final Map<String, dynamic>? aiResult;
|
||||
final String? errorMessage;
|
||||
}
|
||||
|
||||
class NetworkImportController {
|
||||
NetworkImportController(this.ref);
|
||||
final Ref ref;
|
||||
|
||||
/// Upload a source document, create a network_imports row, and trigger the
|
||||
/// extract_topology Edge Function. Returns the import_id immediately so the
|
||||
/// UI can navigate to the extraction-progress screen.
|
||||
Future<String> uploadAndQueueExtraction({
|
||||
required Uint8List bytes,
|
||||
required String filename,
|
||||
required String createdBy,
|
||||
}) async {
|
||||
final client = ref.read(supabaseClientProvider);
|
||||
final fileKind = NetworkImportFileKind.fromExtension(filename);
|
||||
final sanitized = filename
|
||||
.replaceAll(RegExp(r'[^A-Za-z0-9._-]'), '_')
|
||||
.substring(0, filename.length > 80 ? 80 : filename.length);
|
||||
final timestamp = DateTime.now().millisecondsSinceEpoch;
|
||||
final storagePath = '$createdBy/${timestamp}_$sanitized';
|
||||
|
||||
await client.storage.from('network-imports').uploadBinary(
|
||||
storagePath,
|
||||
bytes,
|
||||
fileOptions: const FileOptions(upsert: false),
|
||||
);
|
||||
|
||||
final inserted = await client
|
||||
.from('network_imports')
|
||||
.insert({
|
||||
'storage_path': storagePath,
|
||||
'file_kind': fileKind.wire,
|
||||
'status': NetworkImportStatus.pending.wire,
|
||||
'created_by': createdBy,
|
||||
})
|
||||
.select()
|
||||
.single();
|
||||
|
||||
ref.invalidate(networkImportsProvider);
|
||||
return inserted['id'] as String;
|
||||
}
|
||||
|
||||
/// Invoke the extract_topology Edge Function and return the parsed result.
|
||||
///
|
||||
/// Caller is expected to already have polled/awaited the import being in
|
||||
/// pending status. Long-running (5–60s) — UI should show progress.
|
||||
Future<NetworkImportResult> runExtraction(String importId) async {
|
||||
final client = ref.read(supabaseClientProvider);
|
||||
final response = await client.functions.invoke(
|
||||
'extract_topology',
|
||||
body: {'import_id': importId},
|
||||
);
|
||||
|
||||
final data = response.data;
|
||||
if (data is! Map) {
|
||||
return NetworkImportResult(
|
||||
status: NetworkImportStatus.failed,
|
||||
errorMessage: 'Unexpected response from extract_topology',
|
||||
);
|
||||
}
|
||||
final asMap = Map<String, dynamic>.from(data);
|
||||
|
||||
if (asMap['error'] != null) {
|
||||
ref.invalidate(networkImportByIdProvider(importId));
|
||||
ref.invalidate(networkImportsProvider);
|
||||
return NetworkImportResult(
|
||||
status: NetworkImportStatus.failed,
|
||||
errorMessage: asMap['error'].toString(),
|
||||
);
|
||||
}
|
||||
|
||||
final statusStr = asMap['status']?.toString() ?? 'failed';
|
||||
final aiResult = asMap['ai_result'] is Map
|
||||
? Map<String, dynamic>.from(asMap['ai_result'] as Map)
|
||||
: null;
|
||||
|
||||
ref.invalidate(networkImportByIdProvider(importId));
|
||||
ref.invalidate(networkImportsProvider);
|
||||
|
||||
return NetworkImportResult(
|
||||
status: NetworkImportStatus.fromWire(statusStr),
|
||||
aiResult: aiResult,
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> discard(String importId) async {
|
||||
final client = ref.read(supabaseClientProvider);
|
||||
await client
|
||||
.from('network_imports')
|
||||
.update({'status': NetworkImportStatus.discarded.wire})
|
||||
.eq('id', importId);
|
||||
ref.invalidate(networkImportsProvider);
|
||||
ref.invalidate(networkImportByIdProvider(importId));
|
||||
}
|
||||
|
||||
Future<void> markApplied({
|
||||
required String importId,
|
||||
required List<String> deviceIds,
|
||||
required List<String> linkIds,
|
||||
}) async {
|
||||
final client = ref.read(supabaseClientProvider);
|
||||
await client
|
||||
.from('network_imports')
|
||||
.update({
|
||||
'status': NetworkImportStatus.applied.wire,
|
||||
'applied_device_ids': deviceIds,
|
||||
'applied_link_ids': linkIds,
|
||||
'applied_at': DateTime.now().toUtc().toIso8601String(),
|
||||
})
|
||||
.eq('id', importId);
|
||||
ref.invalidate(networkImportsProvider);
|
||||
ref.invalidate(networkImportByIdProvider(importId));
|
||||
}
|
||||
}
|
||||
|
||||
final networkImportControllerProvider =
|
||||
Provider<NetworkImportController>((ref) => NetworkImportController(ref));
|
||||
@@ -0,0 +1,104 @@
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
import '../../models/network/network_site.dart';
|
||||
import '../../models/network/network_location.dart';
|
||||
import '../supabase_provider.dart';
|
||||
|
||||
final networkSitesProvider = FutureProvider<List<NetworkSite>>((ref) async {
|
||||
final client = ref.watch(supabaseClientProvider);
|
||||
final rows = await client.from('network_sites').select().order('name');
|
||||
return (rows as List)
|
||||
.map((r) => NetworkSite.fromMap(r as Map<String, dynamic>))
|
||||
.toList();
|
||||
});
|
||||
|
||||
final networkLocationsProvider = FutureProvider<List<NetworkLocation>>((ref) async {
|
||||
final client = ref.watch(supabaseClientProvider);
|
||||
final rows = await client.from('network_locations').select().order('name');
|
||||
return (rows as List)
|
||||
.map((r) => NetworkLocation.fromMap(r as Map<String, dynamic>))
|
||||
.toList();
|
||||
});
|
||||
|
||||
final selectedSiteIdProvider = StateProvider<String?>((ref) => null);
|
||||
|
||||
class NetworkSitesController {
|
||||
NetworkSitesController(this.ref);
|
||||
final Ref ref;
|
||||
|
||||
Future<NetworkSite> createSite({
|
||||
required String name,
|
||||
String? address,
|
||||
String? notes,
|
||||
String? createdBy,
|
||||
}) async {
|
||||
final client = ref.read(supabaseClientProvider);
|
||||
final inserted = await client
|
||||
.from('network_sites')
|
||||
.insert({
|
||||
'name': name,
|
||||
'address': ?address,
|
||||
'notes': ?notes,
|
||||
'created_by': ?createdBy,
|
||||
})
|
||||
.select()
|
||||
.single();
|
||||
ref.invalidate(networkSitesProvider);
|
||||
return NetworkSite.fromMap(inserted);
|
||||
}
|
||||
|
||||
Future<void> updateSite(NetworkSite site) async {
|
||||
final client = ref.read(supabaseClientProvider);
|
||||
await client.from('network_sites').update(site.toUpdateMap()).eq('id', site.id);
|
||||
ref.invalidate(networkSitesProvider);
|
||||
}
|
||||
|
||||
Future<void> deleteSite(String id) async {
|
||||
final client = ref.read(supabaseClientProvider);
|
||||
await client.from('network_sites').delete().eq('id', id);
|
||||
ref.invalidate(networkSitesProvider);
|
||||
}
|
||||
|
||||
Future<NetworkLocation> createLocation({
|
||||
required String siteId,
|
||||
String? parentId,
|
||||
required String name,
|
||||
required NetworkLocationKind kind,
|
||||
String? positionLabel,
|
||||
String? notes,
|
||||
}) async {
|
||||
final client = ref.read(supabaseClientProvider);
|
||||
final inserted = await client
|
||||
.from('network_locations')
|
||||
.insert({
|
||||
'site_id': siteId,
|
||||
'parent_id': ?parentId,
|
||||
'name': name,
|
||||
'kind': kind.wire,
|
||||
'position_label': ?positionLabel,
|
||||
'notes': ?notes,
|
||||
})
|
||||
.select()
|
||||
.single();
|
||||
ref.invalidate(networkLocationsProvider);
|
||||
return NetworkLocation.fromMap(inserted);
|
||||
}
|
||||
|
||||
/// Returns an existing root location for the site, or creates a "Main"
|
||||
/// location of kind=other if none exist. Used by the device assignment flow
|
||||
/// so admins can pick a site without having to think about location hierarchy.
|
||||
Future<NetworkLocation> ensureDefaultLocation(String siteId) async {
|
||||
final locations = await ref.read(networkLocationsProvider.future);
|
||||
for (final l in locations) {
|
||||
if (l.siteId == siteId) return l;
|
||||
}
|
||||
return createLocation(
|
||||
siteId: siteId,
|
||||
name: 'Main',
|
||||
kind: NetworkLocationKind.other,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
final networkSitesControllerProvider =
|
||||
Provider<NetworkSitesController>((ref) => NetworkSitesController(ref));
|
||||
@@ -0,0 +1,43 @@
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
import '../../models/network/network_site.dart';
|
||||
import '../../models/network/topology_graph.dart';
|
||||
import 'network_devices_provider.dart';
|
||||
import 'network_sites_provider.dart';
|
||||
|
||||
/// Aggregate topology for a specific site (or null for orphan devices).
|
||||
///
|
||||
/// This rebuilds whenever any of the underlying tables changes — invalidation
|
||||
/// flows from the granular providers (sites, locations, devices, ports, links,
|
||||
/// vlans) up through this aggregate.
|
||||
final topologyForSiteProvider =
|
||||
FutureProvider.family<TopologyGraph, String?>((ref, siteId) async {
|
||||
final sites = await ref.watch(networkSitesProvider.future);
|
||||
final locations = await ref.watch(networkLocationsProvider.future);
|
||||
final devices = await ref.watch(networkDevicesProvider.future);
|
||||
final ports = await ref.watch(networkPortsProvider.future);
|
||||
final links = await ref.watch(networkLinksProvider.future);
|
||||
final vlans = await ref.watch(networkVlansProvider.future);
|
||||
|
||||
NetworkSite? site;
|
||||
if (siteId != null) {
|
||||
for (final s in sites) {
|
||||
if (s.id == siteId) {
|
||||
site = s;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return TopologyGraph.build(
|
||||
site: site,
|
||||
allLocations: locations,
|
||||
allDevices: devices,
|
||||
allPorts: ports,
|
||||
allLinks: links,
|
||||
allVlans: vlans,
|
||||
);
|
||||
});
|
||||
|
||||
final topologyViewModeProvider =
|
||||
StateProvider<TopologyViewMode>((ref) => TopologyViewMode.physical);
|
||||
Reference in New Issue
Block a user