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