ServerCardComponent.razor 17 KB

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