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,