diff --git a/frontend/src/components/Dashboard/DashboardEditor.tsx b/frontend/src/components/Dashboard/DashboardEditor.tsx
index 97687cb..72904c7 100644
--- a/frontend/src/components/Dashboard/DashboardEditor.tsx
+++ b/frontend/src/components/Dashboard/DashboardEditor.tsx
@@ -461,6 +461,7 @@ export function DashboardEditor({
diff --git a/frontend/src/components/Dashboard/DashboardView.tsx b/frontend/src/components/Dashboard/DashboardView.tsx
index c147230..0990784 100644
--- a/frontend/src/components/Dashboard/DashboardView.tsx
+++ b/frontend/src/components/Dashboard/DashboardView.tsx
@@ -306,6 +306,7 @@ export function DashboardView({
renderWidget,
stacked,
rail,
+ editing,
}: {
dashboard: Dashboard
renderWidget?: (widget: WidgetDef) => React.ReactNode
@@ -313,6 +314,9 @@ export function DashboardView({
stacked?: boolean
/** Whether the panel carries a rail, which takes a column of the canvas. */
rail?: boolean
+ /** Being arranged rather than used, so the lock is not mounted — the
+ * stacked editor draws through here, where the canvas draws its own grid. */
+ editing?: boolean
}) {
const all = widgetsOf(dashboard)
if (all.length === 0) {
@@ -337,20 +341,26 @@ export function DashboardView({
: all
const columns = columnsOf(dashboard)
+ const grid = (
+
+
+
+ )
+
return (
-
-
-
-
-
+ {editing ? (
+ grid
+ ) : (
+ {grid}
+ )}
)
}
diff --git a/frontend/tests/mobile.spec.ts b/frontend/tests/mobile.spec.ts
index 78c0011..90921a9 100644
--- a/frontend/tests/mobile.spec.ts
+++ b/frontend/tests/mobile.spec.ts
@@ -15,6 +15,8 @@ const dashboardName = `${flowName}_panel`
/** What the bar and the forecast read. A flow of its own, so the graph the
* editor lays out above stays the shape those assertions were written for. */
const feedName = `${flowName}_feed`
+/** A read-only dashboard, on its own so the width checks above keep a live one. */
+const lockedName = `${flowName}_locked`
test.describe.configure({ mode: "serial" })
@@ -129,6 +131,7 @@ test.beforeAll(async ({ browser }) => {
// this whole name in the chart's legend.
{ name: "climate_series_reading", dtype: "float" },
{ name: "mode", dtype: "str" },
+ { name: "lamp", dtype: "bool" },
],
},
],
@@ -269,12 +272,37 @@ test.beforeAll(async ({ browser }) => {
method: "POST",
data: { version: draft.version },
})
+
+ const locked = await (
+ await api(page, `/dashboards/${lockedName}`, { method: "POST" })
+ ).json()
+ locked.settings = { ...(locked.settings ?? {}), locked: { value: true } }
+ locked.widgets = [
+ {
+ id: "lamp",
+ type: "switch",
+ title: "Lamp",
+ layout: { lg: { x: 0, y: 0, w: 3, h: 2 } },
+ config: { target: `${feedName}.lamp`, dtype: "bool", style: "button" },
+ },
+ ]
+ const lockedDraft = await (
+ await api(page, `/dashboards/${lockedName}`, {
+ method: "PUT",
+ data: locked,
+ })
+ ).json()
+ await api(page, `/dashboards/${lockedName}/publish`, {
+ method: "POST",
+ data: { version: lockedDraft.version },
+ })
await page.close()
})
test.afterAll(async ({ browser }) => {
await deleteAll(browser, [
`/dashboards/${dashboardName}`,
+ `/dashboards/${lockedName}`,
`/flows/${flowName}`,
`/flows/${feedName}`,
])
@@ -360,6 +388,27 @@ test("a dashboard stacks instead of shrinking", async ({ page }) => {
expect(second!.y).toBeGreaterThanOrEqual(first!.y + first!.height - 1)
})
+/**
+ * A read-only dashboard, read and arranged at a phone's width.
+ *
+ * Stacked, both modes draw through `DashboardView` — where the canvas hands
+ * editing to its own grid instead — so the lock has to follow the mode rather
+ * than the page. Arranging a dashboard is not using it.
+ */
+test("a locked dashboard locks when read, not while arranged", async ({
+ page,
+}) => {
+ const lamp = page.getByRole("button", { name: "Lamp" })
+
+ await page.goto(`/dashboards/${lockedName}`)
+ await lamp.waitFor({ timeout: 15000 })
+ await expect(lamp, "a locked dashboard being read").toBeDisabled()
+
+ await page.goto(`/dashboards/${lockedName}?edit=true`)
+ await lamp.waitFor({ timeout: 15000 })
+ await expect(lamp, "a locked dashboard being arranged").toBeEnabled()
+})
+
test("the panel view fits the viewport", async ({ page }) => {
await page.goto(`/view/${dashboardName}`)
await page.waitForSelector("[data-testid=widget-frame]", { timeout: 15000 })