Make a dashboard its widgets: drop the pages and sections nobody drew
This commit is contained in:
@@ -374,32 +374,57 @@ class WidgetDef(BaseModel):
|
|||||||
return self
|
return self
|
||||||
|
|
||||||
|
|
||||||
class SectionDef(BaseModel):
|
def _placement(widget: dict[str, Any]) -> dict[str, Any]:
|
||||||
"""A grid of widgets under a heading."""
|
"""Where a stored widget sits, by the widest breakpoint it names."""
|
||||||
|
layout = widget.get("layout") or {}
|
||||||
id: str
|
for key in ("lg", "md", "sm"):
|
||||||
title: str = ""
|
box = layout.get(key)
|
||||||
widgets: list[WidgetDef] = Field(default_factory=list)
|
if isinstance(box, dict):
|
||||||
|
return dict(box)
|
||||||
@field_validator("id")
|
return {}
|
||||||
@classmethod
|
|
||||||
def _check_id(cls, value: str) -> str:
|
|
||||||
return _validate_name(value)
|
|
||||||
|
|
||||||
|
|
||||||
class PageDef(BaseModel):
|
def _flatten_pages(pages: list[Any]) -> list[dict[str, Any]]:
|
||||||
"""One tab of a dashboard."""
|
"""The widgets of a document written as pages and sections.
|
||||||
|
|
||||||
id: str
|
Only the first page: no UI ever wrote a second one, and a panel carries
|
||||||
title: str = ""
|
several whole dashboards instead. Its sections are stacked into one grid
|
||||||
#: A lucide icon name, or empty.
|
the way the viewer always drew them, so a document that placed its widgets
|
||||||
icon: str = ""
|
keeps the arrangement it had rather than piling everything at row zero.
|
||||||
sections: list[SectionDef] = Field(default_factory=list)
|
"""
|
||||||
|
if not pages or not isinstance(pages[0], dict):
|
||||||
|
return []
|
||||||
|
sections = [s for s in (pages[0].get("sections") or []) if isinstance(s, dict)]
|
||||||
|
lists = [
|
||||||
|
[w for w in (s.get("widgets") or []) if isinstance(w, dict)] for s in sections
|
||||||
|
]
|
||||||
|
flat = [w for widgets in lists for w in widgets]
|
||||||
|
placed = any(
|
||||||
|
(_placement(w).get("x") or 0) > 0 or (_placement(w).get("y") or 0) > 0
|
||||||
|
for w in flat
|
||||||
|
)
|
||||||
|
if len(sections) < 2 or not placed:
|
||||||
|
return flat
|
||||||
|
|
||||||
@field_validator("id")
|
stacked: list[dict[str, Any]] = []
|
||||||
@classmethod
|
offset = 0
|
||||||
def _check_id(cls, value: str) -> str:
|
for widgets in lists:
|
||||||
return _validate_name(value)
|
bottom = 0
|
||||||
|
for widget in widgets:
|
||||||
|
box = _placement(widget)
|
||||||
|
y = max(0, int(box.get("y") or 0))
|
||||||
|
bottom = max(bottom, y + max(1, int(box.get("h") or 2)))
|
||||||
|
if offset:
|
||||||
|
widget = {
|
||||||
|
**widget,
|
||||||
|
"layout": {
|
||||||
|
**(widget.get("layout") or {}),
|
||||||
|
"lg": {**box, "y": y + offset},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
stacked.append(widget)
|
||||||
|
offset += bottom
|
||||||
|
return stacked
|
||||||
|
|
||||||
|
|
||||||
class DashboardDef(BaseModel):
|
class DashboardDef(BaseModel):
|
||||||
@@ -419,7 +444,11 @@ class DashboardDef(BaseModel):
|
|||||||
#: A lucide icon name, drawn on the panel rail; empty falls back to two
|
#: A lucide icon name, drawn on the panel rail; empty falls back to two
|
||||||
#: letters of the title.
|
#: letters of the title.
|
||||||
icon: str = ""
|
icon: str = ""
|
||||||
pages: list[PageDef] = Field(default_factory=list)
|
#: One grid. Pages and sections were in the schema and never in the UI —
|
||||||
|
#: only the first page was ever read and its sections were drawn as one —
|
||||||
|
#: so a dashboard is its widgets, and several dashboards on one device is
|
||||||
|
#: what a panel is for.
|
||||||
|
widgets: list[WidgetDef] = Field(default_factory=list)
|
||||||
#: Settings the whole dashboard carries, by name — see ``SettingDef``. The
|
#: Settings the whole dashboard carries, by name — see ``SettingDef``. The
|
||||||
#: one channel a dashboard consumes as a dashboard rather than as a set of
|
#: one channel a dashboard consumes as a dashboard rather than as a set of
|
||||||
#: tiles, so a screen on a wall can be told things nobody standing at it
|
#: tiles, so a screen on a wall can be told things nobody standing at it
|
||||||
@@ -431,6 +460,21 @@ class DashboardDef(BaseModel):
|
|||||||
#: never stored — the draft file's existence is the only record of it.
|
#: never stored — the draft file's existence is the only record of it.
|
||||||
has_draft: bool = False
|
has_draft: bool = False
|
||||||
|
|
||||||
|
@model_validator(mode="before")
|
||||||
|
@classmethod
|
||||||
|
def _flatten(cls, data: Any) -> Any:
|
||||||
|
"""Read a document written as pages and sections as one grid.
|
||||||
|
|
||||||
|
Stored dashboards live in each installation's git repository, so the
|
||||||
|
old shape is normalised on the way in rather than migrated: an
|
||||||
|
untouched document keeps working, and the next save writes it flat.
|
||||||
|
"""
|
||||||
|
if isinstance(data, dict) and "widgets" not in data and "pages" in data:
|
||||||
|
pages = data.get("pages") or []
|
||||||
|
data = {k: v for k, v in data.items() if k != "pages"}
|
||||||
|
data["widgets"] = _flatten_pages(pages)
|
||||||
|
return data
|
||||||
|
|
||||||
@field_validator("name")
|
@field_validator("name")
|
||||||
@classmethod
|
@classmethod
|
||||||
def _check_name(cls, value: str) -> str:
|
def _check_name(cls, value: str) -> str:
|
||||||
@@ -456,10 +500,6 @@ class DashboardDef(BaseModel):
|
|||||||
)
|
)
|
||||||
return self
|
return self
|
||||||
|
|
||||||
@property
|
|
||||||
def widgets(self) -> list[WidgetDef]:
|
|
||||||
return [w for p in self.pages for s in p.sections for w in s.widgets]
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def setting_messages(self) -> list[str]:
|
def setting_messages(self) -> list[str]:
|
||||||
"""Every message a bound setting reads. Empty for a static dashboard."""
|
"""Every message a bound setting reads. Empty for a static dashboard."""
|
||||||
@@ -471,7 +511,6 @@ class DashboardSummary(BaseModel):
|
|||||||
|
|
||||||
name: str
|
name: str
|
||||||
title: str = ""
|
title: str = ""
|
||||||
page_count: int = 0
|
|
||||||
widget_count: int = 0
|
widget_count: int = 0
|
||||||
has_draft: bool = False
|
has_draft: bool = False
|
||||||
#: Of the working copy, so publishing from a list needs no second read.
|
#: Of the working copy, so publishing from a list needs no second read.
|
||||||
@@ -533,7 +572,6 @@ class DashboardStore:
|
|||||||
DashboardSummary(
|
DashboardSummary(
|
||||||
name=defn.name,
|
name=defn.name,
|
||||||
title=defn.title,
|
title=defn.title,
|
||||||
page_count=len(defn.pages),
|
|
||||||
widget_count=len(defn.widgets),
|
widget_count=len(defn.widgets),
|
||||||
has_draft=defn.has_draft,
|
has_draft=defn.has_draft,
|
||||||
version=defn.version,
|
version=defn.version,
|
||||||
@@ -747,7 +785,6 @@ def default_dashboard(name: str) -> DashboardDef:
|
|||||||
return DashboardDef(
|
return DashboardDef(
|
||||||
name=name,
|
name=name,
|
||||||
title=name.replace("_", " ").capitalize(),
|
title=name.replace("_", " ").capitalize(),
|
||||||
pages=[PageDef(id="main", title="Overview", sections=[SectionDef(id="main")])],
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -840,13 +877,7 @@ def results_dashboard(flow: FlowDef) -> DashboardDef:
|
|||||||
return DashboardDef(
|
return DashboardDef(
|
||||||
name=results_name(flow.name),
|
name=results_name(flow.name),
|
||||||
title=f"{flow.title or flow.name} results",
|
title=f"{flow.title or flow.name} results",
|
||||||
pages=[
|
widgets=widgets,
|
||||||
PageDef(
|
|
||||||
id="main",
|
|
||||||
title="Results",
|
|
||||||
sections=[SectionDef(id="main", widgets=widgets)],
|
|
||||||
)
|
|
||||||
],
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -865,9 +896,7 @@ __all__ = [
|
|||||||
"DashboardStore",
|
"DashboardStore",
|
||||||
"DashboardSummary",
|
"DashboardSummary",
|
||||||
"DashboardsPublic",
|
"DashboardsPublic",
|
||||||
"PageDef",
|
|
||||||
"Placement",
|
"Placement",
|
||||||
"SectionDef",
|
|
||||||
"SettingDef",
|
"SettingDef",
|
||||||
"WidgetDef",
|
"WidgetDef",
|
||||||
"default_dashboard",
|
"default_dashboard",
|
||||||
|
|||||||
@@ -243,7 +243,7 @@ def _dashboard_with(
|
|||||||
) -> None:
|
) -> None:
|
||||||
"""A published dashboard carrying these widgets."""
|
"""A published dashboard carrying these widgets."""
|
||||||
saved = _dashboard(client, headers, name)
|
saved = _dashboard(client, headers, name)
|
||||||
saved["pages"] = [{"id": "main", "sections": [{"id": "main", "widgets": widgets}]}]
|
saved["widgets"] = widgets
|
||||||
written = client.put(f"{DASHBOARDS}/{name}", headers=headers, json=saved)
|
written = client.put(f"{DASHBOARDS}/{name}", headers=headers, json=saved)
|
||||||
assert written.status_code == 200, written.text
|
assert written.status_code == 200, written.text
|
||||||
published = client.post(
|
published = client.post(
|
||||||
@@ -706,23 +706,12 @@ def test_a_panels_socket_carries_only_what_it_draws(
|
|||||||
)
|
)
|
||||||
|
|
||||||
saved = _dashboard(client, superuser_token_headers, "panel_socket")
|
saved = _dashboard(client, superuser_token_headers, "panel_socket")
|
||||||
saved["pages"] = [
|
saved["widgets"] = [
|
||||||
{
|
|
||||||
"id": "main",
|
|
||||||
"title": "Overview",
|
|
||||||
"sections": [
|
|
||||||
{
|
|
||||||
"id": "main",
|
|
||||||
"widgets": [
|
|
||||||
{
|
{
|
||||||
"id": "w1",
|
"id": "w1",
|
||||||
"type": "stat",
|
"type": "stat",
|
||||||
"config": {"message": "house.kitchen.temperature"},
|
"config": {"message": "house.kitchen.temperature"},
|
||||||
}
|
}
|
||||||
],
|
|
||||||
}
|
|
||||||
],
|
|
||||||
}
|
|
||||||
]
|
]
|
||||||
written = client.put(
|
written = client.put(
|
||||||
f"{DASHBOARDS}/panel_socket", headers=superuser_token_headers, json=saved
|
f"{DASHBOARDS}/panel_socket", headers=superuser_token_headers, json=saved
|
||||||
|
|||||||
@@ -6,8 +6,6 @@ from fluksio.flow.dashboards import (
|
|||||||
DashboardDef,
|
DashboardDef,
|
||||||
DashboardNotFound,
|
DashboardNotFound,
|
||||||
DashboardStore,
|
DashboardStore,
|
||||||
PageDef,
|
|
||||||
SectionDef,
|
|
||||||
SettingDef,
|
SettingDef,
|
||||||
WidgetDef,
|
WidgetDef,
|
||||||
default_dashboard,
|
default_dashboard,
|
||||||
@@ -40,7 +38,7 @@ def test_a_dashboard_survives_a_round_trip(store: DashboardStore):
|
|||||||
read = store.read("house")
|
read = store.read("house")
|
||||||
|
|
||||||
assert read.name == "house"
|
assert read.name == "house"
|
||||||
assert [p.id for p in read.pages] == ["main"]
|
assert read.widgets == []
|
||||||
assert read.version == saved.version
|
assert read.version == saved.version
|
||||||
|
|
||||||
|
|
||||||
@@ -138,21 +136,11 @@ def test_the_deepest_chart_decides_how_much_past_is_kept(store: DashboardStore):
|
|||||||
store.write(
|
store.write(
|
||||||
DashboardDef(
|
DashboardDef(
|
||||||
name="house",
|
name="house",
|
||||||
pages=[
|
|
||||||
PageDef(
|
|
||||||
id="main",
|
|
||||||
sections=[
|
|
||||||
SectionDef(id="a", widgets=[chart("heating.temp", 400)]),
|
|
||||||
SectionDef(
|
|
||||||
id="b",
|
|
||||||
widgets=[
|
widgets=[
|
||||||
|
chart("heating.temp", 400),
|
||||||
chart("heating.temp", 900),
|
chart("heating.temp", 900),
|
||||||
chart("solar.watts", 100),
|
chart("solar.watts", 100),
|
||||||
],
|
],
|
||||||
),
|
|
||||||
],
|
|
||||||
)
|
|
||||||
],
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -163,14 +151,7 @@ def test_a_chart_cannot_ask_for_an_unbounded_series(store: DashboardStore):
|
|||||||
store.write(
|
store.write(
|
||||||
DashboardDef(
|
DashboardDef(
|
||||||
name="house",
|
name="house",
|
||||||
pages=[
|
widgets=[chart("heating.temp", 10**9)],
|
||||||
PageDef(
|
|
||||||
id="main",
|
|
||||||
sections=[
|
|
||||||
SectionDef(id="a", widgets=[chart("heating.temp", 10**9)])
|
|
||||||
],
|
|
||||||
)
|
|
||||||
],
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -514,3 +495,49 @@ def test_a_chart_of_runs_must_say_which_runs_and_which_metric():
|
|||||||
runs_chart(metric="study.loss")
|
runs_chart(metric="study.loss")
|
||||||
with pytest.raises(ValueError, match="between 1 and 5"):
|
with pytest.raises(ValueError, match="between 1 and 5"):
|
||||||
runs_chart(metric="study.loss", flow="study", latest=9)
|
runs_chart(metric="study.loss", flow="study", latest=9)
|
||||||
|
|
||||||
|
|
||||||
|
def test_a_document_written_as_pages_is_read_as_one_grid():
|
||||||
|
"""Stored dashboards live in each installation's repository.
|
||||||
|
|
||||||
|
So the old shape is normalised on the way in rather than migrated, and a
|
||||||
|
placed second section keeps its arrangement instead of piling onto the
|
||||||
|
first — which is how the viewer always drew it.
|
||||||
|
"""
|
||||||
|
old = {
|
||||||
|
"name": "house",
|
||||||
|
"pages": [
|
||||||
|
{
|
||||||
|
"id": "main",
|
||||||
|
"sections": [
|
||||||
|
{
|
||||||
|
"id": "a",
|
||||||
|
"widgets": [
|
||||||
|
{
|
||||||
|
"id": "top",
|
||||||
|
"type": "stat",
|
||||||
|
"layout": {"lg": {"x": 0, "y": 0, "w": 3, "h": 2}},
|
||||||
|
}
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "b",
|
||||||
|
"widgets": [
|
||||||
|
{
|
||||||
|
"id": "under",
|
||||||
|
"type": "stat",
|
||||||
|
"layout": {"lg": {"x": 0, "y": 1, "w": 3, "h": 2}},
|
||||||
|
}
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
read = DashboardDef.model_validate(old)
|
||||||
|
|
||||||
|
assert [w.id for w in read.widgets] == ["top", "under"]
|
||||||
|
# The first section is two rows deep, so the second one starts under it.
|
||||||
|
assert read.widgets[1].layout["lg"].y == 3
|
||||||
|
assert "pages" not in read.model_dump()
|
||||||
|
|||||||
@@ -8,8 +8,6 @@ control or another flow, it pulses a node that did nothing at all.
|
|||||||
from fluksio.flow.dashboards import (
|
from fluksio.flow.dashboards import (
|
||||||
DashboardDef,
|
DashboardDef,
|
||||||
DashboardStore,
|
DashboardStore,
|
||||||
PageDef,
|
|
||||||
SectionDef,
|
|
||||||
WidgetDef,
|
WidgetDef,
|
||||||
)
|
)
|
||||||
from fluksio.flow.events import EventBus
|
from fluksio.flow.events import EventBus
|
||||||
@@ -104,12 +102,6 @@ def test_the_widgets_wired_into_a_flow_are_reported(tmp_path):
|
|||||||
DashboardDef(
|
DashboardDef(
|
||||||
name="panel",
|
name="panel",
|
||||||
title="Panel",
|
title="Panel",
|
||||||
pages=[
|
|
||||||
PageDef(
|
|
||||||
id="main",
|
|
||||||
sections=[
|
|
||||||
SectionDef(
|
|
||||||
id="main",
|
|
||||||
widgets=[
|
widgets=[
|
||||||
WidgetDef(
|
WidgetDef(
|
||||||
id="setpoint",
|
id="setpoint",
|
||||||
@@ -131,10 +123,6 @@ def test_the_widgets_wired_into_a_flow_are_reported(tmp_path):
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
],
|
|
||||||
)
|
|
||||||
],
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
bindings = store.bindings_for("house")
|
bindings = store.bindings_for("house")
|
||||||
|
|||||||
@@ -124,7 +124,9 @@ async function captureRuns(page, dir) {
|
|||||||
// watched on, showing finished ones. This is the seam the feature exists for.
|
// watched on, showing finished ones. This is the seam the feature exists for.
|
||||||
const picked = await page
|
const picked = await page
|
||||||
.getByTestId("run-link")
|
.getByTestId("run-link")
|
||||||
.evaluateAll((links) => links.slice(0, 2).map((a) => a.getAttribute("href")))
|
.evaluateAll((links) =>
|
||||||
|
links.slice(0, 2).map((a) => a.getAttribute("href")),
|
||||||
|
)
|
||||||
const ids = picked
|
const ids = picked
|
||||||
.map((href) => (href || "").split("/").pop())
|
.map((href) => (href || "").split("/").pop())
|
||||||
.filter(Boolean)
|
.filter(Boolean)
|
||||||
@@ -132,7 +134,9 @@ async function captureRuns(page, dir) {
|
|||||||
const apiUrl = APP_URL.replace("//app.", "//api.")
|
const apiUrl = APP_URL.replace("//app.", "//api.")
|
||||||
const results = await page.evaluate(async (base) => {
|
const results = await page.evaluate(async (base) => {
|
||||||
const answer = await fetch(`${base}/api/v1/dashboards/`, {
|
const answer = await fetch(`${base}/api/v1/dashboards/`, {
|
||||||
headers: { Authorization: `Bearer ${localStorage.getItem("access_token")}` },
|
headers: {
|
||||||
|
Authorization: `Bearer ${localStorage.getItem("access_token")}`,
|
||||||
|
},
|
||||||
})
|
})
|
||||||
if (!answer.ok) return null
|
if (!answer.ok) return null
|
||||||
const body = await answer.json()
|
const body = await answer.json()
|
||||||
|
|||||||
@@ -428,12 +428,12 @@ export const DashboardDef_InputSchema = {
|
|||||||
title: 'Icon',
|
title: 'Icon',
|
||||||
default: ''
|
default: ''
|
||||||
},
|
},
|
||||||
pages: {
|
widgets: {
|
||||||
items: {
|
items: {
|
||||||
'$ref': '#/components/schemas/PageDef-Input'
|
'$ref': '#/components/schemas/WidgetDef'
|
||||||
},
|
},
|
||||||
type: 'array',
|
type: 'array',
|
||||||
title: 'Pages'
|
title: 'Widgets'
|
||||||
},
|
},
|
||||||
settings: {
|
settings: {
|
||||||
additionalProperties: {
|
additionalProperties: {
|
||||||
@@ -496,12 +496,12 @@ export const DashboardDef_OutputSchema = {
|
|||||||
title: 'Icon',
|
title: 'Icon',
|
||||||
default: ''
|
default: ''
|
||||||
},
|
},
|
||||||
pages: {
|
widgets: {
|
||||||
items: {
|
items: {
|
||||||
'$ref': '#/components/schemas/PageDef-Output'
|
'$ref': '#/components/schemas/WidgetDef'
|
||||||
},
|
},
|
||||||
type: 'array',
|
type: 'array',
|
||||||
title: 'Pages'
|
title: 'Widgets'
|
||||||
},
|
},
|
||||||
settings: {
|
settings: {
|
||||||
additionalProperties: {
|
additionalProperties: {
|
||||||
@@ -538,11 +538,6 @@ export const DashboardSummarySchema = {
|
|||||||
title: 'Title',
|
title: 'Title',
|
||||||
default: ''
|
default: ''
|
||||||
},
|
},
|
||||||
page_count: {
|
|
||||||
type: 'integer',
|
|
||||||
title: 'Page Count',
|
|
||||||
default: 0
|
|
||||||
},
|
|
||||||
widget_count: {
|
widget_count: {
|
||||||
type: 'integer',
|
type: 'integer',
|
||||||
title: 'Widget Count',
|
title: 'Widget Count',
|
||||||
@@ -2048,66 +2043,6 @@ export const OAuthClientRegisterSchema = {
|
|||||||
description: 'RFC 7591 dynamic client registration request.'
|
description: 'RFC 7591 dynamic client registration request.'
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export const PageDef_InputSchema = {
|
|
||||||
properties: {
|
|
||||||
id: {
|
|
||||||
type: 'string',
|
|
||||||
title: 'Id'
|
|
||||||
},
|
|
||||||
title: {
|
|
||||||
type: 'string',
|
|
||||||
title: 'Title',
|
|
||||||
default: ''
|
|
||||||
},
|
|
||||||
icon: {
|
|
||||||
type: 'string',
|
|
||||||
title: 'Icon',
|
|
||||||
default: ''
|
|
||||||
},
|
|
||||||
sections: {
|
|
||||||
items: {
|
|
||||||
'$ref': '#/components/schemas/SectionDef-Input'
|
|
||||||
},
|
|
||||||
type: 'array',
|
|
||||||
title: 'Sections'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
type: 'object',
|
|
||||||
required: ['id'],
|
|
||||||
title: 'PageDef',
|
|
||||||
description: 'One tab of a dashboard.'
|
|
||||||
} as const;
|
|
||||||
|
|
||||||
export const PageDef_OutputSchema = {
|
|
||||||
properties: {
|
|
||||||
id: {
|
|
||||||
type: 'string',
|
|
||||||
title: 'Id'
|
|
||||||
},
|
|
||||||
title: {
|
|
||||||
type: 'string',
|
|
||||||
title: 'Title',
|
|
||||||
default: ''
|
|
||||||
},
|
|
||||||
icon: {
|
|
||||||
type: 'string',
|
|
||||||
title: 'Icon',
|
|
||||||
default: ''
|
|
||||||
},
|
|
||||||
sections: {
|
|
||||||
items: {
|
|
||||||
'$ref': '#/components/schemas/SectionDef-Output'
|
|
||||||
},
|
|
||||||
type: 'array',
|
|
||||||
title: 'Sections'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
type: 'object',
|
|
||||||
required: ['id'],
|
|
||||||
title: 'PageDef',
|
|
||||||
description: 'One tab of a dashboard.'
|
|
||||||
} as const;
|
|
||||||
|
|
||||||
export const PairRequestSchema = {
|
export const PairRequestSchema = {
|
||||||
properties: {
|
properties: {
|
||||||
code: {
|
code: {
|
||||||
@@ -2676,56 +2611,6 @@ export const SecretValueSchema = {
|
|||||||
title: 'SecretValue'
|
title: 'SecretValue'
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export const SectionDef_InputSchema = {
|
|
||||||
properties: {
|
|
||||||
id: {
|
|
||||||
type: 'string',
|
|
||||||
title: 'Id'
|
|
||||||
},
|
|
||||||
title: {
|
|
||||||
type: 'string',
|
|
||||||
title: 'Title',
|
|
||||||
default: ''
|
|
||||||
},
|
|
||||||
widgets: {
|
|
||||||
items: {
|
|
||||||
'$ref': '#/components/schemas/WidgetDef'
|
|
||||||
},
|
|
||||||
type: 'array',
|
|
||||||
title: 'Widgets'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
type: 'object',
|
|
||||||
required: ['id'],
|
|
||||||
title: 'SectionDef',
|
|
||||||
description: 'A grid of widgets under a heading.'
|
|
||||||
} as const;
|
|
||||||
|
|
||||||
export const SectionDef_OutputSchema = {
|
|
||||||
properties: {
|
|
||||||
id: {
|
|
||||||
type: 'string',
|
|
||||||
title: 'Id'
|
|
||||||
},
|
|
||||||
title: {
|
|
||||||
type: 'string',
|
|
||||||
title: 'Title',
|
|
||||||
default: ''
|
|
||||||
},
|
|
||||||
widgets: {
|
|
||||||
items: {
|
|
||||||
'$ref': '#/components/schemas/WidgetDef'
|
|
||||||
},
|
|
||||||
type: 'array',
|
|
||||||
title: 'Widgets'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
type: 'object',
|
|
||||||
required: ['id'],
|
|
||||||
title: 'SectionDef',
|
|
||||||
description: 'A grid of widgets under a heading.'
|
|
||||||
} as const;
|
|
||||||
|
|
||||||
export const SeriesAnswerSchema = {
|
export const SeriesAnswerSchema = {
|
||||||
properties: {
|
properties: {
|
||||||
metric: {
|
metric: {
|
||||||
|
|||||||
@@ -116,7 +116,7 @@ export type DashboardDef_Input = {
|
|||||||
canvas_width?: number;
|
canvas_width?: number;
|
||||||
canvas_height?: number;
|
canvas_height?: number;
|
||||||
icon?: string;
|
icon?: string;
|
||||||
pages?: Array<PageDef_Input>;
|
widgets?: Array<WidgetDef>;
|
||||||
settings?: {
|
settings?: {
|
||||||
[key: string]: SettingDef;
|
[key: string]: SettingDef;
|
||||||
};
|
};
|
||||||
@@ -134,7 +134,7 @@ export type DashboardDef_Output = {
|
|||||||
canvas_width?: number;
|
canvas_width?: number;
|
||||||
canvas_height?: number;
|
canvas_height?: number;
|
||||||
icon?: string;
|
icon?: string;
|
||||||
pages?: Array<PageDef_Output>;
|
widgets?: Array<WidgetDef>;
|
||||||
settings?: {
|
settings?: {
|
||||||
[key: string]: SettingDef;
|
[key: string]: SettingDef;
|
||||||
};
|
};
|
||||||
@@ -153,7 +153,6 @@ export type DashboardsPublic = {
|
|||||||
export type DashboardSummary = {
|
export type DashboardSummary = {
|
||||||
name: string;
|
name: string;
|
||||||
title?: string;
|
title?: string;
|
||||||
page_count?: number;
|
|
||||||
widget_count?: number;
|
widget_count?: number;
|
||||||
has_draft?: boolean;
|
has_draft?: boolean;
|
||||||
version?: number;
|
version?: number;
|
||||||
@@ -750,26 +749,6 @@ export type OAuthClientRegister = {
|
|||||||
token_endpoint_auth_method?: (string | null);
|
token_endpoint_auth_method?: (string | null);
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
|
||||||
* One tab of a dashboard.
|
|
||||||
*/
|
|
||||||
export type PageDef_Input = {
|
|
||||||
id: string;
|
|
||||||
title?: string;
|
|
||||||
icon?: string;
|
|
||||||
sections?: Array<SectionDef_Input>;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* One tab of a dashboard.
|
|
||||||
*/
|
|
||||||
export type PageDef_Output = {
|
|
||||||
id: string;
|
|
||||||
title?: string;
|
|
||||||
icon?: string;
|
|
||||||
sections?: Array<SectionDef_Output>;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type PairRequest = {
|
export type PairRequest = {
|
||||||
code: string;
|
code: string;
|
||||||
};
|
};
|
||||||
@@ -947,24 +926,6 @@ export type SecretValue = {
|
|||||||
value: string;
|
value: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
|
||||||
* A grid of widgets under a heading.
|
|
||||||
*/
|
|
||||||
export type SectionDef_Input = {
|
|
||||||
id: string;
|
|
||||||
title?: string;
|
|
||||||
widgets?: Array<WidgetDef>;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A grid of widgets under a heading.
|
|
||||||
*/
|
|
||||||
export type SectionDef_Output = {
|
|
||||||
id: string;
|
|
||||||
title?: string;
|
|
||||||
widgets?: Array<WidgetDef>;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type SeriesAnswer = {
|
export type SeriesAnswer = {
|
||||||
metric: string;
|
metric: string;
|
||||||
lines?: Array<MetricSeries>;
|
lines?: Array<MetricSeries>;
|
||||||
|
|||||||
@@ -68,32 +68,26 @@ type Block = {
|
|||||||
/**
|
/**
|
||||||
* The first page's widgets as one grid.
|
* The first page's widgets as one grid.
|
||||||
*
|
*
|
||||||
* Sections are separate grids on the real thing, each starting at its own row
|
|
||||||
* zero, so each is pushed down past the one before it to keep them apart here.
|
|
||||||
*/
|
*/
|
||||||
function blocksOf(dashboard: DashboardDef_Output): {
|
function blocksOf(dashboard: DashboardDef_Output): {
|
||||||
blocks: Block[]
|
blocks: Block[]
|
||||||
rows: number
|
rows: number
|
||||||
} {
|
} {
|
||||||
const blocks: Block[] = []
|
const blocks: Block[] = []
|
||||||
let offset = 0
|
let rows = 0
|
||||||
for (const section of (dashboard.pages ?? [])[0]?.sections ?? []) {
|
for (const widget of dashboard.widgets ?? []) {
|
||||||
let bottom = 0
|
|
||||||
for (const widget of section.widgets ?? []) {
|
|
||||||
const { x = 0, y = 0, w = 3, h = 2 } = placement(widget)
|
const { x = 0, y = 0, w = 3, h = 2 } = placement(widget)
|
||||||
blocks.push({
|
blocks.push({
|
||||||
id: widget.id,
|
id: widget.id,
|
||||||
type: widget.type,
|
type: widget.type,
|
||||||
x: Math.max(0, x),
|
x: Math.max(0, x),
|
||||||
y: Math.max(0, y) + offset,
|
y: Math.max(0, y),
|
||||||
w: Math.max(1, w),
|
w: Math.max(1, w),
|
||||||
h: Math.max(1, h),
|
h: Math.max(1, h),
|
||||||
})
|
})
|
||||||
bottom = Math.max(bottom, Math.max(0, y) + Math.max(1, h))
|
rows = Math.max(rows, Math.max(0, y) + Math.max(1, h))
|
||||||
}
|
}
|
||||||
offset += bottom
|
return { blocks, rows: Math.max(1, rows) }
|
||||||
}
|
|
||||||
return { blocks, rows: Math.max(1, offset) }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -59,14 +59,11 @@ import {
|
|||||||
columnsOf,
|
columnsOf,
|
||||||
type Dashboard,
|
type Dashboard,
|
||||||
DashboardView,
|
DashboardView,
|
||||||
flatWidgets,
|
|
||||||
GRID_GAP,
|
GRID_GAP,
|
||||||
isPlaced,
|
isPlaced,
|
||||||
pagesOf,
|
|
||||||
placement,
|
placement,
|
||||||
rowHeightOf,
|
rowHeightOf,
|
||||||
rowsOf,
|
rowsOf,
|
||||||
sectionsOf,
|
|
||||||
widgetsOf,
|
widgetsOf,
|
||||||
} from "./DashboardView"
|
} from "./DashboardView"
|
||||||
import { PanelRail } from "./PanelRail"
|
import { PanelRail } from "./PanelRail"
|
||||||
@@ -147,13 +144,7 @@ const INTERACTIVE =
|
|||||||
"button, a, input, select, textarea, [role='switch'], [role='combobox'], [role='slider'], .react-resizable-handle, .widget-grip"
|
"button, a, input, select, textarea, [role='switch'], [role='combobox'], [role='slider'], .react-resizable-handle, .widget-grip"
|
||||||
|
|
||||||
function nextId(dashboard: DashboardDef_Output, type: string): string {
|
function nextId(dashboard: DashboardDef_Output, type: string): string {
|
||||||
const taken = new Set(
|
const taken = new Set(widgetsOf(dashboard).map((widget) => widget.id))
|
||||||
pagesOf(dashboard).flatMap((page) =>
|
|
||||||
sectionsOf(page).flatMap((section) =>
|
|
||||||
widgetsOf(section).map((widget) => widget.id),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
let candidate = type
|
let candidate = type
|
||||||
for (let i = 2; taken.has(candidate); i++) candidate = `${type}${i}`
|
for (let i = 2; taken.has(candidate); i++) candidate = `${type}${i}`
|
||||||
return candidate
|
return candidate
|
||||||
@@ -323,34 +314,10 @@ export function DashboardEditor({
|
|||||||
// a save in flight is still "no unpublished changes" until it lands.
|
// a save in flight is still "no unpublished changes" until it lands.
|
||||||
const hasDraft = Boolean(dashboard.has_draft)
|
const hasDraft = Boolean(dashboard.has_draft)
|
||||||
const columns = columnsOf(draft)
|
const columns = columnsOf(draft)
|
||||||
const page = pagesOf(draft)[0]
|
const widgets = widgetsOf(draft)
|
||||||
// Every widget the panel shows, not just the first section's — a document
|
|
||||||
// written with several sections is one arrangement here, as it is there.
|
|
||||||
const widgets = page ? flatWidgets(page) : []
|
|
||||||
|
|
||||||
const updateWidgets = (next: WidgetDef[]) => {
|
const updateWidgets = (next: WidgetDef[]) => {
|
||||||
if (!page) return
|
commit({ ...draft, widgets: next })
|
||||||
commit({
|
|
||||||
...draft,
|
|
||||||
// Only this page is rewritten: a document carrying pages the editor
|
|
||||||
// does not show round-trips them untouched.
|
|
||||||
pages: pagesOf(draft).map((candidate) =>
|
|
||||||
candidate.id !== page.id
|
|
||||||
? candidate
|
|
||||||
: {
|
|
||||||
...candidate,
|
|
||||||
// One canvas, one grid: what several sections were read as is
|
|
||||||
// written back as one, which is also what keeps their row
|
|
||||||
// offsets from being applied a second time on the next read.
|
|
||||||
sections: [
|
|
||||||
{
|
|
||||||
...(sectionsOf(candidate)[0] ?? { id: "main" }),
|
|
||||||
widgets: next,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
),
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const addWidget = (type: WidgetKind) => {
|
const addWidget = (type: WidgetKind) => {
|
||||||
@@ -468,13 +435,13 @@ export function DashboardEditor({
|
|||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
|
|
||||||
const body = !page ? (
|
const body =
|
||||||
<p className="text-sm text-muted-foreground">
|
edit && widgets.length === 0 ? (
|
||||||
This dashboard has no pages yet.
|
<p
|
||||||
</p>
|
className="text-sm text-muted-foreground"
|
||||||
) : edit && widgets.length === 0 ? (
|
data-testid="dashboard-empty"
|
||||||
<p className="text-sm text-muted-foreground" data-testid="dashboard-empty">
|
>
|
||||||
Nothing on this page yet. Add a widget from the bar below.
|
Nothing on this dashboard yet. Add a widget from the bar below.
|
||||||
</p>
|
</p>
|
||||||
) : stacked ? (
|
) : stacked ? (
|
||||||
// One column at the viewport's width. Edit mode still picks a widget and
|
// One column at the viewport's width. Edit mode still picks a widget and
|
||||||
|
|||||||
@@ -1,12 +1,6 @@
|
|||||||
import { motion } from "motion/react"
|
import { motion } from "motion/react"
|
||||||
import { useEffect, useRef, useState } from "react"
|
import { useEffect, useRef, useState } from "react"
|
||||||
import type {
|
import type { DashboardDef_Output, Placement, WidgetDef } from "@/client"
|
||||||
DashboardDef_Output,
|
|
||||||
PageDef_Output,
|
|
||||||
Placement,
|
|
||||||
SectionDef_Output,
|
|
||||||
WidgetDef,
|
|
||||||
} from "@/client"
|
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils"
|
||||||
import "./dashboard.css"
|
import "./dashboard.css"
|
||||||
@@ -68,9 +62,8 @@ export const RAIL_INSET = 72
|
|||||||
* The generated client marks every list optional, because the server fills
|
* The generated client marks every list optional, because the server fills
|
||||||
* them in. These three keep that from spreading through the components.
|
* them in. These three keep that from spreading through the components.
|
||||||
*/
|
*/
|
||||||
export const pagesOf = (dashboard: DashboardDef_Output) => dashboard.pages ?? []
|
export const widgetsOf = (dashboard: DashboardDef_Output) =>
|
||||||
export const sectionsOf = (page: PageDef_Output) => page.sections ?? []
|
dashboard.widgets ?? []
|
||||||
export const widgetsOf = (section: SectionDef_Output) => section.widgets ?? []
|
|
||||||
|
|
||||||
export const columnsOf = (dashboard: Dashboard) =>
|
export const columnsOf = (dashboard: Dashboard) =>
|
||||||
dashboard.columns || DEFAULT_COLUMNS
|
dashboard.columns || DEFAULT_COLUMNS
|
||||||
@@ -293,52 +286,6 @@ export const isPlaced = (widgets: WidgetDef[]) =>
|
|||||||
return x > 0 || y > 0
|
return x > 0 || y > 0
|
||||||
})
|
})
|
||||||
|
|
||||||
/**
|
|
||||||
* The page's widgets as one arrangement.
|
|
||||||
*
|
|
||||||
* Sections used to be drawn as separate grids stacked down the page, each
|
|
||||||
* starting at its own row zero, so a document written with several of them is
|
|
||||||
* read as one grid by pushing each below the one before it. That is what lets
|
|
||||||
* the editor and the panel show the same thing: the editor arranges one grid,
|
|
||||||
* and it now arranges all of them.
|
|
||||||
*
|
|
||||||
* An unarranged document is left alone — every widget still sits at 0,0 there,
|
|
||||||
* so there is no arrangement to keep apart and the grid auto-flows instead.
|
|
||||||
*
|
|
||||||
* ponytail: a section's own heading stops being drawn with it. No UI ever
|
|
||||||
* wrote one, so only a seeded document has any — restoring them means giving
|
|
||||||
* a group a box on the canvas, which is a widget, not a section.
|
|
||||||
*/
|
|
||||||
export function flatWidgets(page: PageDef_Output): WidgetDef[] {
|
|
||||||
const sections = sectionsOf(page)
|
|
||||||
const all = sections.flatMap(widgetsOf)
|
|
||||||
if (sections.length < 2 || !isPlaced(all)) return all
|
|
||||||
|
|
||||||
const flat: WidgetDef[] = []
|
|
||||||
let offset = 0
|
|
||||||
for (const section of sections) {
|
|
||||||
let bottom = 0
|
|
||||||
for (const widget of widgetsOf(section)) {
|
|
||||||
const placed = placement(widget)
|
|
||||||
const y = Math.max(0, placed.y ?? 0)
|
|
||||||
bottom = Math.max(bottom, y + Math.max(1, placed.h ?? 2))
|
|
||||||
flat.push(
|
|
||||||
offset === 0
|
|
||||||
? widget
|
|
||||||
: {
|
|
||||||
...widget,
|
|
||||||
layout: {
|
|
||||||
...(widget.layout ?? {}),
|
|
||||||
lg: { ...placed, y: y + offset },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
offset += bottom
|
|
||||||
}
|
|
||||||
return flat
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* One page, drawn as the single grid the panel shows.
|
* One page, drawn as the single grid the panel shows.
|
||||||
*
|
*
|
||||||
@@ -360,26 +307,14 @@ export function DashboardView({
|
|||||||
/** Whether the panel carries a rail, which takes a column of the canvas. */
|
/** Whether the panel carries a rail, which takes a column of the canvas. */
|
||||||
rail?: boolean
|
rail?: boolean
|
||||||
}) {
|
}) {
|
||||||
// ponytail: the first page only. A panel carries several whole dashboards
|
const all = widgetsOf(dashboard)
|
||||||
// — that is what the rail is for — so nothing writes a second page.
|
|
||||||
const page = pagesOf(dashboard)[0]
|
|
||||||
|
|
||||||
if (!page) {
|
|
||||||
return (
|
|
||||||
<p className="text-sm text-muted-foreground">
|
|
||||||
This dashboard has no pages yet.
|
|
||||||
</p>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const all = flatWidgets(page)
|
|
||||||
if (all.length === 0) {
|
if (all.length === 0) {
|
||||||
return (
|
return (
|
||||||
<p
|
<p
|
||||||
className="text-sm text-muted-foreground"
|
className="text-sm text-muted-foreground"
|
||||||
data-testid="dashboard-empty"
|
data-testid="dashboard-empty"
|
||||||
>
|
>
|
||||||
Nothing on this page yet. Edit it to add a widget.
|
Nothing on this dashboard yet. Edit it to add a widget.
|
||||||
</p>
|
</p>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import {
|
|||||||
runQueryOptions,
|
runQueryOptions,
|
||||||
shortId,
|
shortId,
|
||||||
} from "@/components/Runs/queries"
|
} from "@/components/Runs/queries"
|
||||||
import { type Dashboard, flatWidgets, pagesOf } from "./DashboardView"
|
import { type Dashboard, widgetsOf } from "./DashboardView"
|
||||||
import {
|
import {
|
||||||
type ContextLine,
|
type ContextLine,
|
||||||
type DataContext,
|
type DataContext,
|
||||||
@@ -22,10 +22,9 @@ import {
|
|||||||
* asking the server for its whole series would be a query nobody draws.
|
* asking the server for its whole series would be a query nobody draws.
|
||||||
*/
|
*/
|
||||||
function chartedNames(dashboard: Dashboard | undefined): string[] {
|
function chartedNames(dashboard: Dashboard | undefined): string[] {
|
||||||
const page = pagesOf(dashboard ?? ({} as Dashboard))[0]
|
if (!dashboard) return []
|
||||||
if (!page) return []
|
|
||||||
const names = new Set<string>()
|
const names = new Set<string>()
|
||||||
for (const widget of flatWidgets(page)) {
|
for (const widget of widgetsOf(dashboard)) {
|
||||||
if (widget.type !== "chart") continue
|
if (widget.type !== "chart") continue
|
||||||
const config = (widget.config ?? {}) as {
|
const config = (widget.config ?? {}) as {
|
||||||
series?: { message?: string }[]
|
series?: { message?: string }[]
|
||||||
|
|||||||
@@ -217,8 +217,6 @@ function Dashboards() {
|
|||||||
draft={dashboard.has_draft}
|
draft={dashboard.has_draft}
|
||||||
detail={`${dashboard.widget_count} widget${
|
detail={`${dashboard.widget_count} widget${
|
||||||
dashboard.widget_count === 1 ? "" : "s"
|
dashboard.widget_count === 1 ? "" : "s"
|
||||||
} · ${dashboard.page_count} page${
|
|
||||||
dashboard.page_count === 1 ? "" : "s"
|
|
||||||
}`}
|
}`}
|
||||||
selecting={selection.selecting}
|
selecting={selection.selecting}
|
||||||
selected={selection.selected.includes(dashboard.name)}
|
selected={selection.selected.includes(dashboard.name)}
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ test.beforeAll(async ({ browser }) => {
|
|||||||
dashboard.columns = 12
|
dashboard.columns = 12
|
||||||
dashboard.canvas_width = 1920
|
dashboard.canvas_width = 1920
|
||||||
dashboard.canvas_height = 1080
|
dashboard.canvas_height = 1080
|
||||||
dashboard.pages[0].sections[0].widgets = WIDGETS
|
dashboard.widgets = WIDGETS
|
||||||
await api(page, `/dashboards/${dashboardName}`, {
|
await api(page, `/dashboards/${dashboardName}`, {
|
||||||
method: "PUT",
|
method: "PUT",
|
||||||
data: dashboard,
|
data: dashboard,
|
||||||
@@ -65,7 +65,7 @@ async function placements(page: Page) {
|
|||||||
const doc = await (
|
const doc = await (
|
||||||
await api(page, `/dashboards/${dashboardName}?draft=true`)
|
await api(page, `/dashboards/${dashboardName}?draft=true`)
|
||||||
).json()
|
).json()
|
||||||
const widgets = doc.pages[0].sections[0].widgets as {
|
const widgets = doc.widgets as {
|
||||||
id: string
|
id: string
|
||||||
layout: { lg: { x: number; y: number } }
|
layout: { lg: { x: number; y: number } }
|
||||||
}[]
|
}[]
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ test.beforeAll(async ({ browser }) => {
|
|||||||
const dashboard = await (
|
const dashboard = await (
|
||||||
await api(page, `/dashboards/${dashboardName}`, { method: "POST" })
|
await api(page, `/dashboards/${dashboardName}`, { method: "POST" })
|
||||||
).json()
|
).json()
|
||||||
dashboard.pages[0].sections[0].widgets = [
|
dashboard.widgets = [
|
||||||
{
|
{
|
||||||
id: "lever",
|
id: "lever",
|
||||||
type: "slider",
|
type: "slider",
|
||||||
|
|||||||
@@ -174,7 +174,7 @@ test.beforeAll(async ({ browser }) => {
|
|||||||
const dashboard = await (
|
const dashboard = await (
|
||||||
await api(page, `/dashboards/${dashboardName}`, { method: "POST" })
|
await api(page, `/dashboards/${dashboardName}`, { method: "POST" })
|
||||||
).json()
|
).json()
|
||||||
dashboard.pages[0].sections[0].widgets = [
|
dashboard.widgets = [
|
||||||
{
|
{
|
||||||
id: "top",
|
id: "top",
|
||||||
type: "stat",
|
type: "stat",
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ test.beforeAll(async ({ browser }) => {
|
|||||||
await api(page, `/dashboards/${name}`, { method: "POST" })
|
await api(page, `/dashboards/${name}`, { method: "POST" })
|
||||||
const doc = await (await api(page, `/dashboards/${name}?draft=true`)).json()
|
const doc = await (await api(page, `/dashboards/${name}?draft=true`)).json()
|
||||||
doc.settings = { look: { value: "glass" } }
|
doc.settings = { look: { value: "glass" } }
|
||||||
doc.pages[0].sections[0].widgets = [
|
doc.widgets = [
|
||||||
{
|
{
|
||||||
id: "stat",
|
id: "stat",
|
||||||
type: "stat",
|
type: "stat",
|
||||||
|
|||||||
@@ -89,7 +89,7 @@ test.beforeAll(async ({ browser }) => {
|
|||||||
// Two rows taller than the default panel: the tiles below already fill it,
|
// Two rows taller than the default panel: the tiles below already fill it,
|
||||||
// and a widget past the last row is clipped rather than drawn.
|
// and a widget past the last row is clipped rather than drawn.
|
||||||
dashboard.canvas_height = 1400
|
dashboard.canvas_height = 1400
|
||||||
dashboard.pages[0].sections[0].widgets = [
|
dashboard.widgets = [
|
||||||
{
|
{
|
||||||
id: "load",
|
id: "load",
|
||||||
type: "bar",
|
type: "bar",
|
||||||
@@ -202,7 +202,7 @@ test.beforeAll(async ({ browser }) => {
|
|||||||
const stack = await (
|
const stack = await (
|
||||||
await api(page, `/dashboards/${stackName}`, { method: "POST" })
|
await api(page, `/dashboards/${stackName}`, { method: "POST" })
|
||||||
).json()
|
).json()
|
||||||
stack.pages[0].sections[0].widgets = [
|
stack.widgets = [
|
||||||
{
|
{
|
||||||
id: "split",
|
id: "split",
|
||||||
type: "bar",
|
type: "bar",
|
||||||
|
|||||||
Reference in New Issue
Block a user