From e5b9f38490ebc021dbca913d0fbe956c69d242e4 Mon Sep 17 00:00:00 2001 From: Julien Mercier-Rojas <2981531+jeckel@users.noreply.github.com> Date: Wed, 30 Oct 2024 11:38:29 +0100 Subject: [PATCH] Project configuration and upgrade Terminal widget (#6) * Use basic project configuration file * Refactor TabbedContents * Upgrade Terminal Component * Update design --- README.md | 16 ++-- projects/.gitignore | 3 + src/composer_utils.py | 33 ------- src/main.py | 31 ++----- src/models/project.py | 21 +++-- src/presentation/__init__.py | 18 ++-- src/presentation/component/__init__.py | 1 + src/presentation/component/terminal.py | 90 +++++++++++++++++++ src/presentation/component/terminal_modal.py | 67 ++++---------- src/presentation/composer/__init__.py | 5 +- ...{composer_pan.py => composer_container.py} | 86 +++++++++++------- .../composer/composer_packages_table.py | 8 ++ .../composer/composer_script_button.py | 8 +- src/presentation/docker/__init__.py | 18 ++-- src/presentation/summary/__init__.py | 53 ++++++++--- src/service_locator.py | 5 +- src/services/__init__.py | 3 +- src/services/base_service.py | 2 + src/services/composer_client.py | 42 +++++++++ src/services/{docker.py => docker_client.py} | 5 +- src/tcss/layout.tcss | 38 +------- 21 files changed, 334 insertions(+), 219 deletions(-) create mode 100644 projects/.gitignore delete mode 100644 src/composer_utils.py create mode 100644 src/presentation/component/terminal.py rename src/presentation/composer/{composer_pan.py => composer_container.py} (54%) create mode 100644 src/services/base_service.py create mode 100644 src/services/composer_client.py rename src/services/{docker.py => docker_client.py} (77%) diff --git a/README.md b/README.md index 2512791..2124672 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,11 @@ -- composer install -- composer update -- composer outdated -- composer actions -- +# DxCompanion, enhance your Developer eXperience + +DxCompanion is a Terminal UI application used to provide usefull information about you're project status and easy access +to tools ot manage and monitor your project under development environment. + +## Installation + +- install uv +- git clone + +## Your `.json` file diff --git a/projects/.gitignore b/projects/.gitignore new file mode 100644 index 0000000..6f14fe1 --- /dev/null +++ b/projects/.gitignore @@ -0,0 +1,3 @@ +* +!.gitignore +!.project-sample.json diff --git a/src/composer_utils.py b/src/composer_utils.py deleted file mode 100644 index b3041a0..0000000 --- a/src/composer_utils.py +++ /dev/null @@ -1,33 +0,0 @@ -import subprocess - -from models import Project - - -def composer_updatable(project: Project) -> dict[str, str]: - with subprocess.Popen( - ["composer", "update", "--dry-run"], - cwd=project.path, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - text=True, - ) as process: - stdout, stderr = process.communicate() - lines = stderr.strip().split("\n") - packages: dict[str, str] = {} - - # Processing lines for packages - for line in lines: - if line.startswith(" - Upgrading"): - # Extract package name and target version - parts = line.split("(") - package_name = line.strip().split(" ")[2] # Get the package name - version_info = ( - parts[1].strip().rstrip(")") - ) # Get the version info (v2.2.9 => v2.3.0) - target_version = version_info.split("=>")[ - -1 - ].strip() # Get the target version - - # Append to the packages list as a dictionary - packages[package_name] = target_version - return packages diff --git a/src/main.py b/src/main.py index 7a62480..2c4c9c1 100644 --- a/src/main.py +++ b/src/main.py @@ -1,8 +1,6 @@ import typer -from pydantic_core._pydantic_core import ValidationError -from rich import print +from dependency_injector import providers -from composer_utils import composer_updatable from service_locator import Container from models import Project from presentation import MainApp @@ -12,32 +10,13 @@ @app.command() def tui(project_path: str) -> None: - try: - project = Project(path=project_path) - print(f"Composer present: {project.composer}") - except ValidationError as e: - print("Validation error:", e) - exit(1) - - print(f"Launch tui for {project.name} project") - app = MainApp(project) - app.run() - - -@app.command() -def debug(project_path: str) -> None: - try: - project = Project(path=project_path) - print(f"Composer present: {project.composer}") - except ValidationError as e: - print("Validation error:", e) - exit(1) - - print(composer_updatable(project)) + project = Project.from_json(json_path=project_path) + Container() + tui_app = MainApp(project) + tui_app.run() def main() -> None: - Container() app() diff --git a/src/models/project.py b/src/models/project.py index ca4cdc1..e976814 100644 --- a/src/models/project.py +++ b/src/models/project.py @@ -1,3 +1,4 @@ +import json import os from functools import cached_property from typing import Optional @@ -9,13 +10,21 @@ class Project(BaseModel): path: str + project_name: Optional[str] = None composer: Optional[bool] = Field(default=False) composer_cmd: list[str] = ["composer"] docker_composer_cmd: list[str] = ["docker", "compose"] + actions: dict[str, str] = {} + + @classmethod + def from_json(cls, json_path: str): + with open(json_path, "r") as file: + data = json.load(file) + return cls(**data) @cached_property def name(self) -> str: - return os.path.basename(self.path) + return self.project_name or os.path.basename(self.path) @field_validator("path", mode="before") def check_directory_exists(cls, v) -> str: @@ -31,8 +40,8 @@ def check_composer_file(self): self.composer = True return self - @cached_property - def composer_json(self) -> Optional[Composer]: - if not self.composer: - return None - return Composer.from_json(self.path) + # @cached_property + # def composer_json(self) -> Optional[Composer]: + # if not self.composer: + # return None + # return Composer.from_json(self.path) diff --git a/src/presentation/__init__.py b/src/presentation/__init__.py index e8a9c9f..eea9795 100644 --- a/src/presentation/__init__.py +++ b/src/presentation/__init__.py @@ -1,11 +1,11 @@ from textual.app import App, ComposeResult -from textual.widgets import Footer, Header, TabbedContent +from textual.widgets import Footer, Header, TabbedContent, TabPane from models import Project -from .composer import ComposerPan -from .docker import DockerPan -from .summary import ProjectSummaryPan +from .composer import ComposerContainer +from .docker import DockerContainer +from .summary import ProjectSummaryContainer class MainApp(App): @@ -21,11 +21,15 @@ class MainApp(App): def __init__(self, project: Project): self._project = project super().__init__() + self.title = f"DX Companion - {project.name}" def compose(self) -> ComposeResult: yield Header() with TabbedContent(initial="summary-pan"): - yield ProjectSummaryPan(project=self._project) - yield ComposerPan(project=self._project) - yield DockerPan(project=self._project) + with TabPane(title="Summary", id="summary-pan"): + yield ProjectSummaryContainer(project=self._project) + with TabPane(title="Composer", id="composer-pan"): + yield ComposerContainer(project=self._project) + with TabPane(title="Docker", id="docker-pan"): + yield DockerContainer(project=self._project) yield Footer() diff --git a/src/presentation/component/__init__.py b/src/presentation/component/__init__.py index 9141791..d648902 100644 --- a/src/presentation/component/__init__.py +++ b/src/presentation/component/__init__.py @@ -1 +1,2 @@ from .terminal_modal import TerminalModal +from .terminal import Terminal diff --git a/src/presentation/component/terminal.py b/src/presentation/component/terminal.py new file mode 100644 index 0000000..de6f8bc --- /dev/null +++ b/src/presentation/component/terminal.py @@ -0,0 +1,90 @@ +import subprocess +from time import sleep + +from textual.widgets import RichLog +from textual import on +from textual.message import Message +from textual.worker import Worker, WorkerState + + +class Terminal(RichLog): + DEFAULT_CSS = """ + Terminal { + padding: 1 1; + } + """ + command: list[str] = [] + current_worker: Worker | None = None + + def __init__(self, **kwargs): + super().__init__(highlight=True, markup=True, **kwargs) + + def execute(self, command: list[str], path: str) -> None: + self.command = command + self.current_worker = self.run_worker( + self._execute(command, path), exclusive=True, thread=True + ) + + def is_running(self) -> bool: + return self.current_worker is not None and self.current_worker.is_running + + async def _execute(self, command: list[str], path: str) -> None: + self.command = command + self.clear() + self.write(f"Path: [bold blue]{path}[/bold blue]") + self.write(f"Command: [bold blue]{" ".join(command)}[/bold blue]") + self.write( + "----------------------------------------------------------------", + shrink=True, + ) + self.log(f"Running: {command} in {path}") + with subprocess.Popen( + command, + cwd=path, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + text=True, + ) as process: + assert process.stdout is not None + for line in iter(process.stdout.readline, ""): + self.write(line.strip(), shrink=True) + sleep(0.01) + process.wait() + + self.write( + "----------------------------------------------------------------", + shrink=True, + ) + self.write(f"Return code [bold]{process.returncode}[/bold]") + if process.returncode == 0: + self.write("[bold green]Completed![/bold green]") + else: + self.write("[bold red]Completed with errors![/bold red]") + + @on(Worker.StateChanged) + async def worker_state_changed(self, event: Worker.StateChanged) -> None: + if event.state == WorkerState.RUNNING: + self.post_message(self.TerminalStarted(self.command)) + if event.state == WorkerState.SUCCESS: + self.post_message(self.TerminalCompleted(self.command)) + if event.state == WorkerState.CANCELLED or event.state == WorkerState.ERROR: + self.post_message(self.TerminalCompleted(self.command, False)) + + class TerminalStarted(Message): + """ + Message sent when terminal execution starts + """ + + def __init__(self, command: list[str]) -> None: + self.command = command + super().__init__() + + class TerminalCompleted(Message): + """ + Message sent when terminal execution completes + """ + + def __init__(self, command: list[str], success: bool = True) -> None: + self.command = command + self.success = success + super().__init__() diff --git a/src/presentation/component/terminal_modal.py b/src/presentation/component/terminal_modal.py index 1c6c635..6ea40a6 100644 --- a/src/presentation/component/terminal_modal.py +++ b/src/presentation/component/terminal_modal.py @@ -1,10 +1,10 @@ -import subprocess - -from textual import on, work +from textual import on from textual.app import ComposeResult from textual.containers import Container, Horizontal from textual.screen import ModalScreen -from textual.widgets import Button, RichLog, Static +from textual.widgets import Button, Static + +from .terminal import Terminal class TerminalModal(ModalScreen[bool]): @@ -17,9 +17,6 @@ class TerminalModal(ModalScreen[bool]): Horizontal > Label { padding: 0 1; } - RichLog { - padding: 1 1; - } } .modal_title { @@ -42,9 +39,8 @@ class TerminalModal(ModalScreen[bool]): def __init__( self, - command: str | list[str], + command: list[str], path: str, - use_stderr: bool = False, allow_rerun: bool = False, **kwargs, ): @@ -52,12 +48,9 @@ def __init__( self.command = command self.path = path self.modal_title = f"Running: {" ".join(self.command)}" - self.use_stderr = use_stderr self.allow_rerun = allow_rerun - self.terminal = RichLog( + self.terminal = Terminal( id="terminal_command", - highlight=True, - markup=True, classes="modal_container", ) self._result = False @@ -67,46 +60,13 @@ def compose(self) -> ComposeResult: with Horizontal(classes="modal_title"): yield Static(self.modal_title) yield self.terminal - with Horizontal(classes="button_container"): + with Horizontal(classes="button_container", id="modal_button_container"): yield Button("Close", id="modal_close") if self.allow_rerun: yield Button.success(" Rerun", id="modal_rerun") def on_mount(self) -> None: - self._start() - - @work(exclusive=True, thread=True) - async def _start(self) -> None: - self.terminal.write(f"Path: [bold blue]{self.path}[/bold blue]") - self.terminal.write(f"Command: [bold blue]{" ".join(self.command)}[/bold blue]") - self.terminal.write( - "----------------------------------------------------------------", - shrink=True, - ) - self.log(f"Running: {self.command} in {self.path}") - with subprocess.Popen( - self.command, - cwd=self.path, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - text=True, - ) as process: - stdout, stderr = process.communicate() - if stderr and self.use_stderr: - self.terminal.write(f"[italic]{stderr}[/italic]") - self.terminal.write(stdout) - - self.terminal.write( - "----------------------------------------------------------------", - shrink=True, - ) - self.terminal.write(f"Return code [bold]{process.returncode}[/bold]") - if process.returncode == 0: - self.terminal.write("[bold green]Completed![/bold green]") - self._result = True - else: - self.terminal.write("[bold red]Completed with errors![/bold red]") - self._result = False + self.terminal.execute(command=self.command, path=self.path) @on(Button.Pressed, "#modal_close") def on_close(self, event: Button.Pressed) -> None: @@ -114,5 +74,12 @@ def on_close(self, event: Button.Pressed) -> None: @on(Button.Pressed, "#modal_rerun") def on_rerun(self, event: Button.Pressed) -> None: - self.terminal.clear() - self._start() + self.terminal.execute(command=self.command, path=self.path) + + @on(Terminal.TerminalCompleted) + def on_terminal_completed(self, event: Terminal.TerminalCompleted) -> None: + self.query_one("#modal_button_container").loading = False + + @on(Terminal.TerminalStarted) + def on_terminal_started(self, event: Terminal.TerminalStarted) -> None: + self.query_one("#modal_button_container").loading = True diff --git a/src/presentation/composer/__init__.py b/src/presentation/composer/__init__.py index 5bb564c..6b8c59c 100644 --- a/src/presentation/composer/__init__.py +++ b/src/presentation/composer/__init__.py @@ -1,4 +1 @@ -# from .composer_packages_table import ComposerPackagesTable -from .composer_pan import ComposerPan - -# from .composer_script_button import ComposerScriptButton +from .composer_container import ComposerContainer diff --git a/src/presentation/composer/composer_pan.py b/src/presentation/composer/composer_container.py similarity index 54% rename from src/presentation/composer/composer_pan.py rename to src/presentation/composer/composer_container.py index cb47cce..3648664 100644 --- a/src/presentation/composer/composer_pan.py +++ b/src/presentation/composer/composer_container.py @@ -1,23 +1,39 @@ from textual import on, work from textual.app import ComposeResult from textual.containers import Container, Horizontal -from textual.widgets import Button, TabPane +from textual.widgets import Button, TabPane, Label from textual.worker import Worker, WorkerState -from composer_utils import composer_updatable from models import Project from models.composer import Composer from presentation.component import TerminalModal +from service_locator import Container as ServiceContainer from .composer_packages_table import ComposerPackagesTable from .composer_script_button import ComposerScriptButton -class ComposerPan(TabPane): +class ComposerContainer(Container): + DEFAULT_CSS = """ + ComposerContainer { + Container { + layout: grid; + grid-size: 1 3; + + #composer-packages-table { + row-span: 2; + } + } + #composer-actions { + height: 3; + } + } + """ + def __init__(self, project: Project, **kwargs): self.project = project self.composer = Composer.from_json(project.path) - super().__init__(**kwargs, title="Composer", id="composer-pan") + super().__init__(**kwargs) def compose(self) -> ComposeResult: with Container(): @@ -35,38 +51,47 @@ async def on_mount(self): @work(exclusive=True, thread=True) async def _load_composer(self) -> dict[str, str]: - # return {} - return composer_updatable(self.project) + return ServiceContainer.composer_client().updatable_packages(self.project) @on(Worker.StateChanged) async def refresh_listview(self, event: Worker.StateChanged) -> None: """Called when the worker state changes.""" - if event.state == WorkerState.SUCCESS: - packages_updatable = event.worker.result - package_table: ComposerPackagesTable = self.query_one( - "#composer-packages-table" - ) - package_table.set_requirements( - self.composer.required_packages, - self.composer.locked_packages, - packages_updatable, - ) - package_dev_table: ComposerPackagesTable = self.query_one( - "#composer-packages-dev-table" - ) - package_dev_table.set_requirements( - self.composer.required_packages_dev, - self.composer.locked_packages_dev, - packages_updatable, - ) + if event.state != WorkerState.SUCCESS: + return + packages_updatable = event.worker.result + composer = ServiceContainer.composer_client().composer_json(self.project) + package_table: ComposerPackagesTable = self.query_one( + "#composer-packages-table" + ) + package_table.set_requirements( + composer.required_packages, + composer.locked_packages, + packages_updatable, + ) + package_dev_table: ComposerPackagesTable = self.query_one( + "#composer-packages-dev-table" + ) + package_dev_table.set_requirements( + composer.required_packages_dev, + composer.locked_packages_dev, + packages_updatable, + ) + + scripts = self.query_one("#composer-actions") + await scripts.remove_children() + + await scripts.mount(ComposerScriptButton(script_name="install")) + await scripts.mount( + ComposerScriptButton(script_name="update", label="update all") + ) + await scripts.mount(Label(" ")) - scripts = self.query_one("#composer-actions") - for script in self.composer.manual_scripts: - # self.log(f"Bouton {script}") - new_button = ComposerScriptButton(script_name=script) - await scripts.mount(new_button) + for script in self.composer.manual_scripts: + # self.log(f"Bouton {script}") + new_button = ComposerScriptButton(script_name=script) + await scripts.mount(new_button) - self.loading = False + self.loading = False @on(ComposerScriptButton.Pressed) def on_pressed(self, event: Button.Pressed) -> None: @@ -75,7 +100,6 @@ def on_pressed(self, event: Button.Pressed) -> None: TerminalModal( command=["composer", "--no-ansi", event.button.script_name], path=self.project.path, - use_stderr=True, allow_rerun=True, ) ) diff --git a/src/presentation/composer/composer_packages_table.py b/src/presentation/composer/composer_packages_table.py index 7116646..23d93df 100644 --- a/src/presentation/composer/composer_packages_table.py +++ b/src/presentation/composer/composer_packages_table.py @@ -13,6 +13,14 @@ class ComposerPackagesTable(DataTable): - package update (newer version than the installed one, and still matching the requirements) """ + DEFAULT_CSS = """ + ComposerPackagesTable { + border-title-color: $accent; + border: $primary-background round; + content-align: center middle; + } + """ + def __init__(self, title: str, **kwargs): super().__init__(**kwargs) self.border_title = title diff --git a/src/presentation/composer/composer_script_button.py b/src/presentation/composer/composer_script_button.py index 9d232df..7a3e8d2 100644 --- a/src/presentation/composer/composer_script_button.py +++ b/src/presentation/composer/composer_script_button.py @@ -1,3 +1,5 @@ +from typing import Optional + from textual.widgets import Button @@ -6,6 +8,8 @@ class ComposerScriptButton(Button): Button to launch a composer script defined in the composer.json file """ - def __init__(self, script_name: str, **kwargs): + def __init__(self, script_name: str, label: Optional[str] = None, **kwargs): self.script_name = script_name - super().__init__(script_name, id=f"composer-button-{script_name}", **kwargs) + super().__init__( + label or script_name, id=f"composer-button-{script_name}", **kwargs + ) diff --git a/src/presentation/docker/__init__.py b/src/presentation/docker/__init__.py index fbe68f3..27b4bc1 100644 --- a/src/presentation/docker/__init__.py +++ b/src/presentation/docker/__init__.py @@ -1,6 +1,6 @@ from textual.app import ComposeResult -from textual.containers import Horizontal -from textual.widgets import TabPane, Button, Select +from textual.containers import Horizontal, Container +from textual.widgets import TabPane, Button, Select, Static, Label from models import Project from textual import on @@ -9,17 +9,19 @@ from presentation.docker.container_select import ContainerSelect -class DockerPan(TabPane): +class DockerContainer(Container): def __init__(self, project: Project, **kwargs): self.project = project - super().__init__(**kwargs, title="Docker", id="docker-pan") + super().__init__(**kwargs) self.docker_logs = ContainerLogWidget() def compose(self) -> ComposeResult: - with Horizontal(id="docker_container_select_container"): - yield ContainerSelect() - yield Button.success(" Refresh", id="docker_refresh") - yield self.docker_logs + with Container(): + with Horizontal(id="docker_container_select_container"): + yield Label("Container:") + yield ContainerSelect() + yield Button.success(" Refresh", id="docker_refresh") + yield self.docker_logs @on(Select.Changed) def select_changed(self, event: Select.Changed) -> None: diff --git a/src/presentation/summary/__init__.py b/src/presentation/summary/__init__.py index 341d202..66282a2 100644 --- a/src/presentation/summary/__init__.py +++ b/src/presentation/summary/__init__.py @@ -1,17 +1,50 @@ -from rich.text import Text -from textual.app import ComposeResult -from textual.containers import Container -from textual.widgets import Label, TabPane +from textual.containers import Container, Horizontal +from textual.widgets import Markdown, Button +from textual import on from models import Project +from presentation.component import TerminalModal -class ProjectSummaryPan(TabPane): +# service: Service = Provide[Container.service] +class ProjectSummaryContainer(Container): + BORDER_TITLE = "Project's summary" + DEFAULT_CSS = """ + ProjectSummaryContainer { + border-title-color: $accent; + border: $primary-background round; + content-align: center middle; + Markdown { + height: auto; + } + + #summary-actions { + height: 3; + } + } + """ + def __init__(self, project: Project, **kwargs): self.project = project - super().__init__(**kwargs, title="Summary", id="summary-pan") + super().__init__(**kwargs) + + def compose(self): + yield Markdown( + f""" +# Project : {self.project.project_name} +""" + ) + if len(self.project.actions) > 0: + with Horizontal(id="summary-actions"): + for label in self.project.actions.keys(): + yield Button(label, name=label) - def compose(self) -> ComposeResult: - with Container(id="project_summary"): - yield Label(Text(str("Project :"), style="italic #03AC13", justify="right")) - yield Label(Text(str(self.project.name), style="italic")) + @on(Button.Pressed) + def on_pressed(self, event: Button.Pressed) -> None: + self.app.push_screen( + TerminalModal( + command=self.project.actions[event.button.name].split(" "), + path=self.project.path, + allow_rerun=True, + ) + ) diff --git a/src/service_locator.py b/src/service_locator.py index 5da0e24..b01ee89 100644 --- a/src/service_locator.py +++ b/src/service_locator.py @@ -1,11 +1,14 @@ from dependency_injector import containers, providers -from services import DockerClient +from models import Project +from services import DockerClient, ComposerClient class Container(containers.DeclarativeContainer): config = providers.Configuration() docker_client = providers.Singleton(DockerClient) + composer_client = providers.Singleton(ComposerClient) + # project = providers.Factory(Project) # api_client = providers.Singleton( # ApiClient, diff --git a/src/services/__init__.py b/src/services/__init__.py index c706882..e934f8d 100644 --- a/src/services/__init__.py +++ b/src/services/__init__.py @@ -1 +1,2 @@ -from .docker import DockerClient +from .docker_client import DockerClient +from .composer_client import ComposerClient diff --git a/src/services/base_service.py b/src/services/base_service.py new file mode 100644 index 0000000..dd96f48 --- /dev/null +++ b/src/services/base_service.py @@ -0,0 +1,2 @@ +class BaseService: + pass diff --git a/src/services/composer_client.py b/src/services/composer_client.py new file mode 100644 index 0000000..b9607b5 --- /dev/null +++ b/src/services/composer_client.py @@ -0,0 +1,42 @@ +import subprocess + +from models import Project +from models.composer import Composer +from .base_service import BaseService + + +class ComposerClient(BaseService): + @staticmethod + def updatable_packages(project: Project) -> dict[str, str]: + with subprocess.Popen( + ["composer", "update", "--dry-run"], + cwd=project.path, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True, + ) as process: + stdout, stderr = process.communicate() + lines = stderr.strip().split("\n") + packages: dict[str, str] = {} + + # Processing lines for packages + for line in lines: + if line.startswith(" - Upgrading"): + # Extract package name and target version + parts = line.split("(") + package_name = line.strip().split(" ")[2] # Get the package name + version_info = ( + parts[1].strip().rstrip(")") + ) # Get the version info (v2.2.9 => v2.3.0) + target_version = version_info.split("=>")[ + -1 + ].strip() # Get the target version + + # Append to the packages list as a dictionary + packages[package_name] = target_version + return packages + + def composer_json(self, project: Project) -> None | Composer: + if not project.composer: + return None + return Composer.from_json(project.path) diff --git a/src/services/docker.py b/src/services/docker_client.py similarity index 77% rename from src/services/docker.py rename to src/services/docker_client.py index 6e4db63..64d753e 100644 --- a/src/services/docker.py +++ b/src/services/docker_client.py @@ -1,7 +1,9 @@ import docker +from .base_service import BaseService -class DockerClient: + +class DockerClient(BaseService): def __init__(self): self.client = docker.from_env() @@ -12,4 +14,5 @@ def get_running_containers(self) -> list: def get_container_logs(self, container_id): """Fetches logs from a specific container.""" container = self.client.containers.get(container_id) + # return container.logs(follow=True, tail=1000) return container.logs(stream=True, follow=True) diff --git a/src/tcss/layout.tcss b/src/tcss/layout.tcss index 8574ae0..04a345a 100644 --- a/src/tcss/layout.tcss +++ b/src/tcss/layout.tcss @@ -1,35 +1,3 @@ -#project_summary { - layout: grid; - grid-size: 2 2; - grid-gutter: 1; - border-title-color: $accent; - border: $primary-background round; - content-align: center middle; - Label { - width: 100%; - } -} - -ComposerPan { - Container { - layout: grid; - grid-size: 1 3; - - ComposerPackagesTable { - border-title-color: $accent; - border: $primary-background round; - content-align: center middle; - } - - #composer-packages-table { - row-span: 2; - } - } - #composer-actions { - height: 3; - } -} - ComposerScriptButton { width: auto; } @@ -39,13 +7,15 @@ TabPane { margin: 0 0; } -DockerPan { +DockerContainer { Horizontal { height: 3; padding: 0 1; + Label { + padding: 1 1; + } } ContainerLogWidget { - height: auto; border-title-color: $accent; border: $primary-background round; }