Make a dashboard its widgets: drop the pages and sections nobody drew
This commit is contained in:
@@ -428,12 +428,12 @@ export const DashboardDef_InputSchema = {
|
||||
title: 'Icon',
|
||||
default: ''
|
||||
},
|
||||
pages: {
|
||||
widgets: {
|
||||
items: {
|
||||
'$ref': '#/components/schemas/PageDef-Input'
|
||||
'$ref': '#/components/schemas/WidgetDef'
|
||||
},
|
||||
type: 'array',
|
||||
title: 'Pages'
|
||||
title: 'Widgets'
|
||||
},
|
||||
settings: {
|
||||
additionalProperties: {
|
||||
@@ -496,12 +496,12 @@ export const DashboardDef_OutputSchema = {
|
||||
title: 'Icon',
|
||||
default: ''
|
||||
},
|
||||
pages: {
|
||||
widgets: {
|
||||
items: {
|
||||
'$ref': '#/components/schemas/PageDef-Output'
|
||||
'$ref': '#/components/schemas/WidgetDef'
|
||||
},
|
||||
type: 'array',
|
||||
title: 'Pages'
|
||||
title: 'Widgets'
|
||||
},
|
||||
settings: {
|
||||
additionalProperties: {
|
||||
@@ -538,11 +538,6 @@ export const DashboardSummarySchema = {
|
||||
title: 'Title',
|
||||
default: ''
|
||||
},
|
||||
page_count: {
|
||||
type: 'integer',
|
||||
title: 'Page Count',
|
||||
default: 0
|
||||
},
|
||||
widget_count: {
|
||||
type: 'integer',
|
||||
title: 'Widget Count',
|
||||
@@ -2048,66 +2043,6 @@ export const OAuthClientRegisterSchema = {
|
||||
description: 'RFC 7591 dynamic client registration request.'
|
||||
} 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 = {
|
||||
properties: {
|
||||
code: {
|
||||
@@ -2676,56 +2611,6 @@ export const SecretValueSchema = {
|
||||
title: 'SecretValue'
|
||||
} 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 = {
|
||||
properties: {
|
||||
metric: {
|
||||
|
||||
@@ -116,7 +116,7 @@ export type DashboardDef_Input = {
|
||||
canvas_width?: number;
|
||||
canvas_height?: number;
|
||||
icon?: string;
|
||||
pages?: Array<PageDef_Input>;
|
||||
widgets?: Array<WidgetDef>;
|
||||
settings?: {
|
||||
[key: string]: SettingDef;
|
||||
};
|
||||
@@ -134,7 +134,7 @@ export type DashboardDef_Output = {
|
||||
canvas_width?: number;
|
||||
canvas_height?: number;
|
||||
icon?: string;
|
||||
pages?: Array<PageDef_Output>;
|
||||
widgets?: Array<WidgetDef>;
|
||||
settings?: {
|
||||
[key: string]: SettingDef;
|
||||
};
|
||||
@@ -153,7 +153,6 @@ export type DashboardsPublic = {
|
||||
export type DashboardSummary = {
|
||||
name: string;
|
||||
title?: string;
|
||||
page_count?: number;
|
||||
widget_count?: number;
|
||||
has_draft?: boolean;
|
||||
version?: number;
|
||||
@@ -750,26 +749,6 @@ export type OAuthClientRegister = {
|
||||
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 = {
|
||||
code: string;
|
||||
};
|
||||
@@ -947,24 +926,6 @@ export type SecretValue = {
|
||||
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 = {
|
||||
metric: string;
|
||||
lines?: Array<MetricSeries>;
|
||||
|
||||
@@ -68,32 +68,26 @@ type Block = {
|
||||
/**
|
||||
* 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): {
|
||||
blocks: Block[]
|
||||
rows: number
|
||||
} {
|
||||
const blocks: Block[] = []
|
||||
let offset = 0
|
||||
for (const section of (dashboard.pages ?? [])[0]?.sections ?? []) {
|
||||
let bottom = 0
|
||||
for (const widget of section.widgets ?? []) {
|
||||
const { x = 0, y = 0, w = 3, h = 2 } = placement(widget)
|
||||
blocks.push({
|
||||
id: widget.id,
|
||||
type: widget.type,
|
||||
x: Math.max(0, x),
|
||||
y: Math.max(0, y) + offset,
|
||||
w: Math.max(1, w),
|
||||
h: Math.max(1, h),
|
||||
})
|
||||
bottom = Math.max(bottom, Math.max(0, y) + Math.max(1, h))
|
||||
}
|
||||
offset += bottom
|
||||
let rows = 0
|
||||
for (const widget of dashboard.widgets ?? []) {
|
||||
const { x = 0, y = 0, w = 3, h = 2 } = placement(widget)
|
||||
blocks.push({
|
||||
id: widget.id,
|
||||
type: widget.type,
|
||||
x: Math.max(0, x),
|
||||
y: Math.max(0, y),
|
||||
w: Math.max(1, w),
|
||||
h: Math.max(1, h),
|
||||
})
|
||||
rows = Math.max(rows, Math.max(0, y) + Math.max(1, h))
|
||||
}
|
||||
return { blocks, rows: Math.max(1, offset) }
|
||||
return { blocks, rows: Math.max(1, rows) }
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -59,14 +59,11 @@ import {
|
||||
columnsOf,
|
||||
type Dashboard,
|
||||
DashboardView,
|
||||
flatWidgets,
|
||||
GRID_GAP,
|
||||
isPlaced,
|
||||
pagesOf,
|
||||
placement,
|
||||
rowHeightOf,
|
||||
rowsOf,
|
||||
sectionsOf,
|
||||
widgetsOf,
|
||||
} from "./DashboardView"
|
||||
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"
|
||||
|
||||
function nextId(dashboard: DashboardDef_Output, type: string): string {
|
||||
const taken = new Set(
|
||||
pagesOf(dashboard).flatMap((page) =>
|
||||
sectionsOf(page).flatMap((section) =>
|
||||
widgetsOf(section).map((widget) => widget.id),
|
||||
),
|
||||
),
|
||||
)
|
||||
const taken = new Set(widgetsOf(dashboard).map((widget) => widget.id))
|
||||
let candidate = type
|
||||
for (let i = 2; taken.has(candidate); i++) candidate = `${type}${i}`
|
||||
return candidate
|
||||
@@ -323,34 +314,10 @@ export function DashboardEditor({
|
||||
// a save in flight is still "no unpublished changes" until it lands.
|
||||
const hasDraft = Boolean(dashboard.has_draft)
|
||||
const columns = columnsOf(draft)
|
||||
const page = pagesOf(draft)[0]
|
||||
// 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 widgets = widgetsOf(draft)
|
||||
|
||||
const updateWidgets = (next: WidgetDef[]) => {
|
||||
if (!page) return
|
||||
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,
|
||||
},
|
||||
],
|
||||
},
|
||||
),
|
||||
})
|
||||
commit({ ...draft, widgets: next })
|
||||
}
|
||||
|
||||
const addWidget = (type: WidgetKind) => {
|
||||
@@ -468,91 +435,91 @@ export function DashboardEditor({
|
||||
/>
|
||||
)
|
||||
|
||||
const body = !page ? (
|
||||
<p className="text-sm text-muted-foreground">
|
||||
This dashboard has no pages yet.
|
||||
</p>
|
||||
) : edit && widgets.length === 0 ? (
|
||||
<p className="text-sm text-muted-foreground" data-testid="dashboard-empty">
|
||||
Nothing on this page yet. Add a widget from the bar below.
|
||||
</p>
|
||||
) : stacked ? (
|
||||
// One column at the viewport's width. Edit mode still picks a widget and
|
||||
// opens its settings; only the arrangement is missing.
|
||||
<div
|
||||
data-look={root["data-look"]}
|
||||
data-touch={root["data-touch"]}
|
||||
data-palette={root["data-palette"]}
|
||||
className={cn(
|
||||
"h-full overflow-y-auto bg-background text-foreground",
|
||||
root.className,
|
||||
)}
|
||||
style={root.style}
|
||||
>
|
||||
<DashboardView
|
||||
const body =
|
||||
edit && widgets.length === 0 ? (
|
||||
<p
|
||||
className="text-sm text-muted-foreground"
|
||||
data-testid="dashboard-empty"
|
||||
>
|
||||
Nothing on this dashboard yet. Add a widget from the bar below.
|
||||
</p>
|
||||
) : stacked ? (
|
||||
// One column at the viewport's width. Edit mode still picks a widget and
|
||||
// opens its settings; only the arrangement is missing.
|
||||
<div
|
||||
data-look={root["data-look"]}
|
||||
data-touch={root["data-touch"]}
|
||||
data-palette={root["data-palette"]}
|
||||
className={cn(
|
||||
"h-full overflow-y-auto bg-background text-foreground",
|
||||
root.className,
|
||||
)}
|
||||
style={root.style}
|
||||
>
|
||||
<DashboardView
|
||||
dashboard={draft}
|
||||
stacked
|
||||
renderWidget={edit ? pickable : undefined}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<CanvasSurface
|
||||
dashboard={draft}
|
||||
stacked
|
||||
renderWidget={edit ? pickable : undefined}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<CanvasSurface
|
||||
dashboard={draft}
|
||||
dots={edit}
|
||||
rail={
|
||||
railDashboards ? (
|
||||
<PanelRail
|
||||
dashboards={railDashboards}
|
||||
current={draft.name}
|
||||
// Editing one dashboard of a panel and editing its neighbour are
|
||||
// the same job, so the rail keeps whichever mode this one is in.
|
||||
linkFor={(name) => ({
|
||||
to: "/dashboards/$name",
|
||||
params: { name },
|
||||
search: edit ? { edit: true } : {},
|
||||
})}
|
||||
/>
|
||||
) : null
|
||||
}
|
||||
>
|
||||
{(scale) =>
|
||||
!edit ? (
|
||||
<DashboardView dashboard={draft} rail={hasRail} />
|
||||
) : (
|
||||
<GridLayout
|
||||
width={area.width}
|
||||
layout={layout}
|
||||
onLayoutChange={applyLayout}
|
||||
positionStrategy={scaledStrategy(scale)}
|
||||
gridConfig={{
|
||||
cols: columns,
|
||||
rowHeight: rowHeightOf(draft, hasRail),
|
||||
margin: [GRID_GAP, GRID_GAP],
|
||||
containerPadding: [0, 0],
|
||||
// The canvas is the constraint: nothing may be dragged off the
|
||||
// panel it is being drawn for. Shared with the warning above, so
|
||||
// the constraint and what it is warned about cannot disagree.
|
||||
maxRows: rows,
|
||||
}}
|
||||
// Only the header moves a widget, so a slider under the cursor still
|
||||
// slides and a switch still flips while the dashboard is being edited.
|
||||
// The header is the handle, and a chart now draws its range
|
||||
// picker there — so that one control is exempt, or picking a
|
||||
// window would drag the tile instead.
|
||||
dragConfig={{
|
||||
handle: ".widget-grip",
|
||||
cancel: "[data-testid=range-picker]",
|
||||
}}
|
||||
resizeConfig={{ handles: ["e", "s", "se"] }}
|
||||
>
|
||||
{widgets.map((widget) => (
|
||||
<div key={widget.id}>{pickable(widget, true)}</div>
|
||||
))}
|
||||
</GridLayout>
|
||||
)
|
||||
}
|
||||
</CanvasSurface>
|
||||
)
|
||||
dots={edit}
|
||||
rail={
|
||||
railDashboards ? (
|
||||
<PanelRail
|
||||
dashboards={railDashboards}
|
||||
current={draft.name}
|
||||
// Editing one dashboard of a panel and editing its neighbour are
|
||||
// the same job, so the rail keeps whichever mode this one is in.
|
||||
linkFor={(name) => ({
|
||||
to: "/dashboards/$name",
|
||||
params: { name },
|
||||
search: edit ? { edit: true } : {},
|
||||
})}
|
||||
/>
|
||||
) : null
|
||||
}
|
||||
>
|
||||
{(scale) =>
|
||||
!edit ? (
|
||||
<DashboardView dashboard={draft} rail={hasRail} />
|
||||
) : (
|
||||
<GridLayout
|
||||
width={area.width}
|
||||
layout={layout}
|
||||
onLayoutChange={applyLayout}
|
||||
positionStrategy={scaledStrategy(scale)}
|
||||
gridConfig={{
|
||||
cols: columns,
|
||||
rowHeight: rowHeightOf(draft, hasRail),
|
||||
margin: [GRID_GAP, GRID_GAP],
|
||||
containerPadding: [0, 0],
|
||||
// The canvas is the constraint: nothing may be dragged off the
|
||||
// panel it is being drawn for. Shared with the warning above, so
|
||||
// the constraint and what it is warned about cannot disagree.
|
||||
maxRows: rows,
|
||||
}}
|
||||
// Only the header moves a widget, so a slider under the cursor still
|
||||
// slides and a switch still flips while the dashboard is being edited.
|
||||
// The header is the handle, and a chart now draws its range
|
||||
// picker there — so that one control is exempt, or picking a
|
||||
// window would drag the tile instead.
|
||||
dragConfig={{
|
||||
handle: ".widget-grip",
|
||||
cancel: "[data-testid=range-picker]",
|
||||
}}
|
||||
resizeConfig={{ handles: ["e", "s", "se"] }}
|
||||
>
|
||||
{widgets.map((widget) => (
|
||||
<div key={widget.id}>{pickable(widget, true)}</div>
|
||||
))}
|
||||
</GridLayout>
|
||||
)
|
||||
}
|
||||
</CanvasSurface>
|
||||
)
|
||||
|
||||
return (
|
||||
<LookProvider dashboard={draft}>
|
||||
|
||||
@@ -1,12 +1,6 @@
|
||||
import { motion } from "motion/react"
|
||||
import { useEffect, useRef, useState } from "react"
|
||||
import type {
|
||||
DashboardDef_Output,
|
||||
PageDef_Output,
|
||||
Placement,
|
||||
SectionDef_Output,
|
||||
WidgetDef,
|
||||
} from "@/client"
|
||||
import type { DashboardDef_Output, Placement, WidgetDef } from "@/client"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import "./dashboard.css"
|
||||
@@ -68,9 +62,8 @@ export const RAIL_INSET = 72
|
||||
* The generated client marks every list optional, because the server fills
|
||||
* them in. These three keep that from spreading through the components.
|
||||
*/
|
||||
export const pagesOf = (dashboard: DashboardDef_Output) => dashboard.pages ?? []
|
||||
export const sectionsOf = (page: PageDef_Output) => page.sections ?? []
|
||||
export const widgetsOf = (section: SectionDef_Output) => section.widgets ?? []
|
||||
export const widgetsOf = (dashboard: DashboardDef_Output) =>
|
||||
dashboard.widgets ?? []
|
||||
|
||||
export const columnsOf = (dashboard: Dashboard) =>
|
||||
dashboard.columns || DEFAULT_COLUMNS
|
||||
@@ -293,52 +286,6 @@ export const isPlaced = (widgets: WidgetDef[]) =>
|
||||
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.
|
||||
*
|
||||
@@ -360,26 +307,14 @@ export function DashboardView({
|
||||
/** Whether the panel carries a rail, which takes a column of the canvas. */
|
||||
rail?: boolean
|
||||
}) {
|
||||
// ponytail: the first page only. A panel carries several whole dashboards
|
||||
// — 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)
|
||||
const all = widgetsOf(dashboard)
|
||||
if (all.length === 0) {
|
||||
return (
|
||||
<p
|
||||
className="text-sm text-muted-foreground"
|
||||
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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
runQueryOptions,
|
||||
shortId,
|
||||
} from "@/components/Runs/queries"
|
||||
import { type Dashboard, flatWidgets, pagesOf } from "./DashboardView"
|
||||
import { type Dashboard, widgetsOf } from "./DashboardView"
|
||||
import {
|
||||
type ContextLine,
|
||||
type DataContext,
|
||||
@@ -22,10 +22,9 @@ import {
|
||||
* asking the server for its whole series would be a query nobody draws.
|
||||
*/
|
||||
function chartedNames(dashboard: Dashboard | undefined): string[] {
|
||||
const page = pagesOf(dashboard ?? ({} as Dashboard))[0]
|
||||
if (!page) return []
|
||||
if (!dashboard) return []
|
||||
const names = new Set<string>()
|
||||
for (const widget of flatWidgets(page)) {
|
||||
for (const widget of widgetsOf(dashboard)) {
|
||||
if (widget.type !== "chart") continue
|
||||
const config = (widget.config ?? {}) as {
|
||||
series?: { message?: string }[]
|
||||
|
||||
@@ -217,8 +217,6 @@ function Dashboards() {
|
||||
draft={dashboard.has_draft}
|
||||
detail={`${dashboard.widget_count} widget${
|
||||
dashboard.widget_count === 1 ? "" : "s"
|
||||
} · ${dashboard.page_count} page${
|
||||
dashboard.page_count === 1 ? "" : "s"
|
||||
}`}
|
||||
selecting={selection.selecting}
|
||||
selected={selection.selected.includes(dashboard.name)}
|
||||
|
||||
Reference in New Issue
Block a user