mirror of https://github.com/apache/superset.git
refactor: Remove usages of reactable from TimeTable (#11046)
* Use ListView instead of reactable * Refactor TimeTable * Spark column width fix
This commit is contained in:
parent
a879622e07
commit
f01b8a30f7
|
@ -17,6 +17,7 @@
|
||||||
* under the License.
|
* under the License.
|
||||||
*/
|
*/
|
||||||
import { t, styled } from '@superset-ui/core';
|
import { t, styled } from '@superset-ui/core';
|
||||||
|
import { css } from '@emotion/core';
|
||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { Alert } from 'react-bootstrap';
|
import { Alert } from 'react-bootstrap';
|
||||||
import { Empty } from 'src/common/components';
|
import { Empty } from 'src/common/components';
|
||||||
|
@ -37,7 +38,11 @@ import {
|
||||||
} from './types';
|
} from './types';
|
||||||
import { ListViewError, useListViewState } from './utils';
|
import { ListViewError, useListViewState } from './utils';
|
||||||
|
|
||||||
const ListViewStyles = styled.div`
|
interface ListViewStylesProps {
|
||||||
|
fullHeight: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ListViewStyles = styled.div<ListViewStylesProps>`
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
|
||||||
.superset-list-view {
|
.superset-list-view {
|
||||||
|
@ -48,8 +53,12 @@ const ListViewStyles = styled.div`
|
||||||
padding-bottom: 48px;
|
padding-bottom: 48px;
|
||||||
|
|
||||||
.body {
|
.body {
|
||||||
overflow: scroll;
|
${({ fullHeight }) =>
|
||||||
max-height: 64vh;
|
!fullHeight &&
|
||||||
|
css`
|
||||||
|
overflow: scroll;
|
||||||
|
max-height: 64vh;
|
||||||
|
`};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -202,6 +211,9 @@ export interface ListViewProps<T extends object = any> {
|
||||||
renderCard?: (row: T & { loading: boolean }) => React.ReactNode;
|
renderCard?: (row: T & { loading: boolean }) => React.ReactNode;
|
||||||
cardSortSelectOptions?: Array<CardSortSelectOption>;
|
cardSortSelectOptions?: Array<CardSortSelectOption>;
|
||||||
defaultViewMode?: ViewModeType;
|
defaultViewMode?: ViewModeType;
|
||||||
|
sticky?: boolean;
|
||||||
|
fullHeight?: boolean;
|
||||||
|
manualSortBy?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
function ListView<T extends object = any>({
|
function ListView<T extends object = any>({
|
||||||
|
@ -221,6 +233,9 @@ function ListView<T extends object = any>({
|
||||||
renderCard,
|
renderCard,
|
||||||
cardSortSelectOptions,
|
cardSortSelectOptions,
|
||||||
defaultViewMode = 'card',
|
defaultViewMode = 'card',
|
||||||
|
sticky = true,
|
||||||
|
fullHeight = false,
|
||||||
|
manualSortBy = true,
|
||||||
}: ListViewProps<T>) {
|
}: ListViewProps<T>) {
|
||||||
const {
|
const {
|
||||||
getTableProps,
|
getTableProps,
|
||||||
|
@ -244,8 +259,10 @@ function ListView<T extends object = any>({
|
||||||
initialPageSize,
|
initialPageSize,
|
||||||
initialSort,
|
initialSort,
|
||||||
initialFilters: filters,
|
initialFilters: filters,
|
||||||
|
manualSortBy,
|
||||||
});
|
});
|
||||||
const filterable = Boolean(filters.length);
|
const filterable = Boolean(filters.length);
|
||||||
|
const withPagination = Boolean(count);
|
||||||
if (filterable) {
|
if (filterable) {
|
||||||
const columnAccessors = columns.reduce(
|
const columnAccessors = columns.reduce(
|
||||||
(acc, col) => ({ ...acc, [col.accessor || col.id]: true }),
|
(acc, col) => ({ ...acc, [col.accessor || col.id]: true }),
|
||||||
|
@ -266,7 +283,7 @@ function ListView<T extends object = any>({
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ListViewStyles>
|
<ListViewStyles fullHeight={fullHeight}>
|
||||||
<div className={`superset-list-view ${className}`}>
|
<div className={`superset-list-view ${className}`}>
|
||||||
<div className="header">
|
<div className="header">
|
||||||
{cardViewEnabled && (
|
{cardViewEnabled && (
|
||||||
|
@ -350,6 +367,7 @@ function ListView<T extends object = any>({
|
||||||
rows={rows}
|
rows={rows}
|
||||||
columns={columns}
|
columns={columns}
|
||||||
loading={loading}
|
loading={loading}
|
||||||
|
sticky={sticky}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{!loading && rows.length === 0 && (
|
{!loading && rows.length === 0 && (
|
||||||
|
@ -360,23 +378,25 @@ function ListView<T extends object = any>({
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="pagination-container">
|
{withPagination && (
|
||||||
<Pagination
|
<div className="pagination-container">
|
||||||
totalPages={pageCount || 0}
|
<Pagination
|
||||||
currentPage={pageCount ? pageIndex + 1 : 0}
|
totalPages={pageCount || 0}
|
||||||
onChange={(p: number) => gotoPage(p - 1)}
|
currentPage={pageCount ? pageIndex + 1 : 0}
|
||||||
hideFirstAndLastPageLinks
|
onChange={(p: number) => gotoPage(p - 1)}
|
||||||
/>
|
hideFirstAndLastPageLinks
|
||||||
<div className="row-count-container">
|
/>
|
||||||
{!loading &&
|
<div className="row-count-container">
|
||||||
t(
|
{!loading &&
|
||||||
'%s-%s of %s',
|
t(
|
||||||
pageSize * pageIndex + (rows.length && 1),
|
'%s-%s of %s',
|
||||||
pageSize * pageIndex + rows.length,
|
pageSize * pageIndex + (rows.length && 1),
|
||||||
count,
|
pageSize * pageIndex + rows.length,
|
||||||
)}
|
count,
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
)}
|
||||||
</ListViewStyles>
|
</ListViewStyles>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -30,14 +30,19 @@ interface TableCollectionProps {
|
||||||
rows: TableInstance['rows'];
|
rows: TableInstance['rows'];
|
||||||
columns: TableInstance['column'][];
|
columns: TableInstance['column'][];
|
||||||
loading: boolean;
|
loading: boolean;
|
||||||
|
sticky: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const Table = styled.table`
|
interface TableProps {
|
||||||
|
sticky: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Table = styled.table<TableProps>`
|
||||||
border-collapse: separate;
|
border-collapse: separate;
|
||||||
|
|
||||||
th {
|
th {
|
||||||
background: ${({ theme }) => theme.colors.grayscale.light5};
|
background: ${({ theme }) => theme.colors.grayscale.light5};
|
||||||
position: sticky;
|
position: ${({ sticky }) => sticky && 'sticky'};
|
||||||
top: 0;
|
top: 0;
|
||||||
|
|
||||||
&:first-of-type {
|
&:first-of-type {
|
||||||
|
@ -199,9 +204,10 @@ export default function TableCollection({
|
||||||
columns,
|
columns,
|
||||||
rows,
|
rows,
|
||||||
loading,
|
loading,
|
||||||
|
sticky,
|
||||||
}: TableCollectionProps) {
|
}: TableCollectionProps) {
|
||||||
return (
|
return (
|
||||||
<Table {...getTableProps()} className="table table-hover">
|
<Table {...getTableProps()} sticky={sticky} className="table table-hover">
|
||||||
<thead>
|
<thead>
|
||||||
{headerGroups.map(headerGroup => (
|
{headerGroups.map(headerGroup => (
|
||||||
<tr {...headerGroup.getHeaderGroupProps()}>
|
<tr {...headerGroup.getHeaderGroupProps()}>
|
||||||
|
|
|
@ -110,6 +110,7 @@ interface UseListViewConfig {
|
||||||
Header: (conf: any) => React.ReactNode;
|
Header: (conf: any) => React.ReactNode;
|
||||||
Cell: (conf: any) => React.ReactNode;
|
Cell: (conf: any) => React.ReactNode;
|
||||||
};
|
};
|
||||||
|
manualSortBy: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useListViewState({
|
export function useListViewState({
|
||||||
|
@ -122,6 +123,7 @@ export function useListViewState({
|
||||||
initialSort = [],
|
initialSort = [],
|
||||||
bulkSelectMode = false,
|
bulkSelectMode = false,
|
||||||
bulkSelectColumnConfig,
|
bulkSelectColumnConfig,
|
||||||
|
manualSortBy,
|
||||||
}: UseListViewConfig) {
|
}: UseListViewConfig) {
|
||||||
const [query, setQuery] = useQueryParams({
|
const [query, setQuery] = useQueryParams({
|
||||||
filters: JsonParam,
|
filters: JsonParam,
|
||||||
|
@ -177,9 +179,9 @@ export function useListViewState({
|
||||||
initialState,
|
initialState,
|
||||||
manualFilters: true,
|
manualFilters: true,
|
||||||
manualPagination: true,
|
manualPagination: true,
|
||||||
manualSortBy: true,
|
|
||||||
autoResetFilters: false,
|
autoResetFilters: false,
|
||||||
pageCount: Math.ceil(count / initialPageSize),
|
pageCount: Math.ceil(count / initialPageSize),
|
||||||
|
manualSortBy,
|
||||||
},
|
},
|
||||||
useFilters,
|
useFilters,
|
||||||
useSortBy,
|
useSortBy,
|
||||||
|
|
|
@ -20,14 +20,14 @@ import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import Mustache from 'mustache';
|
import Mustache from 'mustache';
|
||||||
import { scaleLinear } from 'd3-scale';
|
import { scaleLinear } from 'd3-scale';
|
||||||
import { Table, Thead, Th, Tr, Td } from 'reactable-arc';
|
|
||||||
import { formatNumber, formatTime } from '@superset-ui/core';
|
import { formatNumber, formatTime } from '@superset-ui/core';
|
||||||
import {
|
import {
|
||||||
InfoTooltipWithTrigger,
|
InfoTooltipWithTrigger,
|
||||||
MetricOption,
|
MetricOption,
|
||||||
} from '@superset-ui/chart-controls';
|
} from '@superset-ui/chart-controls';
|
||||||
import moment from 'moment';
|
import moment from 'moment';
|
||||||
|
import ListView from 'src/components/ListView';
|
||||||
|
import { memoize } from 'lodash-es';
|
||||||
import FormattedNumber from './FormattedNumber';
|
import FormattedNumber from './FormattedNumber';
|
||||||
import SparklineCell from './SparklineCell';
|
import SparklineCell from './SparklineCell';
|
||||||
import './TimeTable.less';
|
import './TimeTable.less';
|
||||||
|
@ -90,6 +90,68 @@ const defaultProps = {
|
||||||
};
|
};
|
||||||
|
|
||||||
class TimeTable extends React.PureComponent {
|
class TimeTable extends React.PureComponent {
|
||||||
|
memoizedColumns = memoize(() => [
|
||||||
|
{ accessor: 'metric', Header: 'Metric' },
|
||||||
|
...this.props.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;
|
||||||
|
},
|
||||||
|
})),
|
||||||
|
]);
|
||||||
|
|
||||||
|
memoizedRows = memoize(() => {
|
||||||
|
const entries = Object.keys(this.props.data)
|
||||||
|
.sort()
|
||||||
|
.map(time => ({ ...this.props.data[time], time }));
|
||||||
|
const reversedEntries = entries.concat().reverse();
|
||||||
|
|
||||||
|
return this.props.rows.map(row => {
|
||||||
|
const valueField = row.label || row.metric_name;
|
||||||
|
const cellValues = this.props.columnConfigs.reduce(
|
||||||
|
(acc, columnConfig) => {
|
||||||
|
if (columnConfig.colType === 'spark') {
|
||||||
|
return {
|
||||||
|
...acc,
|
||||||
|
[columnConfig.key]: this.renderSparklineCell(
|
||||||
|
valueField,
|
||||||
|
columnConfig,
|
||||||
|
entries,
|
||||||
|
),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
...acc,
|
||||||
|
[columnConfig.key]: this.renderValueCell(
|
||||||
|
valueField,
|
||||||
|
columnConfig,
|
||||||
|
reversedEntries,
|
||||||
|
),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
{},
|
||||||
|
);
|
||||||
|
return { ...row, ...cellValues, metric: this.renderLeftCell(row) };
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
initialSort = [{ id: 'metric', desc: false }];
|
||||||
|
|
||||||
renderLeftCell(row) {
|
renderLeftCell(row) {
|
||||||
const { rowType, url } = this.props;
|
const { rowType, url } = this.props;
|
||||||
const context = { metric: row };
|
const context = { metric: row };
|
||||||
|
@ -107,10 +169,9 @@ class TimeTable extends React.PureComponent {
|
||||||
return column.label;
|
return column.label;
|
||||||
}
|
}
|
||||||
|
|
||||||
const metric = row;
|
|
||||||
return (
|
return (
|
||||||
<MetricOption
|
<MetricOption
|
||||||
metric={metric}
|
metric={row}
|
||||||
url={fullUrl}
|
url={fullUrl}
|
||||||
showFormula={false}
|
showFormula={false}
|
||||||
openInNewWindow
|
openInNewWindow
|
||||||
|
@ -136,32 +197,27 @@ class TimeTable extends React.PureComponent {
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Td
|
<SparklineCell
|
||||||
column={column.key}
|
width={parseInt(column.width, 10) || 300}
|
||||||
key={column.key}
|
height={parseInt(column.height, 10) || 50}
|
||||||
value={sparkData[sparkData.length - 1]}
|
data={sparkData}
|
||||||
>
|
data-value={sparkData[sparkData.length - 1]}
|
||||||
<SparklineCell
|
ariaLabel={`spark-${valueField}`}
|
||||||
width={parseInt(column.width, 10) || 300}
|
numberFormat={column.d3format}
|
||||||
height={parseInt(column.height, 10) || 50}
|
yAxisBounds={column.yAxisBounds}
|
||||||
data={sparkData}
|
showYAxis={column.showYAxis}
|
||||||
ariaLabel={`spark-${valueField}`}
|
renderTooltip={({ index }) => (
|
||||||
numberFormat={column.d3format}
|
<div>
|
||||||
yAxisBounds={column.yAxisBounds}
|
<strong>{formatNumber(column.d3format, sparkData[index])}</strong>
|
||||||
showYAxis={column.showYAxis}
|
|
||||||
renderTooltip={({ index }) => (
|
|
||||||
<div>
|
<div>
|
||||||
<strong>{formatNumber(column.d3format, sparkData[index])}</strong>
|
{formatTime(
|
||||||
<div>
|
column.dateFormat,
|
||||||
{formatTime(
|
moment.utc(entries[index].time).toDate(),
|
||||||
column.dateFormat,
|
)}
|
||||||
moment.utc(entries[index].time).toDate(),
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
</div>
|
||||||
/>
|
)}
|
||||||
</Td>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -204,10 +260,9 @@ class TimeTable extends React.PureComponent {
|
||||||
const color = colorFromBounds(v, column.bounds);
|
const color = colorFromBounds(v, column.bounds);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Td
|
<span
|
||||||
column={column.key}
|
|
||||||
key={column.key}
|
key={column.key}
|
||||||
value={v}
|
data-value={v}
|
||||||
style={
|
style={
|
||||||
color && {
|
color && {
|
||||||
boxShadow: `inset 0px -2.5px 0px 0px ${color}`,
|
boxShadow: `inset 0px -2.5px 0px 0px ${color}`,
|
||||||
|
@ -216,87 +271,34 @@ class TimeTable extends React.PureComponent {
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{errorMsg ? (
|
{errorMsg ? (
|
||||||
<div>{errorMsg}</div>
|
{ errorMsg }
|
||||||
) : (
|
) : (
|
||||||
<div style={{ color }}>
|
<span style={{ color }}>
|
||||||
<FormattedNumber num={v} format={column.d3format} />
|
<FormattedNumber num={v} format={column.d3format} />
|
||||||
</div>
|
</span>
|
||||||
)}
|
)}
|
||||||
</Td>
|
</span>
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
renderRow(row, entries, reversedEntries) {
|
|
||||||
const { columnConfigs } = this.props;
|
|
||||||
const valueField = row.label || row.metric_name;
|
|
||||||
const leftCell = this.renderLeftCell(row);
|
|
||||||
|
|
||||||
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>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const {
|
const { className, height } = this.props;
|
||||||
className,
|
|
||||||
height,
|
|
||||||
data,
|
|
||||||
columnConfigs,
|
|
||||||
rowType,
|
|
||||||
rows,
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
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 (
|
return (
|
||||||
<div className={`time-table ${className}`} style={{ height }}>
|
<div className={`time-table ${className}`} style={{ height }}>
|
||||||
<Table
|
<ListView
|
||||||
className="table table-no-hover"
|
columns={this.memoizedColumns()}
|
||||||
defaultSort={defaultSort}
|
data={this.memoizedRows()}
|
||||||
sortBy={defaultSort}
|
count={0}
|
||||||
sortable={columnConfigs.map(c => c.key)}
|
// we don't use pagination
|
||||||
>
|
pageSize={0}
|
||||||
<Thead>
|
initialSort={this.initialSort}
|
||||||
<Th column="metric">Metric</Th>
|
fetchData={() => {}}
|
||||||
{columnConfigs.map((c, i) => (
|
loading={false}
|
||||||
<Th
|
sticky={false}
|
||||||
key={c.key}
|
fullHeight
|
||||||
column={c.key}
|
manualSortBy={false}
|
||||||
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>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue