ansys-solutions-dash-super-components 0.2.dev7__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.
- ansys/solutions/dash_super_components/__init__.py +44 -0
- ansys/solutions/dash_super_components/assets/__init__.py +20 -0
- ansys/solutions/dash_super_components/assets/dashAgGridComponentFunctions.js +71 -0
- ansys/solutions/dash_super_components/authenticator.py +277 -0
- ansys/solutions/dash_super_components/dual_input_range_slider.py +397 -0
- ansys/solutions/dash_super_components/folder_selector.py +830 -0
- ansys/solutions/dash_super_components/input_form.py +908 -0
- ansys/solutions/dash_super_components/input_row_array.py +368 -0
- ansys/solutions/dash_super_components/logs_supervisor.py +887 -0
- ansys/solutions/dash_super_components/transaction_method_status_badge.py +366 -0
- ansys/solutions/dash_super_components/transaction_supervisor.py +587 -0
- ansys/solutions/dash_super_components/tree.py +396 -0
- ansys/solutions/dash_super_components/utils/__init__.py +18 -0
- ansys/solutions/dash_super_components/utils/aio_ids.py +83 -0
- ansys/solutions/dash_super_components/utils/api_requests.py +69 -0
- ansys/solutions/dash_super_components/utils/assets_server.py +52 -0
- ansys/solutions/dash_super_components/utils/config.py +77 -0
- ansys/solutions/dash_super_components/utils/logs_parser.py +151 -0
- ansys/solutions/dash_super_components/utils/path_validator.py +163 -0
- ansys/solutions/dash_super_components/utils/status_badge_properties.py +60 -0
- ansys/solutions/dash_super_components/utils/svg_icons.py +395 -0
- ansys_solutions_dash_super_components-0.2.dev7.dist-info/METADATA +169 -0
- ansys_solutions_dash_super_components-0.2.dev7.dist-info/RECORD +26 -0
- ansys_solutions_dash_super_components-0.2.dev7.dist-info/WHEEL +4 -0
- ansys_solutions_dash_super_components-0.2.dev7.dist-info/licenses/AUTHORS +12 -0
- ansys_solutions_dash_super_components-0.2.dev7.dist-info/licenses/LICENSE +201 -0
|
@@ -0,0 +1,908 @@
|
|
|
1
|
+
# Copyright (C) 2026 ANSYS, Inc. and/or its affiliates.
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
#
|
|
4
|
+
#
|
|
5
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
# you may not use this file except in compliance with the License.
|
|
7
|
+
# You may obtain a copy of the License at
|
|
8
|
+
#
|
|
9
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
#
|
|
11
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
# See the License for the specific language governing permissions and
|
|
15
|
+
# limitations under the License.
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
"""Provide a Dash component for quickly crafting input forms."""
|
|
19
|
+
|
|
20
|
+
from typing import Any
|
|
21
|
+
import uuid
|
|
22
|
+
|
|
23
|
+
try:
|
|
24
|
+
# dash >=3.2.0
|
|
25
|
+
from dash import NoUpdate
|
|
26
|
+
except ImportError:
|
|
27
|
+
# dash >=2.18.2, <3.2.0
|
|
28
|
+
from dash._callback import NoUpdate # pyright: ignore[reportPrivateImportUsage]
|
|
29
|
+
from dash.exceptions import PreventUpdate
|
|
30
|
+
|
|
31
|
+
try:
|
|
32
|
+
# dash-extensions < 2.0.5
|
|
33
|
+
from dash_extensions.enrich import _Wildcard # pyright: ignore[reportAttributeAccessIssue]
|
|
34
|
+
except ImportError:
|
|
35
|
+
# dash-extensions >= 2.0.5
|
|
36
|
+
from dash_extensions.enrich import (
|
|
37
|
+
Wildcard as _Wildcard, # pyright: ignore[reportAttributeAccessIssue]
|
|
38
|
+
)
|
|
39
|
+
from dash_extensions.enrich import (
|
|
40
|
+
MATCH,
|
|
41
|
+
Input,
|
|
42
|
+
Output,
|
|
43
|
+
State,
|
|
44
|
+
callback,
|
|
45
|
+
dcc,
|
|
46
|
+
html,
|
|
47
|
+
no_update,
|
|
48
|
+
)
|
|
49
|
+
import dash_mantine_components as dmc
|
|
50
|
+
|
|
51
|
+
from ansys.solutions.dash_super_components.utils.aio_ids import AIOIds
|
|
52
|
+
from ansys.solutions.dash_super_components.utils.svg_icons import IconNames, create_base64_svg_src
|
|
53
|
+
|
|
54
|
+
DEFAULT_COLUMNS_AND_WIDTHS = {
|
|
55
|
+
"label": 2,
|
|
56
|
+
"fields": 5,
|
|
57
|
+
"unit": 1,
|
|
58
|
+
"description": 3,
|
|
59
|
+
"help": 1,
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
COLUMN_STYLES = {
|
|
63
|
+
"label": {
|
|
64
|
+
"display": "flex",
|
|
65
|
+
"alignItems": "center",
|
|
66
|
+
},
|
|
67
|
+
"unit": {
|
|
68
|
+
"display": "flex",
|
|
69
|
+
"alignItems": "center",
|
|
70
|
+
"justifyContent": "center",
|
|
71
|
+
},
|
|
72
|
+
"description": {
|
|
73
|
+
"display": "flex",
|
|
74
|
+
"alignItems": "center",
|
|
75
|
+
},
|
|
76
|
+
"help": {
|
|
77
|
+
"display": "flex",
|
|
78
|
+
"alignItems": "center",
|
|
79
|
+
"justifyContent": "center",
|
|
80
|
+
},
|
|
81
|
+
"fields": {},
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
SUPPORTED_FIELD_TYPES = [
|
|
85
|
+
"NumberInput",
|
|
86
|
+
"TextInput",
|
|
87
|
+
"PasswordInput",
|
|
88
|
+
"Checkbox",
|
|
89
|
+
"Select",
|
|
90
|
+
"MultiSelect",
|
|
91
|
+
"Switch",
|
|
92
|
+
"Slider",
|
|
93
|
+
]
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def make_form(
|
|
97
|
+
aio_id: str,
|
|
98
|
+
items: list[dict[str, Any]],
|
|
99
|
+
columns: list[str],
|
|
100
|
+
column_names: list[str],
|
|
101
|
+
custom_column_widths: dict[str, int],
|
|
102
|
+
persistence: bool,
|
|
103
|
+
persistence_type: str,
|
|
104
|
+
) -> html.Div:
|
|
105
|
+
"""Create the form layout for an :class:`InputForm` component.
|
|
106
|
+
|
|
107
|
+
Parameters
|
|
108
|
+
----------
|
|
109
|
+
aio_id : str
|
|
110
|
+
The unique identifier for the parent ``InputForm`` instance.
|
|
111
|
+
items : list of dict
|
|
112
|
+
Row definitions passed to the ``InputForm``.
|
|
113
|
+
columns : list of str
|
|
114
|
+
Ordered list of column names to include in each row.
|
|
115
|
+
column_names : list of str
|
|
116
|
+
Header labels for each column; pass an empty list to omit headers.
|
|
117
|
+
custom_column_widths : dict[str, int]
|
|
118
|
+
Mapping of column names to custom widths. Columns not present use
|
|
119
|
+
the default widths from :data:`DEFAULT_COLUMNS_AND_WIDTHS`.
|
|
120
|
+
persistence : bool
|
|
121
|
+
Whether to enable persistence for the form fields.
|
|
122
|
+
persistence_type : str
|
|
123
|
+
The type of persistence to use.
|
|
124
|
+
|
|
125
|
+
Returns
|
|
126
|
+
-------
|
|
127
|
+
html.Div
|
|
128
|
+
A ``Div`` containing the assembled grid and any help modals.
|
|
129
|
+
"""
|
|
130
|
+
column_widths = compute_final_column_widths(columns, custom_column_widths)
|
|
131
|
+
total_column_width = sum(column_widths.values())
|
|
132
|
+
|
|
133
|
+
cells: list[dmc.GridCol] = []
|
|
134
|
+
modals: list[dmc.Modal] = []
|
|
135
|
+
|
|
136
|
+
# add header row if column names are provided
|
|
137
|
+
if column_names:
|
|
138
|
+
cells.extend(_create_header_cells(columns, column_names, column_widths))
|
|
139
|
+
|
|
140
|
+
for row_counter, item in enumerate(items):
|
|
141
|
+
# if the row has an explicit ID, use it as row_index, otherwise use the row_counter:
|
|
142
|
+
row_index = item.get("id", row_counter)
|
|
143
|
+
row_cells, modal = make_row(
|
|
144
|
+
aio_id,
|
|
145
|
+
item,
|
|
146
|
+
columns,
|
|
147
|
+
column_widths,
|
|
148
|
+
row_index,
|
|
149
|
+
persistence,
|
|
150
|
+
persistence_type,
|
|
151
|
+
)
|
|
152
|
+
cells.extend(row_cells)
|
|
153
|
+
modals.append(modal)
|
|
154
|
+
|
|
155
|
+
grid = dmc.Grid(cells, gutter="xs", grow=False, columns=total_column_width)
|
|
156
|
+
|
|
157
|
+
content = [grid, *modals]
|
|
158
|
+
|
|
159
|
+
return html.Div(content)
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def _create_header_cells(
|
|
163
|
+
columns: list[str],
|
|
164
|
+
column_names: list[str],
|
|
165
|
+
column_widths: dict[str, int],
|
|
166
|
+
) -> list[dmc.GridCol]:
|
|
167
|
+
_validate_column_names(column_names, columns)
|
|
168
|
+
|
|
169
|
+
header_cells: list[dmc.GridCol] = []
|
|
170
|
+
for column, column_name in zip(columns, column_names, strict=False):
|
|
171
|
+
header_cells.append(
|
|
172
|
+
dmc.GridCol(
|
|
173
|
+
dmc.Text(column_name, fw=700), # type: ignore - Dash Mantine Components typing issue
|
|
174
|
+
span=column_widths[column],
|
|
175
|
+
),
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
return header_cells
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def make_row(
|
|
182
|
+
aio_id: str,
|
|
183
|
+
item: dict[str, Any],
|
|
184
|
+
columns: list[str],
|
|
185
|
+
column_widths: dict[str, int],
|
|
186
|
+
row_index: int | str,
|
|
187
|
+
persistence: bool,
|
|
188
|
+
persistence_type: str,
|
|
189
|
+
) -> tuple[list[dmc.GridCol], dmc.Modal]:
|
|
190
|
+
"""Create the grid cells and help modal for a single form row.
|
|
191
|
+
|
|
192
|
+
Parameters
|
|
193
|
+
----------
|
|
194
|
+
aio_id : str
|
|
195
|
+
The unique identifier for the parent ``InputForm`` instance.
|
|
196
|
+
item : dict
|
|
197
|
+
Row definition dictionary with optional keys: ``label``, ``unit``,
|
|
198
|
+
``description``, ``help``, and required key ``fields``.
|
|
199
|
+
columns : list of str
|
|
200
|
+
Ordered list of column names to render for this row.
|
|
201
|
+
column_widths : dict[str, int]
|
|
202
|
+
Mapping of column names to grid span widths.
|
|
203
|
+
row_index : int or str
|
|
204
|
+
Index used to generate unique subcomponent IDs for this row.
|
|
205
|
+
persistence : bool
|
|
206
|
+
Whether to enable persistence for the form fields in this row.
|
|
207
|
+
persistence_type : str
|
|
208
|
+
The type of persistence to use for the form fields in this row.
|
|
209
|
+
|
|
210
|
+
Returns
|
|
211
|
+
-------
|
|
212
|
+
tuple[list of dmc.GridCol, dmc.Modal]
|
|
213
|
+
A tuple of ``(cells, modal)`` where *cells* is the list of grid
|
|
214
|
+
columns for the row and *modal* is the associated help modal.
|
|
215
|
+
"""
|
|
216
|
+
cells: list[dmc.GridCol] = []
|
|
217
|
+
for column in columns:
|
|
218
|
+
if column == "label":
|
|
219
|
+
column_content = _create_label_column(aio_id, item, row_index)
|
|
220
|
+
elif column == "unit":
|
|
221
|
+
column_content = _create_unit_column(aio_id, item, row_index)
|
|
222
|
+
elif column == "description":
|
|
223
|
+
column_content = _create_description_column(aio_id, item, row_index)
|
|
224
|
+
elif column == "help":
|
|
225
|
+
column_content = _create_help_column(aio_id, item, row_index)
|
|
226
|
+
elif column == "fields":
|
|
227
|
+
column_content = _create_input_fields_column(
|
|
228
|
+
aio_id, item, row_index, persistence, persistence_type
|
|
229
|
+
)
|
|
230
|
+
else:
|
|
231
|
+
raise ValueError(f"Unknown column: {column}")
|
|
232
|
+
|
|
233
|
+
cells.append(
|
|
234
|
+
dmc.GridCol(
|
|
235
|
+
column_content,
|
|
236
|
+
span=column_widths[column],
|
|
237
|
+
style=COLUMN_STYLES[column],
|
|
238
|
+
),
|
|
239
|
+
)
|
|
240
|
+
|
|
241
|
+
modal = _create_modal(aio_id, item, row_index)
|
|
242
|
+
|
|
243
|
+
return cells, modal
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def _create_label_column(
|
|
247
|
+
aio_id: str,
|
|
248
|
+
item: dict[str, Any],
|
|
249
|
+
row_index: str | int,
|
|
250
|
+
) -> dmc.Text:
|
|
251
|
+
return dmc.Text(item.get("label"), id=InputForm.ids.label(aio_id, row_index))
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
def _create_unit_column(
|
|
255
|
+
aio_id: str,
|
|
256
|
+
item: dict[str, Any],
|
|
257
|
+
row_index: str | int,
|
|
258
|
+
) -> dmc.Text:
|
|
259
|
+
return dmc.Text(
|
|
260
|
+
(
|
|
261
|
+
dcc.Markdown(
|
|
262
|
+
f"${item['unit']}$",
|
|
263
|
+
mathjax=True,
|
|
264
|
+
)
|
|
265
|
+
if "unit" in item
|
|
266
|
+
else None
|
|
267
|
+
),
|
|
268
|
+
id=InputForm.ids.unit(aio_id, row_index),
|
|
269
|
+
style={
|
|
270
|
+
"display": "flex",
|
|
271
|
+
"justifyContent": "center",
|
|
272
|
+
"alignItems": "center",
|
|
273
|
+
},
|
|
274
|
+
)
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
def _create_description_column(
|
|
278
|
+
aio_id: str,
|
|
279
|
+
item: dict[str, Any],
|
|
280
|
+
row_index: str | int,
|
|
281
|
+
) -> dmc.Text:
|
|
282
|
+
return dmc.Text(item.get("description"), id=InputForm.ids.description(aio_id, row_index))
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
def _create_help_column(
|
|
286
|
+
aio_id: str,
|
|
287
|
+
item: dict[str, Any],
|
|
288
|
+
row_index: int | str,
|
|
289
|
+
) -> dmc.ActionIcon:
|
|
290
|
+
return dmc.ActionIcon(
|
|
291
|
+
html.Img(src=create_base64_svg_src(IconNames.MATERIAL_HELP, color="#fab005")),
|
|
292
|
+
size="xl",
|
|
293
|
+
variant="subtle",
|
|
294
|
+
color="yellow",
|
|
295
|
+
id=InputForm.ids.help(aio_id, row_index),
|
|
296
|
+
style={
|
|
297
|
+
"display": "flex" if item.get("help") else "none",
|
|
298
|
+
"textAlign": "center",
|
|
299
|
+
},
|
|
300
|
+
)
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
def _create_modal(aio_id: str, item: dict[str, Any], row_index: int | str) -> dmc.Modal:
|
|
304
|
+
return dmc.Modal(
|
|
305
|
+
id=InputForm.ids.modal(aio_id, row_index),
|
|
306
|
+
title=item.get("label"),
|
|
307
|
+
zIndex=10000,
|
|
308
|
+
children=item.get("help"),
|
|
309
|
+
size="50%", # type: ignore - Dash Mantine Components typing issue
|
|
310
|
+
centered=True,
|
|
311
|
+
)
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
def _create_input_fields_column(
|
|
315
|
+
aio_id: str,
|
|
316
|
+
item: dict[str, Any],
|
|
317
|
+
row_index: int | str,
|
|
318
|
+
persistence: bool,
|
|
319
|
+
persistence_type: str,
|
|
320
|
+
) -> html.Div:
|
|
321
|
+
input_fields: list[html.Div] = []
|
|
322
|
+
|
|
323
|
+
for field in item["fields"]:
|
|
324
|
+
field_type = field["type"]
|
|
325
|
+
identifier = field["id"]
|
|
326
|
+
div_id = InputForm.ids.field_div(aio_id, field_type, identifier, row_index)
|
|
327
|
+
field_id = InputForm.ids.field(aio_id, field_type, identifier, row_index)
|
|
328
|
+
|
|
329
|
+
div_style = {"width": "100%"}
|
|
330
|
+
if "hidden" in field and field["hidden"]:
|
|
331
|
+
div_style["display"] = "none"
|
|
332
|
+
|
|
333
|
+
field_properties = {k: v for k, v in field.items() if k not in ["hidden", "type", "id"]}
|
|
334
|
+
|
|
335
|
+
if "persistence" not in field_properties:
|
|
336
|
+
field_properties["persistence"] = persistence
|
|
337
|
+
if "persistence_type" not in field_properties:
|
|
338
|
+
field_properties["persistence_type"] = persistence_type
|
|
339
|
+
|
|
340
|
+
if "style" in field_properties:
|
|
341
|
+
if "width" not in field_properties["style"]:
|
|
342
|
+
field_properties["style"]["width"] = "100%"
|
|
343
|
+
else:
|
|
344
|
+
field_properties["style"] = {"width": "100%"}
|
|
345
|
+
|
|
346
|
+
input_fields.append(
|
|
347
|
+
_create_field(field_id, field_type, field_properties, div_id, div_style)
|
|
348
|
+
)
|
|
349
|
+
|
|
350
|
+
return html.Div(
|
|
351
|
+
input_fields,
|
|
352
|
+
style={
|
|
353
|
+
"display": "flex",
|
|
354
|
+
"flexDirection": "row",
|
|
355
|
+
"justifyContent": "space-between",
|
|
356
|
+
"alignItems": "center",
|
|
357
|
+
"gap": "10px",
|
|
358
|
+
},
|
|
359
|
+
)
|
|
360
|
+
|
|
361
|
+
|
|
362
|
+
def _create_field(
|
|
363
|
+
field_id: dict[str, Any],
|
|
364
|
+
field_type: str,
|
|
365
|
+
field_properties: dict[str, Any],
|
|
366
|
+
div_id: dict[str, Any],
|
|
367
|
+
div_style: dict[str, Any],
|
|
368
|
+
) -> html.Div:
|
|
369
|
+
if field_type == "NumberInput":
|
|
370
|
+
field_content = dmc.NumberInput(id=field_id, **field_properties)
|
|
371
|
+
elif field_type == "TextInput":
|
|
372
|
+
field_content = dmc.TextInput(id=field_id, **field_properties)
|
|
373
|
+
elif field_type == "Checkbox":
|
|
374
|
+
field_content = dmc.Checkbox(id=field_id, **field_properties)
|
|
375
|
+
elif field_type == "Select":
|
|
376
|
+
field_content = dmc.Select(id=field_id, **field_properties)
|
|
377
|
+
elif field_type == "MultiSelect":
|
|
378
|
+
field_content = dmc.MultiSelect(id=field_id, **field_properties)
|
|
379
|
+
elif field_type == "PasswordInput":
|
|
380
|
+
field_content = dmc.PasswordInput(id=field_id, **field_properties)
|
|
381
|
+
elif field_type == "Slider":
|
|
382
|
+
field_content = dmc.Slider(id=field_id, **field_properties)
|
|
383
|
+
elif field_type == "Switch":
|
|
384
|
+
field_content = dmc.Switch(id=field_id, **field_properties)
|
|
385
|
+
else:
|
|
386
|
+
raise ValueError(f"Unsupported field type: {field_type}")
|
|
387
|
+
|
|
388
|
+
return html.Div(
|
|
389
|
+
id=div_id,
|
|
390
|
+
children=field_content,
|
|
391
|
+
style=div_style,
|
|
392
|
+
)
|
|
393
|
+
|
|
394
|
+
|
|
395
|
+
class InputForm(html.Div):
|
|
396
|
+
"""
|
|
397
|
+
All-in-one component based on Dash Mantine Components to create input forms.
|
|
398
|
+
|
|
399
|
+
Each row of the form is defined by a dictionary with the following keys:
|
|
400
|
+
|
|
401
|
+
- ``fields`` *(required)*: list of field dicts. Each field dict must contain
|
|
402
|
+
``"type"`` and ``"id"`` keys plus any additional properties accepted by the
|
|
403
|
+
underlying Dash Mantine component. Supported types: ``NumberInput``,
|
|
404
|
+
``TextInput``, ``PasswordInput``, ``Checkbox``, ``Select``, ``MultiSelect``,
|
|
405
|
+
``Slider``, ``Switch``.
|
|
406
|
+
- ``label`` *(optional)*: str — label text shown in the label column.
|
|
407
|
+
- ``unit`` *(optional)*: str — LaTeX-rendered unit shown in the unit column.
|
|
408
|
+
- ``description`` *(optional)*: str — description text shown in the description column.
|
|
409
|
+
- ``help`` *(optional)*: Dash component(s), str, or number — content shown in a
|
|
410
|
+
modal when the help icon is clicked.
|
|
411
|
+
- ``id`` *(optional)*: str — explicit row identifier used as the ``row_index`` in
|
|
412
|
+
component IDs. When omitted, the zero-based row position is used instead.
|
|
413
|
+
|
|
414
|
+
UI-side value persistence is disabled by default. It can be enabled globally for
|
|
415
|
+
all fields via the ``persistence`` and ``persistence_type`` constructor parameters.
|
|
416
|
+
Individual fields can override the global setting by including a ``"persistence"``
|
|
417
|
+
key in their field dict.
|
|
418
|
+
|
|
419
|
+
Parameters
|
|
420
|
+
----------
|
|
421
|
+
items : list of dict
|
|
422
|
+
A list of row definitions. Each dict must contain a ``"fields"`` key whose
|
|
423
|
+
value is a list of field dicts (each requiring ``"type"`` and ``"id"``).
|
|
424
|
+
Optional row keys: ``"label"``, ``"unit"``, ``"description"``, ``"help"``,
|
|
425
|
+
``"id"``. Fields also accept a ``"hidden": True`` key to hide the field on
|
|
426
|
+
initialization. To override the global persistence setting for an individual
|
|
427
|
+
field, pass ``"persistence": True`` or ``"persistence": False`` in the field
|
|
428
|
+
dict.
|
|
429
|
+
title : str, optional
|
|
430
|
+
Title displayed above the form.
|
|
431
|
+
with_card : bool, optional
|
|
432
|
+
Whether to wrap the form in a :class:`dmc.Card`. Default is ``True``.
|
|
433
|
+
card_props : dict, optional
|
|
434
|
+
Additional properties forwarded to :class:`dmc.Card`.
|
|
435
|
+
title_props : dict, optional
|
|
436
|
+
Additional properties forwarded to the title :class:`html.P` element.
|
|
437
|
+
column_names : list of str, optional
|
|
438
|
+
Header labels for each column, in the same order as ``columns``. Pass an
|
|
439
|
+
empty list (default) to omit column headers.
|
|
440
|
+
column_widths : dict, optional
|
|
441
|
+
Mapping of column names to integer grid-span widths. Columns omitted here
|
|
442
|
+
use built-in defaults.
|
|
443
|
+
aio_id : str, optional
|
|
444
|
+
Unique identifier for this component instance. A UUID is generated when
|
|
445
|
+
not provided.
|
|
446
|
+
columns : list of str, optional
|
|
447
|
+
Ordered list of column names to display. Supported values: ``"label"``,
|
|
448
|
+
``"fields"``, ``"unit"``, ``"description"``, ``"help"``. Defaults to all
|
|
449
|
+
five columns in the order listed.
|
|
450
|
+
persistence : bool, optional
|
|
451
|
+
Whether to enable UI-side persistence for all form fields. When ``True``,
|
|
452
|
+
user-entered values survive page navigation within the same browser tab.
|
|
453
|
+
Individual fields can override this setting via ``"persistence"`` in the
|
|
454
|
+
field dict. Default is ``False``.
|
|
455
|
+
persistence_type : str, optional
|
|
456
|
+
The Dash persistence storage type to use when ``persistence`` is ``True``.
|
|
457
|
+
Accepted values: ``"memory"`` (cleared on page reload), ``"session"``
|
|
458
|
+
(cleared when the browser tab is closed), ``"local"`` (persists across
|
|
459
|
+
reloads and browser restarts). Default is ``"memory"``.
|
|
460
|
+
"""
|
|
461
|
+
|
|
462
|
+
class InputFormIds(AIOIds):
|
|
463
|
+
"""Provides IDs for the subcomponents of :class:`InputForm`."""
|
|
464
|
+
|
|
465
|
+
@classmethod
|
|
466
|
+
def _make_id_dict_with_row_index(
|
|
467
|
+
cls, subcomponent: str, aio_id: str | _Wildcard, row_index: str | int | _Wildcard
|
|
468
|
+
) -> dict[str, Any]:
|
|
469
|
+
id_dict = cls.make_id_dict("input-form", subcomponent, aio_id)
|
|
470
|
+
id_dict["row_index"] = row_index
|
|
471
|
+
return id_dict
|
|
472
|
+
|
|
473
|
+
@classmethod
|
|
474
|
+
def _make_id_dict_with_field_type_and_identifier_and_row_index(
|
|
475
|
+
cls,
|
|
476
|
+
subcomponent: str,
|
|
477
|
+
aio_id: str | _Wildcard,
|
|
478
|
+
field_type: str | _Wildcard,
|
|
479
|
+
identifier: str | _Wildcard,
|
|
480
|
+
row_index: str | int | _Wildcard,
|
|
481
|
+
) -> dict[str, Any]:
|
|
482
|
+
id_dict = cls.make_id_dict("input-form", subcomponent, aio_id)
|
|
483
|
+
id_dict["row_index"] = row_index
|
|
484
|
+
id_dict["field_type"] = field_type
|
|
485
|
+
id_dict["identifier"] = identifier
|
|
486
|
+
return id_dict
|
|
487
|
+
|
|
488
|
+
@classmethod
|
|
489
|
+
def label(cls, aio_id: str | _Wildcard, row_index: str | int | _Wildcard) -> dict[str, Any]:
|
|
490
|
+
"""
|
|
491
|
+
Return the id for an input form label.
|
|
492
|
+
|
|
493
|
+
Parameters
|
|
494
|
+
----------
|
|
495
|
+
aio_id : str or _Wildcard
|
|
496
|
+
The component's unique identifier.
|
|
497
|
+
row_index : str, int, or _Wildcard
|
|
498
|
+
The unique row index of the label (row["id"] or row number if
|
|
499
|
+
row["id"] is not present).
|
|
500
|
+
|
|
501
|
+
Returns
|
|
502
|
+
-------
|
|
503
|
+
dict[str, Any]
|
|
504
|
+
The unique id dictionary for a label subcomponent of the input form
|
|
505
|
+
"""
|
|
506
|
+
return cls._make_id_dict_with_row_index("label", aio_id, row_index)
|
|
507
|
+
|
|
508
|
+
@classmethod
|
|
509
|
+
def help(cls, aio_id: str | _Wildcard, row_index: str | int | _Wildcard) -> dict[str, Any]:
|
|
510
|
+
"""
|
|
511
|
+
Return the id for an input form help text.
|
|
512
|
+
|
|
513
|
+
Parameters
|
|
514
|
+
----------
|
|
515
|
+
aio_id : str or _Wildcard
|
|
516
|
+
The component's unique identifier.
|
|
517
|
+
row_index : str, int, or _Wildcard
|
|
518
|
+
The unique row index of the help text (row["id"] or row number if
|
|
519
|
+
row["id"] is not present).
|
|
520
|
+
|
|
521
|
+
Returns
|
|
522
|
+
-------
|
|
523
|
+
dict[str, Any]
|
|
524
|
+
The unique id dictionary for a help subcomponent of the input form
|
|
525
|
+
"""
|
|
526
|
+
return cls._make_id_dict_with_row_index("help", aio_id, row_index)
|
|
527
|
+
|
|
528
|
+
@classmethod
|
|
529
|
+
def unit(cls, aio_id: str | _Wildcard, row_index: str | int | _Wildcard) -> dict[str, Any]:
|
|
530
|
+
"""
|
|
531
|
+
Return the id for an input form unit.
|
|
532
|
+
|
|
533
|
+
Parameters
|
|
534
|
+
----------
|
|
535
|
+
aio_id : str or _Wildcard
|
|
536
|
+
The component's unique identifier.
|
|
537
|
+
row_index : str, int, or _Wildcard
|
|
538
|
+
The unique row index of the unit (row["id"] or row number if
|
|
539
|
+
row["id"] is not present).
|
|
540
|
+
|
|
541
|
+
Returns
|
|
542
|
+
-------
|
|
543
|
+
dict[str, Any]
|
|
544
|
+
The unique id dictionary for a unit subcomponent of the input form
|
|
545
|
+
"""
|
|
546
|
+
return cls._make_id_dict_with_row_index("unit", aio_id, row_index)
|
|
547
|
+
|
|
548
|
+
@classmethod
|
|
549
|
+
def description(
|
|
550
|
+
cls, aio_id: str | _Wildcard, row_index: str | int | _Wildcard
|
|
551
|
+
) -> dict[str, Any]:
|
|
552
|
+
"""
|
|
553
|
+
Return the id for an input form description.
|
|
554
|
+
|
|
555
|
+
Parameters
|
|
556
|
+
----------
|
|
557
|
+
aio_id : str or _Wildcard
|
|
558
|
+
The component's unique identifier.
|
|
559
|
+
row_index : str, int, or _Wildcard
|
|
560
|
+
The unique row index of the description (row["id"] or row number if
|
|
561
|
+
row["id"] is not present).
|
|
562
|
+
|
|
563
|
+
Returns
|
|
564
|
+
-------
|
|
565
|
+
dict[str, Any]
|
|
566
|
+
The unique id dictionary for a description subcomponent of the input form
|
|
567
|
+
"""
|
|
568
|
+
return cls._make_id_dict_with_row_index("description", aio_id, row_index)
|
|
569
|
+
|
|
570
|
+
@classmethod
|
|
571
|
+
def modal(cls, aio_id: str | _Wildcard, row_index: str | int | _Wildcard) -> dict[str, Any]:
|
|
572
|
+
"""
|
|
573
|
+
Return the id for an input form modal.
|
|
574
|
+
|
|
575
|
+
Parameters
|
|
576
|
+
----------
|
|
577
|
+
aio_id : str or _Wildcard
|
|
578
|
+
The component's unique identifier.
|
|
579
|
+
row_index : str, int, or _Wildcard
|
|
580
|
+
The unique row index of the modal (row["id"] or row number if
|
|
581
|
+
row["id"] is not present).
|
|
582
|
+
|
|
583
|
+
Returns
|
|
584
|
+
-------
|
|
585
|
+
dict[str, Any]
|
|
586
|
+
The unique id dictionary for a modal subcomponent of the input form
|
|
587
|
+
"""
|
|
588
|
+
return cls._make_id_dict_with_row_index("modal", aio_id, row_index)
|
|
589
|
+
|
|
590
|
+
@classmethod
|
|
591
|
+
def field(
|
|
592
|
+
cls,
|
|
593
|
+
aio_id: str | _Wildcard,
|
|
594
|
+
field_type: str | _Wildcard,
|
|
595
|
+
field_id: str | _Wildcard,
|
|
596
|
+
row_index: str | int | _Wildcard,
|
|
597
|
+
) -> dict[str, Any]:
|
|
598
|
+
"""
|
|
599
|
+
Return the id for an input form field.
|
|
600
|
+
|
|
601
|
+
Parameters
|
|
602
|
+
----------
|
|
603
|
+
aio_id : str or _Wildcard
|
|
604
|
+
The component's unique identifier.
|
|
605
|
+
field_type : str, or _Wildcard
|
|
606
|
+
The unique type of the field within the form (field["type"]).
|
|
607
|
+
field_id : str, or _Wildcard
|
|
608
|
+
The unique identifier of the field within the form (field["id"]).
|
|
609
|
+
row_index : str, int, or _Wildcard
|
|
610
|
+
The unique row index of the field within the form (row["id"] or row number if
|
|
611
|
+
row["id"] is not present).
|
|
612
|
+
|
|
613
|
+
Returns
|
|
614
|
+
-------
|
|
615
|
+
dict[str, Any]
|
|
616
|
+
The unique id dictionary for a field subcomponent of the input form
|
|
617
|
+
"""
|
|
618
|
+
return cls._make_id_dict_with_field_type_and_identifier_and_row_index(
|
|
619
|
+
"field", aio_id, field_type, field_id, row_index
|
|
620
|
+
)
|
|
621
|
+
|
|
622
|
+
@classmethod
|
|
623
|
+
def field_div(
|
|
624
|
+
cls,
|
|
625
|
+
aio_id: str | _Wildcard,
|
|
626
|
+
field_type: str | _Wildcard,
|
|
627
|
+
field_id: str | _Wildcard,
|
|
628
|
+
row_index: str | int | _Wildcard,
|
|
629
|
+
) -> dict[str, Any]:
|
|
630
|
+
"""
|
|
631
|
+
Return the id for an input form field div.
|
|
632
|
+
|
|
633
|
+
Parameters
|
|
634
|
+
----------
|
|
635
|
+
aio_id : str or _Wildcard
|
|
636
|
+
The component's unique identifier.
|
|
637
|
+
field_type : str, or _Wildcard
|
|
638
|
+
The unique type of the field within the form (field["type"]).
|
|
639
|
+
field_id : str, or _Wildcard
|
|
640
|
+
The unique identifier of the field within the form (field["id"]).
|
|
641
|
+
row_index : str, int, or _Wildcard
|
|
642
|
+
The unique row index of the field within the form (row["id"] or row number if
|
|
643
|
+
row["id"] is not present).
|
|
644
|
+
|
|
645
|
+
Returns
|
|
646
|
+
-------
|
|
647
|
+
dict[str, Any]
|
|
648
|
+
The unique id dictionary for a field div subcomponent of the input form
|
|
649
|
+
"""
|
|
650
|
+
return cls._make_id_dict_with_field_type_and_identifier_and_row_index(
|
|
651
|
+
"field-div", aio_id, field_type, field_id, row_index
|
|
652
|
+
)
|
|
653
|
+
|
|
654
|
+
ids = InputFormIds
|
|
655
|
+
|
|
656
|
+
def __init__(
|
|
657
|
+
self,
|
|
658
|
+
items: list[dict[str, Any]],
|
|
659
|
+
title: str | None = None,
|
|
660
|
+
with_card: bool = True,
|
|
661
|
+
card_props: dict[str, Any] | None = None,
|
|
662
|
+
title_props: dict[str, Any] | None = None,
|
|
663
|
+
column_names: list[str] | None = None,
|
|
664
|
+
column_widths: dict[str, int] | None = None,
|
|
665
|
+
aio_id: str | None = None,
|
|
666
|
+
columns: list[str] | None = None,
|
|
667
|
+
persistence: bool = False,
|
|
668
|
+
persistence_type: str | None = None,
|
|
669
|
+
):
|
|
670
|
+
# Set default properties
|
|
671
|
+
if card_props is None:
|
|
672
|
+
card_props = {}
|
|
673
|
+
if title_props is None:
|
|
674
|
+
title_props = {}
|
|
675
|
+
if column_names is None:
|
|
676
|
+
column_names = []
|
|
677
|
+
if column_widths is None:
|
|
678
|
+
column_widths = {}
|
|
679
|
+
if aio_id is None:
|
|
680
|
+
aio_id = str(uuid.uuid4())
|
|
681
|
+
if columns is None:
|
|
682
|
+
columns = list(DEFAULT_COLUMNS_AND_WIDTHS.keys())
|
|
683
|
+
if persistence_type is None:
|
|
684
|
+
persistence_type = "memory"
|
|
685
|
+
|
|
686
|
+
self.aio_id = aio_id
|
|
687
|
+
self.items = items # Store reference as-is (items are not mutated in this class)
|
|
688
|
+
self.title = title
|
|
689
|
+
self.column_widths = column_widths # Store reference as-is (not mutated)
|
|
690
|
+
self.columns = columns # Store reference as-is (not mutated)
|
|
691
|
+
self.column_names = column_names # Store reference as-is (not mutated)
|
|
692
|
+
self.persistence = persistence
|
|
693
|
+
self.persistence_type = persistence_type
|
|
694
|
+
|
|
695
|
+
default_card_props = {
|
|
696
|
+
"withBorder": True,
|
|
697
|
+
"shadow": "sm",
|
|
698
|
+
"radius": "md",
|
|
699
|
+
}
|
|
700
|
+
card_props = self._populate_with_defaults(card_props, default_card_props)
|
|
701
|
+
|
|
702
|
+
default_title_props = {
|
|
703
|
+
"style": {"fontSize": "18px", "fontWeight": "bold"},
|
|
704
|
+
}
|
|
705
|
+
title_props = self._populate_with_defaults(title_props, default_title_props)
|
|
706
|
+
|
|
707
|
+
_validate_columns(columns)
|
|
708
|
+
_validate_items_definition(self.items)
|
|
709
|
+
|
|
710
|
+
layout = self._make_layout(title, with_card, card_props, title_props)
|
|
711
|
+
|
|
712
|
+
super().__init__(
|
|
713
|
+
layout,
|
|
714
|
+
)
|
|
715
|
+
|
|
716
|
+
def _populate_with_defaults(
|
|
717
|
+
self, properties_input: dict[str, Any], default_properties: dict[str, Any]
|
|
718
|
+
) -> dict[str, Any]:
|
|
719
|
+
# make a copy to avoid modifying the original dict
|
|
720
|
+
properties_final = properties_input.copy()
|
|
721
|
+
for key, val in default_properties.items():
|
|
722
|
+
if key not in properties_final:
|
|
723
|
+
properties_final[key] = val
|
|
724
|
+
return properties_final
|
|
725
|
+
|
|
726
|
+
def _make_layout(
|
|
727
|
+
self,
|
|
728
|
+
title: str | None,
|
|
729
|
+
with_card: bool,
|
|
730
|
+
card_props: dict[str, Any],
|
|
731
|
+
title_props: dict[str, Any],
|
|
732
|
+
) -> dmc.Card | html.Div:
|
|
733
|
+
form = make_form(
|
|
734
|
+
self.aio_id,
|
|
735
|
+
self.items,
|
|
736
|
+
self.columns,
|
|
737
|
+
self.column_names,
|
|
738
|
+
self.column_widths,
|
|
739
|
+
self.persistence,
|
|
740
|
+
self.persistence_type,
|
|
741
|
+
)
|
|
742
|
+
|
|
743
|
+
content = (
|
|
744
|
+
html.Div(
|
|
745
|
+
[
|
|
746
|
+
html.P(title, **title_props),
|
|
747
|
+
html.Hr(className="my-2"),
|
|
748
|
+
html.Br(),
|
|
749
|
+
form,
|
|
750
|
+
],
|
|
751
|
+
)
|
|
752
|
+
if title
|
|
753
|
+
else form
|
|
754
|
+
)
|
|
755
|
+
|
|
756
|
+
layout = dmc.Card(content, **card_props) if with_card else content
|
|
757
|
+
|
|
758
|
+
return layout
|
|
759
|
+
|
|
760
|
+
@staticmethod
|
|
761
|
+
@callback(
|
|
762
|
+
Output(ids.modal(aio_id=MATCH, row_index=MATCH), "opened"),
|
|
763
|
+
Input(ids.help(aio_id=MATCH, row_index=MATCH), "n_clicks"),
|
|
764
|
+
State(ids.modal(aio_id=MATCH, row_index=MATCH), "opened"),
|
|
765
|
+
prevent_initial_call=True,
|
|
766
|
+
)
|
|
767
|
+
def display_help(
|
|
768
|
+
n_clicks: int,
|
|
769
|
+
opened: bool,
|
|
770
|
+
) -> bool:
|
|
771
|
+
"""Display help modal."""
|
|
772
|
+
if not n_clicks:
|
|
773
|
+
raise PreventUpdate
|
|
774
|
+
|
|
775
|
+
return not opened
|
|
776
|
+
|
|
777
|
+
@staticmethod
|
|
778
|
+
@callback(
|
|
779
|
+
Output(
|
|
780
|
+
ids.field(aio_id=MATCH, field_type="NumberInput", field_id=MATCH, row_index=MATCH),
|
|
781
|
+
"error",
|
|
782
|
+
),
|
|
783
|
+
Input(
|
|
784
|
+
ids.field(aio_id=MATCH, field_type="NumberInput", field_id=MATCH, row_index=MATCH),
|
|
785
|
+
"value",
|
|
786
|
+
),
|
|
787
|
+
State(
|
|
788
|
+
ids.field(aio_id=MATCH, field_type="NumberInput", field_id=MATCH, row_index=MATCH),
|
|
789
|
+
"max",
|
|
790
|
+
),
|
|
791
|
+
State(
|
|
792
|
+
ids.field(aio_id=MATCH, field_type="NumberInput", field_id=MATCH, row_index=MATCH),
|
|
793
|
+
"min",
|
|
794
|
+
),
|
|
795
|
+
prevent_initial_call=True,
|
|
796
|
+
)
|
|
797
|
+
def validate_input(
|
|
798
|
+
value: float,
|
|
799
|
+
vmax: float | None,
|
|
800
|
+
vmin: float | None,
|
|
801
|
+
) -> str | NoUpdate | None:
|
|
802
|
+
"""Validate number input entry."""
|
|
803
|
+
try:
|
|
804
|
+
float(value)
|
|
805
|
+
except Exception:
|
|
806
|
+
return no_update
|
|
807
|
+
|
|
808
|
+
if vmax is not None and float(value) > vmax:
|
|
809
|
+
print(f"Expect value to be less than {vmax}.")
|
|
810
|
+
return f"Expect value to be less than {vmax}."
|
|
811
|
+
if vmin is not None and float(value) < vmin:
|
|
812
|
+
print(f"Expect value to be greater than {vmin}.")
|
|
813
|
+
return f"Expect value to be greater than {vmin}."
|
|
814
|
+
|
|
815
|
+
return None
|
|
816
|
+
|
|
817
|
+
|
|
818
|
+
def compute_final_column_widths(
|
|
819
|
+
columns: list[str],
|
|
820
|
+
column_widths: dict[str, int],
|
|
821
|
+
) -> dict[str, int]:
|
|
822
|
+
"""
|
|
823
|
+
Calculate column widths from given and default column widths.
|
|
824
|
+
|
|
825
|
+
Parameters
|
|
826
|
+
----------
|
|
827
|
+
columns : list[str]
|
|
828
|
+
List of column names to be used in the form.
|
|
829
|
+
column_widths : dict[str, int]
|
|
830
|
+
Dictionary mapping column names to their desired widths.
|
|
831
|
+
Missing columns will use default widths.
|
|
832
|
+
|
|
833
|
+
Returns
|
|
834
|
+
-------
|
|
835
|
+
dict
|
|
836
|
+
Dictionary containing only the used columns with their calculated widths
|
|
837
|
+
"""
|
|
838
|
+
_validate_column_widths(column_widths)
|
|
839
|
+
|
|
840
|
+
# Get the width for each used column (custom or default)
|
|
841
|
+
result: dict[str, int] = {}
|
|
842
|
+
for column in columns:
|
|
843
|
+
if column in column_widths:
|
|
844
|
+
result[column] = column_widths[column]
|
|
845
|
+
else:
|
|
846
|
+
result[column] = DEFAULT_COLUMNS_AND_WIDTHS[column]
|
|
847
|
+
|
|
848
|
+
return result
|
|
849
|
+
|
|
850
|
+
|
|
851
|
+
def _validate_items_definition(items: list[dict[str, Any]]) -> None:
|
|
852
|
+
"""Validate the items definition."""
|
|
853
|
+
for item in items:
|
|
854
|
+
fields = item.get("fields")
|
|
855
|
+
if not fields:
|
|
856
|
+
raise ValueError("Each item must have a fields key.")
|
|
857
|
+
|
|
858
|
+
for field in fields:
|
|
859
|
+
field_type = field.get("type", None)
|
|
860
|
+
|
|
861
|
+
if field_type is None:
|
|
862
|
+
raise ValueError(
|
|
863
|
+
"Each field must have a type key. Supported types are: "
|
|
864
|
+
f"{', '.join(SUPPORTED_FIELD_TYPES)}.",
|
|
865
|
+
)
|
|
866
|
+
|
|
867
|
+
if field_type not in SUPPORTED_FIELD_TYPES:
|
|
868
|
+
raise ValueError(
|
|
869
|
+
f"Unsupported field type: {field_type}. Supported types are: "
|
|
870
|
+
f"{', '.join(SUPPORTED_FIELD_TYPES)}.",
|
|
871
|
+
)
|
|
872
|
+
|
|
873
|
+
field_id = field.get("id", None)
|
|
874
|
+
if field_id is None:
|
|
875
|
+
raise ValueError("Each field must have an id key.")
|
|
876
|
+
|
|
877
|
+
field_id_is_string = isinstance(field_id, str)
|
|
878
|
+
if not field_id_is_string or not field_id:
|
|
879
|
+
raise ValueError("Field id must be a non-empty string.")
|
|
880
|
+
|
|
881
|
+
|
|
882
|
+
def _validate_columns(columns: list[str]) -> None:
|
|
883
|
+
"""Validate the columns definition."""
|
|
884
|
+
if not columns:
|
|
885
|
+
raise ValueError("Columns list cannot be empty.")
|
|
886
|
+
|
|
887
|
+
if len(set(columns)) != len(columns):
|
|
888
|
+
raise ValueError("Duplicate column names are not allowed in columns list.")
|
|
889
|
+
|
|
890
|
+
for column in columns:
|
|
891
|
+
if column not in DEFAULT_COLUMNS_AND_WIDTHS:
|
|
892
|
+
raise ValueError(f"Unsupported column name: {column}")
|
|
893
|
+
|
|
894
|
+
|
|
895
|
+
def _validate_column_names(column_names: list[str], columns: list[str]) -> None:
|
|
896
|
+
"""Validate the column names definition."""
|
|
897
|
+
if column_names and len(column_names) != len(columns):
|
|
898
|
+
raise ValueError("Length of column_names must match length of columns.")
|
|
899
|
+
|
|
900
|
+
|
|
901
|
+
def _validate_column_widths(column_widths: dict[str, int]) -> None:
|
|
902
|
+
"""Validate the column widths definition."""
|
|
903
|
+
for column in column_widths:
|
|
904
|
+
if column not in DEFAULT_COLUMNS_AND_WIDTHS:
|
|
905
|
+
raise ValueError(f"Unsupported column name: {column}")
|
|
906
|
+
for width in column_widths.values():
|
|
907
|
+
if not isinstance(width, int) or width <= 0: # type: ignore - explicit type check here
|
|
908
|
+
raise ValueError("Column widths must be positive integers.")
|