meerschaum 2.9.4__py3-none-any.whl → 3.0.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 (201) hide show
  1. meerschaum/__init__.py +5 -2
  2. meerschaum/_internal/__init__.py +1 -0
  3. meerschaum/_internal/arguments/_parse_arguments.py +4 -4
  4. meerschaum/_internal/arguments/_parser.py +33 -4
  5. meerschaum/_internal/cli/__init__.py +6 -0
  6. meerschaum/_internal/cli/daemons.py +103 -0
  7. meerschaum/_internal/cli/entry.py +220 -0
  8. meerschaum/_internal/cli/workers.py +435 -0
  9. meerschaum/_internal/docs/index.py +48 -2
  10. meerschaum/_internal/entry.py +50 -14
  11. meerschaum/_internal/shell/Shell.py +121 -29
  12. meerschaum/_internal/shell/__init__.py +4 -1
  13. meerschaum/_internal/static.py +359 -0
  14. meerschaum/_internal/term/TermPageHandler.py +1 -2
  15. meerschaum/_internal/term/__init__.py +40 -6
  16. meerschaum/_internal/term/tools.py +33 -8
  17. meerschaum/actions/__init__.py +6 -4
  18. meerschaum/actions/api.py +53 -13
  19. meerschaum/actions/attach.py +1 -0
  20. meerschaum/actions/bootstrap.py +8 -8
  21. meerschaum/actions/delete.py +4 -2
  22. meerschaum/actions/edit.py +171 -25
  23. meerschaum/actions/login.py +8 -8
  24. meerschaum/actions/register.py +143 -6
  25. meerschaum/actions/reload.py +22 -5
  26. meerschaum/actions/restart.py +14 -0
  27. meerschaum/actions/show.py +184 -31
  28. meerschaum/actions/start.py +166 -17
  29. meerschaum/actions/stop.py +38 -2
  30. meerschaum/actions/sync.py +7 -2
  31. meerschaum/actions/tag.py +9 -8
  32. meerschaum/actions/verify.py +5 -8
  33. meerschaum/api/__init__.py +45 -15
  34. meerschaum/api/_events.py +46 -4
  35. meerschaum/api/_oauth2.py +162 -9
  36. meerschaum/api/_tokens.py +102 -0
  37. meerschaum/api/dash/__init__.py +0 -3
  38. meerschaum/api/dash/callbacks/__init__.py +1 -0
  39. meerschaum/api/dash/callbacks/custom.py +4 -3
  40. meerschaum/api/dash/callbacks/dashboard.py +228 -117
  41. meerschaum/api/dash/callbacks/jobs.py +14 -7
  42. meerschaum/api/dash/callbacks/login.py +10 -1
  43. meerschaum/api/dash/callbacks/pipes.py +194 -14
  44. meerschaum/api/dash/callbacks/plugins.py +0 -1
  45. meerschaum/api/dash/callbacks/register.py +10 -3
  46. meerschaum/api/dash/callbacks/settings/password_reset.py +2 -2
  47. meerschaum/api/dash/callbacks/tokens.py +389 -0
  48. meerschaum/api/dash/components.py +36 -15
  49. meerschaum/api/dash/jobs.py +1 -1
  50. meerschaum/api/dash/keys.py +35 -93
  51. meerschaum/api/dash/pages/__init__.py +2 -1
  52. meerschaum/api/dash/pages/dashboard.py +1 -20
  53. meerschaum/api/dash/pages/{job.py → jobs.py} +10 -7
  54. meerschaum/api/dash/pages/login.py +2 -2
  55. meerschaum/api/dash/pages/pipes.py +16 -5
  56. meerschaum/api/dash/pages/settings/password_reset.py +1 -1
  57. meerschaum/api/dash/pages/tokens.py +53 -0
  58. meerschaum/api/dash/pipes.py +438 -88
  59. meerschaum/api/dash/sessions.py +12 -0
  60. meerschaum/api/dash/tokens.py +603 -0
  61. meerschaum/api/dash/websockets.py +1 -1
  62. meerschaum/api/dash/webterm.py +18 -6
  63. meerschaum/api/models/__init__.py +23 -3
  64. meerschaum/api/models/_actions.py +22 -0
  65. meerschaum/api/models/_pipes.py +91 -7
  66. meerschaum/api/models/_tokens.py +81 -0
  67. meerschaum/api/resources/static/css/dash.css +16 -0
  68. meerschaum/api/resources/static/js/terminado.js +3 -0
  69. meerschaum/api/resources/static/js/xterm-addon-unicode11.js +2 -0
  70. meerschaum/api/resources/templates/termpage.html +13 -0
  71. meerschaum/api/routes/__init__.py +1 -0
  72. meerschaum/api/routes/_actions.py +3 -4
  73. meerschaum/api/routes/_connectors.py +3 -7
  74. meerschaum/api/routes/_jobs.py +26 -35
  75. meerschaum/api/routes/_login.py +120 -15
  76. meerschaum/api/routes/_misc.py +5 -10
  77. meerschaum/api/routes/_pipes.py +178 -143
  78. meerschaum/api/routes/_plugins.py +38 -28
  79. meerschaum/api/routes/_tokens.py +236 -0
  80. meerschaum/api/routes/_users.py +47 -35
  81. meerschaum/api/routes/_version.py +3 -3
  82. meerschaum/api/routes/_webterm.py +3 -3
  83. meerschaum/config/__init__.py +100 -30
  84. meerschaum/config/_default.py +132 -64
  85. meerschaum/config/_edit.py +38 -32
  86. meerschaum/config/_formatting.py +2 -0
  87. meerschaum/config/_patch.py +10 -8
  88. meerschaum/config/_paths.py +133 -13
  89. meerschaum/config/_read_config.py +87 -36
  90. meerschaum/config/_sync.py +6 -3
  91. meerschaum/config/_version.py +1 -1
  92. meerschaum/config/environment.py +262 -0
  93. meerschaum/config/stack/__init__.py +37 -15
  94. meerschaum/config/static.py +18 -0
  95. meerschaum/connectors/_Connector.py +11 -6
  96. meerschaum/connectors/__init__.py +41 -22
  97. meerschaum/connectors/api/_APIConnector.py +34 -6
  98. meerschaum/connectors/api/_actions.py +2 -2
  99. meerschaum/connectors/api/_jobs.py +12 -1
  100. meerschaum/connectors/api/_login.py +33 -7
  101. meerschaum/connectors/api/_misc.py +2 -2
  102. meerschaum/connectors/api/_pipes.py +23 -32
  103. meerschaum/connectors/api/_plugins.py +2 -2
  104. meerschaum/connectors/api/_request.py +1 -1
  105. meerschaum/connectors/api/_tokens.py +146 -0
  106. meerschaum/connectors/api/_users.py +70 -58
  107. meerschaum/connectors/instance/_InstanceConnector.py +83 -0
  108. meerschaum/connectors/instance/__init__.py +10 -0
  109. meerschaum/connectors/instance/_pipes.py +442 -0
  110. meerschaum/connectors/instance/_plugins.py +159 -0
  111. meerschaum/connectors/instance/_tokens.py +317 -0
  112. meerschaum/connectors/instance/_users.py +188 -0
  113. meerschaum/connectors/parse.py +5 -2
  114. meerschaum/connectors/sql/_SQLConnector.py +22 -5
  115. meerschaum/connectors/sql/_cli.py +12 -11
  116. meerschaum/connectors/sql/_create_engine.py +12 -168
  117. meerschaum/connectors/sql/_fetch.py +2 -18
  118. meerschaum/connectors/sql/_pipes.py +295 -278
  119. meerschaum/connectors/sql/_plugins.py +29 -0
  120. meerschaum/connectors/sql/_sql.py +47 -22
  121. meerschaum/connectors/sql/_users.py +36 -2
  122. meerschaum/connectors/sql/tables/__init__.py +254 -122
  123. meerschaum/connectors/valkey/_ValkeyConnector.py +5 -7
  124. meerschaum/connectors/valkey/_pipes.py +60 -31
  125. meerschaum/connectors/valkey/_plugins.py +2 -26
  126. meerschaum/core/Pipe/__init__.py +115 -85
  127. meerschaum/core/Pipe/_attributes.py +425 -124
  128. meerschaum/core/Pipe/_bootstrap.py +54 -24
  129. meerschaum/core/Pipe/_cache.py +555 -0
  130. meerschaum/core/Pipe/_clear.py +0 -11
  131. meerschaum/core/Pipe/_data.py +96 -68
  132. meerschaum/core/Pipe/_deduplicate.py +0 -13
  133. meerschaum/core/Pipe/_delete.py +12 -21
  134. meerschaum/core/Pipe/_drop.py +11 -23
  135. meerschaum/core/Pipe/_dtypes.py +49 -19
  136. meerschaum/core/Pipe/_edit.py +14 -4
  137. meerschaum/core/Pipe/_fetch.py +1 -1
  138. meerschaum/core/Pipe/_index.py +8 -14
  139. meerschaum/core/Pipe/_show.py +5 -5
  140. meerschaum/core/Pipe/_sync.py +123 -204
  141. meerschaum/core/Pipe/_verify.py +4 -4
  142. meerschaum/{plugins → core/Plugin}/_Plugin.py +16 -12
  143. meerschaum/core/Plugin/__init__.py +1 -1
  144. meerschaum/core/Token/_Token.py +220 -0
  145. meerschaum/core/Token/__init__.py +12 -0
  146. meerschaum/core/User/_User.py +35 -10
  147. meerschaum/core/User/__init__.py +9 -1
  148. meerschaum/core/__init__.py +1 -0
  149. meerschaum/jobs/_Executor.py +88 -4
  150. meerschaum/jobs/_Job.py +149 -38
  151. meerschaum/jobs/__init__.py +3 -2
  152. meerschaum/jobs/systemd.py +8 -3
  153. meerschaum/models/__init__.py +35 -0
  154. meerschaum/models/pipes.py +247 -0
  155. meerschaum/models/tokens.py +38 -0
  156. meerschaum/models/users.py +26 -0
  157. meerschaum/plugins/__init__.py +301 -88
  158. meerschaum/plugins/bootstrap.py +510 -4
  159. meerschaum/utils/_get_pipes.py +97 -30
  160. meerschaum/utils/daemon/Daemon.py +199 -43
  161. meerschaum/utils/daemon/FileDescriptorInterceptor.py +0 -1
  162. meerschaum/utils/daemon/RotatingFile.py +63 -36
  163. meerschaum/utils/daemon/StdinFile.py +53 -13
  164. meerschaum/utils/daemon/__init__.py +47 -6
  165. meerschaum/utils/daemon/_names.py +6 -3
  166. meerschaum/utils/dataframe.py +480 -82
  167. meerschaum/utils/debug.py +49 -19
  168. meerschaum/utils/dtypes/__init__.py +478 -37
  169. meerschaum/utils/dtypes/sql.py +369 -29
  170. meerschaum/utils/formatting/__init__.py +5 -2
  171. meerschaum/utils/formatting/_jobs.py +1 -1
  172. meerschaum/utils/formatting/_pipes.py +52 -50
  173. meerschaum/utils/formatting/_pprint.py +1 -0
  174. meerschaum/utils/formatting/_shell.py +44 -18
  175. meerschaum/utils/misc.py +268 -186
  176. meerschaum/utils/packages/__init__.py +25 -40
  177. meerschaum/utils/packages/_packages.py +42 -34
  178. meerschaum/utils/pipes.py +213 -0
  179. meerschaum/utils/process.py +2 -2
  180. meerschaum/utils/prompt.py +175 -144
  181. meerschaum/utils/schedule.py +2 -1
  182. meerschaum/utils/sql.py +135 -49
  183. meerschaum/utils/threading.py +42 -0
  184. meerschaum/utils/typing.py +1 -4
  185. meerschaum/utils/venv/_Venv.py +2 -2
  186. meerschaum/utils/venv/__init__.py +7 -7
  187. meerschaum/utils/warnings.py +19 -13
  188. {meerschaum-2.9.4.dist-info → meerschaum-3.0.0.dist-info}/METADATA +94 -96
  189. meerschaum-3.0.0.dist-info/RECORD +289 -0
  190. {meerschaum-2.9.4.dist-info → meerschaum-3.0.0.dist-info}/WHEEL +1 -1
  191. meerschaum-3.0.0.dist-info/licenses/NOTICE +2 -0
  192. meerschaum/api/models/_interfaces.py +0 -15
  193. meerschaum/api/models/_locations.py +0 -15
  194. meerschaum/api/models/_metrics.py +0 -15
  195. meerschaum/config/_environment.py +0 -145
  196. meerschaum/config/static/__init__.py +0 -186
  197. meerschaum-2.9.4.dist-info/RECORD +0 -263
  198. {meerschaum-2.9.4.dist-info → meerschaum-3.0.0.dist-info}/entry_points.txt +0 -0
  199. {meerschaum-2.9.4.dist-info → meerschaum-3.0.0.dist-info}/licenses/LICENSE +0 -0
  200. {meerschaum-2.9.4.dist-info → meerschaum-3.0.0.dist-info}/top_level.txt +0 -0
  201. {meerschaum-2.9.4.dist-info → meerschaum-3.0.0.dist-info}/zip-safe +0 -0
