From ea346dfa54efd908fdfce762dbbdffab9d23369c Mon Sep 17 00:00:00 2001 From: kri-bak Date: Mon, 8 Sep 2025 16:06:30 +0200 Subject: [PATCH 01/13] deleting queue element --- .../popups/queue_element_popup.py | 35 ++++++++++++++++--- .../orchestrator/tabs/queue_tab.py | 2 +- 2 files changed, 32 insertions(+), 5 deletions(-) diff --git a/OpenOrchestrator/orchestrator/popups/queue_element_popup.py b/OpenOrchestrator/orchestrator/popups/queue_element_popup.py index 9b60980e..b6df4081 100644 --- a/OpenOrchestrator/orchestrator/popups/queue_element_popup.py +++ b/OpenOrchestrator/orchestrator/popups/queue_element_popup.py @@ -3,6 +3,7 @@ from nicegui import ui from OpenOrchestrator.orchestrator import test_helper +from OpenOrchestrator.database import db_util # Styling constants LABEL = 'text-subtitle2 text-grey-7' @@ -14,13 +15,14 @@ class QueueElementPopup(): """A popup to display queue element data. """ - def __init__(self, row_data: ui.row): + def __init__(self, row_data: ui.row, on_dialog_close_callback): """Show a dialogue with details of the row selected. Args: row_data: Data from the row selected. """ - with ui.dialog() as dialog: + self.on_dialog_close_callback = on_dialog_close_callback + with ui.dialog() as self.dialog: with ui.card().style('min-width: 37.5rem; max-width: 50rem'): with ui.row().classes('w-full justify-between items-start mb-4'): @@ -69,6 +71,31 @@ def __init__(self, row_data: ui.row): ui.label("ID:").classes(LABEL) self.id_text = ui.label(row_data.get('ID', 'N/A')).classes(VALUE) - ui.button('Close', on_click=dialog.close).classes('mt-4') + with ui.row().classes('w-full mt-4'): + ui.button( + icon='delete', + on_click=lambda e, id=row_data.get('ID', 'N/A'): self._confirm_delete(id) + ) + ui.button('Close', on_click=self._close_dialog).classes('mt-4') test_helper.set_automation_ids(self, "queue_element_popup") - dialog.open() + self.dialog.open() + + def _close_dialog(self): + self.on_dialog_close_callback() + self.dialog.close() + + def _delete_element(self, element_id): + db_util.delete_queue_element(element_id) + + def _confirm_delete(self, element_id): + with ui.dialog() as dialog, ui.card(): + ui.label('Are you sure you want to delete this element?') + with ui.row(): + ui.button('Cancel', on_click=dialog.close) + ui.button('Delete', on_click=lambda: self._delete_and_close(element_id, dialog)) + dialog.open() + + def _delete_and_close(self, element_id, dialog): + self._delete_element(element_id) + dialog.close() + self._close_dialog() diff --git a/OpenOrchestrator/orchestrator/tabs/queue_tab.py b/OpenOrchestrator/orchestrator/tabs/queue_tab.py index 1d6a1ea4..bb77c100 100644 --- a/OpenOrchestrator/orchestrator/tabs/queue_tab.py +++ b/OpenOrchestrator/orchestrator/tabs/queue_tab.py @@ -95,7 +95,7 @@ def __init__(self, queue_name: str): self.close_button = ui.button(icon="close", on_click=dialog.close) with ui.scroll_area().classes("h-full"): self.table = ui.table(columns=ELEMENT_COLUMNS, rows=[], row_key='ID', title=queue_name, pagination={'rowsPerPage': self.rows_per_page, 'rowsNumber': self.queue_count}).classes("w-full sticky-header h-[calc(100vh-200px)] overflow-auto") - self.table.on('rowClick', lambda e: QueueElementPopup(e.args[1])) + self.table.on('rowClick', lambda e: QueueElementPopup(e.args[1], on_dialog_close_callback=self._update)) self.table.on('request', self._on_table_request) self._update() From 74ec5c7d0d50cac69b298a77513553613438cde3 Mon Sep 17 00:00:00 2001 From: kri-bak Date: Tue, 9 Sep 2025 10:14:10 +0200 Subject: [PATCH 02/13] refactored delete to use generic popup --- .../popups/queue_element_popup.py | 29 +++++++++---------- .../orchestrator/tabs/queue_tab.py | 10 +++++-- 2 files changed, 21 insertions(+), 18 deletions(-) diff --git a/OpenOrchestrator/orchestrator/popups/queue_element_popup.py b/OpenOrchestrator/orchestrator/popups/queue_element_popup.py index b6df4081..77f1c56e 100644 --- a/OpenOrchestrator/orchestrator/popups/queue_element_popup.py +++ b/OpenOrchestrator/orchestrator/popups/queue_element_popup.py @@ -4,6 +4,7 @@ from OpenOrchestrator.orchestrator import test_helper from OpenOrchestrator.database import db_util +from OpenOrchestrator.orchestrator.popups import generic_popups # Styling constants LABEL = 'text-subtitle2 text-grey-7' @@ -22,6 +23,7 @@ def __init__(self, row_data: ui.row, on_dialog_close_callback): row_data: Data from the row selected. """ self.on_dialog_close_callback = on_dialog_close_callback + self.row_data = row_data with ui.dialog() as self.dialog: with ui.card().style('min-width: 37.5rem; max-width: 50rem'): @@ -74,8 +76,9 @@ def __init__(self, row_data: ui.row, on_dialog_close_callback): with ui.row().classes('w-full mt-4'): ui.button( icon='delete', - on_click=lambda e, id=row_data.get('ID', 'N/A'): self._confirm_delete(id) - ) + on_click=self._delete_element, + color="red" + ).classes('mt-4') ui.button('Close', on_click=self._close_dialog).classes('mt-4') test_helper.set_automation_ids(self, "queue_element_popup") self.dialog.open() @@ -84,18 +87,12 @@ def _close_dialog(self): self.on_dialog_close_callback() self.dialog.close() - def _delete_element(self, element_id): - db_util.delete_queue_element(element_id) + async def _delete_element(self): + if not self.row_data: + return - def _confirm_delete(self, element_id): - with ui.dialog() as dialog, ui.card(): - ui.label('Are you sure you want to delete this element?') - with ui.row(): - ui.button('Cancel', on_click=dialog.close) - ui.button('Delete', on_click=lambda: self._delete_and_close(element_id, dialog)) - dialog.open() - - def _delete_and_close(self, element_id, dialog): - self._delete_element(element_id) - dialog.close() - self._close_dialog() + if await generic_popups.question_popup(f"Delete element '{self.row_data.get('ID')}'?", "Delete", "Cancel", color1='red'): + db_util.delete_queue_element(self.row_data.get('ID')) + ui.notify("Queue element deleted", type='positive') + self.dialog.close() + self.on_dialog_close_callback() diff --git a/OpenOrchestrator/orchestrator/tabs/queue_tab.py b/OpenOrchestrator/orchestrator/tabs/queue_tab.py index bb77c100..7a95b899 100644 --- a/OpenOrchestrator/orchestrator/tabs/queue_tab.py +++ b/OpenOrchestrator/orchestrator/tabs/queue_tab.py @@ -62,19 +62,20 @@ def update(self): def _row_click(self, event): row = event.args[1] queue_name = row["Queue Name"] - QueuePopup(queue_name) + QueuePopup(queue_name, self.update) # pylint: disable-next=too-few-public-methods class QueuePopup(): """A popup that displays queue elements in a queue.""" - def __init__(self, queue_name: str): + def __init__(self, queue_name: str, update_callback): self.queue_name = queue_name self.order_by = "Created Date" self.order_descending = False self.page = 1 self.rows_per_page = 25 self.queue_count = 100 + self.update_callback = update_callback # To make sure the main table updates changes to queue elements. with ui.dialog(value=True).props('full-width full-height') as dialog, ui.card(): with ui.row().classes("w-full"): @@ -92,6 +93,7 @@ def __init__(self, queue_name: str): ui.switch("Dense", on_change=lambda e: self._dense_table(e.value)) self._create_column_filter() ui.button(icon='refresh', on_click=self._update) + ui.button(icon='add', on_click=self._open_create_dialog) self.close_button = ui.button(icon="close", on_click=dialog.close) with ui.scroll_area().classes("h-full"): self.table = ui.table(columns=ELEMENT_COLUMNS, rows=[], row_key='ID', title=queue_name, pagination={'rowsPerPage': self.rows_per_page, 'rowsNumber': self.queue_count}).classes("w-full sticky-header h-[calc(100vh-200px)] overflow-auto") @@ -99,6 +101,7 @@ def __init__(self, queue_name: str): self.table.on('request', self._on_table_request) self._update() + self.update_callback() test_helper.set_automation_ids(self, "queue_popup") def _dense_table(self, value: bool): @@ -158,3 +161,6 @@ def _update_pagination(self, queue_count): """ self.queue_count = queue_count self.table.pagination = {"rowsNumber": self.queue_count, "page": self.page, "rowsPerPage": self.rows_per_page, "sortBy": self.order_by, "descending": self.order_descending} + + def _open_create_dialog(self): + print("create things!") From 0baba6b5f2f95e5ac5c4041af47dbe18780f2597 Mon Sep 17 00:00:00 2001 From: kri-bak Date: Wed, 10 Sep 2025 09:37:18 +0200 Subject: [PATCH 03/13] refactored in attempt to use prepopulate and proper input types --- .../popups/queue_element_popup.py | 72 ++++++++++++------- 1 file changed, 47 insertions(+), 25 deletions(-) diff --git a/OpenOrchestrator/orchestrator/popups/queue_element_popup.py b/OpenOrchestrator/orchestrator/popups/queue_element_popup.py index 77f1c56e..8295ebd7 100644 --- a/OpenOrchestrator/orchestrator/popups/queue_element_popup.py +++ b/OpenOrchestrator/orchestrator/popups/queue_element_popup.py @@ -1,10 +1,14 @@ """Class for queue element popups.""" import json from nicegui import ui +from typing import Callable +from datetime import datetime from OpenOrchestrator.orchestrator import test_helper from OpenOrchestrator.database import db_util from OpenOrchestrator.orchestrator.popups import generic_popups +from OpenOrchestrator.orchestrator.datetime_input import DatetimeInput +from OpenOrchestrator.database.queues import QueueStatus # Styling constants LABEL = 'text-subtitle2 text-grey-7' @@ -16,7 +20,7 @@ class QueueElementPopup(): """A popup to display queue element data. """ - def __init__(self, row_data: ui.row, on_dialog_close_callback): + def __init__(self, row_data: ui.row | None, on_dialog_close_callback: Callable): """Show a dialogue with details of the row selected. Args: @@ -29,12 +33,9 @@ def __init__(self, row_data: ui.row, on_dialog_close_callback): with ui.row().classes('w-full justify-between items-start mb-4'): with ui.column().classes(SECTION + ' mb-4'): - ui.label("Reference:").classes(LABEL) - self.reference_text = ui.label(row_data.get('Reference', 'N/A')).classes('text-h5') + self.reference = ui.input("Reference") with ui.column().classes(SECTION + ' items-end'): - ui.label('Status').classes(LABEL) - self.status_text = ui.label(row_data.get('Status', 'N/A')).classes('text-h5') - ui.separator() + self.status = ui.select(options={status.name: status.value for status in QueueStatus}, label="Status") with ui.column().classes('gap-1'): @@ -43,32 +44,23 @@ def __init__(self, row_data: ui.row, on_dialog_close_callback): with ui.row().classes('w-full'): ui.label('Data').classes(LABEL) try: - data = json.loads(data_text) - formatted_data = json.dumps(data, indent=2, ensure_ascii=False) - self.data_text = ui.code(formatted_data).classes('h-12.5rem w-full').style('max-width: 37.5rem;') + self.data_field = ui.json_editor({'content': {'json': ""}}) except (json.JSONDecodeError, TypeError): - self.data_text = ui.code(data_text).classes('h-12.5rem w-full').style('max-width: 37.5rem;') + self.data_field = ui.json_editor({'content': {'text': ""}}) - message_text = row_data.get('Message') - if message_text and len(message_text) > 0: - with ui.row().classes('w-full mt-4'): - ui.label('Message').classes(LABEL) - self.message_text = ui.label(message_text).classes(VALUE) + with ui.row().classes('w-full mt-4'): + self.message = ui.input('Message') with ui.row().classes('w-full mt-4'): with ui.column().classes('flex-1'): - ui.label("Created Date:").classes(LABEL) - self.created_date = ui.label(row_data.get('Created Date', 'N/A')).classes(VALUE) + self.created_date = DatetimeInput("Created Date", allow_empty=True) with ui.column().classes('flex-1'): - ui.label("Start Date:").classes(LABEL) - self.start_date = ui.label(row_data.get('Start Date', 'N/A')).classes(VALUE) + self.start_date = DatetimeInput("Start Date", allow_empty=True) with ui.column().classes('flex-1'): - ui.label("End Date:").classes(LABEL) - self.end_date = ui.label(row_data.get('End Date', 'N/A')).classes(VALUE) + self.end_date = DatetimeInput("End Date", allow_empty=True) with ui.row().classes('w-full mt-4'): - ui.label("Created By:").classes(LABEL) - self.created_by = ui.label(row_data.get('Created By', 'N/A')).classes(VALUE) + self.created_by = ui.input("Created By") with ui.row().classes('w-full'): ui.label("ID:").classes(LABEL) self.id_text = ui.label(row_data.get('ID', 'N/A')).classes(VALUE) @@ -82,6 +74,37 @@ def __init__(self, row_data: ui.row, on_dialog_close_callback): ui.button('Close', on_click=self._close_dialog).classes('mt-4') test_helper.set_automation_ids(self, "queue_element_popup") self.dialog.open() + self._pre_populate() + + def _pre_populate(self): + """Pre populate the inputs with an existing credential.""" + if self.row_data: + self.reference.value = self.row_data.get('Reference', 'N/A') + self.status.value = self.row_data.get('Status', 'New').upper().replace(" ", "_") # Hackiddy hack + self.message.value = self.row_data.get('Message', 'N/A') + self._set_data(self.row_data.get('Data', "")) + self.created_date.value = self._convert_datetime(self.row_data.get('Created Date', None)) + self.start_date.value = self._convert_datetime(self.row_data.get('Start Date', None)) + self.end_date.value = self._convert_datetime(self.row_data.get('End Date', None)) + self.created_by.value = self.row_data.get('Created By', 'N/A') + + async def _get_data(self) -> None: + data = await self.data_field.run_editor_method('get') + ui.notify(data) + + async def _set_data(self, data) -> None: + try: + data = json.loads(data) + except ValueError: + pass + await self.data_field.run_editor_method('update', data) + ui.notify(f"Set data: {data}") + + def _convert_datetime(self, date_string): + try: + return datetime.strptime(date_string, "%d-%m-%Y %H:%M:%S").strftime("%d-%m-%Y %H:%M") + except ValueError: + return None def _close_dialog(self): self.on_dialog_close_callback() @@ -90,9 +113,8 @@ def _close_dialog(self): async def _delete_element(self): if not self.row_data: return - if await generic_popups.question_popup(f"Delete element '{self.row_data.get('ID')}'?", "Delete", "Cancel", color1='red'): db_util.delete_queue_element(self.row_data.get('ID')) ui.notify("Queue element deleted", type='positive') - self.dialog.close() self.on_dialog_close_callback() + self.dialog.close() From 0bf41467a3ed17faec546375ae7ccf25dfee5b79 Mon Sep 17 00:00:00 2001 From: kri-bak Date: Wed, 10 Sep 2025 12:02:06 +0200 Subject: [PATCH 04/13] Fixed datetimes and changed json_editor to textarea, reordered some elements --- OpenOrchestrator/database/db_util.py | 25 ++++++ .../orchestrator/datetime_input.py | 4 +- .../popups/queue_element_popup.py | 77 +++++++++---------- 3 files changed, 63 insertions(+), 43 deletions(-) diff --git a/OpenOrchestrator/database/db_util.py b/OpenOrchestrator/database/db_util.py index 3257038d..07bc6436 100644 --- a/OpenOrchestrator/database/db_util.py +++ b/OpenOrchestrator/database/db_util.py @@ -899,6 +899,31 @@ def _apply_filters(query): return elements_tuple +def update_queue_element(element_id: str, reference: str | None = None, status: QueueStatus | None = None, data: str | None = None, message: str | None = None, + created_date: datetime | None = None, start_date: datetime | None = None, end_date: datetime | None = None): + with _get_session() as session: + query = select(QueueElement).where(QueueElement.id == element_id) + q_element: QueueElement = session.scalar(query) + + if q_element: + if reference: + q_element.reference = reference + if status: + q_element.status = status + if data: + q_element.data = data + if message: + q_element.message = message + if created_date: + q_element.created_date = created_date + if start_date: + q_element.start_date = start_date + if end_date: + q_element.end_date = end_date + session.commit() + session.refresh(q_element) + + def get_queue_count() -> dict[str, dict[QueueStatus, int]]: """Count the number of queue elements of each status for every queue. diff --git a/OpenOrchestrator/orchestrator/datetime_input.py b/OpenOrchestrator/orchestrator/datetime_input.py index 06a15d08..dc945b16 100644 --- a/OpenOrchestrator/orchestrator/datetime_input.py +++ b/OpenOrchestrator/orchestrator/datetime_input.py @@ -8,8 +8,8 @@ class DatetimeInput(ui.input): """A datetime input with a button to show a date and time picker dialog.""" - PY_FORMAT = "%d-%m-%Y %H:%M" - VUE_FORMAT = "DD-MM-YYYY HH:mm" + PY_FORMAT = "%d-%m-%Y %H:%M:%S" + VUE_FORMAT = "DD-MM-YYYY HH:mm:ss" def __init__(self, label: str, on_change: Optional[Callable[..., Any]] = None, allow_empty: bool = False) -> None: """Create a new DatetimeInput. diff --git a/OpenOrchestrator/orchestrator/popups/queue_element_popup.py b/OpenOrchestrator/orchestrator/popups/queue_element_popup.py index 8295ebd7..742dfd33 100644 --- a/OpenOrchestrator/orchestrator/popups/queue_element_popup.py +++ b/OpenOrchestrator/orchestrator/popups/queue_element_popup.py @@ -34,23 +34,18 @@ def __init__(self, row_data: ui.row | None, on_dialog_close_callback: Callable): with ui.row().classes('w-full justify-between items-start mb-4'): with ui.column().classes(SECTION + ' mb-4'): self.reference = ui.input("Reference") + ui.label("ID:").classes(LABEL) + self.id_text = ui.label(row_data.get('ID', 'New')).classes(VALUE) with ui.column().classes(SECTION + ' items-end'): - self.status = ui.select(options={status.name: status.value for status in QueueStatus}, label="Status") - - with ui.column().classes('gap-1'): - - data_text = row_data.get('Data', None) - if data_text and len(data_text) > 0: - with ui.row().classes('w-full'): - ui.label('Data').classes(LABEL) - try: - self.data_field = ui.json_editor({'content': {'json': ""}}) - except (json.JSONDecodeError, TypeError): - self.data_field = ui.json_editor({'content': {'text': ""}}) + self.status = ui.select(options={status.name: status.value for status in QueueStatus}, label="Status").classes("w-24") + ui.label("Created by:").classes(LABEL) + self.created_by = ui.label(row_data.get('Created By', 'N/A')) - with ui.row().classes('w-full mt-4'): + with ui.column(): + with ui.row().classes('w-full'): + self.data_field = ui.textarea("Data").classes('w-full mt-4') + with ui.row().classes('w-full mt-4').classes('w-full mt-4'): self.message = ui.input('Message') - with ui.row().classes('w-full mt-4'): with ui.column().classes('flex-1'): self.created_date = DatetimeInput("Created Date", allow_empty=True) @@ -59,19 +54,11 @@ def __init__(self, row_data: ui.row | None, on_dialog_close_callback: Callable): with ui.column().classes('flex-1'): self.end_date = DatetimeInput("End Date", allow_empty=True) - with ui.row().classes('w-full mt-4'): - self.created_by = ui.input("Created By") - with ui.row().classes('w-full'): - ui.label("ID:").classes(LABEL) - self.id_text = ui.label(row_data.get('ID', 'N/A')).classes(VALUE) - with ui.row().classes('w-full mt-4'): - ui.button( - icon='delete', - on_click=self._delete_element, - color="red" - ).classes('mt-4') + ui.button(icon='delete', on_click=self._delete_element, color="red").classes('mt-4') + ui.button(icon='save', on_click=self._save_element).classes('mt-4') ui.button('Close', on_click=self._close_dialog).classes('mt-4') + test_helper.set_automation_ids(self, "queue_element_popup") self.dialog.open() self._pre_populate() @@ -81,28 +68,22 @@ def _pre_populate(self): if self.row_data: self.reference.value = self.row_data.get('Reference', 'N/A') self.status.value = self.row_data.get('Status', 'New').upper().replace(" ", "_") # Hackiddy hack - self.message.value = self.row_data.get('Message', 'N/A') - self._set_data(self.row_data.get('Data', "")) - self.created_date.value = self._convert_datetime(self.row_data.get('Created Date', None)) - self.start_date.value = self._convert_datetime(self.row_data.get('Start Date', None)) - self.end_date.value = self._convert_datetime(self.row_data.get('End Date', None)) - self.created_by.value = self.row_data.get('Created By', 'N/A') + self.message.value = self.row_data.get('Message', '') + self.data_field.value = self._prettify_json(self.row_data.get('Data', '')) + self.created_date.set_datetime(self._convert_datetime(self.row_data.get('Created Date', None))) + self.start_date.set_datetime(self._convert_datetime(self.row_data.get('Start Date', None))) + self.end_date.set_datetime(self._convert_datetime(self.row_data.get('End Date', None))) - async def _get_data(self) -> None: - data = await self.data_field.run_editor_method('get') - ui.notify(data) - - async def _set_data(self, data) -> None: + def _prettify_json(self, json_string: str) -> str: try: - data = json.loads(data) + data = json.loads(json_string) + return json.dumps(data, indent=2, ensure_ascii=False) except ValueError: - pass - await self.data_field.run_editor_method('update', data) - ui.notify(f"Set data: {data}") + return json_string def _convert_datetime(self, date_string): try: - return datetime.strptime(date_string, "%d-%m-%Y %H:%M:%S").strftime("%d-%m-%Y %H:%M") + return datetime.strptime(date_string, "%d-%m-%Y %H:%M:%S") except ValueError: return None @@ -118,3 +99,17 @@ async def _delete_element(self): ui.notify("Queue element deleted", type='positive') self.on_dialog_close_callback() self.dialog.close() + + def _save_element(self): + if not self.row_data: + return + db_util.update_queue_element(self.row_data.get("ID"), + reference=self.reference.value, + status=self.status.value, + data=self.data_field.value, + message=self.message.value, + created_date=self.created_date.get_datetime(), + start_date=self.start_date.get_datetime(), + end_date=self.end_date.get_datetime()) + self.on_dialog_close_callback() + self.dialog.close() From d359e285a9c617b176101297bdfc50e5b30912db Mon Sep 17 00:00:00 2001 From: kri-bak Date: Wed, 10 Sep 2025 17:02:00 +0200 Subject: [PATCH 05/13] Added tests for new ui stuff, not all are passing --- OpenOrchestrator/database/db_util.py | 4 +- .../orchestrator/datetime_input.py | 2 + .../popups/queue_element_popup.py | 57 ++++---- .../orchestrator/tabs/queue_tab.py | 6 +- .../tests/ui_tests/test_logging_tab.py | 4 +- .../tests/ui_tests/test_queues_tab.py | 123 +++++++++++++++++- .../tests/ui_tests/test_trigger_tab.py | 4 +- 7 files changed, 164 insertions(+), 36 deletions(-) diff --git a/OpenOrchestrator/database/db_util.py b/OpenOrchestrator/database/db_util.py index 07bc6436..f28b1b73 100644 --- a/OpenOrchestrator/database/db_util.py +++ b/OpenOrchestrator/database/db_util.py @@ -900,7 +900,7 @@ def _apply_filters(query): def update_queue_element(element_id: str, reference: str | None = None, status: QueueStatus | None = None, data: str | None = None, message: str | None = None, - created_date: datetime | None = None, start_date: datetime | None = None, end_date: datetime | None = None): + created_by: str | None = None, created_date: datetime | None = None, start_date: datetime | None = None, end_date: datetime | None = None): with _get_session() as session: query = select(QueueElement).where(QueueElement.id == element_id) q_element: QueueElement = session.scalar(query) @@ -920,6 +920,8 @@ def update_queue_element(element_id: str, reference: str | None = None, status: q_element.start_date = start_date if end_date: q_element.end_date = end_date + if created_by: + q_element.created_by = created_by session.commit() session.refresh(q_element) diff --git a/OpenOrchestrator/orchestrator/datetime_input.py b/OpenOrchestrator/orchestrator/datetime_input.py index dc945b16..5b454cb0 100644 --- a/OpenOrchestrator/orchestrator/datetime_input.py +++ b/OpenOrchestrator/orchestrator/datetime_input.py @@ -75,6 +75,8 @@ def set_datetime(self, value: datetime) -> None: Args: value: The new datetime value. """ + if not value: + return self.value = value.strftime(self.PY_FORMAT) def _on_change(self, func: Optional[Callable[..., Any]]) -> None: diff --git a/OpenOrchestrator/orchestrator/popups/queue_element_popup.py b/OpenOrchestrator/orchestrator/popups/queue_element_popup.py index 742dfd33..a7e5ace9 100644 --- a/OpenOrchestrator/orchestrator/popups/queue_element_popup.py +++ b/OpenOrchestrator/orchestrator/popups/queue_element_popup.py @@ -20,7 +20,7 @@ class QueueElementPopup(): """A popup to display queue element data. """ - def __init__(self, row_data: ui.row | None, on_dialog_close_callback: Callable): + def __init__(self, row_data: ui.row | None, on_dialog_close_callback: Callable, queue_name: str): """Show a dialogue with details of the row selected. Args: @@ -28,18 +28,19 @@ def __init__(self, row_data: ui.row | None, on_dialog_close_callback: Callable): """ self.on_dialog_close_callback = on_dialog_close_callback self.row_data = row_data + self.queue_name = queue_name with ui.dialog() as self.dialog: with ui.card().style('min-width: 37.5rem; max-width: 50rem'): with ui.row().classes('w-full justify-between items-start mb-4'): with ui.column().classes(SECTION + ' mb-4'): - self.reference = ui.input("Reference") ui.label("ID:").classes(LABEL) - self.id_text = ui.label(row_data.get('ID', 'New')).classes(VALUE) + self.id_text = ui.label() + self.reference = ui.input("Reference") with ui.column().classes(SECTION + ' items-end'): - self.status = ui.select(options={status.name: status.value for status in QueueStatus}, label="Status").classes("w-24") ui.label("Created by:").classes(LABEL) - self.created_by = ui.label(row_data.get('Created By', 'N/A')) + self.created_by = ui.label() + self.status = ui.select(options={status.name: status.value for status in QueueStatus}, label="Status").classes("w-24") with ui.column(): with ui.row().classes('w-full'): @@ -55,9 +56,9 @@ def __init__(self, row_data: ui.row | None, on_dialog_close_callback: Callable): self.end_date = DatetimeInput("End Date", allow_empty=True) with ui.row().classes('w-full mt-4'): - ui.button(icon='delete', on_click=self._delete_element, color="red").classes('mt-4') - ui.button(icon='save', on_click=self._save_element).classes('mt-4') - ui.button('Close', on_click=self._close_dialog).classes('mt-4') + self.delete_button = ui.button(icon='delete', on_click=self._delete_element, color="red").classes('mt-4') + self.save_button = ui.button(icon='save', on_click=self._save_and_close).classes('mt-4') + self.close_button = ui.button('Close', on_click=self._close_dialog).classes('mt-4') test_helper.set_automation_ids(self, "queue_element_popup") self.dialog.open() @@ -66,27 +67,39 @@ def __init__(self, row_data: ui.row | None, on_dialog_close_callback: Callable): def _pre_populate(self): """Pre populate the inputs with an existing credential.""" if self.row_data: + self.created_by.text = self.row_data.get('Created By') + self.id_text.text = self.row_data.get('ID') self.reference.value = self.row_data.get('Reference', 'N/A') - self.status.value = self.row_data.get('Status', 'New').upper().replace(" ", "_") # Hackiddy hack + self.status.value = self.row_data.get('Status').upper().replace(" ", "_") # Hackiddy hack self.message.value = self.row_data.get('Message', '') self.data_field.value = self._prettify_json(self.row_data.get('Data', '')) self.created_date.set_datetime(self._convert_datetime(self.row_data.get('Created Date', None))) self.start_date.set_datetime(self._convert_datetime(self.row_data.get('Start Date', None))) self.end_date.set_datetime(self._convert_datetime(self.row_data.get('End Date', None))) + else: + new_element = db_util.create_queue_element(self.queue_name) + self.id_text.text = new_element.id + self.created_by.text = "Debug" + self.status.value = "NEW" + self.created_date.set_datetime(new_element.created_date) + self._save_element() + ui.notify("New queue element created", type="positive") + + def _convert_datetime(self, date_string): + try: + return datetime.strptime(date_string, "%d-%m-%Y %H:%M:%S") + except ValueError: + return None def _prettify_json(self, json_string: str) -> str: + if not json_string: + return None try: data = json.loads(json_string) return json.dumps(data, indent=2, ensure_ascii=False) except ValueError: return json_string - def _convert_datetime(self, date_string): - try: - return datetime.strptime(date_string, "%d-%m-%Y %H:%M:%S") - except ValueError: - return None - def _close_dialog(self): self.on_dialog_close_callback() self.dialog.close() @@ -97,19 +110,19 @@ async def _delete_element(self): if await generic_popups.question_popup(f"Delete element '{self.row_data.get('ID')}'?", "Delete", "Cancel", color1='red'): db_util.delete_queue_element(self.row_data.get('ID')) ui.notify("Queue element deleted", type='positive') - self.on_dialog_close_callback() - self.dialog.close() + self._close_dialog() def _save_element(self): - if not self.row_data: - return - db_util.update_queue_element(self.row_data.get("ID"), + db_util.update_queue_element(self.id_text.text, reference=self.reference.value, status=self.status.value, data=self.data_field.value, message=self.message.value, + created_by=self.created_by.text, created_date=self.created_date.get_datetime(), start_date=self.start_date.get_datetime(), end_date=self.end_date.get_datetime()) - self.on_dialog_close_callback() - self.dialog.close() + + def _save_and_close(self): + self._save_element() + self._close_dialog() diff --git a/OpenOrchestrator/orchestrator/tabs/queue_tab.py b/OpenOrchestrator/orchestrator/tabs/queue_tab.py index 7a95b899..ff66a9d6 100644 --- a/OpenOrchestrator/orchestrator/tabs/queue_tab.py +++ b/OpenOrchestrator/orchestrator/tabs/queue_tab.py @@ -93,11 +93,11 @@ def __init__(self, queue_name: str, update_callback): ui.switch("Dense", on_change=lambda e: self._dense_table(e.value)) self._create_column_filter() ui.button(icon='refresh', on_click=self._update) - ui.button(icon='add', on_click=self._open_create_dialog) + self.new_button = ui.button(icon='add', on_click=self._open_create_dialog) self.close_button = ui.button(icon="close", on_click=dialog.close) with ui.scroll_area().classes("h-full"): self.table = ui.table(columns=ELEMENT_COLUMNS, rows=[], row_key='ID', title=queue_name, pagination={'rowsPerPage': self.rows_per_page, 'rowsNumber': self.queue_count}).classes("w-full sticky-header h-[calc(100vh-200px)] overflow-auto") - self.table.on('rowClick', lambda e: QueueElementPopup(e.args[1], on_dialog_close_callback=self._update)) + self.table.on('rowClick', lambda e: QueueElementPopup(e.args[1], on_dialog_close_callback=self._update, queue_name=self.queue_name)) self.table.on('request', self._on_table_request) self._update() @@ -163,4 +163,4 @@ def _update_pagination(self, queue_count): self.table.pagination = {"rowsNumber": self.queue_count, "page": self.page, "rowsPerPage": self.rows_per_page, "sortBy": self.order_by, "descending": self.order_descending} def _open_create_dialog(self): - print("create things!") + QueueElementPopup(None, on_dialog_close_callback=self._update, queue_name=self.queue_name) diff --git a/OpenOrchestrator/tests/ui_tests/test_logging_tab.py b/OpenOrchestrator/tests/ui_tests/test_logging_tab.py index 92ebfdf0..0e65c5eb 100644 --- a/OpenOrchestrator/tests/ui_tests/test_logging_tab.py +++ b/OpenOrchestrator/tests/ui_tests/test_logging_tab.py @@ -140,10 +140,10 @@ def _set_date_filter(self, from_date: datetime | None, to_date: datetime | None) to_input.send_keys(Keys.CONTROL, "a", Keys.DELETE) if from_date: - from_input.send_keys(from_date.strftime("%d-%m-%Y %H:%M")) + from_input.send_keys(from_date.strftime("%d-%m-%Y %H:%M:%S")) if to_date: - to_input.send_keys(to_date.strftime("%d-%m-%Y %H:%M")) + to_input.send_keys(to_date.strftime("%d-%m-%Y %H:%M:%S")) def _set_process_filter(self, index: int): """Select a process in the process filter. diff --git a/OpenOrchestrator/tests/ui_tests/test_queues_tab.py b/OpenOrchestrator/tests/ui_tests/test_queues_tab.py index f126ebce..a311047b 100644 --- a/OpenOrchestrator/tests/ui_tests/test_queues_tab.py +++ b/OpenOrchestrator/tests/ui_tests/test_queues_tab.py @@ -205,12 +205,123 @@ def test_queue_element_popup(self): ui_util.click_table_row(self.browser, "queues_tab_queue_table", 0) table_data = ui_util.get_table_data(self.browser, "queue_popup_table") ui_util.click_table_row(self.browser, "queue_popup_table", 0) - for i, field in enumerate(['reference_text', 'status_text', 'data_text', 'message_text', 'created_date', 'start_date', 'end_date', 'created_by', 'id_text']): - field_content = self.browser.find_element(By.CSS_SELECTOR, f"[auto-id=queue_element_popup_{field}]").text - if field == 'data_text': - field_content = field_content.rsplit('\n')[0] # Fix for data field adding newline + for i, field in enumerate(['reference', 'status', 'data_field', 'message', 'created_date', 'start_date', 'end_date', 'created_by', 'id_text']): + field_content = self.browser.find_element(By.CSS_SELECTOR, f"[auto-id=queue_element_popup_{field}]").get_attribute('value') + if field in ['status', 'id_text', 'created_by']: + field_content = self.browser.find_element(By.CSS_SELECTOR, f"[auto-id=queue_element_popup_{field}]").text + if not field_content: + field_content = "N/A" self.assertEqual(table_data[0][i], field_content) + @ui_util.screenshot_on_error + def test_create_new_queue_element(self): + """Test creating a new queue element through the popup.""" + self._create_queue_elements() + ui_util.refresh_ui(self.browser) + ui_util.click_table_row(self.browser, "queues_tab_queue_table", 0) + + # Click "New" button to create a new queue element + self.browser.find_element(By.CSS_SELECTOR, "[auto-id=queue_popup_new_button]").click() + + # Fill in the form and save + reference_input = self.browser.find_element(By.CSS_SELECTOR, "[auto-id=queue_element_popup_reference]") + reference_input.send_keys("New Reference") + + status_select = self.browser.find_element(By.CSS_SELECTOR, "[auto-id=queue_element_popup_status]") + status_select.click() + status_select.find_element(By.XPATH, "//div[contains(@class,'q-item')]//span[text()='In Progress']").click() + + self.browser.find_element(By.CSS_SELECTOR, "[auto-id=queue_element_popup_save_button]").click() + + # Verify the new element appears in the queue popup table + ui_util.refresh_ui(self.browser) + table_data = ui_util.get_table_data(self.browser, "queue_popup_table") + self.assertTrue(any("New Reference" in row[0] for row in table_data)) + + self.browser.find_element(By.CSS_SELECTOR, "[auto-id=queue_popup_close_button]").click() + + @ui_util.screenshot_on_error + def test_edit_queue_element(self): + """Test editing an existing queue element through the popup.""" + self._create_queue_elements() + ui_util.refresh_ui(self.browser) + ui_util.click_table_row(self.browser, "queues_tab_queue_table", 0) + + # Open the first queue element in the popup + ui_util.click_table_row(self.browser, "queue_popup_table", 0) + + # Edit the reference field + reference_input = self.browser.find_element(By.CSS_SELECTOR, "[auto-id=queue_element_popup_reference]") + reference_input.clear() + reference_input.send_keys("Edited Reference") + + # Change the status + status_select = self.browser.find_element(By.CSS_SELECTOR, "[auto-id=queue_element_popup_status]") + status_select.click() + status_select.find_element(By.XPATH, "//div[contains(@class,'q-item')]//span[text()='Done']").click() + + # Save the changes + self.browser.find_element(By.CSS_SELECTOR, "[auto-id=queue_element_popup_save_button]").click() + + # Verify the changes in the queue popup table + ui_util.refresh_ui(self.browser) + table_data = ui_util.get_table_data(self.browser, "queue_popup_table") + self.assertTrue(any("Edited Reference" in row[0] for row in table_data)) + self.assertTrue(any("Done" in row[1] for row in table_data)) + + self.browser.find_element(By.CSS_SELECTOR, "[auto-id=queue_popup_close_button]").click() + + @ui_util.screenshot_on_error + def test_delete_queue_element(self): + """Test deleting a queue element through the popup.""" + self._create_queue_elements() + ui_util.refresh_ui(self.browser) + ui_util.click_table_row(self.browser, "queues_tab_queue_table", 0) + + # Get the initial count of queue elements + initial_count = len(ui_util.get_table_data(self.browser, "queue_popup_table")) + + # Open the first queue element in the popup + ui_util.click_table_row(self.browser, "queue_popup_table", 0) + + # Click the delete button and confirm + self.browser.find_element(By.CSS_SELECTOR, "[auto-id=queue_element_popup_delete_button]").click() + self.browser.find_element(By.CSS_SELECTOR, "[auto-id=popup_option1_button").click() + + # Verify the element was deleted + ui_util.refresh_ui(self.browser) + table_data = ui_util.get_table_data(self.browser, "queue_popup_table") + self.assertEqual(len(table_data), initial_count - 1) + + self.browser.find_element(By.CSS_SELECTOR, "[auto-id=queue_popup_close_button]").click() + + @ui_util.screenshot_on_error + def test_queue_element_validation(self): + """Test validation of queue element fields.""" + self._create_queue_elements() + ui_util.refresh_ui(self.browser) + ui_util.click_table_row(self.browser, "queues_tab_queue_table", 0) + + # Click "New" button to create a new queue element + self.browser.find_element(By.CSS_SELECTOR, "[auto-id=queue_popup_new_button]").click() + + reference_input = self.browser.find_element(By.CSS_SELECTOR, "[auto-id=queue_element_popup_reference]") + reference_input.send_keys("Valid Reference") + + status_select = self.browser.find_element(By.CSS_SELECTOR, "[auto-id=queue_element_popup_status]") + status_select.click() + status_select.find_element(By.XPATH, "//div[contains(@class,'q-item')]//span[text()='In Progress']").click() + + self.browser.find_element(By.CSS_SELECTOR, "[auto-id=queue_element_popup_save_button]").click() + + # Verify the element was created + self.browser.find_element(By.CSS_SELECTOR, "[auto-id=queue_element_popup_close_button]").click() + ui_util.refresh_ui(self.browser) + table_data = ui_util.get_table_data(self.browser, "queue_popup_table") + self.assertTrue(any("Valid Reference" in row[0] for row in table_data)) + + self.browser.find_element(By.CSS_SELECTOR, "[auto-id=queue_popup_close_button]").click() + def _create_queue_elements(self): """Create some queue elements. Creates 1x'New', 2x'In Progress' and so on. @@ -233,10 +344,10 @@ def _set_date_filter(self, from_date: datetime | None, to_date: datetime | None) to_input.send_keys(Keys.CONTROL, "a", Keys.DELETE) if from_date: - from_input.send_keys(from_date.strftime("%d-%m-%Y %H:%M")) + from_input.send_keys(from_date.strftime("%d-%m-%Y %H:%M:%S")) if to_date: - to_input.send_keys(to_date.strftime("%d-%m-%Y %H:%M")) + to_input.send_keys(to_date.strftime("%d-%m-%Y %H:%M:%S")) def _set_status_filter(self, status=None): """Set status filter in queue popup.""" diff --git a/OpenOrchestrator/tests/ui_tests/test_trigger_tab.py b/OpenOrchestrator/tests/ui_tests/test_trigger_tab.py index 9371bba2..a79c83a0 100644 --- a/OpenOrchestrator/tests/ui_tests/test_trigger_tab.py +++ b/OpenOrchestrator/tests/ui_tests/test_trigger_tab.py @@ -32,7 +32,7 @@ def test_single_trigger_creation(self): # Fill form self.browser.find_element(By.CSS_SELECTOR, "[auto-id=trigger_popup_trigger_input]").send_keys("Trigger name") self.browser.find_element(By.CSS_SELECTOR, "[auto-id=trigger_popup_name_input]").send_keys("Process name") - self.browser.find_element(By.CSS_SELECTOR, "[auto-id=trigger_popup_time_input]").send_keys("12-11-2020 12:34") + self.browser.find_element(By.CSS_SELECTOR, "[auto-id=trigger_popup_time_input]").send_keys("12-11-2020 12:34:56") self.browser.find_element(By.CSS_SELECTOR, "[auto-id=trigger_popup_path_input]").send_keys("Process Path") self.browser.find_element(By.CSS_SELECTOR, "[auto-id=trigger_popup_git_check]").click() self.browser.find_element(By.CSS_SELECTOR, "[auto-id=trigger_popup_branch_input]").send_keys("Branch1") @@ -51,7 +51,7 @@ def test_single_trigger_creation(self): self.assertEqual(trigger.trigger_name, "Trigger name") self.assertEqual(trigger.process_name, "Process name") - self.assertEqual(trigger.next_run, datetime.strptime("12-11-2020 12:34", "%d-%m-%Y %H:%M")) + self.assertEqual(trigger.next_run, datetime.strptime("12-11-2020 12:34:56", "%d-%m-%Y %H:%M:%S")) self.assertEqual(trigger.process_path, "Process Path") self.assertEqual(trigger.git_branch, "Branch1") self.assertEqual(trigger.is_git_repo, True) From 337c630f7a5c2b4bd4cc8df055c55923af3f60e0 Mon Sep 17 00:00:00 2001 From: kri-bak Date: Thu, 11 Sep 2025 09:19:49 +0200 Subject: [PATCH 06/13] tests passing --- OpenOrchestrator/tests/ui_tests/test_queues_tab.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/OpenOrchestrator/tests/ui_tests/test_queues_tab.py b/OpenOrchestrator/tests/ui_tests/test_queues_tab.py index a311047b..9b0a94a6 100644 --- a/OpenOrchestrator/tests/ui_tests/test_queues_tab.py +++ b/OpenOrchestrator/tests/ui_tests/test_queues_tab.py @@ -234,7 +234,6 @@ def test_create_new_queue_element(self): self.browser.find_element(By.CSS_SELECTOR, "[auto-id=queue_element_popup_save_button]").click() # Verify the new element appears in the queue popup table - ui_util.refresh_ui(self.browser) table_data = ui_util.get_table_data(self.browser, "queue_popup_table") self.assertTrue(any("New Reference" in row[0] for row in table_data)) @@ -260,14 +259,19 @@ def test_edit_queue_element(self): status_select.click() status_select.find_element(By.XPATH, "//div[contains(@class,'q-item')]//span[text()='Done']").click() + # Edit the date field + reference_input = self.browser.find_element(By.CSS_SELECTOR, "[auto-id=queue_element_popup_start_date]") + reference_input.clear() + reference_input.send_keys("01-01-2000 12:34:56") + # Save the changes self.browser.find_element(By.CSS_SELECTOR, "[auto-id=queue_element_popup_save_button]").click() # Verify the changes in the queue popup table - ui_util.refresh_ui(self.browser) table_data = ui_util.get_table_data(self.browser, "queue_popup_table") self.assertTrue(any("Edited Reference" in row[0] for row in table_data)) self.assertTrue(any("Done" in row[1] for row in table_data)) + self.assertTrue(any("01-01-2000 12:34:56" in row[5] for row in table_data)) self.browser.find_element(By.CSS_SELECTOR, "[auto-id=queue_popup_close_button]").click() @@ -289,7 +293,6 @@ def test_delete_queue_element(self): self.browser.find_element(By.CSS_SELECTOR, "[auto-id=popup_option1_button").click() # Verify the element was deleted - ui_util.refresh_ui(self.browser) table_data = ui_util.get_table_data(self.browser, "queue_popup_table") self.assertEqual(len(table_data), initial_count - 1) @@ -315,8 +318,6 @@ def test_queue_element_validation(self): self.browser.find_element(By.CSS_SELECTOR, "[auto-id=queue_element_popup_save_button]").click() # Verify the element was created - self.browser.find_element(By.CSS_SELECTOR, "[auto-id=queue_element_popup_close_button]").click() - ui_util.refresh_ui(self.browser) table_data = ui_util.get_table_data(self.browser, "queue_popup_table") self.assertTrue(any("Valid Reference" in row[0] for row in table_data)) From 3b0feb104a1297279316e9b78a268507c337d13c Mon Sep 17 00:00:00 2001 From: kri-bak Date: Thu, 11 Sep 2025 10:08:21 +0200 Subject: [PATCH 07/13] updated changelog, changed icons and colors, removed styling constants, moved add button --- OpenOrchestrator/orchestrator/application.py | 2 +- .../orchestrator/popups/constant_popup.py | 4 +-- .../orchestrator/popups/credential_popup.py | 4 +-- .../popups/queue_element_popup.py | 35 ++++++++----------- .../orchestrator/tabs/queue_tab.py | 6 +++- changelog.md | 3 ++ 6 files changed, 28 insertions(+), 26 deletions(-) diff --git a/OpenOrchestrator/orchestrator/application.py b/OpenOrchestrator/orchestrator/application.py index 6d165f1d..e8137659 100644 --- a/OpenOrchestrator/orchestrator/application.py +++ b/OpenOrchestrator/orchestrator/application.py @@ -43,7 +43,7 @@ def __init__(self, port: int | None = None, show: bool = True) -> None: app.on_connect(self.update_loop) app.on_exception(lambda exc: ui.notify(exc, type='negative')) - ui.run(title="Orchestrator", favicon='🤖', native=False, port=port or get_free_port(), reload=False, show=show) + ui.run(title="Orchestrator", favicon='🤖', native=False, port=port or get_free_port(), reload=True, show=show) def update_tab(self): """Update the date in the currently selected tab.""" diff --git a/OpenOrchestrator/orchestrator/popups/constant_popup.py b/OpenOrchestrator/orchestrator/popups/constant_popup.py index 4ca745b5..42b789ee 100644 --- a/OpenOrchestrator/orchestrator/popups/constant_popup.py +++ b/OpenOrchestrator/orchestrator/popups/constant_popup.py @@ -39,7 +39,7 @@ def __init__(self, constant_tab: ConstantTab, constant: Constant | None = None): self.cancel_button = ui.button("Cancel", on_click=self.dialog.close) if constant: - self.delete_button = ui.button("Delete", color='red', on_click=self._delete_constant) + self.delete_button = ui.button("Delete", color='negative', on_click=self._delete_constant) self._define_validation() self._pre_populate() @@ -92,7 +92,7 @@ def _create_constant(self): async def _delete_constant(self): if not self.constant: return - if await question_popup(f"Delete constant '{self.constant.name}?", "Delete", "Cancel", color1='red'): + if await question_popup(f"Delete constant '{self.constant.name}?", "Delete", "Cancel", color1='negative'): db_util.delete_constant(self.constant.name) self.dialog.close() self.constant_tab.update() diff --git a/OpenOrchestrator/orchestrator/popups/credential_popup.py b/OpenOrchestrator/orchestrator/popups/credential_popup.py index 661a1b97..d2e156e9 100644 --- a/OpenOrchestrator/orchestrator/popups/credential_popup.py +++ b/OpenOrchestrator/orchestrator/popups/credential_popup.py @@ -41,7 +41,7 @@ def __init__(self, constant_tab: ConstantTab, credential: Credential | None = No self.cancel_button = ui.button("Cancel", on_click=self.dialog.close) if credential: - self.delete_button = ui.button("Delete", color='red', on_click=self._delete_credential) + self.delete_button = ui.button("Delete", color='negative', on_click=self._delete_credential) self._define_validation() self._pre_populate() @@ -96,7 +96,7 @@ async def _delete_credential(self): """Delete the selected credential.""" if not self.credential: return - if await question_popup(f"Delete credential '{self.credential.name}'?", "Delete", "Cancel", color1='red'): + if await question_popup(f"Delete credential '{self.credential.name}'?", "Delete", "Cancel", color1='negative'): db_util.delete_credential(self.credential.name) self.dialog.close() self.constant_tab.update() diff --git a/OpenOrchestrator/orchestrator/popups/queue_element_popup.py b/OpenOrchestrator/orchestrator/popups/queue_element_popup.py index a7e5ace9..8506f936 100644 --- a/OpenOrchestrator/orchestrator/popups/queue_element_popup.py +++ b/OpenOrchestrator/orchestrator/popups/queue_element_popup.py @@ -10,11 +10,6 @@ from OpenOrchestrator.orchestrator.datetime_input import DatetimeInput from OpenOrchestrator.database.queues import QueueStatus -# Styling constants -LABEL = 'text-subtitle2 text-grey-7' -VALUE = 'text-body1 gap-0' -SECTION = 'gap-0' - # pylint: disable-next=too-few-public-methods, too-many-instance-attributes class QueueElementPopup(): @@ -33,14 +28,14 @@ def __init__(self, row_data: ui.row | None, on_dialog_close_callback: Callable, with ui.card().style('min-width: 37.5rem; max-width: 50rem'): with ui.row().classes('w-full justify-between items-start mb-4'): - with ui.column().classes(SECTION + ' mb-4'): - ui.label("ID:").classes(LABEL) + with ui.column().classes('gap-0 mb-4'): + ui.label("ID:").classes('text-subtitle2 text-grey-7') self.id_text = ui.label() self.reference = ui.input("Reference") - with ui.column().classes(SECTION + ' items-end'): - ui.label("Created by:").classes(LABEL) + with ui.column().classes('gap-0 items-end'): + ui.label("Created by:").classes('text-subtitle2 text-grey-7') self.created_by = ui.label() - self.status = ui.select(options={status.name: status.value for status in QueueStatus}, label="Status").classes("w-24") + self.status = ui.select(options={status.name: status.value for status in QueueStatus}, label="Status").classes("w-32") with ui.column(): with ui.row().classes('w-full'): @@ -56,9 +51,9 @@ def __init__(self, row_data: ui.row | None, on_dialog_close_callback: Callable, self.end_date = DatetimeInput("End Date", allow_empty=True) with ui.row().classes('w-full mt-4'): - self.delete_button = ui.button(icon='delete', on_click=self._delete_element, color="red").classes('mt-4') - self.save_button = ui.button(icon='save', on_click=self._save_and_close).classes('mt-4') + self.save_button = ui.button(text='Save', on_click=self._save_and_close).classes('mt-4') self.close_button = ui.button('Close', on_click=self._close_dialog).classes('mt-4') + self.delete_button = ui.button(text='Delete', on_click=self._delete_element, color="negative").classes('mt-4') test_helper.set_automation_ids(self, "queue_element_popup") self.dialog.open() @@ -69,13 +64,13 @@ def _pre_populate(self): if self.row_data: self.created_by.text = self.row_data.get('Created By') self.id_text.text = self.row_data.get('ID') - self.reference.value = self.row_data.get('Reference', 'N/A') - self.status.value = self.row_data.get('Status').upper().replace(" ", "_") # Hackiddy hack - self.message.value = self.row_data.get('Message', '') - self.data_field.value = self._prettify_json(self.row_data.get('Data', '')) - self.created_date.set_datetime(self._convert_datetime(self.row_data.get('Created Date', None))) - self.start_date.set_datetime(self._convert_datetime(self.row_data.get('Start Date', None))) - self.end_date.set_datetime(self._convert_datetime(self.row_data.get('End Date', None))) + self.reference.value = self.row_data.get('Reference') + self.status.value = self.row_data.get('Status').upper().replace(" ", "_") + self.message.value = self.row_data.get('Message') + self.data_field.value = self._prettify_json(self.row_data.get('Data')) + self.created_date.set_datetime(self._convert_datetime(self.row_data.get('Created Date'))) + self.start_date.set_datetime(self._convert_datetime(self.row_data.get('Start Date'))) + self.end_date.set_datetime(self._convert_datetime(self.row_data.get('End Date'))) else: new_element = db_util.create_queue_element(self.queue_name) self.id_text.text = new_element.id @@ -107,7 +102,7 @@ def _close_dialog(self): async def _delete_element(self): if not self.row_data: return - if await generic_popups.question_popup(f"Delete element '{self.row_data.get('ID')}'?", "Delete", "Cancel", color1='red'): + if await generic_popups.question_popup(f"Delete element '{self.row_data.get('ID')}'?", "Delete", "Cancel", color1='negative'): db_util.delete_queue_element(self.row_data.get('ID')) ui.notify("Queue element deleted", type='positive') self._close_dialog() diff --git a/OpenOrchestrator/orchestrator/tabs/queue_tab.py b/OpenOrchestrator/orchestrator/tabs/queue_tab.py index ff66a9d6..b3a5986e 100644 --- a/OpenOrchestrator/orchestrator/tabs/queue_tab.py +++ b/OpenOrchestrator/orchestrator/tabs/queue_tab.py @@ -93,13 +93,17 @@ def __init__(self, queue_name: str, update_callback): ui.switch("Dense", on_change=lambda e: self._dense_table(e.value)) self._create_column_filter() ui.button(icon='refresh', on_click=self._update) - self.new_button = ui.button(icon='add', on_click=self._open_create_dialog) self.close_button = ui.button(icon="close", on_click=dialog.close) with ui.scroll_area().classes("h-full"): self.table = ui.table(columns=ELEMENT_COLUMNS, rows=[], row_key='ID', title=queue_name, pagination={'rowsPerPage': self.rows_per_page, 'rowsNumber': self.queue_count}).classes("w-full sticky-header h-[calc(100vh-200px)] overflow-auto") self.table.on('rowClick', lambda e: QueueElementPopup(e.args[1], on_dialog_close_callback=self._update, queue_name=self.queue_name)) self.table.on('request', self._on_table_request) + with self.table.add_slot("top"): + ui.label(self.queue_name).classes("text-xl") + ui.space() + self.new_button = ui.button(icon='playlist_add', on_click=self._open_create_dialog) + self._update() self.update_callback() test_helper.set_automation_ids(self, "queue_popup") diff --git a/changelog.md b/changelog.md index 89b03e3a..aa42ada3 100644 --- a/changelog.md +++ b/changelog.md @@ -19,6 +19,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Added priority and scheduler whitelist to triggers. - Added search and status filter to queue element list. - Added overview of queue element. +- Added option to create new queue element. +- Added editing of queue elements. +- Added option to delete queue element. ### Changed From 2ea8540d5a36f6ff42983dcc9b93c12a9aee2cfe Mon Sep 17 00:00:00 2001 From: kri-bak Date: Thu, 11 Sep 2025 10:59:41 +0200 Subject: [PATCH 08/13] Changed queue element popup to use queue element, hide delete button on new element add --- OpenOrchestrator/database/db_util.py | 27 +++++++++++ OpenOrchestrator/orchestrator/application.py | 2 +- .../popups/queue_element_popup.py | 47 ++++++++++--------- .../orchestrator/tabs/queue_tab.py | 11 ++++- 4 files changed, 62 insertions(+), 25 deletions(-) diff --git a/OpenOrchestrator/database/db_util.py b/OpenOrchestrator/database/db_util.py index f28b1b73..76e627cc 100644 --- a/OpenOrchestrator/database/db_util.py +++ b/OpenOrchestrator/database/db_util.py @@ -899,8 +899,35 @@ def _apply_filters(query): return elements_tuple +def get_queue_element(element_id: str) -> QueueElement: + """Get a specific QueueElement from id. + + Args: + element_id: ID of QueueElement to get. + + Returns: + QueueElement with the requested ID. + """ + with _get_session() as session: + query = select(QueueElement).where(QueueElement.id == element_id) + return session.scalar(query) + + def update_queue_element(element_id: str, reference: str | None = None, status: QueueStatus | None = None, data: str | None = None, message: str | None = None, created_by: str | None = None, created_date: datetime | None = None, start_date: datetime | None = None, end_date: datetime | None = None): + """Update fields of specific QueueElement. + + Args: + element_id: ID of QueueElement to update. + reference: New value for reference. Defaults to None. + status: New value for status. Defaults to None. + data: New value for data. Defaults to None. + message: New value for message. Defaults to None. + created_by: New value for created_by. Defaults to None. + created_date: New value for created_date. Defaults to None. + start_date: New value for start_date. Defaults to None. + end_date: New value for end_date. Defaults to None. + """ with _get_session() as session: query = select(QueueElement).where(QueueElement.id == element_id) q_element: QueueElement = session.scalar(query) diff --git a/OpenOrchestrator/orchestrator/application.py b/OpenOrchestrator/orchestrator/application.py index e8137659..6d165f1d 100644 --- a/OpenOrchestrator/orchestrator/application.py +++ b/OpenOrchestrator/orchestrator/application.py @@ -43,7 +43,7 @@ def __init__(self, port: int | None = None, show: bool = True) -> None: app.on_connect(self.update_loop) app.on_exception(lambda exc: ui.notify(exc, type='negative')) - ui.run(title="Orchestrator", favicon='🤖', native=False, port=port or get_free_port(), reload=True, show=show) + ui.run(title="Orchestrator", favicon='🤖', native=False, port=port or get_free_port(), reload=False, show=show) def update_tab(self): """Update the date in the currently selected tab.""" diff --git a/OpenOrchestrator/orchestrator/popups/queue_element_popup.py b/OpenOrchestrator/orchestrator/popups/queue_element_popup.py index 8506f936..569f1ae0 100644 --- a/OpenOrchestrator/orchestrator/popups/queue_element_popup.py +++ b/OpenOrchestrator/orchestrator/popups/queue_element_popup.py @@ -8,21 +8,21 @@ from OpenOrchestrator.database import db_util from OpenOrchestrator.orchestrator.popups import generic_popups from OpenOrchestrator.orchestrator.datetime_input import DatetimeInput -from OpenOrchestrator.database.queues import QueueStatus +from OpenOrchestrator.database.queues import QueueStatus, QueueElement # pylint: disable-next=too-few-public-methods, too-many-instance-attributes class QueueElementPopup(): """A popup to display queue element data. """ - def __init__(self, row_data: ui.row | None, on_dialog_close_callback: Callable, queue_name: str): + def __init__(self, queue_element: QueueElement | None, on_dialog_close_callback: Callable, queue_name: str): """Show a dialogue with details of the row selected. Args: row_data: Data from the row selected. """ self.on_dialog_close_callback = on_dialog_close_callback - self.row_data = row_data + self.queue_element = queue_element self.queue_name = queue_name with ui.dialog() as self.dialog: with ui.card().style('min-width: 37.5rem; max-width: 50rem'): @@ -61,24 +61,21 @@ def __init__(self, row_data: ui.row | None, on_dialog_close_callback: Callable, def _pre_populate(self): """Pre populate the inputs with an existing credential.""" - if self.row_data: - self.created_by.text = self.row_data.get('Created By') - self.id_text.text = self.row_data.get('ID') - self.reference.value = self.row_data.get('Reference') - self.status.value = self.row_data.get('Status').upper().replace(" ", "_") - self.message.value = self.row_data.get('Message') - self.data_field.value = self._prettify_json(self.row_data.get('Data')) - self.created_date.set_datetime(self._convert_datetime(self.row_data.get('Created Date'))) - self.start_date.set_datetime(self._convert_datetime(self.row_data.get('Start Date'))) - self.end_date.set_datetime(self._convert_datetime(self.row_data.get('End Date'))) + if self.queue_element: + self.created_by.text = self.queue_element.created_by + self.id_text.text = self.queue_element.id + self.reference.value = self.queue_element.reference + self.status.value = self.queue_element.status.name + self.message.value = self.queue_element.message + self.data_field.value = self._prettify_json(self.queue_element.data) + self.created_date.set_datetime(self.queue_element.created_date) + self.start_date.set_datetime(self.queue_element.start_date) + self.end_date.set_datetime(self.queue_element.end_date) else: - new_element = db_util.create_queue_element(self.queue_name) - self.id_text.text = new_element.id - self.created_by.text = "Debug" + self.id_text.text = "NOT SAVED" + self.created_by.text = "Orchestrator UI" self.status.value = "NEW" - self.created_date.set_datetime(new_element.created_date) - self._save_element() - ui.notify("New queue element created", type="positive") + self.delete_button.visible = False def _convert_datetime(self, date_string): try: @@ -100,15 +97,19 @@ def _close_dialog(self): self.dialog.close() async def _delete_element(self): - if not self.row_data: + if not self.queue_element: return - if await generic_popups.question_popup(f"Delete element '{self.row_data.get('ID')}'?", "Delete", "Cancel", color1='negative'): - db_util.delete_queue_element(self.row_data.get('ID')) + if await generic_popups.question_popup(f"Delete element '{self.queue_element.id}'?", "Delete", "Cancel", color1='negative'): + db_util.delete_queue_element(self.queue_element.id) ui.notify("Queue element deleted", type='positive') self._close_dialog() def _save_element(self): - db_util.update_queue_element(self.id_text.text, + if not self.queue_element: + self.queue_element = db_util.create_queue_element(self.queue_name) + self.id_text.text = self.queue_element.id + ui.notify("New queue element created", type="positive") + db_util.update_queue_element(self.queue_element.id, reference=self.reference.value, status=self.status.value, data=self.data_field.value, diff --git a/OpenOrchestrator/orchestrator/tabs/queue_tab.py b/OpenOrchestrator/orchestrator/tabs/queue_tab.py index b3a5986e..5ef5ac1c 100644 --- a/OpenOrchestrator/orchestrator/tabs/queue_tab.py +++ b/OpenOrchestrator/orchestrator/tabs/queue_tab.py @@ -96,7 +96,7 @@ def __init__(self, queue_name: str, update_callback): self.close_button = ui.button(icon="close", on_click=dialog.close) with ui.scroll_area().classes("h-full"): self.table = ui.table(columns=ELEMENT_COLUMNS, rows=[], row_key='ID', title=queue_name, pagination={'rowsPerPage': self.rows_per_page, 'rowsNumber': self.queue_count}).classes("w-full sticky-header h-[calc(100vh-200px)] overflow-auto") - self.table.on('rowClick', lambda e: QueueElementPopup(e.args[1], on_dialog_close_callback=self._update, queue_name=self.queue_name)) + self.table.on('rowClick', lambda e: self._open_queue_element_popup(e.args[1])) self.table.on('request', self._on_table_request) with self.table.add_slot("top"): @@ -166,5 +166,14 @@ def _update_pagination(self, queue_count): self.queue_count = queue_count self.table.pagination = {"rowsNumber": self.queue_count, "page": self.page, "rowsPerPage": self.rows_per_page, "sortBy": self.order_by, "descending": self.order_descending} + def _open_queue_element_popup(self, row_data: ui.row): + """Open editable popup for specified row. + + Args: + row_data: Row data from row clicked. + """ + queue_element = db_util.get_queue_element(row_data.get("ID")) + QueueElementPopup(queue_element=queue_element, on_dialog_close_callback=self._update, queue_name=self.queue_name) + def _open_create_dialog(self): QueueElementPopup(None, on_dialog_close_callback=self._update, queue_name=self.queue_name) From bb018047abf0f2262d359a25fb2282d5e874f536 Mon Sep 17 00:00:00 2001 From: kri-bak Date: Thu, 11 Sep 2025 11:20:17 +0200 Subject: [PATCH 09/13] layout and hide created by label when no value is found --- .../popups/queue_element_popup.py | 21 +++++++++++-------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/OpenOrchestrator/orchestrator/popups/queue_element_popup.py b/OpenOrchestrator/orchestrator/popups/queue_element_popup.py index 569f1ae0..0b9593a4 100644 --- a/OpenOrchestrator/orchestrator/popups/queue_element_popup.py +++ b/OpenOrchestrator/orchestrator/popups/queue_element_popup.py @@ -25,23 +25,23 @@ def __init__(self, queue_element: QueueElement | None, on_dialog_close_callback: self.queue_element = queue_element self.queue_name = queue_name with ui.dialog() as self.dialog: - with ui.card().style('min-width: 37.5rem; max-width: 50rem'): + with ui.card().style('min-width: 37.5rem; max-width: 50rem').classes('gap-0'): - with ui.row().classes('w-full justify-between items-start mb-4'): - with ui.column().classes('gap-0 mb-4'): + with ui.row().classes('w-full justify-between items-start'): + with ui.column().classes('gap-0'): ui.label("ID:").classes('text-subtitle2 text-grey-7') self.id_text = ui.label() self.reference = ui.input("Reference") with ui.column().classes('gap-0 items-end'): - ui.label("Created by:").classes('text-subtitle2 text-grey-7') + self.created_by_label = ui.label("Created by:").classes('text-subtitle2 text-grey-7') self.created_by = ui.label() self.status = ui.select(options={status.name: status.value for status in QueueStatus}, label="Status").classes("w-32") - with ui.column(): + with ui.column().classes('gap-0'): with ui.row().classes('w-full'): - self.data_field = ui.textarea("Data").classes('w-full mt-4') - with ui.row().classes('w-full mt-4').classes('w-full mt-4'): - self.message = ui.input('Message') + self.data_field = ui.textarea("Data").classes('w-full') + with ui.row().classes('w-full mt-4'): + self.message = ui.input('Message').classes('w-full') with ui.row().classes('w-full mt-4'): with ui.column().classes('flex-1'): self.created_date = DatetimeInput("Created Date", allow_empty=True) @@ -62,7 +62,10 @@ def __init__(self, queue_element: QueueElement | None, on_dialog_close_callback: def _pre_populate(self): """Pre populate the inputs with an existing credential.""" if self.queue_element: - self.created_by.text = self.queue_element.created_by + if not self.queue_element.created_by: + self.created_by_label.visible = False + else: + self.created_by.text = self.queue_element.created_by self.id_text.text = self.queue_element.id self.reference.value = self.queue_element.reference self.status.value = self.queue_element.status.name From d2af382632782527b0d331df01549b8f65935513 Mon Sep 17 00:00:00 2001 From: kri-bak Date: Thu, 11 Sep 2025 11:26:56 +0200 Subject: [PATCH 10/13] le linting --- OpenOrchestrator/orchestrator/popups/queue_element_popup.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/OpenOrchestrator/orchestrator/popups/queue_element_popup.py b/OpenOrchestrator/orchestrator/popups/queue_element_popup.py index 0b9593a4..d50f131e 100644 --- a/OpenOrchestrator/orchestrator/popups/queue_element_popup.py +++ b/OpenOrchestrator/orchestrator/popups/queue_element_popup.py @@ -1,9 +1,10 @@ """Class for queue element popups.""" import json -from nicegui import ui from typing import Callable from datetime import datetime +from nicegui import ui + from OpenOrchestrator.orchestrator import test_helper from OpenOrchestrator.database import db_util from OpenOrchestrator.orchestrator.popups import generic_popups From 096123be4cfbb0da6a5571bbb3e1e951ffa69f56 Mon Sep 17 00:00:00 2001 From: kri-bak Date: Thu, 11 Sep 2025 11:33:08 +0200 Subject: [PATCH 11/13] removed unused datetime conversion --- .../orchestrator/popups/queue_element_popup.py | 7 ------- 1 file changed, 7 deletions(-) diff --git a/OpenOrchestrator/orchestrator/popups/queue_element_popup.py b/OpenOrchestrator/orchestrator/popups/queue_element_popup.py index d50f131e..3eeb9164 100644 --- a/OpenOrchestrator/orchestrator/popups/queue_element_popup.py +++ b/OpenOrchestrator/orchestrator/popups/queue_element_popup.py @@ -1,7 +1,6 @@ """Class for queue element popups.""" import json from typing import Callable -from datetime import datetime from nicegui import ui @@ -81,12 +80,6 @@ def _pre_populate(self): self.status.value = "NEW" self.delete_button.visible = False - def _convert_datetime(self, date_string): - try: - return datetime.strptime(date_string, "%d-%m-%Y %H:%M:%S") - except ValueError: - return None - def _prettify_json(self, json_string: str) -> str: if not json_string: return None From e7aeb6adb21e9653db8a14f87031b782cf50e66a Mon Sep 17 00:00:00 2001 From: kri-bak Date: Thu, 11 Sep 2025 12:54:48 +0200 Subject: [PATCH 12/13] removed redundant test and added debug for github actions --- .../orchestrator/tabs/queue_tab.py | 2 ++ .../tests/ui_tests/test_queues_tab.py | 25 ------------------- 2 files changed, 2 insertions(+), 25 deletions(-) diff --git a/OpenOrchestrator/orchestrator/tabs/queue_tab.py b/OpenOrchestrator/orchestrator/tabs/queue_tab.py index 5ef5ac1c..74f19eca 100644 --- a/OpenOrchestrator/orchestrator/tabs/queue_tab.py +++ b/OpenOrchestrator/orchestrator/tabs/queue_tab.py @@ -173,6 +173,8 @@ def _open_queue_element_popup(self, row_data: ui.row): row_data: Row data from row clicked. """ queue_element = db_util.get_queue_element(row_data.get("ID")) + print("Queue element:", vars(queue_element)) + print("Queue element __dict__:", queue_element.__dict__) QueueElementPopup(queue_element=queue_element, on_dialog_close_callback=self._update, queue_name=self.queue_name) def _open_create_dialog(self): diff --git a/OpenOrchestrator/tests/ui_tests/test_queues_tab.py b/OpenOrchestrator/tests/ui_tests/test_queues_tab.py index 9b0a94a6..5f0b287b 100644 --- a/OpenOrchestrator/tests/ui_tests/test_queues_tab.py +++ b/OpenOrchestrator/tests/ui_tests/test_queues_tab.py @@ -298,31 +298,6 @@ def test_delete_queue_element(self): self.browser.find_element(By.CSS_SELECTOR, "[auto-id=queue_popup_close_button]").click() - @ui_util.screenshot_on_error - def test_queue_element_validation(self): - """Test validation of queue element fields.""" - self._create_queue_elements() - ui_util.refresh_ui(self.browser) - ui_util.click_table_row(self.browser, "queues_tab_queue_table", 0) - - # Click "New" button to create a new queue element - self.browser.find_element(By.CSS_SELECTOR, "[auto-id=queue_popup_new_button]").click() - - reference_input = self.browser.find_element(By.CSS_SELECTOR, "[auto-id=queue_element_popup_reference]") - reference_input.send_keys("Valid Reference") - - status_select = self.browser.find_element(By.CSS_SELECTOR, "[auto-id=queue_element_popup_status]") - status_select.click() - status_select.find_element(By.XPATH, "//div[contains(@class,'q-item')]//span[text()='In Progress']").click() - - self.browser.find_element(By.CSS_SELECTOR, "[auto-id=queue_element_popup_save_button]").click() - - # Verify the element was created - table_data = ui_util.get_table_data(self.browser, "queue_popup_table") - self.assertTrue(any("Valid Reference" in row[0] for row in table_data)) - - self.browser.find_element(By.CSS_SELECTOR, "[auto-id=queue_popup_close_button]").click() - def _create_queue_elements(self): """Create some queue elements. Creates 1x'New', 2x'In Progress' and so on. From 2095e1b2ed69912a3a5b83de7913e31f61f4989a Mon Sep 17 00:00:00 2001 From: kri-bak Date: Thu, 11 Sep 2025 13:12:00 +0200 Subject: [PATCH 13/13] fixed id being a string --- OpenOrchestrator/database/db_util.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/OpenOrchestrator/database/db_util.py b/OpenOrchestrator/database/db_util.py index 76e627cc..4c4fd134 100644 --- a/OpenOrchestrator/database/db_util.py +++ b/OpenOrchestrator/database/db_util.py @@ -899,7 +899,7 @@ def _apply_filters(query): return elements_tuple -def get_queue_element(element_id: str) -> QueueElement: +def get_queue_element(element_id: UUID | str) -> QueueElement: """Get a specific QueueElement from id. Args: @@ -908,6 +908,8 @@ def get_queue_element(element_id: str) -> QueueElement: Returns: QueueElement with the requested ID. """ + if isinstance(element_id, str): + element_id = UUID(element_id) with _get_session() as session: query = select(QueueElement).where(QueueElement.id == element_id) return session.scalar(query)