ServiceCardComponent.razor 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365
  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/{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/{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. <div class="md:col-span-2">
  167. <div class="text-zinc-400 mb-1">Notes</div>
  168. @if (_isEditing)
  169. {
  170. <MarkdownEditor
  171. @bind-Value="_edit.Notes"
  172. ShowActionButtons="false"
  173. TestIdPrefix="service-notes-editor" />
  174. }
  175. else
  176. {
  177. <MarkdownViewer
  178. Value="@Service.Notes"
  179. ShowEditButton="false"
  180. TestIdPrefix="service-notes-viewer" />
  181. }
  182. </div>
  183. </div>
  184. </div>
  185. <SystemSelectionModal
  186. IsOpen="@_selectParentOpen"
  187. IsOpenChanged="v => _selectParentOpen = v"
  188. Title="Select a parent"
  189. Value="@SelectedParentName"
  190. OnAccept="HandleParentSelected"/>
  191. <ConfirmModal
  192. IsOpen="_confirmDeleteOpen"
  193. IsOpenChanged="v => _confirmDeleteOpen = v"
  194. Title="Delete service"
  195. ConfirmText="Delete"
  196. ConfirmClass="bg-red-600 hover:bg-red-500"
  197. OnConfirm="DeleteServer" TestIdPrefix="service-delete">
  198. Are you sure you want to delete <strong>@Service.Name</strong>?
  199. </ConfirmModal>
  200. <StringValueModal
  201. IsOpen="_cloneOpen"
  202. IsOpenChanged="v => _cloneOpen = v"
  203. Title="Clone service"
  204. Description="Enter a name for the cloned service"
  205. Label="New service name"
  206. Value="@($"{Service.Name}-copy")"
  207. OnSubmit="HandleCloneSubmit"
  208. TestIdPrefix="service-clone"/>
  209. <StringValueModal
  210. IsOpen="_renameOpen"
  211. IsOpenChanged="v => _renameOpen = v"
  212. Title="Rename service"
  213. Description="Enter a new name for this service"
  214. Label="New service name"
  215. Value="@Service.Name"
  216. OnSubmit="HandleRenameSubmit"
  217. TestIdPrefix="service-rename"/>
  218. @code
  219. {
  220. bool _cloneOpen;
  221. void OpenClone()
  222. {
  223. _cloneOpen = true;
  224. }
  225. async Task HandleCloneSubmit(string newName)
  226. {
  227. await CloneUseCase.ExecuteAsync(Service.Name, newName);
  228. Nav.NavigateTo($"resources/services/{newName}");
  229. }
  230. }
  231. @code {
  232. [Parameter] [EditorRequired] public Service Service { get; set; } = default!;
  233. [Parameter] public EventCallback<string> OnSave { get; set; }
  234. private bool _isEditing;
  235. private ServiceEditModel _edit = new();
  236. void BeginEdit()
  237. {
  238. _edit = ServiceEditModel.From(Service);
  239. _isEditing = true;
  240. }
  241. async Task Save()
  242. {
  243. _isEditing = false;
  244. await UpdateUseCase.ExecuteAsync(
  245. _edit.Name,
  246. _edit.Ip,
  247. _edit.Port,
  248. _edit.Protocol,
  249. _edit.Url,
  250. _edit.RunsOn,
  251. _edit.Notes
  252. );
  253. await OnSave.InvokeAsync(Service.Name);
  254. }
  255. void Cancel()
  256. {
  257. _isEditing = false;
  258. }
  259. bool _selectParentOpen;
  260. string? SelectedParentName;
  261. async Task HandleParentSelected(string? name)
  262. {
  263. SelectedParentName = name;
  264. await UpdateUseCase.ExecuteAsync(
  265. Service.Name,
  266. Service.Network?.Ip,
  267. Service.Network?.Port,
  268. Service.Network?.Protocol,
  269. Service.Network?.Url,
  270. name,
  271. Service.Notes);
  272. Service = await GetByNameUseCase.ExecuteAsync(Service.Name);
  273. _edit = ServiceEditModel.From(Service);
  274. }
  275. }
  276. @code {
  277. private bool _confirmDeleteOpen;
  278. [Parameter] public EventCallback<string> OnDeleted { get; set; }
  279. void ConfirmDelete()
  280. {
  281. _confirmDeleteOpen = true;
  282. }
  283. async Task DeleteServer()
  284. {
  285. _confirmDeleteOpen = false;
  286. await DeleteUseCase.ExecuteAsync(Service.Name);
  287. if (OnDeleted.HasDelegate)
  288. await OnDeleted.InvokeAsync(Service.Name);
  289. }
  290. }
  291. @code
  292. {
  293. bool _renameOpen;
  294. void OpenRename()
  295. {
  296. _renameOpen = true;
  297. }
  298. async Task HandleRenameSubmit(string newName)
  299. {
  300. await RenameUseCase.ExecuteAsync(Service.Name, newName);
  301. Nav.NavigateTo($"resources/services/{newName}");
  302. }
  303. }