You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@superset.apache.org by cc...@apache.org on 2018/06/22 00:54:34 UTC

[incubator-superset] 19/26: Fix dashboard position row data (#5131)

This is an automated email from the ASF dual-hosted git repository.

ccwilliams pushed a commit to branch dashboard-builder
in repository https://gitbox.apache.org/repos/asf/incubator-superset.git

commit 017e797e352d9d00b035f5b330fcac8fe84187e5
Author: Grace Guo <gr...@airbnb.com>
AuthorDate: Wed Jun 6 17:06:12 2018 -0700

    Fix dashboard position row data (#5131)
    
    * add slice_name to markdown
    
    (cherry picked from commit 14b01f1)
    
    * set min grid width be 1 column
    
    * remove empty column
    
    * check total columns count <= 12
    
    * scan position data and fix rows
    
    * fix dashboard url with default_filters
---
 superset/assets/src/dashboard/util/constants.js    |   2 +-
 .../src/dashboard/util/dashboardLayoutConverter.js | 214 +++++++++++++++++++--
 superset/models/core.py                            |  14 +-
 3 files changed, 208 insertions(+), 22 deletions(-)

diff --git a/superset/assets/src/dashboard/util/constants.js b/superset/assets/src/dashboard/util/constants.js
index ef2c8bb..bfe24cc 100644
--- a/superset/assets/src/dashboard/util/constants.js
+++ b/superset/assets/src/dashboard/util/constants.js
@@ -19,7 +19,7 @@ export const DASHBOARD_ROOT_DEPTH = 0;
 export const GRID_BASE_UNIT = 8;
 export const GRID_GUTTER_SIZE = 2 * GRID_BASE_UNIT;
 export const GRID_COLUMN_COUNT = 12;
-export const GRID_MIN_COLUMN_COUNT = 2;
+export const GRID_MIN_COLUMN_COUNT = 1;
 export const GRID_MIN_ROW_UNITS = 5;
 export const GRID_MAX_ROW_UNITS = 100;
 export const GRID_MIN_ROW_HEIGHT = GRID_GUTTER_SIZE;
diff --git a/superset/assets/src/dashboard/util/dashboardLayoutConverter.js b/superset/assets/src/dashboard/util/dashboardLayoutConverter.js
index cf7a493..c6c124b 100644
--- a/superset/assets/src/dashboard/util/dashboardLayoutConverter.js
+++ b/superset/assets/src/dashboard/util/dashboardLayoutConverter.js
@@ -13,7 +13,12 @@ import {
   DASHBOARD_GRID_TYPE,
 } from './componentTypes';
 
-import { DASHBOARD_GRID_ID } from './constants';
+import {
+  DASHBOARD_GRID_ID,
+  GRID_MIN_COLUMN_COUNT,
+  GRID_MIN_ROW_UNITS,
+  GRID_COLUMN_COUNT,
+} from './constants';
 
 const MAX_RECURSIVE_LEVEL = 6;
 const GRID_RATIO = 4;
@@ -72,19 +77,32 @@ function getColContainer() {
 }
 
 function getChartHolder(item) {
-  const { size_x, size_y, slice_id, code } = item;
+  const { size_x, size_y, slice_id, code, slice_name } = item;
 
-  const width = Math.max(1, Math.floor(size_x / GRID_RATIO));
-  const height = Math.max(1, Math.round(size_y / GRID_RATIO));
+  const width = Math.max(
+    GRID_MIN_COLUMN_COUNT,
+    Math.round(size_x / GRID_RATIO),
+  );
+  const height = Math.max(
+    GRID_MIN_ROW_UNITS,
+    Math.round(size_y / GRID_RATIO * 100 / ROW_HEIGHT),
+  );
   if (code !== undefined) {
+    let markdownContent = '';
+    if (slice_name) {
+      markdownContent = `##### **${slice_name}**\n`;
+    }
+    if (code) {
+      markdownContent += code;
+    }
     return {
       type: MARKDOWN_TYPE,
       id: `DASHBOARD_MARKDOWN_TYPE-${generateId()}`,
       children: [],
       meta: {
         width,
-        height: Math.round(height * 100 / ROW_HEIGHT),
-        code,
+        height,
+        code: markdownContent,
       },
     };
   }
@@ -94,7 +112,7 @@ function getChartHolder(item) {
     children: [],
     meta: {
       width,
-      height: Math.round(height * 100 / ROW_HEIGHT),
+      height,
       chartId: parseInt(slice_id, 10),
     },
   };
@@ -135,6 +153,50 @@ function hasOverlap(positions, xAxis = true) {
     });
 }
 
+function isWideLeafComponent(component) {
+  return (
+    [CHART_TYPE, MARKDOWN_TYPE].indexOf(component.type) > -1 &&
+    component.meta.width > GRID_MIN_COLUMN_COUNT
+  );
+}
+
+function canReduceColumnWidth(columnComponent, root) {
+  return (
+    columnComponent.type === COLUMN_TYPE &&
+    columnComponent.meta.width > GRID_MIN_COLUMN_COUNT &&
+    columnComponent.children.every(
+      childId =>
+        isWideLeafComponent(root[childId]) ||
+        (root[childId].type === ROW_TYPE &&
+          root[childId].children.every(id => isWideLeafComponent(root[id]))),
+    )
+  );
+}
+
+function reduceRowWidth(rowComponent, root) {
+  // find widest free chart and reduce width
+  const widestChartId = rowComponent.children
+    .filter(childId => isWideLeafComponent(root[childId]))
+    .reduce((prev, current) => {
+      if (root[prev].meta.width >= root[current].meta.width) {
+        return prev;
+      }
+      return current;
+    });
+
+  if (widestChartId) {
+    root[widestChartId].meta.width -= 1;
+  }
+  return getChildrenSum(rowComponent.children, 'width', root);
+}
+
+function reduceComponentWidth(component) {
+  if (isWideLeafComponent(component)) {
+    component.meta.width -= 1;
+  }
+  return component.meta.width;
+}
+
 function doConvert(positions, level, parent, root) {
   if (positions.length === 0) {
     return;
@@ -235,7 +297,6 @@ function doConvert(positions, level, parent, root) {
             // create a new column
             const colContainer = getColContainer();
             root[colContainer.id] = colContainer;
-            rowContainer.children.push(colContainer.id);
 
             if (!hasOverlap(lower, false)) {
               lower.sort(sortByRowId).forEach(item => {
@@ -248,11 +309,15 @@ function doConvert(positions, level, parent, root) {
             }
 
             // add col meta
-            colContainer.meta.width = getChildrenMax(
-              colContainer.children,
-              'width',
-              root,
-            );
+            if (colContainer.children.length) {
+              rowContainer.children.push(colContainer.id);
+              // add col meta
+              colContainer.meta.width = getChildrenMax(
+                colContainer.children,
+                'width',
+                root,
+              );
+            }
           }
 
           currentItems = upper.slice();
@@ -278,6 +343,50 @@ export function convertToLayout(positions) {
   Object.values(root).forEach(item => {
     if (ROW_TYPE === item.type) {
       const meta = item.meta;
+      if (meta.width > GRID_COLUMN_COUNT) {
+        let currentWidth = meta.width;
+        while (
+          currentWidth > GRID_COLUMN_COUNT &&
+          item.children.filter(id => isWideLeafComponent(root[id])).length
+        ) {
+          currentWidth = reduceRowWidth(item, root);
+        }
+
+        // reduce column width
+        if (currentWidth > GRID_COLUMN_COUNT) {
+          // find column that: width > 2 and each row has at least 1 chart can reduce
+          // 2 loops: same column may reduce multiple times
+          let colIds;
+          do {
+            colIds = item.children.filter(colId =>
+              canReduceColumnWidth(root[colId], root),
+            );
+            let idx = 0;
+            while (idx < colIds.length && currentWidth > GRID_COLUMN_COUNT) {
+              const currentColumn = colIds[idx];
+              root[currentColumn].children.forEach(childId => {
+                if (root[childId].type === ROW_TYPE) {
+                  root[childId].meta.width = reduceRowWidth(
+                    root[childId],
+                    root,
+                  );
+                } else {
+                  root[childId].meta.width = reduceComponentWidth(
+                    root[childId],
+                  );
+                }
+              });
+              root[currentColumn].meta.width = getChildrenMax(
+                root[currentColumn].children,
+                'width',
+                root,
+              );
+              currentWidth = getChildrenSum(item.children, 'width', root);
+              idx += 1;
+            }
+          } while (colIds.length && currentWidth > GRID_COLUMN_COUNT);
+        }
+      }
       delete meta.width;
     }
   });
@@ -286,13 +395,84 @@ export function convertToLayout(positions) {
   return root;
 }
 
+function mergePosition(position, bottomLine, maxColumn) {
+  const { col, size_x, size_y } = position;
+  const endColumn = col + size_x > maxColumn ? bottomLine.length : col + size_x;
+  const nextSectionStart =
+    bottomLine.slice(col).findIndex(value => value > bottomLine[col]) + 1;
+
+  const currentBottom =
+    nextSectionStart > 0 && nextSectionStart < size_x
+      ? Math.max.apply(null, bottomLine.slice(col, col + size_x + 1))
+      : bottomLine[col];
+  bottomLine.fill(currentBottom + size_y, col, endColumn);
+}
+
+// In original position data, a lot of position's row attribute are not correct, and same positions are
+// assigned to more than 1 chart. The convert function depends on row id, col id to split
+// the whole dashboard into nested rows and columns. Bad row id will lead to many empty spaces, or a few
+// charts are overlapped in the same row.
+// This function read positions by row first. Then based on previous col id, width and height attribute,
+// re-calculate next position's row id.
+function scanDashboardPositionsData(positions) {
+  const bottomLine = new Array(49).fill(0);
+  bottomLine[0] = Number.MAX_VALUE;
+  const maxColumn = Math.max.apply(
+    null,
+    positions.slice().map(position => position.col),
+  );
+
+  const positionsByRowId = {};
+  positions
+    .slice()
+    .sort(sortByRowId)
+    .forEach(position => {
+      const { row } = position;
+      if (positionsByRowId[row] === undefined) {
+        positionsByRowId[row] = [];
+      }
+      positionsByRowId[row].push(position);
+    });
+  const rawPositions = Object.values(positionsByRowId);
+  const updatedPositions = [];
+
+  while (rawPositions.length) {
+    const nextRow = rawPositions.shift();
+    let nextCol = 1;
+    while (nextRow.length) {
+      // special treatment for duplicated positions: display wider one first
+      const availableIndexByColumn = nextRow
+        .filter(position => position.col === nextCol)
+        .map((position, index) => index);
+      if (availableIndexByColumn.length) {
+        const idx =
+          availableIndexByColumn.length > 1
+            ? availableIndexByColumn.sort(
+                (idx1, idx2) => nextRow[idx2].size_x - nextRow[idx1].size_x,
+              )[0]
+            : availableIndexByColumn[0];
+
+        const nextPosition = nextRow.splice(idx, 1)[0];
+        mergePosition(nextPosition, bottomLine, maxColumn + 1);
+        nextPosition.row = bottomLine[nextPosition.col] - nextPosition.size_y;
+        updatedPositions.push(nextPosition);
+        nextCol += nextPosition.size_x;
+      } else {
+        nextCol = nextRow[0].col;
+      }
+    }
+  }
+
+  return updatedPositions;
+}
+
 export default function(dashboard) {
   const positions = [];
-
-  // position data clean up. some dashboard didn't have position_json
   let { position_json } = dashboard;
   const positionDict = {};
   if (Array.isArray(position_json)) {
+    // scan and fix positions data: extra spaces, dup rows, .etc
+    position_json = scanDashboardPositionsData(position_json);
     position_json.forEach(position => {
       positionDict[position.slice_id] = position;
     });
@@ -300,12 +480,13 @@ export default function(dashboard) {
     position_json = [];
   }
 
+  // position data clean up. some dashboard didn't have position_json
   const lastRowId = Math.max(
     0,
     Math.max.apply(null, position_json.map(pos => pos.row + pos.size_y)),
   );
   let newSliceCounter = 0;
-  dashboard.slices.forEach(({ slice_id, form_data }) => {
+  dashboard.slices.forEach(({ slice_id, form_data, slice_name }) => {
     let position = positionDict[slice_id];
     if (!position) {
       // append new slices to dashboard bottom, 3 slices per row
@@ -322,6 +503,7 @@ export default function(dashboard) {
       position = {
         ...position,
         code: form_data.code,
+        slice_name,
       };
     }
     positions.push(position);
diff --git a/superset/models/core.py b/superset/models/core.py
index 3b663af..faf0a6f 100644
--- a/superset/models/core.py
+++ b/superset/models/core.py
@@ -347,11 +347,15 @@ class Dashboard(Model, AuditMixinNullable, ImportMixin):
             # add default_filters to the preselect_filters of dashboard
             json_metadata = json.loads(self.json_metadata)
             default_filters = json_metadata.get('default_filters')
-            # make sure default_filters is not empty
-            if default_filters and json.loads(default_filters):
-                filters = parse.quote(default_filters.encode('utf8'))
-                return '/superset/dashboard/{}/?preselect_filters={}'.format(
-                    self.slug or self.id, filters)
+            # make sure default_filters is not empty and is valid
+            if default_filters and default_filters != '{}':
+                try:
+                    if json.loads(default_filters):
+                        filters = parse.quote(default_filters.encode('utf8'))
+                        return '/superset/dashboard/{}/?preselect_filters={}'.format(
+                            self.slug or self.id, filters)
+                except Exception:
+                    pass
         return '/superset/dashboard/{}/'.format(self.slug or self.id)
 
     @property