ServerCardComponent.razor 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655
  1. @using RackPeek.Domain.Resources.Servers
  2. @using RackPeek.Domain.Resources.SubResources
  3. @using RackPeek.Domain.UseCases.Cpus
  4. @using RackPeek.Domain.UseCases.Drives
  5. @using RackPeek.Domain.UseCases.Gpus
  6. @using RackPeek.Domain.UseCases.Nics
  7. @inject IAddCpuUseCase<Server> AddCpuUseCase
  8. @inject IRemoveCpuUseCase<Server> RemoveCpuUseCase
  9. @inject IUpdateCpuUseCase<Server> UpdateCpuUseCase
  10. @inject IAddDriveUseCase<Server> AddDriveUseCase
  11. @inject IUpdateDriveUseCase<Server> UpdateDriveUseCase
  12. @inject IRemoveDriveUseCase<Server> RemoveDriveUseCase
  13. @inject IAddNicUseCase<Server> AddNicUseCase
  14. @inject IUpdateNicUseCase<Server> UpdateNicUseCase
  15. @inject IRemoveNicUseCase<Server> RemoveNicUseCase
  16. @inject IAddGpuUseCase<Server> AddGpuUseCase
  17. @inject IUpdateGpuUseCase<Server> UpdateGpuUseCase
  18. @inject IRemoveGpuUseCase<Server> RemoveGpuUseCase
  19. @inject IGetResourceByNameUseCase<Server> GetByNameUseCase
  20. @inject UpdateServerUseCase UpdateUseCase
  21. @inject IDeleteResourceUseCase<Server> DeleteUseCase
  22. @inject ICloneResourceUseCase<Server> CloneUseCase
  23. @inject IRenameResourceUseCase<Server> RenameUseCase
  24. @inject NavigationManager Nav
  25. <div class="border border-zinc-800 rounded p-4 bg-zinc-900"
  26. data-testid=@($"server-item-{Server.Name.Replace(" ", "-")}")>
  27. <div class="flex justify-between items-center mb-3">
  28. <div class="text-zinc-100 hover:text-emerald-300">
  29. <NavLink href="@($"resources/hardware/{Uri.EscapeDataString(Server.Name)}")" class="block"
  30. data-testid=@($"server-item-{Server.Name.Replace(" ", "-")}-link")>
  31. @Server.Name
  32. </NavLink>
  33. </div>
  34. <div class="flex justify-between items-center mb-3">
  35. <div class="flex items-center gap-2">
  36. <button
  37. data-testid="rename-server-button"
  38. class="text-xs text-blue-400 hover:text-blue-300 transition"
  39. @onclick="OpenRename">
  40. Rename
  41. </button>
  42. <button
  43. data-testid="clone-server-button"
  44. class="text-xs text-emerald-400 hover:text-emerald-300 transition"
  45. @onclick="OpenClone">
  46. Clone
  47. </button>
  48. <button
  49. data-testid="delete-server-button"
  50. class="text-xs text-red-400 hover:text-red-300 transition"
  51. @onclick="ConfirmDelete">
  52. Delete
  53. </button>
  54. </div>
  55. </div>
  56. </div>
  57. <div class="grid grid-cols-1 md:grid-cols-2 gap-3 text-sm">
  58. <div data-testid="server-cpu-section">
  59. <div class="flex items-center justify-between mb-1 group">
  60. <div class="text-zinc-400">CPU
  61. <button
  62. class="hover:text-emerald-400 group-hover:opacity-100 transition"
  63. title="Add CPU"
  64. data-testid="add-cpu-button"
  65. @onclick="OpenAddCpu">
  66. +
  67. </button>
  68. </div>
  69. </div>
  70. @if (Server.Cpus?.Any() == true)
  71. {
  72. <!-- CPU rows -->
  73. @foreach (var cpu in Server.Cpus)
  74. {
  75. <div
  76. class="flex items-center justify-between text-zinc-300 group hover:bg-zinc-800/40 rounded px-1 py-0.5">
  77. <div class="flex gap-2 group-hover:opacity-100 transition">
  78. <button
  79. data-testid=@($"edit-cpu-{cpu.ToString().Replace(" ", "-")}")
  80. class="hover:text-emerald-400"
  81. title="Edit CPU"
  82. @onclick="() => OpenEditCpu(cpu)">
  83. @cpu.Model — @cpu.Cores cores / @cpu.Threads threads
  84. </button>
  85. </div>
  86. </div>
  87. }
  88. }
  89. </div>
  90. <div>
  91. <div class="text-zinc-400 mb-1">RAM
  92. @if (Server.Ram is null)
  93. {
  94. <button
  95. class="hover:text-emerald-400 group-hover:opacity-100 transition"
  96. title="Add RAM"
  97. @onclick="EditRam">
  98. +
  99. </button>
  100. }
  101. </div>
  102. @if (Server.Ram is not null)
  103. {
  104. <div
  105. class="flex items-center justify-between text-zinc-300 group hover:bg-zinc-800/40 rounded px-1 py-0.5">
  106. <div class="flex gap-2 group-hover:opacity-100 transition">
  107. <button
  108. class="hover:text-emerald-400"
  109. title="Edit RAM"
  110. @onclick="EditRam">
  111. @($"{Server.Ram.Size} GB {Server.Ram.Mts} MT/s")
  112. </button>
  113. </div>
  114. </div>
  115. }
  116. </div>
  117. <div>
  118. <div class="flex items-center justify-between mb-1 group">
  119. <div class="text-zinc-400">Drives
  120. <button
  121. class="hover:text-emerald-400 group-hover:opacity-100 transition"
  122. title="Add Drive"
  123. @onclick="OpenAddDrive">
  124. +
  125. </button>
  126. </div>
  127. </div>
  128. @if (Server.Drives?.Any() == true)
  129. {
  130. @foreach (var drive in Server.Drives)
  131. {
  132. <div
  133. class="flex items-center justify-between text-zinc-300 group hover:bg-zinc-800/40 rounded px-1 py-0.5">
  134. <div class="flex gap-2 group-hover:opacity-100 transition">
  135. <button
  136. class="hover:text-emerald-400"
  137. title="Edit Drive"
  138. @onclick="() => OpenEditDrives(drive)">
  139. @drive.Type — @drive.Size GB
  140. </button>
  141. </div>
  142. </div>
  143. }
  144. }
  145. </div>
  146. <div>
  147. <div class="flex items-center justify-between mb-1 group">
  148. <div class="text-zinc-400">
  149. NICs
  150. <button
  151. class="hover:text-emerald-400 group-hover:opacity-100 transition"
  152. title="Add NIC"
  153. @onclick="OpenAddNic">
  154. +
  155. </button>
  156. </div>
  157. </div>
  158. @if (Server.Nics?.Any() == true)
  159. {
  160. @foreach (var nic in Server.Nics)
  161. {
  162. <div
  163. class="flex items-center justify-between text-zinc-300 group hover:bg-zinc-800/40 rounded px-1 py-0.5">
  164. <button
  165. class="hover:text-emerald-400"
  166. title="Edit NIC"
  167. @onclick="() => OpenEditNic(nic)">
  168. @nic.Type — @nic.Speed Gbps (@nic.Ports ports)
  169. </button>
  170. </div>
  171. }
  172. }
  173. </div>
  174. <div>
  175. <div class="flex items-center justify-between mb-1 group">
  176. <div class="text-zinc-400">
  177. GPUs
  178. <button
  179. class="hover:text-emerald-400 group-hover:opacity-100 transition"
  180. title="Add GPU"
  181. @onclick="OpenAddGpu">
  182. +
  183. </button>
  184. </div>
  185. </div>
  186. @if (Server.Gpus?.Any() == true)
  187. {
  188. @foreach (var gpu in Server.Gpus)
  189. {
  190. <div
  191. class="flex items-center justify-between text-zinc-300 group hover:bg-zinc-800/40 rounded px-1 py-0.5">
  192. <button
  193. class="hover:text-emerald-400"
  194. title="Edit GPU"
  195. @onclick="() => OpenEditGpu(gpu)">
  196. @gpu.Model — @gpu.Vram GB VRAM
  197. </button>
  198. </div>
  199. }
  200. }
  201. </div>
  202. <ResourceTagEditor Resource="Server"
  203. TestIdPrefix="server" />
  204. <ResourceLabelEditor Resource="Server"
  205. TestIdPrefix="server" />
  206. <div class="md:col-span-2">
  207. <div class="text-zinc-400 mb-1">Notes</div>
  208. @if (!_editingNotes)
  209. {
  210. <MarkdownViewer
  211. Value="@Server.Notes"
  212. ShowEditButton="true"
  213. OnEdit="BeginNotesEdit"
  214. TestIdPrefix="server-markdown"/>
  215. }
  216. else
  217. {
  218. <MarkdownEditor
  219. @bind-Value="_notesDraft"
  220. ShowActionButtons="true"
  221. OnSave="SaveNotes"
  222. OnCancel="CancelNotesEdit"
  223. TestIdPrefix="server-markdown"/>
  224. }
  225. </div>
  226. </div>
  227. </div>
  228. <CpuModal
  229. IsOpen="@_cpuModalOpen"
  230. IsOpenChanged="v => _cpuModalOpen = v"
  231. Value="@_editingCpu"
  232. OnSubmit="HandleCpuSubmit"
  233. OnDelete="HandleCpuDelete"
  234. TestIdPrefix="server-cpu"/>
  235. <RamModal
  236. IsOpen="@_isRamModalOpen"
  237. IsOpenChanged="v => _isRamModalOpen = v"
  238. Value="@Server.Ram"
  239. OnSubmit="HandleRamSubmit"
  240. TestIdPrefix="server-ram"/>
  241. <DriveModal
  242. IsOpen="@_driveModalOpen"
  243. IsOpenChanged="v => _driveModalOpen = v"
  244. Value="@_editingDrive"
  245. OnSubmit="HandleDriveSubmit"
  246. OnDelete="HandleDriveDelete"
  247. TestIdPrefix="server-drive"/>
  248. <NicModal
  249. IsOpen="@_nicModalOpen"
  250. IsOpenChanged="v => _nicModalOpen = v"
  251. Value="@_editingNic"
  252. OnSubmit="HandleNicSubmit"
  253. OnDelete="HandleNicDelete"
  254. TestIdPrefix="server-nic"/>
  255. <GpuModal
  256. IsOpen="@_gpuModalOpen"
  257. IsOpenChanged="v => _gpuModalOpen = v"
  258. Value="@_editingGpu"
  259. OnSubmit="HandleGpuSubmit"
  260. OnDelete="HandleGpuDelete"
  261. TestIdPrefix="server-gpu"/>
  262. <ConfirmModal
  263. IsOpen="_confirmDeleteOpen"
  264. IsOpenChanged="v => _confirmDeleteOpen = v"
  265. Title="Delete server"
  266. ConfirmText="Delete"
  267. ConfirmClass="bg-red-600 hover:bg-red-500"
  268. OnConfirm="DeleteServer"
  269. TestIdPrefix="server-delete">
  270. Are you sure you want to delete <strong>@Server.Name</strong>?
  271. <br/>
  272. This will detach all dependent systems.
  273. </ConfirmModal>
  274. <StringValueModal
  275. IsOpen="_renameOpen"
  276. IsOpenChanged="v => _renameOpen = v"
  277. Title="Rename server"
  278. Description="Enter a new name for this server"
  279. Label="New server name"
  280. Value="@Server.Name"
  281. OnSubmit="HandleRenameSubmit"
  282. TestIdPrefix="server-rename" />
  283. <StringValueModal
  284. IsOpen="_cloneOpen"
  285. IsOpenChanged="v => _cloneOpen = v"
  286. Title="Clone resource"
  287. Description="Enter a name for the cloned resource"
  288. Label="New resource name"
  289. Value="@($"{Server.Name}-copy")"
  290. OnSubmit="HandleCloneSubmit"
  291. TestIdPrefix="server-clone" />
  292. @code {
  293. [Parameter] [EditorRequired] public Server Server { get; set; } = default!;
  294. #region RAM
  295. private bool _isRamModalOpen;
  296. private void EditRam()
  297. {
  298. _isRamModalOpen = true;
  299. }
  300. private async Task HandleRamSubmit(Ram? value)
  301. {
  302. _isRamModalOpen = false;
  303. await UpdateUseCase.ExecuteAsync(Server.Name, value?.Size ?? 0, value?.Mts ?? 0, Server.Ipmi);
  304. Server = await GetByNameUseCase.ExecuteAsync(Server.Name);
  305. }
  306. #endregion
  307. #region CPU
  308. bool _cpuModalOpen;
  309. int _editingCpuIndex;
  310. Cpu? _editingCpu;
  311. void OpenAddCpu()
  312. {
  313. _editingCpuIndex = -1;
  314. _editingCpu = null;
  315. _cpuModalOpen = true;
  316. }
  317. void OpenEditCpu(Cpu cpu)
  318. {
  319. _editingCpu = cpu;
  320. Server.Cpus ??= new List<Cpu>();
  321. _editingCpuIndex = Server.Cpus.IndexOf(cpu);
  322. ;
  323. _cpuModalOpen = true;
  324. }
  325. async Task HandleCpuSubmit(Cpu cpu)
  326. {
  327. Server.Cpus ??= new List<Cpu>();
  328. if (_editingCpuIndex < 0)
  329. {
  330. await AddCpuUseCase.ExecuteAsync(Server.Name, cpu.Model, cpu.Cores, cpu.Threads);
  331. }
  332. else
  333. {
  334. await UpdateCpuUseCase.ExecuteAsync(Server.Name, _editingCpuIndex, cpu.Model, cpu.Cores, cpu.Threads);
  335. }
  336. Server = await GetByNameUseCase.ExecuteAsync(Server.Name);
  337. }
  338. async Task HandleCpuDelete(Cpu cpu)
  339. {
  340. await RemoveCpuUseCase.ExecuteAsync(Server.Name, _editingCpuIndex);
  341. Server = await GetByNameUseCase.ExecuteAsync(Server.Name);
  342. }
  343. #endregion
  344. #region Drives
  345. bool _driveModalOpen;
  346. int _editingDriveIndex;
  347. Drive? _editingDrive;
  348. void OpenAddDrive()
  349. {
  350. _editingDriveIndex = -1;
  351. _editingDrive = null;
  352. _driveModalOpen = true;
  353. }
  354. void OpenEditDrives(Drive drive)
  355. {
  356. _editingDrive = drive;
  357. Server.Drives ??= new List<Drive>();
  358. _editingDriveIndex = Server.Drives.IndexOf(drive);
  359. ;
  360. _driveModalOpen = true;
  361. }
  362. async Task HandleDriveSubmit(Drive drive)
  363. {
  364. Server.Drives ??= new List<Drive>();
  365. if (_editingDriveIndex < 0)
  366. {
  367. await AddDriveUseCase.ExecuteAsync(Server.Name, drive.Type, drive.Size);
  368. }
  369. else
  370. {
  371. await UpdateDriveUseCase.ExecuteAsync(Server.Name, _editingDriveIndex, drive.Type, drive.Size);
  372. }
  373. Server = await GetByNameUseCase.ExecuteAsync(Server.Name);
  374. StateHasChanged();
  375. }
  376. async Task HandleDriveDelete(Drive drive)
  377. {
  378. await RemoveDriveUseCase.ExecuteAsync(Server.Name, _editingDriveIndex);
  379. Server = await GetByNameUseCase.ExecuteAsync(Server.Name);
  380. StateHasChanged();
  381. }
  382. #endregion
  383. #region NICs
  384. bool _nicModalOpen;
  385. int _editingNicIndex;
  386. Nic? _editingNic;
  387. void OpenAddNic()
  388. {
  389. _editingNicIndex = -1;
  390. _editingNic = null;
  391. _nicModalOpen = true;
  392. }
  393. void OpenEditNic(Nic nic)
  394. {
  395. Server.Nics ??= new List<Nic>();
  396. _editingNicIndex = Server.Nics.IndexOf(nic);
  397. _editingNic = nic;
  398. _nicModalOpen = true;
  399. }
  400. async Task HandleNicSubmit(Nic nic)
  401. {
  402. Server.Nics ??= new List<Nic>();
  403. if (_editingNicIndex < 0)
  404. {
  405. await AddNicUseCase.ExecuteAsync(
  406. Server.Name,
  407. nic.Type,
  408. nic.Speed,
  409. nic.Ports);
  410. }
  411. else
  412. {
  413. await UpdateNicUseCase.ExecuteAsync(
  414. Server.Name,
  415. _editingNicIndex,
  416. nic.Type,
  417. nic.Speed,
  418. nic.Ports);
  419. }
  420. Server = await GetByNameUseCase.ExecuteAsync(Server.Name);
  421. }
  422. async Task HandleNicDelete(Nic nic)
  423. {
  424. await RemoveNicUseCase.ExecuteAsync(Server.Name, _editingNicIndex);
  425. Server = await GetByNameUseCase.ExecuteAsync(Server.Name);
  426. }
  427. #endregion
  428. #region GPUs
  429. bool _gpuModalOpen;
  430. int _editingGpuIndex;
  431. Gpu? _editingGpu;
  432. void OpenAddGpu()
  433. {
  434. _editingGpuIndex = -1;
  435. _editingGpu = null;
  436. _gpuModalOpen = true;
  437. }
  438. void OpenEditGpu(Gpu gpu)
  439. {
  440. Server.Gpus ??= new List<Gpu>();
  441. _editingGpuIndex = Server.Gpus.IndexOf(gpu);
  442. _editingGpu = gpu;
  443. _gpuModalOpen = true;
  444. }
  445. async Task HandleGpuSubmit(Gpu gpu)
  446. {
  447. Server.Gpus ??= new List<Gpu>();
  448. if (_editingGpuIndex < 0)
  449. {
  450. await AddGpuUseCase.ExecuteAsync(
  451. Server.Name,
  452. gpu.Model,
  453. gpu.Vram);
  454. }
  455. else
  456. {
  457. await UpdateGpuUseCase.ExecuteAsync(
  458. Server.Name,
  459. _editingGpuIndex,
  460. gpu.Model,
  461. gpu.Vram);
  462. }
  463. Server = await GetByNameUseCase.ExecuteAsync(Server.Name);
  464. }
  465. async Task HandleGpuDelete(Gpu gpu)
  466. {
  467. await RemoveGpuUseCase.ExecuteAsync(Server.Name, _editingGpuIndex);
  468. Server = await GetByNameUseCase.ExecuteAsync(Server.Name);
  469. }
  470. #endregion
  471. }
  472. @code {
  473. private bool _confirmDeleteOpen;
  474. [Parameter] public EventCallback<string> OnDeleted { get; set; }
  475. void ConfirmDelete()
  476. {
  477. _confirmDeleteOpen = true;
  478. }
  479. async Task DeleteServer()
  480. {
  481. _confirmDeleteOpen = false;
  482. await DeleteUseCase.ExecuteAsync(Server.Name);
  483. if (OnDeleted.HasDelegate)
  484. await OnDeleted.InvokeAsync(Server.Name);
  485. }
  486. }
  487. @code
  488. {
  489. bool _renameOpen;
  490. void OpenRename()
  491. {
  492. _renameOpen = true;
  493. }
  494. async Task HandleRenameSubmit(string newName)
  495. {
  496. await RenameUseCase.ExecuteAsync(Server.Name, newName);
  497. Nav.NavigateTo($"resources/hardware/{Uri.EscapeDataString(newName)}");
  498. }
  499. }
  500. @code
  501. {
  502. bool _cloneOpen;
  503. void OpenClone()
  504. {
  505. _cloneOpen = true;
  506. }
  507. async Task HandleCloneSubmit(string newName)
  508. {
  509. await CloneUseCase.ExecuteAsync(Server.Name, newName);
  510. Nav.NavigateTo($"resources/hardware/{Uri.EscapeDataString(newName)}");
  511. }
  512. }
  513. @code
  514. {
  515. bool _editingNotes;
  516. string? _notesDraft;
  517. void BeginNotesEdit()
  518. {
  519. _editingNotes = true;
  520. _notesDraft = Server.Notes; // draft buffer
  521. }
  522. void CancelNotesEdit()
  523. {
  524. _editingNotes = false;
  525. _notesDraft = null; // discard
  526. }
  527. async Task SaveNotes()
  528. {
  529. _editingNotes = false;
  530. await UpdateUseCase.ExecuteAsync(
  531. Server.Name,
  532. Server.Ram?.Size,
  533. Server.Ram?.Mts,
  534. Server.Ipmi,
  535. _notesDraft);
  536. Server = await GetByNameUseCase.ExecuteAsync(Server.Name);
  537. _notesDraft = null;
  538. }
  539. }