ServerCardComponent.razor 16 KB

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