Draw a round dashboard in the mosaic as its circle with a dot per face

DashboardSummary carries the dashboard's shape, so the list no longer lays
a round dashboard's faces out at grid places they never use.
This commit is contained in:
2026-10-15 15:54:47 +02:00
parent 0c4b23de8a
commit a34f8f436c
5 changed files with 46 additions and 0 deletions
+4
View File
@@ -598,6 +598,9 @@ class DashboardSummary(BaseModel):
version: int = 1
#: The grid `footprint` is placed in — see ``DashboardDef.columns``.
columns: int = 12
#: A round dashboard's widgets are faces rather than tiles, so a list
#: draws them as a circle instead of at the grid places they carry.
shape: Literal["rect", "round"] = "rect"
#: Every widget's shape, so a list can draw the dashboard's outline
#: without a document read per tile.
footprint: list[WidgetFootprint] = Field(default_factory=list)
@@ -666,6 +669,7 @@ class DashboardStore:
has_draft=defn.has_draft,
version=defn.version,
columns=defn.columns,
shape=defn.shape,
footprint=defn.footprint,
updated_at=self.updated_at(name),
)
+7
View File
@@ -415,6 +415,7 @@ def test_a_summary_carries_the_footprint_of_the_document(store: DashboardStore):
(summary,) = store.list()
assert summary.columns == saved.columns
assert summary.shape == "rect"
assert [(f.type, f.x, f.y, f.w, f.h) for f in summary.footprint] == [
("chart", 2, 1, 6, 4),
("gauge", 0, 5, 2, 2),
@@ -422,6 +423,12 @@ def test_a_summary_carries_the_footprint_of_the_document(store: DashboardStore):
]
def test_a_summary_says_a_dashboard_is_round(store: DashboardStore):
store.write(DashboardDef(name="knob", shape="round"))
assert store.list()[0].shape == "round"
def test_a_summary_carries_when_the_working_copy_was_written(store: DashboardStore):
published = store.write(default_dashboard("house"))
document = store.root / "house" / "dashboard.json"
+6
View File
@@ -828,6 +828,12 @@ export const DashboardSummarySchema = {
title: 'Columns',
default: 12
},
shape: {
type: 'string',
enum: ['rect', 'round'],
title: 'Shape',
default: 'rect'
},
footprint: {
items: {
'$ref': '#/components/schemas/WidgetFootprint'
+1
View File
@@ -215,6 +215,7 @@ export type DashboardSummary = {
has_draft?: boolean;
version?: number;
columns?: number;
shape?: 'rect' | 'round';
footprint?: Array<WidgetFootprint>;
updated_at?: (string | null);
};
@@ -65,6 +65,9 @@ function blocksOf(footprint: WidgetFootprint[]): {
* dashboard. The summary carries the shapes, so it costs no request either.
*/
function Footprint({ dashboard }: { dashboard: DashboardSummary }) {
if (dashboard.shape === "round") {
return <RoundFootprint faces={dashboard.footprint ?? []} />
}
const { blocks, rows } = blocksOf(dashboard.footprint ?? [])
const columns = dashboard.columns || DEFAULT_COLUMNS
// Before the editor could place things, every widget was written at 0,0;
@@ -112,6 +115,31 @@ function Footprint({ dashboard }: { dashboard: DashboardSummary }) {
)
}
/**
* A round dashboard shows one face at a time, so its grid places mean
* nothing: it is drawn as the circle, with a dot per face where a panel puts
* its page dots.
*/
function RoundFootprint({ faces }: { faces: WidgetFootprint[] }) {
return (
<div
aria-hidden
className="flex aspect-video items-center justify-center rounded-sm bg-muted p-1"
>
<div className="relative aspect-square h-full rounded-full border-2 border-primary/45">
<div className="absolute inset-x-[15%] bottom-[15%] flex flex-wrap justify-center gap-1">
{faces.map((face, index) => (
<span
key={index}
className={cn("size-2 rounded-full", shade(face.type))}
/>
))}
</div>
</div>
</div>
)
}
/** One dashboard in the mosaic. */
function Tile({
dashboard,