ServiceCardComponent.razor 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404
  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">
  140. Runs On
  141. <button
  142. data-testid="service-runson-button"
  143. class="hover:text-emerald-400 pr-4"
  144. title="Add Runs On"
  145. @onclick="() => _selectParentOpen = true">
  146. @("+")
  147. </button>
  148. </div>
  149. @if (_isEditing)
  150. {
  151. @if (Service.RunsOn?.Count > 0)
  152. {
  153. @foreach(var parent in Service.RunsOn)
  154. {
  155. <button
  156. class="hover:text-emerald-400"
  157. title="Edit Runs On"
  158. @onclick="() => _selectParentOpen = true">
  159. @($"{parent}")
  160. </button>
  161. <button
  162. class="text-red-400 hover:text-red-300 pr-4"
  163. title="Remove"
  164. @onclick="() => HandleParentDeleted(parent)">
  165. @($"✕")
  166. </button>
  167. }
  168. }
  169. }
  170. else if (Service.RunsOn?.Count > 0)
  171. {
  172. @foreach(var parent in Service.RunsOn)
  173. {
  174. <NavLink href="@($"resources/systems/{Uri.EscapeDataString(parent)}")"
  175. data-testid="service-runson-link"
  176. class="text-emerald-400 pr-4">
  177. @parent
  178. </NavLink>
  179. }
  180. }
  181. </div>
  182. <ResourceTagEditor Resource="Service"
  183. TestIdPrefix="service" />
  184. <ResourceLabelEditor Resource="Service"
  185. TestIdPrefix="service" />
  186. <div class="md:col-span-2">
  187. <div class="text-zinc-400 mb-1">Notes</div>
  188. @if (_isEditing)
  189. {
  190. <MarkdownEditor
  191. @bind-Value="_edit.Notes"
  192. ShowActionButtons="false"
  193. TestIdPrefix="service-notes-editor" />
  194. }
  195. else
  196. {
  197. <MarkdownViewer
  198. Value="@Service.Notes"
  199. ShowEditButton="false"
  200. TestIdPrefix="service-notes-viewer" />
  201. }
  202. </div>
  203. </div>
  204. </div>
  205. <SystemSelectionModal
  206. IsOpen="@_selectParentOpen"
  207. IsOpenChanged="v => _selectParentOpen = v"
  208. Title="Select a parent"
  209. Value="@SelectedParentName"
  210. OnAccept="HandleParentSelected"/>
  211. <ConfirmModal
  212. IsOpen="_confirmDeleteOpen"
  213. IsOpenChanged="v => _confirmDeleteOpen = v"
  214. Title="Delete service"
  215. ConfirmText="Delete"
  216. ConfirmClass="bg-red-600 hover:bg-red-500"
  217. OnConfirm="DeleteServer" TestIdPrefix="service-delete">
  218. Are you sure you want to delete <strong>@Service.Name</strong>?
  219. </ConfirmModal>
  220. <StringValueModal
  221. IsOpen="_cloneOpen"
  222. IsOpenChanged="v => _cloneOpen = v"
  223. Title="Clone service"
  224. Description="Enter a name for the cloned service"
  225. Label="New service name"
  226. Value="@($"{Service.Name}-copy")"
  227. OnSubmit="HandleCloneSubmit"
  228. TestIdPrefix="service-clone"/>
  229. <StringValueModal
  230. IsOpen="_renameOpen"
  231. IsOpenChanged="v => _renameOpen = v"
  232. Title="Rename service"
  233. Description="Enter a new name for this service"
  234. Label="New service name"
  235. Value="@Service.Name"
  236. OnSubmit="HandleRenameSubmit"
  237. TestIdPrefix="service-rename"/>
  238. @code
  239. {
  240. bool _cloneOpen;
  241. void OpenClone()
  242. {
  243. _cloneOpen = true;
  244. }
  245. async Task HandleCloneSubmit(string newName)
  246. {
  247. await CloneUseCase.ExecuteAsync(Service.Name, newName);
  248. Nav.NavigateTo($"resources/services/{Uri.EscapeDataString(newName)}");
  249. }
  250. }
  251. @code {
  252. [Parameter] [EditorRequired] public Service Service { get; set; } = default!;
  253. [Parameter] public EventCallback<string> OnSave { get; set; }
  254. private bool _isEditing;
  255. private ServiceEditModel _edit = new();
  256. void BeginEdit()
  257. {
  258. _edit = ServiceEditModel.From(Service);
  259. _isEditing = true;
  260. }
  261. async Task Save()
  262. {
  263. _isEditing = false;
  264. await UpdateUseCase.ExecuteAsync(
  265. _edit.Name,
  266. _edit.Ip,
  267. _edit.Port,
  268. _edit.Protocol,
  269. _edit.Url,
  270. _edit.RunsOn,
  271. _edit.Notes
  272. );
  273. await OnSave.InvokeAsync(Service.Name);
  274. }
  275. void Cancel()
  276. {
  277. _isEditing = false;
  278. }
  279. bool _selectParentOpen;
  280. string? SelectedParentName;
  281. async Task HandleParentSelected(string? name)
  282. {
  283. SelectedParentName = name;
  284. await UpdateUseCase.ExecuteAsync(
  285. Service.Name,
  286. Service.Network?.Ip,
  287. Service.Network?.Port,
  288. Service.Network?.Protocol,
  289. Service.Network?.Url,
  290. new List<string>{name},
  291. Service.Notes);
  292. Service = await GetByNameUseCase.ExecuteAsync(Service.Name);
  293. _edit = ServiceEditModel.From(Service);
  294. }
  295. async Task HandleParentDeleted(string? name)
  296. {
  297. SelectedParentName = name;
  298. Service.RunsOn.Remove(SelectedParentName);
  299. await UpdateUseCase.ExecuteAsync(
  300. Service.Name,
  301. Service.Network?.Ip,
  302. Service.Network?.Port,
  303. Service.Network?.Protocol,
  304. Service.Network?.Url,
  305. Service.RunsOn,
  306. Service.Notes);
  307. Service = await GetByNameUseCase.ExecuteAsync(Service.Name);
  308. _edit = ServiceEditModel.From(Service);
  309. }
  310. }
  311. @code {
  312. private bool _confirmDeleteOpen;
  313. [Parameter] public EventCallback<string> OnDeleted { get; set; }
  314. void ConfirmDelete()
  315. {
  316. _confirmDeleteOpen = true;
  317. }
  318. async Task DeleteServer()
  319. {
  320. _confirmDeleteOpen = false;
  321. await DeleteUseCase.ExecuteAsync(Service.Name);
  322. if (OnDeleted.HasDelegate)
  323. await OnDeleted.InvokeAsync(Service.Name);
  324. }
  325. }
  326. @code
  327. {
  328. bool _renameOpen;
  329. void OpenRename()
  330. {
  331. _renameOpen = true;
  332. }
  333. async Task HandleRenameSubmit(string newName)
  334. {
  335. await RenameUseCase.ExecuteAsync(Service.Name, newName);
  336. Nav.NavigateTo($"resources/services/{Uri.EscapeDataString(newName)}");
  337. }
  338. }