@@ -0,0 +1,389 @@
1
+ #! /usr/bin/env python3
2
+ # vim:fenc=utf-8
3
+
4
+ """
5
+ Define the callbacks for the tokens page.
6
+ """
7
+
8
+ import json
9
+ from typing import Optional, List, Dict, Any, Tuple
10
+ from datetime import datetime, timezone
11
+
12
+ import dash
13
+ from dash.dependencies import Input, Output, State, MATCH, ALL
14
+ from dash.exceptions import PreventUpdate
15
+ import dash_bootstrap_components as dbc
16
+ import dash.html as html
17
+ import dash.dcc as dcc
18
+
19
+ import meerschaum as mrsm
20
+ from meerschaum.api import get_api_connector, debug
21
+ from meerschaum.api.dash import dash_app
22
+ from meerschaum.api.dash.sessions import get_user_from_session
23
+ from meerschaum.api.dash.components import alert_from_success_tuple, build_cards_grid
24
+ from meerschaum.api.dash.tokens import (
25
+ get_tokens_cards,
26
+ get_tokens_table,
27
+ build_tokens_register_input_modal,
28
+ build_tokens_register_output_modal,
29
+ )
30
+ from meerschaum._internal.static import STATIC_CONFIG
31
+ from meerschaum.utils.daemon import get_new_daemon_name
32
+ from meerschaum.core import Token
33
+
34
+
35
+ @dash_app.callback(
36
+ Output('tokens-output-div', 'children'),
37
+ Output('tokens-register-input-modal', 'children'),
38
+ Output('tokens-alert-div', 'children'),
39
+ Input('tokens-refresh-button', 'n_clicks'),
40
+ State('session-store', 'data'),
41
+ )
42
+ def refresh_tokens_button_click(
43
+ n_clicks: Optional[int],
44
+ session_data: Optional[Dict[str, Any]] = None,
45
+ ):
46
+ """
47
+ Build the tokens cards on load or refresh.
48
+ """
49
+ session_id = (session_data or {}).get('session-id', None)
50
+ tokens_table, alerts = get_tokens_table(session_id)
51
+ if not tokens_table:
52
+ return (
53
+ [
54
+ html.H4('No tokens registered.'),
55
+ html.P('Click the `+` button to register a new token.'),
56
+ ],
57
+ build_tokens_register_input_modal(),
58
+ alerts,
59
+ )
60
+
61
+ return tokens_table, build_tokens_register_input_modal(), alerts
62
+
63
+
64
+ @dash_app.callback(
65
+ Output('tokens-register-input-modal', 'is_open'),
66
+ Input('tokens-create-button', 'n_clicks'),
67
+ prevent_initial_call=True,
68
+ )
69
+ def create_tokens_button_click(n_clicks: Optional[int]):
70
+ """
71
+ Open the tokens registration modal when the plus button is clicked.
72
+ """
73
+ if not n_clicks:
74
+ raise PreventUpdate
75
+
76
+ return True
77
+
78
+
79
+ @dash_app.callback(
80
+ Output("tokens-scopes-checklist-div", 'style'),
81
+ Input("tokens-toggle-scopes-switch", 'value'),
82
+ prevent_initial_call=True,
83
+ )
84
+ def toggle_token_scopes_checklist(value: bool):
85
+ """
86
+ Toggle the scopes checklist.
87
+ """
88
+ return {'display': 'none'} if value else {}
89
+
90
+
91
+ @dash_app.callback(
92
+ Output('tokens-scopes-checklist', 'value'),
93
+ Output('tokens-deselect-scopes-button', 'children'),
94
+ Input('tokens-deselect-scopes-button', 'n_clicks'),
95
+ State('tokens-deselect-scopes-button', 'children'),
96
+ prevent_initial_call=True,
97
+ )
98
+ def deselect_scopes_click(n_clicks: Optional[int], name: str):
99
+ """
100
+ Set the value of the scopes checklist to an empty list.
101
+ """
102
+ if not n_clicks:
103
+ raise PreventUpdate
104
+
105
+ new_name = 'Select all' if name == 'Deselect all' else 'Deselect all'
106
+ value = (
107
+ []
108
+ if name == 'Deselect all'
109
+ else list(STATIC_CONFIG['tokens']['scopes'])
110
+ )
111
+
112
+ return value, new_name
113
+
114
+
115
+ @dash_app.callback(
116
+ Output({'type': 'tokens-scopes-checklist', 'index': MATCH}, 'value'),
117
+ Output({'type': 'tokens-deselect-scopes-button', 'index': MATCH}, 'children'),
118
+ Input({'type': 'tokens-deselect-scopes-button', 'index': MATCH}, 'n_clicks'),
119
+ State({'type': 'tokens-deselect-scopes-button', 'index': MATCH}, 'children'),
120
+ prevent_initial_call=True,
121
+ )
122
+ def edit_token_deselect_scopes_click(n_clicks: Optional[int], name: str):
123
+ """
124
+ Set the value of the scopes checklist to an empty list.
125
+ """
126
+ if not n_clicks:
127
+ raise PreventUpdate
128
+
129
+ new_name = 'Select all' if name == 'Deselect all' else 'Deselect all'
130
+ value = (
131
+ []
132
+ if name == 'Deselect all'
133
+ else list(STATIC_CONFIG['tokens']['scopes'])
134
+ )
135
+
136
+ return value, new_name
137
+
138
+
139
+ @dash_app.callback(
140
+ Output('tokens-register-input-modal', 'is_open'),
141
+ Output('tokens-register-output-modal', 'is_open'),
142
+ Output('tokens-register-output-modal', 'children'),
143
+ Input('tokens-register-button', 'n_clicks'),
144
+ State('tokens-name-input', 'value'),
145
+ State('tokens-scopes-checklist', 'value'),
146
+ State('tokens-expiration-datepickersingle', 'date'),
147
+ State('session-store', 'data'),
148
+ prevent_initial_call=True,
149
+ )
150
+ def register_token_click(
151
+ n_clicks: Optional[int],
152
+ name: str,
153
+ scopes: List[str],
154
+ expiration: Optional[datetime] = None,
155
+ session_data: Optional[Dict[str, Any]] = None,
156
+ ):
157
+ """
158
+ Register the token.
159
+ """
160
+ if not n_clicks:
161
+ raise PreventUpdate
162
+
163
+ session_id = (session_data or {}).get('session-id', None)
164
+ token = Token(
165
+ label=(name or None),
166
+ user=get_user_from_session(session_id),
167
+ expiration=(datetime.fromisoformat(f"{expiration}") if expiration is not None else None),
168
+ )
169
+ return False, True, build_tokens_register_output_modal(token)
170
+
171
+
172
+ @dash_app.callback(
173
+ Output("tokens-refresh-button", "n_clicks"),
174
+ Input("tokens-register-output-modal", "is_open"),
175
+ Input({'type': 'tokens-edit-modal', 'index': ALL}, 'is_open'),
176
+ Input({'type': 'tokens-invalidate-modal', 'index': ALL}, 'is_open'),
177
+ Input({'type': 'tokens-delete-modal', 'index': ALL}, 'is_open'),
178
+ State("tokens-refresh-button", "n_clicks"),
179
+ prevent_initial_call=True,
180
+ )
181
+ def register_token_modal_close_refresh(
182
+ register_is_open: bool,
183
+ edit_is_open_list,
184
+ invalidate_is_open_list,
185
+ delete_is_open_list,
186
+ n_clicks: int,
187
+ ):
188
+ """
189
+ Refresh the cards when the registration, edit, invalidate, or delete modals changes visibility.
190
+ """
191
+ if any(
192
+ edit_is_open_list
193
+ + invalidate_is_open_list
194
+ + delete_is_open_list
195
+ ):
196
+ raise PreventUpdate
197
+
198
+ return (n_clicks or 0) + 1
199
+
200
+
201
+ @dash_app.callback(
202
+ Output('tokens-register-clipboard', 'content'),
203
+ Output('tokens-register-clipboard', 'n_clicks'),
204
+ Output('tokens-register-copy-button', 'children'),
205
+ Input('tokens-register-copy-button', 'n_clicks'),
206
+ State('tokens-register-clipboard', 'n_clicks'),
207
+ State('token-id-pre', 'children'),
208
+ State('token-secret-pre', 'children'),
209
+ prevent_initial_call=True,
210
+ )
211
+ def copy_token_button_click(
212
+ n_clicks: int,
213
+ clipboard_n_clicks: Optional[int],
214
+ token_id: str,
215
+ token_secret: str,
216
+ ) -> Tuple[str, int, str]:
217
+ """
218
+ Copy the token's ID and secret to the clipboard.
219
+ """
220
+ if not n_clicks:
221
+ raise PreventUpdate
222
+ return (
223
+ f"Client ID: {token_id}\nClient Secret: {token_secret}",
224
+ (clipboard_n_clicks or 0) + 1,
225
+ "Copied!",
226
+ )
227
+
228
+
229
+ @dash_app.callback(
230
+ Output('tokens-close-register-output-modal-button', 'disabled'),
231
+ Output('tokens-register-output-modal', 'backdrop'),
232
+ Input('tokens-register-clipboard', 'n_clicks'),
233
+ prevent_initial_call=True,
234
+ )
235
+ def enable_close_button(n_clicks):
236
+ """
237
+ Enable the close button once the token has been copied.
238
+ """
239
+ if not n_clicks:
240
+ raise PreventUpdate
241
+ return False, True
242
+
243
+
244
+ @dash_app.callback(
245
+ Output('tokens-register-output-modal', 'is_open'),
246
+ Input('tokens-close-register-output-modal-button', 'n_clicks'),
247
+ prevent_initial_call=True,
248
+ )
249
+ def close_register_output_modal(n_clicks: int) -> bool:
250
+ """
251
+ Close the register output modal when the Close button is clicked.
252
+ """
253
+ if not n_clicks:
254
+ raise PreventUpdate
255
+ return False
256
+
257
+
258
+ @dash_app.callback(
259
+ Output({'type': 'tokens-edit-modal', 'index': MATCH}, 'is_open'),
260
+ Input({'type': 'tokens-edit-button', 'index': MATCH}, 'n_clicks'),
261
+ prevent_initial_call=True,
262
+ )
263
+ def edit_token_button_click(n_clicks: int):
264
+ if not n_clicks:
265
+ raise PreventUpdate
266
+ return True
267
+
268
+
269
+ @dash_app.callback(
270
+ Output({'type': 'tokens-edit-modal', 'index': MATCH}, 'is_open'),
271
+ Output({'type': 'tokens-edit-alerts-div', 'index': MATCH}, 'children'),
272
+ Input({'type': 'tokens-edit-submit-button', 'index': MATCH}, 'n_clicks'),
273
+ State({'type': 'tokens-expiration-datepickersingle', 'index': MATCH}, 'date'),
274
+ State({'type': 'tokens-scopes-checklist', 'index': MATCH}, 'value'),
275
+ State({'type': 'tokens-name-input', 'index': MATCH}, 'value'),
276
+ prevent_initial_call=True,
277
+ )
278
+ def edit_token_submit_button_click(
279
+ n_clicks: int,
280
+ expiration: Optional[datetime],
281
+ scopes: List[str],
282
+ label: str,
283
+ ):
284
+ if not n_clicks:
285
+ raise PreventUpdate
286
+
287
+ ctx = dash.callback_context.triggered
288
+ if ctx[0]['value'] is None:
289
+ raise PreventUpdate
290
+
291
+ component_dict = json.loads(ctx[0]['prop_id'].split('.' + 'n_clicks')[0])
292
+ token_id = component_dict['index']
293
+
294
+ token = Token(
295
+ id=token_id,
296
+ label=label,
297
+ expiration=(datetime.fromisoformat(f"{expiration}T00:00:00Z") if expiration is not None else None),
298
+ scopes=scopes,
299
+ instance=get_api_connector(),
300
+ )
301
+
302
+ success, msg = token.edit(debug=debug)
303
+ if not success:
304
+ return dash.no_update, alert_from_success_tuple((success, msg))
305
+
306
+ return False, dash.no_update
307
+
308
+
309
+ @dash_app.callback(
310
+ Output({'type': 'tokens-invalidate-modal', 'index': MATCH}, 'is_open'),
311
+ Input({'type': 'tokens-invalidate-button', 'index': MATCH}, 'n_clicks'),
312
+ prevent_initial_call=True,
313
+ )
314
+ def invalidate_token_click(n_clicks: int):
315
+ if not n_clicks:
316
+ raise PreventUpdate
317
+ return True
318
+
319
+
320
+ @dash_app.callback(
321
+ Output({'type': 'tokens-delete-modal', 'index': MATCH}, 'is_open'),
322
+ Input({'type': 'tokens-delete-button', 'index': MATCH}, 'n_clicks'),
323
+ prevent_initial_call=True,
324
+ )
325
+ def invalidate_token_click(n_clicks: int):
326
+ if not n_clicks:
327
+ raise PreventUpdate
328
+ return True
329
+
330
+
331
+
332
+ @dash_app.callback(
333
+ Output({'type': 'tokens-edit-modal', 'index': MATCH}, 'is_open'),
334
+ Output({'type': 'tokens-invalidate-modal', 'index': MATCH}, 'is_open'),
335
+ Output({'type': 'tokens-invalidate-alerts-div', 'index': MATCH}, 'children'),
336
+ Input({'type': 'tokens-invalidate-confirm-button', 'index': MATCH}, 'n_clicks'),
337
+ prevent_initial_call=True,
338
+ )
339
+ def invalidate_token_confirm_click(n_clicks: int):
340
+ if not n_clicks:
341
+ raise PreventUpdate
342
+
343
+ ctx = dash.callback_context.triggered
344
+ if ctx[0]['value'] is None:
345
+ raise PreventUpdate
346
+
347
+ component_dict = json.loads(ctx[0]['prop_id'].split('.' + 'n_clicks')[0])
348
+ token_id = component_dict['index']
349
+
350
+ token = Token(
351
+ id=token_id,
352
+ instance=get_api_connector(),
353
+ )
354
+
355
+ success, msg = token.invalidate(debug=debug)
356
+ if not success:
357
+ return dash.no_update, dash.no_update, alert_from_success_tuple((success, msg))
358
+
359
+ return False, False, dash.no_update
360
+
361
+
362
+ @dash_app.callback(
363
+ Output({'type': 'tokens-edit-modal', 'index': MATCH}, 'is_open'),
364
+ Output({'type': 'tokens-delete-modal', 'index': MATCH}, 'is_open'),
365
+ Output({'type': 'tokens-delete-alerts-div', 'index': MATCH}, 'children'),
366
+ Input({'type': 'tokens-delete-confirm-button', 'index': MATCH}, 'n_clicks'),
367
+ prevent_initial_call=True,
368
+ )
369
+ def delete_token_confirm_click(n_clicks: int):
370
+ if not n_clicks:
371
+ raise PreventUpdate
372
+
373
+ ctx = dash.callback_context.triggered
374
+ if ctx[0]['value'] is None:
375
+ raise PreventUpdate
376
+
377
+ component_dict = json.loads(ctx[0]['prop_id'].split('.' + 'n_clicks')[0])
378
+ token_id = component_dict['index']
379
+
380
+ token = Token(
381
+ id=token_id,
382
+ instance=get_api_connector(),
383
+ )
384
+
385
+ success, msg = token.delete(debug=debug)
386
+ if not success:
387
+ return dash.no_update, dash.no_update, alert_from_success_tuple((success, msg))
388
+
389
+ return False, False, []
@@ -10,10 +10,10 @@ from __future__ import annotations
10
10
 
