"""Вкладка Docker: контейнеры, образы, тома, сети, compose.""" import flet as ft from app.core import docker_svc as d from app.core.ssh import SSHClient from app.core.tasks import tasks from app.db.models import Server from app.ui.components import append_log, confirm_dialog, log_view, snack class DockerTab(ft.Column): def __init__(self, page: ft.Page, srv: Server): super().__init__(expand=True, spacing=8) self._page = page self.srv = srv self.inner_tabs = ft.Tabs( selected_index=0, length=5, expand=True, content=ft.Column( expand=True, controls=[ ft.TabBar(tabs=[ ft.Tab(label="Контейнеры"), ft.Tab(label="Образы"), ft.Tab(label="Тома"), ft.Tab(label="Сети"), ft.Tab(label="Система"), ]), ft.TabBarView( expand=True, controls=[ self._containers_view(), self._images_view(), self._volumes_view(), self._networks_view(), self._system_view(), ], ), ], ), ) self.log = log_view() self.log_container = ft.Container( height=200, border=ft.Border.all(1, ft.Colors.GREY_800), border_radius=8, padding=8, content=self.log, visible=False) self.controls = [ft.Container(height=8), self.inner_tabs, self.log_container] self._page.run_task(self._check) async def _check(self): try: async with SSHClient(self.srv) as c: if not await d.available(c): snack(self._page, "Docker не установлен на сервере", error=True) return await self._load_containers() await self._load_images() await self._load_volumes() await self._load_networks() await self._load_system() except Exception as e: snack(self._page, f"Docker: {e}", error=True) # ---------- контейнеры ---------- def _containers_view(self): self.ct_table = ft.DataTable(columns=[ ft.DataColumn(ft.Text("Имя")), ft.DataColumn(ft.Text("Образ")), ft.DataColumn(ft.Text("Статус")), ft.DataColumn(ft.Text("Порты")), ft.DataColumn(ft.Text("Действия")), ], rows=[]) self.stats_row = ft.Row(spacing=16, wrap=True) return ft.Column([ ft.Row([ ft.OutlinedButton("Обновить", icon=ft.Icons.REFRESH, on_click=lambda e: self._page.run_task(self._load_containers)), ]), ft.Container(expand=True, content=ft.Column( [self.ct_table, ft.Divider(), ft.Text("Нагрузка:", weight=ft.FontWeight.BOLD), self.stats_row], scroll=ft.ScrollMode.AUTO)), ], expand=True) async def _load_containers(self): async with SSHClient(self.srv) as c: containers = await d.list_containers(c) try: stats = {s["Name"]: s for s in await d.container_stats(c)} except Exception: stats = {} self.ct_table.rows = [] for ct in containers: running = "Up" in ct.get("Status", "") cid = ct["ID"] name = ct["Names"] self.ct_table.rows.append(ft.DataRow(cells=[ ft.DataCell(ft.Text(name, weight=ft.FontWeight.BOLD)), ft.DataCell(ft.Text(ct["Image"], size=12)), ft.DataCell(ft.Text(ct["Status"], size=12, color=ft.Colors.GREEN_300 if running else ft.Colors.GREY_500)), ft.DataCell(ft.Text(ct.get("Ports", "") or "—", size=11)), ft.DataCell(ft.Row([ ft.IconButton(ft.Icons.PLAY_ARROW if not running else ft.Icons.STOP, icon_size=18, tooltip="start" if not running else "stop", on_click=lambda e, c=cid, a="start" if not running else "stop": self._ct_action(c, a)), ft.IconButton(ft.Icons.RESTART_ALT, icon_size=18, tooltip="restart", on_click=lambda e, c=cid: self._ct_action(c, "restart")), ft.IconButton(ft.Icons.ARTICLE, icon_size=18, tooltip="логи", on_click=lambda e, c=cid, n=name: self._show_logs(c, n)), ft.IconButton(ft.Icons.DELETE_OUTLINE, icon_size=18, tooltip="удалить", on_click=lambda e, c=cid, n=name: self._remove_ct(c, n)), ], spacing=0)), ])) self.stats_row.controls = [ ft.Chip(label=ft.Text(f"{n}: CPU {s.get('CPUPerc','?')} MEM {s.get('MemPerc','?')}")) for n, s in stats.items() ] or [ft.Text("нет запущенных", size=12, color=ft.Colors.GREY_500)] self._page.update() def _ct_action(self, cid, action): async def _do(): async with SSHClient(self.srv) as c: await d.container_action(c, cid, action) await self._load_containers() self._page.run_task(_do) def _show_logs(self, cid, name): self.log_container.visible = True append_log(self.log, f"=== логи {name} ===") self._page.update() async def _do(): async with SSHClient(self.srv) as c: out = await d.container_logs(c, cid) for line in out.splitlines()[-100:]: append_log(self.log, line) self._page.update() self._page.run_task(_do) def _remove_ct(self, cid, name): def do(): async def _d(): async with SSHClient(self.srv) as c: await d.container_action(c, cid, "rm") await self._load_containers() self._page.run_task(_d) confirm_dialog(self._page, f"Удалить контейнер {name}?", "Действие необратимо.", do) # ---------- образы ---------- def _images_view(self): self.img_table = ft.DataTable(columns=[ ft.DataColumn(ft.Text("Репозиторий")), ft.DataColumn(ft.Text("Тег")), ft.DataColumn(ft.Text("Размер")), ft.DataColumn(ft.Text("Создан")), ft.DataColumn(ft.Text("")), ], rows=[]) pull_tf = ft.TextField(hint_text="образ для pull (напр. nginx:alpine)", width=300, dense=True) return ft.Column([ ft.Row([ pull_tf, ft.FilledButton("Pull", icon=ft.Icons.DOWNLOAD, on_click=lambda e: self._pull(pull_tf.value)), ft.OutlinedButton("Обновить", icon=ft.Icons.REFRESH, on_click=lambda e: self._page.run_task(self._load_images)), ]), ft.Container(expand=True, content=ft.Column([self.img_table], scroll=ft.ScrollMode.AUTO)), ], expand=True) async def _load_images(self): async with SSHClient(self.srv) as c: images = await d.list_images(c) self.img_table.rows = [ ft.DataRow(cells=[ ft.DataCell(ft.Text(i["Repository"])), ft.DataCell(ft.Text(i["Tag"])), ft.DataCell(ft.Text(i["Size"])), ft.DataCell(ft.Text(i.get("CreatedSince", ""), size=11)), ft.DataCell(ft.IconButton( ft.Icons.DELETE_OUTLINE, icon_size=18, on_click=lambda e, ref=f"{i['Repository']}:{i['Tag']}": self._rmi(ref))), ]) for i in images ] self._page.update() def _pull(self, image): if not image: return self.log_container.visible = True async def job(log): async with SSHClient(self.srv) as c: stream = await d.pull(c, image) async for line in stream: log(line) tasks.start("docker", f"docker pull {image}", self.srv.id, job, on_log=lambda l: (append_log(self.log, l), self._page.update()), on_done=lambda r: self._page.run_task(self._load_images)) def _rmi(self, ref): def do(): async def _d(): async with SSHClient(self.srv) as c: await d.remove_image(c, ref) await self._load_images() self._page.run_task(_d) confirm_dialog(self._page, f"Удалить образ {ref}?", "", do) # ---------- тома ---------- def _volumes_view(self): self.vol_table = ft.DataTable(columns=[ ft.DataColumn(ft.Text("Имя")), ft.DataColumn(ft.Text("Драйвер")), ft.DataColumn(ft.Text("")), ], rows=[]) return ft.Column([ ft.OutlinedButton("Обновить", icon=ft.Icons.REFRESH, on_click=lambda e: self._page.run_task(self._load_volumes)), ft.Container(expand=True, content=ft.Column([self.vol_table], scroll=ft.ScrollMode.AUTO)), ], expand=True) async def _load_volumes(self): async with SSHClient(self.srv) as c: vols = await d.list_volumes(c) self.vol_table.rows = [ ft.DataRow(cells=[ ft.DataCell(ft.Text(v["Name"])), ft.DataCell(ft.Text(v.get("Driver", ""))), ft.DataCell(ft.IconButton(ft.Icons.DELETE_OUTLINE, icon_size=18, on_click=lambda e, n=v["Name"]: self._rm_vol(n))), ]) for v in vols ] self._page.update() def _rm_vol(self, name): def do(): async def _d(): async with SSHClient(self.srv) as c: await d.remove_volume(c, name) await self._load_volumes() self._page.run_task(_d) confirm_dialog(self._page, f"Удалить том {name}?", "Данные будут потеряны.", do) # ---------- сети ---------- def _networks_view(self): self.net_table = ft.DataTable(columns=[ ft.DataColumn(ft.Text("Имя")), ft.DataColumn(ft.Text("Драйвер")), ft.DataColumn(ft.Text("Scope")), ], rows=[]) return ft.Column([ ft.OutlinedButton("Обновить", icon=ft.Icons.REFRESH, on_click=lambda e: self._page.run_task(self._load_networks)), ft.Container(expand=True, content=ft.Column([self.net_table], scroll=ft.ScrollMode.AUTO)), ], expand=True) async def _load_networks(self): async with SSHClient(self.srv) as c: nets = await d.list_networks(c) self.net_table.rows = [ ft.DataRow(cells=[ ft.DataCell(ft.Text(n["Name"])), ft.DataCell(ft.Text(n.get("Driver", ""))), ft.DataCell(ft.Text(n.get("Scope", ""))), ]) for n in nets ] self._page.update() # ---------- система ---------- def _system_view(self): self.sys_text = ft.Text("…", selectable=True, size=12, font_family="Consolas") return ft.Column([ ft.Row([ ft.OutlinedButton("Обновить df", icon=ft.Icons.REFRESH, on_click=lambda e: self._page.run_task(self._load_system)), ft.FilledButton("System prune", icon=ft.Icons.CLEANING_SERVICES, style=ft.ButtonStyle(bgcolor=ft.Colors.ORANGE_800), on_click=self._prune), ]), ft.Container(padding=8, content=self.sys_text), ]) async def _load_system(self): async with SSHClient(self.srv) as c: ver = await d.server_version(c) df = await d.system_df(c) self.sys_text.value = f"Docker {ver}\n\n{df}" self._page.update() def _prune(self, e): def do(): self.log_container.visible = True async def job(log): async with SSHClient(self.srv) as c: log(await d.system_prune(c)) tasks.start("docker", "docker system prune", self.srv.id, job, on_log=lambda l: (append_log(self.log, l), self._page.update()), on_done=lambda r: self._page.run_task(self._load_system)) confirm_dialog(self._page, "Docker system prune?", "Будут удалены остановленные контейнеры, неиспользуемые сети, " "висячие образы и кэш сборки.", do)