feat(table): enable table filter and better typing (#344)

This commit is contained in:
Jesse Yang 2020-04-20 18:03:10 -07:00 committed by Yongjie Zhao
parent aa2cdcaad5
commit 9b009e7198
23 changed files with 283 additions and 144 deletions

View File

@ -62,7 +62,7 @@
"@types/enzyme": "^3.10.3",
"@types/jest": "^25.1.1",
"@types/jsdom": "^12.2.4",
"@types/react-test-renderer": "^16.9.0",
"@types/react-test-renderer": "^16.9.2",
"enzyme": "^3.10.0",
"enzyme-adapter-react-16": "^1.15.1",
"enzyme-to-json": "^3.4.3",
@ -73,9 +73,9 @@
"jest-mock-console": "^1.0.0",
"lerna": "^3.15.0",
"lint-staged": "^10.0.3",
"react": "^16.9.0",
"react-dom": "^16.9.0",
"react-test-renderer": "^16.9.0"
"react": "^16.13.1",
"react-dom": "^16.13.1",
"react-test-renderer": "^16.13.1"
},
"engines": {
"node": ">=10.10.0",

View File

@ -26,12 +26,12 @@
"access": "public"
},
"dependencies": {
"@types/react": "^16.7.17",
"@types/react": "^16.9.34",
"@vx/responsive": "^0.0.195",
"csstype": "^2.6.4"
},
"peerDependencies": {
"@superset-ui/core": "^0.12.0",
"react": "^16.7.17"
"react": "^16.13.1"
}
}

View File

@ -27,7 +27,7 @@
"access": "public"
},
"dependencies": {
"@types/react": "^16.7.17",
"@types/react": "^16.9.34",
"@types/react-loadable": "^5.4.2",
"@vx/responsive": "^0.0.195",
"prop-types": "^15.6.2",
@ -45,6 +45,6 @@
"@superset-ui/core": "^0.12.0",
"@superset-ui/dimension": "^0.12.0",
"@superset-ui/query": "^0.12.0",
"react": "^16.7.17"
"react": "^16.13.1"
}
}

View File

@ -9,7 +9,7 @@ import {
import { QueryFormData, Datasource } from '@superset-ui/query';
import getChartBuildQueryRegistry from '../registries/ChartBuildQueryRegistrySingleton';
import getChartMetadataRegistry from '../registries/ChartMetadataRegistrySingleton';
import { QueryData } from '../models/ChartProps';
import { QueryData } from '../types/QueryResponse';
import { AnnotationLayerMetadata } from '../types/Annotation';
import { PlainObject } from '../types/Base';
@ -94,7 +94,10 @@ export default class ChartClient {
},
...options,
} as RequestConfig)
.then(response => response.json as Json);
.then(response => {
// let's assume response.json always has the shape of QueryData
return response.json as QueryData;
});
}
return Promise.reject(new Error(`Unknown chart type: ${visType}`));

View File

