ServerCardComponent.razor 18 KB

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