data-sourcerer 0.3.0__py3-none-any.whl → 0.5.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.
- {data_sourcerer-0.3.0.dist-info → data_sourcerer-0.5.0.dist-info}/METADATA +11 -8
- {data_sourcerer-0.3.0.dist-info → data_sourcerer-0.5.0.dist-info}/RECORD +55 -34
- sourcerer/__init__.py +3 -1
- sourcerer/domain/package_meta/__init__.py +0 -0
- sourcerer/domain/package_meta/entities.py +9 -0
- sourcerer/domain/package_meta/services.py +9 -0
- sourcerer/domain/settings/__init__.py +0 -0
- sourcerer/domain/settings/entities.py +11 -0
- sourcerer/domain/settings/repositories.py +20 -0
- sourcerer/domain/settings/services.py +19 -0
- sourcerer/domain/storage_provider/entities.py +1 -1
- sourcerer/infrastructure/db/models.py +23 -0
- sourcerer/infrastructure/package_meta/__init__.py +0 -0
- sourcerer/infrastructure/package_meta/services.py +26 -0
- sourcerer/infrastructure/settings/__init__.py +0 -0
- sourcerer/infrastructure/settings/repositories.py +59 -0
- sourcerer/infrastructure/settings/services.py +16 -0
- sourcerer/infrastructure/storage_provider/services/azure.py +1 -3
- sourcerer/infrastructure/storage_provider/services/gcp.py +1 -3
- sourcerer/infrastructure/storage_provider/services/s3.py +1 -2
- sourcerer/infrastructure/utils.py +1 -0
- sourcerer/presentation/di_container.py +13 -0
- sourcerer/presentation/screens/about/__init__.py +0 -0
- sourcerer/presentation/screens/about/main.py +60 -0
- sourcerer/presentation/screens/about/styles.tcss +32 -0
- sourcerer/presentation/screens/file_system_finder/__init__.py +0 -0
- sourcerer/presentation/screens/file_system_finder/main.py +3 -9
- sourcerer/presentation/screens/main/main.py +116 -8
- sourcerer/presentation/screens/main/messages/preview_request.py +1 -0
- sourcerer/presentation/screens/main/styles.tcss +13 -4
- sourcerer/presentation/screens/main/widgets/storage_content.py +10 -3
- sourcerer/presentation/screens/main/widgets/storage_list_sidebar.py +102 -18
- sourcerer/presentation/screens/preview_content/main.py +202 -15
- sourcerer/presentation/screens/preview_content/styles.tcss +39 -4
- sourcerer/presentation/screens/preview_content/text_area_style.py +60 -0
- sourcerer/presentation/screens/provider_creds_list/main.py +23 -9
- sourcerer/presentation/screens/provider_creds_list/styles.tcss +9 -0
- sourcerer/presentation/screens/provider_creds_registration/main.py +3 -3
- sourcerer/presentation/screens/settings/__init__.py +0 -0
- sourcerer/presentation/screens/settings/main.py +70 -0
- sourcerer/presentation/screens/settings/styles.tcss +44 -0
- sourcerer/presentation/screens/shared/modal_screens.py +37 -0
- sourcerer/presentation/screens/shared/widgets/button.py +11 -0
- sourcerer/presentation/screens/shared/widgets/labeled_input.py +1 -3
- sourcerer/presentation/screens/storage_action_progress/main.py +1 -2
- sourcerer/presentation/screens/storages_list/main.py +24 -9
- sourcerer/presentation/screens/storages_list/styles.tcss +7 -0
- sourcerer/presentation/screens/storages_registration/main.py +3 -3
- sourcerer/presentation/settings.py +1 -0
- sourcerer/presentation/utils.py +1 -0
- sourcerer/settings.py +2 -0
- sourcerer/utils.py +19 -1
- {data_sourcerer-0.3.0.dist-info → data_sourcerer-0.5.0.dist-info}/WHEEL +0 -0
- {data_sourcerer-0.3.0.dist-info → data_sourcerer-0.5.0.dist-info}/entry_points.txt +0 -0
- {data_sourcerer-0.3.0.dist-info → data_sourcerer-0.5.0.dist-info}/licenses/LICENSE +0 -0
@@ -1,52 +1,194 @@
|
|
1
|
+
import contextlib
|
2
|
+
import re
|
3
|
+
from dataclasses import dataclass
|
1
4
|
from pathlib import Path
|
5
|
+
from typing import ClassVar
|
2
6
|
|
7
|
+
import humanize
|
3
8
|
from dependency_injector.wiring import Provide
|
4
9
|
from rich.syntax import Syntax
|
5
|
-
from textual import on
|
10
|
+
from textual import events, on
|
6
11
|
from textual.app import ComposeResult
|
12
|
+
from textual.binding import Binding, BindingType
|
7
13
|
from textual.containers import Container, Horizontal
|
8
|
-
from textual.
|
9
|
-
from textual.
|
14
|
+
from textual.css.query import NoMatches
|
15
|
+
from textual.document._document import Selection
|
16
|
+
from textual.message import Message
|
17
|
+
from textual.reactive import reactive
|
18
|
+
from textual.widgets import Input, Label, LoadingIndicator, Rule, TextArea
|
10
19
|
|
11
20
|
from sourcerer.infrastructure.access_credentials.services import CredentialsService
|
12
21
|
from sourcerer.infrastructure.storage_provider.exceptions import (
|
13
22
|
ReadStorageItemsError,
|
14
23
|
)
|
15
24
|
from sourcerer.presentation.di_container import DiContainer
|
25
|
+
from sourcerer.presentation.screens.preview_content.text_area_style import (
|
26
|
+
SOURCERER_THEME_NAME,
|
27
|
+
sourcerer_text_area_theme,
|
28
|
+
)
|
29
|
+
from sourcerer.presentation.screens.shared.modal_screens import ExitBoundModalScreen
|
16
30
|
from sourcerer.presentation.screens.shared.widgets.button import Button
|
17
31
|
from sourcerer.presentation.utils import get_provider_service_by_access_uuid
|
32
|
+
from sourcerer.settings import PREVIEW_LENGTH_LIMIT, PREVIEW_LIMIT_SIZE
|
33
|
+
|
34
|
+
|
35
|
+
@dataclass
|
36
|
+
class HighlightResult(Message):
|
37
|
+
line: int
|
38
|
+
start: int
|
39
|
+
end: int
|
40
|
+
|
41
|
+
|
42
|
+
@dataclass
|
43
|
+
class HideSearchBar(Message):
|
44
|
+
pass
|
45
|
+
|
46
|
+
|
47
|
+
class ClickableLabel(Label):
|
48
|
+
@dataclass
|
49
|
+
class Click(Message):
|
50
|
+
name: str
|
51
|
+
|
52
|
+
def __init__(self, *args, **kwargs):
|
53
|
+
super().__init__(*args, **kwargs)
|
54
|
+
|
55
|
+
def on_click(self, _: events.Click) -> None:
|
56
|
+
self.post_message(self.Click(name=self.name)) # type: ignore
|
57
|
+
|
58
|
+
|
59
|
+
class Search(Container):
|
60
|
+
total = reactive(0, recompose=False)
|
61
|
+
current = reactive(0, recompose=False)
|
62
|
+
content = reactive("", recompose=False)
|
63
|
+
|
64
|
+
def __init__(self, *args, **kwargs):
|
65
|
+
super().__init__(*args, **kwargs)
|
66
|
+
self.search_result_lines = []
|
67
|
+
self.search_value = ""
|
68
|
+
|
69
|
+
def compose(self) -> ComposeResult:
|
70
|
+
with Horizontal():
|
71
|
+
with Horizontal(id="left"):
|
72
|
+
yield Label("Search:")
|
73
|
+
yield Input(placeholder="...")
|
74
|
+
|
75
|
+
with Horizontal(id="right"):
|
76
|
+
yield ClickableLabel(
|
77
|
+
"◀", id="previous", name="previous", classes="search-button"
|
78
|
+
)
|
79
|
+
yield Label(f"{self.current}/{self.total}", id="search-result")
|
80
|
+
yield ClickableLabel(
|
81
|
+
"▶", id="next", name="next", classes="search-button"
|
82
|
+
)
|
83
|
+
yield ClickableLabel(
|
84
|
+
"❌", id="hide", name="hide", classes="search-button"
|
85
|
+
)
|
86
|
+
yield Rule()
|
87
|
+
|
88
|
+
@on(Input.Submitted)
|
89
|
+
def on_input_submitted(self, event: Input.Submitted) -> None:
|
90
|
+
"""Handle input submitted events."""
|
91
|
+
if not event.value or not self.content:
|
92
|
+
self.total = 0
|
93
|
+
self.current = 0
|
94
|
+
self.search_value = ""
|
95
|
+
self.search_result_lines = []
|
96
|
+
return
|
97
|
+
if event.value == self.search_value:
|
98
|
+
self._increment_current()
|
99
|
+
return
|
100
|
+
|
101
|
+
self.search_value = event.value
|
102
|
+
lines = self.content.split("\n")
|
103
|
+
search_pattern = event.value.lower()
|
18
104
|
|
105
|
+
self.search_result_lines = [
|
106
|
+
(line_n, index)
|
107
|
+
for line_n, line in enumerate(lines)
|
108
|
+
if search_pattern in line.lower()
|
109
|
+
for index in [
|
110
|
+
match.start()
|
111
|
+
for match in re.finditer(rf"(?i){re.escape(search_pattern)}", line)
|
112
|
+
]
|
113
|
+
]
|
19
114
|
|
20
|
-
|
115
|
+
if not self.search_result_lines:
|
116
|
+
self.notify("No matches found", severity="warning")
|
117
|
+
self.total, self.current = 0, 0
|
118
|
+
return
|
119
|
+
|
120
|
+
self.total = len(self.search_result_lines)
|
121
|
+
self.current = 1
|
122
|
+
|
123
|
+
@on(ClickableLabel.Click)
|
124
|
+
def on_click(self, event: ClickableLabel.Click) -> None:
|
125
|
+
if event.name == "next":
|
126
|
+
self._increment_current()
|
127
|
+
elif event.name == "previous":
|
128
|
+
self._decrement_current()
|
129
|
+
elif event.name == "hide":
|
130
|
+
self.post_message(HideSearchBar())
|
131
|
+
|
132
|
+
def _increment_current(self):
|
133
|
+
self.current = self.current + 1 if self.current < self.total else 1
|
134
|
+
|
135
|
+
def _decrement_current(self):
|
136
|
+
self.current = self.current - 1 if self.current > 1 else self.total
|
137
|
+
|
138
|
+
def watch_current(self):
|
139
|
+
with contextlib.suppress(NoMatches):
|
140
|
+
search_result = self.query_one("#search-result", Label)
|
141
|
+
search_result.update(f"{self.current}/{self.total}")
|
142
|
+
if not self.search_result_lines:
|
143
|
+
return
|
144
|
+
line, start = self.search_result_lines[self.current - 1]
|
145
|
+
self.post_message(
|
146
|
+
HighlightResult(line, start=start, end=start + len(self.search_value))
|
147
|
+
)
|
148
|
+
|
149
|
+
|
150
|
+
class PreviewContentScreen(ExitBoundModalScreen):
|
21
151
|
CSS_PATH = "styles.tcss"
|
22
152
|
|
153
|
+
BINDINGS: ClassVar[list[BindingType]] = [
|
154
|
+
Binding("escape", "cancel", "Close the screen"),
|
155
|
+
]
|
156
|
+
|
23
157
|
def __init__(
|
24
158
|
self,
|
25
159
|
storage_name,
|
26
160
|
key,
|
161
|
+
file_size,
|
27
162
|
access_credentials_uuid,
|
28
163
|
*args,
|
29
164
|
credentials_service: CredentialsService = Provide[
|
30
165
|
DiContainer.credentials_repository
|
31
166
|
],
|
32
|
-
**kwargs
|
167
|
+
**kwargs,
|
33
168
|
):
|
34
169
|
super().__init__(*args, **kwargs)
|
35
170
|
|
36
171
|
self.storage_name = storage_name
|
37
172
|
self.key = key
|
173
|
+
self.file_size = file_size
|
38
174
|
self.access_credentials_uuid = access_credentials_uuid
|
39
175
|
self.credentials_service = credentials_service
|
176
|
+
self.content = None
|
40
177
|
|
41
178
|
def compose(self) -> ComposeResult:
|
42
179
|
with Container(id="PreviewContentScreen"):
|
180
|
+
yield Search(id="search-bar")
|
43
181
|
yield LoadingIndicator(id="loading")
|
44
|
-
yield
|
182
|
+
yield TextArea(read_only=True, show_line_numbers=True)
|
45
183
|
with Horizontal(id="controls"):
|
46
184
|
yield Button("Close", name="cancel")
|
47
185
|
|
48
186
|
def on_mount(self) -> None:
|
49
187
|
"""Called when the DOM is ready."""
|
188
|
+
search = self.query_one(Search)
|
189
|
+
text_log = self.query_one(TextArea)
|
190
|
+
text_log.register_theme(sourcerer_text_area_theme)
|
191
|
+
text_log.theme = SOURCERER_THEME_NAME
|
50
192
|
|
51
193
|
provider_service = get_provider_service_by_access_uuid(
|
52
194
|
self.access_credentials_uuid, self.credentials_service
|
@@ -55,28 +197,73 @@ class PreviewContentScreen(ModalScreen):
|
|
55
197
|
self.notify("Could not read file :(", severity="error")
|
56
198
|
return
|
57
199
|
try:
|
58
|
-
content = provider_service.read_storage_item(
|
200
|
+
self.content = provider_service.read_storage_item(
|
201
|
+
self.storage_name, self.key
|
202
|
+
)
|
203
|
+
if self.file_size > PREVIEW_LIMIT_SIZE:
|
204
|
+
self.content = self.content[:PREVIEW_LENGTH_LIMIT]
|
205
|
+
self.notify(
|
206
|
+
f"The file size {humanize.naturalsize(self.file_size)} "
|
207
|
+
f"exceeds {humanize.naturalsize(PREVIEW_LIMIT_SIZE)} preview limit. "
|
208
|
+
f"The content is truncated to {PREVIEW_LENGTH_LIMIT} characters.",
|
209
|
+
severity="warning",
|
210
|
+
)
|
211
|
+
search.content = self.content
|
59
212
|
except ReadStorageItemsError:
|
60
213
|
self.notify("Could not read file :(", severity="error")
|
61
214
|
return
|
62
215
|
self.query_one("#loading").remove()
|
63
|
-
if content is None:
|
216
|
+
if self.content is None:
|
64
217
|
self.notify("Empty file", severity="warning")
|
65
218
|
return
|
66
219
|
|
67
|
-
text_log = self.query_one(RichLog)
|
68
|
-
|
69
220
|
extension = Path(self.key).suffix
|
70
221
|
|
71
222
|
lexer = (
|
72
|
-
"json"
|
223
|
+
"json"
|
224
|
+
if extension == ".tfstate"
|
225
|
+
else Syntax.guess_lexer(self.key, self.content)
|
73
226
|
)
|
74
|
-
|
75
|
-
|
76
|
-
|
227
|
+
if lexer in text_log.available_languages:
|
228
|
+
text_log.language = lexer
|
229
|
+
else:
|
230
|
+
text_log.language = "python"
|
231
|
+
text_log.blur()
|
232
|
+
text_log.load_text(self.content)
|
77
233
|
|
78
234
|
@on(Button.Click)
|
79
235
|
def on_button_click(self, event: Button.Click) -> None:
|
80
236
|
"""Handle button click events."""
|
81
237
|
if event.action == "cancel":
|
82
|
-
self.
|
238
|
+
self.action_cancel_screen()
|
239
|
+
|
240
|
+
@on(HideSearchBar)
|
241
|
+
def on_hide_search_bar(self, _: HideSearchBar) -> None:
|
242
|
+
"""Handle hide search bar events."""
|
243
|
+
search_bar = self.query_one("#search-bar", Search)
|
244
|
+
search_bar.remove_class("-visible")
|
245
|
+
search_bar.query_one(Input).value = ""
|
246
|
+
search_bar.total = 0
|
247
|
+
search_bar.current = 0
|
248
|
+
search_bar.search_result_lines = []
|
249
|
+
search_bar.search_value = ""
|
250
|
+
|
251
|
+
@on(HighlightResult)
|
252
|
+
def on_highlight_result(self, event: HighlightResult) -> None:
|
253
|
+
"""Handle highlight result events."""
|
254
|
+
|
255
|
+
text_area = self.query_one(TextArea)
|
256
|
+
text_area.selection = Selection(
|
257
|
+
start=(event.line, event.start), end=(event.line, event.end)
|
258
|
+
)
|
259
|
+
|
260
|
+
def action_find(self):
|
261
|
+
self.query_one("#search-bar").add_class("-visible")
|
262
|
+
self.query_one(Input).focus()
|
263
|
+
|
264
|
+
def action_cancel(self):
|
265
|
+
self.action_cancel_screen()
|
266
|
+
|
267
|
+
def on_key(self, event: events.Key) -> None:
|
268
|
+
if event.key in ("ctrl+f", "super+f"):
|
269
|
+
self.action_find()
|
@@ -12,16 +12,51 @@ PreviewContentScreen {
|
|
12
12
|
& > #PreviewContentScreen {
|
13
13
|
padding: 1 2 0 2;
|
14
14
|
margin: 0 0;
|
15
|
-
width:
|
15
|
+
width: 70%;
|
16
16
|
height: 40;
|
17
17
|
border: solid $secondary-background;
|
18
18
|
border-title-color: $primary-lighten-2;
|
19
19
|
|
20
|
-
|
20
|
+
|
21
|
+
#search-bar {
|
22
|
+
height: auto;
|
23
|
+
display: none;
|
24
|
+
|
25
|
+
&.-visible {
|
26
|
+
display: block;
|
27
|
+
}
|
28
|
+
}
|
29
|
+
|
30
|
+
Horizontal{
|
31
|
+
height: auto;
|
32
|
+
|
33
|
+
& > Static {
|
34
|
+
width: auto;
|
35
|
+
}
|
36
|
+
|
37
|
+
#left {
|
38
|
+
align: left middle;
|
39
|
+
width: 90%;
|
40
|
+
}
|
41
|
+
#right {
|
42
|
+
align: right middle;
|
43
|
+
width: 10%;
|
44
|
+
}
|
45
|
+
}
|
46
|
+
|
47
|
+
Input {
|
48
|
+
height: 1;
|
49
|
+
border: none;
|
21
50
|
background: transparent;
|
22
|
-
width:
|
51
|
+
width: 78%;
|
23
52
|
}
|
24
53
|
|
54
|
+
TextArea {
|
55
|
+
background-tint: $background;
|
56
|
+
|
57
|
+
&:focus {
|
58
|
+
border: tall $border-blurred;
|
59
|
+
}
|
60
|
+
}
|
25
61
|
}
|
26
62
|
}
|
27
|
-
|
@@ -0,0 +1,60 @@
|
|
1
|
+
from rich.style import Style
|
2
|
+
from textual._text_area_theme import TextAreaTheme
|
3
|
+
|
4
|
+
SOURCERER_THEME_NAME = "sourcerer"
|
5
|
+
# Terraform theme for the text area.
|
6
|
+
sourcerer_text_area_theme = TextAreaTheme(
|
7
|
+
name=SOURCERER_THEME_NAME,
|
8
|
+
cursor_style=Style(color="#1e1e1e", bgcolor="#f0f0f0"),
|
9
|
+
cursor_line_style=Style(bgcolor="#2b2b2b"),
|
10
|
+
bracket_matching_style=Style(bgcolor="#3a3a3a", bold=True),
|
11
|
+
cursor_line_gutter_style=Style(color="#CCCCCC", bgcolor="#2b2b2b"),
|
12
|
+
selection_style=Style(bgcolor="#FFA656"),
|
13
|
+
syntax_styles={
|
14
|
+
"string": Style(color="#79C0FF"),
|
15
|
+
"string.documentation": Style(color="#79C0FF"),
|
16
|
+
"comment": Style(color="#6A9955"),
|
17
|
+
"heading.marker": Style(color="#6E7681"),
|
18
|
+
"keyword": Style(color="#C586C0"),
|
19
|
+
"operator": Style(color="#CCCCCC"),
|
20
|
+
"conditional": Style(color="#569cd6"),
|
21
|
+
"keyword.function": Style(color="#F97970"),
|
22
|
+
"keyword.return": Style(color="#569cd6"),
|
23
|
+
"keyword.operator": Style(color="#569cd6"),
|
24
|
+
"repeat": Style(color="#569cd6"),
|
25
|
+
"exception": Style(color="#569cd6"),
|
26
|
+
"include": Style(color="#569cd6"),
|
27
|
+
"number": Style(color="#b5cea8"),
|
28
|
+
"float": Style(color="#b5cea8"),
|
29
|
+
"class": Style(color="#4EC9B0"),
|
30
|
+
"type": Style(color="#EFCB43"),
|
31
|
+
"type.class": Style(color="#FFA656"),
|
32
|
+
"type.builtin": Style(color="#CDD9E5"),
|
33
|
+
"function": Style(color="#CCB0EA"),
|
34
|
+
"function.call": Style(color="#CCB0EA"),
|
35
|
+
"method": Style(color="#CCB0EA"),
|
36
|
+
"method.call": Style(color="#CCB0EA"),
|
37
|
+
"constructor": Style(color="#DCBDFB"),
|
38
|
+
"boolean": Style(color="#7DAF9C"),
|
39
|
+
"constant.builtin": Style(color="#7DAF9C"),
|
40
|
+
"json.null": Style(color="#FDA556"),
|
41
|
+
"tag": Style(color="#EFCB43"),
|
42
|
+
"yaml.field": Style(color="#569cd6", bold=True),
|
43
|
+
"json.label": Style(color="#8EDB8C", bold=True),
|
44
|
+
"toml.type": Style(color="#569cd6"),
|
45
|
+
"toml.datetime": Style(color="#C586C0", italic=True),
|
46
|
+
"css.property": Style(color="#569cd6"),
|
47
|
+
"heading": Style(color="#569cd6", bold=True),
|
48
|
+
"bold": Style(bold=True),
|
49
|
+
"italic": Style(italic=True),
|
50
|
+
"strikethrough": Style(strike=True),
|
51
|
+
"link.uri": Style(color="#40A6FF", underline=True),
|
52
|
+
"link.label": Style(color="#569cd6"),
|
53
|
+
"list.marker": Style(color="#6E7681"),
|
54
|
+
"inline_code": Style(color="#ce9178"),
|
55
|
+
"info_string": Style(color="#ce9178", bold=True, italic=True),
|
56
|
+
"punctuation.bracket": Style(color="#CCCCCC"),
|
57
|
+
"punctuation.delimiter": Style(color="#CCCCCC"),
|
58
|
+
"punctuation.special": Style(color="#CCCCCC"),
|
59
|
+
},
|
60
|
+
)
|
@@ -1,13 +1,14 @@
|
|
1
1
|
from dataclasses import dataclass
|
2
2
|
from enum import Enum
|
3
|
+
from typing import ClassVar
|
3
4
|
|
4
5
|
from dependency_injector.wiring import Provide
|
5
6
|
from textual import on
|
6
7
|
from textual.app import ComposeResult
|
8
|
+
from textual.binding import Binding, BindingType
|
7
9
|
from textual.containers import Container, Horizontal, VerticalScroll
|
8
10
|
from textual.message import Message
|
9
11
|
from textual.reactive import reactive
|
10
|
-
from textual.screen import ModalScreen
|
11
12
|
from textual.widgets import Checkbox, Label
|
12
13
|
|
13
14
|
from sourcerer.domain.access_credentials.entities import Credentials
|
@@ -22,6 +23,9 @@ from sourcerer.presentation.screens.provider_creds_registration.main import (
|
|
22
23
|
ProviderCredsRegistrationScreen,
|
23
24
|
)
|
24
25
|
from sourcerer.presentation.screens.question.main import QuestionScreen
|
26
|
+
from sourcerer.presentation.screens.shared.modal_screens import (
|
27
|
+
RefreshTriggerableModalScreen,
|
28
|
+
)
|
25
29
|
from sourcerer.presentation.screens.shared.widgets.button import Button
|
26
30
|
|
27
31
|
|
@@ -98,7 +102,7 @@ class ProviderCredentialsRow(Horizontal):
|
|
98
102
|
self.post_message(ReloadCredentialsRequest())
|
99
103
|
|
100
104
|
|
101
|
-
class ProviderCredsListScreen(
|
105
|
+
class ProviderCredsListScreen(RefreshTriggerableModalScreen):
|
102
106
|
CSS_PATH = "styles.tcss"
|
103
107
|
|
104
108
|
MAIN_CONTAINER_ID = "ProviderCredsListScreen"
|
@@ -110,6 +114,11 @@ class ProviderCredsListScreen(ModalScreen):
|
|
110
114
|
PROVIDERS_NAME = "providers"
|
111
115
|
AUTH_METHODS_NAME = "auth_methods"
|
112
116
|
|
117
|
+
BINDINGS: ClassVar[list[BindingType]] = [
|
118
|
+
*RefreshTriggerableModalScreen.BINDINGS,
|
119
|
+
Binding("ctrl+n", "add_credentials", "Add new credentials"),
|
120
|
+
]
|
121
|
+
|
113
122
|
credentials_list = reactive([], recompose=True)
|
114
123
|
|
115
124
|
def __init__(
|
@@ -151,13 +160,15 @@ class ProviderCredsListScreen(ModalScreen):
|
|
151
160
|
"""
|
152
161
|
Initialize the screen by refreshing the credentials list when the screen is composed.
|
153
162
|
"""
|
154
|
-
self.refresh_credentials_list()
|
163
|
+
self.refresh_credentials_list(set_refresh_flag=False)
|
155
164
|
|
156
|
-
def refresh_credentials_list(self):
|
165
|
+
def refresh_credentials_list(self, set_refresh_flag: bool = True):
|
157
166
|
"""
|
158
167
|
Refresh the credentials list by retrieving the latest credentials from the credentials service.
|
159
168
|
"""
|
160
169
|
self.credentials_list = self.credentials_service.list()
|
170
|
+
if set_refresh_flag:
|
171
|
+
self._requires_storage_refresh = True
|
161
172
|
|
162
173
|
def create_provider_creds_registration(
|
163
174
|
self,
|
@@ -195,12 +206,9 @@ class ProviderCredsListScreen(ModalScreen):
|
|
195
206
|
event (Button.Click): The button click event.
|
196
207
|
"""
|
197
208
|
if event.action == ControlsEnum.CANCEL.name:
|
198
|
-
self.
|
209
|
+
self.action_cancel_screen()
|
199
210
|
if event.action == "add_registration":
|
200
|
-
self.
|
201
|
-
ProviderCredsRegistrationScreen(),
|
202
|
-
callback=self.create_provider_creds_registration, # type: ignore
|
203
|
-
)
|
211
|
+
self.action_add_credentials()
|
204
212
|
|
205
213
|
@on(ProviderCredentialsRow.ChangeActiveStatus)
|
206
214
|
def on_change_active_status(self, event: ProviderCredentialsRow.ChangeActiveStatus):
|
@@ -227,3 +235,9 @@ class ProviderCredsListScreen(ModalScreen):
|
|
227
235
|
_ (ReloadCredentialsRequest): The reload credentials request event.
|
228
236
|
"""
|
229
237
|
self.refresh_credentials_list()
|
238
|
+
|
239
|
+
def action_add_credentials(self):
|
240
|
+
self.app.push_screen(
|
241
|
+
ProviderCredsRegistrationScreen(),
|
242
|
+
callback=self.create_provider_creds_registration, # type: ignore
|
243
|
+
)
|
@@ -23,6 +23,14 @@ ProviderCredsListScreen {
|
|
23
23
|
|
24
24
|
.add_registration_button {
|
25
25
|
color: $success-darken-2;
|
26
|
+
|
27
|
+
&:hover {
|
28
|
+
color: $primary;
|
29
|
+
}
|
30
|
+
&:focus {
|
31
|
+
color: $primary;
|
32
|
+
}
|
33
|
+
|
26
34
|
}
|
27
35
|
|
28
36
|
margin-bottom: 1;
|
@@ -30,6 +38,7 @@ ProviderCredsListScreen {
|
|
30
38
|
|
31
39
|
ProviderCredentialsRow.active {
|
32
40
|
background: $primary-lighten-2;
|
41
|
+
color: $background-darken-3;
|
33
42
|
}
|
34
43
|
|
35
44
|
ProviderCredentialsRow, Horizontal {
|
@@ -5,7 +5,6 @@ from dependency_injector.wiring import Provide
|
|
5
5
|
from textual import on
|
6
6
|
from textual.app import ComposeResult
|
7
7
|
from textual.containers import Container, Horizontal, VerticalScroll
|
8
|
-
from textual.screen import ModalScreen
|
9
8
|
from textual.widgets import Label, Select
|
10
9
|
|
11
10
|
from sourcerer.domain.access_credentials.services import (
|
@@ -20,6 +19,7 @@ from sourcerer.infrastructure.access_credentials.registry import (
|
|
20
19
|
)
|
21
20
|
from sourcerer.infrastructure.utils import generate_unique_name
|
22
21
|
from sourcerer.presentation.di_container import DiContainer
|
22
|
+
from sourcerer.presentation.screens.shared.modal_screens import ExitBoundModalScreen
|
23
23
|
from sourcerer.presentation.screens.shared.widgets.button import Button
|
24
24
|
from sourcerer.presentation.screens.shared.widgets.labeled_input import LabeledInput
|
25
25
|
|
@@ -36,7 +36,7 @@ class ProviderCredentialsEntry:
|
|
36
36
|
fields: dict[str, str]
|
37
37
|
|
38
38
|
|
39
|
-
class ProviderCredsRegistrationScreen(
|
39
|
+
class ProviderCredsRegistrationScreen(ExitBoundModalScreen):
|
40
40
|
CSS_PATH = "styles.tcss"
|
41
41
|
|
42
42
|
MAIN_CONTAINER_ID = "ProviderCredsRegistrationScreen"
|
@@ -182,7 +182,7 @@ class ProviderCredsRegistrationScreen(ModalScreen):
|
|
182
182
|
collected authentication fields.
|
183
183
|
"""
|
184
184
|
if event.action == ControlsEnum.CANCEL.name:
|
185
|
-
self.
|
185
|
+
self.action_cancel_screen()
|
186
186
|
elif event.action == ControlsEnum.CREATE.name:
|
187
187
|
if not self.auth_method:
|
188
188
|
self.notify("Please select provider and auth method", severity="error")
|
File without changes
|
@@ -0,0 +1,70 @@
|
|
1
|
+
from textual import on
|
2
|
+
from textual.app import ComposeResult
|
3
|
+
from textual.containers import Container, Horizontal
|
4
|
+
from textual.widgets import Checkbox, Rule, Select, Static
|
5
|
+
|
6
|
+
from sourcerer.domain.settings.entities import Settings, SettingsFields
|
7
|
+
from sourcerer.presentation.screens.shared.modal_screens import ExitBoundModalScreen
|
8
|
+
from sourcerer.presentation.screens.shared.widgets.button import Button
|
9
|
+
|
10
|
+
|
11
|
+
class SettingsScreen(ExitBoundModalScreen):
|
12
|
+
"""Screen with a parameter."""
|
13
|
+
|
14
|
+
CSS_PATH = "styles.tcss"
|
15
|
+
|
16
|
+
def __init__(self, settings: Settings) -> None:
|
17
|
+
super().__init__()
|
18
|
+
self.settings = settings
|
19
|
+
|
20
|
+
def compose(self) -> ComposeResult:
|
21
|
+
with Container():
|
22
|
+
with Horizontal():
|
23
|
+
yield Static("Theme:")
|
24
|
+
yield Select(
|
25
|
+
((theme, theme) for theme in self.app._registered_themes),
|
26
|
+
id="theme",
|
27
|
+
value=self.settings.theme,
|
28
|
+
allow_blank=False,
|
29
|
+
)
|
30
|
+
|
31
|
+
yield Rule()
|
32
|
+
with Horizontal():
|
33
|
+
yield Checkbox(
|
34
|
+
"Group storage by access credentials",
|
35
|
+
value=self.settings.group_by_access_credentials,
|
36
|
+
)
|
37
|
+
|
38
|
+
yield Rule()
|
39
|
+
with Horizontal(id="controls"):
|
40
|
+
yield Button("Save", name="save")
|
41
|
+
yield Button("Close", name="close")
|
42
|
+
|
43
|
+
@on(Button.Click)
|
44
|
+
def on_button_clicked(self, event: Button.Click) -> None:
|
45
|
+
"""Handle button clicked events."""
|
46
|
+
if event.action == "close":
|
47
|
+
self.action_cancel_screen()
|
48
|
+
elif event.action == "save":
|
49
|
+
self.dismiss(
|
50
|
+
{
|
51
|
+
SettingsFields.theme: self.query_one("Select#theme", Select).value,
|
52
|
+
SettingsFields.group_by_access_credentials: self.query_one(
|
53
|
+
Checkbox
|
54
|
+
).value,
|
55
|
+
}
|
56
|
+
)
|
57
|
+
|
58
|
+
def action_cancel_screen(self):
|
59
|
+
self.dismiss(
|
60
|
+
{
|
61
|
+
SettingsFields.theme: self.settings.theme,
|
62
|
+
SettingsFields.group_by_access_credentials: self.settings.group_by_access_credentials,
|
63
|
+
}
|
64
|
+
)
|
65
|
+
|
66
|
+
@on(Select.Changed)
|
67
|
+
def on_select_changed(self, event: Select.Changed) -> None:
|
68
|
+
"""Handle select changed events."""
|
69
|
+
if event.select.id == "theme":
|
70
|
+
self.app.theme = event.value # type: ignore[assignment]
|
@@ -0,0 +1,44 @@
|
|
1
|
+
SettingsScreen {
|
2
|
+
align: center middle;
|
3
|
+
content-align: center top;
|
4
|
+
|
5
|
+
& > Container {
|
6
|
+
padding: 1 2 0 2;
|
7
|
+
margin: 0 0;
|
8
|
+
width: 70%;
|
9
|
+
height: auto;
|
10
|
+
border: solid $border;
|
11
|
+
|
12
|
+
& > Static {
|
13
|
+
text-align: center;
|
14
|
+
text-wrap: wrap;
|
15
|
+
}
|
16
|
+
|
17
|
+
& > Horizontal {
|
18
|
+
align: center bottom;
|
19
|
+
height: auto;
|
20
|
+
|
21
|
+
& > Static {
|
22
|
+
width: 10%;
|
23
|
+
padding-top: 1;
|
24
|
+
}
|
25
|
+
|
26
|
+
& > Checkbox {
|
27
|
+
width: 100%;
|
28
|
+
border: none;
|
29
|
+
background: transparent;
|
30
|
+
|
31
|
+
&:focus {
|
32
|
+
background: transparent;
|
33
|
+
|
34
|
+
& > .toggle--label {
|
35
|
+
color: $text-secondary;
|
36
|
+
background: transparent;
|
37
|
+
}
|
38
|
+
}
|
39
|
+
|
40
|
+
}
|
41
|
+
}
|
42
|
+
}
|
43
|
+
|
44
|
+
}
|