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
|
@@ -8,6 +8,8 @@ import uuid
|
|
|
8
8
|
from sqlalchemy import String, ForeignKey, Engine
|
|
9
9
|
from sqlalchemy.orm import Mapped, DeclarativeBase, mapped_column
|
|
10
10
|
|
|
11
|
+
from OpenOrchestrator.common import datetime_util
|
|
12
|
+
|
|
11
13
|
# All classes in this module are effectively dataclasses without methods.
|
|
12
14
|
# pylint: disable=too-few-public-methods
|
|
13
15
|
|
|
@@ -55,6 +57,17 @@ class Trigger(Base):
|
|
|
55
57
|
def __repr__(self) -> str:
|
|
56
58
|
return f"{self.trigger_name}: {self.type.value}"
|
|
57
59
|
|
|
60
|
+
def to_row_dict(self) -> dict[str, str]:
|
|
61
|
+
"""Convert trigger to a row dictionary for display in a table."""
|
|
62
|
+
return {
|
|
63
|
+
"Trigger Name": self.trigger_name,
|
|
64
|
+
"Type": self.type.value,
|
|
65
|
+
"Status": self.process_status.value,
|
|
66
|
+
"Process Name": self.process_name,
|
|
67
|
+
"Last Run": datetime_util.format_datetime(self.last_run, "Never"),
|
|
68
|
+
"ID": str(self.id)
|
|
69
|
+
}
|
|
70
|
+
|
|
58
71
|
|
|
59
72
|
class SingleTrigger(Trigger):
|
|
60
73
|
"""A class representing single trigger objects in the ORM."""
|
|
@@ -65,24 +78,10 @@ class SingleTrigger(Trigger):
|
|
|
65
78
|
|
|
66
79
|
__mapper_args__ = {"polymorphic_identity": TriggerType.SINGLE}
|
|
67
80
|
|
|
68
|
-
def
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
tuple: A tuple of all the triggers values.
|
|
73
|
-
"""
|
|
74
|
-
return (
|
|
75
|
-
self.trigger_name,
|
|
76
|
-
self.process_status.value,
|
|
77
|
-
self.process_name,
|
|
78
|
-
self.last_run,
|
|
79
|
-
self.next_run,
|
|
80
|
-
self.process_path,
|
|
81
|
-
self.process_args,
|
|
82
|
-
self.is_git_repo,
|
|
83
|
-
self.is_blocking,
|
|
84
|
-
self.id
|
|
85
|
-
)
|
|
81
|
+
def to_row_dict(self) -> dict[str, str]:
|
|
82
|
+
row_dict = super().to_row_dict()
|
|
83
|
+
row_dict["Next Run"] = datetime_util.format_datetime(self.next_run)
|
|
84
|
+
return row_dict
|
|
86
85
|
|
|
87
86
|
|
|
88
87
|
class ScheduledTrigger(Trigger):
|
|
@@ -95,25 +94,10 @@ class ScheduledTrigger(Trigger):
|
|
|
95
94
|
|
|
96
95
|
__mapper_args__ = {"polymorphic_identity": TriggerType.SCHEDULED}
|
|
97
96
|
|
|
98
|
-
def
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
tuple: A tuple of all the triggers values.
|
|
103
|
-
"""
|
|
104
|
-
return (
|
|
105
|
-
self.trigger_name,
|
|
106
|
-
self.process_status.value,
|
|
107
|
-
self.process_name,
|
|
108
|
-
self.cron_expr,
|
|
109
|
-
self.last_run,
|
|
110
|
-
self.next_run,
|
|
111
|
-
self.process_path,
|
|
112
|
-
self.process_args,
|
|
113
|
-
self.is_git_repo,
|
|
114
|
-
self.is_blocking,
|
|
115
|
-
self.id
|
|
116
|
-
)
|
|
97
|
+
def to_row_dict(self) -> dict[str, str]:
|
|
98
|
+
row_dict = super().to_row_dict()
|
|
99
|
+
row_dict["Next Run"] = datetime_util.format_datetime(self.next_run)
|
|
100
|
+
return row_dict
|
|
117
101
|
|
|
118
102
|
|
|
119
103
|
class QueueTrigger(Trigger):
|
|
@@ -126,25 +110,10 @@ class QueueTrigger(Trigger):
|
|
|
126
110
|
|
|
127
111
|
__mapper_args__ = {"polymorphic_identity": TriggerType.QUEUE}
|
|
128
112
|
|
|
129
|
-
def
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
tuple: A tuple of all the triggers values.
|
|
134
|
-
"""
|
|
135
|
-
return (
|
|
136
|
-
self.trigger_name,
|
|
137
|
-
self.process_status.value,
|
|
138
|
-
self.process_name,
|
|
139
|
-
self.queue_name,
|
|
140
|
-
self.min_batch_size,
|
|
141
|
-
self.last_run,
|
|
142
|
-
self.process_path,
|
|
143
|
-
self.process_args,
|
|
144
|
-
self.is_git_repo,
|
|
145
|
-
self.is_blocking,
|
|
146
|
-
self.id
|
|
147
|
-
)
|
|
113
|
+
def to_row_dict(self) -> dict[str, str]:
|
|
114
|
+
row_dict = super().to_row_dict()
|
|
115
|
+
row_dict["Next Run"] = "N/A"
|
|
116
|
+
return row_dict
|
|
148
117
|
|
|
149
118
|
|
|
150
119
|
def create_tables(engine: Engine):
|
|
@@ -1,41 +1,94 @@
|
|
|
1
1
|
"""This module is the entry point for the Orchestrator app. It contains a single class
|
|
2
2
|
that when created starts the application."""
|
|
3
3
|
|
|
4
|
-
import
|
|
5
|
-
from tkinter import ttk
|
|
4
|
+
import socket
|
|
6
5
|
|
|
7
|
-
from
|
|
6
|
+
from nicegui import ui, app
|
|
8
7
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
8
|
+
from OpenOrchestrator.orchestrator.tabs.trigger_tab import TriggerTab
|
|
9
|
+
from OpenOrchestrator.orchestrator.tabs.settings_tab import SettingsTab
|
|
10
|
+
from OpenOrchestrator.orchestrator.tabs.logging_tab import LoggingTab
|
|
11
|
+
from OpenOrchestrator.orchestrator.tabs.constants_tab import ConstantTab
|
|
12
|
+
from OpenOrchestrator.orchestrator.tabs.queue_tab import QueueTab
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class Application():
|
|
16
|
+
"""The main application of Orchestrator.
|
|
17
|
+
It contains a header and the four tabs of the application.
|
|
12
18
|
"""
|
|
13
|
-
def __init__(self):
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
19
|
+
def __init__(self) -> None:
|
|
20
|
+
with ui.header():
|
|
21
|
+
with ui.tabs() as self.tabs:
|
|
22
|
+
ui.tab('Triggers')
|
|
23
|
+
ui.tab('Logs')
|
|
24
|
+
ui.tab('Constants')
|
|
25
|
+
ui.tab('Queues')
|
|
26
|
+
ui.tab('Settings')
|
|
27
|
+
|
|
28
|
+
ui.space()
|
|
29
|
+
ui.button(icon="contrast", on_click=ui.dark_mode().toggle)
|
|
30
|
+
ui.button(icon='refresh', on_click=self.update_tab)
|
|
31
|
+
|
|
32
|
+
with ui.tab_panels(self.tabs, value='Settings', on_change=self.update_tab).classes('w-full') as self.tab_panels:
|
|
33
|
+
self.t_tab = TriggerTab('Triggers')
|
|
34
|
+
self.l_tab = LoggingTab("Logs")
|
|
35
|
+
self.c_tab = ConstantTab("Constants")
|
|
36
|
+
self.q_tab = QueueTab("Queues")
|
|
37
|
+
SettingsTab('Settings')
|
|
38
|
+
|
|
39
|
+
self._define_on_close()
|
|
40
|
+
|
|
41
|
+
app.on_connect(self.update_loop)
|
|
42
|
+
app.on_disconnect(app.shutdown)
|
|
43
|
+
app.on_exception(lambda exc: ui.notify(exc, type='negative'))
|
|
44
|
+
ui.run(title="Orchestrator", favicon='🤖', native=False, port=get_free_port(), reload=False)
|
|
45
|
+
|
|
46
|
+
def update_tab(self):
|
|
47
|
+
"""Update the date in the currently selected tab."""
|
|
48
|
+
match self.tab_panels.value:
|
|
49
|
+
case 'Triggers':
|
|
50
|
+
self.t_tab.update()
|
|
51
|
+
case 'Logs':
|
|
52
|
+
self.l_tab.update()
|
|
53
|
+
case 'Constants':
|
|
54
|
+
self.c_tab.update()
|
|
55
|
+
case 'Queues':
|
|
56
|
+
self.q_tab.update()
|
|
57
|
+
|
|
58
|
+
async def update_loop(self):
|
|
59
|
+
"""Update the selected tab on a timer but only if the page is in focus."""
|
|
60
|
+
try:
|
|
61
|
+
in_focus = await ui.run_javascript("document.hasFocus()")
|
|
62
|
+
if in_focus:
|
|
63
|
+
self.update_tab()
|
|
64
|
+
except TimeoutError:
|
|
65
|
+
pass
|
|
66
|
+
|
|
67
|
+
ui.timer(10, self.update_loop, once=True)
|
|
68
|
+
|
|
69
|
+
def _define_on_close(self) -> None:
|
|
70
|
+
"""Tell the browser to ask for confirmation before leaving the page."""
|
|
71
|
+
ui.add_body_html('''
|
|
72
|
+
<script>
|
|
73
|
+
window.addEventListener("beforeunload", (event) => event.preventDefault());
|
|
74
|
+
</script>
|
|
75
|
+
''')
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def get_free_port():
|
|
79
|
+
"""Get a free port by creating a new socket and bind it
|
|
80
|
+
on port 0 allowing the os to select the port.
|
|
81
|
+
https://docs.python.org/3/library/socket.html#socket.create_connection
|
|
82
|
+
|
|
83
|
+
Returns:
|
|
84
|
+
A port number that should be free to use.
|
|
85
|
+
"""
|
|
86
|
+
with socket.socket() as sock:
|
|
87
|
+
sock.bind(("", 0))
|
|
88
|
+
port = sock.getsockname()[1]
|
|
89
|
+
|
|
90
|
+
return port
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
if __name__ in {'__main__', '__mp_main__'}:
|
|
41
94
|
Application()
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
"""This module provides an input element for entering a datetime."""
|
|
2
|
+
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
from typing import Optional, Callable, Any
|
|
5
|
+
|
|
6
|
+
from nicegui import ui
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class DatetimeInput(ui.input):
|
|
10
|
+
"""A datetime input with a button to show a date and time picker dialog."""
|
|
11
|
+
PY_FORMAT = "%d-%m-%Y %H:%M"
|
|
12
|
+
VUE_FORMAT = "DD-MM-YYYY HH:mm"
|
|
13
|
+
|
|
14
|
+
def __init__(self, label: str, on_change: Optional[Callable[..., Any]] = None, allow_empty: bool = False) -> None:
|
|
15
|
+
"""Create a new DatetimeInput.
|
|
16
|
+
|
|
17
|
+
Args:
|
|
18
|
+
label: The label for the input element.
|
|
19
|
+
on_change: A callable to execute on change. Defaults to None.
|
|
20
|
+
allow_empty: Whether to allow an empty input on validation. Defaults to False.
|
|
21
|
+
"""
|
|
22
|
+
super().__init__(label, value=None, on_change=on_change)
|
|
23
|
+
self.props("clearable")
|
|
24
|
+
|
|
25
|
+
# Define dialog
|
|
26
|
+
with ui.dialog() as self._dialog, ui.card():
|
|
27
|
+
date_input = ui.date(mask=self.VUE_FORMAT).props("today-btn first-day-of-week=1")
|
|
28
|
+
time_input = ui.time(mask=self.VUE_FORMAT).props("format24h")
|
|
29
|
+
|
|
30
|
+
# Define input
|
|
31
|
+
with self:
|
|
32
|
+
ui.button(icon="event", on_click=self._dialog.open).props("flat")
|
|
33
|
+
self.on("click", self._dialog.open)
|
|
34
|
+
|
|
35
|
+
# Bind inputs together
|
|
36
|
+
self.bind_value(date_input)
|
|
37
|
+
self.bind_value(time_input)
|
|
38
|
+
|
|
39
|
+
self._define_validation(allow_empty)
|
|
40
|
+
|
|
41
|
+
def _define_validation(self, allow_empty: bool):
|
|
42
|
+
if not allow_empty:
|
|
43
|
+
self.validation = {
|
|
44
|
+
"Please enter a datetime": bool,
|
|
45
|
+
f"Invalid datetime: {self.PY_FORMAT}": lambda v: self.get_datetime() is not None
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
else:
|
|
49
|
+
def validate(value: str):
|
|
50
|
+
if value is None:
|
|
51
|
+
return True
|
|
52
|
+
|
|
53
|
+
return self.get_datetime() is not None
|
|
54
|
+
|
|
55
|
+
self.validation = {f"Invalid datetime: {self.PY_FORMAT}": validate}
|
|
56
|
+
|
|
57
|
+
def get_datetime(self) -> datetime | None:
|
|
58
|
+
"""Get the text from the input as a datetime object, if
|
|
59
|
+
the current text in the input is valid else None.
|
|
60
|
+
|
|
61
|
+
Returns:
|
|
62
|
+
datetime: The value as a datetime object if any.
|
|
63
|
+
"""
|
|
64
|
+
try:
|
|
65
|
+
return datetime.strptime(self.value, self.PY_FORMAT)
|
|
66
|
+
except (TypeError, ValueError):
|
|
67
|
+
return None
|
|
68
|
+
|
|
69
|
+
def set_datetime(self, value: datetime) -> None:
|
|
70
|
+
"""Set the value of the datetime input.
|
|
71
|
+
|
|
72
|
+
Args:
|
|
73
|
+
value: The new datetime value.
|
|
74
|
+
"""
|
|
75
|
+
self.value = value.strftime(self.PY_FORMAT)
|
|
@@ -1,77 +1,95 @@
|
|
|
1
1
|
"""This module is responsible for the layout and functionality of the 'New constant' popup."""
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
# pylint: disable=R0801
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
from typing import TYPE_CHECKING
|
|
6
5
|
|
|
7
|
-
import
|
|
8
|
-
from tkinter import ttk, messagebox
|
|
6
|
+
from nicegui import ui
|
|
9
7
|
|
|
10
8
|
from OpenOrchestrator.database import db_util
|
|
9
|
+
from OpenOrchestrator.database.constants import Constant
|
|
10
|
+
from OpenOrchestrator.orchestrator.popups.generic_popups import question_popup
|
|
11
|
+
|
|
12
|
+
if TYPE_CHECKING:
|
|
13
|
+
from OpenOrchestrator.orchestrator.tabs.constants_tab import ConstantTab
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
# pylint: disable-next=too-few-public-methods
|
|
17
|
+
class ConstantPopup():
|
|
18
|
+
"""A popup for creating/updating queue triggers."""
|
|
19
|
+
def __init__(self, constant_tab: ConstantTab, constant: Constant = None):
|
|
20
|
+
"""Create a new popup.
|
|
21
|
+
If a constant is given it will be updated instead of creating a new constant.
|
|
22
|
+
|
|
23
|
+
Args:
|
|
24
|
+
constant: The constant to update if any.
|
|
25
|
+
"""
|
|
26
|
+
self.constant_tab = constant_tab
|
|
27
|
+
self.constant = constant
|
|
28
|
+
title = 'Update Constant' if constant else 'New Constant'
|
|
29
|
+
button_text = "Update" if constant else "Create"
|
|
30
|
+
|
|
31
|
+
with ui.dialog(value=True).props('persistent') as self.dialog, ui.card().classes('w-full'):
|
|
32
|
+
ui.label(title).classes("text-xl")
|
|
33
|
+
self.name_input = ui.input("Constant Name").classes("w-full")
|
|
34
|
+
self.value_input = ui.input("Constant Value").classes("w-full")
|
|
35
|
+
|
|
36
|
+
with ui.row():
|
|
37
|
+
ui.button(button_text, on_click=self._create_constant)
|
|
38
|
+
ui.button("Cancel", on_click=self.dialog.close)
|
|
39
|
+
|
|
40
|
+
if constant:
|
|
41
|
+
ui.button("Delete", color='red', on_click=self._delete_constant)
|
|
42
|
+
|
|
43
|
+
self._define_validation()
|
|
44
|
+
|
|
45
|
+
if constant:
|
|
46
|
+
self._pre_populate()
|
|
47
|
+
|
|
48
|
+
def _define_validation(self):
|
|
49
|
+
"""Define validation rules for input elements."""
|
|
50
|
+
self.name_input.validation = {"Please enter a name": bool}
|
|
51
|
+
self.value_input.validation = {"Please enter a value": bool}
|
|
52
|
+
|
|
53
|
+
def _pre_populate(self):
|
|
54
|
+
"""Pre populate the inputs with an existing constant."""
|
|
55
|
+
self.name_input.value = self.constant.name
|
|
56
|
+
self.name_input.disable()
|
|
57
|
+
self.value_input.value = self.constant.value
|
|
58
|
+
|
|
59
|
+
def _create_constant(self):
|
|
60
|
+
"""Creates a new constant in the database using the data from the
|
|
61
|
+
UI.
|
|
62
|
+
"""
|
|
63
|
+
self.name_input.validate()
|
|
64
|
+
self.value_input.validate()
|
|
65
|
+
|
|
66
|
+
if self.name_input.error or self.value_input.error:
|
|
67
|
+
return
|
|
68
|
+
|
|
69
|
+
name = self.name_input.value
|
|
70
|
+
value = self.value_input.value
|
|
11
71
|
|
|
12
|
-
|
|
13
|
-
"""Creates and shows a popup to create a new constant.
|
|
14
|
-
|
|
15
|
-
Returns:
|
|
16
|
-
tkinter.TopLevel: The created Toplevel object (Popup Window).
|
|
17
|
-
"""
|
|
18
|
-
window = tkinter.Toplevel()
|
|
19
|
-
window.grab_set()
|
|
20
|
-
window.title("New Constant")
|
|
21
|
-
window.geometry("300x300")
|
|
22
|
-
|
|
23
|
-
ttk.Label(window, text="Name:").pack()
|
|
24
|
-
name_entry = ttk.Entry(window)
|
|
25
|
-
name_entry.pack()
|
|
26
|
-
|
|
27
|
-
ttk.Label(window, text="Value:").pack()
|
|
28
|
-
value_entry = ttk.Entry(window)
|
|
29
|
-
value_entry.pack()
|
|
30
|
-
|
|
31
|
-
def create_command():
|
|
32
|
-
create_constant(window, name_entry,value_entry)
|
|
33
|
-
ttk.Button(window, text='Create', command=create_command ).pack()
|
|
34
|
-
ttk.Button(window, text='Cancel', command=window.destroy).pack()
|
|
35
|
-
|
|
36
|
-
if name:
|
|
37
|
-
name_entry.insert('end', name)
|
|
38
|
-
if value:
|
|
39
|
-
value_entry.insert('end', value)
|
|
40
|
-
|
|
41
|
-
return window
|
|
42
|
-
|
|
43
|
-
def create_constant(window, name_entry: ttk.Entry, value_entry: ttk.Entry):
|
|
44
|
-
"""Creates a new constant in the database using the data from the
|
|
45
|
-
UI.
|
|
46
|
-
|
|
47
|
-
Args:
|
|
48
|
-
window: The popup window.
|
|
49
|
-
name_entry: The name entry.
|
|
50
|
-
value_entry: The value entry.
|
|
51
|
-
"""
|
|
52
|
-
name = name_entry.get()
|
|
53
|
-
value = value_entry.get()
|
|
54
|
-
|
|
55
|
-
if not name:
|
|
56
|
-
messagebox.showerror('Error', 'Please enter a name')
|
|
57
|
-
return
|
|
58
|
-
|
|
59
|
-
if not value:
|
|
60
|
-
messagebox.showerror('Error', 'Please enter a value')
|
|
61
|
-
return
|
|
62
|
-
|
|
63
|
-
try:
|
|
64
|
-
db_util.get_constant(name)
|
|
65
|
-
exists = True
|
|
66
|
-
except ValueError:
|
|
67
|
-
exists = False
|
|
68
|
-
|
|
69
|
-
if exists:
|
|
70
|
-
if messagebox.askyesno('Error', 'A constant with that name already exists. Do you want to overwrite it?'):
|
|
72
|
+
if self.constant:
|
|
71
73
|
db_util.update_constant(name, value)
|
|
72
74
|
else:
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
75
|
+
# Check if constant already exists
|
|
76
|
+
try:
|
|
77
|
+
db_util.get_constant(name)
|
|
78
|
+
exists = True
|
|
79
|
+
except ValueError:
|
|
80
|
+
exists = False
|
|
81
|
+
|
|
82
|
+
if exists:
|
|
83
|
+
ui.notify("A constant with that name already exists.", type='negative')
|
|
84
|
+
return
|
|
85
|
+
|
|
86
|
+
db_util.create_constant(name, value)
|
|
87
|
+
|
|
88
|
+
self.dialog.close()
|
|
89
|
+
self.constant_tab.update()
|
|
90
|
+
|
|
91
|
+
async def _delete_constant(self):
|
|
92
|
+
if await question_popup(f"Delete constant '{self.constant.name}?", "Delete", "Cancel", color1='red'):
|
|
93
|
+
db_util.delete_constant(self.constant.name)
|
|
94
|
+
self.dialog.close()
|
|
95
|
+
self.constant_tab.update()
|
|
@@ -1,89 +1,99 @@
|
|
|
1
|
-
"""This module is responsible for the layout and functionality of the 'New
|
|
1
|
+
"""This module is responsible for the layout and functionality of the 'New constant' popup."""
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
# pylint: disable=R0801
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
from typing import TYPE_CHECKING
|
|
6
5
|
|
|
7
|
-
|
|
8
|
-
import tkinter
|
|
9
|
-
from tkinter import ttk, messagebox
|
|
6
|
+
from nicegui import ui
|
|
10
7
|
|
|
11
8
|
from OpenOrchestrator.database import db_util
|
|
9
|
+
from OpenOrchestrator.database.constants import Credential
|
|
10
|
+
from OpenOrchestrator.orchestrator.popups.generic_popups import question_popup
|
|
11
|
+
|
|
12
|
+
if TYPE_CHECKING:
|
|
13
|
+
from OpenOrchestrator.orchestrator.tabs.constants_tab import ConstantTab
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
# pylint: disable-next=too-few-public-methods
|
|
17
|
+
class CredentialPopup():
|
|
18
|
+
"""A popup for creating/updating queue triggers."""
|
|
19
|
+
def __init__(self, constant_tab: ConstantTab, credential: Credential = None):
|
|
20
|
+
"""Create a new popup.
|
|
21
|
+
If a credential is given it will be updated instead of creating a new credential.
|
|
22
|
+
|
|
23
|
+
Args:
|
|
24
|
+
constant_tab: The tab that is opening this popup.
|
|
25
|
+
credential: The credential to update if any.
|
|
26
|
+
"""
|
|
27
|
+
self.constant_tab = constant_tab
|
|
28
|
+
self.credential = credential
|
|
29
|
+
title = 'Update Credential' if credential else 'New Credential'
|
|
30
|
+
button_text = "Update" if credential else "Create"
|
|
31
|
+
|
|
32
|
+
with ui.dialog(value=True).props('persistent') as self.dialog, ui.card().classes('w-full'):
|
|
33
|
+
ui.label(title).classes("text-xl")
|
|
34
|
+
self.name_input = ui.input("Credential Name").classes("w-full")
|
|
35
|
+
self.username_input = ui.input("Username").classes("w-full")
|
|
36
|
+
self.password_input = ui.input("Password").classes("w-full")
|
|
37
|
+
|
|
38
|
+
with ui.row():
|
|
39
|
+
ui.button(button_text, on_click=self._save_credential)
|
|
40
|
+
ui.button("Cancel", on_click=self.dialog.close)
|
|
41
|
+
|
|
42
|
+
if credential:
|
|
43
|
+
ui.button("Delete", color='red', on_click=self._delete_credential)
|
|
44
|
+
|
|
45
|
+
self._define_validation()
|
|
46
|
+
|
|
47
|
+
if credential:
|
|
48
|
+
self._pre_populate()
|
|
49
|
+
|
|
50
|
+
def _define_validation(self):
|
|
51
|
+
"""Define validation functions for ui elements."""
|
|
52
|
+
self.name_input.validation = {"Please enter a name": bool}
|
|
53
|
+
self.username_input.validation = {"Please enter a username": bool}
|
|
54
|
+
self.password_input.validation = {"Please enter a password": bool}
|
|
55
|
+
|
|
56
|
+
def _pre_populate(self):
|
|
57
|
+
"""Pre populate the inputs with an existing credential."""
|
|
58
|
+
self.name_input.value = self.credential.name
|
|
59
|
+
self.name_input.disable()
|
|
60
|
+
self.username_input.value = self.credential.username
|
|
61
|
+
|
|
62
|
+
def _save_credential(self):
|
|
63
|
+
"""Create or update a credential in the database using the data from the UI."""
|
|
64
|
+
self.name_input.validate()
|
|
65
|
+
self.username_input.validate()
|
|
66
|
+
self.password_input.validate()
|
|
67
|
+
|
|
68
|
+
if self.name_input.error or self.username_input.error or self.password_input.error:
|
|
69
|
+
return
|
|
70
|
+
|
|
71
|
+
name = self.name_input.value
|
|
72
|
+
username = self.username_input.value
|
|
73
|
+
password = self.password_input.value
|
|
12
74
|
|
|
13
|
-
|
|
14
|
-
"""Creates and shows a popup to create a new credential.
|
|
15
|
-
|
|
16
|
-
Returns:
|
|
17
|
-
tkinter.TopLevel: The created Toplevel object (Popup Window).
|
|
18
|
-
"""
|
|
19
|
-
window = tkinter.Toplevel()
|
|
20
|
-
window.grab_set()
|
|
21
|
-
window.title("New Credential")
|
|
22
|
-
window.geometry("300x300")
|
|
23
|
-
|
|
24
|
-
ttk.Label(window, text="Name:").pack()
|
|
25
|
-
name_entry = ttk.Entry(window)
|
|
26
|
-
name_entry.pack()
|
|
27
|
-
|
|
28
|
-
ttk.Label(window, text="Username:").pack()
|
|
29
|
-
username_entry = ttk.Entry(window)
|
|
30
|
-
username_entry.pack()
|
|
31
|
-
|
|
32
|
-
ttk.Label(window, text="Password:").pack()
|
|
33
|
-
password_entry = ttk.Entry(window)
|
|
34
|
-
password_entry.pack()
|
|
35
|
-
|
|
36
|
-
def create_command():
|
|
37
|
-
create_credential(window, name_entry,username_entry, password_entry)
|
|
38
|
-
ttk.Button(window, text='Create', command=create_command).pack()
|
|
39
|
-
ttk.Button(window, text='Cancel', command=window.destroy).pack()
|
|
40
|
-
|
|
41
|
-
if name:
|
|
42
|
-
name_entry.insert('end', name)
|
|
43
|
-
if username:
|
|
44
|
-
username_entry.insert('end', username)
|
|
45
|
-
|
|
46
|
-
return window
|
|
47
|
-
|
|
48
|
-
def create_credential(window: tkinter.Toplevel, name_entry: ttk.Entry,
|
|
49
|
-
username_entry: ttk.Entry, password_entry:ttk.Entry):
|
|
50
|
-
"""Creates a new credential in the database using the data from the
|
|
51
|
-
UI. The password is encrypted before sending it to the database.
|
|
52
|
-
|
|
53
|
-
Args:
|
|
54
|
-
window: The popup window.
|
|
55
|
-
name_entry: The name entry.
|
|
56
|
-
username_entry: The username entry.
|
|
57
|
-
password_entry: The password entry.
|
|
58
|
-
"""
|
|
59
|
-
name = name_entry.get()
|
|
60
|
-
username = username_entry.get()
|
|
61
|
-
password = password_entry.get()
|
|
62
|
-
|
|
63
|
-
if not name:
|
|
64
|
-
messagebox.showerror('Error', 'Please enter a name')
|
|
65
|
-
return
|
|
66
|
-
|
|
67
|
-
if not username:
|
|
68
|
-
messagebox.showerror('Error', 'Please enter a username')
|
|
69
|
-
return
|
|
70
|
-
|
|
71
|
-
if not password:
|
|
72
|
-
messagebox.showerror('Error', 'Please enter a password')
|
|
73
|
-
return
|
|
74
|
-
|
|
75
|
-
try:
|
|
76
|
-
db_util.get_credential(name)
|
|
77
|
-
exists = True
|
|
78
|
-
except ValueError:
|
|
79
|
-
exists = False
|
|
80
|
-
|
|
81
|
-
if exists:
|
|
82
|
-
if messagebox.askyesno('Error', 'A credential with that name already exists. Do you want to overwrite it?'):
|
|
75
|
+
if self.credential:
|
|
83
76
|
db_util.update_credential(name, username, password)
|
|
84
77
|
else:
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
78
|
+
# Check if credential already exists
|
|
79
|
+
try:
|
|
80
|
+
db_util.get_credential(name)
|
|
81
|
+
exists = True
|
|
82
|
+
except ValueError:
|
|
83
|
+
exists = False
|
|
84
|
+
|
|
85
|
+
if exists:
|
|
86
|
+
ui.notify("A credential with that name already exists.", type='negative')
|
|
87
|
+
return
|
|
88
|
+
|
|
89
|
+
db_util.create_credential(name, username, password)
|
|
90
|
+
|
|
91
|
+
self.dialog.close()
|
|
92
|
+
self.constant_tab.update()
|
|
93
|
+
|
|
94
|
+
async def _delete_credential(self):
|
|
95
|
+
"""Delete the selected credential."""
|
|
96
|
+
if await question_popup(f"Delete credential '{self.credential.name}'?", "Delete", "Cancel", color1='red'):
|
|
97
|
+
db_util.delete_credential(self.credential.name)
|
|
98
|
+
self.dialog.close()
|
|
99
|
+
self.constant_tab.update()
|