mirror of
https://github.com/apache/superset.git
synced 2026-05-25 09:45:18 +00:00
423 lines
12 KiB
TypeScript
423 lines
12 KiB
TypeScript
/* eslint-disable camelcase */
|
|
/**
|
|
* 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 { ColDef } from '@superset-ui/core/components/ThemedAgGridReact';
|
|
import { useCallback, useMemo } from 'react';
|
|
import { DataRecord, DataRecordValue } from '@superset-ui/core';
|
|
import { GenericDataType } from '@apache-superset/core/api/core';
|
|
import { ColorFormatters } from '@superset-ui/chart-controls';
|
|
import { extent as d3Extent, max as d3Max } from 'd3-array';
|
|
import {
|
|
BasicColorFormatterType,
|
|
CellRendererProps,
|
|
InputColumn,
|
|
ValueRange,
|
|
} from '../types';
|
|
import getCellClass from './getCellClass';
|
|
import filterValueGetter from './filterValueGetter';
|
|
import dateFilterComparator from './dateFilterComparator';
|
|
import DateWithFormatter from './DateWithFormatter';
|
|
import { getAggFunc } from './getAggFunc';
|
|
import { TextCellRenderer } from '../renderers/TextCellRenderer';
|
|
import { NumericCellRenderer } from '../renderers/NumericCellRenderer';
|
|
import CustomHeader from '../AgGridTable/components/CustomHeader';
|
|
import { NOOP_FILTER_COMPARATOR } from '../consts';
|
|
import { valueFormatter, valueGetter } from './formatValue';
|
|
import getCellStyle from './getCellStyle';
|
|
|
|
interface InputData {
|
|
[key: string]: DataRecordValue;
|
|
}
|
|
|
|
type UseColDefsProps = {
|
|
columns: InputColumn[];
|
|
data: InputData[];
|
|
serverPagination: boolean;
|
|
isRawRecords: boolean;
|
|
defaultAlignPN: boolean;
|
|
showCellBars: boolean;
|
|
colorPositiveNegative: boolean;
|
|
totals: DataRecord | undefined;
|
|
columnColorFormatters: ColorFormatters;
|
|
allowRearrangeColumns?: boolean;
|
|
basicColorFormatters?: { [Key: string]: BasicColorFormatterType }[];
|
|
isUsingTimeComparison?: boolean;
|
|
emitCrossFilters?: boolean;
|
|
alignPositiveNegative: boolean;
|
|
slice_id: number;
|
|
};
|
|
|
|
function getValueRange(
|
|
key: string,
|
|
alignPositiveNegative: boolean,
|
|
data: InputData[],
|
|
) {
|
|
const nums = data
|
|
.map(row => {
|
|
const raw = row[key];
|
|
return raw instanceof Number ? raw.valueOf() : raw;
|
|
})
|
|
.filter(
|
|
(value): value is number =>
|
|
typeof value === 'number' && Number.isFinite(value),
|
|
) as number[];
|
|
if (nums.length > 0) {
|
|
const maxAbs = d3Max(nums.map(Math.abs));
|
|
if (alignPositiveNegative) {
|
|
return [0, maxAbs ?? 0] as ValueRange;
|
|
}
|
|
const extent = d3Extent(nums) as ValueRange | undefined;
|
|
return extent ?? [0, 0];
|
|
}
|
|
return null;
|
|
}
|
|
|
|
const getCellDataType = (col: InputColumn) => {
|
|
switch (col.dataType) {
|
|
case GenericDataType.Numeric:
|
|
return 'number';
|
|
case GenericDataType.Temporal:
|
|
return 'date';
|
|
case GenericDataType.Boolean:
|
|
return 'boolean';
|
|
default:
|
|
return 'text';
|
|
}
|
|
};
|
|
|
|
const getFilterType = (col: InputColumn) => {
|
|
switch (col.dataType) {
|
|
case GenericDataType.Numeric:
|
|
return 'agNumberColumnFilter';
|
|
case GenericDataType.String:
|
|
return 'agTextColumnFilter';
|
|
case GenericDataType.Temporal:
|
|
return 'agDateColumnFilter';
|
|
default:
|
|
return true;
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Filter value getter for temporal columns.
|
|
* Returns null for DateWithFormatter objects with null input,
|
|
* enabling AG Grid's blank filter to correctly identify null dates.
|
|
*/
|
|
const dateFilterValueGetter = (params: {
|
|
data: Record<string, unknown>;
|
|
colDef: { field?: string };
|
|
}) => {
|
|
const value = params.data?.[params.colDef.field as string];
|
|
// Return null for DateWithFormatter with null input so AG Grid blank filter works
|
|
if (value instanceof DateWithFormatter && value.input === null) {
|
|
return null;
|
|
}
|
|
return value;
|
|
};
|
|
|
|
/**
|
|
* Custom date filter options for server-side pagination.
|
|
* Each option has a predicate that always returns true, allowing all rows to pass
|
|
* client-side filtering since the actual filtering is handled by the server.
|
|
*/
|
|
const SERVER_SIDE_DATE_FILTER_OPTIONS = [
|
|
{
|
|
displayKey: 'serverEquals',
|
|
displayName: 'Equals',
|
|
predicate: () => true,
|
|
numberOfInputs: 1,
|
|
},
|
|
{
|
|
displayKey: 'serverNotEqual',
|
|
displayName: 'Not Equal',
|
|
predicate: () => true,
|
|
numberOfInputs: 1,
|
|
},
|
|
{
|
|
displayKey: 'serverBefore',
|
|
displayName: 'Before',
|
|
predicate: () => true,
|
|
numberOfInputs: 1,
|
|
},
|
|
{
|
|
displayKey: 'serverAfter',
|
|
displayName: 'After',
|
|
predicate: () => true,
|
|
numberOfInputs: 1,
|
|
},
|
|
{
|
|
displayKey: 'serverInRange',
|
|
displayName: 'In Range',
|
|
predicate: () => true,
|
|
numberOfInputs: 2,
|
|
},
|
|
{
|
|
displayKey: 'serverBlank',
|
|
displayName: 'Blank',
|
|
predicate: () => true,
|
|
numberOfInputs: 0,
|
|
},
|
|
{
|
|
displayKey: 'serverNotBlank',
|
|
displayName: 'Not blank',
|
|
predicate: () => true,
|
|
numberOfInputs: 0,
|
|
},
|
|
];
|
|
|
|
function getHeaderLabel(col: InputColumn) {
|
|
let headerLabel: string | undefined;
|
|
|
|
const hasOriginalLabel = !!col?.originalLabel;
|
|
const isMain = col?.key?.includes('Main');
|
|
const hasDisplayTypeIcon = col?.config?.displayTypeIcon !== false;
|
|
const hasCustomColumnName = !!col?.config?.customColumnName;
|
|
|
|
if (hasOriginalLabel && hasCustomColumnName) {
|
|
if ('displayTypeIcon' in col.config) {
|
|
headerLabel =
|
|
hasDisplayTypeIcon && !isMain
|
|
? `${col.label} ${col.config.customColumnName}`
|
|
: col.config.customColumnName;
|
|
} else {
|
|
headerLabel = col.config.customColumnName;
|
|
}
|
|
} else if (hasOriginalLabel && isMain) {
|
|
headerLabel = col.originalLabel;
|
|
} else if (hasOriginalLabel && !hasDisplayTypeIcon) {
|
|
headerLabel = '';
|
|
} else {
|
|
headerLabel = col?.label;
|
|
}
|
|
return headerLabel || '';
|
|
}
|
|
|
|
export const useColDefs = ({
|
|
columns,
|
|
data,
|
|
serverPagination,
|
|
isRawRecords,
|
|
defaultAlignPN,
|
|
showCellBars,
|
|
colorPositiveNegative,
|
|
totals,
|
|
columnColorFormatters,
|
|
allowRearrangeColumns,
|
|
basicColorFormatters,
|
|
isUsingTimeComparison,
|
|
emitCrossFilters,
|
|
alignPositiveNegative,
|
|
slice_id,
|
|
}: UseColDefsProps) => {
|
|
const getCommonColProps = useCallback(
|
|
(
|
|
col: InputColumn,
|
|
): ColDef & {
|
|
isMain: boolean;
|
|
} => {
|
|
const {
|
|
config,
|
|
isMetric,
|
|
isPercentMetric,
|
|
isNumeric,
|
|
key: originalKey,
|
|
dataType,
|
|
originalLabel,
|
|
} = col;
|
|
|
|
const alignPN =
|
|
config.alignPositiveNegative === undefined
|
|
? defaultAlignPN
|
|
: config.alignPositiveNegative;
|
|
|
|
const hasColumnColorFormatters =
|
|
isNumeric &&
|
|
Array.isArray(columnColorFormatters) &&
|
|
columnColorFormatters.length > 0;
|
|
|
|
const hasBasicColorFormatters =
|
|
isUsingTimeComparison &&
|
|
Array.isArray(basicColorFormatters) &&
|
|
basicColorFormatters.length > 0;
|
|
|
|
const isMain = originalKey?.includes('Main');
|
|
const colId = isMain
|
|
? originalKey.replace('Main', '').trim()
|
|
: originalKey;
|
|
const isTextColumn =
|
|
dataType === GenericDataType.String ||
|
|
dataType === GenericDataType.Temporal;
|
|
const isBooleanColumn = dataType === GenericDataType.Boolean;
|
|
|
|
const valueRange =
|
|
!hasBasicColorFormatters &&
|
|
!hasColumnColorFormatters &&
|
|
showCellBars &&
|
|
(config.showCellBars ?? true) &&
|
|
(isMetric || isRawRecords || isPercentMetric) &&
|
|
getValueRange(originalKey, alignPN || alignPositiveNegative, data);
|
|
|
|
const filter = getFilterType(col);
|
|
|
|
return {
|
|
field: colId,
|
|
headerName: getHeaderLabel(col),
|
|
valueFormatter: p => valueFormatter(p, col),
|
|
valueGetter: p => valueGetter(p, col),
|
|
cellStyle: p =>
|
|
getCellStyle({
|
|
...p,
|
|
hasColumnColorFormatters,
|
|
columnColorFormatters,
|
|
hasBasicColorFormatters,
|
|
basicColorFormatters,
|
|
col,
|
|
}),
|
|
cellClass: p =>
|
|
getCellClass({
|
|
...p,
|
|
col,
|
|
emitCrossFilters,
|
|
}),
|
|
minWidth: config?.columnWidth ?? 100,
|
|
filter,
|
|
...(isPercentMetric && {
|
|
filterValueGetter,
|
|
}),
|
|
...(dataType === GenericDataType.Temporal && {
|
|
// Use dateFilterValueGetter so AG Grid correctly identifies null dates for blank filter
|
|
filterValueGetter: dateFilterValueGetter,
|
|
filterParams: serverPagination
|
|
? {
|
|
filterOptions: SERVER_SIDE_DATE_FILTER_OPTIONS,
|
|
comparator: NOOP_FILTER_COMPARATOR,
|
|
}
|
|
: {
|
|
comparator: dateFilterComparator,
|
|
},
|
|
}),
|
|
cellDataType: getCellDataType(col),
|
|
defaultAggFunc: getAggFunc(col),
|
|
initialAggFunc: getAggFunc(col),
|
|
...(!(isMetric || isPercentMetric) && {
|
|
allowedAggFuncs: [
|
|
'sum',
|
|
'min',
|
|
'max',
|
|
'count',
|
|
'avg',
|
|
'first',
|
|
'last',
|
|
],
|
|
}),
|
|
...(isBooleanColumn
|
|
? {
|
|
cellRenderer: 'agCheckboxCellRenderer',
|
|
cellRendererParams: { disabled: true },
|
|
}
|
|
: {
|
|
cellRenderer: (p: CellRendererProps) =>
|
|
isTextColumn ? TextCellRenderer(p) : NumericCellRenderer(p),
|
|
cellRendererParams: {
|
|
allowRenderHtml: true,
|
|
columns,
|
|
hasBasicColorFormatters,
|
|
col,
|
|
basicColorFormatters,
|
|
valueRange,
|
|
alignPositiveNegative: alignPN || alignPositiveNegative,
|
|
colorPositiveNegative,
|
|
},
|
|
}),
|
|
context: {
|
|
isMetric,
|
|
isPercentMetric,
|
|
isNumeric,
|
|
},
|
|
lockPinned: !allowRearrangeColumns,
|
|
sortable: !serverPagination || !isPercentMetric,
|
|
...(serverPagination && {
|
|
headerComponent: CustomHeader,
|
|
comparator: () => 0,
|
|
headerComponentParams: {
|
|
slice_id,
|
|
},
|
|
}),
|
|
isMain,
|
|
...(!isMain &&
|
|
originalLabel && {
|
|
columnGroupShow: 'open',
|
|
}),
|
|
...(originalLabel && {
|
|
timeComparisonKey: originalLabel,
|
|
}),
|
|
wrapText: !config?.truncateLongCells,
|
|
autoHeight: !config?.truncateLongCells,
|
|
};
|
|
},
|
|
[
|
|
columns,
|
|
data,
|
|
defaultAlignPN,
|
|
columnColorFormatters,
|
|
basicColorFormatters,
|
|
showCellBars,
|
|
colorPositiveNegative,
|
|
isUsingTimeComparison,
|
|
isRawRecords,
|
|
emitCrossFilters,
|
|
allowRearrangeColumns,
|
|
serverPagination,
|
|
alignPositiveNegative,
|
|
],
|
|
);
|
|
|
|
const stringifiedCols = JSON.stringify(columns);
|
|
|
|
const colDefs = useMemo(() => {
|
|
const groupIndexMap = new Map<string, number>();
|
|
|
|
return columns.reduce<ColDef[]>((acc, col) => {
|
|
const colDef = getCommonColProps(col);
|
|
|
|
if (col?.originalLabel) {
|
|
if (groupIndexMap.has(col.originalLabel)) {
|
|
const groupIdx = groupIndexMap.get(col.originalLabel)!;
|
|
(acc[groupIdx] as { children: ColDef[] }).children.push(colDef);
|
|
} else {
|
|
const group = {
|
|
headerName: col.originalLabel,
|
|
marryChildren: true,
|
|
openByDefault: true,
|
|
children: [colDef],
|
|
};
|
|
groupIndexMap.set(col.originalLabel, acc.length);
|
|
acc.push(group);
|
|
}
|
|
} else {
|
|
acc.push(colDef);
|
|
}
|
|
|
|
return acc;
|
|
}, []);
|
|
}, [stringifiedCols, getCommonColProps]);
|
|
|
|
return colDefs;
|
|
};
|