diff --git a/superset-frontend/plugins/plugin-chart-ag-grid-table/src/AgGridTable/index.tsx b/superset-frontend/plugins/plugin-chart-ag-grid-table/src/AgGridTable/index.tsx index 5659077765dc..5829fb66c1b2 100644 --- a/superset-frontend/plugins/plugin-chart-ag-grid-table/src/AgGridTable/index.tsx +++ b/superset-frontend/plugins/plugin-chart-ag-grid-table/src/AgGridTable/index.tsx @@ -526,6 +526,7 @@ const AgGridDataTable: FunctionComponent = memo( paginationPageSizeSelector={PAGE_SIZE_OPTIONS} suppressDragLeaveHidesColumns pinnedBottomRowData={showTotals ? [cleanedTotals] : undefined} + tooltipShowDelay={500} localeText={{ // Pagination controls next: t('Next'), diff --git a/superset-frontend/plugins/plugin-chart-ag-grid-table/src/transformProps.ts b/superset-frontend/plugins/plugin-chart-ag-grid-table/src/transformProps.ts index 33ac8aa0183d..4285383f9640 100644 --- a/superset-frontend/plugins/plugin-chart-ag-grid-table/src/transformProps.ts +++ b/superset-frontend/plugins/plugin-chart-ag-grid-table/src/transformProps.ts @@ -345,6 +345,7 @@ const processColumns = memoizeOne(function processColumns( column_config: columnConfig = {}, query_mode: queryMode, }, + rawDatasource, queriesData, } = props; const granularity = extractTimegrain(props.rawFormData); @@ -384,6 +385,17 @@ const processColumns = memoizeOne(function processColumns( ? config.currencyFormat : savedCurrency; + const metricLookupKey = key.startsWith('%') ? key.slice(1) : key; + const description = + rawDatasource.columns?.find( + (item: { column_name?: string; description?: string | null }) => + item.column_name === key, + )?.description ?? + rawDatasource.metrics?.find( + (item: { metric_name?: string; description?: string | null }) => + item.metric_name === metricLookupKey, + )?.description; + let formatter; if (isTime || config.d3TimeFormat) { @@ -430,6 +442,7 @@ const processColumns = memoizeOne(function processColumns( isPercentMetric, formatter, config, + description, }; }) .sort((a, b) => { diff --git a/superset-frontend/plugins/plugin-chart-ag-grid-table/src/types.ts b/superset-frontend/plugins/plugin-chart-ag-grid-table/src/types.ts index e3d6bf070793..498f93575baa 100644 --- a/superset-frontend/plugins/plugin-chart-ag-grid-table/src/types.ts +++ b/superset-frontend/plugins/plugin-chart-ag-grid-table/src/types.ts @@ -192,6 +192,7 @@ export interface InputColumn { | CurrencyFormatter; originalLabel?: string; metricName?: string; + description?: string; } export type ValueRange = [number, number] | null; diff --git a/superset-frontend/plugins/plugin-chart-ag-grid-table/src/utils/useColDefs.ts b/superset-frontend/plugins/plugin-chart-ag-grid-table/src/utils/useColDefs.ts index 48f713aabbeb..2c08d6f8754d 100644 --- a/superset-frontend/plugins/plugin-chart-ag-grid-table/src/utils/useColDefs.ts +++ b/superset-frontend/plugins/plugin-chart-ag-grid-table/src/utils/useColDefs.ts @@ -280,6 +280,7 @@ export const useColDefs = ({ return { field: colId, headerName: getHeaderLabel(col), + headerTooltip: col.description, valueFormatter: p => valueFormatter(p, col), valueGetter: p => valueGetter(p, col), cellStyle: p => { diff --git a/superset-frontend/plugins/plugin-chart-ag-grid-table/test/transformProps.test.ts b/superset-frontend/plugins/plugin-chart-ag-grid-table/test/transformProps.test.ts new file mode 100644 index 000000000000..7b870c5ab72c --- /dev/null +++ b/superset-frontend/plugins/plugin-chart-ag-grid-table/test/transformProps.test.ts @@ -0,0 +1,266 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import transformProps from '../src/transformProps'; +import { TableChartProps } from '../src/types'; +import { GenericDataType } from '@apache-superset/core/common'; +import { QueryMode } from '@superset-ui/core'; + +function createMockChartProps( + overrides: Partial = {}, +): TableChartProps { + const defaultProps = { + height: 400, + width: 800, + rawFormData: { + viz_type: 'table', + datasource: '1__table', + query_mode: QueryMode.Aggregate, + metrics: [], + percent_metrics: [], + column_config: {}, + table_timestamp_format: '', + granularity_sqla: 'day', + time_range: 'No filter', + }, + queriesData: [ + { + data: [], + colnames: [], + coltypes: [], + rowcount: 0, + applied_filters: [], + rejected_filters: [], + }, + ], + datasource: { + columns: [], + metrics: [], + columnFormats: {}, + currencyFormats: {}, + verboseMap: {}, + }, + rawDatasource: { + columns: [], + metrics: [], + }, + filterState: {}, + hooks: { setDataMask: jest.fn(), onChartStateChange: jest.fn() }, + ownState: {}, + emitCrossFilters: false, + theme: {}, + ...overrides, + }; + return defaultProps as unknown as TableChartProps; +} + +test('extracts description from datasource.columns for a regular column', () => { + const props = createMockChartProps({ + queriesData: [ + { + data: [{ col1: 'value' }], + colnames: ['col1'], + coltypes: [GenericDataType.String], + rowcount: 1, + applied_filters: [], + rejected_filters: [], + } as unknown as TableChartProps['queriesData'][number], + ], + rawDatasource: { + columns: [ + { column_name: 'col1', description: 'This is a column description' }, + ], + metrics: [], + }, + }); + + const result = transformProps(props); + const { columns } = result; + const columnMeta = columns.find(c => c.key === 'col1'); + expect(columnMeta).toBeDefined(); + expect(columnMeta!.description).toBe('This is a column description'); +}); + +test('extracts description from datasource.metrics for a metric column', () => { + const props = createMockChartProps({ + rawFormData: { + viz_type: 'table', + datasource: '1__table', + query_mode: QueryMode.Aggregate, + metrics: ['sum_sales'], + percent_metrics: [], + column_config: {}, + table_timestamp_format: '', + granularity_sqla: 'day', + time_range: 'No filter', + }, + queriesData: [ + { + data: [{ sum_sales: 100 }], + colnames: ['sum_sales'], + coltypes: [GenericDataType.Numeric], + rowcount: 1, + applied_filters: [], + rejected_filters: [], + }, + ] as unknown as TableChartProps['queriesData'], + rawDatasource: { + columns: [], + metrics: [ + { metric_name: 'sum_sales', description: 'Total sales amount' }, + ], + }, + }); + + const result = transformProps(props); + const { columns } = result; + const columnMeta = columns.find(c => c.key === 'sum_sales'); + expect(columnMeta).toBeDefined(); + expect(columnMeta!.description).toBe('Total sales amount'); +}); + +test('prefers column description over metric description when both exist with same key', () => { + const props = createMockChartProps({ + rawFormData: { + viz_type: 'table', + datasource: '1__table', + query_mode: QueryMode.Aggregate, + metrics: ['revenue'], + percent_metrics: [], + column_config: {}, + table_timestamp_format: '', + granularity_sqla: 'day', + time_range: 'No filter', + }, + queriesData: [ + { + data: [{ revenue: 500 }], + colnames: ['revenue'], + coltypes: [GenericDataType.Numeric], + rowcount: 1, + applied_filters: [], + rejected_filters: [], + }, + ] as unknown as TableChartProps['queriesData'], + rawDatasource: { + columns: [{ column_name: 'revenue', description: 'Column desc' }], + metrics: [{ metric_name: 'revenue', description: 'Metric desc' }], + }, + }); + + const result = transformProps(props); + const { columns } = result; + const columnMeta = columns.find(c => c.key === 'revenue'); + expect(columnMeta!.description).toBe('Column desc'); +}); + +test('handles percent metrics correctly – uses base metric name for lookup', () => { + const props = createMockChartProps({ + rawFormData: { + viz_type: 'table', + datasource: '1__table', + query_mode: QueryMode.Aggregate, + metrics: ['profit'], + percent_metrics: ['profit'], + column_config: {}, + table_timestamp_format: '', + granularity_sqla: 'day', + time_range: 'No filter', + }, + queriesData: [ + { + data: [{ '%profit': 0.15 }], + colnames: ['%profit'], + coltypes: [GenericDataType.Numeric], + rowcount: 1, + applied_filters: [], + rejected_filters: [], + }, + ] as unknown as TableChartProps['queriesData'], + rawDatasource: { + columns: [], + metrics: [ + { metric_name: 'profit', description: 'Profit margin percent' }, + ], + }, + }); + + const result = transformProps(props); + const { columns } = result; + const columnMeta = columns.find(c => c.key === '%profit'); + expect(columnMeta).toBeDefined(); + expect(columnMeta!.description).toBe('Profit margin percent'); +}); + +test('sets description to undefined when no matching column or metric is found', () => { + const props = createMockChartProps({ + queriesData: [ + { + data: [{ unknown_col: 'x' }], + colnames: ['unknown_col'], + coltypes: [GenericDataType.String], + rowcount: 1, + applied_filters: [], + rejected_filters: [], + }, + ] as unknown as TableChartProps['queriesData'], + rawDatasource: { + columns: [], + metrics: [], + }, + }); + + const result = transformProps(props); + const { columns } = result; + const columnMeta = columns.find(c => c.key === 'unknown_col'); + expect(columnMeta!.description).toBeUndefined(); +}); + +test('uses description from column even when verboseMap renames the column', () => { + const props = createMockChartProps({ + queriesData: [ + { + data: [{ col_x: 10 }], + colnames: ['col_x'], + coltypes: [GenericDataType.Numeric], + rowcount: 1, + applied_filters: [], + rejected_filters: [], + }, + ] as unknown as TableChartProps['queriesData'], + datasource: { + columns: [], + metrics: [], + columnFormats: {}, + currencyFormats: {}, + verboseMap: { col_x: 'Custom Label' }, + } as unknown as TableChartProps['datasource'], + rawDatasource: { + columns: [ + { column_name: 'col_x', description: 'Original column description' }, + ], + metrics: [], + }, + }); + + const result = transformProps(props); + const { columns } = result; + const columnMeta = columns.find(c => c.key === 'col_x'); + expect(columnMeta!.label).toBe('Custom Label'); + expect(columnMeta!.description).toBe('Original column description'); +});