Add loading spinner to operation log during running tasks
Shows an animated spinner next to the title while backup, restore, retention, remote test, and schedule operations are running. Spinner hides when the operation completes via finish(). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -193,10 +193,21 @@ SelectionList {
|
|||||||
border: thick $accent;
|
border: thick $accent;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#ol-header {
|
||||||
|
height: auto;
|
||||||
|
margin: 0 0 1 0;
|
||||||
|
}
|
||||||
|
|
||||||
#ol-title {
|
#ol-title {
|
||||||
text-style: bold;
|
text-style: bold;
|
||||||
color: #00cc00;
|
color: #00cc00;
|
||||||
margin: 0 0 1 0;
|
width: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
#ol-spinner {
|
||||||
|
width: auto;
|
||||||
|
min-width: 4;
|
||||||
|
height: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
#ol-log {
|
#ol-log {
|
||||||
|
|||||||
@@ -77,6 +77,7 @@ class BackupScreen(Screen):
|
|||||||
log_screen.write("\n[green]Backup completed successfully.[/green]")
|
log_screen.write("\n[green]Backup completed successfully.[/green]")
|
||||||
else:
|
else:
|
||||||
log_screen.write(f"\n[red]Backup failed (exit code {rc}).[/red]")
|
log_screen.write(f"\n[red]Backup failed (exit code {rc}).[/red]")
|
||||||
|
log_screen.finish()
|
||||||
|
|
||||||
@work
|
@work
|
||||||
async def _do_backup_all(self) -> None:
|
async def _do_backup_all(self) -> None:
|
||||||
@@ -87,6 +88,7 @@ class BackupScreen(Screen):
|
|||||||
log_screen.write("\n[green]All backups completed.[/green]")
|
log_screen.write("\n[green]All backups completed.[/green]")
|
||||||
else:
|
else:
|
||||||
log_screen.write(f"\n[red]Backup failed (exit code {rc}).[/red]")
|
log_screen.write(f"\n[red]Backup failed (exit code {rc}).[/red]")
|
||||||
|
log_screen.finish()
|
||||||
|
|
||||||
def action_go_back(self) -> None:
|
def action_go_back(self) -> None:
|
||||||
self.app.pop_screen()
|
self.app.pop_screen()
|
||||||
|
|||||||
@@ -94,6 +94,7 @@ class RemotesScreen(Screen):
|
|||||||
log_screen.write("\n[green]Connection test passed.[/green]")
|
log_screen.write("\n[green]Connection test passed.[/green]")
|
||||||
else:
|
else:
|
||||||
log_screen.write(f"\n[red]Connection test failed (exit code {rc}).[/red]")
|
log_screen.write(f"\n[red]Connection test failed (exit code {rc}).[/red]")
|
||||||
|
log_screen.finish()
|
||||||
|
|
||||||
def _delete_remote(self, name: str) -> None:
|
def _delete_remote(self, name: str) -> None:
|
||||||
conf = CONFIG_DIR / "remotes.d" / f"{name}.conf"
|
conf = CONFIG_DIR / "remotes.d" / f"{name}.conf"
|
||||||
|
|||||||
@@ -122,6 +122,7 @@ class RestoreScreen(Screen):
|
|||||||
log_screen.write("\n[green]Restore completed successfully.[/green]")
|
log_screen.write("\n[green]Restore completed successfully.[/green]")
|
||||||
else:
|
else:
|
||||||
log_screen.write(f"\n[red]Restore failed (exit code {rc}).[/red]")
|
log_screen.write(f"\n[red]Restore failed (exit code {rc}).[/red]")
|
||||||
|
log_screen.finish()
|
||||||
|
|
||||||
def action_go_back(self) -> None:
|
def action_go_back(self) -> None:
|
||||||
self.app.pop_screen()
|
self.app.pop_screen()
|
||||||
|
|||||||
@@ -75,6 +75,7 @@ class RetentionScreen(Screen):
|
|||||||
log_screen.write("\n[green]Cleanup completed.[/green]")
|
log_screen.write("\n[green]Cleanup completed.[/green]")
|
||||||
else:
|
else:
|
||||||
log_screen.write(f"\n[red]Cleanup failed (exit code {rc}).[/red]")
|
log_screen.write(f"\n[red]Cleanup failed (exit code {rc}).[/red]")
|
||||||
|
log_screen.finish()
|
||||||
|
|
||||||
@work
|
@work
|
||||||
async def _do_cleanup_all(self) -> None:
|
async def _do_cleanup_all(self) -> None:
|
||||||
@@ -85,6 +86,7 @@ class RetentionScreen(Screen):
|
|||||||
log_screen.write("\n[green]All cleanups completed.[/green]")
|
log_screen.write("\n[green]All cleanups completed.[/green]")
|
||||||
else:
|
else:
|
||||||
log_screen.write(f"\n[red]Cleanup failed (exit code {rc}).[/red]")
|
log_screen.write(f"\n[red]Cleanup failed (exit code {rc}).[/red]")
|
||||||
|
log_screen.finish()
|
||||||
|
|
||||||
def action_go_back(self) -> None:
|
def action_go_back(self) -> None:
|
||||||
self.app.pop_screen()
|
self.app.pop_screen()
|
||||||
|
|||||||
@@ -225,6 +225,7 @@ class ScheduleScreen(Screen):
|
|||||||
log_screen.write(stdout)
|
log_screen.write(stdout)
|
||||||
if stderr:
|
if stderr:
|
||||||
log_screen.write(stderr)
|
log_screen.write(stderr)
|
||||||
|
log_screen.finish()
|
||||||
|
|
||||||
@work
|
@work
|
||||||
async def _remove_schedules(self) -> None:
|
async def _remove_schedules(self) -> None:
|
||||||
@@ -235,6 +236,7 @@ class ScheduleScreen(Screen):
|
|||||||
log_screen.write(stdout)
|
log_screen.write(stdout)
|
||||||
if stderr:
|
if stderr:
|
||||||
log_screen.write(stderr)
|
log_screen.write(stderr)
|
||||||
|
log_screen.finish()
|
||||||
|
|
||||||
@work
|
@work
|
||||||
async def _show_crontab(self) -> None:
|
async def _show_crontab(self) -> None:
|
||||||
@@ -245,6 +247,7 @@ class ScheduleScreen(Screen):
|
|||||||
log_screen.write(stdout)
|
log_screen.write(stdout)
|
||||||
if stderr:
|
if stderr:
|
||||||
log_screen.write(stderr)
|
log_screen.write(stderr)
|
||||||
|
log_screen.finish()
|
||||||
|
|
||||||
def action_go_back(self) -> None:
|
def action_go_back(self) -> None:
|
||||||
self.app.pop_screen()
|
self.app.pop_screen()
|
||||||
|
|||||||
@@ -3,8 +3,8 @@ import asyncio
|
|||||||
from rich.text import Text
|
from rich.text import Text
|
||||||
from textual.app import ComposeResult
|
from textual.app import ComposeResult
|
||||||
from textual.screen import ModalScreen
|
from textual.screen import ModalScreen
|
||||||
from textual.widgets import RichLog, Button, Static
|
from textual.widgets import RichLog, Button, Static, LoadingIndicator
|
||||||
from textual.containers import Vertical
|
from textual.containers import Vertical, Horizontal
|
||||||
|
|
||||||
|
|
||||||
class OperationLog(ModalScreen[None]):
|
class OperationLog(ModalScreen[None]):
|
||||||
@@ -16,10 +16,13 @@ class OperationLog(ModalScreen[None]):
|
|||||||
self._title = title
|
self._title = title
|
||||||
self._mounted_event = asyncio.Event()
|
self._mounted_event = asyncio.Event()
|
||||||
self._buffer: list[str] = []
|
self._buffer: list[str] = []
|
||||||
|
self._running = True
|
||||||
|
|
||||||
def compose(self) -> ComposeResult:
|
def compose(self) -> ComposeResult:
|
||||||
with Vertical(id="op-log"):
|
with Vertical(id="op-log"):
|
||||||
yield Static(self._title, id="ol-title")
|
with Horizontal(id="ol-header"):
|
||||||
|
yield Static(self._title, id="ol-title")
|
||||||
|
yield LoadingIndicator(id="ol-spinner")
|
||||||
yield RichLog(id="ol-log", wrap=True, highlight=True, markup=True)
|
yield RichLog(id="ol-log", wrap=True, highlight=True, markup=True)
|
||||||
yield Button("Close", variant="primary", id="ol-close")
|
yield Button("Close", variant="primary", id="ol-close")
|
||||||
|
|
||||||
@@ -43,6 +46,13 @@ class OperationLog(ModalScreen[None]):
|
|||||||
else:
|
else:
|
||||||
log.write(text)
|
log.write(text)
|
||||||
|
|
||||||
|
def finish(self) -> None:
|
||||||
|
self._running = False
|
||||||
|
try:
|
||||||
|
self.query_one("#ol-spinner", LoadingIndicator).display = False
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
def write(self, text: str) -> None:
|
def write(self, text: str) -> None:
|
||||||
if not self._mounted_event.is_set():
|
if not self._mounted_event.is_set():
|
||||||
self._buffer.append(text)
|
self._buffer.append(text)
|
||||||
|
|||||||
Reference in New Issue
Block a user