Skip to content

dashboard

Generic container dashboard.

To configure a container dashboard: attach DashboardConf metadata to MetadorContainer nodes.

To show a container dashboard: create a Dashboard instance.

NodeWidgetPair module-attribute

NodeWidgetPair = Tuple[MetadorNode, WidgetConf]

A container node paired up with a widget configuration.

NodeWidgetRow module-attribute

NodeWidgetRow = List[NodeWidgetPair]

Sorted list of NodeWidgetPairs.

Ordered first by descending priority, then by ascending node path.

DashboardPriority

Bases: int, Inclusive

Dashboard priority of a widget.

Source code in src/metador_core/widget/dashboard.py
27
28
class DashboardPriority(int, Inclusive, low=1, high=10):
    """Dashboard priority of a widget."""

DashboardGroup

Bases: int, Inclusive

Dashboard group of a widget.

Source code in src/metador_core/widget/dashboard.py
31
32
class DashboardGroup(int, Inclusive, low=1):
    """Dashboard group of a widget."""

WidgetConf

Bases: MetadataSchema

Configuration of a widget in the dashboard.

Source code in src/metador_core/widget/dashboard.py
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
class WidgetConf(MetadataSchema):
    """Configuration of a widget in the dashboard."""

    priority: Optional[DashboardPriority] = DashboardPriority(1)
    """Priority of the widget (1-10), higher priority nodes are shown first."""

    group: Optional[DashboardGroup]
    """Dashboard group of the widget.

    Groups are presented in ascending order.
    Widgets are ordered by priority within a group.
    All widgets in a group are shown in a single row.

    Widgets without an assigned group come last.
    """

    # ----

    schema_name: Optional[NonEmptyStr]
    """Name of schema of an metadata object at the current node that is to be visualized.

    If not given, any suitable will be selected if possible.
    """

    schema_version: Optional[SemVerTuple]
    """Version of schema to be used.

    If not given, any suitable will be selected if possible.
    """

    widget_name: Optional[str]
    """Name of widget to be used.

    If not given, any suitable will be selected if possible.
    """

    widget_version: Optional[SemVerTuple]
    """Version of widget to be used.

    If not given, any suitable will be selected if possible.
    """

priority class-attribute instance-attribute

priority: Optional[DashboardPriority] = DashboardPriority(1)

Priority of the widget (1-10), higher priority nodes are shown first.

group instance-attribute

group: Optional[DashboardGroup]

Dashboard group of the widget.

Groups are presented in ascending order. Widgets are ordered by priority within a group. All widgets in a group are shown in a single row.

Widgets without an assigned group come last.

schema_name instance-attribute

schema_name: Optional[NonEmptyStr]

Name of schema of an metadata object at the current node that is to be visualized.

If not given, any suitable will be selected if possible.

schema_version instance-attribute

schema_version: Optional[SemVerTuple]

Version of schema to be used.

If not given, any suitable will be selected if possible.

widget_name instance-attribute

widget_name: Optional[str]

Name of widget to be used.

If not given, any suitable will be selected if possible.

widget_version instance-attribute

widget_version: Optional[SemVerTuple]

Version of widget to be used.

If not given, any suitable will be selected if possible.

DashboardConf

Bases: MetadataSchema

Schema describing dashboard configuration for a node in a container.

Instantiating without passing a list of widget configurations will return an instance that will show an arbitrary suitable widget, i.e. is equivalent to DashboardConf.show()

Source code in src/metador_core/widget/dashboard.py
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
class DashboardConf(MetadataSchema):
    """Schema describing dashboard configuration for a node in a container.

    Instantiating without passing a list of widget configurations will
    return an instance that will show an arbitrary suitable widget, i.e.
    is equivalent to `DashboardConf.show()`
    """

    class Plugin:
        name = "core.dashboard"
        version = (0, 1, 0)

    widgets: List[WidgetConf] = [WidgetConf()]
    """Widgets to present for this node in the dashboard.

    If left empty, will try present any widget usable for this node.
    """

    @staticmethod
    def widget(**kwargs) -> WidgetConf:
        """Construct a dashboard widget configuration (see `WidgetConf`)."""
        # for convenience
        return WidgetConf(**kwargs)

    @classmethod
    def show(cls, _arg: List[WidgetConf] = None, **kwargs):
        """Construct a dashboard configuration for the widget(s) of one container node.

        For one widget, pass the widget config (if any) as keyword arguments,
        e.g.  `DashboardConf.show(group=1)`.

        For multiple widgets, create widget configurations with `widget(...)`,
        and pass them to `show`, e.g.:
        `DashboardConf.show([DashboardConf.widget(), DashboardConf.widget(group=2)])`.
        """
        if _arg and kwargs:
            msg = "Pass widget config arguments or list of widget configs - not both!"
            raise ValueError(msg)

        if _arg is None:
            # kwargs have config for a singleton widget
            widgets = [cls.widget(**kwargs)]
        else:
            # multiple widgets, preconfigured
            widgets = list(_arg)
        return cls(widgets=widgets)

