diff --git a/client/app/components/dashboards/dashboard-grid.less b/client/app/components/dashboards/dashboard-grid.less index 1f5d0b39bf..62467d639a 100644 --- a/client/app/components/dashboards/dashboard-grid.less +++ b/client/app/components/dashboards/dashboard-grid.less @@ -51,7 +51,7 @@ right: 0; background: linear-gradient(to bottom, transparent, transparent 2px, #f6f8f9 2px, #f6f8f9 5px), linear-gradient(to left, #b3babf, #b3babf 1px, transparent 1px, transparent); - background-size: calc((100% + 15px) / 6) 5px; + background-size: calc((100% + 15px) / 12) 5px; background-position: -7px 1px; } } diff --git a/client/app/config/dashboard-grid-options.js b/client/app/config/dashboard-grid-options.js index a07c5691bb..d9bbbab0d5 100644 --- a/client/app/config/dashboard-grid-options.js +++ b/client/app/config/dashboard-grid-options.js @@ -1,13 +1,13 @@ export default { - columns: 6, // grid columns count + columns: 12, // grid columns count rowHeight: 50, // grid row height (incl. bottom padding) margins: 15, // widget margins mobileBreakPoint: 800, // defaults for widgets defaultSizeX: 3, defaultSizeY: 3, - minSizeX: 1, - maxSizeX: 6, - minSizeY: 1, + minSizeX: 2, + maxSizeX: 12, + minSizeY: 2, maxSizeY: 1000, }; diff --git a/client/cypress/integration/dashboard/dashboard_spec.js b/client/cypress/integration/dashboard/dashboard_spec.js index 7d5fff9db3..9ff55a15af 100644 --- a/client/cypress/integration/dashboard/dashboard_spec.js +++ b/client/cypress/integration/dashboard/dashboard_spec.js @@ -23,7 +23,7 @@ describe("Dashboard", () => { cy.getByTestId("DashboardSaveButton").click(); }); - cy.wait("@NewDashboard").then(xhr => { + cy.wait("@NewDashboard").then((xhr) => { const id = Cypress._.get(xhr, "response.body.id"); assert.isDefined(id, "Dashboard api call returns id"); @@ -40,13 +40,9 @@ describe("Dashboard", () => { cy.getByTestId("DashboardMoreButton").click(); - cy.getByTestId("DashboardMoreButtonMenu") - .contains("Archive") - .click(); + cy.getByTestId("DashboardMoreButtonMenu").contains("Archive").click(); - cy.get(".ant-modal .ant-btn") - .contains("Archive") - .click({ force: true }); + cy.get(".ant-modal .ant-btn").contains("Archive").click({ force: true }); cy.get(".label-tag-archived").should("exist"); cy.visit("/dashboards"); @@ -60,7 +56,7 @@ describe("Dashboard", () => { cy.server(); cy.route("GET", "**/api/dashboards/*").as("LoadDashboard"); cy.createDashboard("Dashboard multiple urls").then(({ id, slug }) => { - [`/dashboards/${id}`, `/dashboards/${id}-anything-here`, `/dashboard/${slug}`].forEach(url => { + [`/dashboards/${id}`, `/dashboards/${id}-anything-here`, `/dashboard/${slug}`].forEach((url) => { cy.visit(url); cy.wait("@LoadDashboard"); cy.getByTestId(`DashboardId${id}Container`).should("exist"); @@ -72,7 +68,7 @@ describe("Dashboard", () => { }); context("viewport width is at 800px", () => { - before(function() { + before(function () { cy.login(); cy.createDashboard("Foo Bar") .then(({ id }) => { @@ -80,49 +76,42 @@ describe("Dashboard", () => { this.dashboardEditUrl = `/dashboards/${id}?edit`; return cy.addTextbox(id, "Hello World!").then(getWidgetTestId); }) - .then(elTestId => { + .then((elTestId) => { cy.visit(this.dashboardUrl); cy.getByTestId(elTestId).as("textboxEl"); }); }); - beforeEach(function() { + beforeEach(function () { cy.login(); cy.visit(this.dashboardUrl); cy.viewport(800 + menuWidth, 800); }); it("shows widgets with full width", () => { - cy.get("@textboxEl").should($el => { + cy.get("@textboxEl").should(($el) => { expect($el.width()).to.eq(770); }); cy.viewport(801 + menuWidth, 800); - cy.get("@textboxEl").should($el => { - expect($el.width()).to.eq(378); + cy.get("@textboxEl").should(($el) => { + expect($el.width()).to.eq(182); }); }); it("hides edit option", () => { - cy.getByTestId("DashboardMoreButton") - .click() - .should("be.visible"); + cy.getByTestId("DashboardMoreButton").click().should("be.visible"); - cy.getByTestId("DashboardMoreButtonMenu") - .contains("Edit") - .as("editButton") - .should("not.be.visible"); + cy.getByTestId("DashboardMoreButtonMenu").contains("Edit").as("editButton").should("not.be.visible"); cy.viewport(801 + menuWidth, 800); cy.get("@editButton").should("be.visible"); }); - it("disables edit mode", function() { + it("disables edit mode", function () { cy.viewport(801 + menuWidth, 800); cy.visit(this.dashboardEditUrl); - cy.contains("button", "Done Editing") - .as("saveButton") - .should("exist"); + cy.contains("button", "Done Editing").as("saveButton").should("exist"); cy.viewport(800 + menuWidth, 800); cy.contains("button", "Done Editing").should("not.exist"); @@ -130,14 +119,14 @@ describe("Dashboard", () => { }); context("viewport width is at 767px", () => { - before(function() { + before(function () { cy.login(); cy.createDashboard("Foo Bar").then(({ id }) => { this.dashboardUrl = `/dashboards/${id}`; }); }); - beforeEach(function() { + beforeEach(function () { cy.visit(this.dashboardUrl); cy.viewport(767, 800); }); diff --git a/client/cypress/integration/dashboard/grid_compliant_widgets_spec.js b/client/cypress/integration/dashboard/grid_compliant_widgets_spec.js index 19883a0177..be497b8dc2 100644 --- a/client/cypress/integration/dashboard/grid_compliant_widgets_spec.js +++ b/client/cypress/integration/dashboard/grid_compliant_widgets_spec.js @@ -5,7 +5,7 @@ import { getWidgetTestId, editDashboard, resizeBy } from "../../support/dashboar const menuWidth = 80; describe("Grid compliant widgets", () => { - beforeEach(function() { + beforeEach(function () { cy.login(); cy.viewport(1215 + menuWidth, 800); cy.createDashboard("Foo Bar") @@ -13,7 +13,7 @@ describe("Grid compliant widgets", () => { this.dashboardUrl = `/dashboards/${id}`; return cy.addTextbox(id, "Hello World!").then(getWidgetTestId); }) - .then(elTestId => { + .then((elTestId) => { cy.visit(this.dashboardUrl); cy.getByTestId(elTestId).as("textboxEl"); }); @@ -27,7 +27,7 @@ describe("Grid compliant widgets", () => { it("stays put when dragged under snap threshold", () => { cy.get("@textboxEl") - .dragBy(90) + .dragBy(30) .invoke("offset") .should("have.property", "left", 15 + menuWidth); // no change, 15 -> 15 }); @@ -36,14 +36,14 @@ describe("Grid compliant widgets", () => { cy.get("@textboxEl") .dragBy(110) .invoke("offset") - .should("have.property", "left", 215 + menuWidth); // moved by 200, 15 -> 215 + .should("have.property", "left", 115 + menuWidth); // moved by 100, 15 -> 115 }); it("moves two columns when dragged over snap threshold", () => { cy.get("@textboxEl") - .dragBy(330) + .dragBy(200) .invoke("offset") - .should("have.property", "left", 415 + menuWidth); // moved by 400, 15 -> 415 + .should("have.property", "left", 215 + menuWidth); // moved by 200, 15 -> 215 }); }); @@ -52,7 +52,7 @@ describe("Grid compliant widgets", () => { cy.route("POST", "**/api/widgets/*").as("WidgetSave"); editDashboard(); - cy.get("@textboxEl").dragBy(330); + cy.get("@textboxEl").dragBy(100); cy.wait("@WidgetSave"); }); }); @@ -64,24 +64,24 @@ describe("Grid compliant widgets", () => { }); it("stays put when dragged under snap threshold", () => { - resizeBy(cy.get("@textboxEl"), 90) + resizeBy(cy.get("@textboxEl"), 30) .then(() => cy.get("@textboxEl")) .invoke("width") - .should("eq", 585); // no change, 585 -> 585 + .should("eq", 285); // no change, 285 -> 285 }); it("moves one column when dragged over snap threshold", () => { resizeBy(cy.get("@textboxEl"), 110) .then(() => cy.get("@textboxEl")) .invoke("width") - .should("eq", 785); // resized by 200, 585 -> 785 + .should("eq", 385); // resized by 200, 185 -> 385 }); it("moves two columns when dragged over snap threshold", () => { resizeBy(cy.get("@textboxEl"), 400) .then(() => cy.get("@textboxEl")) .invoke("width") - .should("eq", 985); // resized by 400, 585 -> 985 + .should("eq", 685); // resized by 400, 285 -> 685 }); }); @@ -101,16 +101,16 @@ describe("Grid compliant widgets", () => { resizeBy(cy.get("@textboxEl"), 0, 30) .then(() => cy.get("@textboxEl")) .invoke("height") - .should("eq", 185); // resized by 50, , 135 -> 185 + .should("eq", 185); }); it("shrinks to minimum", () => { cy.get("@textboxEl") - .then($el => resizeBy(cy.get("@textboxEl"), -$el.width(), -$el.height())) // resize to 0,0 + .then(($el) => resizeBy(cy.get("@textboxEl"), -$el.width(), -$el.height())) // resize to 0,0 .then(() => cy.get("@textboxEl")) - .should($el => { + .should(($el) => { expect($el.width()).to.eq(185); // min textbox width - expect($el.height()).to.eq(35); // min textbox height + expect($el.height()).to.eq(85); // min textbox height }); }); }); diff --git a/client/cypress/integration/dashboard/textbox_spec.js b/client/cypress/integration/dashboard/textbox_spec.js index 669e73e912..006eeff4e6 100644 --- a/client/cypress/integration/dashboard/textbox_spec.js +++ b/client/cypress/integration/dashboard/textbox_spec.js @@ -3,7 +3,7 @@ import { getWidgetTestId, editDashboard } from "../../support/dashboard"; describe("Textbox", () => { - beforeEach(function() { + beforeEach(function () { cy.login(); cy.createDashboard("Foo Bar").then(({ id }) => { this.dashboardId = id; @@ -12,12 +12,10 @@ describe("Textbox", () => { }); const confirmDeletionInModal = () => { - cy.get(".ant-modal .ant-btn") - .contains("Delete") - .click({ force: true }); + cy.get(".ant-modal .ant-btn").contains("Delete").click({ force: true }); }; - it("adds textbox", function() { + it("adds textbox", function () { cy.visit(this.dashboardUrl); editDashboard(); cy.getByTestId("AddTextboxButton").click(); @@ -29,10 +27,10 @@ describe("Textbox", () => { cy.get(".widget-text").should("exist"); }); - it("removes textbox by X button", function() { + it("removes textbox by X button", function () { cy.addTextbox(this.dashboardId, "Hello World!") .then(getWidgetTestId) - .then(elTestId => { + .then((elTestId) => { cy.visit(this.dashboardUrl); editDashboard(); @@ -45,32 +43,30 @@ describe("Textbox", () => { }); }); - it("removes textbox by menu", function() { + it("removes textbox by menu", function () { cy.addTextbox(this.dashboardId, "Hello World!") .then(getWidgetTestId) - .then(elTestId => { + .then((elTestId) => { cy.visit(this.dashboardUrl); cy.getByTestId(elTestId).within(() => { cy.getByTestId("WidgetDropdownButton").click(); }); - cy.getByTestId("WidgetDropdownButtonMenu") - .contains("Remove from Dashboard") - .click(); + cy.getByTestId("WidgetDropdownButtonMenu").contains("Remove from Dashboard").click(); confirmDeletionInModal(); cy.getByTestId(elTestId).should("not.exist"); }); }); - it("allows opening menu after removal", function() { + it("allows opening menu after removal", function () { let elTestId1; cy.addTextbox(this.dashboardId, "txb 1") .then(getWidgetTestId) - .then(elTestId => { + .then((elTestId) => { elTestId1 = elTestId; return cy.addTextbox(this.dashboardId, "txb 2").then(getWidgetTestId); }) - .then(elTestId2 => { + .then((elTestId2) => { cy.visit(this.dashboardUrl); editDashboard(); @@ -97,10 +93,10 @@ describe("Textbox", () => { }); }); - it("edits textbox", function() { + it("edits textbox", function () { cy.addTextbox(this.dashboardId, "Hello World!") .then(getWidgetTestId) - .then(elTestId => { + .then((elTestId) => { cy.visit(this.dashboardUrl); cy.getByTestId(elTestId) .as("textboxEl") @@ -108,17 +104,13 @@ describe("Textbox", () => { cy.getByTestId("WidgetDropdownButton").click(); }); - cy.getByTestId("WidgetDropdownButtonMenu") - .contains("Edit") - .click(); + cy.getByTestId("WidgetDropdownButtonMenu").contains("Edit").click(); const newContent = "[edited]"; cy.getByTestId("TextboxDialog") .should("exist") .within(() => { - cy.get("textarea") - .clear() - .type(newContent); + cy.get("textarea").clear().type(newContent); cy.contains("button", "Save").click(); }); @@ -126,7 +118,7 @@ describe("Textbox", () => { }); }); - it("renders textbox according to position configuration", function() { + it("renders textbox according to position configuration", function () { const id = this.dashboardId; const txb1Pos = { col: 0, row: 0, sizeX: 3, sizeY: 2 }; const txb2Pos = { col: 1, row: 1, sizeX: 3, sizeY: 4 }; @@ -135,15 +127,15 @@ describe("Textbox", () => { cy.addTextbox(id, "x", { position: txb1Pos }) .then(() => cy.addTextbox(id, "x", { position: txb2Pos })) .then(getWidgetTestId) - .then(elTestId => { + .then((elTestId) => { cy.visit(this.dashboardUrl); return cy.getByTestId(elTestId); }) - .should($el => { + .should(($el) => { const { top, left } = $el.offset(); expect(top).to.be.oneOf([162, 162.015625]); - expect(left).to.eq(282); - expect($el.width()).to.eq(545); + expect(left).to.eq(188); + expect($el.width()).to.eq(265); expect($el.height()).to.eq(185); }); }); diff --git a/migrations/versions/db0aca1ebd32_12_column_dashboard_layout.py b/migrations/versions/db0aca1ebd32_12_column_dashboard_layout.py new file mode 100644 index 0000000000..c8557b5672 --- /dev/null +++ b/migrations/versions/db0aca1ebd32_12_column_dashboard_layout.py @@ -0,0 +1,34 @@ +"""12-column dashboard layout + +Revision ID: db0aca1ebd32 +Revises: 9e8c841d1a30 +Create Date: 2025-03-31 13:45:43.160893 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = 'db0aca1ebd32' +down_revision = '9e8c841d1a30' +branch_labels = None +depends_on = None + + +def upgrade(): + op.execute(""" + UPDATE widgets + SET options = jsonb_set(options, '{position,col}', to_json((options->'position'->>'col')::int * 2)::jsonb); + UPDATE widgets + SET options = jsonb_set(options, '{position,sizeX}', to_json((options->'position'->>'sizeX')::int * 2)::jsonb); + """) + + +def downgrade(): + op.execute(""" + UPDATE widgets + SET options = jsonb_set(options, '{position,col}', to_json((options->'position'->>'col')::int / 2)::jsonb); + UPDATE widgets + SET options = jsonb_set(options, '{position,sizeX}', to_json((options->'position'->>'sizeX')::int / 2)::jsonb); + """)