ServiceCardComponent.razor 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370
  1. @inject UpdateServiceUseCase UpdateUseCase
  2. @inject IGetResourceByNameUseCase<Service> GetByNameUseCase
  3. @inject IDeleteResourceUseCase<Service> DeleteUseCase
  4. @inject ICloneResourceUseCase<Service> CloneUseCase
  5. @inject IRenameResourceUseCase<Service> RenameUseCase
  6. @inject NavigationManager Nav
  7. <div class="border border-zinc-800 rounded p-4 bg-zinc-900"
  8. data-testid=@($"service-item-{Service.Name.Replace(" ", "-")}")>
  9. <div class="flex justify-between items-center mb-3">
  10. <NavLink href="@($"resources/services/{Uri.EscapeDataString(Service.Name)}")" class="block"
  11. data-testid=@($"service-item-{Service.Name.Replace(" ", "-")}-link")>
  12. <div class="text-zinc-100 hover:text-emerald-300">
  13. @Service.Name
  14. </div>
  15. </NavLink>
  16. <div class="flex gap-3 text-xs">
  17. @if (!_isEditing)
  18. {
  19. <button class="text-zinc-400 hover:text-zinc-200"
  20. data-testid="edit-service-button"
  21. @onclick="BeginEdit">
  22. Edit
  23. </button>
  24. <button class="text-xs text-blue-400 hover:text-blue-300 transition"
  25. title="Rename service"
  26. data-testid="rename-service-button"
  27. @onclick="OpenRename">
  28. Rename
  29. </button>
  30. <button
  31. class="text-xs text-emerald-400 hover:text-emerald-300 transition"
  32. title="Clone service"
  33. data-testid="clone-service-button"
  34. @onclick="OpenClone">
  35. Clone
  36. </button>
  37. <button
  38. class="text-xs text-red-400 hover:text-red-300 transition"
  39. title="Delete server"
  40. data-testid="delete-service-button"
  41. @onclick="ConfirmDelete">
  42. Delete
  43. </button>
  44. }
  45. else
  46. {
  47. <button class="text-emerald-400 hover:text-emerald-300"
  48. data-testid="save-service-button"
  49. @onclick="Save">
  50. Save
  51. </button>
  52. <button class="text-zinc-500 hover:text-zinc-300"
  53. data-testid="cancel-service-button"
  54. @onclick="Cancel">
  55. Cancel
  56. </button>
  57. }
  58. </div>
  59. </div>
  60. <div class="grid grid-cols-1 md:grid-cols-2 gap-3 text-sm">
  61. <!-- IP -->
  62. <div data-testid="service-ip-section">
  63. <div class="text-zinc-400 mb-1">IP</div>
  64. @if (_isEditing)
  65. {
  66. <input
  67. data-testid="service-ip-input"
  68. class="w-full px-3 py-2 rounded-md
  69. bg-zinc-800 text-zinc-100
  70. border border-zinc-600"
  71. @bind="_edit.Ip"/>
  72. }
  73. else if (!string.IsNullOrWhiteSpace(Service.Network?.Ip))
  74. {
  75. <div class="text-zinc-300"
  76. data-testid="service-ip-value">
  77. @Service.Network!.Ip
  78. </div>
  79. }
  80. </div>
  81. <!-- Port -->
  82. <div data-testid="service-port-section">
  83. <div class="text-zinc-400 mb-1">Port</div>
  84. @if (_isEditing)
  85. {
  86. <input type="number"
  87. data-testid="service-port-input"
  88. @bind="_edit.Port"/>
  89. }
  90. else if (Service.Network?.Port.HasValue == true)
  91. {
  92. <div class="text-zinc-300"
  93. data-testid="service-port-value">
  94. @Service.Network.Port
  95. </div>
  96. }
  97. </div>
  98. <!-- Protocol -->
  99. <div data-testid="service-protocol-section">
  100. <div class="text-zinc-400 mb-1">Protocol</div>
  101. @if (_isEditing)
  102. {
  103. <input
  104. data-testid="service-protocol-input"
  105. class="w-full px-3 py-2 rounded-md bg-zinc-800 text-zinc-100 border border-zinc-600"
  106. @bind="_edit.Protocol"/>
  107. }
  108. else if (!string.IsNullOrWhiteSpace(Service.Network?.Protocol))
  109. {
  110. <div class="text-zinc-300"
  111. data-testid="service-protocol-value">
  112. @Service.Network!.Protocol
  113. </div>
  114. }
  115. </div>
  116. <!-- URL -->
  117. <div data-testid="service-url-section">
  118. <div class="text-zinc-400 mb-1">URL</div>
  119. @if (_isEditing)
  120. {
  121. <input
  122. data-testid="service-url-input"
  123. class="w-full px-3 py-2 rounded-md bg-zinc-800 text-zinc-100 border border-zinc-600"
  124. @bind="_edit.Url"/>
  125. }
  126. else if (!string.IsNullOrWhiteSpace(Service.Network?.Url))
  127. {
  128. <a href="@Service.Network!.Url"
  129. data-testid="service-url-value"
  130. target="_blank"
  131. rel="noopener noreferrer"
  132. class="text-emerald-400 hover:underline break-all">
  133. @Service.Network.Url
  134. </a>
  135. }
  136. </div>
  137. <!-- Runs On -->
  138. <div data-testid="service-runson-section">
  139. <div class="text-zinc-400 mb-1">Runs On</div>
  140. @if (_isEditing)
  141. {
  142. <button
  143. data-testid="service-runson-button"
  144. class="hover:text-emerald-400"
  145. @onclick="() => _selectParentOpen = true">
  146. @if (!string.IsNullOrWhiteSpace(Service.RunsOn))
  147. {
  148. @($"{Service.RunsOn} +")
  149. }
  150. else
  151. {
  152. @("Edit parent")
  153. }
  154. </button>
  155. }
  156. else if (!string.IsNullOrWhiteSpace(Service.RunsOn))
  157. {
  158. <NavLink href="@($"resources/systems/{Uri.EscapeDataString(Service.RunsOn)}")"
  159. data-testid="service-runson-link"
  160. class="text-emerald-400">
  161. @Service.RunsOn
  162. </NavLink>
  163. }
  164. </div>
  165. <ResourceTagEditor Resource="Service"
  166. TestIdPrefix="service" />
  167. <ResourceLabelEditor Resource="Service"
  168. TestIdPrefix="service" />
  169. <div class="md:col-span-2">
  170. <div class="text-zinc-400 mb-1">Notes</div>
  171. @if (_isEditing)
  172. {
  173. <MarkdownEditor
  174. @bind-Value="_edit.Notes"
  175. ShowActionButtons="false"
  176. TestIdPrefix="service-notes-editor" />
  177. }
  178. else
  179. {
  180. <MarkdownViewer
  181. Value="@Service.Notes"
  182. ShowEditButton="false"
  183. TestIdPrefix="service-notes-viewer" />
  184. }
  185. </div>
  186. </div>
  187. </div>
  188. <SystemSelectionModal
  189. IsOpen="@_selectParentOpen"
  190. IsOpenChanged="v => _selectParentOpen = v"
  191. Title="Select a parent"
  192. Value="@SelectedParentName"
  193. OnAccept="HandleParentSelected"/>
  194. <ConfirmModal
  195. IsOpen="_confirmDeleteOpen"
  196. IsOpenChanged="v => _confirmDeleteOpen = v"
  197. Title="Delete service"
  198. ConfirmText="Delete"
  199. ConfirmClass="bg-red-600 hover:bg-red-500"
  200. OnConfirm="DeleteServer" TestIdPrefix="service-delete">
  201. Are you sure you want to delete <strong>@Service.Name</strong>?
  202. </ConfirmModal>
  203. <StringValueModal
  204. IsOpen="_cloneOpen"
  205. IsOpenChanged="v => _cloneOpen = v"
  206. Title="Clone service"
  207. Description="Enter a name for the cloned service"
  208. Label="New service name"
  209. Value="@($"{Service.Name}-copy")"
  210. OnSubmit="HandleCloneSubmit"
  211. TestIdPrefix="service-clone"/>
  212. <StringValueModal
  213. IsOpen="_renameOpen"
  214. IsOpenChanged="v => _renameOpen = v"
  215. Title="Rename service"
  216. Description="Enter a new name for this service"
  217. Label="New service name"
  218. Value="@Service.Name"
  219. OnSubmit="HandleRenameSubmit"
  220. TestIdPrefix="service-rename"/>
  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/{Uri.EscapeDataString(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 UpdateUseCase.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 UpdateUseCase.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 DeleteUseCase.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/{Uri.EscapeDataString(newName)}");
  305. }
  306. }