widgets class-attribute instance-attribute

widgets: List[WidgetConf] = [WidgetConf()]

Widgets to present for this node in the dashboard.

If left empty, will try present any widget usable for this node.

widget staticmethod

widget(**kwargs) -> WidgetConf

Construct a dashboard widget configuration (see WidgetConf).

Source code in src/metador_core/widget/dashboard.py
 96
 97
 98
 99
100
@staticmethod
def widget(**kwargs) -> WidgetConf:
    """Construct a dashboard widget configuration (see `WidgetConf`)."""
    # for convenience
    return WidgetConf(**kwargs)

show classmethod

show(_arg: List[WidgetConf] = None, **kwargs)

Construct a dashboard configuration for the widget(s) of one container node.

For one widget, pass the widget config (if any) as keyword arguments, e.g. DashboardConf.show(group=1).

For multiple widgets, create widget configurations with widget(...), and pass them to show, e.g.: DashboardConf.show([DashboardConf.widget(), DashboardConf.widget(group=2)]).

Source code in src/metador_core/widget/dashboard.py
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
@classmethod
def show(cls, _arg: List[WidgetConf] = None, **kwargs):
    """Construct a dashboard configuration for the widget(s) of one container node.

    For one widget, pass the widget config (if any) as keyword arguments,
    e.g.  `DashboardConf.show(group=1)`.

    For multiple widgets, create widget configurations with `widget(...)`,
    and pass them to `show`, e.g.:
    `DashboardConf.show([DashboardConf.widget(), DashboardConf.widget(group=2)])`.
    """
    if _arg and kwargs:
        msg = "Pass widget config arguments or list of widget configs - not both!"
        raise ValueError(msg)

    if _arg is None:
        # kwargs have config for a singleton widget
        widgets = [cls.widget(**kwargs)]
    else:
        # multiple widgets, preconfigured
        widgets = list(_arg)
    return cls(widgets=widgets)

Dashboard

The dashboard presents a view of all marked nodes in a container.

To be included in the dashboard, a node must be marked by a DashboardConf object configuring at least one widget for that node.

Note that the Dashboard needs * either a widget server to be passed (embedding in a website), * or the container is wrapped by metador_core.widget.jupyter.Previewable (notebook mode)

