mirror of
https://github.com/apache/superset.git
synced 2024-09-18 19:49:37 -04:00
feat(table): enable table filter and better typing (#344)
This commit is contained in:
parent
aa2cdcaad5
commit
9b009e7198
@ -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",
|
||||
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
@ -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}`));
|
||||
|
@ -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>;
|
||||
|
@ -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';
|
||||
|
@ -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 = {},
|
||||
|
@ -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;
|
@ -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);
|
||||
});
|
||||
|
@ -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": {
|
||||
|
@ -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": "",
|
||||
|
@ -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;
|
||||
};
|
||||
}
|
||||
|
@ -22,6 +22,8 @@ export type QueryObjectFilterClause = {
|
||||
|
||||
export type QueryObjectMetric = {
|
||||
label: string;
|
||||
metric_name?: string;
|
||||
d3format?: string;
|
||||
} & Partial<AdhocMetric>;
|
||||
|
||||
export type QueryObjectExtras = Partial<{
|
||||
|
@ -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);
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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',
|
||||
},
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
@ -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>
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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,
|
||||
};
|
||||
}
|
||||
|
@ -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 || [])],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
@ -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`);
|
||||
|
@ -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==
|
||||
|
Loading…
Reference in New Issue
Block a user