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.
- OpenOrchestrator/__init__.py +7 -0
- OpenOrchestrator/__main__.py +2 -0
- OpenOrchestrator/common/connection_frame.py +35 -49
- OpenOrchestrator/common/crypto_util.py +4 -4
- OpenOrchestrator/common/datetime_util.py +20 -0
- OpenOrchestrator/database/constants.py +25 -19
- OpenOrchestrator/database/db_util.py +77 -30
- OpenOrchestrator/database/logs.py +13 -0
- OpenOrchestrator/database/queues.py +17 -0
- OpenOrchestrator/database/triggers.py +25 -56
- OpenOrchestrator/orchestrator/application.py +87 -34
- OpenOrchestrator/orchestrator/datetime_input.py +75 -0
- OpenOrchestrator/orchestrator/popups/constant_popup.py +87 -69
- OpenOrchestrator/orchestrator/popups/credential_popup.py +92 -82
- OpenOrchestrator/orchestrator/popups/generic_popups.py +27 -0
- OpenOrchestrator/orchestrator/popups/trigger_popup.py +216 -0
- OpenOrchestrator/orchestrator/tabs/constants_tab.py +52 -0
- OpenOrchestrator/orchestrator/tabs/logging_tab.py +70 -0
- OpenOrchestrator/orchestrator/tabs/queue_tab.py +116 -0
- OpenOrchestrator/orchestrator/tabs/settings_tab.py +22 -0
- OpenOrchestrator/orchestrator/tabs/trigger_tab.py +87 -0
- OpenOrchestrator/scheduler/application.py +3 -3
- OpenOrchestrator/scheduler/connection_frame.py +96 -0
- OpenOrchestrator/scheduler/run_tab.py +87 -80
- OpenOrchestrator/scheduler/runner.py +33 -25
- OpenOrchestrator/scheduler/settings_tab.py +2 -1
- {OpenOrchestrator-1.0.2.dist-info → OpenOrchestrator-1.2.0.dist-info}/METADATA +2 -2
- OpenOrchestrator-1.2.0.dist-info/RECORD +38 -0
- OpenOrchestrator/orchestrator/constants_tab.py +0 -169
- OpenOrchestrator/orchestrator/logging_tab.py +0 -221
- OpenOrchestrator/orchestrator/popups/queue_trigger_popup.py +0 -129
- OpenOrchestrator/orchestrator/popups/scheduled_trigger_popup.py +0 -129
- OpenOrchestrator/orchestrator/popups/single_trigger_popup.py +0 -134
- OpenOrchestrator/orchestrator/settings_tab.py +0 -31
- OpenOrchestrator/orchestrator/table_util.py +0 -76
- OpenOrchestrator/orchestrator/trigger_tab.py +0 -231
- OpenOrchestrator-1.0.2.dist-info/RECORD +0 -36
- {OpenOrchestrator-1.0.2.dist-info → OpenOrchestrator-1.2.0.dist-info}/LICENSE +0 -0
- {OpenOrchestrator-1.0.2.dist-info → OpenOrchestrator-1.2.0.dist-info}/WHEEL +0 -0
- {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.
|
|
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()
|