Source code in src/metador_core/widget/dashboard.py
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
class Dashboard:
    """The dashboard presents a view of all marked nodes in a container.

    To be included in the dashboard, a node must be marked by a `DashboardConf`
    object configuring at least one widget for that node.


    Note that the `Dashboard` needs
    * either a widget server to be passed (embedding in a website),
    * or the container is wrapped by `metador_core.widget.jupyter.Previewable` (notebook mode)
    """

    def __init__(
        self,
        container: MetadorContainer,
        *,
        server: WidgetServer = None,
        container_id: Optional[str] = None,
    ):
        """Return instance of a dashboard.

        Args:
            container: Actual Metador container that is open and readable
            server: `WidgetServer` to use for the widgets (default: standalone server / Jupyter mode)
            container_id: Container id usable with the server to get this container (default: container UUID)
        """
        self._container: MetadorContainer = container
        self._server = server
        self._container_id: str = container_id

        # figure out what schemas to show and what widgets to use and collect
        ws: List[NodeWidgetPair] = []
        for node in self._container.metador.query(DashboardConf):
            dbmeta = node.meta.get(DashboardConf)
            restr_node = node.restrict(read_only=True, local_only=True)
            for wmeta in dbmeta.widgets:
                ws.append((restr_node, self._resolve_node(node, wmeta)))

        grps, ungrp = sorted_widgets(ws)
        self._groups = grps
        self._ungrouped = ungrp

    def _resolve_node(self, node: MetadorNode, wmeta: WidgetConf) -> WidgetConf:
        """Check and resolve widget dashboard metadata for a node."""
        wmeta = wmeta.copy()  # use copy, abandon original

        s_ref: PluginRef = _resolve_schema(node, wmeta)
        wmeta.schema_name = s_ref.name
        wmeta.schema_version = s_ref.version

        w_ref: PluginRef = _resolve_widget(
            node, s_ref, wmeta.widget_name, wmeta.widget_version
        )
        wmeta.widget_name = w_ref.name
        wmeta.widget_version = w_ref.version

        return wmeta

    def show(self) -> Viewable:
        """Instantiate widgets for container and return resulting dashboard."""
        # Outermost element: The Dashboard is a column of widget groups
        db = pn.FlexBox(
            flex_direction="column",
            justify_content="space-evenly",
            align_content="space-evenly",
            align_items="center",
            sizing_mode="scale_both",
        )

        # add each widget group within individual, flexibly-wrapping rows
        for idx, widget_group in enumerate(self._groups.values()):
            db.append(
                get_grp_row(
                    idx=idx,
                    widget_group=widget_group,
                    divider=False,  # does not work offline with panel >= 1.0?
                    server=self._server,
                    container_id=self._container_id,
                )
            )

        # dump remaining ungrouped widgets into a separate flexibly-wrapping row
        ungrp_exist = len(self._ungrouped) != 0
        if ungrp_exist:
            db.append(
                get_grp_row(
                    widget_group=self._ungrouped,
                    server=self._server,
                    container_id=self._container_id,
                )
            )
        return db

__init__

__init__(
    container: MetadorContainer,
    *,
    server: WidgetServer = None,
    container_id: Optional[str] = None
)

Return instance of a dashboard.

Parameters:

Name Type Description Default
container MetadorContainer

Actual Metador container that is open and readable

required
server WidgetServer

WidgetServer to use for the widgets (default: standalone server / Jupyter mode)

None
container_id Optional[str]

Container id usable with the server to get this container (default: container UUID)

None
Source code in src/metador_core/widget/dashboard.py
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
def __init__(
    self,
    container: MetadorContainer,
    *,
    server: WidgetServer = None,
    container_id: Optional[str] = None,
):
    """Return instance of a dashboard.

    Args:
        container: Actual Metador container that is open and readable
        server: `WidgetServer` to use for the widgets (default: standalone server / Jupyter mode)
        container_id: Container id usable with the server to get this container (default: container UUID)
    """
    self._container: MetadorContainer = container
    self._server = server
    self._container_id: str = container_id

    # figure out what schemas to show and what widgets to use and collect
    ws: List[NodeWidgetPair] = []
    for node in self._container.metador.query(DashboardConf):
        dbmeta = node.meta.get(DashboardConf)
        restr_node = node.restrict(read_only=True, local_only=True)
        for wmeta in dbmeta.widgets:
            ws.append((restr_node, self._resolve_node(node, wmeta)))

    grps, ungrp = sorted_widgets(ws)
    self._groups = grps
    self._ungrouped = ungrp

show

show() -> Viewable

Instantiate widgets for container and return resulting dashboard.

Source code in src/metador_core/widget/dashboard.py
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
def show(self) -> Viewable:
    """Instantiate widgets for container and return resulting dashboard."""
    # Outermost element: The Dashboard is a column of widget groups
    db = pn.FlexBox(
        flex_direction="column",
        justify_content="space-evenly",
        align_content="space-evenly",
        align_items="center",
        sizing_mode="scale_both",
    )

    # add each widget group within individual, flexibly-wrapping rows
    for idx, widget_group in enumerate(self._groups.values()):
        db.append(
            get_grp_row(
                idx=idx,
                widget_group=widget_group,
                divider=False,  # does not work offline with panel >= 1.0?
                server=self._server,
                container_id=self._container_id,
            )
        )

    # dump remaining ungrouped widgets into a separate flexibly-wrapping row
    ungrp_exist = len(self._ungrouped) != 0
    if ungrp_exist:
        db.append(
            get_grp_row(
                widget_group=self._ungrouped,
                server=self._server,
                container_id=self._container_id,
            )
        )
    return db

sorted_widgets

sorted_widgets(
    widgets: Iterable[NodeWidgetPair],
) -> Tuple[Dict[int, NodeWidgetRow], NodeWidgetRow]

