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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. OpenOrchestrator/__init__.py +7 -0
  2. OpenOrchestrator/__main__.py +2 -0
  3. OpenOrchestrator/common/connection_frame.py +35 -49
  4. OpenOrchestrator/common/crypto_util.py +4 -4
  5. OpenOrchestrator/common/datetime_util.py +20 -0
  6. OpenOrchestrator/database/constants.py +25 -19
  7. OpenOrchestrator/database/db_util.py +77 -30
  8. OpenOrchestrator/database/logs.py +13 -0
  9. OpenOrchestrator/database/queues.py +17 -0
  10. OpenOrchestrator/database/triggers.py +25 -56
  11. OpenOrchestrator/orchestrator/application.py +87 -34
  12. OpenOrchestrator/orchestrator/datetime_input.py +75 -0
  13. OpenOrchestrator/orchestrator/popups/constant_popup.py +87 -69
  14. OpenOrchestrator/orchestrator/popups/credential_popup.py +92 -82
  15. OpenOrchestrator/orchestrator/popups/generic_popups.py +27 -0
  16. OpenOrchestrator/orchestrator/popups/trigger_popup.py +216 -0
  17. OpenOrchestrator/orchestrator/tabs/constants_tab.py +52 -0
  18. OpenOrchestrator/orchestrator/tabs/logging_tab.py +70 -0
  19. OpenOrchestrator/orchestrator/tabs/queue_tab.py +116 -0
  20. OpenOrchestrator/orchestrator/tabs/settings_tab.py +22 -0
  21. OpenOrchestrator/orchestrator/tabs/trigger_tab.py +87 -0
  22. OpenOrchestrator/scheduler/application.py +3 -3
  23. OpenOrchestrator/scheduler/connection_frame.py +96 -0
  24. OpenOrchestrator/scheduler/run_tab.py +87 -80
  25. OpenOrchestrator/scheduler/runner.py +33 -25
  26. OpenOrchestrator/scheduler/settings_tab.py +2 -1
  27. {OpenOrchestrator-1.0.2.dist-info → OpenOrchestrator-1.2.0.dist-info}/METADATA +2 -2
  28. OpenOrchestrator-1.2.0.dist-info/RECORD +38 -0
  29. OpenOrchestrator/orchestrator/constants_tab.py +0 -169
  30. OpenOrchestrator/orchestrator/logging_tab.py +0 -221
  31. OpenOrchestrator/orchestrator/popups/queue_trigger_popup.py +0 -129
  32. OpenOrchestrator/orchestrator/popups/scheduled_trigger_popup.py +0 -129
  33. OpenOrchestrator/orchestrator/popups/single_trigger_popup.py +0 -134
  34. OpenOrchestrator/orchestrator/settings_tab.py +0 -31
  35. OpenOrchestrator/orchestrator/table_util.py +0 -76
  36. OpenOrchestrator/orchestrator/trigger_tab.py +0 -231
  37. OpenOrchestrator-1.0.2.dist-info/RECORD +0 -36
  38. {OpenOrchestrator-1.0.2.dist-info → OpenOrchestrator-1.2.0.dist-info}/LICENSE +0 -0
  39. {OpenOrchestrator-1.0.2.dist-info → OpenOrchestrator-1.2.0.dist-info}/WHEEL +0 -0
  40. {OpenOrchestrator-1.0.2.dist-info → OpenOrchestrator-1.2.0.dist-info}/top_level.txt +0 -0
@@ -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 to_tuple(self) -> tuple:
69
- """Convert the trigger to a tuple of values.
70
-
71
- Returns:
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 to_tuple(self) -> tuple:
99
- """Convert the trigger to a tuple of values.
100
-
101
- Returns:
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 to_tuple(self) -> tuple:
130
- """Convert the trigger to a tuple of values.
131
-
132
- Returns:
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 tkinter
5
- from tkinter import ttk
4
+ import socket
6
5
 
7
- from OpenOrchestrator.orchestrator import logging_tab, settings_tab, trigger_tab, constants_tab
6
+ from nicegui import ui, app
8
7
 
9
- class Application(tkinter.Tk):
10
- """The main application object of the Orchestrator app.
11
- Extends the tkinter.Tk object.
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
- # Disable pylint duplicate code error since it
15
- # mostly reacts to the layout code being similar.
16
- # pylint: disable=R0801
17
- super().__init__()
18
- self.title("OpenOrchestrator")
19
- self.geometry("850x600")
20
- style = ttk.Style(self)
21
- style.theme_use('vista')
22
-
23
- notebook = ttk.Notebook(self)
24
- notebook.pack(expand=True, fill='both')
25
-
26
- trig_tab = trigger_tab.create_tab(notebook)
27
- log_tab = logging_tab.create_tab(notebook)
28
- const_tab = constants_tab.create_tab(notebook)
29
- set_tab = settings_tab.create_tab(notebook)
30
-
31
- notebook.add(trig_tab, text="Triggers")
32
- notebook.add(log_tab, text="Logs")
33
- notebook.add(const_tab, text="Constants")
34
- notebook.add(set_tab, text="Settings")
35
-
36
- notebook.select(3)
37
-
38
- self.mainloop()
39
-
40
- if __name__=='__main__':
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
- # Disable pylint duplicate code error since it
4
- # mostly reacts to the layout code being similar.
5
- # pylint: disable=R0801
3
+ from __future__ import annotations
4
+ from typing import TYPE_CHECKING
6
5
 
7
- import tkinter
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
- def show_popup(name=None, value=None):
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
- return
74
- else:
75
- db_util.create_constant(name, value)
76
-
77
- window.destroy()
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 credential' popup."""
1
+ """This module is responsible for the layout and functionality of the 'New constant' popup."""
2
2
 
3
- # Disable pylint duplicate code error since it
4
- # mostly reacts to the layout code being similar.
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
- def show_popup(name=None, username=None):
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
- return
86
- else:
87
- db_util.create_credential(name, username, password)
88
-
89
- window.destroy()
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()