meerschaum 2.1.7__py3-none-any.whl → 2.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.
- meerschaum/__main__.py +1 -1
- meerschaum/_internal/arguments/_parser.py +3 -0
- meerschaum/_internal/entry.py +3 -2
- meerschaum/actions/install.py +7 -3
- meerschaum/actions/show.py +128 -42
- meerschaum/actions/sync.py +7 -3
- meerschaum/api/__init__.py +24 -14
- meerschaum/api/_oauth2.py +4 -4
- meerschaum/api/dash/callbacks/dashboard.py +93 -23
- meerschaum/api/dash/callbacks/jobs.py +55 -3
- meerschaum/api/dash/jobs.py +34 -8
- meerschaum/api/dash/keys.py +1 -1
- meerschaum/api/dash/pages/dashboard.py +14 -4
- meerschaum/api/dash/pipes.py +137 -26
- meerschaum/api/dash/plugins.py +25 -9
- meerschaum/api/resources/static/js/xterm.js +1 -1
- meerschaum/api/resources/templates/termpage.html +3 -0
- meerschaum/api/routes/_login.py +5 -4
- meerschaum/api/routes/_plugins.py +6 -3
- meerschaum/config/_dash.py +11 -0
- meerschaum/config/_default.py +3 -1
- meerschaum/config/_jobs.py +13 -4
- meerschaum/config/_paths.py +2 -0
- meerschaum/config/_sync.py +2 -3
- meerschaum/config/_version.py +1 -1
- meerschaum/config/stack/__init__.py +6 -7
- meerschaum/config/stack/grafana/__init__.py +1 -1
- meerschaum/config/static/__init__.py +4 -1
- meerschaum/connectors/__init__.py +2 -0
- meerschaum/connectors/api/_plugins.py +2 -1
- meerschaum/connectors/sql/SQLConnector.py +4 -2
- meerschaum/connectors/sql/_create_engine.py +9 -9
- meerschaum/connectors/sql/_instance.py +3 -1
- meerschaum/connectors/sql/_pipes.py +54 -38
- meerschaum/connectors/sql/_plugins.py +0 -2
- meerschaum/connectors/sql/_sql.py +7 -9
- meerschaum/core/User/_User.py +158 -16
- meerschaum/core/User/__init__.py +1 -1
- meerschaum/plugins/_Plugin.py +12 -3
- meerschaum/plugins/__init__.py +23 -1
- meerschaum/utils/daemon/Daemon.py +89 -36
- meerschaum/utils/daemon/FileDescriptorInterceptor.py +140 -0
- meerschaum/utils/daemon/RotatingFile.py +130 -14
- meerschaum/utils/daemon/__init__.py +3 -0
- meerschaum/utils/dtypes/__init__.py +9 -5
- meerschaum/utils/packages/__init__.py +21 -5
- meerschaum/utils/packages/_packages.py +18 -20
- meerschaum/utils/process.py +13 -10
- meerschaum/utils/schedule.py +276 -30
- meerschaum/utils/threading.py +1 -0
- meerschaum/utils/typing.py +1 -1
- {meerschaum-2.1.7.dist-info → meerschaum-2.2.0.dist-info}/METADATA +59 -62
- {meerschaum-2.1.7.dist-info → meerschaum-2.2.0.dist-info}/RECORD +59 -57
- {meerschaum-2.1.7.dist-info → meerschaum-2.2.0.dist-info}/WHEEL +1 -1
- {meerschaum-2.1.7.dist-info → meerschaum-2.2.0.dist-info}/LICENSE +0 -0
- {meerschaum-2.1.7.dist-info → meerschaum-2.2.0.dist-info}/NOTICE +0 -0
- {meerschaum-2.1.7.dist-info → meerschaum-2.2.0.dist-info}/entry_points.txt +0 -0
- {meerschaum-2.1.7.dist-info → meerschaum-2.2.0.dist-info}/top_level.txt +0 -0
- {meerschaum-2.1.7.dist-info → meerschaum-2.2.0.dist-info}/zip-safe +0 -0
@@ -11,6 +11,7 @@ import json
|
|
11
11
|
import functools
|
12
12
|
import time
|
13
13
|
import traceback
|
14
|
+
from datetime import datetime, timezone
|
14
15
|
import meerschaum as mrsm
|
15
16
|
from meerschaum.utils.typing import Optional, Dict, Any
|
16
17
|
from meerschaum.api import get_api_connector, endpoints, CHECK_UPDATE
|
@@ -54,9 +55,14 @@ def download_job_logs(n_clicks):
|
|
54
55
|
component_dict = json.loads(ctx[0]['prop_id'].split('.' + 'n_clicks')[0])
|
55
56
|
daemon_id = component_dict['index']
|
56
57
|
daemon = Daemon(daemon_id=daemon_id)
|
58
|
+
now = datetime.now(timezone.utc)
|
59
|
+
filename = (
|
60
|
+
daemon.rotating_log.file_path.name[:(-1 * len('.log'))]
|
61
|
+
+ '_' + str(int(now.timestamp())) + '.log'
|
62
|
+
)
|
57
63
|
return {
|
58
64
|
'content': daemon.log_text,
|
59
|
-
'filename':
|
65
|
+
'filename': filename,
|
60
66
|
}
|
61
67
|
|
62
68
|
|
@@ -74,7 +80,7 @@ def manage_job_button_click(
|
|
74
80
|
session_data: Optional[Dict[str, Any]] = None,
|
75
81
|
):
|
76
82
|
"""
|
77
|
-
Start, stop, or
|
83
|
+
Start, stop, pause, or delete the given job.
|
78
84
|
"""
|
79
85
|
if not n_clicks:
|
80
86
|
raise PreventUpdate
|
@@ -98,12 +104,18 @@ def manage_job_button_click(
|
|
98
104
|
component_dict = json.loads(ctx[0]['prop_id'].split('.' + 'n_clicks')[0])
|
99
105
|
daemon_id = component_dict['index']
|
100
106
|
manage_job_action = component_dict['action']
|
101
|
-
|
107
|
+
try:
|
108
|
+
daemon = Daemon(daemon_id=daemon_id)
|
109
|
+
except Exception as e:
|
110
|
+
daemon = None
|
111
|
+
if daemon is None:
|
112
|
+
raise PreventUpdate
|
102
113
|
|
103
114
|
manage_functions = {
|
104
115
|
'start': functools.partial(daemon.run, allow_dirty_run=True),
|
105
116
|
'stop': daemon.quit,
|
106
117
|
'pause': daemon.pause,
|
118
|
+
'delete': daemon.cleanup,
|
107
119
|
}
|
108
120
|
if manage_job_action not in manage_functions:
|
109
121
|
return (
|
@@ -135,6 +147,46 @@ def manage_job_button_click(
|
|
135
147
|
build_process_timestamps_children(daemon),
|
136
148
|
)
|
137
149
|
|
150
|
+
dash_app.clientside_callback(
|
151
|
+
"""
|
152
|
+
function(n_clicks_arr, url){
|
153
|
+
display_block = {"display": "block"};
|
154
|
+
|
155
|
+
var clicked = false;
|
156
|
+
for (var i = 0; i < n_clicks_arr.length; i++){
|
157
|
+
if (n_clicks_arr[i]){
|
158
|
+
clicked = true;
|
159
|
+
break;
|
160
|
+
}
|
161
|
+
}
|
162
|
+
|
163
|
+
if (!clicked){
|
164
|
+
return dash_clientside.no_update;
|
165
|
+
}
|
166
|
+
|
167
|
+
const triggered_id = dash_clientside.callback_context.triggered_id;
|
168
|
+
const job_daemon_id = triggered_id["index"];
|
169
|
+
|
170
|
+
iframe = document.getElementById('webterm-iframe');
|
171
|
+
if (!iframe){ return dash_clientside.no_update; }
|
172
|
+
|
173
|
+
iframe.contentWindow.postMessage(
|
174
|
+
{
|
175
|
+
action: "show",
|
176
|
+
subaction: "logs",
|
177
|
+
subaction_text: job_daemon_id,
|
178
|
+
},
|
179
|
+
url
|
180
|
+
);
|
181
|
+
dash_clientside.set_props("webterm-div", {"style": display_block});
|
182
|
+
return [];
|
183
|
+
}
|
184
|
+
""",
|
185
|
+
Output('content-div-right', 'children'),
|
186
|
+
Input({'type': 'follow-logs-button', 'index': ALL}, 'n_clicks'),
|
187
|
+
State('location', 'href'),
|
188
|
+
)
|
189
|
+
|
138
190
|
|
139
191
|
@dash_app.callback(
|
140
192
|
Output({'type': 'manage-job-buttons-div', 'index': ALL}, 'children'),
|
meerschaum/api/dash/jobs.py
CHANGED
@@ -49,6 +49,17 @@ def get_jobs_cards(state: WebState):
|
|
49
49
|
build_process_timestamps_children(d),
|
50
50
|
id = {'type': 'process-timestamps-div', 'index': d.daemon_id},
|
51
51
|
)
|
52
|
+
follow_logs_button = dbc.DropdownMenuItem(
|
53
|
+
"Follow logs",
|
54
|
+
id = {'type': 'follow-logs-button', 'index': d.daemon_id},
|
55
|
+
)
|
56
|
+
download_logs_button = dbc.DropdownMenuItem(
|
57
|
+
"Download logs",
|
58
|
+
id = {'type': 'job-download-logs-button', 'index': d.daemon_id},
|
59
|
+
)
|
60
|
+
logs_menu_children = (
|
61
|
+
([follow_logs_button] if is_authenticated else []) + [download_logs_button]
|
62
|
+
)
|
52
63
|
header_children = [
|
53
64
|
html.Div(
|
54
65
|
build_status_children(d),
|
@@ -56,11 +67,13 @@ def get_jobs_cards(state: WebState):
|
|
56
67
|
style = {'float': 'left'},
|
57
68
|
),
|
58
69
|
html.Div(
|
59
|
-
dbc.
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
70
|
+
dbc.DropdownMenu(
|
71
|
+
logs_menu_children,
|
72
|
+
label = "Logs",
|
73
|
+
size = "sm",
|
74
|
+
align_end = True,
|
75
|
+
color = "secondary",
|
76
|
+
menu_variant = 'dark',
|
64
77
|
),
|
65
78
|
style = {'float': 'right'},
|
66
79
|
),
|
@@ -74,7 +87,7 @@ def get_jobs_cards(state: WebState):
|
|
74
87
|
className = "card-text job-card-text",
|
75
88
|
style = {"word-wrap": "break-word"}
|
76
89
|
),
|
77
|
-
style={"white-space": "pre-wrap"},
|
90
|
+
style = {"white-space": "pre-wrap"},
|
78
91
|
),
|
79
92
|
html.Div(
|
80
93
|
(
|
@@ -82,7 +95,7 @@ def get_jobs_cards(state: WebState):
|
|
82
95
|
if is_authenticated
|
83
96
|
else []
|
84
97
|
),
|
85
|
-
id={'type': 'manage-job-buttons-div', 'index': d.daemon_id}
|
98
|
+
id = {'type': 'manage-job-buttons-div', 'index': d.daemon_id},
|
86
99
|
),
|
87
100
|
html.Div(id={'type': 'manage-job-alert-div', 'index': d.daemon_id}),
|
88
101
|
]
|
@@ -108,7 +121,7 @@ def build_manage_job_buttons_div_children(daemon: Daemon):
|
|
108
121
|
return [
|
109
122
|
html.Br(),
|
110
123
|
dbc.Row([
|
111
|
-
dbc.Col(button, width=
|
124
|
+
dbc.Col(button, width=6)
|
112
125
|
for button in buttons
|
113
126
|
])
|
114
127
|
]
|
@@ -153,9 +166,22 @@ def build_manage_job_buttons(daemon: Daemon):
|
|
153
166
|
'index': daemon.daemon_id,
|
154
167
|
},
|
155
168
|
)
|
169
|
+
delete_button = dbc.Button(
|
170
|
+
'Delete',
|
171
|
+
size = 'sm',
|
172
|
+
color = 'danger',
|
173
|
+
style = {'width': '100%'},
|
174
|
+
id = {
|
175
|
+
'type': 'manage-job-button',
|
176
|
+
'action': 'delete',
|
177
|
+
'index': daemon.daemon_id,
|
178
|
+
},
|
179
|
+
)
|
156
180
|
buttons = []
|
157
181
|
if daemon.status in ('stopped', 'paused'):
|
158
182
|
buttons.append(start_button)
|
183
|
+
if daemon.status == 'stopped':
|
184
|
+
buttons.append(delete_button)
|
159
185
|
if daemon.status in ('running',):
|
160
186
|
buttons.append(pause_button)
|
161
187
|
if daemon.status in ('running', 'paused'):
|
meerschaum/api/dash/keys.py
CHANGED
@@ -11,12 +11,22 @@ from meerschaum.config import __doc__ as doc, get_config
|
|
11
11
|
from meerschaum.utils.misc import get_connector_labels
|
12
12
|
from meerschaum.utils.packages import attempt_import, import_html, import_dcc, import_pandas
|
13
13
|
from meerschaum.api import endpoints, CHECK_UPDATE
|
14
|
-
|
15
|
-
|
14
|
+
(
|
15
|
+
dex,
|
16
|
+
px,
|
17
|
+
daq,
|
18
|
+
dbc,
|
19
|
+
) = attempt_import(
|
20
|
+
'dash_extensions',
|
21
|
+
'plotly.express',
|
22
|
+
'dash_daq',
|
23
|
+
'dash_bootstrap_components',
|
24
|
+
lazy = False,
|
25
|
+
warn = False,
|
26
|
+
check_update = CHECK_UPDATE,
|
27
|
+
)
|
16
28
|
html, dcc = import_html(check_update=CHECK_UPDATE), import_dcc(check_update=CHECK_UPDATE)
|
17
29
|
pd = import_pandas(check_update=CHECK_UPDATE)
|
18
|
-
px = attempt_import('plotly.express', warn=False, check_update=CHECK_UPDATE)
|
19
|
-
daq = attempt_import('dash_daq', warn=False, check_update=CHECK_UPDATE)
|
20
30
|
|
21
31
|
from meerschaum.api.dash.components import (
|
22
32
|
go_button,
|
meerschaum/api/dash/pipes.py
CHANGED
@@ -24,6 +24,7 @@ from meerschaum.api.dash import (
|
|
24
24
|
from meerschaum.api.dash.connectors import get_web_connector
|
25
25
|
from meerschaum.api.dash.components import alert_from_success_tuple
|
26
26
|
from meerschaum.api.dash.users import is_session_authenticated
|
27
|
+
from meerschaum.config import get_config
|
27
28
|
import meerschaum as mrsm
|
28
29
|
dbc = attempt_import('dash_bootstrap_components', lazy=False, check_update=CHECK_UPDATE)
|
29
30
|
dash_ace = attempt_import('dash_ace', lazy=False, check_update=CHECK_UPDATE)
|
@@ -110,43 +111,127 @@ def get_pipes_cards(*keys, session_data: Optional[Dict[str, Any]] = None):
|
|
110
111
|
session_id = (session_data or {}).get('session-id', None)
|
111
112
|
authenticated = is_session_authenticated(str(session_id))
|
112
113
|
|
113
|
-
|
114
|
-
alerts = [alert_from_success_tuple(
|
115
|
-
if not isinstance(
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
114
|
+
pipes = pipes_from_state(*keys, as_list=True)
|
115
|
+
alerts = [alert_from_success_tuple(pipes)]
|
116
|
+
if not isinstance(pipes, list):
|
117
|
+
pipes = []
|
118
|
+
|
119
|
+
max_num_pipes_cards = get_config('dash', 'max_num_pipes_cards')
|
120
|
+
overflow_pipes = pipes[max_num_pipes_cards:]
|
121
|
+
|
122
|
+
for pipe in pipes[:max_num_pipes_cards]:
|
123
|
+
meta_str = json.dumps(pipe.meta)
|
124
|
+
footer_children = dbc.Row(
|
125
|
+
[
|
126
|
+
dbc.Col(
|
127
|
+
(
|
128
|
+
dbc.DropdownMenu(
|
129
|
+
label = "Manage",
|
130
|
+
children = [
|
131
|
+
dbc.DropdownMenuItem(
|
132
|
+
'Delete',
|
133
|
+
id = {
|
134
|
+
'type': 'manage-pipe-button',
|
135
|
+
'index': meta_str,
|
136
|
+
'action': 'delete',
|
137
|
+
},
|
138
|
+
),
|
139
|
+
dbc.DropdownMenuItem(
|
140
|
+
'Drop',
|
141
|
+
id = {
|
142
|
+
'type': 'manage-pipe-button',
|
143
|
+
'index': meta_str,
|
144
|
+
'action': 'drop',
|
145
|
+
},
|
146
|
+
),
|
147
|
+
dbc.DropdownMenuItem(
|
148
|
+
'Clear',
|
149
|
+
id = {
|
150
|
+
'type': 'manage-pipe-button',
|
151
|
+
'index': meta_str,
|
152
|
+
'action': 'clear',
|
153
|
+
},
|
154
|
+
),
|
155
|
+
dbc.DropdownMenuItem(
|
156
|
+
'Verify',
|
157
|
+
id = {
|
158
|
+
'type': 'manage-pipe-button',
|
159
|
+
'index': meta_str,
|
160
|
+
'action': 'verify',
|
161
|
+
},
|
162
|
+
),
|
163
|
+
dbc.DropdownMenuItem(
|
164
|
+
'Sync',
|
165
|
+
id = {
|
166
|
+
'type': 'manage-pipe-button',
|
167
|
+
'index': meta_str,
|
168
|
+
'action': 'sync',
|
169
|
+
},
|
170
|
+
),
|
171
|
+
],
|
172
|
+
direction = "up",
|
173
|
+
menu_variant = "dark",
|
174
|
+
size = 'sm',
|
175
|
+
color = 'secondary',
|
176
|
+
)
|
177
|
+
) if authenticated else [],
|
178
|
+
width = 2,
|
179
|
+
),
|
180
|
+
dbc.Col(width=6),
|
181
|
+
dbc.Col(
|
182
|
+
dbc.Button(
|
183
|
+
'Download CSV',
|
184
|
+
size = 'sm',
|
185
|
+
color = 'link',
|
186
|
+
style = {'float': 'right'},
|
187
|
+
id = {'type': 'pipe-download-csv-button', 'index': meta_str},
|
188
|
+
),
|
189
|
+
width = 4,
|
190
|
+
),
|
191
|
+
],
|
192
|
+
justify = 'start',
|
193
|
+
)
|
128
194
|
card_body_children = [
|
129
195
|
html.H5(
|
130
|
-
html.B(str(
|
196
|
+
html.B(str(pipe)),
|
131
197
|
className = 'card-title',
|
132
198
|
style = {'font-family': ['monospace']}
|
133
199
|
),
|
134
200
|
html.Div(
|
135
201
|
dbc.Accordion(
|
136
|
-
accordion_items_from_pipe(
|
202
|
+
accordion_items_from_pipe(pipe, authenticated=authenticated),
|
137
203
|
flush = True,
|
138
204
|
start_collapsed = True,
|
139
|
-
id = {'type': 'pipe-accordion', 'index':
|
205
|
+
id = {'type': 'pipe-accordion', 'index': meta_str},
|
140
206
|
)
|
141
207
|
)
|
142
208
|
|
143
209
|
]
|
144
210
|
cards.append(
|
145
|
-
dbc.Card(
|
211
|
+
dbc.Card([
|
146
212
|
dbc.CardBody(children=card_body_children),
|
147
213
|
dbc.CardFooter(children=footer_children),
|
148
214
|
])
|
149
215
|
)
|
216
|
+
|
217
|
+
if overflow_pipes:
|
218
|
+
cards.append(
|
219
|
+
dbc.Card([
|
220
|
+
dbc.CardBody(
|
221
|
+
html.Ul(
|
222
|
+
[
|
223
|
+
html.Li(html.H5(
|
224
|
+
html.B(str(pipe)),
|
225
|
+
className = 'card-title',
|
226
|
+
style = {'font-family': ['monospace']}
|
227
|
+
))
|
228
|
+
for pipe in overflow_pipes
|
229
|
+
]
|
230
|
+
)
|
231
|
+
)
|
232
|
+
])
|
233
|
+
)
|
234
|
+
|
150
235
|
return cards, alerts
|
151
236
|
|
152
237
|
|
@@ -188,14 +273,40 @@ def accordion_items_from_pipe(
|
|
188
273
|
overview_header = [html.Thead(html.Tr([html.Th("Attribute"), html.Th("Value")]))]
|
189
274
|
dt_name, id_name, val_name = pipe.get_columns('datetime', 'id', 'value', error=False)
|
190
275
|
overview_rows = [
|
191
|
-
html.Tr([html.Td("Connector"), html.Td(f"{pipe.connector_keys}")]),
|
192
|
-
html.Tr([html.Td("Metric"), html.Td(f"{pipe.metric_key}")]),
|
193
|
-
html.Tr([html.Td("Location"), html.Td(f"{pipe.location_key}")]),
|
194
|
-
html.Tr([html.Td("Instance"), html.Td(f"{pipe.instance_keys}")]),
|
195
|
-
html.Tr([html.Td("Target Table"), html.Td(f"{pipe.target}")]),
|
276
|
+
html.Tr([html.Td("Connector"), html.Td(html.Pre(f"{pipe.connector_keys}"))]),
|
277
|
+
html.Tr([html.Td("Metric"), html.Td(html.Pre(f"{pipe.metric_key}"))]),
|
278
|
+
html.Tr([html.Td("Location"), html.Td(html.Pre(f"{pipe.location_key}"))]),
|
279
|
+
html.Tr([html.Td("Instance"), html.Td(html.Pre(f"{pipe.instance_keys}"))]),
|
280
|
+
html.Tr([html.Td("Target Table"), html.Td(html.Pre(f"{pipe.target}"))]),
|
196
281
|
]
|
197
|
-
|
198
|
-
|
282
|
+
columns = pipe.columns.copy()
|
283
|
+
if columns:
|
284
|
+
datetime_index = columns.pop('datetime', None)
|
285
|
+
columns_items = []
|
286
|
+
if datetime_index:
|
287
|
+
columns_items.append(html.Li(f"{datetime_index} (datetime)"))
|
288
|
+
columns_items.extend([
|
289
|
+
html.Li(f"{col}")
|
290
|
+
for col_key, col in columns.items()
|
291
|
+
])
|
292
|
+
overview_rows.append(
|
293
|
+
html.Tr([
|
294
|
+
html.Td("Indices" if len(columns_items) != 1 else "Index"),
|
295
|
+
html.Td(html.Pre(html.Ul(columns_items))),
|
296
|
+
])
|
297
|
+
)
|
298
|
+
tags = pipe.tags
|
299
|
+
if tags:
|
300
|
+
tags_items = html.Ul([
|
301
|
+
html.Li(tag)
|
302
|
+
for tag in tags
|
303
|
+
])
|
304
|
+
overview_rows.append(
|
305
|
+
html.Tr([
|
306
|
+
html.Td("Tags"),
|
307
|
+
html.Td(html.Pre(tags_items)),
|
308
|
+
])
|
309
|
+
)
|
199
310
|
|
200
311
|
items_bodies['overview'] = dbc.Table(
|
201
312
|
overview_header + [html.Tbody(overview_rows)],
|
meerschaum/api/dash/plugins.py
CHANGED
@@ -32,17 +32,22 @@ def get_plugins_cards(
|
|
32
32
|
desc = get_api_connector().get_plugin_attributes(plugin).get(
|
33
33
|
'description', 'No description provided.'
|
34
34
|
)
|
35
|
-
desc_textarea_kw =
|
36
|
-
value
|
37
|
-
|
38
|
-
|
39
|
-
|
35
|
+
desc_textarea_kw = {
|
36
|
+
'value': desc,
|
37
|
+
'readOnly': True,
|
38
|
+
'debounce': False,
|
39
|
+
'className': 'plugin-description',
|
40
|
+
'draggable': False,
|
41
|
+
'wrap': 'overflow',
|
42
|
+
'placeholder': "Edit the plugin's description",
|
43
|
+
'id': {'type': 'description-textarea', 'index': plugin_name},
|
44
|
+
}
|
40
45
|
|
41
46
|
card_body_children = [html.H4(plugin_name)]
|
42
47
|
|
43
48
|
if is_plugin_owner(plugin_name, session_data):
|
44
49
|
desc_textarea_kw['readOnly'] = False
|
45
|
-
card_body_children
|
50
|
+
card_body_children.append(dbc.Textarea(**desc_textarea_kw))
|
46
51
|
if not desc_textarea_kw['readOnly']:
|
47
52
|
card_body_children += [
|
48
53
|
dbc.Button(
|
@@ -53,12 +58,23 @@ def get_plugins_cards(
|
|
53
58
|
),
|
54
59
|
html.Div(id={'type': 'edit-alert-div', 'index': plugin_name}),
|
55
60
|
]
|
56
|
-
|
61
|
+
plugin_username = get_api_connector().get_plugin_username(plugin, debug=debug)
|
62
|
+
plugin_version = get_api_connector().get_plugin_version(plugin, debug=debug) or ' '
|
57
63
|
card_children = [
|
58
|
-
dbc.CardHeader(
|
64
|
+
dbc.CardHeader(
|
65
|
+
[
|
66
|
+
dbc.Row(
|
67
|
+
[
|
68
|
+
dbc.Col(html.A('👤 ' + str(plugin_username), href='#')),
|
69
|
+
dbc.Col(html.Pre(str(plugin_version), style={'text-align': 'right'})),
|
70
|
+
],
|
71
|
+
justify = 'between',
|
72
|
+
),
|
73
|
+
],
|
74
|
+
),
|
59
75
|
dbc.CardBody(card_body_children),
|
60
76
|
dbc.CardFooter([
|
61
|
-
html.A('⬇️ Download
|
77
|
+
html.A('⬇️ Download', href=(endpoints['plugins'] + '/' + plugin_name))
|
62
78
|
]),
|
63
79
|
]
|
64
80
|
cards.append(
|