refactor: Replace usages of reactable in TimeTable (#11239)

* Refactor TimeTable to use react-table

* Fix import

* Refactor TimeTable into functional component
This commit is contained in:
Kamil Gabryjelski 2020-10-26 18:25:41 +01:00 committed by GitHub
parent 10a54b52ac
commit 6acae9ab6d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

View File

@ -16,12 +16,12 @@
* specific language governing permissions and limitations
* under the License.
*/
import React from 'react';
import React, { useMemo } from 'react';
import PropTypes from 'prop-types';
import Mustache from 'mustache';
import { scaleLinear } from 'd3-scale';
import { Table, Thead, Th, Tr, Td } from 'reactable-arc';
import { formatNumber, formatTime } from '@superset-ui/core';
import TableView from 'src/components/TableView';
import { formatNumber, formatTime, styled } from '@superset-ui/core';
import {
InfoTooltipWithTrigger,
MetricOption,
@ -89,62 +89,77 @@ const defaultProps = {
url: '',
};
class TimeTable extends React.PureComponent {
renderLeftCell(row) {
const { rowType, url } = this.props;
const context = { metric: row };
const fullUrl = url ? Mustache.render(url, context) : null;
const TimeTableStyles = styled.div`
height: ${props => props.height}px;
if (rowType === 'column') {
const column = row;
if (fullUrl) {
return (
<a href={fullUrl} rel="noopener noreferrer" target="_blank">
{column.label}
</a>
);
}
return column.label;
}
const metric = row;
return (
<MetricOption
metric={metric}
url={fullUrl}
showFormula={false}
openInNewWindow
/>
);
th {
z-index: 1; // to cover sparkline
}
`;
renderSparklineCell(valueField, column, entries) {
let sparkData;
if (column.timeRatio) {
// Period ratio sparkline
sparkData = [];
for (let i = column.timeRatio; i < entries.length; i += 1) {
const prevData = entries[i - column.timeRatio][valueField];
if (prevData && prevData !== 0) {
sparkData.push(entries[i][valueField] / prevData);
} else {
sparkData.push(null);
const TimeTable = ({
className,
height,
data,
columnConfigs,
rowType,
rows,
url,
}) => {
const memoizedColumns = useMemo(
() => [
{ accessor: 'metric', Header: 'Metric' },
...columnConfigs.map((columnConfig, i) => ({
accessor: columnConfig.key,
cellProps: columnConfig.colType === 'spark' && {
style: { width: '1%' },
},
Header: () => (
<>
{columnConfig.label}{' '}
{columnConfig.tooltip && (
<InfoTooltipWithTrigger
tooltip={columnConfig.tooltip}
label={`tt-col-${i}`}
placement="top"
/>
)}
</>
),
sortType: (rowA, rowB, columnId) => {
const rowAVal = rowA.values[columnId].props['data-value'];
const rowBVal = rowB.values[columnId].props['data-value'];
return rowAVal - rowBVal;
},
})),
],
[columnConfigs],
);
const memoizedRows = useMemo(() => {
const renderSparklineCell = (valueField, column, entries) => {
let sparkData;
if (column.timeRatio) {
// Period ratio sparkline
sparkData = [];
for (let i = column.timeRatio; i < entries.length; i += 1) {
const prevData = entries[i - column.timeRatio][valueField];
if (prevData && prevData !== 0) {
sparkData.push(entries[i][valueField] / prevData);
} else {
sparkData.push(null);
}
}
} else {
sparkData = entries.map(d => d[valueField]);
}
} else {
sparkData = entries.map(d => d[valueField]);
}
return (
<Td
column={column.key}
key={column.key}
value={sparkData[sparkData.length - 1]}
>
return (
<SparklineCell
width={parseInt(column.width, 10) || 300}
height={parseInt(column.height, 10) || 50}
data={sparkData}
data-value={sparkData[sparkData.length - 1]}
ariaLabel={`spark-${valueField}`}
numberFormat={column.d3format}
yAxisBounds={column.yAxisBounds}
@ -161,146 +176,148 @@ class TimeTable extends React.PureComponent {
</div>
)}
/>
</Td>
);
}
);
};
renderValueCell(valueField, column, reversedEntries) {
const recent = reversedEntries[0][valueField];
let v;
let errorMsg;
if (column.colType === 'time') {
// Time lag ratio
const timeLag = column.timeLag || 0;
const totalLag = Object.keys(reversedEntries).length;
if (timeLag >= totalLag) {
errorMsg = `The time lag set at ${timeLag} is too large for the length of data at ${reversedEntries.length}. No data available.`;
} else {
v = reversedEntries[timeLag][valueField];
}
if (column.comparisonType === 'diff') {
v = recent - v;
} else if (column.comparisonType === 'perc') {
v = recent / v;
} else if (column.comparisonType === 'perc_change') {
v = recent / v - 1;
}
v = v || 0;
} else if (column.colType === 'contrib') {
// contribution to column total
v =
recent /
Object.keys(reversedEntries[0])
.map(k => (k !== 'time' ? reversedEntries[0][k] : null))
.reduce((a, b) => a + b);
} else if (column.colType === 'avg') {
// Average over the last {timeLag}
v =
reversedEntries
.map((k, i) => (i < column.timeLag ? k[valueField] : 0))
.reduce((a, b) => a + b) / column.timeLag;
}
const color = colorFromBounds(v, column.bounds);
return (
<Td
column={column.key}
key={column.key}
value={v}
style={
color && {
boxShadow: `inset 0px -2.5px 0px 0px ${color}`,
borderRight: '2px solid #fff',
}
const renderValueCell = (valueField, column, reversedEntries) => {
const recent = reversedEntries[0][valueField];
let v;
let errorMsg;
if (column.colType === 'time') {
// Time lag ratio
const timeLag = column.timeLag || 0;
const totalLag = Object.keys(reversedEntries).length;
if (timeLag >= totalLag) {
errorMsg = `The time lag set at ${timeLag} is too large for the length of data at ${reversedEntries.length}. No data available.`;
} else {
v = reversedEntries[timeLag][valueField];
}
>
{errorMsg ? (
<div>{errorMsg}</div>
) : (
<div style={{ color }}>
<FormattedNumber num={v} format={column.d3format} />
</div>
)}
</Td>
);
}
if (column.comparisonType === 'diff') {
v = recent - v;
} else if (column.comparisonType === 'perc') {
v = recent / v;
} else if (column.comparisonType === 'perc_change') {
v = recent / v - 1;
}
v = v || 0;
} else if (column.colType === 'contrib') {
// contribution to column total
v =
recent /
Object.keys(reversedEntries[0])
.map(k => (k !== 'time' ? reversedEntries[0][k] : null))
.reduce((a, b) => a + b);
} else if (column.colType === 'avg') {
// Average over the last {timeLag}
v =
reversedEntries
.map((k, i) => (i < column.timeLag ? k[valueField] : 0))
.reduce((a, b) => a + b) / column.timeLag;
}
renderRow(row, entries, reversedEntries) {
const { columnConfigs } = this.props;
const valueField = row.label || row.metric_name;
const leftCell = this.renderLeftCell(row);
const color = colorFromBounds(v, column.bounds);
return (
<Tr key={leftCell}>
<Td column="metric" data={leftCell}>
{leftCell}
</Td>
{columnConfigs.map(c =>
c.colType === 'spark'
? this.renderSparklineCell(valueField, c, entries)
: this.renderValueCell(valueField, c, reversedEntries),
)}
</Tr>
);
}
return (
<span
key={column.key}
data-value={v}
style={
color && {
boxShadow: `inset 0px -2.5px 0px 0px ${color}`,
borderRight: '2px solid #fff',
}
}
>
{errorMsg ? (
{ errorMsg }
) : (
<span style={{ color }}>
<FormattedNumber num={v} format={column.d3format} />
</span>
)}
</span>
);
};
render() {
const {
className,
height,
data,
columnConfigs,
rowType,
rows,
} = this.props;
const renderLeftCell = row => {
const context = { metric: row };
const fullUrl = url ? Mustache.render(url, context) : null;
if (rowType === 'column') {
const column = row;
if (fullUrl) {
return (
<a href={fullUrl} rel="noopener noreferrer" target="_blank">
{column.label}
</a>
);
}
return column.label;
}
return (
<MetricOption
metric={row}
url={fullUrl}
showFormula={false}
openInNewWindow
/>
);
};
const entries = Object.keys(data)
.sort()
.map(time => ({ ...data[time], time }));
const reversedEntries = entries.concat().reverse();
const defaultSort =
rowType === 'column' && columnConfigs.length
? {
column: columnConfigs[0].key,
direction: 'desc',
}
: false;
return rows.map(row => {
const valueField = row.label || row.metric_name;
const cellValues = columnConfigs.reduce((acc, columnConfig) => {
if (columnConfig.colType === 'spark') {
return {
...acc,
[columnConfig.key]: renderSparklineCell(
valueField,
columnConfig,
entries,
),
};
}
return {
...acc,
[columnConfig.key]: renderValueCell(
valueField,
columnConfig,
reversedEntries,
),
};
}, {});
return { ...row, ...cellValues, metric: renderLeftCell(row) };
});
}, [columnConfigs, data, rowType, rows, url]);
return (
<div className={`time-table ${className}`} style={{ height }}>
<Table
className="table table-no-hover"
defaultSort={defaultSort}
sortBy={defaultSort}
sortable={columnConfigs.map(c => c.key)}
>
<Thead>
<Th column="metric">Metric</Th>
{columnConfigs.map((c, i) => (
<Th
key={c.key}
column={c.key}
width={c.colType === 'spark' ? '1%' : null}
>
{c.label}{' '}
{c.tooltip && (
<InfoTooltipWithTrigger
tooltip={c.tooltip}
label={`tt-col-${i}`}
placement="top"
/>
)}
</Th>
))}
</Thead>
{rows.map(row => this.renderRow(row, entries, reversedEntries))}
</Table>
</div>
);
}
}
const defaultSort =
rowType === 'column' && columnConfigs.length
? [
{
id: columnConfigs[0].key,
desc: 'true',
},
]
: [];
return (
<TimeTableStyles className={`time-table ${className}`} height={height}>
<TableView
className="table-no-hover"
columns={memoizedColumns}
data={memoizedRows}
initialSortBy={defaultSort}
withPagination={false}
/>
</TimeTableStyles>
);
};
TimeTable.propTypes = propTypes;
TimeTable.defaultProps = defaultProps;