diff --git a/superset-frontend/temporary_superset_ui/superset-ui/package.json b/superset-frontend/temporary_superset_ui/superset-ui/package.json index 0b4d87f49ae8..41364f316caf 100644 --- a/superset-frontend/temporary_superset_ui/superset-ui/package.json +++ b/superset-frontend/temporary_superset_ui/superset-ui/package.json @@ -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", diff --git a/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-chart-composition/package.json b/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-chart-composition/package.json index c6cead431c27..8b0e5636c0f3 100644 --- a/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-chart-composition/package.json +++ b/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-chart-composition/package.json @@ -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" } } diff --git a/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-chart/package.json b/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-chart/package.json index e1da12a8493c..9a67bad6c16e 100644 --- a/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-chart/package.json +++ b/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-chart/package.json @@ -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" } } diff --git a/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-chart/src/clients/ChartClient.ts b/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-chart/src/clients/ChartClient.ts index 74a1e38a64ac..bf7f6eb782cf 100644 --- a/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-chart/src/clients/ChartClient.ts +++ b/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-chart/src/clients/ChartClient.ts @@ -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}`)); diff --git a/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-chart/src/components/ChartDataProvider.tsx b/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-chart/src/components/ChartDataProvider.tsx index 63aea577b367..b32191758ed6 100644 --- a/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-chart/src/components/ChartDataProvider.tsx +++ b/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-chart/src/components/ChartDataProvider.tsx @@ -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; diff --git a/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-chart/src/index.ts b/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-chart/src/index.ts index 3c09526c7eb1..a19e7bbfc00c 100644 --- a/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-chart/src/index.ts +++ b/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-chart/src/index.ts @@ -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'; diff --git a/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-chart/src/models/ChartProps.ts b/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-chart/src/models/ChartProps.ts index 09895b816eec..2767155ce05e 100644 --- a/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-chart/src/models/ChartProps.ts +++ b/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-chart/src/models/ChartProps.ts @@ -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 = {}, diff --git a/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-chart/src/types/QueryResponse.ts b/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-chart/src/types/QueryResponse.ts new file mode 100644 index 000000000000..e6b865664ce8 --- /dev/null +++ b/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-chart/src/types/QueryResponse.ts @@ -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; diff --git a/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-chart/test/models/ChartProps.test.ts b/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-chart/test/models/ChartProps.test.ts index 61c615d4c7a5..b55c8e784efb 100644 --- a/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-chart/test/models/ChartProps.test.ts +++ b/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-chart/test/models/ChartProps.test.ts @@ -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); }); diff --git a/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-demo/package.json b/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-demo/package.json index ecec4cabf726..3ffcf7cc692f 100644 --- a/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-demo/package.json +++ b/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-demo/package.json @@ -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": { diff --git a/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-demo/storybook/stories/plugins/table/birthNames.json b/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-demo/storybook/stories/plugins/table/birthNames.json index 90697999755a..64cdccaa9811 100644 --- a/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-demo/storybook/stories/plugins/table/birthNames.json +++ b/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-demo/storybook/stories/plugins/table/birthNames.json @@ -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": "", diff --git a/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-query/src/types/Datasource.ts b/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-query/src/types/Datasource.ts index 03f23a29e4f5..93c1c6d7ea7b 100644 --- a/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-query/src/types/Datasource.ts +++ b/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-query/src/types/Datasource.ts @@ -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; + }; } diff --git a/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-query/src/types/Query.ts b/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-query/src/types/Query.ts index af04416c2ecb..2b3f38b3298c 100644 --- a/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-query/src/types/Query.ts +++ b/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-query/src/types/Query.ts @@ -22,6 +22,8 @@ export type QueryObjectFilterClause = { export type QueryObjectMetric = { label: string; + metric_name?: string; + d3format?: string; } & Partial; export type QueryObjectExtras = Partial<{ diff --git a/superset-frontend/temporary_superset_ui/superset-ui/plugins/big-number/src/BigNumber/transformProps.ts b/superset-frontend/temporary_superset_ui/superset-ui/plugins/big-number/src/BigNumber/transformProps.ts index 4115d58cfdb6..7b9526a17e36 100644 --- a/superset-frontend/temporary_superset_ui/superset-ui/plugins/big-number/src/BigNumber/transformProps.ts +++ b/superset-frontend/temporary_superset_ui/superset-ui/plugins/big-number/src/BigNumber/transformProps.ts @@ -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); diff --git a/superset-frontend/temporary_superset_ui/superset-ui/plugins/big-number/src/utils/getTimeFormatterForGranularity.ts b/superset-frontend/temporary_superset_ui/superset-ui/plugins/big-number/src/utils/getTimeFormatterForGranularity.ts index 64fb580601dc..3655736de551 100644 --- a/superset-frontend/temporary_superset_ui/superset-ui/plugins/big-number/src/utils/getTimeFormatterForGranularity.ts +++ b/superset-frontend/temporary_superset_ui/superset-ui/plugins/big-number/src/utils/getTimeFormatterForGranularity.ts @@ -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; } diff --git a/superset-frontend/temporary_superset_ui/superset-ui/plugins/big-number/src/tests/transformProps.test.ts b/superset-frontend/temporary_superset_ui/superset-ui/plugins/big-number/test/transformProps.test.ts similarity index 82% rename from superset-frontend/temporary_superset_ui/superset-ui/plugins/big-number/src/tests/transformProps.test.ts rename to superset-frontend/temporary_superset_ui/superset-ui/plugins/big-number/test/transformProps.test.ts index e1854610268e..38aa572d3d8e 100644 --- a/superset-frontend/temporary_superset_ui/superset-ui/plugins/big-number/src/tests/transformProps.test.ts +++ b/superset-frontend/temporary_superset_ui/superset-ui/plugins/big-number/test/transformProps.test.ts @@ -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', }, diff --git a/superset-frontend/temporary_superset_ui/superset-ui/plugins/table/package.json b/superset-frontend/temporary_superset_ui/superset-ui/plugins/table/package.json index b4f392dfe287..8d487405fee8 100644 --- a/superset-frontend/temporary_superset_ui/superset-ui/plugins/table/package.json +++ b/superset-frontend/temporary_superset_ui/superset-ui/plugins/table/package.json @@ -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" } } diff --git a/superset-frontend/temporary_superset_ui/superset-ui/plugins/table/src/ReactDataTable.tsx b/superset-frontend/temporary_superset_ui/superset-ui/plugins/table/src/ReactDataTable.tsx index 22f6c5237693..9f84f8c68285 100644 --- a/superset-frontend/temporary_superset_ui/superset-ui/plugins/table/src/ReactDataTable.tsx +++ b/superset-frontend/temporary_superset_ui/superset-ui/plugins/table/src/ReactDataTable.tsx @@ -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 ( {isHtml ? null : text} diff --git a/superset-frontend/temporary_superset_ui/superset-ui/plugins/table/src/Table.css b/superset-frontend/temporary_superset_ui/superset-ui/plugins/table/src/Table.css index 235e0b5cde75..85f0575c6b85 100644 --- a/superset-frontend/temporary_superset_ui/superset-ui/plugins/table/src/Table.css +++ b/superset-frontend/temporary_superset_ui/superset-ui/plugins/table/src/Table.css @@ -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; } diff --git a/superset-frontend/temporary_superset_ui/superset-ui/plugins/table/src/transformProps.ts b/superset-frontend/temporary_superset_ui/superset-ui/plugins/table/src/transformProps.ts index cbadf5d3e985..161bc5e5e4d5 100644 --- a/superset-frontend/temporary_superset_ui/superset-ui/plugins/table/src/transformProps.ts +++ b/superset-frontend/temporary_superset_ui/superset-ui/plugins/table/src/transformProps.ts @@ -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, }; } diff --git a/superset-frontend/temporary_superset_ui/superset-ui/plugins/table/test/testData.ts b/superset-frontend/temporary_superset_ui/superset-ui/plugins/table/test/testData.ts index 88405c8b5e3e..323264e9a86b 100644 --- a/superset-frontend/temporary_superset_ui/superset-ui/plugins/table/test/testData.ts +++ b/superset-frontend/temporary_superset_ui/superset-ui/plugins/table/test/testData.ts @@ -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 || [])], }, }, }; diff --git a/superset-frontend/temporary_superset_ui/superset-ui/scripts/build.js b/superset-frontend/temporary_superset_ui/superset-ui/scripts/build.js index 80f83b79b95e..1efc7041cd81 100644 --- a/superset-frontend/temporary_superset_ui/superset-ui/scripts/build.js +++ b/superset-frontend/temporary_superset_ui/superset-ui/scripts/build.js @@ -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`); diff --git a/superset-frontend/temporary_superset_ui/superset-ui/yarn.lock b/superset-frontend/temporary_superset_ui/superset-ui/yarn.lock index 7bb5a48ad7ac..7fb3edeade29 100644 --- a/superset-frontend/temporary_superset_ui/superset-ui/yarn.lock +++ b/superset-frontend/temporary_superset_ui/superset-ui/yarn.lock @@ -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==