rework live dashboard grid layout and add natural mode

This commit is contained in:
Josh Hawkins
2026-09-24 13:26:57 -05:00
parent 40f8ba1f7f
commit e760868048
19 changed files with 1375 additions and 253 deletions
@@ -13,7 +13,26 @@ import type { Page } from "@playwright/test";
const OUTDOOR_LAYOUT_KEY = "outdoor-draggable-layout:admin";
const STREAMING_KEY = "streaming-settings:admin";
const OUTDOOR_LAYOUT = [
// the shape DraggableGridLayout writes
const OUTDOOR_LAYOUT = {
version: 2,
naturalAspect: false,
layout: [
{ i: "front_door", x: 0, y: 0, w: 32, h: 72 },
{ i: "backyard", x: 32, y: 0, w: 32, h: 72 },
],
};
const NATURAL_OUTDOOR_LAYOUT = {
version: 2,
naturalAspect: true,
layout: [
{ i: "front_door", x: 0, y: 0, w: 32, h: 72 },
{ i: "backyard", x: 32, y: 0, w: 24, h: 96 },
],
};
const LEGACY_OUTDOOR_LAYOUT = [
{ i: "front_door", x: 0, y: 0, w: 6, h: 4 },
{ i: "backyard", x: 6, y: 0, w: 6, h: 4 },
];
@@ -159,6 +178,111 @@ test.describe("UI settings import/export @medium", () => {
expect(payload.sections.preferences.playbackRate).toBe(2);
});
test("round-trips a layout left unconverted by an upgrade", async ({
frigateApp,
}) => {
// DraggableGridLayout rewrites a pre-0.19 layout only when that group's
// dashboard is opened, so exporting first carries the bare array into the
// file. Import must accept it back rather than rejecting the whole file.
await frigateApp.goto("/settings?page=uiSettings");
await writeIdb(frigateApp.page, {
[OUTDOOR_LAYOUT_KEY]: LEGACY_OUTDOOR_LAYOUT,
"playbackRate:admin": 2,
});
const downloadPromise = frigateApp.page.waitForEvent("download");
await frigateApp.page
.getByRole("button", { name: "Export Settings" })
.click();
const download = await downloadPromise;
const contents = readFileSync((await download.path())!, "utf-8");
expect(JSON.parse(contents).sections.layouts.outdoor).toEqual(
LEGACY_OUTDOOR_LAYOUT,
);
await clearIdb(frigateApp.page);
await chooseImportText(frigateApp.page, contents);
await confirmImport(frigateApp.page);
expect(await readIdb(frigateApp.page, OUTDOOR_LAYOUT_KEY)).toEqual(
LEGACY_OUTDOOR_LAYOUT,
);
// the rest of the file must survive alongside it
expect(await readIdb(frigateApp.page, "playbackRate:admin")).toBe(2);
});
test("legacy layouts import turns natural aspect off so they display", async ({
frigateApp,
}) => {
// Bare-array layouts only render in bucketed mode; with natural aspect on
// they would be discarded and regenerated on the next dashboard visit. The
// import applies the mode the layouts were built for, and the file's own
// naturalAspectLayout preference must not override that.
await frigateApp.goto("/settings?page=uiSettings");
await writeIdb(frigateApp.page, { "naturalAspectLayout:admin": true });
await chooseImportFile(
frigateApp.page,
importPayload({
sections: {
layouts: { outdoor: LEGACY_OUTDOOR_LAYOUT },
streaming: {},
preferences: { naturalAspectLayout: true },
},
}),
);
const note = frigateApp.page.getByText(/standard tile sizing/);
await expect(note).toBeVisible();
// the note is about the layouts section, so it follows its switch
await frigateApp.page.getByText("Camera group layouts (1 group)").click();
await expect(note).toBeHidden();
await frigateApp.page.getByText("Camera group layouts (1 group)").click();
await expect(note).toBeVisible();
await confirmImport(frigateApp.page);
expect(await readIdb(frigateApp.page, OUTDOOR_LAYOUT_KEY)).toEqual(
LEGACY_OUTDOOR_LAYOUT,
);
expect(await readIdb(frigateApp.page, "naturalAspectLayout:admin")).toBe(
false,
);
});
test("natural aspect layouts import turns the setting on", async ({
frigateApp,
}) => {
await frigateApp.goto("/settings?page=uiSettings");
await chooseImportFile(
frigateApp.page,
importPayload({
sections: {
layouts: { outdoor: NATURAL_OUTDOOR_LAYOUT },
streaming: {},
preferences: {},
},
}),
);
await expect(
frigateApp.page.getByText(/camera aspect ratio tile sizing/),
).toBeVisible();
await confirmImport(frigateApp.page);
expect(await readIdb(frigateApp.page, OUTDOOR_LAYOUT_KEY)).toEqual(
NATURAL_OUTDOOR_LAYOUT,
);
expect(await readIdb(frigateApp.page, "naturalAspectLayout:admin")).toBe(
true,
);
});
test("omits settings that were never stored", async ({ frigateApp }) => {
await frigateApp.goto("/settings?page=uiSettings");