| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540 |
- @using RackPeek.Domain.Git
- @using RackPeek.Domain.Git.UseCases
- @inject InitRepoUseCase InitRepo
- @inject CommitAllUseCase CommitAll
- @inject RestoreAllUseCase RestoreAll
- @inject PushUseCase PushUseCase
- @inject PullUseCase PullUseCase
- @inject AddRemoteUseCase AddRemoteUseCase
- @inject IGitRepository GitRepo
- @implements IDisposable
- @if (_status == GitRepoStatus.NotAvailable)
- {
- <div class="flex items-center gap-2 text-sm" data-testid="git-init-indicator">
- @if (_confirmInit)
- {
- <span class="text-zinc-400 text-xs">Enable git tracking?</span>
- <button class="px-2 py-0.5 text-xs rounded bg-emerald-600 hover:bg-emerald-500 text-white transition disabled:opacity-50"
- disabled="@_isInitializing"
- data-testid="git-init-confirm"
- @onclick="InitRepoAsync">
- @(_isInitializing ? "..." : "Yes")
- </button>
- <button class="px-2 py-0.5 text-xs rounded text-zinc-400 hover:text-white transition"
- data-testid="git-init-cancel"
- @onclick="() => _confirmInit = false">
- No
- </button>
- }
- else
- {
- <button class="px-2 py-1 text-xs rounded text-zinc-500 hover:text-emerald-400 hover:bg-zinc-800 transition"
- data-testid="git-init-button"
- @onclick="() => _confirmInit = true">
- Enable Git
- </button>
- }
- @if (_errorMessage is not null)
- {
- <span class="text-red-400 text-xs" data-testid="git-error">@_errorMessage</span>
- }
- </div>
- }
- else
- {
- <div class="relative flex items-center gap-2 text-sm" data-testid="git-status-indicator">
- @if (!string.IsNullOrEmpty(_branch) && _hasRemote)
- {
- <button class="text-zinc-400 text-xs hover:text-emerald-400 transition"
- data-testid="git-branch"
- @onclick="ToggleHistoryAsync">
- @_branch
- </button>
- }
- @if (_status == GitRepoStatus.Clean && _hasRemote)
- {
- <span class="inline-block w-2 h-2 rounded-full bg-emerald-400"
- data-testid="git-status-dot-clean"
- title="All changes committed"></span>
- <span class="text-zinc-500 text-xs" data-testid="git-status-text">
- Saved
- </span>
- }
- else if (_status == GitRepoStatus.Dirty && _hasRemote)
- {
- <span class="inline-block w-2 h-2 rounded-full bg-amber-400 animate-pulse"
- data-testid="git-status-dot-dirty"
- title="Uncommitted changes"></span>
- <div class="relative flex" data-testid="git-save-group">
- <button class="px-2 py-1 text-xs rounded-l bg-emerald-600 hover:bg-emerald-500 text-white transition disabled:opacity-50"
- disabled="@(_isBusy || !_hasRemote)"
- data-testid="git-save-button"
- @onclick="CommitAsync">
- @(_isCommitting ? "Saving..." : "Save")
- </button>
- <button class="px-1.5 py-1 text-xs rounded-r bg-emerald-700 hover:bg-emerald-600 text-white transition border-l border-emerald-800 disabled:opacity-50"
- disabled="@(_isBusy || !_hasRemote)"
- data-testid="git-save-dropdown"
- @onclick="ToggleDropdown">
- ▾
- </button>
- @if (_showDropdown)
- {
- <div class="absolute top-full right-0 mt-1 bg-zinc-800 border border-zinc-700 rounded shadow-lg z-50 min-w-[120px]"
- data-testid="git-dropdown-menu">
- <button class="w-full text-left px-3 py-2 text-xs text-zinc-300 hover:bg-zinc-700 hover:text-white transition"
- data-testid="git-diff-button"
- @onclick="OpenDiffAsync">
- Diff
- </button>
- @if (!_confirmDiscard)
- {
- <button class="w-full text-left px-3 py-2 text-xs text-zinc-400 hover:bg-zinc-700 hover:text-red-400 transition"
- data-testid="git-discard-button"
- @onclick="() => _confirmDiscard = true">
- Discard
- </button>
- }
- else
- {
- <div class="px-3 py-2 flex items-center gap-2">
- <span class="text-red-400 text-xs">Sure?</span>
- <button class="px-2 py-0.5 text-xs rounded bg-red-600 hover:bg-red-500 text-white transition"
- data-testid="git-discard-confirm"
- @onclick="DiscardAsync">
- Yes
- </button>
- <button class="px-2 py-0.5 text-xs rounded text-zinc-400 hover:text-white transition"
- data-testid="git-discard-cancel"
- @onclick="() => _confirmDiscard = false">
- No
- </button>
- </div>
- }
- </div>
- }
- </div>
- }
- @if (!_hasRemote)
- {
- <div class="relative flex items-center gap-1" data-testid="git-remote-group">
- @if (_showAddRemote)
- {
- <input type="text"
- class="px-2 py-1 text-xs rounded bg-zinc-800 border border-zinc-700 text-zinc-200 placeholder-zinc-500 w-56 focus:outline-none focus:border-emerald-500"
- placeholder="https://github.com/user/repo.git"
- @bind="_remoteUrl"
- @bind:event="oninput"
- data-testid="git-remote-url" />
- <button class="px-2 py-0.5 text-xs rounded bg-emerald-600 hover:bg-emerald-500 text-white transition disabled:opacity-50"
- disabled="@(string.IsNullOrWhiteSpace(_remoteUrl))"
- data-testid="git-remote-save"
- @onclick="AddRemoteAsync">
- Add
- </button>
- <button class="px-2 py-0.5 text-xs rounded text-zinc-400 hover:text-white transition"
- data-testid="git-remote-cancel"
- @onclick="CancelAddRemote">
- ×
- </button>
- }
- else
- {
- <button class="px-2 py-1 text-xs rounded text-zinc-500 hover:text-emerald-400 hover:bg-zinc-800 transition"
- data-testid="git-add-remote-button"
- @onclick="() => _showAddRemote = true">
- Add Remote
- </button>
- }
- <span class="text-xs text-zinc-500">
- Connect a remote repository to enable saving and sync.
- </span>
- </div>
- }
- else
- {
- <div class="relative flex" data-testid="git-sync-group">
- <button class="px-2 py-1 text-xs rounded text-zinc-400 hover:text-white hover:bg-zinc-700 transition disabled:opacity-50"
- disabled="@_isSyncing"
- data-testid="git-sync-button"
- @onclick="ToggleSyncAsync">
- @if (_isFetching)
- {
- <span>Checking...</span>
- }
- else if (_syncStatus.Ahead > 0 || _syncStatus.Behind > 0)
- {
- <span>
- Sync
- @if (_syncStatus.Ahead > 0) { <span class="text-emerald-400">↑@_syncStatus.Ahead</span> }
- @if (_syncStatus.Behind > 0) { <span class="text-blue-400">↓@_syncStatus.Behind</span> }
- </span>
- }
- else
- {
- <span>Sync</span>
- }
- </button>
- @if (_showSyncDropdown)
- {
- <div class="absolute top-full right-0 mt-1 bg-zinc-800 border border-zinc-700 rounded shadow-lg z-50 min-w-[140px]"
- data-testid="git-sync-dropdown">
- <button class="w-full text-left px-3 py-2 text-xs text-zinc-300 hover:bg-zinc-700 hover:text-white transition disabled:opacity-50"
- disabled="@_isSyncing"
- data-testid="git-push-button"
- @onclick="PushAsync">
- @(_isPushing ? "Pushing..." : "Push")
- </button>
- <button class="w-full text-left px-3 py-2 text-xs text-zinc-300 hover:bg-zinc-700 hover:text-white transition disabled:opacity-50"
- disabled="@(_isSyncing || _syncStatus.Error is not null)"
- data-testid="git-pull-button"
- @onclick="PullAsync">
- @(_isPulling ? "Pulling..." : "Pull")
- </button>
- </div>
- }
- </div>
- }
- @if (_errorMessage is not null)
- {
- <span class="text-red-400 text-xs">@_errorMessage</span>
- }
- </div>
- }
- @code {
- private GitRepoStatus _status = GitRepoStatus.NotAvailable;
- private string _branch = "";
- private bool _isCommitting;
- private bool _isRestoring;
- private bool _confirmDiscard;
- private bool _showDropdown;
- private bool _showAddRemote;
- private bool _showSyncDropdown;
- private bool _showHistory;
- private bool _hasRemote;
- private bool _isFetching;
- private bool _isPushing;
- private bool _isPulling;
- private bool _isInitializing;
- private bool _confirmInit;
- private string? _errorMessage;
- private string _remoteUrl = "";
- private string _diffContent = "";
- private string[] _changedFiles = [];
- private GitLogEntry[] _logEntries = [];
- private PeriodicTimer? _timer;
- private CancellationTokenSource? _cts;
- private GitSyncStatus _syncStatus = new(0,0,false);
- private bool _isBusy => _isCommitting || _isRestoring || _isSyncing;
- private bool _isSyncing => _isPushing || _isPulling || _isFetching;
- protected override async Task OnInitializedAsync()
- {
- _status = GitRepo.GetStatus();
- if (_status == GitRepoStatus.NotAvailable)
- return;
- _branch = GitRepo.GetCurrentBranch();
- _hasRemote = GitRepo.HasRemote();
- _cts = new CancellationTokenSource();
- _timer = new PeriodicTimer(TimeSpan.FromSeconds(15));
- _ = PollStatusAsync(_cts.Token);
- await Task.CompletedTask;
- }
- private async Task PollStatusAsync(CancellationToken ct)
- {
- try
- {
- while (_timer != null && await _timer.WaitForNextTickAsync(ct))
- {
- if (_isBusy)
- continue;
- var newStatus = GitRepo.GetStatus();
- if (newStatus != _status)
- {
- _status = newStatus;
- await InvokeAsync(StateHasChanged);
- }
- }
- }
- catch (OperationCanceledException) {}
- }
- private async Task InitRepoAsync()
- {
- _errorMessage = null;
- _isInitializing = true;
- try
- {
- var error = await InitRepo.ExecuteAsync();
- if (error != null)
- {
- _errorMessage = error;
- return;
- }
- _status = GitRepo.GetStatus();
- _branch = GitRepo.GetCurrentBranch();
- _hasRemote = GitRepo.HasRemote();
- }
- catch (Exception ex)
- {
- _errorMessage = $"Init error: {ex.Message}";
- }
- finally
- {
- _isInitializing = false;
- }
- }
- private void CancelAddRemote()
- {
- _showAddRemote = false;
- _remoteUrl = "";
- }
- private async Task AddRemoteAsync()
- {
- _errorMessage = null;
- try
- {
- var error = await AddRemoteUseCase.ExecuteAsync(_remoteUrl);
- if (error != null)
- {
- _errorMessage = error;
- return;
- }
- _hasRemote = true;
- _showAddRemote = false;
- _remoteUrl = "";
- _syncStatus = GitRepo.FetchAndGetSyncStatus();
- if (_syncStatus.Behind > 0)
- await PullAsync();
- }
- catch (Exception ex)
- {
- _errorMessage = $"Remote error: {ex.Message}";
- }
- }
- private async Task CommitAsync()
- {
- if (!_hasRemote)
- {
- _errorMessage = "Add a remote repository before saving.";
- return;
- }
- _errorMessage = null;
- _isCommitting = true;
- try
- {
- var error = await CommitAll.ExecuteAsync(
- $"rackpeek: save config {DateTime.UtcNow:yyyy-MM-dd HH:mm:ss} UTC");
- if (error != null)
- _errorMessage = error;
- _status = GitRepo.GetStatus();
- }
- catch (Exception ex)
- {
- _errorMessage = $"Commit error: {ex.Message}";
- }
- finally
- {
- _isCommitting = false;
- }
- }
- private void ToggleDropdown()
- {
- _showDropdown = !_showDropdown;
- _showSyncDropdown = false;
- }
- private void ToggleSyncAsync()
- {
- _showSyncDropdown = !_showSyncDropdown;
- _showDropdown = false;
- if (_showSyncDropdown)
- {
- _isFetching = true;
- try
- {
- _syncStatus = GitRepo.FetchAndGetSyncStatus();
- }
- finally
- {
- _isFetching = false;
- }
- }
- }
- private async Task PushAsync()
- {
- if (!_hasRemote)
- {
- _errorMessage = "Add a remote first.";
- return;
- }
- _errorMessage = null;
- _isPushing = true;
- try
- {
- var error = await PushUseCase.ExecuteAsync();
- if (error != null)
- _errorMessage = error;
- _syncStatus = GitRepo.FetchAndGetSyncStatus();
- }
- catch (Exception ex)
- {
- _errorMessage = $"Push error: {ex.Message}";
- }
- finally
- {
- _isPushing = false;
- }
- }
- private async Task PullAsync()
- {
- _errorMessage = null;
- _isPulling = true;
- try
- {
- var error = await PullUseCase.ExecuteAsync();
- if (error != null)
- _errorMessage = error;
- _syncStatus = GitRepo.FetchAndGetSyncStatus();
- _status = GitRepo.GetStatus();
- }
- catch (Exception ex)
- {
- _errorMessage = $"Pull error: {ex.Message}";
- }
- finally
- {
- _isPulling = false;
- }
- }
- public void Dispose()
- {
- _cts?.Cancel();
- _cts?.Dispose();
- _timer?.Dispose();
- }
-
- private void OpenDiffAsync()
- {
- _showDropdown = false;
- try
- {
- _changedFiles = GitRepo.GetChangedFiles();
- _diffContent = GitRepo.GetDiff();
- }
- catch (Exception ex)
- {
- _errorMessage = $"Diff error: {ex.Message}";
- }
- }
- private async Task DiscardAsync()
- {
- _errorMessage = null;
- _isRestoring = true;
- _confirmDiscard = false;
- _showDropdown = false;
- try
- {
- var error = await RestoreAll.ExecuteAsync();
- if (error != null)
- _errorMessage = error;
- _status = GitRepo.GetStatus();
- }
- catch (Exception ex)
- {
- _errorMessage = $"Discard error: {ex.Message}";
- }
- finally
- {
- _isRestoring = false;
- }
- }
- private void ToggleHistoryAsync()
- {
- if (_showHistory)
- {
- _showHistory = false;
- return;
- }
- try
- {
- _logEntries = GitRepo.GetLog(20);
- _showHistory = true;
- }
- catch (Exception ex)
- {
- _errorMessage = $"History error: {ex.Message}";
- }
- }
- }
|