OpenOrchestrator 1.3.1__py3-none-any.whl → 2.0.0rc2__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/__main__.py +66 -12
- OpenOrchestrator/common/connection_frame.py +10 -4
- OpenOrchestrator/database/base.py +8 -0
- OpenOrchestrator/database/constants.py +3 -15
- OpenOrchestrator/database/db_util.py +110 -37
- OpenOrchestrator/database/logs.py +3 -15
- OpenOrchestrator/database/queues.py +3 -15
- OpenOrchestrator/database/schedulers.py +32 -0
- OpenOrchestrator/database/triggers.py +7 -16
- OpenOrchestrator/orchestrator/application.py +13 -8
- OpenOrchestrator/orchestrator/datetime_input.py +2 -2
- OpenOrchestrator/orchestrator/popups/constant_popup.py +8 -6
- OpenOrchestrator/orchestrator/popups/credential_popup.py +10 -8
- OpenOrchestrator/orchestrator/popups/generic_popups.py +15 -2
- OpenOrchestrator/orchestrator/popups/trigger_popup.py +25 -14
- OpenOrchestrator/orchestrator/tabs/constants_tab.py +5 -2
- OpenOrchestrator/orchestrator/tabs/logging_tab.py +3 -0
- OpenOrchestrator/orchestrator/tabs/queue_tab.py +4 -1
- OpenOrchestrator/orchestrator/tabs/schedulers_tab.py +45 -0
- OpenOrchestrator/orchestrator/tabs/settings_tab.py +2 -5
- OpenOrchestrator/orchestrator/tabs/trigger_tab.py +9 -5
- OpenOrchestrator/orchestrator/test_helper.py +17 -0
- OpenOrchestrator/orchestrator_connection/connection.py +21 -4
- OpenOrchestrator/scheduler/application.py +3 -2
- OpenOrchestrator/scheduler/run_tab.py +16 -6
- OpenOrchestrator/scheduler/runner.py +52 -64
- OpenOrchestrator/scheduler/settings_tab.py +90 -14
- OpenOrchestrator/scheduler/util.py +8 -0
- OpenOrchestrator/tests/__init__.py +0 -0
- OpenOrchestrator/tests/db_test_util.py +40 -0
- OpenOrchestrator/tests/test_db_util.py +372 -0
- OpenOrchestrator/tests/test_orchestrator_connection.py +142 -0
- OpenOrchestrator/tests/test_trigger_polling.py +143 -0
- OpenOrchestrator/tests/ui_tests/__init__.py +0 -0
- OpenOrchestrator/tests/ui_tests/test_constants_tab.py +167 -0
- OpenOrchestrator/tests/ui_tests/test_logging_tab.py +180 -0
- OpenOrchestrator/tests/ui_tests/test_queues_tab.py +126 -0
- OpenOrchestrator/tests/ui_tests/test_schedulers_tab.py +47 -0
- OpenOrchestrator/tests/ui_tests/test_trigger_tab.py +243 -0
- OpenOrchestrator/tests/ui_tests/ui_util.py +151 -0
- openorchestrator-2.0.0rc2.dist-info/METADATA +158 -0
- openorchestrator-2.0.0rc2.dist-info/RECORD +55 -0
- {openorchestrator-1.3.1.dist-info → openorchestrator-2.0.0rc2.dist-info}/WHEEL +1 -1
- OpenOrchestrator/scheduler/connection_frame.py +0 -96
- openorchestrator-1.3.1.dist-info/METADATA +0 -60
- openorchestrator-1.3.1.dist-info/RECORD +0 -39
- {openorchestrator-1.3.1.dist-info → openorchestrator-2.0.0rc2.dist-info/licenses}/LICENSE +0 -0
- {openorchestrator-1.3.1.dist-info → openorchestrator-2.0.0rc2.dist-info}/top_level.txt +0 -0
@@ -10,29 +10,32 @@ from OpenOrchestrator.orchestrator.tabs.settings_tab import SettingsTab
|
|
10
10
|
from OpenOrchestrator.orchestrator.tabs.logging_tab import LoggingTab
|
11
11
|
from OpenOrchestrator.orchestrator.tabs.constants_tab import ConstantTab
|
12
12
|
from OpenOrchestrator.orchestrator.tabs.queue_tab import QueueTab
|
13
|
+
from OpenOrchestrator.orchestrator.tabs.schedulers_tab import SchedulerTab
|
13
14
|
|
14
15
|
|
15
16
|
class Application():
|
16
17
|
"""The main application of Orchestrator.
|
17
18
|
It contains a header and the four tabs of the application.
|
18
19
|
"""
|
19
|
-
def __init__(self) -> None:
|
20
|
+
def __init__(self, port: int | None = None, show: bool = True) -> None:
|
20
21
|
with ui.header():
|
21
22
|
with ui.tabs() as self.tabs:
|
22
|
-
ui.tab('Triggers')
|
23
|
-
ui.tab('Logs')
|
24
|
-
ui.tab('Constants')
|
25
|
-
ui.tab('
|
26
|
-
ui.tab('
|
23
|
+
ui.tab('Triggers').props("auto-id=trigger_tab")
|
24
|
+
ui.tab('Logs').props("auto-id=logs_tab")
|
25
|
+
ui.tab('Constants').props("auto-id=constants_tab")
|
26
|
+
ui.tab('Schedulers').props("auto-id=schedulers_tab")
|
27
|
+
ui.tab('Queues').props("auto-id=queues_tab")
|
28
|
+
ui.tab('Settings').props("auto-id=settings_tab")
|
27
29
|
|
28
30
|
ui.space()
|
29
31
|
ui.button(icon="contrast", on_click=ui.dark_mode().toggle)
|
30
|
-
ui.button(icon='refresh', on_click=self.update_tab)
|
32
|
+
ui.button(icon='refresh', on_click=self.update_tab).props("auto-id=refresh_button")
|
31
33
|
|
32
34
|
with ui.tab_panels(self.tabs, value='Settings', on_change=self.update_tab).classes('w-full') as self.tab_panels:
|
33
35
|
self.t_tab = TriggerTab('Triggers')
|
34
36
|
self.l_tab = LoggingTab("Logs")
|
35
37
|
self.c_tab = ConstantTab("Constants")
|
38
|
+
self.s_tab = SchedulerTab("Schedulers")
|
36
39
|
self.q_tab = QueueTab("Queues")
|
37
40
|
SettingsTab('Settings')
|
38
41
|
|
@@ -41,7 +44,7 @@ class Application():
|
|
41
44
|
app.on_connect(self.update_loop)
|
42
45
|
app.on_disconnect(app.shutdown)
|
43
46
|
app.on_exception(lambda exc: ui.notify(exc, type='negative'))
|
44
|
-
ui.run(title="Orchestrator", favicon='🤖', native=False, port=get_free_port(), reload=False)
|
47
|
+
ui.run(title="Orchestrator", favicon='🤖', native=False, port=port or get_free_port(), reload=False, show=show)
|
45
48
|
|
46
49
|
def update_tab(self):
|
47
50
|
"""Update the date in the currently selected tab."""
|
@@ -52,6 +55,8 @@ class Application():
|
|
52
55
|
self.l_tab.update()
|
53
56
|
case 'Constants':
|
54
57
|
self.c_tab.update()
|
58
|
+
case 'Schedulers':
|
59
|
+
self.s_tab.update()
|
55
60
|
case 'Queues':
|
56
61
|
self.q_tab.update()
|
57
62
|
|
@@ -43,7 +43,7 @@ class DatetimeInput(ui.input):
|
|
43
43
|
|
44
44
|
def _define_validation(self, allow_empty: bool):
|
45
45
|
if not allow_empty:
|
46
|
-
self.
|
46
|
+
self._validation = { # pylint: disable=protected-access
|
47
47
|
"Please enter a datetime": bool,
|
48
48
|
f"Invalid datetime: {self.PY_FORMAT}": lambda v: self.get_datetime() is not None
|
49
49
|
}
|
@@ -55,7 +55,7 @@ class DatetimeInput(ui.input):
|
|
55
55
|
|
56
56
|
return self.get_datetime() is not None
|
57
57
|
|
58
|
-
self.
|
58
|
+
self._validation = {f"Invalid datetime: {self.PY_FORMAT}": validate} # pylint: disable=protected-access
|
59
59
|
|
60
60
|
def get_datetime(self) -> datetime | None:
|
61
61
|
"""Get the text from the input as a datetime object, if
|
@@ -8,12 +8,13 @@ from nicegui import ui
|
|
8
8
|
from OpenOrchestrator.database import db_util
|
9
9
|
from OpenOrchestrator.database.constants import Constant
|
10
10
|
from OpenOrchestrator.orchestrator.popups.generic_popups import question_popup
|
11
|
+
from OpenOrchestrator.orchestrator import test_helper
|
11
12
|
|
12
13
|
if TYPE_CHECKING:
|
13
14
|
from OpenOrchestrator.orchestrator.tabs.constants_tab import ConstantTab
|
14
15
|
|
15
16
|
|
16
|
-
# pylint: disable-next=too-few-public-methods
|
17
|
+
# pylint: disable-next=too-few-public-methods, too-many-instance-attributes
|
17
18
|
class ConstantPopup():
|
18
19
|
"""A popup for creating/updating queue triggers."""
|
19
20
|
def __init__(self, constant_tab: ConstantTab, constant: Constant | None = None):
|
@@ -34,19 +35,20 @@ class ConstantPopup():
|
|
34
35
|
self.value_input = ui.input("Constant Value").classes("w-full")
|
35
36
|
|
36
37
|
with ui.row():
|
37
|
-
ui.button(button_text, on_click=self._create_constant)
|
38
|
-
ui.button("Cancel", on_click=self.dialog.close)
|
38
|
+
self.save_button = ui.button(button_text, on_click=self._create_constant)
|
39
|
+
self.cancel_button = ui.button("Cancel", on_click=self.dialog.close)
|
39
40
|
|
40
41
|
if constant:
|
41
|
-
ui.button("Delete", color='red', on_click=self._delete_constant)
|
42
|
+
self.delete_button = ui.button("Delete", color='red', on_click=self._delete_constant)
|
42
43
|
|
43
44
|
self._define_validation()
|
44
45
|
self._pre_populate()
|
46
|
+
test_helper.set_automation_ids(self, "constant_popup")
|
45
47
|
|
46
48
|
def _define_validation(self):
|
47
49
|
"""Define validation rules for input elements."""
|
48
|
-
self.name_input.
|
49
|
-
self.value_input.
|
50
|
+
self.name_input._validation = {"Please enter a name": bool} # pylint: disable=protected-access
|
51
|
+
self.value_input._validation = {"Please enter a value": bool} # pylint: disable=protected-access
|
50
52
|
|
51
53
|
def _pre_populate(self):
|
52
54
|
"""Pre populate the inputs with an existing constant."""
|
@@ -8,12 +8,13 @@ from nicegui import ui
|
|
8
8
|
from OpenOrchestrator.database import db_util
|
9
9
|
from OpenOrchestrator.database.constants import Credential
|
10
10
|
from OpenOrchestrator.orchestrator.popups.generic_popups import question_popup
|
11
|
+
from OpenOrchestrator.orchestrator import test_helper
|
11
12
|
|
12
13
|
if TYPE_CHECKING:
|
13
14
|
from OpenOrchestrator.orchestrator.tabs.constants_tab import ConstantTab
|
14
15
|
|
15
16
|
|
16
|
-
# pylint: disable-next=too-few-public-methods
|
17
|
+
# pylint: disable-next=too-few-public-methods, too-many-instance-attributes
|
17
18
|
class CredentialPopup():
|
18
19
|
"""A popup for creating/updating queue triggers."""
|
19
20
|
def __init__(self, constant_tab: ConstantTab, credential: Credential | None = None):
|
@@ -36,20 +37,21 @@ class CredentialPopup():
|
|
36
37
|
self.password_input = ui.input("Password").classes("w-full")
|
37
38
|
|
38
39
|
with ui.row():
|
39
|
-
ui.button(button_text, on_click=self._save_credential)
|
40
|
-
ui.button("Cancel", on_click=self.dialog.close)
|
40
|
+
self.save_button = ui.button(button_text, on_click=self._save_credential)
|
41
|
+
self.cancel_button = ui.button("Cancel", on_click=self.dialog.close)
|
41
42
|
|
42
43
|
if credential:
|
43
|
-
ui.button("Delete", color='red', on_click=self._delete_credential)
|
44
|
+
self.delete_button = ui.button("Delete", color='red', on_click=self._delete_credential)
|
44
45
|
|
45
46
|
self._define_validation()
|
46
47
|
self._pre_populate()
|
48
|
+
test_helper.set_automation_ids(self, "credential_popup")
|
47
49
|
|
48
50
|
def _define_validation(self):
|
49
51
|
"""Define validation functions for ui elements."""
|
50
|
-
self.name_input.
|
51
|
-
self.username_input.
|
52
|
-
self.password_input.
|
52
|
+
self.name_input._validation = {"Please enter a name": bool} # pylint: disable=protected-access
|
53
|
+
self.username_input._validation = {"Please enter a username": bool} # pylint: disable=protected-access
|
54
|
+
self.password_input._validation = {"Please enter a password": bool} # pylint: disable=protected-access
|
53
55
|
|
54
56
|
def _pre_populate(self):
|
55
57
|
"""Pre populate the inputs with an existing credential."""
|
@@ -76,7 +78,7 @@ class CredentialPopup():
|
|
76
78
|
else:
|
77
79
|
# Check if credential already exists
|
78
80
|
try:
|
79
|
-
db_util.get_credential(name)
|
81
|
+
db_util.get_credential(name, decrypt_password=False)
|
80
82
|
exists = True
|
81
83
|
except ValueError:
|
82
84
|
exists = False
|
@@ -21,7 +21,20 @@ async def question_popup(question: str, option1: str, option2: str, color1: str
|
|
21
21
|
with ui.dialog(value=True).props('persistent') as dialog, ui.card():
|
22
22
|
ui.label(question).classes("text-lg")
|
23
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)
|
24
|
+
ui.button(option1, on_click=lambda e: dialog.submit(True), color=color1).props("auto-id=popup_option1_button")
|
25
|
+
ui.button(option2, on_click=lambda e: dialog.submit(False), color=color2).props("auto-id=popup_option2_button")
|
26
26
|
|
27
27
|
return await dialog
|
28
|
+
|
29
|
+
|
30
|
+
async def info_popup(text: str) -> None:
|
31
|
+
"""Show a generic popup with the given text.
|
32
|
+
|
33
|
+
Args:
|
34
|
+
text: The text to display in the popup.
|
35
|
+
"""
|
36
|
+
with ui.dialog(value=True).props('persistent') as dialog, ui.card():
|
37
|
+
ui.label(text).classes("text-lg")
|
38
|
+
ui.button("Close", on_click=lambda e: dialog.submit(True)).props("auto-id=popup_button")
|
39
|
+
|
40
|
+
await dialog
|
@@ -3,6 +3,7 @@
|
|
3
3
|
from __future__ import annotations
|
4
4
|
from typing import TYPE_CHECKING
|
5
5
|
from datetime import datetime
|
6
|
+
import json
|
6
7
|
|
7
8
|
from nicegui import ui
|
8
9
|
from cronsim import CronSim, CronSimError
|
@@ -11,6 +12,7 @@ from OpenOrchestrator.orchestrator.datetime_input import DatetimeInput
|
|
11
12
|
from OpenOrchestrator.database import db_util
|
12
13
|
from OpenOrchestrator.database.triggers import Trigger, TriggerStatus, TriggerType, ScheduledTrigger, SingleTrigger, QueueTrigger
|
13
14
|
from OpenOrchestrator.orchestrator.popups import generic_popups
|
15
|
+
from OpenOrchestrator.orchestrator import test_helper
|
14
16
|
|
15
17
|
if TYPE_CHECKING:
|
16
18
|
from OpenOrchestrator.orchestrator.tabs.trigger_tab import TriggerTab
|
@@ -48,30 +50,33 @@ class TriggerPopup():
|
|
48
50
|
self.path_input = ui.input("Process Path").classes("w-full")
|
49
51
|
self.git_check = ui.checkbox("Is path a Git Repo?")
|
50
52
|
self.args_input = ui.input("Process Arguments").classes("w-full")
|
51
|
-
self.blocking_check = ui.checkbox("Is process blocking?")
|
53
|
+
self.blocking_check = ui.checkbox(text="Is process blocking?", value=True)
|
54
|
+
self.priority_input = ui.number("Priority", value=0, precision=0, format="%.0f")
|
55
|
+
self.whitelist_input = ui.input_chips("Scheduler whitelist").classes("w-full")
|
52
56
|
|
53
57
|
if trigger:
|
54
58
|
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')
|
59
|
+
self.enable_button = ui.button("Enable", on_click=self._enable_trigger)
|
60
|
+
self.disable_button = ui.button("Disable", on_click=self._disable_trigger)
|
61
|
+
self.delete_button = ui.button("Delete", on_click=self._delete_trigger, color='red')
|
58
62
|
else:
|
59
63
|
# Dialog should only be persistent when a new trigger is being created
|
60
64
|
self.dialog.props('persistent')
|
61
65
|
|
62
66
|
with ui.row():
|
63
|
-
ui.button("Save", on_click=self._create_trigger)
|
64
|
-
ui.button("Cancel", on_click=self.dialog.close)
|
67
|
+
self.save_button = ui.button("Save", on_click=self._create_trigger)
|
68
|
+
self.cancel_button = ui.button("Cancel", on_click=self.dialog.close)
|
65
69
|
|
66
70
|
self._disable_unused()
|
67
71
|
self._define_validation()
|
68
72
|
self._pre_populate()
|
73
|
+
test_helper.set_automation_ids(self, "trigger_popup")
|
69
74
|
|
70
75
|
def _define_validation(self):
|
71
|
-
self.trigger_input.
|
72
|
-
self.name_input.
|
73
|
-
self.path_input.
|
74
|
-
self.queue_input.
|
76
|
+
self.trigger_input._validation = {"Please enter a trigger name": bool} # pylint: disable=protected-access
|
77
|
+
self.name_input._validation = {"Please enter a process name": bool} # pylint: disable=protected-access
|
78
|
+
self.path_input._validation = {"Please enter a process path": bool} # pylint: disable=protected-access
|
79
|
+
self.queue_input._validation = {"Please enter a queue name": bool} # pylint: disable=protected-access
|
75
80
|
|
76
81
|
def validate_cron(value: str):
|
77
82
|
try:
|
@@ -80,7 +85,7 @@ class TriggerPopup():
|
|
80
85
|
except CronSimError:
|
81
86
|
return False
|
82
87
|
|
83
|
-
self.cron_input.
|
88
|
+
self.cron_input._validation = {"Invalid cron expression": validate_cron} # pylint: disable=protected-access
|
84
89
|
|
85
90
|
def _pre_populate(self):
|
86
91
|
"""Populate the form with values from an existing trigger"""
|
@@ -93,6 +98,8 @@ class TriggerPopup():
|
|
93
98
|
self.args_input.value = self.trigger.process_args
|
94
99
|
self.git_check.value = self.trigger.is_git_repo
|
95
100
|
self.blocking_check.value = self.trigger.is_blocking
|
101
|
+
self.priority_input.value = self.trigger.priority
|
102
|
+
self.whitelist_input.value = json.loads(self.trigger.scheduler_whitelist)
|
96
103
|
|
97
104
|
if isinstance(self.trigger, ScheduledTrigger):
|
98
105
|
self.cron_input.value = self.trigger.cron_expr
|
@@ -163,15 +170,17 @@ class TriggerPopup():
|
|
163
170
|
args = self.args_input.value
|
164
171
|
is_git = self.git_check.value
|
165
172
|
is_blocking = self.blocking_check.value
|
173
|
+
priority = self.priority_input.value
|
174
|
+
whitelist = self.whitelist_input.value
|
166
175
|
|
167
176
|
if self.trigger is None:
|
168
177
|
# Create new trigger in database
|
169
178
|
if self.trigger_type == TriggerType.SINGLE:
|
170
|
-
db_util.create_single_trigger(trigger_name, process_name, next_run, path, args, is_git, is_blocking)
|
179
|
+
db_util.create_single_trigger(trigger_name, process_name, next_run, path, args, is_git, is_blocking, priority, whitelist)
|
171
180
|
elif self.trigger_type == TriggerType.SCHEDULED:
|
172
|
-
db_util.create_scheduled_trigger(trigger_name, process_name, cron_expr, next_run, path, args, is_git, is_blocking)
|
181
|
+
db_util.create_scheduled_trigger(trigger_name, process_name, cron_expr, next_run, path, args, is_git, is_blocking, priority, whitelist)
|
173
182
|
elif self.trigger_type == TriggerType.QUEUE:
|
174
|
-
db_util.create_queue_trigger(trigger_name, process_name, queue_name, path, args, is_git, is_blocking, min_batch_size)
|
183
|
+
db_util.create_queue_trigger(trigger_name, process_name, queue_name, path, args, is_git, is_blocking, min_batch_size, priority, whitelist)
|
175
184
|
|
176
185
|
ui.notify("Trigger created", type='positive')
|
177
186
|
else:
|
@@ -182,6 +191,8 @@ class TriggerPopup():
|
|
182
191
|
self.trigger.process_args = args
|
183
192
|
self.trigger.is_git_repo = is_git
|
184
193
|
self.trigger.is_blocking = is_blocking
|
194
|
+
self.trigger.priority = priority
|
195
|
+
self.trigger.scheduler_whitelist = json.dumps(whitelist)
|
185
196
|
|
186
197
|
if isinstance(self.trigger, SingleTrigger):
|
187
198
|
self.trigger.next_run = next_run
|
@@ -6,6 +6,7 @@ from nicegui import ui
|
|
6
6
|
from OpenOrchestrator.database import db_util
|
7
7
|
from OpenOrchestrator.orchestrator.popups.constant_popup import ConstantPopup
|
8
8
|
from OpenOrchestrator.orchestrator.popups.credential_popup import CredentialPopup
|
9
|
+
from OpenOrchestrator.orchestrator import test_helper
|
9
10
|
|
10
11
|
CONSTANT_COLUMNS = ("Constant Name", "Value", "Last Changed")
|
11
12
|
CREDENTIAL_COLUMNS = ("Credential Name", "Username", "Password", "Last Changed")
|
@@ -16,8 +17,8 @@ class ConstantTab():
|
|
16
17
|
def __init__(self, tab_name: str) -> None:
|
17
18
|
with ui.tab_panel(tab_name):
|
18
19
|
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))
|
20
|
+
self.constant_button = ui.button("New Constant", icon='add', on_click=lambda e: ConstantPopup(self))
|
21
|
+
self.credential_button = ui.button("New Credential", icon='add', on_click=lambda e: CredentialPopup(self))
|
21
22
|
|
22
23
|
columns = [{'name': label, 'label': label, 'field': label, 'align': 'left', 'sortable': True} for label in CONSTANT_COLUMNS]
|
23
24
|
self.constants_table = ui.table(title="Constants", columns=columns, rows=[], row_key='Constant Name', pagination=10).classes("w-full")
|
@@ -27,6 +28,8 @@ class ConstantTab():
|
|
27
28
|
self.credentials_table = ui.table(title="Credentials", columns=columns, rows=[], row_key='Credential Name', pagination=10).classes("w-full")
|
28
29
|
self.credentials_table.on('rowClick', self.row_click_credential)
|
29
30
|
|
31
|
+
test_helper.set_automation_ids(self, "constants_tab")
|
32
|
+
|
30
33
|
def row_click_constant(self, event):
|
31
34
|
"""Callback for when a row is clicked in the table."""
|
32
35
|
row = event.args[1]
|
@@ -6,6 +6,7 @@ from nicegui import ui
|
|
6
6
|
from OpenOrchestrator.database import db_util
|
7
7
|
from OpenOrchestrator.database.logs import LogLevel
|
8
8
|
from OpenOrchestrator.orchestrator.datetime_input import DatetimeInput
|
9
|
+
from OpenOrchestrator.orchestrator import test_helper
|
9
10
|
|
10
11
|
|
11
12
|
COLUMNS = [
|
@@ -32,6 +33,8 @@ class LoggingTab():
|
|
32
33
|
self.logs_table = ui.table(title="Logs", columns=COLUMNS, rows=[], row_key='ID', pagination=50).classes("w-full")
|
33
34
|
self.logs_table.on("rowClick", self._row_click)
|
34
35
|
|
36
|
+
test_helper.set_automation_ids(self, "logs_tab")
|
37
|
+
|
35
38
|
def update(self):
|
36
39
|
"""Update the logs table and Process input list"""
|
37
40
|
self._update_table()
|
@@ -6,6 +6,7 @@ from nicegui import ui
|
|
6
6
|
from OpenOrchestrator.database import db_util
|
7
7
|
from OpenOrchestrator.database.queues import QueueStatus
|
8
8
|
from OpenOrchestrator.orchestrator.datetime_input import DatetimeInput
|
9
|
+
from OpenOrchestrator.orchestrator import test_helper
|
9
10
|
|
10
11
|
|
11
12
|
QUEUE_COLUMNS = [
|
@@ -37,6 +38,7 @@ class QueueTab():
|
|
37
38
|
with ui.tab_panel(tab_name):
|
38
39
|
self.queue_table = ui.table(title="Queues", columns=QUEUE_COLUMNS, rows=[], row_key='Queue Name', pagination={'rowsPerPage': 50, 'sortBy': 'Queue Name'}).classes("w-full")
|
39
40
|
self.queue_table.on("rowClick", self._row_click)
|
41
|
+
test_helper.set_automation_ids(self, "queues_tab")
|
40
42
|
|
41
43
|
def update(self):
|
42
44
|
"""Update the queue table with data from the database."""
|
@@ -85,11 +87,12 @@ class QueuePopup():
|
|
85
87
|
ui.switch("Dense", on_change=lambda e: self._dense_table(e.value))
|
86
88
|
self._create_column_filter()
|
87
89
|
ui.button(icon='refresh', on_click=self._update)
|
88
|
-
ui.button(icon="close", on_click=dialog.close)
|
90
|
+
self.close_button = ui.button(icon="close", on_click=dialog.close)
|
89
91
|
with ui.scroll_area().classes("h-full"):
|
90
92
|
self.table = ui.table(columns=ELEMENT_COLUMNS, rows=[], row_key='ID', title=queue_name, pagination=100).classes("w-full")
|
91
93
|
|
92
94
|
self._update()
|
95
|
+
test_helper.set_automation_ids(self, "queue_popup")
|
93
96
|
|
94
97
|
def _dense_table(self, value: bool):
|
95
98
|
"""Change if the table is dense or not."""
|
@@ -0,0 +1,45 @@
|
|
1
|
+
"""This module is responsible for the layout and functionality of the Schedulers tab
|
2
|
+
in Orchestrator."""
|
3
|
+
|
4
|
+
from nicegui import ui
|
5
|
+
|
6
|
+
from OpenOrchestrator.database import db_util
|
7
|
+
from OpenOrchestrator.orchestrator import test_helper
|
8
|
+
|
9
|
+
COLUMNS = [
|
10
|
+
{'name': "machine_name", 'label': "Machine Name", 'field': "Machine Name", 'align': 'left', 'sortable': True},
|
11
|
+
{'name': "last_connection", 'label': "Last Connection", 'field': "Last Connection", 'align': 'left', 'sortable': True},
|
12
|
+
{'name': "latest_trigger", 'label': "Latest Trigger", 'field': "Latest Trigger", 'align': 'left', 'sortable': True},
|
13
|
+
{'name': "latest_trigger_time", 'label': "Latest Trigger Time", 'field': "Latest Trigger Time", 'align': 'left', 'sortable': True},
|
14
|
+
]
|
15
|
+
|
16
|
+
|
17
|
+
class SchedulerTab():
|
18
|
+
"""A class for the scheduler tab."""
|
19
|
+
def __init__(self, tab_name: str) -> None:
|
20
|
+
with ui.tab_panel(tab_name):
|
21
|
+
self.schedulers_table = ui.table(title="Schedulers", columns=COLUMNS, rows=[], row_key='Machine Name', pagination=50).classes("w-full")
|
22
|
+
self.add_column_colors()
|
23
|
+
test_helper.set_automation_ids(self, "schedulers_tab")
|
24
|
+
|
25
|
+
def update(self):
|
26
|
+
"""Updates the tables on the tab."""
|
27
|
+
schedulers = db_util.get_schedulers()
|
28
|
+
self.schedulers_table.rows = [s.to_row_dict() for s in schedulers]
|
29
|
+
self.schedulers_table.update()
|
30
|
+
|
31
|
+
def add_column_colors(self):
|
32
|
+
"""Add red coloring to the scheduler if more than a minute has passed since last ping."""
|
33
|
+
self.schedulers_table.add_slot(
|
34
|
+
"body-cell-last_connection",
|
35
|
+
'''
|
36
|
+
<q-td key="last_connection" :props="props">
|
37
|
+
<q-badge v-if="(new Date() - 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))) > 60 * 1000" color='red'>
|
38
|
+
{{props.value}}
|
39
|
+
</q-badge>
|
40
|
+
<p v-else>
|
41
|
+
{{props.value}}
|
42
|
+
</p>
|
43
|
+
</q-td>
|
44
|
+
'''
|
45
|
+
)
|
@@ -3,8 +3,8 @@ in Orchestrator."""
|
|
3
3
|
|
4
4
|
from nicegui import ui
|
5
5
|
|
6
|
-
from OpenOrchestrator.database import db_util
|
7
6
|
from OpenOrchestrator.common.connection_frame import ConnectionFrame
|
7
|
+
from OpenOrchestrator.orchestrator import test_helper
|
8
8
|
|
9
9
|
|
10
10
|
# pylint: disable-next=too-few-public-methods
|
@@ -15,8 +15,5 @@ class SettingsTab():
|
|
15
15
|
conn_frame = ConnectionFrame()
|
16
16
|
with ui.row().classes("w-full"):
|
17
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
18
|
|
20
|
-
|
21
|
-
db_util.initialize_database()
|
22
|
-
ui.notify("Database initialized!", type='positive')
|
19
|
+
test_helper.set_automation_ids(self, "settings_tab")
|
@@ -6,9 +6,11 @@ from nicegui import ui
|
|
6
6
|
from OpenOrchestrator.database import db_util
|
7
7
|
from OpenOrchestrator.database.triggers import SingleTrigger, ScheduledTrigger, QueueTrigger, TriggerType
|
8
8
|
from OpenOrchestrator.orchestrator.popups.trigger_popup import TriggerPopup
|
9
|
+
from OpenOrchestrator.orchestrator import test_helper
|
9
10
|
|
10
11
|
COLUMNS = [
|
11
12
|
{'name': "Trigger Name", 'label': "Trigger Name", 'field': "Trigger Name", 'align': 'left', 'sortable': True},
|
13
|
+
{'name': "Priority", 'label': "Priority", 'field': "Priority", 'align': 'left', 'sortable': True, ':sort': '(a, b, rowA, rowB) => b-a'},
|
12
14
|
{'name': "Type", 'label': "Type", 'field': "Type", 'align': 'left', 'sortable': True},
|
13
15
|
{'name': "Status", 'label': "Status", 'field': "Status", 'align': 'left', 'sortable': True},
|
14
16
|
{'name': "Process Name", 'label': "Process Name", 'field': "Process Name", 'align': 'left', 'sortable': True},
|
@@ -24,14 +26,16 @@ class TriggerTab():
|
|
24
26
|
def __init__(self, tab_name: str) -> None:
|
25
27
|
with ui.tab_panel(tab_name):
|
26
28
|
with ui.row():
|
27
|
-
ui.button("New Single Trigger", icon="add", on_click=lambda e: TriggerPopup(self, TriggerType.SINGLE))
|
28
|
-
ui.button("New Scheduled Trigger", icon="add", on_click=lambda e: TriggerPopup(self, TriggerType.SCHEDULED))
|
29
|
-
ui.button("New Queue Trigger", icon="add", on_click=lambda e: TriggerPopup(self, TriggerType.QUEUE))
|
29
|
+
self.single_button = ui.button("New Single Trigger", icon="add", on_click=lambda e: TriggerPopup(self, TriggerType.SINGLE))
|
30
|
+
self.scheduled_button = ui.button("New Scheduled Trigger", icon="add", on_click=lambda e: TriggerPopup(self, TriggerType.SCHEDULED))
|
31
|
+
self.queue_button = ui.button("New Queue Trigger", icon="add", on_click=lambda e: TriggerPopup(self, TriggerType.QUEUE))
|
30
32
|
|
31
|
-
self.trigger_table = ui.table(COLUMNS, [], title="Triggers", pagination={'rowsPerPage': 50, 'sortBy': 'Trigger Name'}, row_key='ID').classes("w-full")
|
33
|
+
self.trigger_table = ui.table(columns=COLUMNS, rows=[], title="Triggers", pagination={'rowsPerPage': 50, 'sortBy': 'Trigger Name'}, row_key='ID').classes("w-full")
|
32
34
|
self.trigger_table.on('rowClick', self._row_click)
|
33
35
|
self.add_column_colors()
|
34
36
|
|
37
|
+
test_helper.set_automation_ids(self, "trigger_tab")
|
38
|
+
|
35
39
|
def _row_click(self, event):
|
36
40
|
"""Callback for when a row is clicked in the table."""
|
37
41
|
row = event.args[1]
|
@@ -58,7 +62,7 @@ class TriggerTab():
|
|
58
62
|
"body-cell-Status",
|
59
63
|
'''
|
60
64
|
<q-td key="Status" :props="props">
|
61
|
-
<q-badge v-if="{Running: 'green', Pausing: 'orange', Failed: 'red'}[props.value]" :color="{Running: 'green', Pausing: 'orange', Failed: 'red'}[props.value]">
|
65
|
+
<q-badge v-if="{Running: 'green', Pausing: 'orange', Paused: 'orange', Failed: 'red'}[props.value]" :color="{Running: 'green', Pausing: 'orange', Paused: 'orange', Failed: 'red'}[props.value]">
|
62
66
|
{{props.value}}
|
63
67
|
</q-badge>
|
64
68
|
<p v-else>
|
@@ -0,0 +1,17 @@
|
|
1
|
+
"""This module contains helper functions for automated tests."""
|
2
|
+
|
3
|
+
from nicegui import ui
|
4
|
+
|
5
|
+
|
6
|
+
def set_automation_ids(container, prefix: str):
|
7
|
+
"""Set automation ids for automated tests.
|
8
|
+
Iterate though all attributes of the container and set 'auto-id'
|
9
|
+
on any ui-elements.
|
10
|
+
|
11
|
+
Args:
|
12
|
+
container: The object that contains a number of ui-elements.
|
13
|
+
prefix: The prefix to add to the automation ids for all elements.
|
14
|
+
"""
|
15
|
+
for name, obj in container.__dict__.items():
|
16
|
+
if isinstance(obj, (ui.button, ui.input, ui.checkbox, ui.number, ui.table, ui.tab, ui.select, ui.input_chips)):
|
17
|
+
obj.props(f"auto-id={prefix}_{name}")
|
@@ -10,6 +10,7 @@ from OpenOrchestrator.database import db_util
|
|
10
10
|
from OpenOrchestrator.database.queues import QueueElement, QueueStatus
|
11
11
|
from OpenOrchestrator.database.logs import LogLevel
|
12
12
|
from OpenOrchestrator.database.constants import Constant, Credential
|
13
|
+
from OpenOrchestrator.database.triggers import TriggerStatus
|
13
14
|
|
14
15
|
|
15
16
|
class OrchestratorConnection:
|
@@ -19,16 +20,18 @@ class OrchestratorConnection:
|
|
19
20
|
to instead of initializing the object manually.
|
20
21
|
"""
|
21
22
|
|
22
|
-
def __init__(self, process_name: str, connection_string: str, crypto_key: str, process_arguments: str):
|
23
|
+
def __init__(self, process_name: str, connection_string: str, crypto_key: str, process_arguments: str, trigger_id: str):
|
23
24
|
"""
|
24
25
|
Args:
|
25
26
|
process_name: A human friendly tag to identify the process.
|
26
|
-
connection_string: An ODBC connection to the OpenOrchestrator database
|
27
|
-
crypto_key: Secret key for decrypting database content
|
27
|
+
connection_string: An ODBC connection to the OpenOrchestrator database.
|
28
|
+
crypto_key: Secret key for decrypting database content.
|
28
29
|
process_arguments (optional): Arguments for the controlling how the process should run.
|
30
|
+
trigger_id: ID of trigger used to start this process.
|
29
31
|
"""
|
30
32
|
self.process_name = process_name
|
31
33
|
self.process_arguments = process_arguments
|
34
|
+
self.trigger_id = trigger_id
|
32
35
|
crypto_util.set_key(crypto_key)
|
33
36
|
db_util.connect(connection_string)
|
34
37
|
|
@@ -188,6 +191,19 @@ class OrchestratorConnection:
|
|
188
191
|
"""
|
189
192
|
db_util.delete_queue_element(element_id)
|
190
193
|
|
194
|
+
def is_trigger_pausing(self) -> bool:
|
195
|
+
"""Check if my trigger is pausing.
|
196
|
+
|
197
|
+
Returns:
|
198
|
+
bool: Whether or not the trigger used to start this process is pausing.
|
199
|
+
"""
|
200
|
+
my_trigger = db_util.get_trigger(self.trigger_id)
|
201
|
+
return my_trigger.process_status in (TriggerStatus.PAUSING, TriggerStatus.PAUSED)
|
202
|
+
|
203
|
+
def pause_my_trigger(self) -> None:
|
204
|
+
"""Pause the trigger used to start this process."""
|
205
|
+
db_util.set_trigger_status(self.trigger_id, TriggerStatus.PAUSING)
|
206
|
+
|
191
207
|
@classmethod
|
192
208
|
def create_connection_from_args(cls):
|
193
209
|
"""Create a Connection object using the arguments passed to sys.argv.
|
@@ -196,4 +212,5 @@ class OrchestratorConnection:
|
|
196
212
|
connection_string = sys.argv[2]
|
197
213
|
crypto_key = sys.argv[3]
|
198
214
|
process_arguments = sys.argv[4]
|
199
|
-
|
215
|
+
trigger_id = sys.argv[5]
|
216
|
+
return OrchestratorConnection(process_name, connection_string, crypto_key, process_arguments, trigger_id)
|
@@ -3,6 +3,7 @@ that when created starts the application."""
|
|
3
3
|
|
4
4
|
import tkinter
|
5
5
|
from tkinter import ttk, messagebox
|
6
|
+
|
6
7
|
from OpenOrchestrator.scheduler import settings_tab, run_tab
|
7
8
|
|
8
9
|
|
@@ -27,10 +28,10 @@ class Application(tkinter.Tk):
|
|
27
28
|
notebook.pack(expand=True, fill='both')
|
28
29
|
|
29
30
|
run_tab_ = run_tab.RunTab(notebook, self)
|
30
|
-
settings_tab_ = settings_tab.
|
31
|
+
self.settings_tab_ = settings_tab.SettingsTab(notebook)
|
31
32
|
|
32
33
|
notebook.add(run_tab_, text='Run')
|
33
|
-
notebook.add(settings_tab_, text="Settings")
|
34
|
+
notebook.add(self.settings_tab_, text="Settings")
|
34
35
|
|
35
36
|
notebook.select(1)
|
36
37
|
|
@@ -12,7 +12,7 @@ from sqlalchemy import exc as alc_exc
|
|
12
12
|
|
13
13
|
from OpenOrchestrator.common import crypto_util
|
14
14
|
from OpenOrchestrator.database import db_util
|
15
|
-
from OpenOrchestrator.scheduler import runner
|
15
|
+
from OpenOrchestrator.scheduler import runner, util
|
16
16
|
|
17
17
|
if TYPE_CHECKING:
|
18
18
|
from OpenOrchestrator.scheduler.application import Application
|
@@ -111,13 +111,15 @@ def loop(app: Application) -> None:
|
|
111
111
|
app: The Scheduler Application object.
|
112
112
|
"""
|
113
113
|
try:
|
114
|
+
send_ping_to_orchestrator()
|
115
|
+
|
114
116
|
check_heartbeats(app)
|
115
117
|
|
116
118
|
if app.running:
|
117
119
|
check_triggers(app)
|
118
120
|
|
119
|
-
except alc_exc.OperationalError:
|
120
|
-
print("Couldn't connect to database.")
|
121
|
+
except (alc_exc.OperationalError, alc_exc.ProgrammingError) as e:
|
122
|
+
print(f"Couldn't connect to database. {e}")
|
121
123
|
|
122
124
|
if len(app.running_jobs) == 0:
|
123
125
|
print("Doing cleanup...")
|
@@ -169,7 +171,15 @@ def check_triggers(app: Application) -> None:
|
|
169
171
|
# Check triggers
|
170
172
|
if not blocking:
|
171
173
|
print('Checking triggers...')
|
172
|
-
|
174
|
+
trigger = runner.poll_triggers(app)
|
175
|
+
|
176
|
+
if trigger:
|
177
|
+
job = runner.run_trigger(trigger)
|
178
|
+
|
179
|
+
if job:
|
180
|
+
app.running_jobs.append(job)
|
181
|
+
|
173
182
|
|
174
|
-
|
175
|
-
|
183
|
+
def send_ping_to_orchestrator():
|
184
|
+
"""Send a ping to the connected Orchestrator with the Scheduler application's name."""
|
185
|
+
db_util.send_ping_from_scheduler(util.get_scheduler_name())
|