inventory.dart 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389
  1. import 'dart:convert';
  2. import 'package:flutter/material.dart';
  3. /// Parsed `GET /api/inventory` document. One flat [Resource] shape carries
  4. /// every kind's optional fields; kind-specific parsing decides which are set.
  5. class InventoryDocument {
  6. InventoryDocument({
  7. required this.version,
  8. required this.resources,
  9. required this.connections,
  10. Map<String, dynamic>? raw,
  11. }) : _raw = raw ?? {'version': version, 'resources': [], 'connections': []};
  12. final int version;
  13. final List<Resource> resources;
  14. final List<Connection> connections;
  15. final Map<String, dynamic> _raw;
  16. Map<String, dynamic> toJson() => _copyMap(_raw);
  17. InventoryDocument withResourcePatch(Map<String, dynamic> patch) {
  18. final next = toJson();
  19. final resources = (next['resources'] as List<dynamic>? ?? [])
  20. .map((resource) => Map<String, dynamic>.from(resource as Map))
  21. .toList();
  22. final kind = patch['kind']?.toString();
  23. final name = patch['name']?.toString();
  24. final index = resources.indexWhere(
  25. (resource) =>
  26. resource['kind'] == kind &&
  27. resource['name'].toString().toLowerCase() == name?.toLowerCase(),
  28. );
  29. if (index < 0) {
  30. resources.add(_copyMap(patch));
  31. } else {
  32. _mergeMap(resources[index], patch);
  33. }
  34. next['resources'] = resources;
  35. return InventoryDocument.fromJson(next);
  36. }
  37. factory InventoryDocument.fromJson(Map<String, dynamic> json) {
  38. return InventoryDocument(
  39. version: json['version'] as int? ?? 0,
  40. resources: (json['resources'] as List<dynamic>? ?? [])
  41. .map((e) => Resource.fromJson(e as Map<String, dynamic>))
  42. .toList(),
  43. connections: (json['connections'] as List<dynamic>? ?? [])
  44. .map((e) => Connection.fromJson(e as Map<String, dynamic>))
  45. .toList(),
  46. raw: _copyMap(json),
  47. );
  48. }
  49. static Map<String, dynamic> _copyMap(Map source) =>
  50. Map<String, dynamic>.from(jsonDecode(jsonEncode(source)) as Map);
  51. static void _mergeMap(
  52. Map<String, dynamic> target,
  53. Map<String, dynamic> patch,
  54. ) {
  55. for (final entry in patch.entries) {
  56. final current = target[entry.key];
  57. final value = entry.value;
  58. if (current is Map && value is Map) {
  59. final nested = Map<String, dynamic>.from(current);
  60. _mergeMap(nested, Map<String, dynamic>.from(value));
  61. target[entry.key] = nested;
  62. } else {
  63. target[entry.key] = value;
  64. }
  65. }
  66. }
  67. Resource? byName(String name) {
  68. for (final resource in resources) {
  69. if (resource.name.toLowerCase() == name.toLowerCase()) return resource;
  70. }
  71. return null;
  72. }
  73. List<Resource> dependantsOf(String name) {
  74. return resources
  75. .where(
  76. (r) => r.runsOn.any(
  77. (parent) => parent.toLowerCase() == name.toLowerCase(),
  78. ),
  79. )
  80. .toList();
  81. }
  82. List<Connection> connectionsFor(String name) {
  83. return connections
  84. .where(
  85. (c) =>
  86. c.a.resource.toLowerCase() == name.toLowerCase() ||
  87. c.b.resource.toLowerCase() == name.toLowerCase(),
  88. )
  89. .toList();
  90. }
  91. }
  92. class Resource {
  93. Resource({
  94. required this.kind,
  95. required this.name,
  96. this.tags = const [],
  97. this.labels = const {},
  98. this.notes,
  99. this.runsOn = const [],
  100. this.discoveryId,
  101. // hardware
  102. this.ram,
  103. this.ipmi,
  104. this.cpus = const [],
  105. this.drives = const [],
  106. this.gpus = const [],
  107. this.ports = const [],
  108. this.model,
  109. this.managed,
  110. this.poe,
  111. this.speed,
  112. this.va,
  113. this.description,
  114. // system
  115. this.type,
  116. this.os,
  117. this.cores,
  118. this.ramGb,
  119. this.ip,
  120. // service
  121. this.network,
  122. this.endpoints = const [],
  123. });
  124. final String kind;
  125. final String name;
  126. final List<String> tags;
  127. final Map<String, String> labels;
  128. final String? notes;
  129. final List<String> runsOn;
  130. final String? discoveryId;
  131. final Ram? ram;
  132. final bool? ipmi;
  133. final List<Cpu> cpus;
  134. final List<Drive> drives;
  135. final List<Gpu> gpus;
  136. final List<PortGroup> ports;
  137. final String? model;
  138. final bool? managed;
  139. final bool? poe;
  140. final double? speed;
  141. final int? va;
  142. final String? description;
  143. final String? type;
  144. final String? os;
  145. final int? cores;
  146. final double? ramGb;
  147. final String? ip;
  148. final Network? network;
  149. final List<Network> endpoints;
  150. bool get isHardware => const [
  151. 'Server',
  152. 'Switch',
  153. 'Firewall',
  154. 'Router',
  155. 'AccessPoint',
  156. 'Desktop',
  157. 'Laptop',
  158. 'Ups',
  159. 'Other',
  160. ].contains(kind);
  161. factory Resource.fromJson(Map<String, dynamic> json) {
  162. final kind = json['kind'] as String? ?? '';
  163. final isSystem = kind == 'System';
  164. List<T> list<T>(String key, T Function(Map<String, dynamic>) parse) =>
  165. (json[key] as List<dynamic>? ?? [])
  166. .map((e) => parse(e as Map<String, dynamic>))
  167. .toList();
  168. return Resource(
  169. kind: kind,
  170. name: json['name'] as String? ?? '',
  171. tags: (json['tags'] as List<dynamic>? ?? []).cast<String>(),
  172. labels: (json['labels'] as Map<String, dynamic>? ?? {}).map(
  173. (k, v) => MapEntry(k, v.toString()),
  174. ),
  175. notes: json['notes'] as String?,
  176. runsOn: (json['runsOn'] as List<dynamic>? ?? []).cast<String>(),
  177. discoveryId: json['discoveryId'] as String?,
  178. ram: isSystem ? null : _parse(json['ram'], (m) => Ram.fromJson(m)),
  179. ipmi: json['ipmi'] as bool?,
  180. cpus: list('cpus', Cpu.fromJson),
  181. drives: list('drives', Drive.fromJson),
  182. gpus: list('gpus', Gpu.fromJson),
  183. ports: list('ports', PortGroup.fromJson),
  184. model: json['model'] as String?,
  185. managed: json['managed'] as bool?,
  186. poe: json['poe'] as bool?,
  187. speed: (json['speed'] as num?)?.toDouble(),
  188. va: json['va'] as int?,
  189. description: json['description'] as String?,
  190. type: json['type'] as String?,
  191. os: json['os'] as String?,
  192. cores: json['cores'] as int?,
  193. ramGb: isSystem ? (json['ram'] as num?)?.toDouble() : null,
  194. ip: json['ip'] as String?,
  195. network: _parse(json['network'], (m) => Network.fromJson(m)),
  196. endpoints: list('endpoints', Network.fromJson),
  197. );
  198. }
  199. static T? _parse<T>(dynamic value, T Function(Map<String, dynamic>) parse) {
  200. if (value is Map<String, dynamic>) return parse(value);
  201. return null;
  202. }
  203. /// A short human line for list rows.
  204. String get subtitle {
  205. if (kind == 'Service') {
  206. final net = network?.describe();
  207. if (net != null && net.isNotEmpty) return net;
  208. }
  209. if (kind == 'System') {
  210. final parts = [type, os, ip].where((p) => p != null && p.isNotEmpty);
  211. if (parts.isNotEmpty) return parts.join(' · ');
  212. }
  213. if (model != null && model!.isNotEmpty) return model!;
  214. if (runsOn.isNotEmpty) return 'on ${runsOn.join(', ')}';
  215. return kind;
  216. }
  217. IconData get glyph => switch (kind) {
  218. 'Server' => Icons.dns,
  219. 'Switch' => Icons.swap_horiz,
  220. 'Router' => Icons.router,
  221. 'Firewall' => Icons.lock,
  222. 'AccessPoint' => Icons.wifi,
  223. 'Desktop' => Icons.desktop_windows,
  224. 'Laptop' => Icons.laptop,
  225. 'Ups' => Icons.bolt,
  226. 'Other' => Icons.apps,
  227. 'System' => Icons.memory,
  228. 'Service' => Icons.public,
  229. _ => Icons.help_outline,
  230. };
  231. }
  232. class Ram {
  233. Ram({this.size, this.mts});
  234. final double? size;
  235. final int? mts;
  236. factory Ram.fromJson(Map<String, dynamic> json) =>
  237. Ram(size: (json['size'] as num?)?.toDouble(), mts: json['mts'] as int?);
  238. }
  239. class Cpu {
  240. Cpu({this.model, this.cores, this.threads});
  241. final String? model;
  242. final int? cores;
  243. final int? threads;
  244. factory Cpu.fromJson(Map<String, dynamic> json) => Cpu(
  245. model: json['model'] as String?,
  246. cores: json['cores'] as int?,
  247. threads: json['threads'] as int?,
  248. );
  249. String describe() => [
  250. model,
  251. if (cores != null) '$cores cores',
  252. if (threads != null) '$threads threads',
  253. ].where((p) => p != null).join(' · ');
  254. }
  255. class Drive {
  256. Drive({this.type, this.size});
  257. final String? type;
  258. final int? size;
  259. factory Drive.fromJson(Map<String, dynamic> json) =>
  260. Drive(type: json['type'] as String?, size: json['size'] as int?);
  261. String describe() =>
  262. [if (size != null) '$size GB', type].where((p) => p != null).join(' ');
  263. }
  264. class Gpu {
  265. Gpu({this.model, this.vram});
  266. final String? model;
  267. final int? vram;
  268. factory Gpu.fromJson(Map<String, dynamic> json) =>
  269. Gpu(model: json['model'] as String?, vram: json['vram'] as int?);
  270. String describe() => [
  271. model,
  272. if (vram != null) '${vram}GB VRAM',
  273. ].where((p) => p != null).join(' · ');
  274. }
  275. class PortGroup {
  276. PortGroup({this.type, this.speed, this.count});
  277. final String? type;
  278. final double? speed;
  279. final int? count;
  280. factory PortGroup.fromJson(Map<String, dynamic> json) => PortGroup(
  281. type: json['type'] as String?,
  282. speed: (json['speed'] as num?)?.toDouble(),
  283. count: json['count'] as int?,
  284. );
  285. String describe() => [
  286. '${count ?? 0} × ${type ?? 'port'}',
  287. if (speed != null) '${speed!.toStringAsFixed(0)} Gbps',
  288. ].join(' · ');
  289. }
  290. class Network {
  291. Network({this.ip, this.port, this.protocol, this.url});
  292. final String? ip;
  293. final int? port;
  294. final String? protocol;
  295. final String? url;
  296. factory Network.fromJson(Map<String, dynamic> json) => Network(
  297. ip: json['ip'] as String?,
  298. port: json['port'] as int?,
  299. protocol: json['protocol'] as String?,
  300. url: json['url'] as String?,
  301. );
  302. String describe() {
  303. if (url != null && url!.isNotEmpty) return url!;
  304. if (ip == null) return '';
  305. return port != null ? '$ip:$port' : ip!;
  306. }
  307. }
  308. class Connection {
  309. Connection({required this.a, required this.b, this.label, this.notes});
  310. final PortReference a;
  311. final PortReference b;
  312. final String? label;
  313. final String? notes;
  314. factory Connection.fromJson(Map<String, dynamic> json) => Connection(
  315. a: PortReference.fromJson(json['a'] as Map<String, dynamic>),
  316. b: PortReference.fromJson(json['b'] as Map<String, dynamic>),
  317. label: json['label'] as String?,
  318. notes: json['notes'] as String?,
  319. );
  320. String describe() {
  321. final ends = [
  322. '${a.resource}:${a.portGroup}.$a.portIndex',
  323. '${b.resource}:${b.portGroup}.$b.portIndex',
  324. ];
  325. final link = ends.join(' ↔ ');
  326. return label != null && label!.isNotEmpty ? '$link ($label)' : link;
  327. }
  328. }
  329. class PortReference {
  330. PortReference({
  331. required this.resource,
  332. required this.portGroup,
  333. required this.portIndex,
  334. });
  335. final String resource;
  336. final int portGroup;
  337. final int portIndex;
  338. factory PortReference.fromJson(Map<String, dynamic> json) => PortReference(
  339. resource: json['resource'] as String? ?? '',
  340. portGroup: json['portGroup'] as int? ?? 0,
  341. portIndex: json['portIndex'] as int? ?? 0,
  342. );
  343. }