ServerCardComponent.razor 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652
  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/{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. </div>
  203. <ResourceTagEditor Resource="Server"
  204. TestIdPrefix="server" />
  205. <div class="md:col-span-2">
  206. <div class="text-zinc-400 mb-1">Notes</div>
  207. @if (!_editingNotes)
  208. {
  209. <MarkdownViewer
  210. Value="@Server.Notes"
  211. ShowEditButton="true"
  212. OnEdit="BeginNotesEdit"
  213. TestIdPrefix="server-markdown"/>
  214. }
  215. else
  216. {
  217. <MarkdownEditor
  218. @bind-Value="_notesDraft"
  219. ShowActionButtons="true"
  220. OnSave="SaveNotes"
  221. OnCancel="CancelNotesEdit"
  222. TestIdPrefix="server-markdown"/>
  223. }
  224. </div>
  225. </div>
  226. <CpuModal
  227. IsOpen="@_cpuModalOpen"
  228. IsOpenChanged="v => _cpuModalOpen = v"
  229. Value="@_editingCpu"
  230. OnSubmit="HandleCpuSubmit"
  231. OnDelete="HandleCpuDelete"
  232. TestIdPrefix="server-cpu"/>
  233. <RamModal
  234. IsOpen="@_isRamModalOpen"
  235. IsOpenChanged="v => _isRamModalOpen = v"
  236. Value="@Server.Ram"
  237. OnSubmit="HandleRamSubmit"
  238. TestIdPrefix="server-ram"/>
  239. <DriveModal
  240. IsOpen="@_driveModalOpen"
  241. IsOpenChanged="v => _driveModalOpen = v"
  242. Value="@_editingDrive"
  243. OnSubmit="HandleDriveSubmit"
  244. OnDelete="HandleDriveDelete"
  245. TestIdPrefix="server-drive"/>
  246. <NicModal
  247. IsOpen="@_nicModalOpen"
  248. IsOpenChanged="v => _nicModalOpen = v"
  249. Value="@_editingNic"
  250. OnSubmit="HandleNicSubmit"
  251. OnDelete="HandleNicDelete"
  252. TestIdPrefix="server-nic"/>
  253. <GpuModal
  254. IsOpen="@_gpuModalOpen"
  255. IsOpenChanged="v => _gpuModalOpen = v"
  256. Value="@_editingGpu"
  257. OnSubmit="HandleGpuSubmit"
  258. OnDelete="HandleGpuDelete"
  259. TestIdPrefix="server-gpu"/>
  260. <ConfirmModal
  261. IsOpen="_confirmDeleteOpen"
  262. IsOpenChanged="v => _confirmDeleteOpen = v"
  263. Title="Delete server"
  264. ConfirmText="Delete"
  265. ConfirmClass="bg-red-600 hover:bg-red-500"
  266. OnConfirm="DeleteServer"
  267. TestIdPrefix="server-delete">
  268. Are you sure you want to delete <strong>@Server.Name</strong>?
  269. <br/>
  270. This will detach all dependent systems.
  271. </ConfirmModal>
  272. <StringValueModal
  273. IsOpen="_renameOpen"
  274. IsOpenChanged="v => _renameOpen = v"
  275. Title="Rename server"
  276. Description="Enter a new name for this server"
  277. Label="New server name"
  278. Value="@Server.Name"
  279. OnSubmit="HandleRenameSubmit"
  280. TestIdPrefix="server-rename" />
  281. <StringValueModal
  282. IsOpen="_cloneOpen"
  283. IsOpenChanged="v => _cloneOpen = v"
  284. Title="Clone resource"
  285. Description="Enter a name for the cloned resource"
  286. Label="New resource name"
  287. Value="@($"{Server.Name}-copy")"
  288. OnSubmit="HandleCloneSubmit"
  289. TestIdPrefix="server-clone" />
  290. @code {
  291. [Parameter] [EditorRequired] public Server Server { get; set; } = default!;
  292. #region RAM
  293. private bool _isRamModalOpen;
  294. private void EditRam()
  295. {
  296. _isRamModalOpen = true;
  297. }
  298. private async Task HandleRamSubmit(Ram? value)
  299. {
  300. _isRamModalOpen = false;
  301. await UpdateUseCase.ExecuteAsync(Server.Name, value?.Size ?? 0, value?.Mts ?? 0, Server.Ipmi);
  302. Server = await GetByNameUseCase.ExecuteAsync(Server.Name);
  303. }
  304. #endregion
  305. #region CPU
  306. bool _cpuModalOpen;
  307. int _editingCpuIndex;
  308. Cpu? _editingCpu;
  309. void OpenAddCpu()
  310. {
  311. _editingCpuIndex = -1;
  312. _editingCpu = null;
  313. _cpuModalOpen = true;
  314. }
  315. void OpenEditCpu(Cpu cpu)
  316. {
  317. _editingCpu = cpu;
  318. Server.Cpus ??= new List<Cpu>();
  319. _editingCpuIndex = Server.Cpus.IndexOf(cpu);
  320. ;
  321. _cpuModalOpen = true;
  322. }
  323. async Task HandleCpuSubmit(Cpu cpu)
  324. {
  325. Server.Cpus ??= new List<Cpu>();
  326. if (_editingCpuIndex < 0)
  327. {
  328. await AddCpuUseCase.ExecuteAsync(Server.Name, cpu.Model, cpu.Cores, cpu.Threads);
  329. }
  330. else
  331. {
  332. await UpdateCpuUseCase.ExecuteAsync(Server.Name, _editingCpuIndex, cpu.Model, cpu.Cores, cpu.Threads);
  333. }
  334. Server = await GetByNameUseCase.ExecuteAsync(Server.Name);
  335. }
  336. async Task HandleCpuDelete(Cpu cpu)
  337. {
  338. await RemoveCpuUseCase.ExecuteAsync(Server.Name, _editingCpuIndex);
  339. Server = await GetByNameUseCase.ExecuteAsync(Server.Name);
  340. }
  341. #endregion
  342. #region Drives
  343. bool _driveModalOpen;
  344. int _editingDriveIndex;
  345. Drive? _editingDrive;
  346. void OpenAddDrive()
  347. {
  348. _editingDriveIndex = -1;
  349. _editingDrive = null;
  350. _driveModalOpen = true;
  351. }
  352. void OpenEditDrives(Drive drive)
  353. {
  354. _editingDrive = drive;
  355. Server.Drives ??= new List<Drive>();
  356. _editingDriveIndex = Server.Drives.IndexOf(drive);
  357. ;
  358. _driveModalOpen = true;
  359. }
  360. async Task HandleDriveSubmit(Drive drive)
  361. {
  362. Server.Drives ??= new List<Drive>();
  363. if (_editingDriveIndex < 0)
  364. {
  365. await AddDriveUseCase.ExecuteAsync(Server.Name, drive.Type, drive.Size);
  366. }
  367. else
  368. {
  369. await UpdateDriveUseCase.ExecuteAsync(Server.Name, _editingDriveIndex, drive.Type, drive.Size);
  370. }
  371. Server = await GetByNameUseCase.ExecuteAsync(Server.Name);
  372. StateHasChanged();
  373. }
  374. async Task HandleDriveDelete(Drive drive)
  375. {
  376. await RemoveDriveUseCase.ExecuteAsync(Server.Name, _editingDriveIndex);
  377. Server = await GetByNameUseCase.ExecuteAsync(Server.Name);
  378. StateHasChanged();
  379. }
  380. #endregion
  381. #region NICs
  382. bool _nicModalOpen;
  383. int _editingNicIndex;
  384. Nic? _editingNic;
  385. void OpenAddNic()
  386. {
  387. _editingNicIndex = -1;
  388. _editingNic = null;
  389. _nicModalOpen = true;
  390. }
  391. void OpenEditNic(Nic nic)
  392. {
  393. Server.Nics ??= new List<Nic>();
  394. _editingNicIndex = Server.Nics.IndexOf(nic);
  395. _editingNic = nic;
  396. _nicModalOpen = true;
  397. }
  398. async Task HandleNicSubmit(Nic nic)
  399. {
  400. Server.Nics ??= new List<Nic>();
  401. if (_editingNicIndex < 0)
  402. {
  403. await AddNicUseCase.ExecuteAsync(
  404. Server.Name,
  405. nic.Type,
  406. nic.Speed,
  407. nic.Ports);
  408. }
  409. else
  410. {
  411. await UpdateNicUseCase.ExecuteAsync(
  412. Server.Name,
  413. _editingNicIndex,
  414. nic.Type,
  415. nic.Speed,
  416. nic.Ports);
  417. }
  418. Server = await GetByNameUseCase.ExecuteAsync(Server.Name);
  419. }
  420. async Task HandleNicDelete(Nic nic)
  421. {
  422. await RemoveNicUseCase.ExecuteAsync(Server.Name, _editingNicIndex);
  423. Server = await GetByNameUseCase.ExecuteAsync(Server.Name);
  424. }
  425. #endregion
  426. #region GPUs
  427. bool _gpuModalOpen;
  428. int _editingGpuIndex;
  429. Gpu? _editingGpu;
  430. void OpenAddGpu()
  431. {
  432. _editingGpuIndex = -1;
  433. _editingGpu = null;
  434. _gpuModalOpen = true;
  435. }
  436. void OpenEditGpu(Gpu gpu)
  437. {
  438. Server.Gpus ??= new List<Gpu>();
  439. _editingGpuIndex = Server.Gpus.IndexOf(gpu);
  440. _editingGpu = gpu;
  441. _gpuModalOpen = true;
  442. }
  443. async Task HandleGpuSubmit(Gpu gpu)
  444. {
  445. Server.Gpus ??= new List<Gpu>();
  446. if (_editingGpuIndex < 0)
  447. {
  448. await AddGpuUseCase.ExecuteAsync(
  449. Server.Name,
  450. gpu.Model,
  451. gpu.Vram);
  452. }
  453. else
  454. {
  455. await UpdateGpuUseCase.ExecuteAsync(
  456. Server.Name,
  457. _editingGpuIndex,
  458. gpu.Model,
  459. gpu.Vram);
  460. }
  461. Server = await GetByNameUseCase.ExecuteAsync(Server.Name);
  462. }
  463. async Task HandleGpuDelete(Gpu gpu)
  464. {
  465. await RemoveGpuUseCase.ExecuteAsync(Server.Name, _editingGpuIndex);
  466. Server = await GetByNameUseCase.ExecuteAsync(Server.Name);
  467. }
  468. #endregion
  469. }
  470. @code {
  471. private bool _confirmDeleteOpen;
  472. [Parameter] public EventCallback<string> OnDeleted { get; set; }
  473. void ConfirmDelete()
  474. {
  475. _confirmDeleteOpen = true;
  476. }
  477. async Task DeleteServer()
  478. {
  479. _confirmDeleteOpen = false;
  480. await DeleteUseCase.ExecuteAsync(Server.Name);
  481. if (OnDeleted.HasDelegate)
  482. await OnDeleted.InvokeAsync(Server.Name);
  483. }
  484. }
  485. @code
  486. {
  487. bool _renameOpen;
  488. void OpenRename()
  489. {
  490. _renameOpen = true;
  491. }
  492. async Task HandleRenameSubmit(string newName)
  493. {
  494. await RenameUseCase.ExecuteAsync(Server.Name, newName);
  495. Nav.NavigateTo($"resources/hardware/{newName}");
  496. }
  497. }
  498. @code
  499. {
  500. bool _cloneOpen;
  501. void OpenClone()
  502. {
  503. _cloneOpen = true;
  504. }
  505. async Task HandleCloneSubmit(string newName)
  506. {
  507. await CloneUseCase.ExecuteAsync(Server.Name, newName);
  508. Nav.NavigateTo($"resources/hardware/{newName}");
  509. }
  510. }
  511. @code
  512. {
  513. bool _editingNotes;
  514. string? _notesDraft;
  515. void BeginNotesEdit()
  516. {
  517. _editingNotes = true;
  518. _notesDraft = Server.Notes; // draft buffer
  519. }
  520. void CancelNotesEdit()
  521. {
  522. _editingNotes = false;
  523. _notesDraft = null; // discard
  524. }
  525. async Task SaveNotes()
  526. {
  527. _editingNotes = false;
  528. await UpdateUseCase.ExecuteAsync(
  529. Server.Name,
  530. Server.Ram?.Size,
  531. Server.Ram?.Mts,
  532. Server.Ipmi,
  533. _notesDraft);
  534. Server = await GetByNameUseCase.ExecuteAsync(Server.Name);
  535. _notesDraft = null;
  536. }
  537. }