diff --git a/client/app/components/dashboards/DashboardGrid.jsx b/client/app/components/dashboards/DashboardGrid.jsx index 6a7d51703f..13db3c3978 100644 --- a/client/app/components/dashboards/DashboardGrid.jsx +++ b/client/app/components/dashboards/DashboardGrid.jsx @@ -5,7 +5,7 @@ import cx from "classnames"; import { Responsive, WidthProvider } from "react-grid-layout"; import { VisualizationWidget, TextboxWidget, RestrictedWidget } from "@/components/dashboards/dashboard-widget"; import { FiltersType } from "@/components/Filters"; -import cfg from "@/config/dashboard-grid-options"; +import cfg, { dashboard12ColGridOptions as cfg12 } from "@/config/dashboard-grid-options"; import AutoHeightController from "./AutoHeightController"; import { WidgetTypeEnum } from "@/services/widget"; @@ -132,9 +132,11 @@ class DashboardGrid extends React.Component { constructor(props) { super(props); + const currentCfg = this.props.dashboard.use_12_column_layout ? cfg12 : cfg; this.state = { layouts: {}, disableAnimations: true, + cfg: currentCfg, }; // init AutoHeightController @@ -143,6 +145,7 @@ class DashboardGrid extends React.Component { } componentDidMount() { + this.onColumnCountChange(this.props.dashboard.use_12_column_layout); this.onBreakpointChange(document.body.offsetWidth <= cfg.mobileBreakPoint ? SINGLE : MULTI); // Work-around to disable initial animation on widgets; `measureBeforeMount` doesn't work properly: // it disables animation, but it cannot detect scrollbars. @@ -151,9 +154,26 @@ class DashboardGrid extends React.Component { }, 50); } - componentDidUpdate() { + onColumnCountChange(use12) { + const editGrid = document.getElementsByClassName("dashboard-wrapper")[0]; + + if (editGrid) { + // use CSS attribute to change background grid while editing dashboard + const columnsCountAttribute = document.createAttribute("columns-count"); + columnsCountAttribute.value = use12 ? 12 : 6; + editGrid.attributes.setNamedItem(columnsCountAttribute); + this.setState({ cfg: use12 ? cfg12 : cfg }); + } + } + + componentDidUpdate(prevProps) { // update, in case widgets added or removed this.autoHeightCtrl.update(this.props.widgets); + + // listen to changes of columns layout + if (prevProps.dashboard.use_12_column_layout !== this.props.dashboard.use_12_column_layout) { + this.onColumnCountChange(this.props.dashboard.use_12_column_layout); + } } componentWillUnmount() { @@ -239,7 +259,7 @@ class DashboardGrid extends React.Component {
updateDashboard({ dashboard_filters_enabled: target.checked })}> Use Dashboard Level Filters + updateDashboard({ use_12_column_layout: target.checked })}> + Use 12-column layout +
); } diff --git a/client/app/pages/dashboards/hooks/useDashboard.js b/client/app/pages/dashboards/hooks/useDashboard.js index ff129ea143..f96d6ae42d 100644 --- a/client/app/pages/dashboards/hooks/useDashboard.js +++ b/client/app/pages/dashboards/hooks/useDashboard.js @@ -199,7 +199,7 @@ function useDashboard(dashboardData) { // reload dashboard when filter option changes useEffect(() => { loadDashboard(); - }, [dashboard.dashboard_filters_enabled]); // eslint-disable-line react-hooks/exhaustive-deps + }, [dashboard.dashboard_filters_enabled, dashboard.use_12_column_layout]); // eslint-disable-line react-hooks/exhaustive-deps return { dashboard, diff --git a/client/app/services/dashboard.js b/client/app/services/dashboard.js index f347048ca0..d36f0b714e 100644 --- a/client/app/services/dashboard.js +++ b/client/app/services/dashboard.js @@ -1,6 +1,6 @@ import _ from "lodash"; import { axios } from "@/services/axios"; -import dashboardGridOptions from "@/config/dashboard-grid-options"; +import dashboardGridOptions, { dashboard12ColGridOptions } from "@/config/dashboard-grid-options"; import Widget from "./widget"; import { currentUser } from "@/services/auth"; import location from "@/services/location"; @@ -78,8 +78,9 @@ function prepareWidgetsForDashboard(widgets) { return widgets; } -function calculateNewWidgetPosition(existingWidgets, newWidget) { - const width = _.extend({ sizeX: dashboardGridOptions.defaultSizeX }, _.extend({}, newWidget.options).position).sizeX; +function calculateNewWidgetPosition(existingWidgets, newWidget, use12Cols) { + const gridOptions = use12Cols ? dashboard12ColGridOptions : dashboardGridOptions; + const width = _.extend({ sizeX: gridOptions.defaultSizeX }, _.extend({}, newWidget.options).position).sizeX; // Find first free row for each column const bottomLine = _.chain(existingWidgets) @@ -102,13 +103,13 @@ function calculateNewWidgetPosition(existingWidgets, newWidget) { result[i] = Math.max(result[i], item.bottom); } return result; - }, _.map(new Array(dashboardGridOptions.columns), _.constant(0))) + }, _.map(new Array(gridOptions.columns), _.constant(0))) .value(); // Go through columns, pick them by count necessary to hold new block, // and calculate bottom-most free row per group. // Choose group with the top-most free row (comparing to other groups) - return _.chain(_.range(0, dashboardGridOptions.columns - width + 1)) + return _.chain(_.range(0, gridOptions.columns - width + 1)) .map(col => ({ col, row: _.chain(bottomLine) @@ -227,7 +228,7 @@ Dashboard.prototype.addWidget = function addWidget(textOrVisualization, options const widget = new Widget(props); - const position = calculateNewWidgetPosition(this.widgets, widget); + const position = calculateNewWidgetPosition(this.widgets, widget, this.use_12_column_layout); widget.options.position.col = position.col; widget.options.position.row = position.row; diff --git a/migrations/versions/dedeb4b35ab8_add_use_12_column_layout.py b/migrations/versions/dedeb4b35ab8_add_use_12_column_layout.py new file mode 100644 index 0000000000..58174bec39 --- /dev/null +++ b/migrations/versions/dedeb4b35ab8_add_use_12_column_layout.py @@ -0,0 +1,29 @@ +"""add use_12_column_layout + +Revision ID: dedeb4b35ab8 +Revises: e5c7a4e2df4d +Create Date: 2020-04-18 18:20:00.333654 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = "dedeb4b35ab8" +down_revision = "e5c7a4e2df4d" +branch_labels = None +depends_on = None + + +def upgrade(): + op.add_column( + "dashboards", + sa.Column( + "use_12_column_layout", sa.Boolean(), nullable=False, server_default=False + ), + ) + + +def downgrade(): + op.drop_column("dashboards", "use_12_column_layout") diff --git a/redash/handlers/dashboards.py b/redash/handlers/dashboards.py index c5fceab7c1..232aea11d4 100644 --- a/redash/handlers/dashboards.py +++ b/redash/handlers/dashboards.py @@ -128,6 +128,7 @@ def get(self, dashboard_slug=None): :>json string updated_at: ISO format timestamp for last dashboard modification :>json number version: Revision number of dashboard :>json boolean dashboard_filters_enabled: Whether filters are enabled or not + :>json booleann use_12_column_layout: Whether to use 12-column layout or not :>json boolean is_archived: Whether this dashboard has been removed from the index or not :>json boolean is_draft: Whether this dashboard is a draft or not. :>json array layout: Array of arrays containing widget IDs, corresponding to the rows and columns the widgets are displayed in @@ -199,6 +200,7 @@ def post(self, dashboard_slug): "is_draft", "is_archived", "dashboard_filters_enabled", + "use_12_column_layout" ), ) diff --git a/redash/models/__init__.py b/redash/models/__init__.py index 6304afc44d..30ae2a7733 100644 --- a/redash/models/__init__.py +++ b/redash/models/__init__.py @@ -1067,6 +1067,7 @@ class Dashboard(ChangeTrackingMixin, TimestampMixin, BelongsToOrgMixin, db.Model # layout is no longer used, but kept so we know how to render old dashboards. layout = Column(db.Text) dashboard_filters_enabled = Column(db.Boolean, default=False) + use_12_column_layout = Column(db.Boolean, default=False) is_archived = Column(db.Boolean, default=False, index=True) is_draft = Column(db.Boolean, default=True, index=True) widgets = db.relationship("Widget", backref="dashboard", lazy="dynamic") diff --git a/redash/serializers/__init__.py b/redash/serializers/__init__.py index 992d7d6b56..245efbfba4 100644 --- a/redash/serializers/__init__.py +++ b/redash/serializers/__init__.py @@ -55,7 +55,7 @@ def public_widget(widget): def public_dashboard(dashboard): dashboard_dict = project( serialize_dashboard(dashboard, with_favorite_state=False), - ("name", "layout", "dashboard_filters_enabled", "updated_at", "created_at"), + ("name", "layout", "dashboard_filters_enabled", "use_12_column_layout", "updated_at", "created_at"), ) widget_list = ( @@ -252,6 +252,7 @@ def serialize_dashboard(obj, with_widgets=False, user=None, with_favorite_state= "user": obj.user.to_dict(), "layout": layout, "dashboard_filters_enabled": obj.dashboard_filters_enabled, + "use_12_column_layout": obj.use_12_column_layout, "widgets": widgets, "is_archived": obj.is_archived, "is_draft": obj.is_draft,