OpenOrchestrator 1.0.2__py3-none-any.whl → 1.2.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. OpenOrchestrator/__init__.py +7 -0
  2. OpenOrchestrator/__main__.py +2 -0
  3. OpenOrchestrator/common/connection_frame.py +35 -49
  4. OpenOrchestrator/common/crypto_util.py +4 -4
  5. OpenOrchestrator/common/datetime_util.py +20 -0
  6. OpenOrchestrator/database/constants.py +25 -19
  7. OpenOrchestrator/database/db_util.py +77 -30
  8. OpenOrchestrator/database/logs.py +13 -0
  9. OpenOrchestrator/database/queues.py +17 -0
  10. OpenOrchestrator/database/triggers.py +25 -56
  11. OpenOrchestrator/orchestrator/application.py +87 -34
  12. OpenOrchestrator/orchestrator/datetime_input.py +75 -0
  13. OpenOrchestrator/orchestrator/popups/constant_popup.py +87 -69
  14. OpenOrchestrator/orchestrator/popups/credential_popup.py +92 -82
  15. OpenOrchestrator/orchestrator/popups/generic_popups.py +27 -0
  16. OpenOrchestrator/orchestrator/popups/trigger_popup.py +216 -0
  17. OpenOrchestrator/orchestrator/tabs/constants_tab.py +52 -0
  18. OpenOrchestrator/orchestrator/tabs/logging_tab.py +70 -0
  19. OpenOrchestrator/orchestrator/tabs/queue_tab.py +116 -0
  20. OpenOrchestrator/orchestrator/tabs/settings_tab.py +22 -0
  21. OpenOrchestrator/orchestrator/tabs/trigger_tab.py +87 -0
  22. OpenOrchestrator/scheduler/application.py +3 -3
  23. OpenOrchestrator/scheduler/connection_frame.py +96 -0
  24. OpenOrchestrator/scheduler/run_tab.py +87 -80
  25. OpenOrchestrator/scheduler/runner.py +33 -25
  26. OpenOrchestrator/scheduler/settings_tab.py +2 -1
  27. {OpenOrchestrator-1.0.2.dist-info → OpenOrchestrator-1.2.0.dist-info}/METADATA +2 -2
  28. OpenOrchestrator-1.2.0.dist-info/RECORD +38 -0
  29. OpenOrchestrator/orchestrator/constants_tab.py +0 -169
  30. OpenOrchestrator/orchestrator/logging_tab.py +0 -221
  31. OpenOrchestrator/orchestrator/popups/queue_trigger_popup.py +0 -129
  32. OpenOrchestrator/orchestrator/popups/scheduled_trigger_popup.py +0 -129
  33. OpenOrchestrator/orchestrator/popups/single_trigger_popup.py +0 -134
  34. OpenOrchestrator/orchestrator/settings_tab.py +0 -31
  35. OpenOrchestrator/orchestrator/table_util.py +0 -76
  36. OpenOrchestrator/orchestrator/trigger_tab.py +0 -231
  37. OpenOrchestrator-1.0.2.dist-info/RECORD +0 -36
  38. {OpenOrchestrator-1.0.2.dist-info → OpenOrchestrator-1.2.0.dist-info}/LICENSE +0 -0
  39. {OpenOrchestrator-1.0.2.dist-info → OpenOrchestrator-1.2.0.dist-info}/WHEEL +0 -0
  40. {OpenOrchestrator-1.0.2.dist-info → OpenOrchestrator-1.2.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,27 @@
1
+ """This module contains functions to generate generic popups for various use cases."""
2
+
3
+ from nicegui import ui
4
+
5
+
6
+ async def question_popup(question: str, option1: str, option2: str, color1: str = 'primary', color2: str = 'primary') -> bool:
7
+ """Shows a popup with a question and two buttons with the given options.
8
+ Example:
9
+ result = await question_popup("Do you like candy", "YES!", "Not really")
10
+
11
+ Args:
12
+ question: The question to display.
13
+ option1: The text on button 1.
14
+ option2: The text on button 2.
15
+ color1: The color of button 1.
16
+ color2: The color of button 2.
17
+
18
+ Returns:
19
+ bool: True if button 1 is clicked, or False if button 2 is clicked.
20
+ """
21
+ with ui.dialog(value=True).props('persistent') as dialog, ui.card():
22
+ ui.label(question).classes("text-lg")
23
+ with ui.row():
24
+ ui.button(option1, on_click=lambda e: dialog.submit(True), color=color1)
25
+ ui.button(option2, on_click=lambda e: dialog.submit(False), color=color2)
26
+
27
+ return await dialog
@@ -0,0 +1,216 @@
1
+ """This module is responsible for the layout and functionality of the 'New single Trigger' popup."""
2
+
3
+ from __future__ import annotations
4
+ from typing import TYPE_CHECKING
5
+ from datetime import datetime
6
+
7
+ from nicegui import ui
8
+ from croniter import croniter, CroniterBadCronError
9
+
10
+ from OpenOrchestrator.orchestrator.datetime_input import DatetimeInput
11
+ from OpenOrchestrator.database import db_util
12
+ from OpenOrchestrator.database.triggers import Trigger, TriggerStatus, TriggerType
13
+ from OpenOrchestrator.orchestrator.popups import generic_popups
14
+
15
+ if TYPE_CHECKING:
16
+ from OpenOrchestrator.orchestrator.tabs.trigger_tab import TriggerTab
17
+
18
+
19
+ # pylint: disable-next=(too-many-instance-attributes, too-few-public-methods)
20
+ class TriggerPopup():
21
+ """A popup for creating/updating triggers."""
22
+ def __init__(self, trigger_tab: TriggerTab, trigger_type: TriggerType, trigger: Trigger = None):
23
+ """Create a new popup.
24
+ If a trigger is given it will be updated instead of creating a new trigger.
25
+
26
+ Args:
27
+ trigger_tab: The tab parent of the popup.
28
+ trigger_type: The type of trigger popup to show.
29
+ trigger: The Trigger to update if any.
30
+ """
31
+ self.trigger_tab = trigger_tab
32
+ self.trigger_type = trigger_type
33
+ self.trigger = trigger
34
+ title = f'Update {trigger_type.value} Trigger' if trigger else f'New {trigger_type.value} Trigger'
35
+
36
+ with ui.dialog(value=True) as self.dialog, ui.card().classes('w-full'):
37
+ ui.label(title).classes("text-xl")
38
+ self.trigger_input = ui.input("Trigger Name").classes("w-full")
39
+ self.name_input = ui.input("Process Name").classes("w-full")
40
+ self.cron_input = ui.input("Cron expression", on_change=self._cron_change).classes("w-full") # For scheduled triggers
41
+ self.time_input = DatetimeInput("Trigger Time") # For scheduled/single triggers
42
+ with self.cron_input:
43
+ with ui.link(target="https://crontab.guru/", new_tab=True):
44
+ with ui.button(icon="help").props("flat dense"):
45
+ ui.tooltip("Help with cron: https://crontab.guru/")
46
+ self.queue_input = ui.input("Queue Name").classes("w-full") # For queue triggers
47
+ self.batch_input = ui.number("Min Batch Size", value=1, min=1, precision=0, format="%.0f") # For queue triggers
48
+ self.path_input = ui.input("Process Path").classes("w-full")
49
+ self.git_check = ui.checkbox("Is path a Git Repo?")
50
+ self.args_input = ui.input("Process Arguments").classes("w-full")
51
+ self.blocking_check = ui.checkbox("Is process blocking?")
52
+
53
+ if trigger:
54
+ with ui.row():
55
+ ui.button("Enable", on_click=self._enable_trigger)
56
+ ui.button("Disable", on_click=self._disable_trigger)
57
+ ui.button("Delete", on_click=self._delete_trigger, color='red')
58
+ else:
59
+ # Dialog should only be persistent when a new trigger is being created
60
+ self.dialog.props('persistent')
61
+
62
+ with ui.row():
63
+ ui.button("Save", on_click=self._create_trigger)
64
+ ui.button("Cancel", on_click=self.dialog.close)
65
+
66
+ self._disable_unused()
67
+ self._define_validation()
68
+
69
+ if trigger:
70
+ self._pre_populate()
71
+
72
+ def _define_validation(self):
73
+ self.trigger_input.validation = {"Please enter a trigger name": bool}
74
+ self.name_input.validation = {"Please enter a process name": bool}
75
+ self.path_input.validation = {"Please enter a process path": bool}
76
+ self.queue_input.validation = {"Please enter a queue name": bool}
77
+
78
+ def validate_cron(value: str):
79
+ try:
80
+ croniter(value)
81
+ return True
82
+ except CroniterBadCronError:
83
+ return False
84
+
85
+ self.cron_input.validation = {"Invalid cron expression": validate_cron}
86
+
87
+ def _pre_populate(self):
88
+ """Populate the form with values from an existing trigger"""
89
+ self.trigger_input.value = self.trigger.trigger_name
90
+ self.name_input.value = self.trigger.process_name
91
+ self.path_input.value = self.trigger.process_path
92
+ self.args_input.value = self.trigger.process_args
93
+ self.git_check.value = self.trigger.is_git_repo
94
+ self.blocking_check.value = self.trigger.is_blocking
95
+
96
+ if self.trigger_type == TriggerType.SCHEDULED:
97
+ self.cron_input.value = self.trigger.cron_expr
98
+
99
+ if self.trigger_type in (TriggerType.SINGLE, TriggerType.SCHEDULED):
100
+ self.time_input.set_datetime(self.trigger.next_run)
101
+
102
+ if self.trigger_type == TriggerType.QUEUE:
103
+ self.queue_input.value = self.trigger.queue_name
104
+ self.batch_input.value = self.trigger.min_batch_size
105
+
106
+ def _disable_unused(self):
107
+ """Disable all inputs that aren't being used by the current trigger type."""
108
+ if self.trigger_type == TriggerType.QUEUE:
109
+ self.time_input.visible = False
110
+
111
+ if self.trigger_type != TriggerType.SCHEDULED:
112
+ self.cron_input.visible = False
113
+
114
+ if self.trigger_type != TriggerType.QUEUE:
115
+ self.queue_input.visible = False
116
+ self.batch_input.visible = False
117
+
118
+ def _cron_change(self):
119
+ if self.cron_input.validate():
120
+ cron_iter = croniter(self.cron_input.value, datetime.now())
121
+ self.time_input.set_datetime(cron_iter.next(datetime))
122
+
123
+ async def _validate(self) -> bool:
124
+ result = True
125
+
126
+ result &= self.trigger_input.validate()
127
+ result &= self.name_input.validate()
128
+ result &= self.path_input.validate()
129
+
130
+ if self.trigger_type in (TriggerType.SINGLE, TriggerType.SCHEDULED):
131
+ result &= self.time_input.validate()
132
+
133
+ next_run = self.time_input.get_datetime()
134
+ if next_run and next_run < datetime.now():
135
+ result &= await generic_popups.question_popup(
136
+ "The selected datetime is in the past. Do you want to create the trigger anyway?",
137
+ "Create", "Cancel")
138
+
139
+ if self.trigger_type == TriggerType.SCHEDULED:
140
+ result &= self.cron_input.validate()
141
+
142
+ if self.trigger_type == TriggerType.QUEUE:
143
+ result &= self.queue_input.validate()
144
+
145
+ return result
146
+
147
+ async def _create_trigger(self):
148
+ """Creates a new single trigger in the database using the data entered in the UI.
149
+ If an existing trigger was given when creating the popup it is updated instead.
150
+ """
151
+ if not await self._validate():
152
+ ui.notify("Please fill out required information.", type='warning')
153
+ return
154
+
155
+ trigger_name = self.trigger_input.value
156
+ process_name = self.name_input.value
157
+ next_run = self.time_input.get_datetime()
158
+ cron_expr = self.cron_input.value
159
+ queue_name = self.queue_input.value
160
+ min_batch_size = self.batch_input.value
161
+ path = self.path_input.value
162
+ args = self.args_input.value
163
+ is_git = self.git_check.value
164
+ is_blocking = self.blocking_check.value
165
+
166
+ if self.trigger is None:
167
+ # Create new trigger in database
168
+ if self.trigger_type == TriggerType.SINGLE:
169
+ db_util.create_single_trigger(trigger_name, process_name, next_run, path, args, is_git, is_blocking)
170
+ elif self.trigger_type == TriggerType.SCHEDULED:
171
+ db_util.create_scheduled_trigger(trigger_name, process_name, cron_expr, next_run, path, args, is_git, is_blocking)
172
+ elif self.trigger_type == TriggerType.QUEUE:
173
+ db_util.create_queue_trigger(trigger_name, process_name, queue_name, path, args, is_git, is_blocking, min_batch_size)
174
+
175
+ ui.notify("Trigger created", type='positive')
176
+ else:
177
+ # Update existing trigger
178
+ self.trigger.trigger_name = trigger_name
179
+ self.trigger.process_name = process_name
180
+ self.trigger.next_run = next_run
181
+ self.trigger.process_path = path
182
+ self.trigger.process_args = args
183
+ self.trigger.is_git_repo = is_git
184
+ self.trigger.is_blocking = is_blocking
185
+
186
+ if self.trigger_type == TriggerType.SINGLE:
187
+ self.trigger.next_run = next_run
188
+ elif self.trigger_type == TriggerType.SCHEDULED:
189
+ self.trigger.cron_expr = cron_expr
190
+ self.trigger.next_run = next_run
191
+ elif self.trigger_type == TriggerType.QUEUE:
192
+ self.trigger.queue_name = queue_name
193
+ self.trigger.min_batch_size = min_batch_size
194
+
195
+ db_util.update_trigger(self.trigger)
196
+ ui.notify("Trigger updated", type='positive')
197
+
198
+ self.dialog.close()
199
+ self.trigger_tab.update()
200
+
201
+ async def _delete_trigger(self):
202
+ if await generic_popups.question_popup(f"Delete trigger '{self.trigger.trigger_name}'?", "Delete", "Cancel", color1='red'):
203
+ db_util.delete_trigger(self.trigger.id)
204
+ ui.notify("Trigger deleted", type='positive')
205
+ self.dialog.close()
206
+ self.trigger_tab.update()
207
+
208
+ def _disable_trigger(self):
209
+ db_util.set_trigger_status(self.trigger.id, TriggerStatus.PAUSED)
210
+ ui.notify("Trigger status set to 'Paused'.", type='positive')
211
+ self.trigger_tab.update()
212
+
213
+ def _enable_trigger(self):
214
+ db_util.set_trigger_status(self.trigger.id, TriggerStatus.IDLE)
215
+ ui.notify("Trigger status set to 'Idle'.", type='positive')
216
+ self.trigger_tab.update()
@@ -0,0 +1,52 @@
1
+ """This module is responsible for the layout and functionality of the Constants tab
2
+ in Orchestrator."""
3
+
4
+ from nicegui import ui
5
+
6
+ from OpenOrchestrator.database import db_util
7
+ from OpenOrchestrator.orchestrator.popups.constant_popup import ConstantPopup
8
+ from OpenOrchestrator.orchestrator.popups.credential_popup import CredentialPopup
9
+
10
+ CONSTANT_COLUMNS = ("Constant Name", "Value", "Last Changed")
11
+ CREDENTIAL_COLUMNS = ("Credential Name", "Username", "Password", "Last Changed")
12
+
13
+
14
+ class ConstantTab():
15
+ """The 'Constants' tab object."""
16
+ def __init__(self, tab_name: str) -> None:
17
+ with ui.tab_panel(tab_name):
18
+ with ui.row():
19
+ ui.button("New Constant", icon='add', on_click=lambda e: ConstantPopup(self))
20
+ ui.button("New Credential", icon='add', on_click=lambda e: CredentialPopup(self))
21
+
22
+ columns = [{'name': label, 'label': label, 'field': label, 'align': 'left', 'sortable': True} for label in CONSTANT_COLUMNS]
23
+ self.constants_table = ui.table(title="Constants", columns=columns, rows=[], row_key='Constant Name', pagination=10).classes("w-full")
24
+ self.constants_table.on('rowClick', self.row_click_constant)
25
+
26
+ columns = [{'name': label, 'label': label, 'field': label, 'align': 'left', 'sortable': True} for label in CREDENTIAL_COLUMNS]
27
+ self.credentials_table = ui.table(title="Credentials", columns=columns, rows=[], row_key='Credential Name', pagination=10).classes("w-full")
28
+ self.credentials_table.on('rowClick', self.row_click_credential)
29
+
30
+ def row_click_constant(self, event):
31
+ """Callback for when a row is clicked in the table."""
32
+ row = event.args[1]
33
+ name = row['Constant Name']
34
+ constant = db_util.get_constant(name)
35
+ ConstantPopup(self, constant)
36
+
37
+ def row_click_credential(self, event):
38
+ """Callback for when a row is clicked in the table."""
39
+ row = event.args[1]
40
+ name = row['Credential Name']
41
+ credential = db_util.get_credential(name)
42
+ CredentialPopup(self, credential)
43
+
44
+ def update(self):
45
+ """Updates the tables on the tab."""
46
+ constants = db_util.get_constants()
47
+ self.constants_table.rows = [c.to_row_dict() for c in constants]
48
+ self.constants_table.update()
49
+
50
+ credentials = db_util.get_credentials()
51
+ self.credentials_table.rows = [c.to_row_dict() for c in credentials]
52
+ self.credentials_table.update()
@@ -0,0 +1,70 @@
1
+ """This module is responsible for the layout and functionality of the Logging tab
2
+ in Orchestrator."""
3
+
4
+ from nicegui import ui
5
+
6
+ from OpenOrchestrator.database import db_util
7
+ from OpenOrchestrator.orchestrator.datetime_input import DatetimeInput
8
+
9
+
10
+ COLUMNS = (
11
+ {'name': "Log Time", 'label': "Log Time", 'field': "Log Time", 'align': 'left', 'sortable': True},
12
+ {'name': "Process Name", 'label': "Process Name", 'field': "Process Name", 'align': 'left'},
13
+ {'name': "Level", 'label': "Level", 'field': "Level", 'align': 'left'},
14
+ {'name': "Message", 'label': "Message", 'field': "Message", 'align': 'left', ':format': 'value => value.length < 100 ? value : value.substring(0, 100)+"..."'},
15
+ {'name': "ID", 'label': "ID", 'field': "ID", 'headerClasses': 'hidden', 'classes': 'hidden'}
16
+ )
17
+
18
+
19
+ # pylint: disable-next=too-few-public-methods
20
+ class LoggingTab():
21
+ """The 'Logs' tab object."""
22
+ def __init__(self, tab_name: str) -> None:
23
+ with ui.tab_panel(tab_name):
24
+ with ui.row():
25
+ self.from_input = DatetimeInput("From Date", on_change=self.update, allow_empty=True)
26
+ self.to_input = DatetimeInput("To Date", on_change=self.update, allow_empty=True)
27
+ self.process_input = ui.select(["All"], label="Process Name", value="All", on_change=self.update).classes("w-48")
28
+ self.level_input = ui.select(["All", "Trace", "Info", "Error"], value="All", label="Level", on_change=self.update).classes("w-48")
29
+ self.limit_input = ui.select([100, 200, 500, 1000], value=100, label="Limit", on_change=self.update).classes("w-24")
30
+
31
+ self.logs_table = ui.table(title="Logs", columns=COLUMNS, rows=[], row_key='ID', pagination=50).classes("w-full")
32
+ self.logs_table.on("rowClick", self._row_click)
33
+
34
+ def update(self):
35
+ """Update the logs table and Process input list"""
36
+ self._update_table()
37
+ self._update_process_input()
38
+
39
+ def _update_table(self):
40
+ """Update the table with logs from the database applying the filters."""
41
+ from_date = self.from_input.get_datetime()
42
+ to_date = self.to_input.get_datetime()
43
+ process_name = self.process_input.value if self.process_input.value != 'All' else None
44
+ level = self.level_input.value if self.level_input.value != "All" else None
45
+ limit = self.limit_input.value
46
+
47
+ logs = db_util.get_logs(0, limit=limit, from_date=from_date, to_date=to_date, log_level=level, process_name=process_name)
48
+ self.logs_table.rows = [log.to_row_dict() for log in logs]
49
+
50
+ def _update_process_input(self):
51
+ """Update the process input with names from the database."""
52
+ process_names = list(db_util.get_unique_log_process_names())
53
+ process_names.insert(0, "All")
54
+ self.process_input.options = process_names
55
+ self.process_input.update()
56
+
57
+ def _row_click(self, event):
58
+ """Display a dialog with info on the clicked log."""
59
+ row = event.args[1]
60
+ with ui.dialog(value=True), ui.card():
61
+ ui.label("Log ID:").classes("font-bold")
62
+ ui.label(row['ID'])
63
+ ui.label("Log Time:").classes("font-bold")
64
+ ui.label(row['Log Time'])
65
+ ui.label("Process Name:").classes("font-bold")
66
+ ui.label(row['Process Name'])
67
+ ui.label("Log Level:").classes("font-bold")
68
+ ui.label(row['Level'])
69
+ ui.label("Message:").classes("font-bold")
70
+ ui.html(f"<pre>{row['Message']}</pre>")
@@ -0,0 +1,116 @@
1
+ """This module is responsible for the layout and functionality of the Queues tab
2
+ in Orchestrator."""
3
+
4
+ from nicegui import ui
5
+
6
+ from OpenOrchestrator.database import db_util
7
+ from OpenOrchestrator.database.queues import QueueStatus
8
+
9
+
10
+ QUEUE_COLUMNS = (
11
+ {'name': "Queue Name", 'label': "Queue Name", 'field': "Queue Name", 'align': 'left', 'sortable': True},
12
+ {'name': "New", 'label': "New", 'field': "New", 'align': 'left', 'sortable': True},
13
+ {'name': "In Progress", 'label': "In Progress", 'field': "In Progress", 'align': 'left', 'sortable': True},
14
+ {'name': "Done", 'label': "Done", 'field': "Done", 'align': 'left', 'sortable': True},
15
+ {'name': "Failed", 'label': "Failed", 'field': "Failed", 'align': 'left', 'sortable': True}
16
+ )
17
+
18
+ ELEMENT_COLUMNS = (
19
+ {'name': "Reference", 'label': "Reference", 'field': "Reference", 'align': 'left', 'sortable': True},
20
+ {'name': "Status", 'label': "Status", 'field': "Status", 'align': 'left', 'sortable': True},
21
+ {'name': "Data", 'label': "Data", 'field': "Data", 'align': 'left', 'sortable': True},
22
+ {'name': "Message", 'label': "Message", 'field': "Message", 'align': 'left', 'sortable': True},
23
+ {'name': "Created Date", 'label': "Created Date", 'field': "Created Date", 'align': 'left', 'sortable': True},
24
+ {'name': "Start Date", 'label': "Start Date", 'field': "Start Date", 'align': 'left', 'sortable': True},
25
+ {'name': "End Date", 'label': "End Date", 'field': "End Date", 'align': 'left', 'sortable': True},
26
+ {'name': "Created By", 'label': "Created By", 'field': "Created By", 'align': 'left', 'sortable': True},
27
+ {'name': "ID", 'label': "ID", 'field': "ID", 'align': 'left', 'sortable': True}
28
+ )
29
+
30
+
31
+ # pylint: disable-next=too-few-public-methods
32
+ class QueueTab():
33
+ """The 'Queues' tab object. It contains tables and buttons for dealing with queues."""
34
+ def __init__(self, tab_name: str) -> None:
35
+ with ui.tab_panel(tab_name):
36
+ self.queue_table = ui.table(title="Queues", columns=QUEUE_COLUMNS, rows=[], row_key='Queue Name', pagination=50).classes("w-full")
37
+ self.queue_table.on("rowClick", self._row_click)
38
+
39
+ def update(self):
40
+ """Update the queue table with data from the database."""
41
+ queue_count = db_util.get_queue_count()
42
+
43
+ # Convert queue count to row elements
44
+ rows = []
45
+ for queue_name, count in queue_count.items():
46
+ row = {
47
+ "Queue Name": queue_name,
48
+ "New": count.get(QueueStatus.NEW, 0),
49
+ "In Progress": count.get(QueueStatus.IN_PROGRESS, 0),
50
+ "Done": count.get(QueueStatus.DONE, 0),
51
+ "Failed": count.get(QueueStatus.FAILED, 0)
52
+ }
53
+ rows.append(row)
54
+
55
+ self.queue_table.update_rows(rows)
56
+
57
+ def _row_click(self, event):
58
+ row = event.args[1]
59
+ queue_name = row["Queue Name"]
60
+ QueuePopup(queue_name)
61
+
62
+
63
+ # pylint: disable-next=too-few-public-methods
64
+ class QueuePopup():
65
+ """A popup that displays queue elements in a queue."""
66
+ def __init__(self, queue_name) -> None:
67
+ self.queue_name = queue_name
68
+
69
+ with ui.dialog(value=True).props('full-width full-height') as dialog, ui.card():
70
+ with ui.row().classes("w-full"):
71
+ self._create_column_filter()
72
+
73
+ self.limit_select = ui.select(
74
+ options=[100, 200, 500, 1000, "All"],
75
+ label="Limit",
76
+ value=100,
77
+ on_change=self._update).classes("w-24")
78
+
79
+ ui.switch("Dense", on_change=lambda e: self._dense_table(e.value))
80
+
81
+ ui.space()
82
+ ui.button(icon='refresh', on_click=self._update)
83
+ ui.button(icon="close", on_click=dialog.close)
84
+ with ui.scroll_area().classes("h-full"):
85
+ self.table = ui.table(columns=ELEMENT_COLUMNS, rows=[], row_key='ID', title=queue_name, pagination=100).classes("w-full")
86
+
87
+ self._update()
88
+
89
+ def _dense_table(self, value: bool):
90
+ """Change if the table is dense or not."""
91
+ if value:
92
+ self.table.props("dense")
93
+ else:
94
+ self.table.props(remove="dense")
95
+
96
+ def _create_column_filter(self):
97
+ """Create a menu with switches for toggling columns on and off."""
98
+ def toggle(column: dict, visible: bool) -> None:
99
+ column['classes'] = '' if visible else 'hidden'
100
+ column['headerClasses'] = '' if visible else 'hidden'
101
+ self.table.update()
102
+
103
+ with ui.button("Columns", icon="menu"):
104
+ with ui.menu(), ui.column().classes('gap-0 p-2'):
105
+ for column in ELEMENT_COLUMNS:
106
+ ui.switch(column['label'], value=True, on_change=lambda e, column=column: toggle(column, e.value))
107
+
108
+ def _update(self):
109
+ """Update the table with values from the database."""
110
+ limit = self.limit_select.value
111
+ if limit == 'All':
112
+ limit = 1_000_000_000
113
+
114
+ queue_elements = db_util.get_queue_elements(self.queue_name, limit=limit)
115
+ rows = [element.to_row_dict() for element in queue_elements]
116
+ self.table.update_rows(rows)
@@ -0,0 +1,22 @@
1
+ """This module is responsible for the layout and functionality of the Settings tab
2
+ in Orchestrator."""
3
+
4
+ from nicegui import ui
5
+
6
+ from OpenOrchestrator.database import db_util
7
+ from OpenOrchestrator.common.connection_frame import ConnectionFrame
8
+
9
+
10
+ # pylint: disable-next=too-few-public-methods
11
+ class SettingsTab():
12
+ """The settings tab object for Orchestrator."""
13
+ def __init__(self, tab: ui.tab) -> None:
14
+ with ui.tab_panel(tab):
15
+ conn_frame = ConnectionFrame()
16
+ with ui.row().classes("w-full"):
17
+ self.key_button = ui.button("Generate Key", on_click=conn_frame.new_key)
18
+ self.init_button = ui.button("Initialize Database", on_click=self._init_database)
19
+
20
+ def _init_database(self):
21
+ db_util.initialize_database()
22
+ ui.notify("Database initialized!", type='positive')
@@ -0,0 +1,87 @@
1
+ """This module is responsible for the layout and functionality of the Trigger tab
2
+ in Orchestrator."""
3
+
4
+ from nicegui import ui
5
+
6
+ from OpenOrchestrator.database import db_util
7
+ from OpenOrchestrator.database.triggers import SingleTrigger, ScheduledTrigger, QueueTrigger, TriggerType
8
+ from OpenOrchestrator.orchestrator.popups.trigger_popup import TriggerPopup
9
+
10
+ COLUMNS = ("Trigger Name", "Type", "Status", "Process Name", "Last Run", "Next Run", "ID")
11
+
12
+ COLUMNS = (
13
+ {'name': "Trigger Name", 'label': "Trigger Name", 'field': "Trigger Name", 'align': 'left', 'sortable': True},
14
+ {'name': "Type", 'label': "Type", 'field': "Type", 'align': 'left', 'sortable': True},
15
+ {'name': "Status", 'label': "Status", 'field': "Status", 'align': 'left', 'sortable': True},
16
+ {'name': "Process Name", 'label': "Process Name", 'field': "Process Name", 'align': 'left', 'sortable': True},
17
+ {'name': "Last Run", 'label': "Last Run", 'field': "Last Run", 'align': 'left', 'sortable': True},
18
+ {'name': "Next_Run", 'label': "Next Run", 'field': "Next Run", 'align': 'left', 'sortable': True},
19
+ {'name': "ID", 'label': "ID", 'field': "ID", 'align': 'left', 'sortable': True}
20
+ )
21
+
22
+
23
+ # pylint disable-next=too-few-public-methods
24
+ class TriggerTab():
25
+ """The 'Trigger' tab object. It contains tables and buttons for dealing with triggers."""
26
+ def __init__(self, tab_name: str) -> None:
27
+ with ui.tab_panel(tab_name):
28
+ with ui.row():
29
+ ui.button("New Single Trigger", icon="add", on_click=lambda e: TriggerPopup(self, TriggerType.SINGLE))
30
+ ui.button("New Scheduled Trigger", icon="add", on_click=lambda e: TriggerPopup(self, TriggerType.SCHEDULED))
31
+ ui.button("New Queue Trigger", icon="add", on_click=lambda e: TriggerPopup(self, TriggerType.QUEUE))
32
+
33
+ self.trigger_table = ui.table(COLUMNS, [], title="Triggers", pagination=50, row_key='ID').classes("w-full")
34
+ self.trigger_table.on('rowClick', self._row_click)
35
+
36
+ self.add_column_colors()
37
+
38
+ def _row_click(self, event):
39
+ """Callback for when a row is clicked in the table."""
40
+ row = event.args[1]
41
+ trigger_id = row["ID"]
42
+ trigger = db_util.get_trigger(trigger_id)
43
+
44
+ if isinstance(trigger, SingleTrigger):
45
+ TriggerPopup(self, TriggerType.SINGLE, trigger)
46
+ elif isinstance(trigger, ScheduledTrigger):
47
+ TriggerPopup(self, TriggerType.SCHEDULED, trigger)
48
+ elif isinstance(trigger, QueueTrigger):
49
+ TriggerPopup(self, TriggerType.QUEUE, trigger)
50
+
51
+ def update(self):
52
+ """Updates the tab and it's data."""
53
+ triggers = db_util.get_all_triggers()
54
+ self.trigger_table.rows = [t.to_row_dict() for t in triggers]
55
+ self.trigger_table.update()
56
+
57
+ def add_column_colors(self):
58
+ """Add custom coloring to the trigger table."""
59
+ # Add coloring to the status column
60
+ self.trigger_table.add_slot(
61
+ "body-cell-Status",
62
+ '''
63
+ <q-td key="Status" :props="props">
64
+ <q-badge v-if="{Running: 'green', Failed: 'red'}[props.value]" :color="{Running: 'green', Failed: 'red'}[props.value]">
65
+ {{props.value}}
66
+ </q-badge>
67
+ <p v-else>
68
+ {{props.value}}
69
+ </p>
70
+ </q-td>
71
+ '''
72
+ )
73
+
74
+ # Add coloring to 'Next run' column
75
+ # If the next run is in the past the value should be red.
76
+ # Use a very ugly parser to create a date from a dd-MM-yyyy HH:mm:ss date string.
77
+ self.trigger_table.add_slot(
78
+ "body-cell-Next_Run",
79
+ '''
80
+ <q-td key="Next_Run" :props="props">
81
+ {{props.value}}
82
+ <q-badge v-if="new Date(+props.value.substr(6,4), +props.value.substr(3,2)-1, +props.value.substr(0,2), +props.value.substr(11,2), +props.value.substr(14,2)) < new Date()" color='red'>
83
+ Overdue
84
+ </q-badge>
85
+ </q-td>
86
+ '''
87
+ )
@@ -5,6 +5,7 @@ import tkinter
5
5
  from tkinter import ttk, messagebox
6
6
  from OpenOrchestrator.scheduler import settings_tab, run_tab
7
7
 
8
+
8
9
  class Application(tkinter.Tk):
9
10
  """The main application object of the Scheduler app.
10
11
  Extends the tkinter.Tk object.
@@ -25,7 +26,7 @@ class Application(tkinter.Tk):
25
26
  notebook = ttk.Notebook(self)
26
27
  notebook.pack(expand=True, fill='both')
27
28
 
28
- run_tab_ = run_tab.create_tab(notebook, self)
29
+ run_tab_ = run_tab.RunTab(notebook, self)
29
30
  settings_tab_ = settings_tab.create_tab(notebook)
30
31
 
31
32
  notebook.add(run_tab_, text='Run')
@@ -44,6 +45,5 @@ class Application(tkinter.Tk):
44
45
  self.destroy()
45
46
 
46
47
 
47
-
48
- if __name__=='__main__':
48
+ if __name__ == '__main__':
49
49
  Application()