PortConnectionModal.razor 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437
  1. @using RackPeek.Domain.Persistence
  2. @using RackPeek.Domain.Resources
  3. @using RackPeek.Domain.Resources.Connections
  4. @using RackPeek.Domain.Resources.Servers
  5. @using RackPeek.Domain.Resources.SubResources
  6. @inject IResourceCollection Repository
  7. @inject IAddConnectionUseCase AddConnectionUseCase
  8. @if (IsOpen)
  9. {
  10. <div class="fixed inset-0 z-50 flex items-center justify-center">
  11. <div class="absolute inset-0 bg-black/70" @onclick="Cancel"></div>
  12. <div
  13. class="relative bg-zinc-900 border border-zinc-800 rounded w-full max-w-3xl p-4"
  14. data-testid="@($"{BaseTestId}-container")">
  15. <div class="flex justify-between mb-4">
  16. <div class="text-zinc-100 text-sm font-medium">
  17. Create Connection
  18. </div>
  19. <button class="text-zinc-400 hover:text-zinc-200"
  20. @onclick="Cancel">
  21. ✕
  22. </button>
  23. </div>
  24. <div class="grid grid-cols-2 gap-6 text-sm">
  25. <!-- SIDE A -->
  26. <div class="space-y-3">
  27. <div class="text-zinc-400">Side A</div>
  28. <select class="w-full bg-zinc-800 border border-zinc-700 rounded px-2 py-1 text-zinc-100"
  29. data-testid="@($"{BaseTestId}-resource-a")"
  30. @bind="_resourceAIndex">
  31. <option value="">Select resource</option>
  32. @for (var i = 0; i < HardwareWithPorts.Count; i++)
  33. {
  34. var hw = (Resource)HardwareWithPorts[i];
  35. <option value="@i">@hw.Name</option>
  36. }
  37. </select>
  38. @if (_resourceA?.Ports?.Any() == true)
  39. {
  40. <select class="w-full bg-zinc-800 border border-zinc-700 rounded px-2 py-1 text-zinc-100"
  41. data-testid="@($"{BaseTestId}-group-a")"
  42. @bind="_groupAIndex">
  43. <option value="">Select group</option>
  44. @for (var i = 0; i < _resourceA.Ports.Count; i++)
  45. {
  46. var g = _resourceA.Ports[i];
  47. <option value="@i">@g.Type — @g.Speed Gbps (@g.Count)</option>
  48. }
  49. </select>
  50. }
  51. @if (_groupA is not null)
  52. {
  53. <select class="w-full bg-zinc-800 border border-zinc-700 rounded px-2 py-1 text-zinc-100"
  54. data-testid="@($"{BaseTestId}-port-a")"
  55. @bind="_portAIndex">
  56. <option value="">Select port</option>
  57. @for (var i = 0; i < _groupA.Count; i++)
  58. {
  59. <option value="@i">Port @(i + 1)</option>
  60. }
  61. </select>
  62. <PortGroupVisualizer
  63. ResourceName="@_portA.Resource"
  64. PortGroupIndex="@_portA.PortGroup"
  65. PortGroup="@_groupA"
  66. @bind-SelectedPortIndex="_portAIndex"
  67. OnPortClicked="HandleLeftPortClicked"/>
  68. }
  69. </div>
  70. <!-- SIDE B -->
  71. <div class="space-y-3">
  72. <div class="text-zinc-400">Side B</div>
  73. <select class="w-full bg-zinc-800 border border-zinc-700 rounded px-2 py-1 text-zinc-100"
  74. data-testid="@($"{BaseTestId}-resource-b")"
  75. @bind="_resourceBIndex">
  76. <option value="">Select resource</option>
  77. @for (var i = 0; i < HardwareWithPorts.Count; i++)
  78. {
  79. var hw = (Resource)HardwareWithPorts[i];
  80. <option value="@i">@hw.Name</option>
  81. }
  82. </select>
  83. @if (_resourceB?.Ports?.Any() == true)
  84. {
  85. <select class="w-full bg-zinc-800 border border-zinc-700 rounded px-2 py-1 text-zinc-100"
  86. data-testid="@($"{BaseTestId}-group-b")"
  87. @bind="_groupBIndex">
  88. <option value="">Select group</option>
  89. @for (var i = 0; i < _resourceB.Ports.Count; i++)
  90. {
  91. var g = _resourceB.Ports[i];
  92. <option value="@i">@g.Type — @g.Speed Gbps (@g.Count)</option>
  93. }
  94. </select>
  95. }
  96. @if (_groupB is not null)
  97. {
  98. <select class="w-full bg-zinc-800 border border-zinc-700 rounded px-2 py-1 text-zinc-100"
  99. data-testid="@($"{BaseTestId}-port-b")"
  100. @bind="_portBIndex">
  101. <option value="">Select port</option>
  102. @for (var i = 0; i < _groupB.Count; i++)
  103. {
  104. <option value="@i">Port @(i + 1)</option>
  105. }
  106. </select>
  107. <PortGroupVisualizer
  108. ResourceName="@_portB.Resource"
  109. PortGroupIndex="@_portB.PortGroup"
  110. PortGroup="@_groupB"
  111. @bind-SelectedPortIndex="_portBIndex"
  112. OnPortClicked="HandleRightPortClicked"/>
  113. }
  114. </div>
  115. </div>
  116. <div class="mt-6 space-y-1">
  117. <label class="text-zinc-400 text-xs uppercase tracking-wider"
  118. for="@($"{BaseTestId}-label-input")">
  119. Label
  120. </label>
  121. <input id="@($"{BaseTestId}-label-input")"
  122. class="w-full bg-zinc-800 border border-zinc-700 rounded px-2 py-1 text-zinc-100 text-sm"
  123. data-testid="@($"{BaseTestId}-label")"
  124. placeholder="optional — shown on the topology diagram"
  125. maxlength="80"
  126. @bind="_label"/>
  127. </div>
  128. <div class="flex justify-end gap-2 mt-6">
  129. <button class="px-3 py-1 border border-zinc-700 rounded text-zinc-300"
  130. data-testid="@($"{BaseTestId}-cancel")"
  131. @onclick="Cancel">
  132. Cancel
  133. </button>
  134. <button class="px-3 py-1 rounded bg-emerald-600 text-black"
  135. disabled="@(!CanSubmit)"
  136. data-testid="@($"{BaseTestId}-submit")"
  137. @onclick="HandleSubmit">
  138. Add Connection
  139. </button>
  140. </div>
  141. </div>
  142. </div>
  143. }
  144. @code {
  145. [Parameter] public bool IsOpen { get; set; }
  146. [Parameter] public EventCallback<bool> IsOpenChanged { get; set; }
  147. [Parameter] public string? TestIdPrefix { get; set; }
  148. private string BaseTestId =>
  149. string.IsNullOrWhiteSpace(TestIdPrefix)
  150. ? "connection-modal"
  151. : $"{TestIdPrefix}-connection-modal";
  152. [Parameter] public PortReference? SeedPort { get; set; }
  153. List<IPortResource> HardwareWithPorts = new();
  154. IPortResource? _resourceA;
  155. IPortResource? _resourceB;
  156. Port? _groupA;
  157. Port? _groupB;
  158. readonly PortReference _portA = new();
  159. readonly PortReference _portB = new();
  160. int? _resourceAIndexValue;
  161. int? _resourceBIndexValue;
  162. int? _groupAIndexValue;
  163. int? _groupBIndexValue;
  164. int? _portAIndex;
  165. int? _portBIndex;
  166. string _label = string.Empty;
  167. int? _resourceAIndex
  168. {
  169. get => _resourceAIndexValue;
  170. set
  171. {
  172. _resourceAIndexValue = value;
  173. if (value is null)
  174. {
  175. _resourceA = null;
  176. _groupA = null;
  177. _portAIndex = null;
  178. return;
  179. }
  180. _resourceA = HardwareWithPorts[value.Value];
  181. _portA.Resource = ((Resource)_resourceA).Name;
  182. _groupAIndex = null;
  183. _portAIndex = null;
  184. }
  185. }
  186. int? _resourceBIndex
  187. {
  188. get => _resourceBIndexValue;
  189. set
  190. {
  191. _resourceBIndexValue = value;
  192. if (value is null)
  193. {
  194. _resourceB = null;
  195. _groupB = null;
  196. _portBIndex = null;
  197. return;
  198. }
  199. _resourceB = HardwareWithPorts[value.Value];
  200. _portB.Resource = ((Resource)_resourceB).Name;
  201. _groupBIndex = null;
  202. _portBIndex = null;
  203. }
  204. }
  205. int? _groupAIndex
  206. {
  207. get => _groupAIndexValue;
  208. set
  209. {
  210. _groupAIndexValue = value;
  211. if (value is null || _resourceA == null)
  212. {
  213. _groupA = null;
  214. _portAIndex = null;
  215. return;
  216. }
  217. _groupA = _resourceA.Ports![value.Value];
  218. _portA.PortGroup = value.Value;
  219. _portAIndex = null;
  220. }
  221. }
  222. int? _groupBIndex
  223. {
  224. get => _groupBIndexValue;
  225. set
  226. {
  227. _groupBIndexValue = value;
  228. if (value is null || _resourceB == null)
  229. {
  230. _groupB = null;
  231. _portBIndex = null;
  232. return;
  233. }
  234. _groupB = _resourceB.Ports![value.Value];
  235. _portB.PortGroup = value.Value;
  236. _portBIndex = null;
  237. }
  238. }
  239. bool CanSubmit =>
  240. _groupA != null &&
  241. _groupB != null &&
  242. _portAIndex != null &&
  243. _portBIndex != null;
  244. protected override async Task OnParametersSetAsync()
  245. {
  246. if (!IsOpen) return;
  247. var all = await Repository.GetAllOfTypeAsync<IPortResource>();
  248. HardwareWithPorts = all
  249. .Where(h => h.Ports?.Any() == true)
  250. .ToList();
  251. if (SeedPort != null)
  252. {
  253. // If the seed port is already part of a connection, hydrate the
  254. // whole modal (side B + label) so the user can see and edit what
  255. // they already configured. Otherwise just preselect side A.
  256. var existing = await Repository.GetConnectionForPortAsync(SeedPort);
  257. if (existing != null)
  258. SeedConnection(existing);
  259. else
  260. SeedSinglePortA(SeedPort);
  261. }
  262. }
  263. async Task HandleLeftPortClicked(PortReference port)
  264. {
  265. var existing = await Repository.GetConnectionForPortAsync(port);
  266. if (existing != null)
  267. SeedConnection(existing);
  268. else
  269. SeedSinglePortA(port);
  270. }
  271. async Task HandleRightPortClicked(PortReference port)
  272. {
  273. var existing = await Repository.GetConnectionForPortAsync(port);
  274. if (existing != null)
  275. SeedConnection(existing);
  276. else
  277. SeedSinglePortB(port);
  278. }
  279. void SeedSinglePortA(PortReference port)
  280. {
  281. _resourceAIndex = HardwareWithPorts.FindIndex(r => ((Resource)r).Name == port.Resource);
  282. _groupAIndex = port.PortGroup;
  283. _portAIndex = port.PortIndex;
  284. }
  285. void SeedSinglePortB(PortReference port)
  286. {
  287. _resourceBIndex = HardwareWithPorts.FindIndex(r => ((Resource)r).Name == port.Resource);
  288. _groupBIndex = port.PortGroup;
  289. _portBIndex = port.PortIndex;
  290. }
  291. void SeedConnection(Connection conn)
  292. {
  293. SeedSinglePortA(conn.A);
  294. SeedSinglePortB(conn.B);
  295. _label = conn.Label ?? string.Empty;
  296. }
  297. async Task HandleSubmit()
  298. {
  299. if (!CanSubmit) return;
  300. var a = new PortReference
  301. {
  302. Resource = _portA.Resource,
  303. PortGroup = _portA.PortGroup,
  304. PortIndex = _portAIndex!.Value
  305. };
  306. var b = new PortReference
  307. {
  308. Resource = _portB.Resource,
  309. PortGroup = _portB.PortGroup,
  310. PortIndex = _portBIndex!.Value
  311. };
  312. var trimmedLabel = string.IsNullOrWhiteSpace(_label) ? null : _label.Trim();
  313. await AddConnectionUseCase.ExecuteAsync(a, b, trimmedLabel);
  314. await Cancel();
  315. }
  316. async Task Cancel()
  317. {
  318. _label = string.Empty;
  319. await IsOpenChanged.InvokeAsync(false);
  320. }
  321. }