Initial Commit : Network Map
This commit is contained in:
@@ -0,0 +1,274 @@
|
||||
import 'package:brick_offline_first_with_supabase/brick_offline_first_with_supabase.dart';
|
||||
import 'package:brick_supabase/brick_supabase.dart';
|
||||
|
||||
enum NetworkDeviceKind {
|
||||
router,
|
||||
switchDevice,
|
||||
ap,
|
||||
firewall,
|
||||
server,
|
||||
endpoint,
|
||||
patchPanel,
|
||||
other;
|
||||
|
||||
String get wire {
|
||||
switch (this) {
|
||||
case NetworkDeviceKind.router:
|
||||
return 'router';
|
||||
case NetworkDeviceKind.switchDevice:
|
||||
return 'switch';
|
||||
case NetworkDeviceKind.ap:
|
||||
return 'ap';
|
||||
case NetworkDeviceKind.firewall:
|
||||
return 'firewall';
|
||||
case NetworkDeviceKind.server:
|
||||
return 'server';
|
||||
case NetworkDeviceKind.endpoint:
|
||||
return 'endpoint';
|
||||
case NetworkDeviceKind.patchPanel:
|
||||
return 'patch_panel';
|
||||
case NetworkDeviceKind.other:
|
||||
return 'other';
|
||||
}
|
||||
}
|
||||
|
||||
String get label {
|
||||
switch (this) {
|
||||
case NetworkDeviceKind.router:
|
||||
return 'Router';
|
||||
case NetworkDeviceKind.switchDevice:
|
||||
return 'Switch';
|
||||
case NetworkDeviceKind.ap:
|
||||
return 'Access Point';
|
||||
case NetworkDeviceKind.firewall:
|
||||
return 'Firewall';
|
||||
case NetworkDeviceKind.server:
|
||||
return 'Server';
|
||||
case NetworkDeviceKind.endpoint:
|
||||
return 'Endpoint';
|
||||
case NetworkDeviceKind.patchPanel:
|
||||
return 'Patch Panel';
|
||||
case NetworkDeviceKind.other:
|
||||
return 'Other';
|
||||
}
|
||||
}
|
||||
|
||||
static NetworkDeviceKind fromWire(String? value) {
|
||||
switch (value) {
|
||||
case 'router':
|
||||
return NetworkDeviceKind.router;
|
||||
case 'switch':
|
||||
return NetworkDeviceKind.switchDevice;
|
||||
case 'ap':
|
||||
return NetworkDeviceKind.ap;
|
||||
case 'firewall':
|
||||
return NetworkDeviceKind.firewall;
|
||||
case 'server':
|
||||
return NetworkDeviceKind.server;
|
||||
case 'endpoint':
|
||||
return NetworkDeviceKind.endpoint;
|
||||
case 'patch_panel':
|
||||
return NetworkDeviceKind.patchPanel;
|
||||
default:
|
||||
return NetworkDeviceKind.other;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum NetworkDeviceRole {
|
||||
core,
|
||||
distribution,
|
||||
access,
|
||||
edge,
|
||||
endpoint;
|
||||
|
||||
String get wire {
|
||||
switch (this) {
|
||||
case NetworkDeviceRole.core:
|
||||
return 'core';
|
||||
case NetworkDeviceRole.distribution:
|
||||
return 'distribution';
|
||||
case NetworkDeviceRole.access:
|
||||
return 'access';
|
||||
case NetworkDeviceRole.edge:
|
||||
return 'edge';
|
||||
case NetworkDeviceRole.endpoint:
|
||||
return 'endpoint';
|
||||
}
|
||||
}
|
||||
|
||||
String get label {
|
||||
switch (this) {
|
||||
case NetworkDeviceRole.core:
|
||||
return 'Core';
|
||||
case NetworkDeviceRole.distribution:
|
||||
return 'Distribution';
|
||||
case NetworkDeviceRole.access:
|
||||
return 'Access';
|
||||
case NetworkDeviceRole.edge:
|
||||
return 'Edge';
|
||||
case NetworkDeviceRole.endpoint:
|
||||
return 'Endpoint';
|
||||
}
|
||||
}
|
||||
|
||||
static NetworkDeviceRole? fromWire(String? value) {
|
||||
switch (value) {
|
||||
case 'core':
|
||||
return NetworkDeviceRole.core;
|
||||
case 'distribution':
|
||||
return NetworkDeviceRole.distribution;
|
||||
case 'access':
|
||||
return NetworkDeviceRole.access;
|
||||
case 'edge':
|
||||
return NetworkDeviceRole.edge;
|
||||
case 'endpoint':
|
||||
return NetworkDeviceRole.endpoint;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum NetworkImportSource {
|
||||
manual,
|
||||
aiImport,
|
||||
agent;
|
||||
|
||||
String get wire {
|
||||
switch (this) {
|
||||
case NetworkImportSource.manual:
|
||||
return 'manual';
|
||||
case NetworkImportSource.aiImport:
|
||||
return 'ai_import';
|
||||
case NetworkImportSource.agent:
|
||||
return 'agent';
|
||||
}
|
||||
}
|
||||
|
||||
static NetworkImportSource fromWire(String? value) {
|
||||
switch (value) {
|
||||
case 'ai_import':
|
||||
return NetworkImportSource.aiImport;
|
||||
case 'agent':
|
||||
return NetworkImportSource.agent;
|
||||
default:
|
||||
return NetworkImportSource.manual;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ConnectOfflineFirstWithSupabase(
|
||||
supabaseConfig: SupabaseSerializable(tableName: 'network_devices'),
|
||||
)
|
||||
class NetworkDevice extends OfflineFirstWithSupabaseModel {
|
||||
final String id;
|
||||
final String name;
|
||||
final NetworkDeviceKind kind;
|
||||
final NetworkDeviceRole? role;
|
||||
final String? vendor;
|
||||
final String? model;
|
||||
final String? serial;
|
||||
final String? mgmtIp;
|
||||
final String? mac;
|
||||
final String? locationId;
|
||||
final NetworkImportSource importSource;
|
||||
final String? notes;
|
||||
final DateTime createdAt;
|
||||
final DateTime updatedAt;
|
||||
|
||||
NetworkDevice({
|
||||
required this.id,
|
||||
required this.name,
|
||||
required this.kind,
|
||||
this.role,
|
||||
this.vendor,
|
||||
this.model,
|
||||
this.serial,
|
||||
this.mgmtIp,
|
||||
this.mac,
|
||||
this.locationId,
|
||||
this.importSource = NetworkImportSource.manual,
|
||||
this.notes,
|
||||
required this.createdAt,
|
||||
required this.updatedAt,
|
||||
});
|
||||
|
||||
factory NetworkDevice.fromMap(Map<String, dynamic> map) {
|
||||
return NetworkDevice(
|
||||
id: map['id'] as String,
|
||||
name: (map['name'] as String?) ?? '',
|
||||
kind: NetworkDeviceKind.fromWire(map['kind'] as String?),
|
||||
role: NetworkDeviceRole.fromWire(map['role'] as String?),
|
||||
vendor: map['vendor'] as String?,
|
||||
model: map['model'] as String?,
|
||||
serial: map['serial'] as String?,
|
||||
mgmtIp: map['mgmt_ip']?.toString(),
|
||||
mac: map['mac']?.toString(),
|
||||
locationId: map['location_id'] as String?,
|
||||
importSource: NetworkImportSource.fromWire(map['import_source'] as String?),
|
||||
notes: map['notes'] as String?,
|
||||
createdAt:
|
||||
DateTime.tryParse(map['created_at']?.toString() ?? '') ?? DateTime.now(),
|
||||
updatedAt:
|
||||
DateTime.tryParse(map['updated_at']?.toString() ?? '') ?? DateTime.now(),
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toInsertMap() => {
|
||||
'name': name,
|
||||
'kind': kind.wire,
|
||||
if (role != null) 'role': role!.wire,
|
||||
if (vendor != null) 'vendor': vendor,
|
||||
if (model != null) 'model': model,
|
||||
if (serial != null) 'serial': serial,
|
||||
if (mgmtIp != null) 'mgmt_ip': mgmtIp,
|
||||
if (mac != null) 'mac': mac,
|
||||
if (locationId != null) 'location_id': locationId,
|
||||
'import_source': importSource.wire,
|
||||
if (notes != null) 'notes': notes,
|
||||
};
|
||||
|
||||
Map<String, dynamic> toUpdateMap() => {
|
||||
'name': name,
|
||||
'kind': kind.wire,
|
||||
'role': role?.wire,
|
||||
'vendor': vendor,
|
||||
'model': model,
|
||||
'serial': serial,
|
||||
'mgmt_ip': mgmtIp,
|
||||
'mac': mac,
|
||||
'location_id': locationId,
|
||||
'notes': notes,
|
||||
};
|
||||
|
||||
NetworkDevice copyWith({
|
||||
String? name,
|
||||
NetworkDeviceKind? kind,
|
||||
NetworkDeviceRole? role,
|
||||
String? vendor,
|
||||
String? model,
|
||||
String? serial,
|
||||
String? mgmtIp,
|
||||
String? mac,
|
||||
String? locationId,
|
||||
String? notes,
|
||||
}) {
|
||||
return NetworkDevice(
|
||||
id: id,
|
||||
name: name ?? this.name,
|
||||
kind: kind ?? this.kind,
|
||||
role: role ?? this.role,
|
||||
vendor: vendor ?? this.vendor,
|
||||
model: model ?? this.model,
|
||||
serial: serial ?? this.serial,
|
||||
mgmtIp: mgmtIp ?? this.mgmtIp,
|
||||
mac: mac ?? this.mac,
|
||||
locationId: locationId ?? this.locationId,
|
||||
importSource: importSource,
|
||||
notes: notes ?? this.notes,
|
||||
createdAt: createdAt,
|
||||
updatedAt: updatedAt,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,175 @@
|
||||
import 'package:brick_offline_first_with_supabase/brick_offline_first_with_supabase.dart';
|
||||
import 'package:brick_supabase/brick_supabase.dart';
|
||||
|
||||
enum NetworkImportFileKind {
|
||||
pdf,
|
||||
image,
|
||||
vsdx,
|
||||
csv,
|
||||
xlsx,
|
||||
docx,
|
||||
other;
|
||||
|
||||
String get wire {
|
||||
switch (this) {
|
||||
case NetworkImportFileKind.pdf:
|
||||
return 'pdf';
|
||||
case NetworkImportFileKind.image:
|
||||
return 'image';
|
||||
case NetworkImportFileKind.vsdx:
|
||||
return 'vsdx';
|
||||
case NetworkImportFileKind.csv:
|
||||
return 'csv';
|
||||
case NetworkImportFileKind.xlsx:
|
||||
return 'xlsx';
|
||||
case NetworkImportFileKind.docx:
|
||||
return 'docx';
|
||||
case NetworkImportFileKind.other:
|
||||
return 'other';
|
||||
}
|
||||
}
|
||||
|
||||
static NetworkImportFileKind fromExtension(String filename) {
|
||||
final lower = filename.toLowerCase();
|
||||
if (lower.endsWith('.pdf')) return NetworkImportFileKind.pdf;
|
||||
if (lower.endsWith('.vsdx') || lower.endsWith('.vsd')) {
|
||||
return NetworkImportFileKind.vsdx;
|
||||
}
|
||||
if (lower.endsWith('.csv')) return NetworkImportFileKind.csv;
|
||||
if (lower.endsWith('.xlsx') || lower.endsWith('.xls')) {
|
||||
return NetworkImportFileKind.xlsx;
|
||||
}
|
||||
if (lower.endsWith('.docx') || lower.endsWith('.doc')) {
|
||||
return NetworkImportFileKind.docx;
|
||||
}
|
||||
if (lower.endsWith('.png') ||
|
||||
lower.endsWith('.jpg') ||
|
||||
lower.endsWith('.jpeg') ||
|
||||
lower.endsWith('.webp') ||
|
||||
lower.endsWith('.heic')) {
|
||||
return NetworkImportFileKind.image;
|
||||
}
|
||||
return NetworkImportFileKind.other;
|
||||
}
|
||||
}
|
||||
|
||||
enum NetworkImportStatus {
|
||||
pending,
|
||||
extracting,
|
||||
review,
|
||||
applied,
|
||||
discarded,
|
||||
failed;
|
||||
|
||||
String get wire {
|
||||
switch (this) {
|
||||
case NetworkImportStatus.pending:
|
||||
return 'pending';
|
||||
case NetworkImportStatus.extracting:
|
||||
return 'extracting';
|
||||
case NetworkImportStatus.review:
|
||||
return 'review';
|
||||
case NetworkImportStatus.applied:
|
||||
return 'applied';
|
||||
case NetworkImportStatus.discarded:
|
||||
return 'discarded';
|
||||
case NetworkImportStatus.failed:
|
||||
return 'failed';
|
||||
}
|
||||
}
|
||||
|
||||
static NetworkImportStatus fromWire(String? value) {
|
||||
switch (value) {
|
||||
case 'extracting':
|
||||
return NetworkImportStatus.extracting;
|
||||
case 'review':
|
||||
return NetworkImportStatus.review;
|
||||
case 'applied':
|
||||
return NetworkImportStatus.applied;
|
||||
case 'discarded':
|
||||
return NetworkImportStatus.discarded;
|
||||
case 'failed':
|
||||
return NetworkImportStatus.failed;
|
||||
default:
|
||||
return NetworkImportStatus.pending;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ConnectOfflineFirstWithSupabase(
|
||||
supabaseConfig: SupabaseSerializable(tableName: 'network_imports'),
|
||||
)
|
||||
class NetworkImport extends OfflineFirstWithSupabaseModel {
|
||||
final String id;
|
||||
final String storagePath;
|
||||
final NetworkImportFileKind fileKind;
|
||||
final NetworkImportStatus status;
|
||||
final Map<String, dynamic>? aiResult;
|
||||
final String? errorMessage;
|
||||
final List<String> appliedDeviceIds;
|
||||
final List<String> appliedLinkIds;
|
||||
final DateTime? appliedAt;
|
||||
final String createdBy;
|
||||
final DateTime createdAt;
|
||||
|
||||
NetworkImport({
|
||||
required this.id,
|
||||
required this.storagePath,
|
||||
required this.fileKind,
|
||||
required this.status,
|
||||
this.aiResult,
|
||||
this.errorMessage,
|
||||
this.appliedDeviceIds = const [],
|
||||
this.appliedLinkIds = const [],
|
||||
this.appliedAt,
|
||||
required this.createdBy,
|
||||
required this.createdAt,
|
||||
});
|
||||
|
||||
factory NetworkImport.fromMap(Map<String, dynamic> map) {
|
||||
List<String> idList(dynamic raw) {
|
||||
if (raw is List) {
|
||||
return raw.map((v) => v.toString()).toList();
|
||||
}
|
||||
return const [];
|
||||
}
|
||||
|
||||
return NetworkImport(
|
||||
id: map['id'] as String,
|
||||
storagePath: (map['storage_path'] as String?) ?? '',
|
||||
fileKind: _fileKindFromWire(map['file_kind'] as String?),
|
||||
status: NetworkImportStatus.fromWire(map['status'] as String?),
|
||||
aiResult: map['ai_result'] is Map<String, dynamic>
|
||||
? map['ai_result'] as Map<String, dynamic>
|
||||
: null,
|
||||
errorMessage: map['error_message'] as String?,
|
||||
appliedDeviceIds: idList(map['applied_device_ids']),
|
||||
appliedLinkIds: idList(map['applied_link_ids']),
|
||||
appliedAt: map['applied_at'] == null
|
||||
? null
|
||||
: DateTime.tryParse(map['applied_at']?.toString() ?? ''),
|
||||
createdBy: map['created_by'] as String,
|
||||
createdAt:
|
||||
DateTime.tryParse(map['created_at']?.toString() ?? '') ?? DateTime.now(),
|
||||
);
|
||||
}
|
||||
|
||||
static NetworkImportFileKind _fileKindFromWire(String? value) {
|
||||
switch (value) {
|
||||
case 'pdf':
|
||||
return NetworkImportFileKind.pdf;
|
||||
case 'image':
|
||||
return NetworkImportFileKind.image;
|
||||
case 'vsdx':
|
||||
return NetworkImportFileKind.vsdx;
|
||||
case 'csv':
|
||||
return NetworkImportFileKind.csv;
|
||||
case 'xlsx':
|
||||
return NetworkImportFileKind.xlsx;
|
||||
case 'docx':
|
||||
return NetworkImportFileKind.docx;
|
||||
default:
|
||||
return NetworkImportFileKind.other;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
import 'package:brick_offline_first_with_supabase/brick_offline_first_with_supabase.dart';
|
||||
import 'package:brick_supabase/brick_supabase.dart';
|
||||
|
||||
enum NetworkLinkKind {
|
||||
copper,
|
||||
fiber,
|
||||
wireless,
|
||||
virtual,
|
||||
unknown;
|
||||
|
||||
String get wire {
|
||||
switch (this) {
|
||||
case NetworkLinkKind.copper:
|
||||
return 'copper';
|
||||
case NetworkLinkKind.fiber:
|
||||
return 'fiber';
|
||||
case NetworkLinkKind.wireless:
|
||||
return 'wireless';
|
||||
case NetworkLinkKind.virtual:
|
||||
return 'virtual';
|
||||
case NetworkLinkKind.unknown:
|
||||
return 'unknown';
|
||||
}
|
||||
}
|
||||
|
||||
String get label {
|
||||
switch (this) {
|
||||
case NetworkLinkKind.copper:
|
||||
return 'Copper';
|
||||
case NetworkLinkKind.fiber:
|
||||
return 'Fiber';
|
||||
case NetworkLinkKind.wireless:
|
||||
return 'Wireless';
|
||||
case NetworkLinkKind.virtual:
|
||||
return 'Virtual';
|
||||
case NetworkLinkKind.unknown:
|
||||
return 'Unknown';
|
||||
}
|
||||
}
|
||||
|
||||
static NetworkLinkKind? fromWire(String? value) {
|
||||
switch (value) {
|
||||
case 'copper':
|
||||
return NetworkLinkKind.copper;
|
||||
case 'fiber':
|
||||
return NetworkLinkKind.fiber;
|
||||
case 'wireless':
|
||||
return NetworkLinkKind.wireless;
|
||||
case 'virtual':
|
||||
return NetworkLinkKind.virtual;
|
||||
case 'unknown':
|
||||
return NetworkLinkKind.unknown;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ConnectOfflineFirstWithSupabase(
|
||||
supabaseConfig: SupabaseSerializable(tableName: 'network_links'),
|
||||
)
|
||||
class NetworkLink extends OfflineFirstWithSupabaseModel {
|
||||
final String id;
|
||||
final String portA;
|
||||
final String portB;
|
||||
final NetworkLinkKind? linkKind;
|
||||
final String? cableLabel;
|
||||
final String? notes;
|
||||
final DateTime createdAt;
|
||||
|
||||
NetworkLink({
|
||||
required this.id,
|
||||
required this.portA,
|
||||
required this.portB,
|
||||
this.linkKind,
|
||||
this.cableLabel,
|
||||
this.notes,
|
||||
required this.createdAt,
|
||||
});
|
||||
|
||||
factory NetworkLink.fromMap(Map<String, dynamic> map) {
|
||||
return NetworkLink(
|
||||
id: map['id'] as String,
|
||||
portA: map['port_a'] as String,
|
||||
portB: map['port_b'] as String,
|
||||
linkKind: NetworkLinkKind.fromWire(map['link_kind'] as String?),
|
||||
cableLabel: map['cable_label'] as String?,
|
||||
notes: map['notes'] as String?,
|
||||
createdAt:
|
||||
DateTime.tryParse(map['created_at']?.toString() ?? '') ?? DateTime.now(),
|
||||
);
|
||||
}
|
||||
|
||||
/// Insert helper that enforces the canonical port_a < port_b ordering
|
||||
/// required by the database CHECK constraint.
|
||||
static Map<String, dynamic> insertMapFor({
|
||||
required String portA,
|
||||
required String portB,
|
||||
NetworkLinkKind? linkKind,
|
||||
String? cableLabel,
|
||||
String? notes,
|
||||
}) {
|
||||
final ordered = portA.compareTo(portB) < 0 ? [portA, portB] : [portB, portA];
|
||||
return {
|
||||
'port_a': ordered[0],
|
||||
'port_b': ordered[1],
|
||||
'link_kind': ?linkKind?.wire,
|
||||
'cable_label': ?cableLabel,
|
||||
'notes': ?notes,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
import 'package:brick_offline_first_with_supabase/brick_offline_first_with_supabase.dart';
|
||||
import 'package:brick_supabase/brick_supabase.dart';
|
||||
|
||||
enum NetworkLocationKind {
|
||||
building,
|
||||
floor,
|
||||
room,
|
||||
rack,
|
||||
wallJack,
|
||||
other;
|
||||
|
||||
String get wire {
|
||||
switch (this) {
|
||||
case NetworkLocationKind.building:
|
||||
return 'building';
|
||||
case NetworkLocationKind.floor:
|
||||
return 'floor';
|
||||
case NetworkLocationKind.room:
|
||||
return 'room';
|
||||
case NetworkLocationKind.rack:
|
||||
return 'rack';
|
||||
case NetworkLocationKind.wallJack:
|
||||
return 'wall_jack';
|
||||
case NetworkLocationKind.other:
|
||||
return 'other';
|
||||
}
|
||||
}
|
||||
|
||||
static NetworkLocationKind fromWire(String? value) {
|
||||
switch (value) {
|
||||
case 'building':
|
||||
return NetworkLocationKind.building;
|
||||
case 'floor':
|
||||
return NetworkLocationKind.floor;
|
||||
case 'room':
|
||||
return NetworkLocationKind.room;
|
||||
case 'rack':
|
||||
return NetworkLocationKind.rack;
|
||||
case 'wall_jack':
|
||||
return NetworkLocationKind.wallJack;
|
||||
default:
|
||||
return NetworkLocationKind.other;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ConnectOfflineFirstWithSupabase(
|
||||
supabaseConfig: SupabaseSerializable(tableName: 'network_locations'),
|
||||
)
|
||||
class NetworkLocation extends OfflineFirstWithSupabaseModel {
|
||||
final String id;
|
||||
final String siteId;
|
||||
final String? parentId;
|
||||
final String name;
|
||||
final NetworkLocationKind kind;
|
||||
final String? positionLabel;
|
||||
final String? notes;
|
||||
final DateTime createdAt;
|
||||
|
||||
NetworkLocation({
|
||||
required this.id,
|
||||
required this.siteId,
|
||||
this.parentId,
|
||||
required this.name,
|
||||
required this.kind,
|
||||
this.positionLabel,
|
||||
this.notes,
|
||||
required this.createdAt,
|
||||
});
|
||||
|
||||
factory NetworkLocation.fromMap(Map<String, dynamic> map) {
|
||||
return NetworkLocation(
|
||||
id: map['id'] as String,
|
||||
siteId: map['site_id'] as String,
|
||||
parentId: map['parent_id'] as String?,
|
||||
name: (map['name'] as String?) ?? '',
|
||||
kind: NetworkLocationKind.fromWire(map['kind'] as String?),
|
||||
positionLabel: map['position_label'] as String?,
|
||||
notes: map['notes'] as String?,
|
||||
createdAt:
|
||||
DateTime.tryParse(map['created_at']?.toString() ?? '') ?? DateTime.now(),
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toInsertMap() => {
|
||||
'site_id': siteId,
|
||||
if (parentId != null) 'parent_id': parentId,
|
||||
'name': name,
|
||||
'kind': kind.wire,
|
||||
if (positionLabel != null) 'position_label': positionLabel,
|
||||
if (notes != null) 'notes': notes,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,148 @@
|
||||
import 'package:brick_offline_first_with_supabase/brick_offline_first_with_supabase.dart';
|
||||
import 'package:brick_supabase/brick_supabase.dart';
|
||||
|
||||
enum NetworkPortKind {
|
||||
rj45,
|
||||
sfp,
|
||||
sfpPlus,
|
||||
console,
|
||||
power,
|
||||
wireless,
|
||||
virtual,
|
||||
other;
|
||||
|
||||
String get wire {
|
||||
switch (this) {
|
||||
case NetworkPortKind.rj45:
|
||||
return 'rj45';
|
||||
case NetworkPortKind.sfp:
|
||||
return 'sfp';
|
||||
case NetworkPortKind.sfpPlus:
|
||||
return 'sfp_plus';
|
||||
case NetworkPortKind.console:
|
||||
return 'console';
|
||||
case NetworkPortKind.power:
|
||||
return 'power';
|
||||
case NetworkPortKind.wireless:
|
||||
return 'wireless';
|
||||
case NetworkPortKind.virtual:
|
||||
return 'virtual';
|
||||
case NetworkPortKind.other:
|
||||
return 'other';
|
||||
}
|
||||
}
|
||||
|
||||
String get label {
|
||||
switch (this) {
|
||||
case NetworkPortKind.rj45:
|
||||
return 'RJ45';
|
||||
case NetworkPortKind.sfp:
|
||||
return 'SFP';
|
||||
case NetworkPortKind.sfpPlus:
|
||||
return 'SFP+';
|
||||
case NetworkPortKind.console:
|
||||
return 'Console';
|
||||
case NetworkPortKind.power:
|
||||
return 'Power';
|
||||
case NetworkPortKind.wireless:
|
||||
return 'Wireless';
|
||||
case NetworkPortKind.virtual:
|
||||
return 'Virtual';
|
||||
case NetworkPortKind.other:
|
||||
return 'Other';
|
||||
}
|
||||
}
|
||||
|
||||
static NetworkPortKind? fromWire(String? value) {
|
||||
switch (value) {
|
||||
case 'rj45':
|
||||
return NetworkPortKind.rj45;
|
||||
case 'sfp':
|
||||
return NetworkPortKind.sfp;
|
||||
case 'sfp_plus':
|
||||
return NetworkPortKind.sfpPlus;
|
||||
case 'console':
|
||||
return NetworkPortKind.console;
|
||||
case 'power':
|
||||
return NetworkPortKind.power;
|
||||
case 'wireless':
|
||||
return NetworkPortKind.wireless;
|
||||
case 'virtual':
|
||||
return NetworkPortKind.virtual;
|
||||
case 'other':
|
||||
return NetworkPortKind.other;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ConnectOfflineFirstWithSupabase(
|
||||
supabaseConfig: SupabaseSerializable(tableName: 'network_ports'),
|
||||
)
|
||||
class NetworkPort extends OfflineFirstWithSupabaseModel {
|
||||
final String id;
|
||||
final String deviceId;
|
||||
final String portNumber;
|
||||
final NetworkPortKind? portKind;
|
||||
final int? speedMbps;
|
||||
final int? accessVlan;
|
||||
final bool isTrunk;
|
||||
final List<int> trunkVlans;
|
||||
final String? notes;
|
||||
|
||||
NetworkPort({
|
||||
required this.id,
|
||||
required this.deviceId,
|
||||
required this.portNumber,
|
||||
this.portKind,
|
||||
this.speedMbps,
|
||||
this.accessVlan,
|
||||
this.isTrunk = false,
|
||||
this.trunkVlans = const [],
|
||||
this.notes,
|
||||
});
|
||||
|
||||
factory NetworkPort.fromMap(Map<String, dynamic> map) {
|
||||
final rawTrunk = map['trunk_vlans'];
|
||||
final trunk = <int>[];
|
||||
if (rawTrunk is List) {
|
||||
for (final v in rawTrunk) {
|
||||
if (v is int) {
|
||||
trunk.add(v);
|
||||
} else if (v is num) {
|
||||
trunk.add(v.toInt());
|
||||
} else {
|
||||
final parsed = int.tryParse(v?.toString() ?? '');
|
||||
if (parsed != null) trunk.add(parsed);
|
||||
}
|
||||
}
|
||||
}
|
||||
return NetworkPort(
|
||||
id: map['id'] as String,
|
||||
deviceId: map['device_id'] as String,
|
||||
portNumber: (map['port_number'] as String?) ?? '',
|
||||
portKind: NetworkPortKind.fromWire(map['port_kind'] as String?),
|
||||
speedMbps: map['speed_mbps'] is int
|
||||
? map['speed_mbps'] as int
|
||||
: int.tryParse(map['speed_mbps']?.toString() ?? ''),
|
||||
accessVlan: map['access_vlan'] is int
|
||||
? map['access_vlan'] as int
|
||||
: int.tryParse(map['access_vlan']?.toString() ?? ''),
|
||||
isTrunk: (map['is_trunk'] as bool?) ?? false,
|
||||
trunkVlans: trunk,
|
||||
notes: map['notes'] as String?,
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toInsertMap() => {
|
||||
'device_id': deviceId,
|
||||
'port_number': portNumber,
|
||||
if (portKind != null) 'port_kind': portKind!.wire,
|
||||
if (speedMbps != null) 'speed_mbps': speedMbps,
|
||||
if (accessVlan != null) 'access_vlan': accessVlan,
|
||||
'is_trunk': isTrunk,
|
||||
if (trunkVlans.isNotEmpty) 'trunk_vlans': trunkVlans,
|
||||
if (notes != null) 'notes': notes,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
import 'package:brick_offline_first_with_supabase/brick_offline_first_with_supabase.dart';
|
||||
import 'package:brick_supabase/brick_supabase.dart';
|
||||
|
||||
@ConnectOfflineFirstWithSupabase(
|
||||
supabaseConfig: SupabaseSerializable(tableName: 'network_sites'),
|
||||
)
|
||||
class NetworkSite extends OfflineFirstWithSupabaseModel {
|
||||
final String id;
|
||||
final String name;
|
||||
final String? address;
|
||||
final String? notes;
|
||||
final DateTime createdAt;
|
||||
final DateTime updatedAt;
|
||||
final String? createdBy;
|
||||
|
||||
NetworkSite({
|
||||
required this.id,
|
||||
required this.name,
|
||||
this.address,
|
||||
this.notes,
|
||||
required this.createdAt,
|
||||
required this.updatedAt,
|
||||
this.createdBy,
|
||||
});
|
||||
|
||||
factory NetworkSite.fromMap(Map<String, dynamic> map) {
|
||||
return NetworkSite(
|
||||
id: map['id'] as String,
|
||||
name: (map['name'] as String?) ?? '',
|
||||
address: map['address'] as String?,
|
||||
notes: map['notes'] as String?,
|
||||
createdAt:
|
||||
DateTime.tryParse(map['created_at']?.toString() ?? '') ?? DateTime.now(),
|
||||
updatedAt:
|
||||
DateTime.tryParse(map['updated_at']?.toString() ?? '') ?? DateTime.now(),
|
||||
createdBy: map['created_by'] as String?,
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toInsertMap() => {
|
||||
'name': name,
|
||||
if (address != null) 'address': address,
|
||||
if (notes != null) 'notes': notes,
|
||||
if (createdBy != null) 'created_by': createdBy,
|
||||
};
|
||||
|
||||
Map<String, dynamic> toUpdateMap() => {
|
||||
'name': name,
|
||||
'address': address,
|
||||
'notes': notes,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
import 'package:brick_offline_first_with_supabase/brick_offline_first_with_supabase.dart';
|
||||
import 'package:brick_supabase/brick_supabase.dart';
|
||||
|
||||
@ConnectOfflineFirstWithSupabase(
|
||||
supabaseConfig: SupabaseSerializable(tableName: 'network_vlans'),
|
||||
)
|
||||
class NetworkVlan extends OfflineFirstWithSupabaseModel {
|
||||
final String id;
|
||||
final int vlanId;
|
||||
final String name;
|
||||
final String? description;
|
||||
final String? color;
|
||||
|
||||
NetworkVlan({
|
||||
required this.id,
|
||||
required this.vlanId,
|
||||
required this.name,
|
||||
this.description,
|
||||
this.color,
|
||||
});
|
||||
|
||||
factory NetworkVlan.fromMap(Map<String, dynamic> map) {
|
||||
return NetworkVlan(
|
||||
id: map['id'] as String,
|
||||
vlanId: map['vlan_id'] is int
|
||||
? map['vlan_id'] as int
|
||||
: int.tryParse(map['vlan_id']?.toString() ?? '') ?? 0,
|
||||
name: (map['name'] as String?) ?? '',
|
||||
description: map['description'] as String?,
|
||||
color: map['color'] as String?,
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toInsertMap() => {
|
||||
'vlan_id': vlanId,
|
||||
'name': name,
|
||||
if (description != null) 'description': description,
|
||||
if (color != null) 'color': color,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,143 @@
|
||||
import 'network_device.dart';
|
||||
import 'network_link.dart';
|
||||
import 'network_location.dart';
|
||||
import 'network_port.dart';
|
||||
import 'network_site.dart';
|
||||
import 'network_vlan.dart';
|
||||
|
||||
/// Client-side aggregate view-model assembled from raw network_* rows.
|
||||
///
|
||||
/// The canvas consumes this directly rather than re-querying tables for every
|
||||
/// pan/zoom event. Built once per site by [TopologyGraph.build] and cached in
|
||||
/// the topology provider.
|
||||
class TopologyNode {
|
||||
TopologyNode({
|
||||
required this.device,
|
||||
required this.ports,
|
||||
});
|
||||
|
||||
final NetworkDevice device;
|
||||
final List<NetworkPort> ports;
|
||||
|
||||
String get id => device.id;
|
||||
}
|
||||
|
||||
class TopologyEdge {
|
||||
TopologyEdge({
|
||||
required this.link,
|
||||
required this.fromDeviceId,
|
||||
required this.toDeviceId,
|
||||
required this.fromPortLabel,
|
||||
required this.toPortLabel,
|
||||
});
|
||||
|
||||
final NetworkLink link;
|
||||
final String fromDeviceId;
|
||||
final String toDeviceId;
|
||||
final String fromPortLabel;
|
||||
final String toPortLabel;
|
||||
}
|
||||
|
||||
enum TopologyViewMode {
|
||||
physical,
|
||||
logical;
|
||||
|
||||
String get label {
|
||||
switch (this) {
|
||||
case TopologyViewMode.physical:
|
||||
return 'Physical';
|
||||
case TopologyViewMode.logical:
|
||||
return 'Logical';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class TopologyGraph {
|
||||
TopologyGraph({
|
||||
required this.site,
|
||||
required this.locations,
|
||||
required this.nodes,
|
||||
required this.edges,
|
||||
required this.vlans,
|
||||
});
|
||||
|
||||
final NetworkSite? site;
|
||||
final List<NetworkLocation> locations;
|
||||
final List<TopologyNode> nodes;
|
||||
final List<TopologyEdge> edges;
|
||||
final List<NetworkVlan> vlans;
|
||||
|
||||
bool get isEmpty => nodes.isEmpty;
|
||||
|
||||
/// Build a graph for a specific site from raw tables.
|
||||
///
|
||||
/// Filters devices to those in the site's location subtree. If [site] is
|
||||
/// null, all devices with no location are included (orphan view).
|
||||
static TopologyGraph build({
|
||||
required NetworkSite? site,
|
||||
required List<NetworkLocation> allLocations,
|
||||
required List<NetworkDevice> allDevices,
|
||||
required List<NetworkPort> allPorts,
|
||||
required List<NetworkLink> allLinks,
|
||||
required List<NetworkVlan> allVlans,
|
||||
}) {
|
||||
final siteLocations = site == null
|
||||
? <NetworkLocation>[]
|
||||
: allLocations.where((l) => l.siteId == site.id).toList();
|
||||
final siteLocationIds = siteLocations.map((l) => l.id).toSet();
|
||||
|
||||
final siteDevices = site == null
|
||||
? allDevices.where((d) => d.locationId == null).toList()
|
||||
: allDevices
|
||||
.where((d) =>
|
||||
d.locationId != null && siteLocationIds.contains(d.locationId))
|
||||
.toList();
|
||||
final siteDeviceIds = siteDevices.map((d) => d.id).toSet();
|
||||
|
||||
final portsByDevice = <String, List<NetworkPort>>{};
|
||||
for (final port in allPorts) {
|
||||
if (!siteDeviceIds.contains(port.deviceId)) continue;
|
||||
portsByDevice.putIfAbsent(port.deviceId, () => []).add(port);
|
||||
}
|
||||
|
||||
final portToDevice = <String, String>{};
|
||||
final portLabel = <String, String>{};
|
||||
for (final port in allPorts) {
|
||||
portToDevice[port.id] = port.deviceId;
|
||||
portLabel[port.id] = port.portNumber;
|
||||
}
|
||||
|
||||
final nodes = siteDevices
|
||||
.map((d) => TopologyNode(
|
||||
device: d,
|
||||
ports: portsByDevice[d.id] ?? const [],
|
||||
))
|
||||
.toList();
|
||||
|
||||
final edges = <TopologyEdge>[];
|
||||
for (final link in allLinks) {
|
||||
final fromDeviceId = portToDevice[link.portA];
|
||||
final toDeviceId = portToDevice[link.portB];
|
||||
if (fromDeviceId == null || toDeviceId == null) continue;
|
||||
if (!siteDeviceIds.contains(fromDeviceId) ||
|
||||
!siteDeviceIds.contains(toDeviceId)) {
|
||||
continue;
|
||||
}
|
||||
edges.add(TopologyEdge(
|
||||
link: link,
|
||||
fromDeviceId: fromDeviceId,
|
||||
toDeviceId: toDeviceId,
|
||||
fromPortLabel: portLabel[link.portA] ?? '',
|
||||
toPortLabel: portLabel[link.portB] ?? '',
|
||||
));
|
||||
}
|
||||
|
||||
return TopologyGraph(
|
||||
site: site,
|
||||
locations: siteLocations,
|
||||
nodes: nodes,
|
||||
edges: edges,
|
||||
vlans: allVlans,
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user