feat(viz-type): Ag grid table plugin Integration (#33517)

Signed-off-by: hainenber <dotronghai96@gmail.com>
Co-authored-by: Amaan Nawab <nelsondrew07@gmail.com>
Co-authored-by: Levis Mbote <111055098+LevisNgigi@users.noreply.github.com>
Co-authored-by: Enzo Martellucci <52219496+EnxDev@users.noreply.github.com>
Co-authored-by: Paul Rhodes <withnale@users.noreply.github.com>
Co-authored-by: Vitor Avila <96086495+Vitor-Avila@users.noreply.github.com>
Co-authored-by: Đỗ Trọng Hải <41283691+hainenber@users.noreply.github.com>
Co-authored-by: Michael S. Molina <70410625+michael-s-molina@users.noreply.github.com>
Co-authored-by: Sam Firke <sfirke@users.noreply.github.com>
This commit is contained in:
amaannawab923
2025-07-07 19:20:44 +05:30
committed by GitHub
parent 0fc4119728
commit 0a5941edd7
47 changed files with 5468 additions and 0 deletions

View File

@@ -0,0 +1,55 @@
/**
* 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 {
DataRecordValue,
normalizeTimestamp,
TimeFormatFunction,
} from '@superset-ui/core';
/**
* Extended Date object with a custom formatter, and retains the original input
* when the formatter is simple `String(..)`.
*/
export default class DateWithFormatter extends Date {
formatter: TimeFormatFunction;
input: DataRecordValue;
constructor(
input: DataRecordValue,
{ formatter = String }: { formatter?: TimeFormatFunction } = {},
) {
let value = input;
// assuming timestamps without a timezone is in UTC time
if (typeof value === 'string') {
value = normalizeTimestamp(value);
}
super(value as string);
this.input = input;
this.formatter = formatter;
this.toString = (): string => {
if (this.formatter === String) {
return String(this.input);
}
return this.formatter ? this.formatter(this) : Date.toString.call(this);
};
}
}

View File

@@ -0,0 +1,43 @@
/**
* 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.
*/
const dateFilterComparator = (filterDate: Date, cellValue: Date) => {
const cellDate = new Date(cellValue);
cellDate.setHours(0, 0, 0, 0);
if (Number.isNaN(cellDate?.getTime())) return -1;
const cellDay = cellDate.getDate();
const cellMonth = cellDate.getMonth();
const cellYear = cellDate.getFullYear();
const filterDay = filterDate.getDate();
const filterMonth = filterDate.getMonth();
const filterYear = filterDate.getFullYear();
if (cellYear < filterYear) return -1;
if (cellYear > filterYear) return 1;
if (cellMonth < filterMonth) return -1;
if (cellMonth > filterMonth) return 1;
if (cellDay < filterDay) return -1;
if (cellDay > filterDay) return 1;
return 0;
};
export default dateFilterComparator;

View File

@@ -0,0 +1,48 @@
/**
* 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 { isNil } from 'lodash';
export default function extent<T = number | string | Date | undefined | null>(
values: T[],
) {
let min: T | undefined;
let max: T | undefined;
// eslint-disable-next-line no-restricted-syntax
for (const value of values) {
if (value !== null) {
if (isNil(min)) {
if (value !== undefined) {
min = value;
max = value;
}
} else if (value !== undefined) {
if (min > value) {
min = value;
}
if (!isNil(max)) {
if (max < value) {
max = value;
}
}
}
}
}
return [min, max];
}

View File

@@ -0,0 +1,38 @@
/* 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 { SetDataMaskHook } from '@superset-ui/core';
import { SortByItem } from '../types';
interface TableOwnState {
currentPage?: number;
pageSize?: number;
sortColumn?: string;
sortOrder?: 'asc' | 'desc';
searchText?: string;
sortBy?: SortByItem[];
}
export const updateTableOwnState = (
setDataMask: SetDataMaskHook = () => {},
modifiedOwnState: TableOwnState,
) =>
setDataMask({
ownState: modifiedOwnState,
});

View File

@@ -0,0 +1,34 @@
/* 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 { ValueGetterParams } from 'ag-grid-community';
const filterValueGetter = (params: ValueGetterParams) => {
const raw = params.data[params.colDef.field as string];
const formatter = params.colDef.valueFormatter as Function;
if (!raw || !formatter) return null;
const formatted = formatter({
value: raw,
});
const numeric = parseFloat(String(formatted).replace('%', '').trim());
return Number.isNaN(numeric) ? null : numeric;
};
export default filterValueGetter;

View File

@@ -0,0 +1,115 @@
/**
* 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 {
CurrencyFormatter,
DataRecordValue,
GenericDataType,
getNumberFormatter,
isDefined,
isProbablyHTML,
sanitizeHtml,
} from '@superset-ui/core';
import { ValueFormatterParams, ValueGetterParams } from 'ag-grid-community';
import { DataColumnMeta, InputColumn } from '../types';
import DateWithFormatter from './DateWithFormatter';
/**
* Format text for cell value.
*/
function formatValue(
formatter: DataColumnMeta['formatter'],
value: DataRecordValue,
): [boolean, string] {
// render undefined as empty string
if (value === undefined) {
return [false, ''];
}
// render null as `N/A`
if (
value === null ||
// null values in temporal columns are wrapped in a Date object, so make sure we
// handle them here too
(value instanceof DateWithFormatter && value.input === null)
) {
return [false, 'N/A'];
}
if (formatter) {
return [false, formatter(value as number)];
}
if (typeof value === 'string') {
return isProbablyHTML(value) ? [true, sanitizeHtml(value)] : [false, value];
}
return [false, value.toString()];
}
export function formatColumnValue(
column: DataColumnMeta,
value: DataRecordValue,
) {
const { dataType, formatter, config = {} } = column;
const isNumber = dataType === GenericDataType.Numeric;
const smallNumberFormatter =
config.d3SmallNumberFormat === undefined
? formatter
: config.currencyFormat
? new CurrencyFormatter({
d3Format: config.d3SmallNumberFormat,
currency: config.currencyFormat,
})
: getNumberFormatter(config.d3SmallNumberFormat);
return formatValue(
isNumber && typeof value === 'number' && Math.abs(value) < 1
? smallNumberFormatter
: formatter,
value,
);
}
export const valueFormatter = (
params: ValueFormatterParams,
col: InputColumn,
): string => {
const { value, node } = params;
if (
isDefined(value) &&
value !== '' &&
!(value instanceof DateWithFormatter && value.input === null)
) {
return col.formatter?.(value) || value;
}
if (node?.level === -1) {
return '';
}
return 'N/A';
};
export const valueGetter = (params: ValueGetterParams, col: InputColumn) => {
// @ts-ignore
if (params?.colDef?.isMain) {
const modifiedColId = `Main ${params.column.getColId()}`;
return params.data[modifiedColId];
}
if (isDefined(params.data?.[params.column.getColId()])) {
return params.data[params.column.getColId()];
}
if (col.isNumeric) {
return undefined;
}
return '';
};

View File

@@ -0,0 +1,28 @@
/* 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 { CUSTOM_AGG_FUNCS } from '../consts';
import { InputColumn } from '../types';
export const getAggFunc = (col: InputColumn) =>
col.isMetric || col.isPercentMetric
? CUSTOM_AGG_FUNCS.queryTotal
: col.isNumeric
? 'sum'
: undefined;

View File

@@ -0,0 +1,46 @@
/**
* 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 { CellClassParams } from 'ag-grid-community';
import { InputColumn } from '../types';
type GetCellClassParams = CellClassParams & {
col: InputColumn;
emitCrossFilters: boolean | undefined;
};
const getCellClass = (params: GetCellClassParams) => {
const { col, emitCrossFilters } = params;
const isActiveFilterValue = params?.context?.isActiveFilterValue;
let className = '';
if (emitCrossFilters) {
if (!col?.isMetric) {
className += ' dt-is-filter';
}
if (isActiveFilterValue?.(col?.key, params?.value)) {
className += ' dt-is-active-filter';
}
if (col?.config?.truncateLongCells) {
className += ' dt-truncate-cell';
}
}
return className;
};
export default getCellClass;

View File

@@ -0,0 +1,82 @@
/**
* 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 { ColorFormatters } from '@superset-ui/chart-controls';
import { CellClassParams } from 'ag-grid-community';
import { BasicColorFormatterType, InputColumn } from '../types';
type CellStyleParams = CellClassParams & {
hasColumnColorFormatters: boolean | undefined;
columnColorFormatters: ColorFormatters;
hasBasicColorFormatters: boolean | undefined;
basicColorFormatters?: {
[Key: string]: BasicColorFormatterType;
}[];
col: InputColumn;
};
const getCellStyle = (params: CellStyleParams) => {
const {
value,
colDef,
rowIndex,
hasBasicColorFormatters,
basicColorFormatters,
hasColumnColorFormatters,
columnColorFormatters,
col,
node,
} = params;
let backgroundColor;
if (hasColumnColorFormatters) {
columnColorFormatters!
.filter(formatter => {
const colTitle = formatter?.column?.includes('Main')
? formatter?.column?.replace('Main', '').trim()
: formatter?.column;
return colTitle === colDef.field;
})
.forEach(formatter => {
const formatterResult =
value || value === 0 ? formatter.getColorFromValue(value) : false;
if (formatterResult) {
backgroundColor = formatterResult;
}
});
}
if (
hasBasicColorFormatters &&
col?.metricName &&
node?.rowPinned !== 'bottom'
) {
backgroundColor =
basicColorFormatters?.[rowIndex]?.[col.metricName]?.backgroundColor;
}
const textAlign =
col?.config?.horizontalAlign || (col?.isNumeric ? 'right' : 'left');
return {
backgroundColor: backgroundColor || '',
textAlign,
};
};
export default getCellStyle;

View File

@@ -0,0 +1,103 @@
/**
* 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 {
DataRecordFilters,
DataRecordValue,
DTTM_ALIAS,
ensureIsArray,
TimeGranularity,
} from '@superset-ui/core';
type GetCrossFilterDataMaskProps = {
key: string;
value: DataRecordValue;
filters?: DataRecordFilters;
timeGrain?: TimeGranularity;
isActiveFilterValue: (key: string, val: DataRecordValue) => boolean;
timestampFormatter: (value: DataRecordValue) => string;
};
export const getCrossFilterDataMask = ({
key,
value,
filters,
timeGrain,
isActiveFilterValue,
timestampFormatter,
}: GetCrossFilterDataMaskProps) => {
let updatedFilters = { ...(filters || {}) };
if (filters && isActiveFilterValue(key, value)) {
updatedFilters = {};
} else {
updatedFilters = {
[key]: [value],
};
}
if (Array.isArray(updatedFilters[key]) && updatedFilters[key].length === 0) {
delete updatedFilters[key];
}
const groupBy = Object.keys(updatedFilters);
const groupByValues = Object.values(updatedFilters);
const labelElements: string[] = [];
groupBy.forEach(col => {
const isTimestamp = col === DTTM_ALIAS;
const filterValues = ensureIsArray(updatedFilters?.[col]);
if (filterValues.length) {
const valueLabels = filterValues.map(value =>
isTimestamp ? timestampFormatter(value) : value,
);
labelElements.push(`${valueLabels.join(', ')}`);
}
});
return {
dataMask: {
extraFormData: {
filters:
groupBy.length === 0
? []
: groupBy.map(col => {
const val = ensureIsArray(updatedFilters?.[col]);
if (!val.length)
return {
col,
op: 'IS NULL' as const,
};
return {
col,
op: 'IN' as const,
val: val.map(el => (el instanceof Date ? el.getTime() : el!)),
grain: col === DTTM_ALIAS ? timeGrain : undefined,
};
}),
},
filterState: {
label: labelElements.join(', '),
value: groupByValues.length ? groupByValues : null,
filters:
updatedFilters && Object.keys(updatedFilters).length
? updatedFilters
: null,
},
},
isCurrentValueSelected: isActiveFilterValue(key, value),
};
};

View File

@@ -0,0 +1,65 @@
/**
* 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.
*/
// All ag grid sort related stuff
import { GridState, SortModelItem } from 'ag-grid-community';
import { SortByItem } from '../types';
const getInitialSortState = (sortBy?: SortByItem[]): SortModelItem[] => {
if (Array.isArray(sortBy) && sortBy.length > 0) {
return [
{
colId: sortBy[0]?.id,
sort: sortBy[0]?.desc ? 'desc' : 'asc',
},
];
}
return [];
};
export const shouldSort = ({
colId,
sortDir,
percentMetrics,
serverPagination,
gridInitialState,
}: {
colId: string;
sortDir: string;
percentMetrics: string[];
serverPagination: boolean;
gridInitialState: GridState;
}) => {
// percent metrics are not sortable
if (percentMetrics.includes(colId)) return false;
// if server pagination is not enabled, return false
// since this is server pagination sort
if (!serverPagination) return false;
const {
colId: initialColId = '',
sort: initialSortDir,
}: Partial<SortModelItem> = gridInitialState?.sort?.sortModel?.[0] || {};
// if the initial sort is the same as the current sort, return false
if (initialColId === colId && initialSortDir === sortDir) return false;
return true;
};
export default getInitialSortState;

View File

@@ -0,0 +1,46 @@
/**
* 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 { isEqualArray } from '@superset-ui/core';
import { TableChartProps } from '../types';
export default function isEqualColumns(
propsA: TableChartProps[],
propsB: TableChartProps[],
) {
const a = propsA[0];
const b = propsB[0];
return (
a.datasource.columnFormats === b.datasource.columnFormats &&
a.datasource.currencyFormats === b.datasource.currencyFormats &&
a.datasource.verboseMap === b.datasource.verboseMap &&
a.formData.tableTimestampFormat === b.formData.tableTimestampFormat &&
a.formData.timeGrainSqla === b.formData.timeGrainSqla &&
JSON.stringify(a.formData.columnConfig || null) ===
JSON.stringify(b.formData.columnConfig || null) &&
isEqualArray(a.formData.metrics, b.formData.metrics) &&
isEqualArray(a.queriesData?.[0]?.colnames, b.queriesData?.[0]?.colnames) &&
isEqualArray(a.queriesData?.[0]?.coltypes, b.queriesData?.[0]?.coltypes) &&
JSON.stringify(a.formData.extraFilters || null) ===
JSON.stringify(b.formData.extraFilters || null) &&
JSON.stringify(a.formData.extraFormData || null) ===
JSON.stringify(b.formData.extraFormData || null) &&
JSON.stringify(a.rawFormData.column_config || null) ===
JSON.stringify(b.rawFormData.column_config || null)
);
}

View File

@@ -0,0 +1,327 @@
/* 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 'ag-grid-community';
import { useCallback, useMemo } from 'react';
import { DataRecord, GenericDataType } from '@superset-ui/core';
import { ColorFormatters } from '@superset-ui/chart-controls';
import { extent as d3Extent, max as d3Max } from 'd3-array';
import {
BasicColorFormatterType,
CellRendererProps,
InputColumn,
} from '../types';
import getCellClass from './getCellClass';
import filterValueGetter from './filterValueGetter';
import dateFilterComparator from './dateFilterComparator';
import { getAggFunc } from './getAggFunc';
import { TextCellRenderer } from '../renderers/TextCellRenderer';
import { NumericCellRenderer } from '../renderers/NumericCellRenderer';
import CustomHeader from '../AgGridTable/components/CustomHeader';
import { valueFormatter, valueGetter } from './formatValue';
import getCellStyle from './getCellStyle';
interface InputData {
[key: string]: any;
}
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;
};
type ValueRange = [number, number];
function getValueRange(
key: string,
alignPositiveNegative: boolean,
data: InputData[],
) {
if (typeof data?.[0]?.[key] === 'number') {
const nums = data.map(row => row[key]) as number[];
return (
alignPositiveNegative ? [0, d3Max(nums.map(Math.abs))] : d3Extent(nums)
) as ValueRange;
}
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 'agMultiColumnFilter';
case GenericDataType.Temporal:
return 'agDateColumnFilter';
default:
return true;
}
};
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 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 && {
filterParams: {
comparator: dateFilterComparator,
},
}),
cellDataType: getCellDataType(col),
defaultAggFunc: getAggFunc(col),
initialAggFunc: getAggFunc(col),
...(!(isMetric || isPercentMetric) && {
allowedAggFuncs: [
'sum',
'min',
'max',
'count',
'avg',
'first',
'last',
],
}),
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;
};

View File

@@ -0,0 +1,42 @@
/**
* 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 { useTheme } from '@superset-ui/core';
import {
colorSchemeDark,
colorSchemeLight,
themeQuartz,
} from 'ag-grid-community';
// eslint-disable-next-line import/no-extraneous-dependencies
import tinycolor from 'tinycolor2';
export const useIsDark = () => {
const theme = useTheme();
return tinycolor(theme.colorBgContainer).isDark();
};
const useTableTheme = () => {
const baseTheme = themeQuartz;
const isDarkTheme = useIsDark();
const tableTheme = isDarkTheme
? baseTheme.withPart(colorSchemeDark)
: baseTheme.withPart(colorSchemeLight);
return tableTheme;
};
export default useTableTheme;