@ -3,7 +3,7 @@ import React, { ReactNode } from 'react';
import { SupersetClientInterface, RequestConfig } from '@superset-ui/connection';
import { QueryFormData, Datasource } from '@superset-ui/query';
import ChartClient, { SliceIdAndOrFormData } from '../clients/ChartClient';
import { QueryData } from '../models/ChartProps';
import { QueryData } from '../types/QueryResponse';
interface Payload {
formData: Partial<QueryFormData>;

View File

@ -16,3 +16,4 @@ export { default as getChartTransformPropsRegistry } from './registries/ChartTra
export { default as ChartDataProvider } from './components/ChartDataProvider';
export * from './types/TransformFunction';
export * from './types/QueryResponse';

View File

@ -1,30 +1,32 @@
import { createSelector } from 'reselect';
import { convertKeysToCamelCase } from '@superset-ui/core';
import { Datasource } from '@superset-ui/query';
import { HandlerFunction, PlainObject } from '../types/Base';
import { QueryData, DataRecordFilters } from '../types/QueryResponse';
// TODO: more specific typing for these fields of ChartProps
type AnnotationData = PlainObject;
type CamelCaseDatasource = PlainObject;
type SnakeCaseDatasource = PlainObject;
type CamelCaseFormData = PlainObject;
type SnakeCaseFormData = PlainObject;
export type QueryData = PlainObject;
/** Initial values for the visualizations, currently used by only filter_box and table */
type InitialValues = PlainObject;
type RawFormData = CamelCaseFormData | SnakeCaseFormData;
type ChartPropsSelector = (c: ChartPropsConfig) => ChartProps;
/** Optional field for event handlers, renderers */
type Hooks = {
/** handle adding filters */
onAddFilter?: HandlerFunction;
/** handle errors */
/**
* sync active filters between chart and dashboard, "add" actually
* also handles "change" and "remove".
*/
onAddFilter?: (newFilters: DataRecordFilters, merge?: boolean) => void;
/** handle errors */
onError?: HandlerFunction;
/** use the vis as control to update state */
setControlValue?: HandlerFunction;
/** handle tooltip */
setTooltip?: HandlerFunction;
[key: string]: any;
};
} & PlainObject;
/**
* Preferred format for ChartProps config
@ -37,9 +39,9 @@ export interface ChartPropsConfig {
* Formerly called "filters", which was misleading because it is actually
* initial values of the filter_box and table vis
*/
initialValues?: InitialValues;
initialValues?: DataRecordFilters;
/** Main configuration of the chart */
formData?: SnakeCaseFormData;
formData?: RawFormData;
/** Chart height */
height?: number;
/** Programmatic overrides such as event handlers, renderers */
@ -53,22 +55,20 @@ export interface ChartPropsConfig {
const DEFAULT_WIDTH = 800;
const DEFAULT_HEIGHT = 600;
export default class ChartProps<
FormDataType extends CamelCaseFormData | SnakeCaseFormData = CamelCaseFormData
> {
export default class ChartProps {
static createSelector: () => ChartPropsSelector;
annotationData: AnnotationData;
datasource: CamelCaseDatasource;
datasource: Datasource;
rawDatasource: SnakeCaseDatasource;
initialValues: InitialValues;
initialValues: DataRecordFilters;
formData: CamelCaseFormData;
rawFormData: SnakeCaseFormData | CamelCaseFormData;
rawFormData: RawFormData;
height: number;
@ -82,7 +82,7 @@ export default class ChartProps<
const {
annotationData = {},
datasource = {},
formData = {} as FormDataType,
formData = {},
hooks = {},
initialValues = {},
queryData = {},

View File

@ -0,0 +1,18 @@
/**
* Types for query response
*/
import { PlainObject } from './Base';
export type DataRecordValue = number | string | boolean | Date | null;
export interface DataRecord {
[key: string]: DataRecordValue;
}
// data record value filters from FilterBox
export interface DataRecordFilters {
[key: string]: DataRecordValue[];
}
// the response json from query API
export type QueryData = PlainObject;

View File

@ -5,10 +5,10 @@ const RAW_FORM_DATA = {
};
const RAW_DATASOURCE = {
another_field: 2,
column_formats: { test: '%s' },
};
const QUERY_DATA = {};
const QUERY_DATA = { data: {} };
describe('ChartProps', () => {
it('exists', () => {
@ -33,7 +33,7 @@ describe('ChartProps', () => {
queryData: QUERY_DATA,
});
expect(props.formData.someField).toEqual(1);
expect(props.datasource.anotherField).toEqual(2);
expect(props.datasource.columnFormats).toEqual(RAW_DATASOURCE.column_formats);
expect(props.rawFormData).toEqual(RAW_FORM_DATA);
expect(props.rawDatasource).toEqual(RAW_DATASOURCE);
});

View File

@ -72,7 +72,7 @@
"core-js": "3.6.5",
"gh-pages": "^2.2.0",
"jquery": "^3.4.1",
"react": "^16.6.0",
"react": "^16.13.1",
"storybook-addon-jsx": "^7.1.0"
},
"devDependencies": {

View File

@ -190,7 +190,7 @@
"fetch_values_predicate": null,
"template_params": null
},
"initialValues": {},
"activeFilters": {},
"formData": {
"datasource": "3__table",
"viz_type": "table",
@ -300,7 +300,7 @@
"table_timestamp_format": "%Y-%m-%d %H:%M:%S",
"page_length": 0,
"include_search": true,
"table_filter": false,
"table_filter": true,
"align_pn": false,
"color_pn": true
},
@ -419,7 +419,7 @@
"table_timestamp_format": "%Y-%m-%d %H:%M:%S",
"page_length": 0,
"include_search": true,
"table_filter": false,
"table_filter": true,
"align_pn": false,
"color_pn": true,
"where": "",

View File

@ -10,8 +10,15 @@ export enum DatasourceType {
export interface Datasource {
id: number;
name: string;
description?: string;
type: DatasourceType;
columns: Column[];
metrics: QueryObjectMetric[];
description?: string;
// key is column names (labels)
columnFormats?: {
[key: string]: string;
};
verboseMap?: {
[key: string]: string;
};
}

View File

@ -22,6 +22,8 @@ export type QueryObjectFilterClause = {
export type QueryObjectMetric = {
label: string;
metric_name?: string;
d3format?: string;
} & Partial<AdhocMetric>;
export type QueryObjectExtras = Partial<{

View File

@ -19,25 +19,56 @@
import * as color from 'd3-color';
import { getNumberFormatter, NumberFormats } from '@superset-ui/number-format';
import { ChartProps } from '@superset-ui/chart';
import getTimeFormatterForGranularity from '../utils/getTimeFormatterForGranularity';
import getTimeFormatterForGranularity, {
TimeGranularity,
} from '../utils/getTimeFormatterForGranularity';
const TIME_COLUMN = '__timestamp';
const formatPercentChange = getNumberFormatter(NumberFormats.PERCENT_SIGNED_1_POINT);
export interface DatasourceMetric {
label: string;
// eslint-disable-next-line camelcase
metric_name?: string;
d3format?: string;
}
// we trust both the x (time) and y (big number) to be numeric
type BigNumberDatum = {
[TIME_COLUMN]: number;
export interface BigNumberDatum {
[key: string]: number | null;
}
export type BigNumberFormData = {
colorPicker?: {
r: number;
g: number;
b: number;
};
metric?:
| {
label: string;
}
| string;
compareLag?: string | number;
yAxisFormat?: string;
timeGrainSqla?: TimeGranularity;
};
export default function transformProps(chartProps: ChartProps) {
const { width, height, formData, queryData } = chartProps;
export type BignumberChartProps = ChartProps & {
formData: BigNumberFormData;
queryData: ChartProps['queryData'] & {
data?: BigNumberDatum[];
};
};
export default function transformProps(chartProps: BignumberChartProps) {
const { width, height, queryData, formData } = chartProps;
const {
colorPicker,
compareLag: compareLagInput,
compareLag: compareLag_,
compareSuffix = '',
headerFontSize,
metric,
metric = 'value',
showTrendLine,
startYAxisAtZero,
subheader = '',
@ -47,9 +78,9 @@ export default function transformProps(chartProps: ChartProps) {
timeRangeFixed = false,
} = formData;
let { yAxisFormat } = formData;
const { data, from_dttm: fromDatetime, to_dttm: toDatetime } = queryData;
const metricName = metric?.label ? metric.label : metric;
const compareLag = Number(compareLagInput) || 0;
const { data = [], from_dttm: fromDatetime, to_dttm: toDatetime } = queryData;
const metricName = typeof metric === 'string' ? metric : metric.label;
const compareLag = Number(compareLag_) || 0;
const supportTrendLine = vizType === 'big_number';
const supportAndShowTrendLine = supportTrendLine && showTrendLine;
let formattedSubheader = subheader;
@ -68,7 +99,8 @@ export default function transformProps(chartProps: ChartProps) {
if (data.length > 0) {
const sortedData = (data as BigNumberDatum[])
.map(d => ({ x: d[TIME_COLUMN], y: d[metricName] }))
.sort((a, b) => b.x - a.x); // sort in time descending order
// sort in time descending order
.sort((a, b) => (a.x !== null && b.x !== null ? b.x - a.x : 0));
bigNumber = sortedData[0].y;
if (bigNumber === null) {
@ -103,14 +135,11 @@ export default function transformProps(chartProps: ChartProps) {
}
if (!yAxisFormat && chartProps.datasource && chartProps.datasource.metrics) {
chartProps.datasource.metrics.forEach(
// eslint-disable-next-line camelcase
(metricEntry: { metric_name?: string; d3format: string }) => {
if (metricEntry.metric_name === metric && metricEntry.d3format) {
yAxisFormat = metricEntry.d3format;
}
},
);
chartProps.datasource.metrics.forEach((metricEntry: DatasourceMetric) => {
if (metricEntry.metric_name === metric && metricEntry.d3format) {
yAxisFormat = metricEntry.d3format;
}
});
}
const formatNumber = getNumberFormatter(yAxisFormat);

View File

@ -46,25 +46,10 @@ const formats = {
'P1W/1970-01-04T00:00:00Z': MONDAY_BASED_WEEK, // 'week_ending_sunday'
};
type TimeGranularity =
| 'date'
| 'PT1S'
| 'PT1M'
| 'PT5M'
| 'PT10M'
| 'PT15M'
| 'PT0.5H'
| 'PT1H'
| 'P1D'
| 'P1W'
| 'P0.25Y'
| 'P1Y'
| '1969-12-28T00:00:00Z/P1W'
| '1969-12-29T00:00:00Z/P1W'
| 'P1W/1970-01-03T00:00:00Z';
export type TimeGranularity = keyof typeof formats;
export default function getTimeFormatterForGranularity(granularity: TimeGranularity) {
return granularity in formats
export default function getTimeFormatterForGranularity(granularity: TimeGranularity | undefined) {
return granularity && granularity in formats
? getTimeFormatter(formats[granularity])
: smartDateVerboseFormatter;
}

View File

@ -16,7 +16,12 @@
* specific language governing permissions and limitations
* under the License.
*/
import transformProps from '../BigNumber/transformProps';
import { DatasourceType } from '@superset-ui/query';
import transformProps, {
BignumberChartProps,
BigNumberDatum,
} from '../src/BigNumber/transformProps';
import { TimeGranularity } from '../src/utils/getTimeFormatterForGranularity';
const formData = {
metric: 'value',
@ -27,18 +32,27 @@ const formData = {
a: 1,
},
compareLag: 1,
timeGrainSqla: 'P0.25Y',
timeGrainSqla: 'P0.25Y' as TimeGranularity,
compareSuffix: 'over last quarter',
vizType: 'big_number',
yAxisFormat: '.3s',
};
function generateProps(data: object[], extraFormData = {}, extraQueryData = {}) {
function generateProps(
data: BigNumberDatum[],
extraFormData = {},
extraQueryData = {},
): BignumberChartProps {
return {
width: 200,
height: 500,
annotationData: {},
datasource: {
id: 0,
name: '',
type: DatasourceType.Table,
columns: [],
metrics: [],
columnFormats: {},
verboseMap: {},
},
@ -94,8 +108,10 @@ describe('BigNumber', () => {
const propsWithDatasource = {
...props,
datasource: {
...props.datasource,
metrics: [
{
label: 'value',
metric_name: 'value',
d3format: '.2f',
},

View File

@ -42,7 +42,7 @@
"@superset-ui/time-format": "^0.12.0",
"@superset-ui/translation": "^0.12.0",
"jquery": "^3.4.1",
"react": "^16.8.0",
"react-dom": "^16.8.0"
"react": "^16.13.1",
"react-dom": "^16.13.1"
}
}

View File

@ -20,6 +20,7 @@ import { t } from '@superset-ui/translation';
import React, { useEffect, createRef } from 'react';
import ReactDOMServer from 'react-dom/server';
import { formatNumber, NumberFormats } from '@superset-ui/number-format';
import { DataRecordValue } from '@superset-ui/chart';
import { getTimeFormatter } from '@superset-ui/time-format';
import { filterXSS } from 'xss';
@ -41,6 +42,10 @@ if (!dt.$) {
const { PERCENT_3_POINT } = NumberFormats;
const isProbablyHTML = (text: string) => /<[^>]+>/.test(text);
function isTimeColumn(key: string) {
return key === '__timestamp';
}
export default function ReactDataTable(props: DataTableProps) {
const {
data,
@ -54,23 +59,19 @@ export default function ReactDataTable(props: DataTableProps) {
percentMetrics,
showCellBars = true,
tableTimestampFormat,
// orderDesc,
// TODO: add back the broken dashboard filters feature
// filters = {},
// onAddFilter = NOOP,
// onRemoveFilter = NOOP,
// tableFilter,
// timeseriesLimitMetric,
emitFilter = false,
onChangeFilter = () => {},
filters = {},
} = props;
const formatTimestamp = getTimeFormatter(tableTimestampFormat);
const metrics = (aggMetrics || [])
.concat(percentMetrics || [])
// actual records must be of numeric types as well
.filter(m => data[0] && typeof data[0][m] === 'number');
.filter(m => data.length > 0 && typeof data[0][m] === 'number');
// check whethere a key is a metric
const metricsSet = new Set(aggMetrics);
const aggMetricsSet = new Set(aggMetrics);
const percentMetricsSet = new Set(percentMetrics);
// collect min/max for rendering bars
@ -109,8 +110,8 @@ export default function ReactDataTable(props: DataTableProps) {
/**
* Format text for cell value
*/
function cellText(key: string, format: string | undefined, val: any) {
if (key === '__timestamp') {
function cellText(key: string, format: string | undefined, val: DataRecordValue) {
if (isTimeColumn(key)) {
let value = val;
if (typeof val === 'string') {
// force UTC time zone if is an ISO timestamp without timezone
@ -118,7 +119,7 @@ export default function ReactDataTable(props: DataTableProps) {
value = val.match(/T(\d{2}:){2}\d{2}$/) ? `${val}Z` : val;
value = new Date(value);
}
return formatTimestamp(value) as string;
return formatTimestamp(value as Date | number | null) as string;
}
if (typeof val === 'string') {
return filterXSS(val, { stripIgnoreTag: true });
@ -127,11 +128,14 @@ export default function ReactDataTable(props: DataTableProps) {
// in case percent metric can specify percent format in the future
return formatNumber(format || PERCENT_3_POINT, val as number);
}
if (metricsSet.has(key)) {
if (aggMetricsSet.has(key)) {
// default format '' will return human readable numbers (e.g. 50M, 33k)
return formatNumber(format, val as number);
}
return String(val);
if (val === null) {
return 'N/A';
}
return val.toString();
}
/**
@ -162,6 +166,15 @@ export default function ReactDataTable(props: DataTableProps) {
);
}
function isFilterColumn(key: string) {
// anything that is not a metric column is a filter column
return !(aggMetricsSet.has(key) || percentMetricsSet.has(key));
}
function isActiveFilterValue(key: string, val: DataRecordValue) {
return filters[key]?.includes(val);
}
const options = {
aaSorting: [], // initial sorting order, reset to [] to use backend ordering
autoWidth: false,
@ -191,6 +204,19 @@ export default function ReactDataTable(props: DataTableProps) {
useEffect(() => {
const $root = $(rootElem.current as HTMLElement);
const dataTable = $root.find('table').DataTable(options);
const CSS_FILTER_ACTIVE = 'dt-is-active-filter';
function toggleFilter(key: string, val: DataRecordValue) {
const cellSelector = `td[data-key="${key}"][data-sort="${val}"]`;
if (isActiveFilterValue(key, val)) {
filters[key] = filters[key].filter((x: DataRecordValue) => x !== val);
$root.find(cellSelector).removeClass(CSS_FILTER_ACTIVE);
} else {
filters[key] = [...(filters[key] || []), val];
$root.find(cellSelector).addClass(CSS_FILTER_ACTIVE);
}
onChangeFilter({ ...filters });
}
// adjust table height
const scrollHeadHeight = $root.find('.dataTables_scrollHead').height() || 0;
@ -198,8 +224,17 @@ export default function ReactDataTable(props: DataTableProps) {
const searchBarHeight =
$root.find('.dataTables_length,.dataTables_filter').closest('.row').height() || 0;
const scrollBodyHeight = viewportHeight - scrollHeadHeight - paginationHeight - searchBarHeight;
$root.find('.dataTables_scrollBody').css('max-height', scrollBodyHeight);
if (emitFilter) {
$root.find('tbody').on('click', 'td.dt-is-filter', function onClickCell(this: HTMLElement) {
const { row, column } = dataTable.cell(this).index();
const { key } = columns[column];
toggleFilter(key, data[row][key]);
});
}
return () => {
// there may be weird lifecycle issues, so put destroy in try/catch
try {
@ -234,21 +269,31 @@ export default function ReactDataTable(props: DataTableProps) {
>
{columns.map(({ key, format }) => {
const val = record[key];
const keyIsMetric = metricsSet.has(key);
const keyIsAggMetric = aggMetricsSet.has(key);
const text = cellText(key, format, val);
const isHtml = !keyIsMetric && isProbablyHTML(text);
const showCellBar = keyIsMetric && showCellBars;
const isHtml = !keyIsAggMetric && isProbablyHTML(text);
const showCellBar = keyIsAggMetric && showCellBars;
let className = '';
if (keyIsAggMetric) {
className += ' dt-metric';
} else if (isFilterColumn(key) && emitFilter) {
className += ' dt-is-filter';
if (isActiveFilterValue(key, val)) {
className += ' dt-is-active-filter';
}
}
return (
<td
key={key}
// only set innerHTML for actual html content, this saves time
dangerouslySetInnerHTML={isHtml ? { __html: text } : undefined}
data-key={key}
data-sort={val}
className={keyIsMetric ? 'text-right' : ''}
className={className}
style={{
backgroundImage: showCellBar ? cellBar(key, val as number) : undefined,
}}
title={keyIsMetric || percentMetricsSet.has(key) ? String(val) : ''}
title={keyIsAggMetric || percentMetricsSet.has(key) ? String(val) : ''}
>
{isHtml ? null : text}
</td>

View File

@ -25,6 +25,16 @@
.superset-legacy-chart-table .dt-metric {
text-align: right;
}
.superset-legacy-chart-table td.dt-is-filter {
cursor: pointer;
}
.superset-legacy-chart-table td.dt-is-filter:hover {
background-color: linen;
}
.superset-legacy-chart-table td.dt-is-active-filter,
.superset-legacy-chart-table td.dt-is-active-filter:hover {
background-color: lightcyan;
}
.superset-legacy-chart-table div.dataTables_wrapper div.dataTables_paginate {
line-height: 0;
}

View File

@ -16,13 +16,9 @@
* specific language governing permissions and limitations
* under the License.
*/
import { ChartProps } from '@superset-ui/chart';
import { ChartProps, DataRecord, DataRecordFilters } from '@superset-ui/chart';
import { QueryFormDataMetric } from '@superset-ui/query';
interface DataRecord {
[key: string]: any;
}
interface DataColumnMeta {
// `key` is what is called `label` in the input props
key: string;
@ -31,39 +27,51 @@ interface DataColumnMeta {
format?: string;
}
interface TableChartData {
records: DataRecord[];
columns: string[];
}
interface TableChartFormData {
alignPn?: boolean;
colorPn?: boolean;
includeSearch?: boolean;
orderDesc?: boolean;
pageLength?: string | number;
metrics?: QueryFormDataMetric[] | null;
percentMetrics?: QueryFormDataMetric[] | null;
showCellBars?: boolean;
tableTimestampFormat?: string;
tableFilter?: boolean;
}
export interface DataTableProps {
// Each object is { field1: value1, field2: value2 }
data: DataRecord[];
height: number;
alignPositiveNegative: boolean;
colorPositiveNegative: boolean;
columns: DataColumnMeta[];
showCellBars: boolean;
metrics: string[];
percentMetrics: string[];
data: DataRecord[];
height: number;
includeSearch: boolean;
metrics: string[];
orderDesc: boolean;
pageLength: number;
percentMetrics: string[];
showCellBars: boolean;
tableTimestampFormat?: string;
// TODO: add filters back or clean up
// filters: object;
// onAddFilter?: (key: string, value: number[]) => void;
// onRemoveFilter?: (key: string, value: number[]) => void;
// tableFilter: boolean;
// timeseriesLimitMetric: string | object;
// These are dashboard filters, don't be confused with in-chart search filter
filters: DataRecordFilters;
emitFilter: boolean;
onChangeFilter: ChartProps['hooks']['onAddFilter'];
}
export interface TableChartFormData {
alignPn?: boolean;
colorPn?: boolean;
showCellBars?: boolean;
includeSearch?: boolean;
orderDesc?: boolean;
pageLength?: string;
metrics?: QueryFormDataMetric[];
percentMetrics?: QueryFormDataMetric[];
tableTimestampFormat?: string;
}
export type TableChartProps = ChartProps & {
formData: TableChartFormData;
queryData: ChartProps['queryData'] & {
data?: TableChartData;
};
};
/**
* Consolidate list of metrics to string, identified by its unique identifier
@ -76,9 +84,15 @@ const consolidateMetricShape = (metric: QueryFormDataMetric) => {
return metric.label || 'NOT_LABLED';
};
export default function transformProps(chartProps: ChartProps): DataTableProps {
const { height, datasource, formData, queryData } = chartProps;
export default function transformProps(chartProps: TableChartProps): DataTableProps {
const {
height,
datasource,
formData,
queryData,
initialValues: filters = {},
hooks: { onAddFilter: onChangeFilter = () => {} },
} = chartProps;
const {
alignPn = true,
colorPn = true,
@ -89,19 +103,19 @@ export default function transformProps(chartProps: ChartProps): DataTableProps {
metrics: metrics_ = [],
percentMetrics: percentMetrics_ = [],
tableTimestampFormat,
} = formData as TableChartFormData;
tableFilter,
} = formData;
const { columnFormats, verboseMap } = datasource;
const {
records,
columns: columns_,
}: { records: DataRecord[]; columns: string[] } = queryData.data;
const { records, columns: columns_ } = queryData.data || { records: [], columns: [] };
const metrics = (metrics_ ?? []).map(consolidateMetricShape);
// percent metrics always starts with a '%' sign.
const percentMetrics = (percentMetrics_ ?? [])
.map(consolidateMetricShape)
.map((x: string) => `%${x}`);
const columns = columns_.map((key: string) => {
let label = verboseMap[key] || key;
let label = verboseMap?.[key] || key;
// make sure there is a " " after "%" for percent metrics
if (label[0] === '%' && label[1] !== ' ') {
label = `% ${label.slice(1)}`;
@ -124,7 +138,10 @@ export default function transformProps(chartProps: ChartProps): DataTableProps {
showCellBars,
includeSearch,
orderDesc,
pageLength: pageLength ? parseInt(pageLength, 10) : 0,
pageLength: typeof pageLength === 'string' ? parseInt(pageLength, 10) || 0 : 0,
tableTimestampFormat,
filters,
emitFilter: tableFilter === true,
onChangeFilter,
};
}

View File

@ -17,6 +17,8 @@
* under the License.
*/
import { ChartProps } from '@superset-ui/chart';
import { DatasourceType } from '@superset-ui/query';
import { TableChartProps } from '../src/transformProps';
const basicFormData = {
alignPn: false,
@ -37,6 +39,11 @@ const basicChartProps = {
height: 500,
annotationData: {},
datasource: {
id: 0,
name: '',
type: DatasourceType.Table,
columns: [],
metrics: [],
columnFormats: {},
verboseMap: {},
},
@ -56,7 +63,7 @@ const basicChartProps = {
/**
* Basic data input
*/
const basic: ChartProps = {
const basic: TableChartProps = {
...basicChartProps,
queryData: {
data: {
@ -87,7 +94,7 @@ const basic: ChartProps = {
const advanced: ChartProps = {
...basic,
datasource: {
columnFormats: {},
...basic.datasource,
verboseMap: {
sum__num: 'Sum of Num',
},
@ -100,7 +107,7 @@ const advanced: ChartProps = {
queryData: {
data: {
columns: ['name', 'sum__num', '%pct_nice'],
records: [...basic.queryData.data.records],
records: [...(basic.queryData.data?.records || [])],
},
},
};

View File

@ -19,10 +19,9 @@ const run = cmd => {
};
if (glob) {
run(`nimbus prettier plugins/${glob}/{src,test}/**/*.{js,jsx,ts,tsx,css}"`);
// lint is slow, so not turning it on by default
if (extraArgs.includes('--lint')) {
run(`nimbus eslint plugins/${glob}/{src,test}`);
run(`nimbus eslint {packages,plugins}/${glob}/{src,test}`);
}
run(`nimbus babel --clean --workspaces="@superset-ui/${glob}"`);
run(`nimbus babel --clean --workspaces="@superset-ui/${glob}" --esm`);

View File

@ -3683,7 +3683,7 @@
dependencies:
"@types/react" "*"
"@types/react-test-renderer@^16.9.0":
"@types/react-test-renderer@^16.9.2":
version "16.9.2"
resolved "https://registry.yarnpkg.com/@types/react-test-renderer/-/react-test-renderer-16.9.2.tgz#e1c408831e8183e5ad748fdece02214a7c2ab6c5"
integrity sha512-4eJr1JFLIAlWhzDkBCkhrOIWOvOxcCAfQh+jiKg7l/nNZcCIL2MHl2dZhogIFKyHzedVWHaVP1Yydq/Ruu4agw==
@ -3697,7 +3697,7 @@
dependencies:
"@types/react" "*"
"@types/react@*", "@types/react@^16.3.0", "@types/react@^16.7.17":
"@types/react@*", "@types/react@^16.3.0", "@types/react@^16.9.34":
version "16.9.34"
resolved "https://registry.yarnpkg.com/@types/react/-/react-16.9.34.tgz#f7d5e331c468f53affed17a8a4d488cd44ea9349"
integrity sha512-8AJlYMOfPe1KGLKyHpflCg5z46n0b5DbRfqDksxBLBTUpB75ypDBAO9eCUcjNwE6LCUslwTz00yyG/X9gaVtow==
@ -14183,7 +14183,7 @@ react-docgen@^5.0.0:
node-dir "^0.1.10"
strip-indent "^3.0.0"
react-dom@^16.8.3, react-dom@^16.9.0:
react-dom@^16.13.1, react-dom@^16.8.3:
version "16.13.1"
resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-16.13.1.tgz#c1bd37331a0486c078ee54c4740720993b2e0e7f"
integrity sha512-81PIMmVLnCNLO/fFOQxdQkvEq/+Hfpv24XNJfpyZhTRfO0QcmQIF/PgCa1zCOj2w1hrn12MFLyaJ/G0+Mxtfag==
@ -14366,7 +14366,7 @@ react-syntax-highlighter@^11.0.2:
prismjs "^1.8.4"
refractor "^2.4.1"
react-test-renderer@^16.0.0-0, react-test-renderer@^16.9.0:
react-test-renderer@^16.0.0-0, react-test-renderer@^16.13.1:
version "16.13.1"
resolved "https://registry.yarnpkg.com/react-test-renderer/-/react-test-renderer-16.13.1.tgz#de25ea358d9012606de51e012d9742e7f0deabc1"
integrity sha512-Sn2VRyOK2YJJldOqoh8Tn/lWQ+ZiKhyZTPtaO0Q6yNj+QDbmRkVFap6pZPy3YQk8DScRDfyqm/KxKYP9gCMRiQ==
@ -14399,7 +14399,7 @@ react-virtualized-auto-sizer@^1.0.2:
resolved "https://registry.yarnpkg.com/react-virtualized-auto-sizer/-/react-virtualized-auto-sizer-1.0.2.tgz#a61dd4f756458bbf63bd895a92379f9b70f803bd"
integrity sha512-MYXhTY1BZpdJFjUovvYHVBmkq79szK/k7V3MO+36gJkWGkrXKtyr4vCPtpphaTLRAdDNoYEYFZWE8LjN+PIHNg==
react@^16.6.0, react@^16.8.3, react@^16.9.0:
react@^16.13.1, react@^16.8.3:
version "16.13.1"
resolved "https://registry.yarnpkg.com/react/-/react-16.13.1.tgz#2e818822f1a9743122c063d6410d85c1e3afe48e"
integrity sha512-YMZQQq32xHLX0bz5Mnibv1/LHb3Sqzngu7xstSM+vrkE5Kzr9xE0yMByK5kMoTK30YVJE61WfbxIFFvfeDKT1w==