Add a runs screen: a table, a run in full, and curves side by side
This commit is contained in:
@@ -23,7 +23,8 @@ from typing import Any, Literal
|
||||
|
||||
from pydantic import BaseModel, Field, field_validator, model_validator
|
||||
|
||||
from fluksio.flow.schemas import _validate_name
|
||||
from fluksio.flow.messages import DType, qualify
|
||||
from fluksio.flow.schemas import FlowDef, _validate_name
|
||||
from fluksio.flow.store import FlowStore, StaleVersion
|
||||
|
||||
#: Sibling of the shared-node library, and likewise not a flow.
|
||||
@@ -716,6 +717,105 @@ def default_dashboard(name: str) -> DashboardDef:
|
||||
)
|
||||
|
||||
|
||||
#: What the generated dashboard is called, for a flow of this name.
|
||||
def results_name(flow: str) -> str:
|
||||
return f"{flow}_results"
|
||||
|
||||
|
||||
#: Numbers a stat or a chart can draw.
|
||||
_NUMERIC = {DType.FLOAT, DType.INT}
|
||||
|
||||
|
||||
def results_dashboard(flow: FlowDef) -> DashboardDef:
|
||||
"""A results dashboard for a batch flow, from the ports it declares.
|
||||
|
||||
A streaming output is a curve and gets a chart; a scalar output is a
|
||||
number and gets a stat. Nothing here is specific to runs: the widgets bind
|
||||
to the flow's own message names, which is what makes the same page draw a
|
||||
run live, draw a finished one when opened in a run's context, and stay an
|
||||
ordinary dashboard anyone can edit afterwards.
|
||||
|
||||
A starting point rather than a finished page — which is the only reason
|
||||
generating one is worth doing at all.
|
||||
"""
|
||||
charts = [
|
||||
spec
|
||||
for node in flow.nodes
|
||||
for spec in node.provides
|
||||
if spec.stream and spec.dtype in _NUMERIC and spec.name
|
||||
]
|
||||
# What a run reports. Declared outputs are unqualified names; an empty list
|
||||
# means "everything the flow ends up holding", which is not a set this can
|
||||
# enumerate, so it draws no stats rather than guessing at them.
|
||||
produced = {
|
||||
spec.name.rsplit(".", 1)[-1]: spec
|
||||
for node in flow.nodes
|
||||
for spec in node.provides
|
||||
if spec.name
|
||||
}
|
||||
stats = [
|
||||
produced[name]
|
||||
for name in flow.outputs
|
||||
if name in produced and not produced[name].stream
|
||||
]
|
||||
|
||||
widgets: list[WidgetDef] = []
|
||||
for index, spec in enumerate(charts):
|
||||
widgets.append(
|
||||
WidgetDef(
|
||||
id=f"chart_{spec.port or index}",
|
||||
type="chart",
|
||||
title=spec.port or spec.name,
|
||||
layout={"lg": Placement(x=0, y=index * 4, w=8, h=4)},
|
||||
config={
|
||||
"series": [
|
||||
{
|
||||
"message": qualify(flow.name, spec.name),
|
||||
"dtype": spec.dtype.value,
|
||||
"label": spec.port or spec.name,
|
||||
}
|
||||
],
|
||||
"history": {"points": 600},
|
||||
},
|
||||
)
|
||||
)
|
||||
|
||||
row = 0
|
||||
for spec in stats:
|
||||
if spec.dtype is DType.RECORD:
|
||||
kind: WidgetType = "notification"
|
||||
elif spec.dtype in _NUMERIC or spec.dtype is DType.STR:
|
||||
kind = "stat"
|
||||
else:
|
||||
# A list, a series or an artifact has no single reading to show.
|
||||
continue
|
||||
widgets.append(
|
||||
WidgetDef(
|
||||
id=f"out_{spec.port}",
|
||||
type=kind,
|
||||
title=spec.port,
|
||||
layout={"lg": Placement(x=8, y=row * 2, w=4, h=2)},
|
||||
config={
|
||||
"message": qualify(flow.name, spec.name),
|
||||
"dtype": spec.dtype.value,
|
||||
},
|
||||
)
|
||||
)
|
||||
row += 1
|
||||
|
||||
return DashboardDef(
|
||||
name=results_name(flow.name),
|
||||
title=f"{flow.title or flow.name} results",
|
||||
pages=[
|
||||
PageDef(
|
||||
id="main",
|
||||
title="Results",
|
||||
sections=[SectionDef(id="main", widgets=widgets)],
|
||||
)
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
__all__ = [
|
||||
"BAR_ROWS",
|
||||
"COLOR_DTYPES",
|
||||
|
||||
Reference in New Issue
Block a user