Files

173 lines
7.3 KiB
Python
Raw Permalink Normal View History

"""Вкладка Cron: редактор crontab."""
import flet as ft
from app.core import cron_svc as cron
from app.core.ssh import SSHClient
from app.db.models import Server
from app.ui.components import confirm_dialog, snack
class CronTab(ft.Column):
def __init__(self, page: ft.Page, srv: Server):
super().__init__(expand=True, spacing=8, scroll=ft.ScrollMode.AUTO)
self._page = page
self.srv = srv
self._lines: list[cron.CronLine] = []
self._user = "" # пусто = текущий sudo-пользовательский контекст
self.entries_col = ft.Column(spacing=4)
self.controls = [
ft.Container(height=8),
ft.Row([
ft.FilledButton("Добавить задачу", icon=ft.Icons.ADD,
on_click=self._add_dialog),
ft.OutlinedButton("Обновить", icon=ft.Icons.REFRESH,
on_click=lambda e: self._page.run_task(self._load)),
ft.OutlinedButton("Откатить к бэкапу", icon=ft.Icons.HISTORY,
on_click=self._restore_dialog),
]),
self.entries_col,
]
self._page.run_task(self._load)
async def _load(self):
try:
async with SSHClient(self.srv) as c:
content = await cron.get_crontab(c)
self._lines = cron.parse_crontab(content)
except Exception as e:
snack(self._page, f"cron: {e}", error=True)
self._render()
def _render(self):
self.entries_col.controls = []
for i, l in enumerate(self._lines):
if not l.is_entry:
if l.raw.strip() and not l.raw.strip().startswith(("#", "SHELL", "PATH", "MAILTO")):
self.entries_col.controls.append(
ft.Text(l.raw, size=11, color=ft.Colors.GREY_600,
font_family="Consolas"))
continue
self.entries_col.controls.append(
ft.Card(content=ft.Container(padding=8, content=ft.Row([
ft.Checkbox(value=l.enabled,
on_change=lambda e, idx=i: self._toggle(idx)),
ft.Column([
ft.Text(l.schedule, size=13, weight=ft.FontWeight.BOLD,
font_family="Consolas"),
ft.Text(l.command, size=12, font_family="Consolas"),
ft.Text(cron.humanize(l.schedule), size=10,
color=ft.Colors.GREY_500),
], spacing=1, expand=True),
ft.IconButton(ft.Icons.EDIT, icon_size=16,
on_click=lambda e, idx=i: self._edit_dialog(idx)),
ft.IconButton(ft.Icons.DELETE_OUTLINE, icon_size=16,
on_click=lambda e, idx=i: self._delete(idx)),
])))
)
if not any(l.is_entry for l in self._lines):
self.entries_col.controls.append(
ft.Text("crontab пуст", color=ft.Colors.GREY_500))
self._page.update()
async def _save(self):
content = cron.render(self._lines)
async with SSHClient(self.srv) as c:
await cron.set_crontab(c, content, self.srv.id)
snack(self._page, "crontab сохранён (бэкап записан)")
def _toggle(self, idx):
self._lines[idx].enabled = not self._lines[idx].enabled
self._page.run_task(self._save)
def _delete(self, idx):
def do():
del self._lines[idx]
self._page.run_task(self._save_and_reload)
confirm_dialog(self._page, "Удалить задачу?", self._lines[idx].command[:80], do)
async def _save_and_reload(self):
await self._save()
await self._load()
def _edit_dialog(self, idx):
l = self._lines[idx]
sched_tf = ft.TextField(label="Расписание (мин час день мес день_нед)",
value=l.schedule, width=400)
cmd_tf = ft.TextField(label="Команда", value=l.command, width=400)
hint = ft.Text(cron.humanize(l.schedule), size=11, color=ft.Colors.GREY_500)
def upd_hint(e):
hint.value = cron.humanize(sched_tf.value)
dlg.content.update()
sched_tf.on_change = upd_hint
def save(e):
if "неверное" in hint.value:
snack(self._page, "Неверное cron-выражение", error=True)
return
self._lines[idx] = cron.CronLine("", l.enabled, sched_tf.value.strip(),
cmd_tf.value.strip())
self._page.pop_dialog()
self._page.run_task(self._save_and_reload)
dlg = ft.AlertDialog(
modal=True, title=ft.Text("Редактировать задачу"),
content=ft.Column([sched_tf, cmd_tf, hint], tight=True),
actions=[ft.TextButton("Отмена", on_click=lambda e: self._close(dlg)),
ft.FilledButton("Сохранить", on_click=save)],
)
self._page.show_dialog(dlg)
def _add_dialog(self, e):
sched_tf = ft.TextField(label="Расписание", hint_text="0 3 * * *", width=400)
cmd_tf = ft.TextField(label="Команда", width=400)
hint = ft.Text("", size=11, color=ft.Colors.GREY_500)
def upd_hint(ev):
hint.value = cron.humanize(sched_tf.value)
dlg.content.update()
sched_tf.on_change = upd_hint
def save(e):
if not sched_tf.value or not cmd_tf.value:
return
self._lines.append(cron.CronLine("", True, sched_tf.value.strip(),
cmd_tf.value.strip()))
self._page.pop_dialog()
self._page.run_task(self._save_and_reload)
dlg = ft.AlertDialog(
modal=True, title=ft.Text("Новая cron-задача"),
content=ft.Column([sched_tf, cmd_tf, hint], tight=True),
actions=[ft.TextButton("Отмена", on_click=lambda e: self._close(dlg)),
ft.FilledButton("Добавить", on_click=save)],
)
self._page.show_dialog(dlg)
def _restore_dialog(self, e):
backups = cron.backups(self.srv.id, "current")
if not backups:
snack(self._page, "Бэкапов пока нет", error=True)
return
def restore(b):
self._page.pop_dialog()
async def _r():
async with SSHClient(self.srv) as c:
await cron.set_crontab(c, b.content, self.srv.id)
await self._load()
self._page.run_task(_r)
dlg = ft.AlertDialog(
modal=True, title=ft.Text("Откатить к бэкапу"),
content=ft.Column([
ft.TextButton(f"{b.created_at:%Y-%m-%d %H:%M} ({len(b.content)} символов)",
on_click=lambda e, b=b: restore(b))
for b in backups
], tight=True),
actions=[ft.TextButton("Отмена", on_click=lambda e: self._close(dlg))],
)
self._page.show_dialog(dlg)
def _close(self, dlg):
self._page.pop_dialog()