ServiceCardComponent.razor 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356
  1. @using RackPeek.Domain.Resources.Hardware.Desktops
  2. @using RackPeek.Domain.Resources.Services
  3. @using RackPeek.Domain.Resources.Services.UseCases
  4. @using RackPeek.Domain.UseCases
  5. @inject UpdateServiceUseCase UpdateServiceUseCase
  6. @inject IGetAllResourcesByKindUseCase<Service> GetAllUseCase
  7. @inject IGetResourceByNameUseCase<Service> GetByNameUseCase
  8. @inject IDeleteResourceUseCase<Service> DeleteServiceUseCase
  9. @inject ICloneResourceUseCase<Service> CloneUseCase
  10. @inject NavigationManager Nav
  11. @inject IRenameResourceUseCase<Service> RenameUseCase
  12. <div class="border border-zinc-800 rounded p-4 bg-zinc-900" data-testid=@($"service-item-{@Service.Name.Replace(" ", "-")}")>
  13. <div class="flex justify-between items-center mb-3">
  14. <NavLink href="@($"resources/services/{Service.Name}")" class="block">
  15. <div class="text-zinc-100 hover:text-emerald-300">
  16. @Service.Name
  17. </div>
  18. </NavLink>
  19. <div class="flex gap-3 text-xs">
  20. @if (!_isEditing)
  21. {
  22. <button class="text-zinc-400 hover:text-zinc-200"
  23. data-testid="edit-service-button"
  24. @onclick="BeginEdit">
  25. Edit
  26. </button>
  27. <button class="text-xs text-blue-400 hover:text-blue-300 transition"
  28. title="Rename service"
  29. data-testid="rename-service-button"
  30. @onclick="OpenRename">
  31. Rename
  32. </button>
  33. <button
  34. class="text-xs text-emerald-400 hover:text-emerald-300 transition"
  35. title="Clone service"
  36. data-testid="clone-service-button"
  37. @onclick="OpenClone">
  38. Clone
  39. </button>
  40. <button
  41. class="text-xs text-red-400 hover:text-red-300 transition"
  42. title="Delete server"
  43. data-testid="delete-service-button"
  44. @onclick="ConfirmDelete">
  45. Delete
  46. </button>
  47. }
  48. else
  49. {
  50. <button class="text-emerald-400 hover:text-emerald-300"
  51. data-testid="save-service-button"
  52. @onclick="Save">
  53. Save
  54. </button>
  55. <button class="text-zinc-500 hover:text-zinc-300"
  56. data-testid="cancel-service-button"
  57. @onclick="Cancel">
  58. Cancel
  59. </button>
  60. }
  61. </div>
  62. </div>
  63. <div class="grid grid-cols-1 md:grid-cols-2 gap-3 text-sm">
  64. <!-- IP -->
  65. <div>
  66. <div class="text-zinc-400 mb-1">IP</div>
  67. @if (_isEditing)
  68. {
  69. <input
  70. class="w-full px-3 py-2 rounded-md
  71. bg-zinc-800 text-zinc-100
  72. border border-zinc-600
  73. placeholder-zinc-500
  74. focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500
  75. hover:border-zinc-400
  76. transition-colors duration-150
  77. cursor-text"
  78. @bind="_edit.Ip"/>
  79. }
  80. else if (!string.IsNullOrWhiteSpace(Service.Network?.Ip))
  81. {
  82. <div class="text-zinc-300">@Service.Network!.Ip</div>
  83. }
  84. </div>
  85. <!-- Port -->
  86. <div>
  87. <div class="text-zinc-400 mb-1">Port</div>
  88. @if (_isEditing)
  89. {
  90. <input type="number"
  91. @bind="_edit.Port"/>
  92. }
  93. else if (Service.Network?.Port.HasValue == true)
  94. {
  95. <div class="text-zinc-300">@Service.Network.Port</div>
  96. }
  97. </div>
  98. <!-- Protocol -->
  99. <div>
  100. <div class="text-zinc-400 mb-1">Protocol</div>
  101. @if (_isEditing)
  102. {
  103. <input
  104. class="w-full px-3 py-2 rounded-md
  105. bg-zinc-800 text-zinc-100
  106. border border-zinc-600
  107. placeholder-zinc-500
  108. focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500
  109. hover:border-zinc-400
  110. transition-colors duration-150
  111. cursor-text"
  112. @bind="_edit.Protocol"/>
  113. }
  114. else if (!string.IsNullOrWhiteSpace(Service.Network?.Protocol))
  115. {
  116. <div class="text-zinc-300">@Service.Network!.Protocol</div>
  117. }
  118. </div>
  119. <!-- URL -->
  120. <div>
  121. <div class="text-zinc-400 mb-1">URL</div>
  122. @if (_isEditing)
  123. {
  124. <input
  125. class="w-full px-3 py-2 rounded-md
  126. bg-zinc-800 text-zinc-100
  127. border border-zinc-600
  128. placeholder-zinc-500
  129. focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500
  130. hover:border-zinc-400
  131. transition-colors duration-150
  132. cursor-text"
  133. @bind="_edit.Url"/>
  134. }
  135. else if (!string.IsNullOrWhiteSpace(Service.Network?.Url))
  136. {
  137. <a href="@Service.Network!.Url"
  138. target="_blank"
  139. rel="noopener noreferrer"
  140. class="text-emerald-400 hover:underline break-all">
  141. @Service.Network.Url
  142. </a>
  143. }
  144. </div>
  145. <!-- Runs On -->
  146. <div>
  147. <div class="text-zinc-400 mb-1">Runs On</div>
  148. @if (_isEditing)
  149. {
  150. <button
  151. class="hover:text-emerald-400"
  152. title="Edit Runs On"
  153. @onclick="() => _selectParentOpen = true">
  154. @if (!string.IsNullOrWhiteSpace(Service.RunsOn))
  155. {
  156. @($"{Service.RunsOn} +")
  157. }
  158. else
  159. {
  160. @("Edit parent")
  161. }
  162. </button>
  163. }
  164. else if (!string.IsNullOrWhiteSpace(Service.RunsOn))
  165. {
  166. <NavLink href="@($"resources/systems/{Service.RunsOn}")"
  167. class="text-emerald-400">
  168. @Service.RunsOn
  169. </NavLink>
  170. }
  171. </div>
  172. <ResourceTagEditor Resource="Service" />
  173. <div class="md:col-span-2">
  174. <div class="text-zinc-400 mb-1">Notes</div>
  175. @if (_isEditing)
  176. {
  177. <MarkdownEditor
  178. @bind-Value="_edit.Notes"
  179. ShowActionButtons="false" />
  180. }
  181. else
  182. {
  183. <MarkdownViewer
  184. Value="@Service.Notes"
  185. ShowEditButton="false" />
  186. }
  187. </div>
  188. </div>
  189. </div>
  190. <SystemSelectionModal
  191. IsOpen="@_selectParentOpen"
  192. IsOpenChanged="v => _selectParentOpen = v"
  193. Title="Select a parent"
  194. Value="@SelectedParentName"
  195. OnAccept="HandleParentSelected"/>
  196. <ConfirmModal
  197. IsOpen="_confirmDeleteOpen"
  198. IsOpenChanged="v => _confirmDeleteOpen = v"
  199. Title="Delete service"
  200. ConfirmText="Delete"
  201. ConfirmClass="bg-red-600 hover:bg-red-500"
  202. OnConfirm="DeleteServer" TestIdPrefix="Service">
  203. Are you sure you want to delete <strong>@Service.Name</strong>?
  204. </ConfirmModal>
  205. <StringValueModal
  206. IsOpen="_cloneOpen"
  207. IsOpenChanged="v => _cloneOpen = v"
  208. Title="Clone service"
  209. Description="Enter a name for the cloned service"
  210. Label="New service name"
  211. Value="@($"{Service.Name}-copy")"
  212. OnSubmit="HandleCloneSubmit" />
  213. <StringValueModal
  214. IsOpen="_renameOpen"
  215. IsOpenChanged="v => _renameOpen = v"
  216. Title="Rename service"
  217. Description="Enter a new name for this service"
  218. Label="New service name"
  219. Value="@Service.Name"
  220. OnSubmit="HandleRenameSubmit" />
  221. @code
  222. {
  223. bool _cloneOpen;
  224. void OpenClone()
  225. {
  226. _cloneOpen = true;
  227. }
  228. async Task HandleCloneSubmit(string newName)
  229. {
  230. await CloneUseCase.ExecuteAsync(Service.Name, newName);
  231. Nav.NavigateTo($"resources/services/{newName}");
  232. }
  233. }
  234. @code {
  235. [Parameter] [EditorRequired] public Service Service { get; set; } = default!;
  236. [Parameter] public EventCallback<string> OnSave { get; set; }
  237. private bool _isEditing;
  238. private ServiceEditModel _edit = new();
  239. void BeginEdit()
  240. {
  241. _edit = ServiceEditModel.From(Service);
  242. _isEditing = true;
  243. }
  244. async Task Save()
  245. {
  246. _isEditing = false;
  247. await UpdateServiceUseCase.ExecuteAsync(
  248. _edit.Name,
  249. _edit.Ip,
  250. _edit.Port,
  251. _edit.Protocol,
  252. _edit.Url,
  253. _edit.RunsOn,
  254. _edit.Notes
  255. );
  256. await OnSave.InvokeAsync(Service.Name);
  257. }
  258. void Cancel()
  259. {
  260. _isEditing = false;
  261. }
  262. bool _selectParentOpen;
  263. string? SelectedParentName;
  264. async Task HandleParentSelected(string? name)
  265. {
  266. SelectedParentName = name;
  267. await UpdateServiceUseCase.ExecuteAsync(
  268. Service.Name,
  269. Service.Network?.Ip,
  270. Service.Network?.Port,
  271. Service.Network?.Protocol,
  272. Service.Network?.Url,
  273. name,
  274. Service.Notes);
  275. Service = await GetByNameUseCase.ExecuteAsync(Service.Name);
  276. _edit = ServiceEditModel.From(Service);
  277. }
  278. }
  279. @code {
  280. private bool _confirmDeleteOpen;
  281. [Parameter] public EventCallback<string> OnDeleted { get; set; }
  282. void ConfirmDelete()
  283. {
  284. _confirmDeleteOpen = true;
  285. }
  286. async Task DeleteServer()
  287. {
  288. _confirmDeleteOpen = false;
  289. await DeleteServiceUseCase.ExecuteAsync(Service.Name);
  290. if (OnDeleted.HasDelegate)
  291. await OnDeleted.InvokeAsync(Service.Name);
  292. }
  293. }
  294. @code
  295. {
  296. bool _renameOpen;
  297. void OpenRename()
  298. {
  299. _renameOpen = true;
  300. }
  301. async Task HandleRenameSubmit(string newName)
  302. {
  303. await RenameUseCase.ExecuteAsync(Service.Name, newName);
  304. Nav.NavigateTo($"resources/services/{newName}");
  305. }
  306. }