Return widgets in groups, ordered by priority and node path.

Returns tuple with dict of groups and a remainder of ungrouped widgets.

Source code in src/metador_core/widget/dashboard.py
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
def sorted_widgets(
    widgets: Iterable[NodeWidgetPair],
) -> Tuple[Dict[int, NodeWidgetRow], NodeWidgetRow]:
    """Return widgets in groups, ordered by priority and node path.

    Returns tuple with dict of groups and a remainder of ungrouped widgets.
    """

    def nwp_group(tup: NodeWidgetPair) -> int:
        return tup[1].group or 0

    def nwp_prio(tup: NodeWidgetPair) -> int:
        return -tup[1].priority or 0  # in descending order of priority

    def sorted_group(ws: Iterable[NodeWidgetPair]) -> NodeWidgetRow:
        """Sort first on priority, and for same priority on container node."""
        return list(sorted(sorted(ws, key=lambda x: x[0].name), key=nwp_prio))

    # dict, sorted in ascending group order (but ungrouped are 0)
    ret = dict(
        sorted(
            {
                k: sorted_group(v)
                for k, v in groupby(sorted(widgets, key=nwp_group), key=nwp_group)
            }.items()
        )
    )
    ungrp = ret.pop(0, [])  # separate out the ungrouped (were mapped to 0)
    return ret, ungrp

get_grp_label

get_grp_label(idx)

Create and return a styled group label.

Source code in src/metador_core/widget/dashboard.py
263
264
265
266
267
268
269
270
271
272
def get_grp_label(idx):
    """Create and return a styled group label."""
    return pn.pane.Str(
        f"Group {idx+1}" if idx is not None else "Ungrouped resources",
        style={
            "font-size": "15px",
            "font-weight": "bold",
            "text-decoration": "underline",
        },
    )

add_widgets

add_widgets(
    w_grp,
    ui_grp,
    *,
    server=None,
    container_id: Optional[str] = None
)

Instantiate and add widget to the flexibly wrapping row that handles the entire group.

Source code in src/metador_core/widget/dashboard.py
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
def add_widgets(w_grp, ui_grp, *, server=None, container_id: Optional[str] = None):
    """Instantiate and add widget to the flexibly wrapping row that handles the entire group."""
    w_width, w_height = 500, 500  # max size of a widget tile, arbitrarily set
    for node, wmeta in w_grp:
        w_cls = widgets.get(wmeta.widget_name, wmeta.widget_version)
        label = pn.pane.Str(f"{node.name}:")

        # instantiating the appropriate widget
        w_obj = w_cls(
            node,
            wmeta.schema_name,
            wmeta.schema_version,
            server=server,
            container_id=container_id,
            # reset max widget of a widget tile,  only if it is for a pdf, text or video file
            max_width=700
            if "pdf" in wmeta.widget_name
            or "text" in wmeta.widget_name
            or "video" in wmeta.widget_name
            else w_width,
            # reset max height of a widget tile, only if it is for a text file
            max_height=700 if "text" in wmeta.widget_name else w_height,
        )

        # adding the new widget to the given row
        ui_grp.append(
            pn.Column(
                label,
                w_obj.show(),
                sizing_mode="scale_both",
                scroll=False
                if "image" in wmeta.widget_name or "pdf" in wmeta.widget_name
                else True,
            )
        )
    return ui_grp

get_grp_row

get_grp_row(
    *,
    idx=None,
    widget_group=None,
    divider=False,
    server=None,
    container_id: Optional[str] = None
)

Create a flexible and wrapping row for all widgets within a single group.

Source code in src/metador_core/widget/dashboard.py
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
def get_grp_row(
    *,
    idx=None,
    widget_group=None,
    divider=False,
    server=None,
    container_id: Optional[str] = None,
):
    """Create a flexible and wrapping row for all widgets within a single group."""
    return pn.FlexBox(
        get_grp_label(idx=idx),
        add_widgets(
            widget_group,
            pn.FlexBox(
                flex_direction="row",
                justify_content="space-evenly",
                align_content="space-evenly",
                align_items="center",
                sizing_mode="scale_both",
            ),
            server=server,
            container_id=container_id,
        ),
        pn.layout.Divider(margin=(100, 0, 20, 0)) if divider else None,
        flex_direction="column",
        justify_content="space-evenly",
        align_content="space-evenly",
        align_items="center",
        sizing_mode="scale_both",
    )