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}