Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[SIP-5] Refactor table #5707

Merged
merged 17 commits into from
Aug 31, 2018
8 changes: 0 additions & 8 deletions superset/assets/src/chart/Chart.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import PropTypes from 'prop-types';
import Mustache from 'mustache';
import { Tooltip } from 'react-bootstrap';

import { d3format } from '../modules/utils';
import ChartBody from './ChartBody';
import Loading from '../components/Loading';
import { Logger, LOG_ACTIONS_RENDER_CHART } from '../logger';
Expand Down Expand Up @@ -168,13 +167,6 @@ class Chart extends React.PureComponent {
);
}

d3format(col, number) {
const { datasource } = this.props;
const format = (datasource.column_formats && datasource.column_formats[col]) || '0.3s';

return d3format(format, number);
}

error(e) {
this.props.actions.chartRenderingFailed(e, this.props.chartId);
}
Expand Down
18 changes: 10 additions & 8 deletions superset/assets/src/visualizations/table.css
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
.slice-grid .widget.table .slice_container {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

possibly good news: anything with .widget I think can be removed. this is what grid cells on the old dashboard used to be called.

overflow: auto !important;
overflow: auto !important;
}

.slice_container.table table.table {
Expand All @@ -9,21 +9,22 @@
}

.widget.table td.filtered {
background-color: #005a63;
color: white;
background-color: #005a63;
color: white;
}

.widget.table tr>th {
padding: 1px 5px !important;
font-size: small !important;
padding: 1px 5px !important;
font-size: small !important;
}

.widget.table tr>td {
padding: 1px 5px !important;
font-size: small !important;
padding: 1px 5px !important;
font-size: small !important;
}

table.table thead th.sorting:after, table.table thead th.sorting_asc:after, table.table thead th.sorting_desc:after {
top: 0px;
top: 0px;
}

.like-pre {
Expand All @@ -34,6 +35,7 @@ table.table thead th.sorting:after, table.table thead th.sorting_asc:after, tabl
width: auto;
max-width: unset;
}

.widget.table thead tr {
height: 25px;
}
195 changes: 142 additions & 53 deletions superset/assets/src/visualizations/table.js
Original file line number Diff line number Diff line change
@@ -1,58 +1,108 @@
import d3 from 'd3';
import PropTypes from 'prop-types';
import $ from 'jquery';
import dt from 'datatables.net-bs';
import 'datatables.net-bs/css/dataTables.bootstrap.css';
import dompurify from 'dompurify';

import { fixDataTableBodyHeight, d3TimeFormatPreset } from '../modules/utils';
import './table.css';

const $ = require('jquery');

dt(window, $);

