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 {
@@ -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();
}
}