GitStatusIndicator.razor 8.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353
  1. @using RackPeek.Domain.Git
  2. @using RackPeek.Domain.Git.UseCases
  3. @using RackPeek.Domain.Persistence
  4. @inject InitRepoUseCase InitRepo
  5. @inject CommitAllUseCase CommitAll
  6. @inject RestoreAllUseCase RestoreAll
  7. @inject PushUseCase PushUseCase
  8. @inject PullUseCase PullUseCase
  9. @inject AddRemoteUseCase AddRemoteUseCase
  10. @inject IGitRepository GitRepo
  11. @inject IResourceCollection Resources
  12. @implements IDisposable
  13. <div class="flex items-center gap-3 text-xs">
  14. @if (_status == GitRepoStatus.Clean)
  15. {
  16. <span class="flex items-center gap-1 text-zinc-400">
  17. <span class="w-2 h-2 rounded-full bg-emerald-400"></span>
  18. Saved
  19. </span>
  20. }
  21. else if (_status == GitRepoStatus.Dirty)
  22. {
  23. <span class="flex items-center gap-1 text-zinc-400">
  24. <span class="w-2 h-2 rounded-full bg-amber-400 animate-pulse"></span>
  25. </span>
  26. <span class="text-zinc-600">·</span>
  27. <button class="hover:text-emerald-400"
  28. disabled="@_isBusy"
  29. @onclick="CommitAsync">
  30. @(_isCommitting ? "Saving…" : "Save")
  31. </button>
  32. <span class="text-zinc-600">·</span>
  33. <button class="hover:text-red-400"
  34. disabled="@_isBusy"
  35. @onclick="DiscardAsync">
  36. @(_isRestoring ? "Discarding…" : "Discard")
  37. </button>
  38. }
  39. @if (!_hasRemote)
  40. {
  41. <span class="text-zinc-600">·</span>
  42. @if (_showAddRemote)
  43. {
  44. <input type="text"
  45. class="px-2 py-1 text-xs rounded bg-zinc-800 border border-zinc-700 text-zinc-200 w-56"
  46. placeholder="https://github.com/user/repo.git"
  47. @bind="_remoteUrl"
  48. @bind:event="oninput" />
  49. <button class="hover:text-emerald-400"
  50. disabled="@(string.IsNullOrWhiteSpace(_remoteUrl))"
  51. @onclick="AddRemoteAsync">
  52. Add
  53. </button>
  54. <button class="hover:text-zinc-400"
  55. @onclick="CancelAddRemote">
  56. Cancel
  57. </button>
  58. }
  59. else
  60. {
  61. <button class="text-zinc-400 hover:text-emerald-400"
  62. @onclick="() => _showAddRemote = true">
  63. Add Remote
  64. </button>
  65. }
  66. }
  67. else
  68. {
  69. <span class="text-zinc-600">·</span>
  70. <button class="text-zinc-400 hover:text-white"
  71. disabled="@(_isSyncing || _isFetching)"
  72. @onclick="ToggleSyncAsync">
  73. @if (_isFetching)
  74. {
  75. <span>Checking…</span>
  76. }
  77. else
  78. {
  79. <span>
  80. Sync
  81. @if (_syncStatus.Ahead > 0)
  82. {
  83. <span class="text-emerald-400"> ↑@_syncStatus.Ahead</span>
  84. }
  85. @if (_syncStatus.Behind > 0)
  86. {
  87. <span class="text-blue-400"> ↓@_syncStatus.Behind</span>
  88. }
  89. </span>
  90. }
  91. </button>
  92. @if (_syncStatus.Ahead > 0)
  93. {
  94. <span class="text-zinc-600">·</span>
  95. <button class="hover:text-emerald-400"
  96. disabled="@_isSyncing"
  97. @onclick="PushAsync">
  98. @(_isPushing ? "Pushing…" : "Push")
  99. </button>
  100. }
  101. @if (_syncStatus.Behind > 0)
  102. {
  103. <span class="text-zinc-600">·</span>
  104. <button class="hover:text-blue-400"
  105. disabled="@_isSyncing"
  106. @onclick="PullAsync">
  107. @(_isPulling ? "Pulling…" : "Pull")
  108. </button>
  109. }
  110. }
  111. @if (_errorMessage is not null)
  112. {
  113. <span class="text-red-400">@_errorMessage</span>
  114. }
  115. </div>
  116. @code {
  117. private GitRepoStatus _status = GitRepoStatus.NotAvailable;
  118. private bool _isCommitting;
  119. private bool _isRestoring;
  120. private bool _showAddRemote;
  121. private bool _hasRemote;
  122. private bool _isFetching;
  123. private bool _isPushing;
  124. private bool _isPulling;
  125. private string? _errorMessage;
  126. private string _remoteUrl = "";
  127. private PeriodicTimer? _timer;
  128. private CancellationTokenSource? _cts;
  129. private GitSyncStatus _syncStatus = new(0, 0, false);
  130. private bool _isBusy => _isCommitting || _isRestoring || _isSyncing;
  131. private bool _isSyncing => _isPushing || _isPulling || _isFetching;
  132. protected override async Task OnInitializedAsync()
  133. {
  134. _status = await Task.Run(() => GitRepo.GetStatus());
  135. if (_status == GitRepoStatus.NotAvailable)
  136. return;
  137. _hasRemote = await Task.Run(() => GitRepo.HasRemote());
  138. _cts = new CancellationTokenSource();
  139. _timer = new PeriodicTimer(TimeSpan.FromSeconds(15));
  140. _ = PollStatusAsync(_cts.Token);
  141. }
  142. private async Task PollStatusAsync(CancellationToken ct)
  143. {
  144. try
  145. {
  146. while (_timer != null && await _timer.WaitForNextTickAsync(ct))
  147. {
  148. if (_isBusy)
  149. continue;
  150. var newStatus = await Task.Run(() => GitRepo.GetStatus(), ct);
  151. if (newStatus != _status)
  152. {
  153. _status = newStatus;
  154. await InvokeAsync(StateHasChanged);
  155. }
  156. }
  157. }
  158. catch (OperationCanceledException) {}
  159. }
  160. private void CancelAddRemote()
  161. {
  162. _showAddRemote = false;
  163. _remoteUrl = "";
  164. }
  165. private async Task AddRemoteAsync()
  166. {
  167. _errorMessage = null;
  168. try
  169. {
  170. var error = await AddRemoteUseCase.ExecuteAsync(_remoteUrl);
  171. if (error != null)
  172. {
  173. _errorMessage = error;
  174. return;
  175. }
  176. _hasRemote = true;
  177. _showAddRemote = false;
  178. _remoteUrl = "";
  179. _syncStatus = await Task.Run(() => GitRepo.FetchAndGetSyncStatus());
  180. if (_syncStatus.Behind > 0)
  181. await PullAsync();
  182. }
  183. catch (Exception ex)
  184. {
  185. _errorMessage = $"Remote error: {ex.Message}";
  186. }
  187. }
  188. private async Task CommitAsync()
  189. {
  190. _errorMessage = null;
  191. _isCommitting = true;
  192. try
  193. {
  194. var error = await CommitAll.ExecuteAsync(
  195. $"rackpeek: save config {DateTime.UtcNow:yyyy-MM-dd HH:mm:ss} UTC");
  196. if (error != null)
  197. _errorMessage = error;
  198. _status = await Task.Run(() => GitRepo.GetStatus());
  199. await Resources.LoadAsync();
  200. }
  201. catch (Exception ex)
  202. {
  203. _errorMessage = $"Commit error: {ex.Message}";
  204. }
  205. finally
  206. {
  207. _isCommitting = false;
  208. }
  209. }
  210. private async Task ToggleSyncAsync()
  211. {
  212. _isFetching = true;
  213. try
  214. {
  215. _syncStatus = await Task.Run(() => GitRepo.FetchAndGetSyncStatus());
  216. }
  217. finally
  218. {
  219. _isFetching = false;
  220. }
  221. }
  222. private async Task PushAsync()
  223. {
  224. _errorMessage = null;
  225. _isPushing = true;
  226. try
  227. {
  228. var error = await PushUseCase.ExecuteAsync();
  229. if (error != null)
  230. _errorMessage = error;
  231. _syncStatus = await Task.Run(() => GitRepo.FetchAndGetSyncStatus());
  232. }
  233. catch (Exception ex)
  234. {
  235. _errorMessage = $"Push error: {ex.Message}";
  236. }
  237. finally
  238. {
  239. _isPushing = false;
  240. }
  241. }
  242. private async Task PullAsync()
  243. {
  244. _errorMessage = null;
  245. _isPulling = true;
  246. try
  247. {
  248. var error = await PullUseCase.ExecuteAsync();
  249. if (error != null)
  250. _errorMessage = error;
  251. _syncStatus = await Task.Run(() => GitRepo.FetchAndGetSyncStatus());
  252. _status = await Task.Run(() => GitRepo.GetStatus());
  253. await Resources.LoadAsync();
  254. }
  255. catch (Exception ex)
  256. {
  257. _errorMessage = $"Pull error: {ex.Message}";
  258. }
  259. finally
  260. {
  261. _isPulling = false;
  262. }
  263. }
  264. private async Task DiscardAsync()
  265. {
  266. _errorMessage = null;
  267. _isRestoring = true;
  268. try
  269. {
  270. var error = await RestoreAll.ExecuteAsync();
  271. if (error != null)
  272. _errorMessage = error;
  273. _status = await Task.Run(() => GitRepo.GetStatus());
  274. await Resources.LoadAsync();
  275. }
  276. catch (Exception ex)
  277. {
  278. _errorMessage = $"Discard error: {ex.Message}";
  279. }
  280. finally
  281. {
  282. _isRestoring = false;
  283. }
  284. }
  285. public void Dispose()
  286. {
  287. _cts?.Cancel();
  288. _cts?.Dispose();
  289. _timer?.Dispose();
  290. }
  291. }