function tableVis(slice, payload) {
const container = $(slice.selector);
const fC = d3.format('0,000');
const propTypes = {
data: PropTypes.shape({
records: PropTypes.arrayOf(PropTypes.object),
columns: PropTypes.arrayOf(PropTypes.string),
}),
height: PropTypes.number,
alignPn: PropTypes.bool,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"Pn" means positive/negative? could we update that variable name to be more descriptive to indicate that?

colorPn: PropTypes.bool,
columnFormats: PropTypes.object,
filters: PropTypes.object,
includeSearch: PropTypes.bool,
metrics: PropTypes.arrayOf(PropTypes.string),
onAddFilter: PropTypes.func,
onRemoveFilter: PropTypes.func,
orderDesc: PropTypes.bool,
pageLength: PropTypes.oneOfType([
PropTypes.number,
PropTypes.string,
]),
percentMetrics: PropTypes.array,
tableFilter: PropTypes.bool,
tableTimestampFormat: PropTypes.string,
timeseriesLimitMetric: PropTypes.oneOfType([
PropTypes.string,
PropTypes.object,
]),
verboseMap: PropTypes.object,
};

const fC = d3.format('0,000');
function NOOP() {}

function TableVis(element, props) {
PropTypes.checkPropTypes(propTypes, props, 'prop', 'TableVis');

const {
data,
height,
alignPn,
colorPn,
columnFormats,
filters,
includeSearch,
metrics: rawMetrics,
onAddFilter = NOOP,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we need a No Op on these prop functions?

Copy link
Contributor Author

@kristw kristw Aug 24, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Set NOOP as default value to avoid checking if onAddFilter exists every time before calling it.

onRemoveFilter = NOOP,
orderDesc,
pageLength,
percentMetrics,
tableFilter,
tableTimestampFormat,
timeseriesLimitMetric,
verboseMap,
} = props;

const { columns, records } = data;

const data = payload.data;
const fd = slice.formData;
const $container = $(element);

let metrics = (fd.metrics || []).map(m => m.label || m);
// Add percent metrics
metrics = metrics.concat((fd.percent_metrics || []).map(m => '%' + m));
// Removing metrics (aggregates) that are strings
metrics = metrics.filter(m => !isNaN(data.records[0][m]));
const metrics = (rawMetrics || []).map(m => m.label || m)
// Add percent metrics
.concat((percentMetrics || []).map(m => '%' + m))
// Removing metrics (aggregates) that are strings
.filter(m => !Number.isNaN(records[0][m]));

function col(c) {
const arr = [];
for (let i = 0; i < data.records.length; i += 1) {
arr.push(data.records[i][c]);
for (let i = 0; i < records.length; i += 1) {
arr.push(records[i][c]);
}
return arr;
}
const maxes = {};
const mins = {};
for (let i = 0; i < metrics.length; i += 1) {
if (fd.align_pn) {
if (alignPn) {
maxes[metrics[i]] = d3.max(col(metrics[i]).map(Math.abs));
} else {
maxes[metrics[i]] = d3.max(col(metrics[i]));
mins[metrics[i]] = d3.min(col(metrics[i]));
}
}

const tsFormatter = d3TimeFormatPreset(fd.table_timestamp_format);
const tsFormatter = d3TimeFormatPreset(tableTimestampFormat);

const div = d3.select(slice.selector);
const div = d3.select(element);
div.html('');
const table = div.append('table')
.classed(
'dataframe dataframe table table-striped ' +
'table-condensed table-hover dataTable no-footer', true)
.attr('width', '100%');

const verboseMap = slice.datasource.verbose_map;
const cols = data.columns.map((c) => {
const cols = columns.map((c) => {
if (verboseMap[c]) {
return verboseMap[c];
}
Expand All @@ -69,18 +119,15 @@ function tableVis(slice, payload) {
.data(cols)
.enter()
.append('th')
.text(function (d) {
return d;
});
.text(d => d);

const filters = slice.getFilters();
table.append('tbody')
.selectAll('tr')
.data(data.records)
.data(records)
.enter()
.append('tr')
.selectAll('td')
.data(row => data.columns.map((c) => {
.data(row => columns.map((c) => {
const val = row[c];
let html;
const isMetric = metrics.indexOf(c) >= 0;
Expand All @@ -91,9 +138,9 @@ function tableVis(slice, payload) {
html = `<span class="like-pre">${dompurify.sanitize(val)}</span>`;
}
if (isMetric) {
html = slice.d3format(c, val);
}
if (c[0] === '%') {
const format = (columnFormats && columnFormats[c]) || '0.3s';
html = d3.format(format)(val);
} else if (c[0] === '%') {
html = d3.format('.3p')(val);
}
return {
Expand All @@ -107,8 +154,8 @@ function tableVis(slice, payload) {
.append('td')
.style('background-image', function (d) {
if (d.isMetric) {
const r = (fd.color_pn && d.val < 0) ? 150 : 0;
if (fd.align_pn) {
const r = (colorPn && d.val < 0) ? 150 : 0;
if (alignPn) {
const perc = Math.abs(Math.round((d.val / maxes[d.col]) * 100));
// The 0.01 to 0.001 is a workaround for what appears to be a
// CSS rendering bug on flat, transparent colors
Expand All @@ -134,7 +181,7 @@ function tableVis(slice, payload) {
})
.classed('text-right', d => d.isMetric)
.attr('title', (d) => {
if (!isNaN(d.val)) {
if (!Number.isNaN(d.val)) {
return fC(d.val);
}
return null;
Expand All @@ -149,58 +196,100 @@ function tableVis(slice, payload) {
filters[d.col].indexOf(d.val) >= 0,
)
.on('click', function (d) {
if (!d.isMetric && fd.table_filter) {
if (!d.isMetric && tableFilter) {
const td = d3.select(this);
if (td.classed('filtered')) {
slice.removeFilter(d.col, [d.val]);
onRemoveFilter(d.col, [d.val]);
d3.select(this).classed('filtered', false);
} else {
d3.select(this).classed('filtered', true);
slice.addFilter(d.col, [d.val]);
onAddFilter(d.col, [d.val]);
}
}
})
.style('cursor', function (d) {
return (!d.isMetric) ? 'pointer' : '';
})
.html(d => d.html ? d.html : d.val);
const height = slice.height();
let paging = false;
let pageLength;
if (fd.page_length && fd.page_length > 0) {
paging = true;
pageLength = parseInt(fd.page_length, 10);
}
const datatable = container.find('.dataTable').DataTable({

const paging = pageLength && pageLength > 0;

const datatable = $container.find('.dataTable').DataTable({
paging,
pageLength,
pageLength: paging ? parseInt(pageLength, 10) : undefined,
aaSorting: [],
searching: fd.include_search,
searching: includeSearch,
bInfo: false,
scrollY: height + 'px',
scrollCollapse: true,
scrollX: true,
});
fixDataTableBodyHeight(
container.find('.dataTables_wrapper'), height);

fixDataTableBodyHeight($container.find('.dataTables_wrapper'), height);
// Sorting table by main column
let sortBy;
if (fd.timeseries_limit_metric) {
if (timeseriesLimitMetric) {
// Sort by as specified
sortBy = fd.timeseries_limit_metric.label || fd.timeseries_limit_metric;
sortBy = timeseriesLimitMetric.label || timeseriesLimitMetric;
} else if (metrics.length > 0) {
// If not specified, use the first metric from the list
sortBy = metrics[0];
}
if (sortBy) {
datatable.column(data.columns.indexOf(sortBy)).order(fd.order_desc ? 'desc' : 'asc');
datatable.column(columns.indexOf(sortBy)).order(orderDesc ? 'desc' : 'asc');
}
if (sortBy && metrics.indexOf(sortBy) < 0) {
// Hiding the sortBy column if not in the metrics list
datatable.column(data.columns.indexOf(sortBy)).visible(false);
datatable.column(columns.indexOf(sortBy)).visible(false);
}
datatable.draw();
container.parents('.widget').find('.tooltip').remove();
$container.parents('.widget').find('.tooltip').remove();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would guess that this tried to remove tooltips from other dashboard charts 🤔

}

TableVis.propTypes = propTypes;

function adaptor(slice, payload) {
const { selector, formData, datasource } = slice;
const {
align_pn: alignPn,
color_pn: colorPn,
include_search: includeSearch,
metrics,
order_desc: orderDesc,
page_length: pageLength,
percent_metrics: percentMetrics,
table_filter: tableFilter,
table_timestamp_format: tableTimestampFormat,
timeseries_limit_metric: timeseriesLimitMetric,
} = formData;
const {
verbose_map: verboseMap,
column_formats: columnFormats,
} = datasource;
const element = document.querySelector(selector);

return TableVis(element, {
data: payload.data,
height: slice.height(),
alignPn,
colorPn,
columnFormats,
filters: slice.getFilters(),
includeSearch,
metrics,
onAddFilter(...args) { slice.addFilter(...args); },
orderDesc,
pageLength,
percentMetrics,
// Aug 22, 2018
// Perhaps this `tableFilter` field can be removed as there is
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

that's interesting. I think this table used to be able to function as a filter on dashboards but perhaps that broke at some point. @mistercrunch any thoughts?

// no code left in repo to set tableFilter to true.
// which make `onAddFilter` will never be called as well.
tableFilter,
tableTimestampFormat,
timeseriesLimitMetric,
verboseMap,
});
}

module.exports = tableVis;
export default adaptor;