From 75095a841f1a3f94a652d7973aeba06da7c5d3b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stephan=20L=C3=BCscher?= Date: Tue, 7 May 2024 20:12:02 +0000 Subject: [PATCH] wip - nicegui load config --- anvil/nicegui/_tests/main.py | 59 ----------------- anvil/nicegui/_tests/runner.py | 14 ---- anvil/nicegui/_tests/test.py | 88 ------------------------- anvil/nicegui/main.py | 0 anvil/nicegui/pages/ansible.py | 63 ++++++++++++++---- anvil/nicegui/utils.py | 113 +++++++++++++++++++++++++++++++++ forge-pod.yml | 5 ++ 7 files changed, 168 insertions(+), 174 deletions(-) delete mode 100644 anvil/nicegui/_tests/main.py delete mode 100644 anvil/nicegui/_tests/runner.py delete mode 100644 anvil/nicegui/_tests/test.py mode change 100755 => 100644 anvil/nicegui/main.py diff --git a/anvil/nicegui/_tests/main.py b/anvil/nicegui/_tests/main.py deleted file mode 100644 index 1b97148..0000000 --- a/anvil/nicegui/_tests/main.py +++ /dev/null @@ -1,59 +0,0 @@ -#!/usr/bin/env python3 -import asyncio -import os.path -import platform -import shlex -import sys - -from nicegui import ui - -async def run_command(command: str) -> None: - """Run a command in the background and display the output in the pre-created dialog.""" - dialog.open() - result.content = '' - command = command.replace('python3', sys.executable) # NOTE replace with machine-independent Python path (#1240) - process = await asyncio.create_subprocess_exec( - *shlex.split(command, posix='win' not in sys.platform.lower()), - stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.STDOUT, - cwd=os.path.dirname(os.path.abspath(__file__)) - ) - # NOTE we need to read the output in chunks, otherwise the process will block - output = '' - while True: - new = await process.stdout.read(4096) - if not new: - break - output += new.decode() - # NOTE the content of the markdown element is replaced every time we have new output - result.content = f'```\n{output}\n```' - -with ui.header().classes(replace='row items-center') as header: - ui.button(on_click=lambda: left_drawer.toggle(), icon='menu').props('flat color=white') - with ui.tabs() as tabs: - ui.tab('Home') - ui.tab('Ansible') - ui.tab('Registry') - -with ui.footer(value=False) as footer: - ui.label('Footer') - -with ui.left_drawer().classes('bg-blue-100') as left_drawer: - ui.label('Side menu') - -ui.button('Build Project', on_click=lambda: run_command('python3 runner.py')).props('no-caps') -with ui.dialog() as dialog, ui.card(): - result = ui.markdown() - - -with ui.page_sticky(position='bottom-right', x_offset=20, y_offset=20): - ui.button(on_click=footer.toggle, icon='contact_support').props('fab') - -with ui.tab_panels(tabs, value='A').classes('w-full'): - with ui.tab_panel('A'): - ui.label('Content of A') - with ui.tab_panel('B'): - ui.label('Content of B') - with ui.tab_panel('C'): - ui.label('Content of C') - -ui.run() \ No newline at end of file diff --git a/anvil/nicegui/_tests/runner.py b/anvil/nicegui/_tests/runner.py deleted file mode 100644 index b209774..0000000 --- a/anvil/nicegui/_tests/runner.py +++ /dev/null @@ -1,14 +0,0 @@ -import sys -import ansible_runner - -# run ansible/generic commands in interactive mode locally -out, err, rc = ansible_runner.run_command( - executable_cmd='ansible-playbook', - cmdline_args=['../playbooks/project_clone.yml', '-i', '../inventory.yml'], - input_fd=sys.stdin, - output_fd=sys.stdout, - error_fd=sys.stderr, -) -print("rc: {}".format(rc)) -print("out: {}".format(out)) -print("err: {}".format(err)) \ No newline at end of file diff --git a/anvil/nicegui/_tests/test.py b/anvil/nicegui/_tests/test.py deleted file mode 100644 index cb1effb..0000000 --- a/anvil/nicegui/_tests/test.py +++ /dev/null @@ -1,88 +0,0 @@ -# from nicegui import ui - -# @ui.page('/') -# def page_layout(): -# ui.label('CONTENT') -# [ui.label(f'Line {i}') for i in range(100)] -# with ui.header(elevated=True).style('background-color: #3874c8').classes('items-center justify-between'): -# ui.button(on_click=lambda: left_drawer.toggle(), icon='menu').props('flat color=white') -# ui.label('HEADER') -# with ui.left_drawer(fixed=False).style('background-color: #ebf1fa').props('bordered') as left_drawer: -# ui.label('LEFT DRAWER') -# with ui.footer().style('background-color: #3874c8'): -# ui.label('FOOTER') - -# # ui.link('show page with fancy layout', page_layout) - -# ui.run() - -# from nicegui import ui - -# with ui.header().classes(replace='row items-center') as header: -# ui.button(on_click=lambda: left_drawer.toggle(), icon='menu').props('flat color=white') -# with ui.tabs() as tabs: -# ui.tab('Home') -# ui.tab('Ansible') -# ui.tab('Registry') - -# with ui.footer(value=False) as footer: -# ui.label('Footer') - -# with ui.left_drawer().classes('bg-blue-100') as left_drawer: -# ui.label('Side menu') - -# with ui.page_sticky(position='bottom-right', x_offset=20, y_offset=20): -# ui.button(on_click=footer.toggle, icon='contact_support').props('fab') - -# with ui.tab_panels(tabs, value='A').classes('w-full'): -# with ui.tab_panel('A'): -# ui.label('Content of A') -# with ui.tab_panel('B'): -# ui.label('Content of B') -# with ui.tab_panel('C'): -# ui.label('Content of C') - -# ui.run() - -#!/usr/bin/env python3 -import asyncio -import os.path -import platform -import shlex -import sys - -from nicegui import ui - - -async def run_command(command: str) -> None: - """Run a command in the background and display the output in the pre-created dialog.""" - dialog.open() - result.content = '' - command = command.replace('python3', sys.executable) # NOTE replace with machine-independent Python path (#1240) - process = await asyncio.create_subprocess_exec( - *shlex.split(command, posix='win' not in sys.platform.lower()), - stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.STDOUT, - cwd=os.path.dirname(os.path.abspath(__file__)) - ) - # NOTE we need to read the output in chunks, otherwise the process will block - output = '' - while True: - new = await process.stdout.read(4096) - if not new: - break - output += new.decode() - # NOTE the content of the markdown element is replaced every time we have new output - result.content = f'```\n{output}\n```' - -with ui.dialog() as dialog, ui.card(): - result = ui.markdown() - -ui.button('python3 runner.py', on_click=lambda: run_command('python3 runner.py')).props('no-caps') -ui.button('python3 slow.py', on_click=lambda: run_command('python3 slow.py')).props('no-caps') -with ui.row().classes('items-center'): - ui.button('python3 hello.py ""', on_click=lambda: run_command(f'python3 hello.py "{message.value}"')) \ - .props('no-caps') - message = ui.input('message', value='NiceGUI') - -# NOTE: On Windows reload must be disabled to make asyncio.create_subprocess_exec work (see https://github.com/zauberzeug/nicegui/issues/486) -ui.run(reload=platform.system() != 'Windows') \ No newline at end of file diff --git a/anvil/nicegui/main.py b/anvil/nicegui/main.py old mode 100755 new mode 100644 diff --git a/anvil/nicegui/pages/ansible.py b/anvil/nicegui/pages/ansible.py index 9a542ae..4455c84 100644 --- a/anvil/nicegui/pages/ansible.py +++ b/anvil/nicegui/pages/ansible.py @@ -1,28 +1,65 @@ import ansible_runner import re from nicegui import ui -from utils import get_project_root +from utils import get_project_root, local_file_picker +from multiprocessing import Manager, Queue + +ANSIBLE_EXTRA_VARS = None # Ansible integration -def run_ansible_playbook(playbook_name: str, ngui_log: ui.log): +@ui.refreshable #https://www.reddit.com/r/nicegui/comments/1bphjk5/comment/kx7l5kj/?utm_source=share&utm_medium=web3x&utm_name=web3xcss&utm_term=1&utm_content=share_button +async def load_configuration_file() -> None: + result = await local_file_picker( + directory="/data", multiple=False, file_name_filter=".yml" + ) + file_path = result[0] + with open(file_path, "r") as file: + data = file.read() + ## Give feedback to user + ui.notify(f"You chose {file_path}") + ## Display content + ui.code(content=data, language="yaml") + ## Preserve configuration file path for ansible-playbook --extra-vars + ANSIBLE_EXTRA_VARS = f'"@{file_path}"' + +async def run_ansible_playbook(playbook_name: str, ngui_log: ui.log): project_root = str(get_project_root()) playbook_path = project_root + "/ansible/playbooks/" inventory_path = project_root + "/ansible/inventory.yml" - response, error, return_code = ansible_runner.interface.run_command( + extra_vars_file = str(ANSIBLE_EXTRA_VARS) + thread, runner = ansible_runner.interface.run_command_async( executable_cmd="ansible-playbook", - cmdline_args=[playbook_path + playbook_name, "-i", inventory_path], + cmdline_args=[ + playbook_path + playbook_name, + "-i", + inventory_path, + "--extra-vars", + extra_vars_file, + ] ) - # remove color characters from response until clear how to display them in a log - ansi_escape = re.compile(r"\x1B(?:[@-Z\\-_]|\[[0-9;#]+[mGK]?)") - ansible_log = format(ansi_escape.sub("", response)) + # clear log ngui_log.clear() - ngui_log.push(ansible_log) - + # regex to remove color characters from response until clear how to display them in a log + ansi_escape = re.compile(r"\x1B(?:[@-Z\\-_]|\[[0-9;#]+[mGK]?)") + # show log from asynchronous job + while runner.rc is None: + for event in runner.events: + ansible_log = format(ansi_escape.sub("", event['stdout'])) + ngui_log.push(ansible_log) # Page content def content() -> None: with ui.row().classes("w-full"): + with ui.row().classes("w-full"): + with ui.card().classes("h-full"): + ui.button(text= + "Load configuration file", + on_click=lambda: load_configuration_file.refresh(), + icon="folder", + ) + ## show configuration file with ui.code + load_configuration_file() with ui.row().classes("w-full"): # First Row with ui.card().classes("h-full"): @@ -30,13 +67,13 @@ def content() -> None: ui.button( text="Clone project", on_click=lambda: run_ansible_playbook( - "project_clone.yml", ngui_log=log + playbook_name="project_clone.yml", ngui_log=playbook_log ), ) ui.button( text="Build project", on_click=lambda: run_ansible_playbook( - "project_build.yml", ngui_log=log + "project_build.yml", ngui_log=playbook_log ), ) # Second Row @@ -50,5 +87,5 @@ def content() -> None: with ui.row().classes("w-full"): with ui.card().classes("w-full"): ui.label("Playbook Log").classes("text-h6") - ui.button("Clear Log", on_click=lambda: log.clear()) - log = ui.log().classes("w-full h-full") + ui.button("Clear Log", on_click=lambda: playbook_log.clear()) + playbook_log = ui.log().classes("w-full h-full") diff --git a/anvil/nicegui/utils.py b/anvil/nicegui/utils.py index 01299e9..3f381c8 100644 --- a/anvil/nicegui/utils.py +++ b/anvil/nicegui/utils.py @@ -1,4 +1,117 @@ from pathlib import Path +import platform +from typing import Optional +from nicegui import events, ui + + +# most of the local_file_picker class is from https://github.com/zauberzeug/nicegui/blob/main/examples/local_file_picker/local_file_picker.py +# i just added the file_name_filter +class local_file_picker(ui.dialog): + + def __init__( + self, + directory: str, + *, + upper_limit: Optional[str] = ..., + multiple: bool = False, + show_hidden_files: bool = False, + file_name_filter: Optional[str] = None, + ) -> None: + """Local File Picker + + This is a simple file picker that allows you to select a file from the local filesystem where NiceGUI is running. + + :param directory: The directory to start in. + :param upper_limit: The directory to stop at (None: no limit, default: same as the starting directory). + :param multiple: Whether to allow multiple files to be selected. + :param show_hidden_files: Whether to show hidden files. + :param file_name_filter: Filter files in directory based on file name + """ + super().__init__() + + self.path = Path(directory).expanduser() + if upper_limit is None: + self.upper_limit = None + else: + self.upper_limit = Path( + directory if upper_limit == ... else upper_limit + ).expanduser() + self.show_hidden_files = show_hidden_files + self.file_name_filter = file_name_filter + with self, ui.card(): + self.add_drives_toggle() + self.grid = ( + ui.aggrid( + { + "columnDefs": [{"field": "name", "headerName": "File"}], + "rowSelection": "multiple" if multiple else "single", + }, + html_columns=[0], + ) + .classes("w-96") + .on("cellDoubleClicked", self.handle_double_click) + ) + with ui.row().classes("w-full justify-end"): + ui.button("Cancel", on_click=self.close).props("outline") + ui.button("Ok", on_click=self._handle_ok) + self.update_grid() + + def add_drives_toggle(self): + if platform.system() == "Windows": + import win32api + + drives = win32api.GetLogicalDriveStrings().split("\000")[:-1] + self.drives_toggle = ui.toggle( + drives, value=drives[0], on_change=self.update_drive + ) + + def update_drive(self): + self.path = Path(self.drives_toggle.value).expanduser() + self.update_grid() + + def update_grid(self) -> None: + paths = list(self.path.glob("*")) + if not self.show_hidden_files: + paths = [p for p in paths if not p.name.startswith(".")] + if self.file_name_filter: # Add this condition + paths = [p for p in paths if p.is_file() and p.suffix == self.file_name_filter] + paths.sort(key=lambda p: p.name.lower()) + paths.sort(key=lambda p: not p.is_dir()) + + self.grid.options["rowData"] = [ + { + "name": f"📁 {p.name}" if p.is_dir() else p.name, + "path": str(p), + } + for p in paths + ] + if ( + self.upper_limit is None + and self.path != self.path.parent + or self.upper_limit is not None + and self.path != self.upper_limit + ): + self.grid.options["rowData"].insert( + 0, + { + "name": "📁 ..", + "path": str(self.path.parent), + }, + ) + self.grid.update() + + def handle_double_click(self, e: events.GenericEventArguments) -> None: + self.path = Path(e.args["data"]["path"]) + if self.path.is_dir(): + self.update_grid() + else: + self.submit([str(self.path)]) + + async def _handle_ok(self): + rows = await ui.run_javascript( + f"getElement({self.grid.id}).gridOptions.api.getSelectedRows()" + ) + self.submit([r["path"] for r in rows]) def get_project_root() -> Path: diff --git a/forge-pod.yml b/forge-pod.yml index d022d04..e7c4e3b 100644 --- a/forge-pod.yml +++ b/forge-pod.yml @@ -107,6 +107,9 @@ spec: - name: ublue-os_forge-certs-pvc persistentVolumeClaim: claimName: ublue-os_forge-certs + - name: ublue-os_forge-data-pvc + persistentVolumeClaim: + claimName: ublue-os_forge-data containers: - name: ansible.${FORGE_DOMAIN_NAME} image: anvil # will be built on pod start @@ -114,6 +117,8 @@ spec: - mountPath: /certs name: ublue-os_forge-certs-pvc readOnly: true + - mountPath: /data + name: ublue-os_forge-data-pvc env: - name: ANSIBLE_HOST_USER valueFrom: