You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@superset.apache.org by be...@apache.org on 2019/06/21 16:12:17 UTC

[incubator-superset] branch master updated: [SQL Lab] Add JSON modal when clicking on cells with JSON objects (#7720)

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

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


The following commit(s) were added to refs/heads/master by this push:
     new 0d248fe  [SQL Lab] Add JSON modal when clicking on cells with JSON objects (#7720)
0d248fe is described below

commit 0d248fe630c03af5426ff542947e431e4e109849
Author: Erik Ritter <er...@airbnb.com>
AuthorDate: Fri Jun 21 09:12:09 2019 -0700

    [SQL Lab] Add JSON modal when clicking on cells with JSON objects (#7720)
---
 superset/assets/package-lock.json                  | 49 +++++++++++++
 superset/assets/package.json                       |  2 +
 .../components/FilterableTable/FilterableTable.jsx | 80 +++++++++++++++++++++-
 3 files changed, 129 insertions(+), 2 deletions(-)

diff --git a/superset/assets/package-lock.json b/superset/assets/package-lock.json
index de69685..8d3f8ac 100644
--- a/superset/assets/package-lock.json
+++ b/superset/assets/package-lock.json
@@ -3350,6 +3350,14 @@
         "@types/react": "*"
       }
     },
+    "@types/react-json-tree": {
+      "version": "0.6.11",
+      "resolved": "https://registry.npmjs.org/@types/react-json-tree/-/react-json-tree-0.6.11.tgz",
+      "integrity": "sha512-HP0Sf0ZHjCi1FHLJxh/pLaxaevEW6ILlV2C5Dn3EZFTkLjWkv+EVf/l/zvtmoU9ZwuO/3TKVeWK/700UDxunTw==",
+      "requires": {
+        "@types/react": "*"
+      }
+    },
     "@types/react-loadable": {
       "version": "5.5.1",
       "resolved": "https://registry.npmjs.org/@types/react-loadable/-/react-loadable-5.5.1.tgz",
@@ -5177,6 +5185,11 @@
         }
       }
     },
+    "base16": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/base16/-/base16-1.0.0.tgz",
+      "integrity": "sha1-4pf2DX7BAUp6lxo568ipjAtoHnA="
+    },
     "batch": {
       "version": "0.6.1",
       "resolved": "https://registry.npmjs.org/batch/-/batch-0.6.1.tgz",
@@ -13415,6 +13428,11 @@
       "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.11.tgz",
       "integrity": "sha512-DHb1ub+rMjjrxqlB3H56/6MXtm1lSksDp2rA2cNWjG8mlDUYFhUj3Di2Zn5IwSU87xLv8tNIQ7sSwE/YOX/D/Q=="
     },
+    "lodash.curry": {
+      "version": "4.1.1",
+      "resolved": "https://registry.npmjs.org/lodash.curry/-/lodash.curry-4.1.1.tgz",
+      "integrity": "sha1-JI42By7ekGUB11lmIAqG2riyMXA="
+    },
     "lodash.debounce": {
       "version": "4.0.8",
       "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz",
@@ -13433,6 +13451,11 @@
       "integrity": "sha1-+wMJF/hqMTTlvJvsDWngAT3f7bI=",
       "dev": true
     },
+    "lodash.flow": {
+      "version": "3.5.0",
+      "resolved": "https://registry.npmjs.org/lodash.flow/-/lodash.flow-3.5.0.tgz",
+      "integrity": "sha1-h79AKSuM+D5OjOGjrkIJ4gBxZ1o="
+    },
     "lodash.get": {
       "version": "4.4.2",
       "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz",
@@ -17392,6 +17415,11 @@
       "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz",
       "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A=="
     },
+    "pure-color": {
+      "version": "1.3.0",
+      "resolved": "https://registry.npmjs.org/pure-color/-/pure-color-1.3.0.tgz",
+      "integrity": "sha1-H+Bk+wrIUfDeYTIKi/eWg2Qi8z4="
+    },
     "q": {
       "version": "1.5.1",
       "resolved": "https://registry.npmjs.org/q/-/q-1.5.1.tgz",
@@ -17544,6 +17572,17 @@
         "prop-types": "^15.5.8"
       }
     },
+    "react-base16-styling": {
+      "version": "0.5.3",
+      "resolved": "https://registry.npmjs.org/react-base16-styling/-/react-base16-styling-0.5.3.tgz",
+      "integrity": "sha1-OFjyTpxN2MvT9wLz901YHKKRcmk=",
+      "requires": {
+        "base16": "^1.0.0",
+        "lodash.curry": "^4.0.1",
+        "lodash.flow": "^3.3.0",
+        "pure-color": "^1.2.0"
+      }
+    },
     "react-bootstrap": {
       "version": "0.31.5",
       "resolved": "https://registry.npmjs.org/react-bootstrap/-/react-bootstrap-0.31.5.tgz",
@@ -17702,6 +17741,16 @@
       "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.6.3.tgz",
       "integrity": "sha512-u7FDWtthB4rWibG/+mFbVd5FvdI20yde86qKGx4lVUTWmPlSWQ4QxbBIrrs+HnXGbxOUlUzTAP/VDmvCwaP2yA=="
     },
+    "react-json-tree": {
+      "version": "0.11.2",
+      "resolved": "https://registry.npmjs.org/react-json-tree/-/react-json-tree-0.11.2.tgz",
+      "integrity": "sha512-aYhUPj1y5jR3ZQ+G3N7aL8FbTyO03iLwnVvvEikLcNFqNTyabdljo9xDftZndUBFyyyL0aK3qGO9+8EilILHUw==",
+      "requires": {
+        "babel-runtime": "^6.6.1",
+        "prop-types": "^15.5.8",
+        "react-base16-styling": "^0.5.1"
+      }
+    },
     "react-jsonschema-form": {
       "version": "1.2.0",
       "resolved": "https://registry.npmjs.org/react-jsonschema-form/-/react-jsonschema-form-1.2.0.tgz",
diff --git a/superset/assets/package.json b/superset/assets/package.json
index c403dba..f314f6a 100644
--- a/superset/assets/package.json
+++ b/superset/assets/package.json
@@ -79,6 +79,7 @@
     "@superset-ui/number-format": "^0.11.3",
     "@superset-ui/time-format": "^0.11.3",
     "@superset-ui/translation": "^0.11.0",
+    "@types/react-json-tree": "^0.6.11",
     "@vx/responsive": "0.0.172",
     "abortcontroller-polyfill": "^1.1.9",
     "bootstrap": "^3.3.6",
@@ -118,6 +119,7 @@
     "react-dom": "^16.4.1",
     "react-gravatar": "^2.6.1",
     "react-hot-loader": "^4.3.6",
+    "react-json-tree": "^0.11.2",
     "react-jsonschema-form": "^1.2.0",
     "react-map-gl": "^4.0.10",
     "react-markdown": "^3.3.0",
diff --git a/superset/assets/src/components/FilterableTable/FilterableTable.jsx b/superset/assets/src/components/FilterableTable/FilterableTable.jsx
index fa1fc44..ef5bcd7 100644
--- a/superset/assets/src/components/FilterableTable/FilterableTable.jsx
+++ b/superset/assets/src/components/FilterableTable/FilterableTable.jsx
@@ -20,6 +20,7 @@ import { List } from 'immutable';
 import PropTypes from 'prop-types';
 import JSONbig from 'json-bigint';
 import React, { PureComponent } from 'react';
+import JSONTree from 'react-json-tree';
 import {
   Column,
   Grid,
@@ -29,14 +30,59 @@ import {
   Table,
 } from 'react-virtualized';
 import { getTextDimension } from '@superset-ui/dimension';
+import { t } from '@superset-ui/translation';
+
+import Button from '../Button';
+import CopyToClipboard from '../CopyToClipboard';
+import ModalTrigger from '../ModalTrigger';
 import TooltipWrapper from '../TooltipWrapper';
 
 function getTextWidth(text, font = '12px Roboto') {
   return getTextDimension({ text, style: { font } }).width;
 }
 
+function safeJsonObjectParse(data) {
+  // First perform a cheap proxy to avoid calling JSON.parse on data that is clearly not a
+  // JSON object or array
+  if (typeof data !== 'string' || ['{', '['].indexOf(data.substring(0, 1)) === -1) {
+    return null;
+  }
+
+  // We know `data` is a string starting with '{' or '[', so try to parse it as a valid object
+  try {
+    const jsonData = JSON.parse(data);
+    if (jsonData && typeof jsonData === 'object') {
+      return jsonData;
+    }
+    return null;
+  } catch (_) {
+    return null;
+  }
+}
+
 const SCROLL_BAR_HEIGHT = 15;
 const GRID_POSITION_ADJUSTMENT = 4;
+const JSON_TREE_THEME = {
+  scheme: 'monokai',
+  author: 'wimer hazenberg (http://www.monokai.nl)',
+  base00: '#272822',
+  base01: '#383830',
+  base02: '#49483e',
+  base03: '#75715e',
+  base04: '#a59f85',
+  base05: '#f8f8f2',
+  base06: '#f5f4f1',
+  base07: '#f9f8f5',
+  base08: '#f92672',
+  base09: '#fd971f',
+  base0A: '#f4bf75',
+  base0B: '#a6e22e',
+  base0C: '#a1efe4',
+  base0D: '#66d9ef',
+  base0E: '#ae81ff',
+  base0F: '#cc6633',
+};
+
 
 // when more than MAX_COLUMNS_FOR_TABLE are returned, switch from table to grid view
 export const MAX_COLUMNS_FOR_TABLE = 50;
@@ -68,9 +114,11 @@ export default class FilterableTable extends PureComponent {
   constructor(props) {
     super(props);
     this.list = List(this.formatTableData(props.data));
+    this.addJsonModal = this.addJsonModal.bind(this);
     this.renderGridCell = this.renderGridCell.bind(this);
     this.renderGridCellHeader = this.renderGridCellHeader.bind(this);
     this.renderGrid = this.renderGrid.bind(this);
+    this.renderTableCell = this.renderTableCell.bind(this);
     this.renderTableHeader = this.renderTableHeader.bind(this);
     this.renderTable = this.renderTable.bind(this);
     this.rowClassName = this.rowClassName.bind(this);
@@ -165,6 +213,17 @@ export default class FilterableTable extends PureComponent {
     this.setState({ sortBy, sortDirection });
   }
 
+  addJsonModal(node, jsonObject, jsonString) {
+    return (
+      <ModalTrigger
+        modalBody={<JSONTree data={jsonObject} theme={JSON_TREE_THEME} />}
+        modalFooter={<Button><CopyToClipboard shouldShowText={false} text={jsonString} /></Button>}
+        modalTitle={t('Cell Content')}
+        triggerNode={node}
+      />
+    );
+  }
+
   renderTableHeader({ dataKey, label, sortBy, sortDirection }) {
     const className = this.props.expandedColumns.indexOf(label) > -1
       ? 'header-style-disabled'
@@ -200,15 +259,22 @@ export default class FilterableTable extends PureComponent {
 
   renderGridCell({ columnIndex, key, rowIndex, style }) {
     const columnKey = this.props.orderedColumnKeys[columnIndex];
-    return (
+    const cellData = this.list.get(rowIndex)[columnKey];
+    const cellNode = (
       <div
         key={key}
         style={{ ...style, top: style.top - GRID_POSITION_ADJUSTMENT }}
         className={`grid-cell ${this.rowClassName({ index: rowIndex })}`}
       >
-        {this.list.get(rowIndex)[columnKey]}
+        {cellData}
       </div>
     );
+
+    const jsonObject = safeJsonObjectParse(cellData);
+    if (jsonObject) {
+      return this.addJsonModal(cellNode, jsonObject, cellData);
+    }
+    return cellNode;
   }
 
   renderGrid() {
@@ -266,6 +332,15 @@ export default class FilterableTable extends PureComponent {
     );
   }
 
+  renderTableCell({ cellData }) {
+    const cellNode = String(cellData);
+    const jsonObject = safeJsonObjectParse(cellData);
+    if (jsonObject) {
+      return this.addJsonModal(cellNode, jsonObject, cellData);
+    }
+    return cellNode;
+  }
+
   renderTable() {
     const { sortBy, sortDirection } = this.state;
     const {
@@ -321,6 +396,7 @@ export default class FilterableTable extends PureComponent {
           >
             {orderedColumnKeys.map(columnKey => (
               <Column
+                cellRenderer={this.renderTableCell}
                 dataKey={columnKey}
                 disableSort={false}
                 headerRenderer={this.renderTableHeader}