diff --git a/OpenOrchestrator/database/db_util.py b/OpenOrchestrator/database/db_util.py index 3257038d..4c4fd134 100644 --- a/OpenOrchestrator/database/db_util.py +++ b/OpenOrchestrator/database/db_util.py @@ -899,6 +899,62 @@ def _apply_filters(query): return elements_tuple +def get_queue_element(element_id: UUID | str) -> QueueElement: + """Get a specific QueueElement from id. + + Args: + element_id: ID of QueueElement to get. + + 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) + + +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) + + 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 + if created_by: + q_element.created_by = created_by + 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..5b454cb0 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. @@ -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/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 9b60980e..3eeb9164 100644 --- a/OpenOrchestrator/orchestrator/popups/queue_element_popup.py +++ b/OpenOrchestrator/orchestrator/popups/queue_element_popup.py @@ -1,74 +1,121 @@ """Class for queue element popups.""" import json +from typing import Callable + from nicegui import ui from OpenOrchestrator.orchestrator import test_helper - -# Styling constants -LABEL = 'text-subtitle2 text-grey-7' -VALUE = 'text-body1 gap-0' -SECTION = 'gap-0' +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, 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): + 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. """ - with ui.dialog() as 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'): - ui.label("Reference:").classes(LABEL) - self.reference_text = ui.label(row_data.get('Reference', 'N/A')).classes('text-h5') - 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() - - with ui.column().classes('gap-1'): + self.on_dialog_close_callback = 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').classes('gap-0'): - 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: - 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;') - except (json.JSONDecodeError, TypeError): - self.data_text = ui.code(data_text).classes('h-12.5rem w-full').style('max-width: 37.5rem;') - - 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 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'): + 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().classes('gap-0'): + with ui.row().classes('w-full'): + 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'): - 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) - 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'): + 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') - ui.button('Close', on_click=dialog.close).classes('mt-4') test_helper.set_automation_ids(self, "queue_element_popup") - dialog.open() + self.dialog.open() + self._pre_populate() + + def _pre_populate(self): + """Pre populate the inputs with an existing credential.""" + if self.queue_element: + 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 + 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: + self.id_text.text = "NOT SAVED" + self.created_by.text = "Orchestrator UI" + self.status.value = "NEW" + self.delete_button.visible = False + + 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 _close_dialog(self): + self.on_dialog_close_callback() + self.dialog.close() + + async def _delete_element(self): + if not self.queue_element: + return + 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): + 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, + 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()) + + 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 1d6a1ea4..74f19eca 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"): @@ -95,10 +96,16 @@ 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: self._open_queue_element_popup(e.args[1])) 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") def _dense_table(self, value: bool): @@ -158,3 +165,17 @@ 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")) + 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): + 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..5f0b287b 100644 --- a/OpenOrchestrator/tests/ui_tests/test_queues_tab.py +++ b/OpenOrchestrator/tests/ui_tests/test_queues_tab.py @@ -205,12 +205,99 @@ 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 + 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() + + # 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 + 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() + + @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 + 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() + def _create_queue_elements(self): """Create some queue elements. Creates 1x'New', 2x'In Progress' and so on. @@ -233,10 +320,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) 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