diff --git a/ui/src/app/components/miner-switcher/miner-switcher.component.ts b/ui/src/app/components/miner-switcher/miner-switcher.component.ts index aef1783..c928fbd 100644 --- a/ui/src/app/components/miner-switcher/miner-switcher.component.ts +++ b/ui/src/app/components/miner-switcher/miner-switcher.component.ts @@ -9,6 +9,11 @@ interface ContextMenuState { minerName: string; } +// Spinner SVG for loading states +const SPINNER_SVG = ` + +`; + @Component({ selector: 'app-miner-switcher', standalone: true, @@ -64,12 +69,20 @@ interface ContextMenuState {
@@ -152,11 +175,21 @@ interface ContextMenuState { Edit Configuration
-
@@ -490,6 +523,38 @@ interface ContextMenuState { .context-menu-item.danger svg { color: var(--color-danger-500); } + + /* Spinner animation */ + .spinner { + animation: spin 1s linear infinite; + } + + @keyframes spin { + from { transform: rotate(0deg); } + to { transform: rotate(360deg); } + } + + /* Loading state styles */ + .action-btn.loading, + .dropdown-item.loading, + .context-menu-item.loading { + opacity: 0.7; + cursor: not-allowed; + pointer-events: none; + } + + .action-btn:disabled, + .dropdown-item:disabled, + .context-menu-item:disabled { + cursor: not-allowed; + pointer-events: none; + } + + .action-btn.loading .spinner, + .dropdown-item.loading .spinner, + .context-menu-item.loading .spinner { + color: var(--color-accent-400); + } `] }) export class MinerSwitcherComponent { @@ -505,6 +570,25 @@ export class MinerSwitcherComponent { dropdownOpen = signal(false); contextMenu = signal({ visible: false, x: 0, y: 0, minerName: '' }); + // Track loading states for actions (e.g., "stop-minerName", "start-profileId") + private loadingActions = signal>(new Set()); + + isLoading(actionKey: string): boolean { + return this.loadingActions().has(actionKey); + } + + private setLoading(actionKey: string, loading: boolean) { + this.loadingActions.update(set => { + const newSet = new Set(set); + if (loading) { + newSet.add(actionKey); + } else { + newSet.delete(actionKey); + } + return newSet; + }); + } + // Close context menu on Escape @HostListener('document:keydown.escape') onEscape() { @@ -544,12 +628,20 @@ export class MinerSwitcherComponent { stopMiner(event: Event, name: string) { event.stopPropagation(); + const actionKey = `stop-${name}`; + if (this.isLoading(actionKey)) return; + + this.setLoading(actionKey, true); this.minerService.stopMiner(name).subscribe({ next: () => { // If this was the selected miner, switch to all view if (this.selectedMinerName() === name) { this.minerService.selectAllMiners(); } + this.setLoading(actionKey, false); + }, + error: () => { + this.setLoading(actionKey, false); } }); } @@ -565,8 +657,19 @@ export class MinerSwitcherComponent { } startProfile(profileId: string, profileName: string) { - this.minerService.startMiner(profileId).subscribe(); - this.closeDropdown(); + const actionKey = `start-${profileId}`; + if (this.isLoading(actionKey)) return; + + this.setLoading(actionKey, true); + this.minerService.startMiner(profileId).subscribe({ + next: () => { + this.setLoading(actionKey, false); + this.closeDropdown(); + }, + error: () => { + this.setLoading(actionKey, false); + } + }); } getHashrate(miner: any): number { @@ -632,13 +735,22 @@ export class MinerSwitcherComponent { stopFromContext() { const minerName = this.contextMenu().minerName; + const actionKey = `stop-${minerName}`; + if (this.isLoading(actionKey)) return; + + this.setLoading(actionKey, true); this.minerService.stopMiner(minerName).subscribe({ next: () => { if (this.selectedMinerName() === minerName) { this.minerService.selectAllMiners(); } + this.setLoading(actionKey, false); + this.closeContextMenu(); + }, + error: () => { + this.setLoading(actionKey, false); + this.closeContextMenu(); } }); - this.closeContextMenu(); } }