11
11
  from meerschaum.utils.packages import attempt_import, import_dcc, import_html
12
12
  from meerschaum.utils.typing import SuccessTuple, List
13
- from meerschaum.config.static import STATIC_CONFIG
13
+ from meerschaum._internal.static import STATIC_CONFIG
14
14
  from meerschaum.utils.misc import remove_ansi
15
15
  from meerschaum._internal.shell.Shell import get_shell_intro
16
- from meerschaum.api import endpoints, CHECK_UPDATE, docs_enabled
16
+ from meerschaum.api import endpoints, CHECK_UPDATE, docs_enabled, get_api_connector
17
17
  from meerschaum.connectors import instance_types, _load_builtin_custom_connectors
18
18
  from meerschaum.utils.misc import get_connector_labels
19
19
  from meerschaum.config import __doc__ as doc
@@ -104,7 +104,7 @@ instance_select = dbc.Select(
104
104
  id='instance-select',
105
105
  size='sm',
106
106
  options=[
107
- {'label': i, 'value': i}
107
+ {'label': (i[:32] + '…') if len(i) > 32 else i, 'value': i}
108
108
  for i in get_connector_labels(*instance_types)
109
109
  ],
110
110
  class_name='dbc_dark custom-select custom-select-sm',
@@ -163,6 +163,7 @@ pages_navbar = html.Div(
163
163
  id='pages-navbar-div',
164
164
  )
165
165
 
166
+
166
167
  navbar = dbc.Navbar(
167
168
  dbc.Container(
168
169
  [
@@ -171,13 +172,11 @@ navbar = dbc.Navbar(
171
172
  dbc.Collapse(
172
173
  dbc.Row(
173
174
  [
174
- dbc.Col(instance_select),
175
- dbc.Col(
176
- sign_out_button,
177
- className="ms-auto",
178
- ),
175
+ dbc.Col(instance_select, width="auto"),
176
+ dbc.Col(sign_out_button, width="auto"),
179
177
  ],
180
178
  className="g-0 ms-auto flex-nowrap mt-3 mt-md-0",
179
+ align="center",
181
180
  ),
182
181
  id='navbar-collapse',
183
182
  is_open=False,
@@ -267,6 +266,26 @@ def build_pages_offcanvas_children():
267
266
 
268
267
  plugins_accordion_items = []
269
268
  for page_group, pages_dicts in _plugin_endpoints_to_pages.items():
269
+ if len(pages_dicts) == 1:
270
+ page_href, page_dict = list(pages_dicts.items())[0]
271
+ if page_dict['page_key'].lower() == page_group.lower():
272
+ pages_listgroup_items.append(
273
+ dbc.ListGroupItem(
274
+ dbc.Button(
275
+ ' '.join([word.capitalize() for word in page_dict['page_key'].split(' ')]),
276
+ style={
277
+ 'width': '100%',
278
+ 'text-align': 'left',
279
+ 'text-decoration': 'none',
280
+ 'padding-left': '0',
281
+ },
282
+ href=page_href,
283
+ color='dark',
284
+ )
285
+ )
286
+ )
287
+ continue
288
+
270
289
  plugin_listgroup_items = [
271
290
  dbc.ListGroupItem(
272
291
  dbc.Button(
@@ -294,13 +313,15 @@ def build_pages_offcanvas_children():
294
313
  class_name='pages-offcanvas-accordion',
295
314
  )
296
315
  plugins_accordion_items.append(plugin_accordion_item)
297
- plugins_accordion = dbc.Accordion(
298
- plugins_accordion_items,
299
- start_collapsed=True,
300
- flush=True,
301
- always_open=True,
302
- )
303
- pages_listgroup_items.append(plugins_accordion)
316
+
317
+ if plugins_accordion_items:
318
+ plugins_accordion = dbc.Accordion(
319
+ plugins_accordion_items,
320
+ start_collapsed=True,
321
+ flush=True,
322
+ always_open=True,
323
+ )
324
+ pages_listgroup_items.append(plugins_accordion)
304
325
 
305
326
  pages_children = dbc.Card(
306
327
  dbc.ListGroup(
@@ -98,7 +98,7 @@ def build_job_card(
98
98
  html.B(
99
99
  html.A(
100
100
  "🔗 " + job.name,
101
- href=f"/dash/job/{job.name}",
101
+ href=f"/dash/jobs/{job.name}",
102
102
  target="_blank",
103
103
  style={
104
104
  'color': 'white',
@@ -16,13 +16,10 @@ dbc = attempt_import('dash_bootstrap_components', lazy=False, check_update=CHECK
16
16
  html, dcc = import_html(check_update=CHECK_UPDATE), import_dcc(check_update=CHECK_UPDATE)
17
17
 
18
18
  placeholders = {
19
- 'ck' : 'Connectors',
20
- 'mk' : 'Metrics',
21
- 'lk' : 'Locations',
22
- 'params' : (
23
- 'Additional search parameters. ' +
24
- 'Simple dictionary format or JSON accepted.'
25
- ),
19
+ 'ck': 'Connectors',
20
+ 'mk': 'Metrics',
21
+ 'lk': 'Locations',
22
+ 'tags': 'Tags',
26
23
  }
27
24
  widths = {
28
25
  'flags' : {'size' : 12},
@@ -212,11 +209,41 @@ dropdown_keys_row = dbc.Row(
212
209
  ),
213
210
  ] ### end of filters row children
214
211
  )
212
+ tags_dropdown = html.Div(
213
+ dcc.Dropdown(
214
+ id='tags-dropdown',
215
+ options=[],
216
+ placeholder=placeholders['tags'],
217
+ multi=True,
218
+ searchable=True,
219
+ ),
220
+ className="dbc_dark",
221
+ id="tags-dropdown-div",
222
+ )
223
+
215
224
  dropdown_tab_content = html.Div([
216
225
  dbc.Card(
217
226
  dbc.CardBody(
218
227
  [
219
228
  dropdown_keys_row,
229
+ html.Br(),
230
+ dbc.Row(
231
+ [
232
+ dbc.Col(tags_dropdown, width=True),
233
+ dbc.Col(
234
+ dbc.Button(
235
+ "Clear all",
236
+ id='clear-all-keys-button',
237
+ color='link',
238
+ size='sm',
239
+ style={'text-decoration': 'none'},
240
+ ),
241
+ width='auto',
242
+ ),
243
+ ],
244
+ className='g-0',
245
+ align='center',
246
+ ),
220
247
  ], ### end of card children
221
248
  className='card-text',
222
249
  )
@@ -232,94 +259,9 @@ dropdown_tab_content = html.Div([
232
259
  ),
233
260
  ])
234
261
 
235
- text_tab_content = dbc.Card(
236
- dbc.CardBody(
237
- [
238
- dbc.Row(
239
- [
240
- dbc.Col(html.Div(className='dbc_dark', children=[
241
- dbc.InputGroup(
242
- [
243
- dbc.Button(
244
- 'Clear',
245
- id='clear-connector-keys-input-button',
246
- color='link',
247
- size='sm',
248
- ),
249
- dbc.Input(
250
- id='connector-keys-input',
251
- placeholder=placeholders['ck'],
252
- type='text',
253
- value='',
254
- list='connector-keys-list',
255
- className='dbc_dark'
256
- ),
257
- ],
258
- size=input_group_sizes['ck'],
259
- )]),
260
- width=4,
261
- ),
262
- dbc.Col(
263
- dbc.InputGroup(
264
- [
265
- dbc.Button(
266
- 'Clear',
267
- id='clear-metric-keys-input-button',
268
- color='link',
269
- size='sm',
270
- ),
271
- dbc.Input(
272
- id='metric-keys-input',
273
- placeholder=placeholders['mk'],
274
- type='text',
275
- value='',
276
- list='metric-keys-list',
277
- ),
278
- ],
279
- size=input_group_sizes['mk'],
280
- ),
281
- width=4,
282
- ),
283
- dbc.Col(
284
- dbc.InputGroup(
285
- [
286
- dbc.Button(
287
- 'Clear',
288
- id='clear-location-keys-input-button',
289
- color='link',
290
- size='sm',
291
- ),
292
- dbc.Input(
293
- id='location-keys-input',
294
- placeholder=placeholders['lk'],
295
- type='text',
296
- value='',
297
- list='location-keys-list',
298
- ),
299
- ],
300
- size=input_group_sizes['lk'],
301
- ),
302
- width=4,
303
- ),
304
- ]
305
- ),
306
- html.Br(),
307
- dbc.Row(
308
- dbc.Col(
309
- dbc.InputGroup(
310
- [
311
- search_parameters_editor,
312
- ],
313
- size=input_group_sizes['params'],
314
- )
315
- )
316
- ),
317
- ]
318
- )
319
- )
320
-
321
262
  keys_lists_content = html.Div([
322
263
  html.Datalist(id='connector-keys-list'),
323
264
  html.Datalist(id='metric-keys-list'),
324
265
  html.Datalist(id='location-keys-list'),
266
+ html.Datalist(id='tags-list'),
325
267
  ], hidden=True)
@@ -9,7 +9,8 @@ import meerschaum.api.dash.pages.error
9
9
  import meerschaum.api.dash.pages.login
10
10
  import meerschaum.api.dash.pages.dashboard
11
11
  import meerschaum.api.dash.pages.plugins
12
+ import meerschaum.api.dash.pages.tokens
12
13
  import meerschaum.api.dash.pages.register
13
14
  import meerschaum.api.dash.pages.pipes
14
- import meerschaum.api.dash.pages.job
15
+ import meerschaum.api.dash.pages.jobs
15
16
  import meerschaum.api.dash.pages.settings