Merge branch 'master' into msyavuz/chore/react-18

This commit is contained in:
Mehmet Salih Yavuz
2025-11-24 12:05:19 +03:00
2034 changed files with 115854 additions and 34591 deletions

View File

@@ -36,12 +36,13 @@
"xss": "^1.0.15"
},
"peerDependencies": {
"@apache-superset/core": "*",
"@ant-design/icons": "^5.2.6",
"@superset-ui/chart-controls": "*",
"@superset-ui/core": "*",
"@testing-library/dom": "^8.20.1",
"@testing-library/jest-dom": "*",
"@testing-library/react": "^13.0.0",
"@testing-library/react": "^14.0.0",
"@testing-library/user-event": "*",
"@types/classnames": "*",
"@types/react": "*",

View File

@@ -27,7 +27,7 @@ import {
DragEvent,
useEffect,
} from 'react';
import { styled, typedMemo, usePrevious } from '@superset-ui/core';
import { typedMemo, usePrevious } from '@superset-ui/core';
import {
useTable,
usePagination,
@@ -42,7 +42,7 @@ import {
} from 'react-table';
import { matchSorter, rankings } from 'match-sorter';
import { isEqual } from 'lodash';
import { Space } from '@superset-ui/core/components';
import { Flex, Space } from '@superset-ui/core/components';
import GlobalFilter, { GlobalFilterProps } from './components/GlobalFilter';
import SelectPageSize, {
SelectPageSizeProps,
@@ -77,7 +77,7 @@ export interface DataTableProps<D extends object> extends TableOptions<D> {
sticky?: boolean;
rowCount: number;
wrapperRef?: MutableRefObject<HTMLDivElement>;
onColumnOrderChange: () => void;
onColumnOrderChange?: () => void;
renderGroupingHeaders?: () => JSX.Element;
renderTimeComparisonDropdown?: () => JSX.Element;
handleSortByChange: (sortBy: SortByItem[]) => void;
@@ -98,24 +98,6 @@ const sortTypes = {
alphanumeric: sortAlphanumericCaseInsensitive,
};
const StyledSpace = styled(Space)`
display: flex;
justify-content: flex-end;
.search-select-container {
display: flex;
}
.search-by-label {
align-self: center;
margin-right: 4px;
}
`;
const StyledRow = styled.div`
display: flex;
`;
// Be sure to pass our updateMyData and the skipReset option
export default typedMemo(function DataTable<D extends object>({
tableClassName,
@@ -336,8 +318,7 @@ export default typedMemo(function DataTable<D extends object>({
const colToBeMoved = currentCols.splice(columnBeingDragged, 1);
currentCols.splice(newPosition, 0, colToBeMoved[0]);
setColumnOrder(currentCols);
// toggle value in TableChart to trigger column width recalc
onColumnOrderChange();
onColumnOrderChange?.();
}
e.preventDefault();
};
@@ -450,30 +431,36 @@ export default typedMemo(function DataTable<D extends object>({
>
{hasGlobalControl ? (
<div ref={globalControlRef} className="form-inline dt-controls">
<StyledRow className="row">
<StyledSpace size="middle">
{hasPagination ? (
<SelectPageSize
total={resultsSize}
current={resultCurrentPageSize}
options={pageSizeOptions}
selectRenderer={
typeof selectPageSize === 'boolean'
? undefined
: selectPageSize
}
onChange={setPageSize}
/>
) : null}
<Flex
wrap
className="row"
align="center"
justify="space-between"
gap="middle"
>
{hasPagination ? (
<SelectPageSize
total={resultsSize}
current={resultCurrentPageSize}
options={pageSizeOptions}
selectRenderer={
typeof selectPageSize === 'boolean'
? undefined
: selectPageSize
}
onChange={setPageSize}
/>
) : null}
<Flex wrap align="center" gap="middle">
{serverPagination && (
<div className="search-select-container">
<span className="search-by-label">Search by: </span>
<Space size="small" className="search-select-container">
<span className="search-by-label">Search by:</span>
<SearchSelectDropdown
searchOptions={searchOptions}
value={serverPaginationData?.searchColumn || ''}
onChange={onSearchColChange}
/>
</div>
</Space>
)}
{searchInput && (
<GlobalFilter<D>
@@ -493,8 +480,8 @@ export default typedMemo(function DataTable<D extends object>({
{renderTimeComparisonDropdown
? renderTimeComparisonDropdown()
: null}
</StyledSpace>
</StyledRow>
</Flex>
</Flex>
</div>
) : null}
{wrapStickyTable ? wrapStickyTable(renderTable) : renderTable()}

View File

@@ -17,7 +17,7 @@
* under the License.
*/
/* eslint-disable import/no-extraneous-dependencies */
import { styled } from '@superset-ui/core';
import { styled } from '@apache-superset/core/ui';
import { RawAntdSelect } from '@superset-ui/core/components';
import { SearchOption } from '../../types';

View File

@@ -17,7 +17,8 @@
* under the License.
*/
import { memo } from 'react';
import { css, t } from '@superset-ui/core';
import { t } from '@superset-ui/core';
import { css } from '@apache-superset/core/ui';
import { formatSelectOptions } from '@superset-ui/chart-controls';
import { RawAntdSelect } from '@superset-ui/core/components';

View File

@@ -30,6 +30,7 @@ import {
UIEventHandler,
} from 'react';
import { TableInstance, Hooks } from 'react-table';
import { useTheme, css } from '@apache-superset/core/ui';
import getScrollBarSize from '../utils/getScrollBarSize';
import needScrollBar from '../utils/needScrollBar';
import useMountedMemo from '../utils/useMountedMemo';
@@ -125,6 +126,8 @@ function StickyWrap({
children: Table;
sticky?: StickyState; // current sticky element sizes
}) {
const theme = useTheme();
if (!table || table.type !== 'table') {
throw new Error('<StickyWrap> must have only one <table> element as child');
}
@@ -221,6 +224,26 @@ function StickyWrap({
let footerTable: ReactElement | undefined;
let bodyTable: ReactElement | undefined;
const scrollBarStyles = css`
&::-webkit-scrollbar {
width: 8px;
height: 8px;
}
&::-webkit-scrollbar-track {
background: ${theme.colorFillQuaternary};
}
&::-webkit-scrollbar-thumb {
background: ${theme.colorFillSecondary};
border-radius: ${theme.borderRadiusSM}px;
&:hover {
background: ${theme.colorFillTertiary};
}
}
&::-webkit-scrollbar-corner {
background: ${theme.colorFillQuaternary};
}
`;
if (needSizer) {
const theadWithRef = cloneElement(thead, { ref: theadRef });
const tfootWithRef = tfoot && cloneElement(tfoot, { ref: tfootRef });
@@ -233,6 +256,7 @@ function StickyWrap({
visibility: 'hidden',
scrollbarGutter: 'stable',
}}
css={scrollBarStyles}
role="presentation"
>
{cloneElement(
@@ -316,6 +340,7 @@ function StickyWrap({
overflow: 'auto',
scrollbarGutter: 'stable',
}}
css={scrollBarStyles}
onScroll={sticky.hasHorizontalScroll ? onScroll : undefined}
role="presentation"
>

View File

@@ -16,7 +16,7 @@
* specific language governing permissions and limitations
* under the License.
*/
import { css, styled } from '@superset-ui/core';
import { css, styled } from '@apache-superset/core/ui';
export default styled.div`
${({ theme }) => css`

View File

@@ -43,17 +43,15 @@ import {
DataRecordValue,
DTTM_ALIAS,
ensureIsArray,
GenericDataType,
getSelectedText,
getTimeFormatterForGranularity,
BinaryQueryObjectFilterClause,
styled,
css,
t,
tn,
useTheme,
SupersetTheme,
extractTextFromHTML,
} from '@superset-ui/core';
import { styled, css, useTheme, SupersetTheme } from '@apache-superset/core/ui';
import { GenericDataType } from '@apache-superset/core/api/core';
import {
Input,
Space,
@@ -70,6 +68,7 @@ import {
TableOutlined,
} from '@ant-design/icons';
import { isEmpty, debounce, isEqual } from 'lodash';
import { ColorFormatters } from '@superset-ui/chart-controls';
import {
ColorSchemeEnum,
DataColumnMeta,
@@ -83,7 +82,6 @@ import DataTable, {
SelectPageSizeRendererProps,
SizeOption,
} from './DataTable';
import Styles from './Styles';
import { formatColumnValue } from './utils/formatValue';
import { PAGE_SIZE_OPTIONS, SERVER_PAGE_SIZE_OPTIONS } from './consts';
@@ -141,6 +139,31 @@ function cellWidth({
return perc2;
}
/**
* Sanitize a column identifier for use in HTML id attributes and CSS selectors.
* Replaces characters that are invalid in CSS selectors with safe alternatives.
*
* Note: The returned value should be prefixed with a string (e.g., "header-")
* to ensure it forms a valid HTML ID (IDs cannot start with a digit).
*
* Exported for testing.
*/
export function sanitizeHeaderId(columnId: string): string {
return (
columnId
// Semantic replacements first: preserve meaning in IDs for readability
// (e.g., '%pct_nice' → 'percentpct_nice' instead of '_pct_nice')
.replace(/%/g, 'percent')
.replace(/#/g, 'hash')
.replace(/△/g, 'delta')
// Generic sanitization for remaining special characters
.replace(/\s+/g, '_')
.replace(/[^a-zA-Z0-9_-]/g, '_')
.replace(/_+/g, '_') // Collapse consecutive underscores
.replace(/^_+|_+$/g, '') // Trim leading/trailing underscores
);
}
/**
* Cell left margin (offset) calculation for horizontal bar chart elements
* when alignPositiveNegative is not set
@@ -170,12 +193,21 @@ function cellOffset({
function cellBackground({
value,
colorPositiveNegative = false,
theme,
}: {
value: number;
colorPositiveNegative: boolean;
theme: SupersetTheme;
}) {
const r = colorPositiveNegative && value < 0 ? 150 : 0;
return `rgba(${r},0,0,0.2)`;
if (!colorPositiveNegative) {
return `${theme.colorFill}`;
}
if (value < 0) {
return `${theme.colorError}50`;
}
return `${theme.colorSuccess}50`;
}
function SortIcon<D extends object>({ column }: { column: ColumnInstance<D> }) {
@@ -187,6 +219,21 @@ function SortIcon<D extends object>({ column }: { column: ColumnInstance<D> }) {
return sortIcon;
}
/**
* Label that is visually hidden but accessible
*/
const VisuallyHidden = styled.label`
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border: 0;
`;
function SearchInput({
count,
value,
@@ -217,10 +264,10 @@ function SelectPageSize({
const { Option } = Select;
return (
<>
<label htmlFor="pageSizeSelect" className="sr-only">
<span className="dt-select-page-size">
<VisuallyHidden htmlFor="pageSizeSelect">
{t('Select page size')}
</label>
</VisuallyHidden>
{t('Show')}{' '}
<Select<number>
id="pageSizeSelect"
@@ -244,7 +291,7 @@ function SelectPageSize({
})}
</Select>{' '}
{t('entries per page')}
</>
</span>
);
}
@@ -288,12 +335,17 @@ export default function TableChart<D extends DataRecord = DataRecord>(
serverPageLength,
slice_id,
} = props;
const comparisonColumns = [
{ key: 'all', label: t('Display all') },
{ key: '#', label: '#' },
{ key: '', label: '△' },
{ key: '%', label: '%' },
];
const comparisonColumns = useMemo(
() => [
{ key: 'all', label: t('Display all') },
{ key: '#', label: '#' },
{ key: '△', label: '△' },
{ key: '%', label: '%' },
],
[],
);
const timestampFormatter = useCallback(
(value: any) => getTimeFormatterForGranularity(timeGrain)(value),
[timeGrain],
@@ -345,71 +397,74 @@ export default function TableChart<D extends DataRecord = DataRecord>(
[filters],
);
const getCrossFilterDataMask = (key: string, value: DataRecordValue) => {
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(', ')}`);
const getCrossFilterDataMask = useCallback(
(key: string, value: DataRecordValue) => {
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];
}
});
return {
dataMask: {
extraFormData: {
filters:
groupBy.length === 0
? []
: groupBy.map(col => {
const val = ensureIsArray(updatedFilters?.[col]);
if (!val.length)
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: 'IS NULL' as const,
op: 'IN' as const,
val: val.map(el =>
el instanceof Date ? el.getTime() : el!,
),
grain: col === DTTM_ALIAS ? timeGrain : undefined,
};
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,
},
},
filterState: {
label: labelElements.join(', '),
value: groupByValues.length ? groupByValues : null,
filters:
updatedFilters && Object.keys(updatedFilters).length
? updatedFilters
: null,
},
},
isCurrentValueSelected: isActiveFilterValue(key, value),
};
};
isCurrentValueSelected: isActiveFilterValue(key, value),
};
},
[filters, isActiveFilterValue, timestampFormatter, timeGrain],
);
const toggleFilter = useCallback(
function toggleFilter(key: string, val: DataRecordValue) {
@@ -421,17 +476,21 @@ export default function TableChart<D extends DataRecord = DataRecord>(
[emitCrossFilters, getCrossFilterDataMask, setDataMask],
);
const getSharedStyle = (column: DataColumnMeta): CSSProperties => {
const { isNumeric, config = {} } = column;
const textAlign =
config.horizontalAlign ||
(isNumeric && !isUsingTimeComparison ? 'right' : 'left');
return {
textAlign,
};
};
const getSharedStyle = useCallback(
(column: DataColumnMeta): CSSProperties => {
const { isNumeric, config = {} } = column;
const textAlign =
config.horizontalAlign ||
(isNumeric && !isUsingTimeComparison ? 'right' : 'left');
return {
textAlign,
};
},
[isUsingTimeComparison],
);
const comparisonLabels = useMemo(() => [t('Main'), '#', '△', '%'], []);
const comparisonLabels = [t('Main'), '#', '△', '%'];
const filteredColumnsMeta = useMemo(() => {
if (!isUsingTimeComparison) {
return columnsMeta;
@@ -463,79 +522,88 @@ export default function TableChart<D extends DataRecord = DataRecord>(
selectedComparisonColumns,
]);
const handleContextMenu =
onContextMenu && !isRawRecords
? (
value: D,
cellPoint: {
key: string;
value: DataRecordValue;
isMetric?: boolean;
},
clientX: number,
clientY: number,
) => {
const drillToDetailFilters: BinaryQueryObjectFilterClause[] = [];
filteredColumnsMeta.forEach(col => {
if (!col.isMetric) {
const dataRecordValue = value[col.key];
drillToDetailFilters.push({
col: col.key,
op: '==',
val: dataRecordValue as string | number | boolean,
formattedVal: formatColumnValue(col, dataRecordValue)[1],
});
}
});
onContextMenu(clientX, clientY, {
drillToDetail: drillToDetailFilters,
crossFilter: cellPoint.isMetric
? undefined
: getCrossFilterDataMask(cellPoint.key, cellPoint.value),
drillBy: cellPoint.isMetric
? undefined
: {
filters: [
{
col: cellPoint.key,
op: '==',
val: cellPoint.value as string | number | boolean,
},
],
groupbyFieldName: 'groupby',
},
});
}
: undefined;
const handleContextMenu = useMemo(() => {
if (onContextMenu && !isRawRecords) {
return (
value: D,
cellPoint: {
key: string;
value: DataRecordValue;
isMetric?: boolean;
},
clientX: number,
clientY: number,
) => {
const drillToDetailFilters: BinaryQueryObjectFilterClause[] = [];
filteredColumnsMeta.forEach(col => {
if (!col.isMetric) {
let dataRecordValue = value[col.key];
dataRecordValue = extractTextFromHTML(dataRecordValue);
const getHeaderColumns = (
columnsMeta: DataColumnMeta[],
enableTimeComparison?: boolean,
) => {
const resultMap: Record<string, number[]> = {};
if (!enableTimeComparison) {
return resultMap;
drillToDetailFilters.push({
col: col.key,
op: '==',
val: dataRecordValue as string | number | boolean,
formattedVal: formatColumnValue(col, dataRecordValue)[1],
});
}
});
onContextMenu(clientX, clientY, {
drillToDetail: drillToDetailFilters,
crossFilter: cellPoint.isMetric
? undefined
: getCrossFilterDataMask(cellPoint.key, cellPoint.value),
drillBy: cellPoint.isMetric
? undefined
: {
filters: [
{
col: cellPoint.key,
op: '==',
val: extractTextFromHTML(cellPoint.value),
},
],
groupbyFieldName: 'groupby',
},
});
};
}
return undefined;
}, [
onContextMenu,
isRawRecords,
filteredColumnsMeta,
getCrossFilterDataMask,
]);
columnsMeta.forEach((element, index) => {
// Check if element's label is one of the comparison labels
if (comparisonLabels.includes(element.label)) {
// Extract the key portion after the space, assuming the format is always "label key"
const keyPortion = element.key.substring(element.label.length);
const getHeaderColumns = useCallback(
(columnsMeta: DataColumnMeta[], enableTimeComparison?: boolean) => {
const resultMap: Record<string, number[]> = {};
// If the key portion is not in the map, initialize it with the current index
if (!resultMap[keyPortion]) {
resultMap[keyPortion] = [index];
} else {
// Add the index to the existing array
resultMap[keyPortion].push(index);
}
if (!enableTimeComparison) {
return resultMap;
}
});
return resultMap;
};
columnsMeta.forEach((element, index) => {
// Check if element's label is one of the comparison labels
if (comparisonLabels.includes(element.label)) {
// Extract the key portion after the space, assuming the format is always "label key"
const keyPortion = element.key.substring(element.label.length);
// If the key portion is not in the map, initialize it with the current index
if (!resultMap[keyPortion]) {
resultMap[keyPortion] = [index];
} else {
// Add the index to the existing array
resultMap[keyPortion].push(index);
}
}
});
return resultMap;
},
[comparisonLabels],
);
const renderTimeComparisonDropdown = (): JSX.Element => {
const allKey = comparisonColumns[0].key;
@@ -630,6 +698,11 @@ export default function TableChart<D extends DataRecord = DataRecord>(
);
};
const groupHeaderColumns = useMemo(
() => getHeaderColumns(filteredColumnsMeta, isUsingTimeComparison),
[filteredColumnsMeta, getHeaderColumns, isUsingTimeComparison],
);
const renderGroupingHeaders = (): JSX.Element => {
// TODO: Make use of ColumnGroup to render the aditional headers
const headers: any = [];
@@ -711,11 +784,6 @@ export default function TableChart<D extends DataRecord = DataRecord>(
);
};
const groupHeaderColumns = useMemo(
() => getHeaderColumns(filteredColumnsMeta, isUsingTimeComparison),
[filteredColumnsMeta, isUsingTimeComparison],
);
const getColumnConfigs = useCallback(
(
column: DataColumnMeta,
@@ -801,6 +869,9 @@ export default function TableChart<D extends DataRecord = DataRecord>(
}
}
// Cache sanitized header ID to avoid recomputing it multiple times
const headerId = sanitizeHeaderId(column.originalLabel ?? column.key);
return {
id: String(i), // to allow duplicate column keys
// must use custom accessor to allow `.` in column names
@@ -813,6 +884,7 @@ export default function TableChart<D extends DataRecord = DataRecord>(
const html = isHtml && allowRenderHtml ? { __html: text } : undefined;
let backgroundColor;
let color;
let arrow = '';
const originKey = column.key.substring(column.label.length).trim();
if (!hasColumnColorFormatters && hasBasicColorFormatters) {
@@ -825,17 +897,33 @@ export default function TableChart<D extends DataRecord = DataRecord>(
}
if (hasColumnColorFormatters) {
columnColorFormatters!
const applyFormatter = (
formatter: ColorFormatters[number],
valueToFormat: any,
) => {
const hasValue =
valueToFormat !== undefined && valueToFormat !== null;
if (!hasValue) return;
const formatterResult =
formatter.getColorFromValue(valueToFormat);
if (!formatterResult) return;
if (formatter.toTextColor) {
color = formatterResult.slice(0, -2);
} else {
backgroundColor = formatterResult;
}
};
columnColorFormatters
.filter(formatter => formatter.column === column.key)
.forEach(formatter => {
const formatterResult =
value || value === 0
? formatter.getColorFromValue(value as number)
: false;
if (formatterResult) {
backgroundColor = formatterResult;
}
});
.forEach(formatter => applyFormatter(formatter, value));
columnColorFormatters
.filter(formatter => formatter.toAllRow)
.forEach(formatter =>
applyFormatter(formatter, row.original[formatter.column]),
);
}
if (
@@ -851,7 +939,7 @@ export default function TableChart<D extends DataRecord = DataRecord>(
: '';
}
const StyledCell = styled.td`
color: ${theme.colorText};
color: ${color ? `${color}FF` : theme.colorText};
text-align: ${sharedStyle.textAlign};
white-space: ${value instanceof Date ? 'nowrap' : undefined};
position: relative;
@@ -881,6 +969,7 @@ export default function TableChart<D extends DataRecord = DataRecord>(
background-color: ${cellBackground({
value: value as number,
colorPositiveNegative,
theme,
})};
`}
`;
@@ -908,7 +997,7 @@ export default function TableChart<D extends DataRecord = DataRecord>(
}
const cellProps = {
'aria-labelledby': `header-${column.key}`,
'aria-labelledby': `header-${headerId}`,
role: 'cell',
// show raw number in title in case of numeric values
title: typeof value === 'number' ? String(value) : undefined,
@@ -995,7 +1084,7 @@ export default function TableChart<D extends DataRecord = DataRecord>(
},
Header: ({ column: col, onClick, style, onDragStart, onDrop }) => (
<th
id={`header-${column.originalLabel}`}
id={`header-${headerId}`}
title={t('Shift + Click to sort by multiple columns')}
className={[className, col.isSorted ? 'is-sorted' : ''].join(' ')}
style={{
@@ -1077,18 +1166,27 @@ export default function TableChart<D extends DataRecord = DataRecord>(
};
},
[
getSharedStyle,
defaultAlignPN,
defaultColorPN,
emitCrossFilters,
getValueRange,
isActiveFilterValue,
isRawRecords,
showCellBars,
sortDesc,
toggleFilter,
totals,
columnColorFormatters,
columnOrderToggle,
isUsingTimeComparison,
basicColorFormatters,
showCellBars,
isRawRecords,
getValueRange,
emitCrossFilters,
comparisonLabels,
totals,
theme,
sortDesc,
groupHeaderColumns,
allowRenderHtml,
basicColorColumnFormatters,
isActiveFilterValue,
toggleFilter,
handleContextMenu,
allowRearrangeColumns,
],
);
@@ -1121,7 +1219,7 @@ export default function TableChart<D extends DataRecord = DataRecord>(
if (!isEqual(options, searchOptions)) {
setSearchOptions(options || []);
}
}, [columns]);
}, [columns, searchOptions]);
const handleServerPaginationChange = useCallback(
(pageNumber: number, pageSize: number) => {
@@ -1132,7 +1230,7 @@ export default function TableChart<D extends DataRecord = DataRecord>(
};
updateTableOwnState(setDataMask, modifiedOwnState);
},
[setDataMask],
[serverPaginationData, setDataMask],
);
useEffect(() => {
@@ -1144,7 +1242,12 @@ export default function TableChart<D extends DataRecord = DataRecord>(
};
updateTableOwnState(setDataMask, modifiedOwnState);
}
}, []);
}, [
hasServerPageLengthChanged,
serverPageLength,
serverPaginationData,
setDataMask,
]);
const handleSizeChange = useCallback(
({ width, height }: { width: number; height: number }) => {
@@ -1190,12 +1293,12 @@ export default function TableChart<D extends DataRecord = DataRecord>(
};
updateTableOwnState(setDataMask, modifiedOwnState);
},
[setDataMask, serverPagination],
[serverPagination, serverPaginationData, setDataMask],
);
const handleSearch = (searchText: string) => {
const modifiedOwnState = {
...(serverPaginationData || {}),
...serverPaginationData,
searchColumn:
serverPaginationData?.searchColumn || searchOptions[0]?.value,
searchText,
@@ -1209,7 +1312,7 @@ export default function TableChart<D extends DataRecord = DataRecord>(
const handleChangeSearchCol = (searchCol: string) => {
if (!isEqual(searchCol, serverPaginationData?.searchColumn)) {
const modifiedOwnState = {
...(serverPaginationData || {}),
...serverPaginationData,
searchColumn: searchCol,
searchText: '',
};

View File

@@ -265,7 +265,7 @@ const buildQuery: BuildQuery<TableChartFormData> = (
) {
queryObject = { ...queryObject, row_offset: 0 };
const modifiedOwnState = {
...(options?.ownState || {}),
...options?.ownState,
currentPage: 0,
pageSize: queryObject.row_limit ?? 0,
};

View File

@@ -39,10 +39,10 @@ import {
shouldSkipMetricColumn,
isRegularMetric,
isPercentMetric,
ConditionalFormattingConfig,
} from '@superset-ui/chart-controls';
import {
ensureIsArray,
GenericDataType,
isAdhocColumn,
isPhysicalColumn,
legacyValidateInteger,
@@ -53,7 +53,7 @@ import {
validateMaxValue,
validateServerPagination,
} from '@superset-ui/core';
import { GenericDataType } from '@apache-superset/core/api/core';
import { isEmpty, last } from 'lodash';
import { PAGE_SIZE_OPTIONS, SERVER_PAGE_SIZE_OPTIONS } from './consts';
import { ColorSchemeEnum } from './types';
@@ -768,6 +768,26 @@ const config: ControlPanelConfig = {
? (explore?.datasource as Dataset)?.verbose_map
: (explore?.datasource?.columns ?? {});
const chartStatus = chart?.chartStatus;
const value = _?.value ?? [];
if (value && Array.isArray(value)) {
value.forEach(
(item: ConditionalFormattingConfig, index, array) => {
if (
item.colorScheme &&
!['Green', 'Red'].includes(item.colorScheme)
) {
if (!item.toAllRow || !item.toTextColor) {
// eslint-disable-next-line no-param-reassign
array[index] = {
...item,
toAllRow: item.toAllRow ?? false,
toTextColor: item.toTextColor ?? false,
};
}
}
},
);
}
const { colnames, coltypes } =
chart?.queriesResponse?.[0] ?? {};
const numericColumns =
@@ -802,6 +822,10 @@ const config: ControlPanelConfig = {
removeIrrelevantConditions: chartStatus === 'success',
columnOptions,
verboseMap,
conditionalFormattingFlag: {
toAllRowCheck: true,
toColorTextCheck: true,
},
};
},
},

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 93 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 159 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

View File

@@ -19,9 +19,13 @@
import { Behavior, ChartMetadata, ChartPlugin, t } from '@superset-ui/core';
import transformProps from './transformProps';
import thumbnail from './images/thumbnail.png';
import thumbnailDark from './images/thumbnail-dark.png';
import example1 from './images/Table.jpg';
import example1Dark from './images/Table-dark.jpg';
import example2 from './images/Table2.jpg';
import example2Dark from './images/Table2-dark.jpg';
import example3 from './images/Table3.jpg';
import example3Dark from './images/Table3-dark.jpg';
import controlPanel from './controlPanel';
import buildQuery from './buildQuery';
import { TableChartFormData, TableChartProps } from './types';
@@ -41,7 +45,11 @@ const metadata = new ChartMetadata({
description: t(
'Classic row-by-column spreadsheet like view of a dataset. Use tables to showcase a view into the underlying data or to show aggregated metrics.',
),
exampleGallery: [{ url: example1 }, { url: example2 }, { url: example3 }],
exampleGallery: [
{ url: example1, urlDark: example1Dark },
{ url: example2, urlDark: example2Dark },
{ url: example3, urlDark: example3Dark },
],
name: t('Table'),
tags: [
t('Additive'),
@@ -53,6 +61,7 @@ const metadata = new ChartMetadata({
t('Tabular'),
],
thumbnail,
thumbnailDark,
});
export default class TableChartPlugin extends ChartPlugin<

View File

@@ -24,7 +24,6 @@ import {
DataRecord,
ensureIsArray,
extractTimegrain,
GenericDataType,
getMetricLabel,
getNumberFormatter,
getTimeFormatter,
@@ -36,6 +35,7 @@ import {
TimeFormats,
TimeFormatter,
} from '@superset-ui/core';
import { GenericDataType } from '@apache-superset/core/api/core';
import {
ColorFormatters,
ConditionalFormattingConfig,
@@ -469,6 +469,7 @@ const transformProps = (
onContextMenu,
},
emitCrossFilters,
theme,
} = chartProps;
const formData = merge(
@@ -682,7 +683,7 @@ const transformProps = (
const basicColorFormatters =
comparisonColorEnabled && getBasicColorFormatter(baseQuery?.data, columns);
const columnColorFormatters =
getColorFormatters(conditionalFormatting, passedData) ??
getColorFormatters(conditionalFormatting, passedData, theme) ??
defaultColorFormatters;
const basicColorColumnFormatters = getBasicColorFormatterForColumn(

View File

@@ -25,7 +25,6 @@ import {
DataRecord,
DataRecordValue,
DataRecordFilters,
GenericDataType,
QueryMode,
ChartDataResponseResult,
QueryFormData,
@@ -34,6 +33,7 @@ import {
CurrencyFormatter,
Currency,
} from '@superset-ui/core';
import { GenericDataType } from '@apache-superset/core/api/core';
import { ColorFormatters } from '@superset-ui/chart-controls';
export type CustomFormatter = (value: DataRecordValue) => string;

View File

@@ -19,11 +19,11 @@
import {
CurrencyFormatter,
DataRecordValue,
GenericDataType,
getNumberFormatter,
isProbablyHTML,
sanitizeHtml,
} from '@superset-ui/core';
import { GenericDataType } from '@apache-superset/core/api/core';
import { DataColumnMeta } from '../types';
import DateWithFormatter from './DateWithFormatter';

View File

@@ -18,15 +18,120 @@
*/
import '@testing-library/jest-dom';
import { render, screen } from '@superset-ui/core/spec';
import TableChart from '../src/TableChart';
import { cloneDeep } from 'lodash';
import TableChart, { sanitizeHeaderId } from '../src/TableChart';
import transformProps from '../src/transformProps';
import DateWithFormatter from '../src/utils/DateWithFormatter';
import testData from './testData';
import { ProviderWrapper } from './testHelpers';
const expectValidAriaLabels = (container: HTMLElement) => {
const allCells = container.querySelectorAll('tbody td');
const cellsWithLabels = container.querySelectorAll(
'tbody td[aria-labelledby]',
);
// Table must render data cells (catch empty table regression)
expect(allCells.length).toBeGreaterThan(0);
// ALL data cells must have aria-labelledby (no unlabeled cells)
expect(cellsWithLabels.length).toBe(allCells.length);
// ALL aria-labelledby values should be valid
cellsWithLabels.forEach(cell => {
const labelledBy = cell.getAttribute('aria-labelledby');
expect(labelledBy).not.toBeNull();
expect(labelledBy).toEqual(expect.stringMatching(/\S/));
const labelledByValue = labelledBy as string;
expect(labelledByValue).not.toMatch(/\s/);
expect(labelledByValue).not.toMatch(/[%#△]/);
const referencedHeader = container.querySelector(
`#${CSS.escape(labelledByValue)}`,
);
expect(referencedHeader).toBeTruthy();
});
};
test('sanitizeHeaderId should sanitize percent sign', () => {
expect(sanitizeHeaderId('%pct_nice')).toBe('percentpct_nice');
});
test('sanitizeHeaderId should sanitize hash/pound sign', () => {
expect(sanitizeHeaderId('# metric_1')).toBe('hash_metric_1');
});
test('sanitizeHeaderId should sanitize delta symbol', () => {
expect(sanitizeHeaderId('△ delta')).toBe('delta_delta');
});
test('sanitizeHeaderId should replace spaces with underscores', () => {
expect(sanitizeHeaderId('Main metric_1')).toBe('Main_metric_1');
expect(sanitizeHeaderId('multiple spaces')).toBe('multiple_spaces');
});
test('sanitizeHeaderId should handle multiple special characters', () => {
expect(sanitizeHeaderId('% #△ test')).toBe('percent_hashdelta_test');
expect(sanitizeHeaderId('% # △ test')).toBe('percent_hash_delta_test');
});
test('sanitizeHeaderId should preserve alphanumeric, underscore, and hyphen', () => {
expect(sanitizeHeaderId('valid-name_123')).toBe('valid-name_123');
});
test('sanitizeHeaderId should replace other special characters with underscore', () => {
expect(sanitizeHeaderId('col@name!test')).toBe('col_name_test');
expect(sanitizeHeaderId('test.column')).toBe('test_column');
});
test('sanitizeHeaderId should handle edge cases', () => {
expect(sanitizeHeaderId('')).toBe('');
expect(sanitizeHeaderId('simple')).toBe('simple');
});
test('sanitizeHeaderId should collapse consecutive underscores', () => {
expect(sanitizeHeaderId('test @@ space')).toBe('test_space');
expect(sanitizeHeaderId('col___name')).toBe('col_name');
expect(sanitizeHeaderId('a b c')).toBe('a_b_c');
expect(sanitizeHeaderId('test@@name')).toBe('test_name');
});
test('sanitizeHeaderId should remove leading underscores', () => {
expect(sanitizeHeaderId('@col')).toBe('col');
expect(sanitizeHeaderId('!revenue')).toBe('revenue');
expect(sanitizeHeaderId('@@test')).toBe('test');
expect(sanitizeHeaderId(' leading_spaces')).toBe('leading_spaces');
});
test('sanitizeHeaderId should remove trailing underscores', () => {
expect(sanitizeHeaderId('col@')).toBe('col');
expect(sanitizeHeaderId('revenue!')).toBe('revenue');
expect(sanitizeHeaderId('test@@')).toBe('test');
expect(sanitizeHeaderId('trailing_spaces ')).toBe('trailing_spaces');
});
test('sanitizeHeaderId should remove leading and trailing underscores', () => {
expect(sanitizeHeaderId('@col@')).toBe('col');
expect(sanitizeHeaderId('!test!')).toBe('test');
expect(sanitizeHeaderId(' spaced ')).toBe('spaced');
expect(sanitizeHeaderId('@@multiple@@')).toBe('multiple');
});
test('sanitizeHeaderId should handle inputs with only special characters', () => {
expect(sanitizeHeaderId('@')).toBe('');
expect(sanitizeHeaderId('@@')).toBe('');
expect(sanitizeHeaderId(' ')).toBe('');
expect(sanitizeHeaderId('!@$')).toBe('');
expect(sanitizeHeaderId('!@#$')).toBe('hash'); // # is replaced with 'hash' (semantic replacement)
// Semantic replacements produce readable output even when alone
expect(sanitizeHeaderId('%')).toBe('percent');
expect(sanitizeHeaderId('#')).toBe('hash');
expect(sanitizeHeaderId('△')).toBe('delta');
expect(sanitizeHeaderId('% # △')).toBe('percent_hash_delta');
});
describe('plugin-chart-table', () => {
describe('transformProps', () => {
it('should parse pageLength to pageSize', () => {
test('should parse pageLength to pageSize', () => {
expect(transformProps(testData.basic).pageSize).toBe(20);
expect(
transformProps({
@@ -42,13 +147,13 @@ describe('plugin-chart-table', () => {
).toBe(0);
});
it('should memoize data records', () => {
test('should memoize data records', () => {
expect(transformProps(testData.basic).data).toBe(
transformProps(testData.basic).data,
);
});
it('should memoize columns meta', () => {
test('should memoize columns meta', () => {
expect(transformProps(testData.basic).columns).toBe(
transformProps({
...testData.basic,
@@ -57,14 +162,14 @@ describe('plugin-chart-table', () => {
);
});
it('should format timestamp', () => {
test('should format timestamp', () => {
// eslint-disable-next-line no-underscore-dangle
const parsedDate = transformProps(testData.basic).data[0]
.__timestamp as DateWithFormatter;
expect(String(parsedDate)).toBe('2020-01-01 12:34:56');
expect(parsedDate.getTime()).toBe(1577882096000);
});
it('should process comparison columns when time_compare and comparison_type are set', () => {
test('should process comparison columns when time_compare and comparison_type are set', () => {
const transformedProps = transformProps(testData.comparison);
const comparisonColumns = transformedProps.columns.filter(
col =>
@@ -86,7 +191,7 @@ describe('plugin-chart-table', () => {
expect(comparisonColumns.some(col => col.label === '%')).toBe(true);
});
it('should not process comparison columns when time_compare is empty', () => {
test('should not process comparison columns when time_compare is empty', () => {
const propsWithoutTimeCompare = {
...testData.comparison,
rawFormData: {
@@ -109,7 +214,7 @@ describe('plugin-chart-table', () => {
expect(comparisonColumns.length).toBe(0);
});
it('should correctly apply column configuration for comparison columns', () => {
test('should correctly apply column configuration for comparison columns', () => {
const transformedProps = transformProps(testData.comparisonWithConfig);
const comparisonColumns = transformedProps.columns.filter(
@@ -147,7 +252,7 @@ describe('plugin-chart-table', () => {
expect(percentMetricConfig?.config).toEqual({ d3NumberFormat: '.3f' });
});
it('should correctly format comparison columns using getComparisonColFormatter', () => {
test('should correctly format comparison columns using getComparisonColFormatter', () => {
const transformedProps = transformProps(testData.comparisonWithConfig);
const comparisonColumns = transformedProps.columns.filter(
col =>
@@ -178,7 +283,7 @@ describe('plugin-chart-table', () => {
expect(formattedPercentMetric).toBe('0.123');
});
it('should set originalLabel for comparison columns when time_compare and comparison_type are set', () => {
test('should set originalLabel for comparison columns when time_compare and comparison_type are set', () => {
const transformedProps = transformProps(testData.comparison);
// Check if comparison columns are processed
@@ -265,7 +370,7 @@ describe('plugin-chart-table', () => {
});
describe('TableChart', () => {
it('render basic data', () => {
test('render basic data', () => {
render(
<TableChart {...transformProps(testData.basic)} sticky={false} />,
);
@@ -284,12 +389,9 @@ describe('plugin-chart-table', () => {
expect(cells[8]).toHaveTextContent('N/A');
});
it('render advanced data', () => {
test('render advanced data', () => {
render(
<>
<TableChart {...transformProps(testData.advanced)} sticky={false} />
,
</>,
<TableChart {...transformProps(testData.advanced)} sticky={false} />,
);
const secondColumnHeader = screen.getByText('Sum of Num');
expect(secondColumnHeader).toBeInTheDocument();
@@ -304,7 +406,7 @@ describe('plugin-chart-table', () => {
expect(cells[4]).toHaveTextContent('2.47k');
});
it('render advanced data with currencies', () => {
test('render advanced data with currencies', () => {
render(
ProviderWrapper({
children: (
@@ -324,7 +426,7 @@ describe('plugin-chart-table', () => {
expect(cells[4]).toHaveTextContent('$ 2.47k');
});
it('render data with a bigint value in a raw record mode', () => {
test('render data with a bigint value in a raw record mode', () => {
render(
ProviderWrapper({
children: (
@@ -345,7 +447,7 @@ describe('plugin-chart-table', () => {
expect(cells[3]).toHaveTextContent('1234567890123456789');
});
it('render raw data', () => {
test('render raw data', () => {
const props = transformProps({
...testData.raw,
rawFormData: { ...testData.raw.rawFormData },
@@ -362,7 +464,7 @@ describe('plugin-chart-table', () => {
expect(cells[1]).toHaveTextContent('0');
});
it('render raw data with currencies', () => {
test('render raw data with currencies', () => {
const props = transformProps({
...testData.raw,
rawFormData: {
@@ -387,7 +489,7 @@ describe('plugin-chart-table', () => {
expect(cells[2]).toHaveTextContent('$ 0');
});
it('render small formatted data with currencies', () => {
test('render small formatted data with currencies', () => {
const props = transformProps({
...testData.raw,
rawFormData: {
@@ -429,14 +531,14 @@ describe('plugin-chart-table', () => {
expect(cells[2]).toHaveTextContent('$ 0.61');
});
it('render empty data', () => {
test('render empty data', () => {
render(
<TableChart {...transformProps(testData.empty)} sticky={false} />,
);
expect(screen.getByText('No records found')).toBeInTheDocument();
});
it('render color with column color formatter', () => {
test('render color with column color formatter', () => {
render(
ProviderWrapper({
children: (
@@ -466,8 +568,8 @@ describe('plugin-chart-table', () => {
expect(getComputedStyle(screen.getByTitle('2467')).background).toBe('');
});
it('render cell without color', () => {
const dataWithEmptyCell = testData.advanced.queriesData[0];
test('render cell without color', () => {
const dataWithEmptyCell = cloneDeep(testData.advanced.queriesData[0]);
dataWithEmptyCell.data.push({
__timestamp: null,
name: 'Noah',
@@ -507,7 +609,7 @@ describe('plugin-chart-table', () => {
);
expect(getComputedStyle(screen.getByText('N/A')).background).toBe('');
});
it('should display original label in grouped headers', () => {
test('should display original label in grouped headers', () => {
const props = transformProps(testData.comparison);
render(<TableChart {...props} sticky={false} />);
@@ -522,7 +624,128 @@ describe('plugin-chart-table', () => {
expect(hasMetricHeaders).toBe(true);
});
it('render cell bars properly, and only when it is toggled on in both regular and percent metrics', () => {
test('should set meaningful header IDs for time-comparison columns', () => {
// Test time-comparison columns have proper IDs
// Uses originalLabel (e.g., "metric_1") which is sanitized for CSS safety
const props = transformProps(testData.comparison);
render(<TableChart {...props} sticky={false} />);
const headers = screen.getAllByRole('columnheader');
// All headers should have IDs
const headersWithIds = headers.filter(header => header.id);
expect(headersWithIds.length).toBeGreaterThan(0);
// None should have "header-undefined"
const undefinedHeaders = headersWithIds.filter(header =>
header.id.includes('undefined'),
);
expect(undefinedHeaders).toHaveLength(0);
// Should have IDs based on sanitized originalLabel (e.g., "metric_1")
const hasMetricHeaders = headersWithIds.some(
header =>
header.id.includes('metric_1') || header.id.includes('metric_2'),
);
expect(hasMetricHeaders).toBe(true);
// CRITICAL: Verify sanitization - no spaces or special chars in any header ID
headersWithIds.forEach(header => {
// IDs must not contain spaces (would break CSS selectors and ARIA)
expect(header.id).not.toMatch(/\s/);
// IDs must not contain special chars like %, #, △
expect(header.id).not.toMatch(/[%#△]/);
// IDs should only contain valid characters: alphanumeric, underscore, hyphen
expect(header.id).toMatch(/^header-[a-zA-Z0-9_-]+$/);
});
});
test('should validate ARIA references for time-comparison table cells', () => {
// Test that ALL cells with aria-labelledby have valid references
// This is critical for screen reader accessibility
const props = transformProps(testData.comparison);
const { container } = render(<TableChart {...props} sticky={false} />);
expectValidAriaLabels(container);
});
test('should set meaningful header IDs for regular table columns', () => {
// Test regular (non-time-comparison) columns have proper IDs
// Uses fallback to column.key since originalLabel is undefined
const props = transformProps(testData.advanced);
const { container } = render(
ProviderWrapper({
children: <TableChart {...props} sticky={false} />,
}),
);
const headers = screen.getAllByRole('columnheader');
// Test 1: "name" column (regular string column)
const nameHeader = headers.find(header =>
header.textContent?.includes('name'),
);
expect(nameHeader).toBeDefined();
expect(nameHeader?.id).toBe('header-name'); // Falls back to column.key
// Verify cells reference this header correctly
const nameCells = container.querySelectorAll(
'td[aria-labelledby="header-name"]',
);
expect(nameCells.length).toBeGreaterThan(0);
// Test 2: "sum__num" column (metric with verbose map "Sum of Num")
const sumHeader = headers.find(header =>
header.textContent?.includes('Sum of Num'),
);
expect(sumHeader).toBeDefined();
expect(sumHeader?.id).toBe('header-sum_num'); // Falls back to column.key, consecutive underscores collapsed
// Verify cells reference this header correctly
const sumCells = container.querySelectorAll(
'td[aria-labelledby="header-sum_num"]',
);
expect(sumCells.length).toBeGreaterThan(0);
// Test 3: Verify NO headers have "undefined" in their ID
const undefinedHeaders = headers.filter(header =>
header.id?.includes('undefined'),
);
expect(undefinedHeaders).toHaveLength(0);
// Test 4: Verify ALL headers have proper IDs (no missing IDs)
const headersWithIds = headers.filter(header => header.id);
expect(headersWithIds.length).toBe(headers.length);
// Test 5: Verify ALL header IDs are properly sanitized
headersWithIds.forEach(header => {
// IDs must not contain spaces
expect(header.id).not.toMatch(/\s/);
// IDs must not contain special chars like % (from %pct_nice column)
expect(header.id).not.toMatch(/[%#△]/);
// IDs should only contain valid CSS selector characters
expect(header.id).toMatch(/^header-[a-zA-Z0-9_-]+$/);
});
});
test('should validate ARIA references for regular table cells', () => {
// Test that ALL cells with aria-labelledby have valid references
// This is critical for screen reader accessibility
const props = transformProps(testData.advanced);
const { container } = render(
ProviderWrapper({
children: <TableChart {...props} sticky={false} />,
}),
);
expectValidAriaLabels(container);
});
test('render cell bars properly, and only when it is toggled on in both regular and percent metrics', () => {
const props = transformProps({
...testData.raw,
rawFormData: { ...testData.raw.rawFormData },
@@ -572,7 +795,7 @@ describe('plugin-chart-table', () => {
cells = document.querySelectorAll('td');
});
it('render color with string column color formatter(operator begins with)', () => {
test('render color with string column color formatter(operator begins with)', () => {
render(
ProviderWrapper({
children: (
@@ -604,7 +827,7 @@ describe('plugin-chart-table', () => {
);
});
it('render color with string column color formatter (operator ends with)', () => {
test('render color with string column color formatter (operator ends with)', () => {
render(
ProviderWrapper({
children: (
@@ -633,7 +856,7 @@ describe('plugin-chart-table', () => {
expect(getComputedStyle(screen.getByText('Joe')).background).toBe('');
});
it('render color with string column color formatter (operator containing)', () => {
test('render color with string column color formatter (operator containing)', () => {
render(
ProviderWrapper({
children: (
@@ -662,7 +885,7 @@ describe('plugin-chart-table', () => {
expect(getComputedStyle(screen.getByText('Joe')).background).toBe('');
});
it('render color with string column color formatter (operator not containing)', () => {
test('render color with string column color formatter (operator not containing)', () => {
render(
ProviderWrapper({
children: (
@@ -693,7 +916,7 @@ describe('plugin-chart-table', () => {
);
});
it('render color with string column color formatter (operator =)', () => {
test('render color with string column color formatter (operator =)', () => {
render(
ProviderWrapper({
children: (
@@ -724,7 +947,7 @@ describe('plugin-chart-table', () => {
);
});
it('render color with string column color formatter (operator None)', () => {
test('render color with string column color formatter (operator None)', () => {
render(
ProviderWrapper({
children: (
@@ -756,6 +979,112 @@ describe('plugin-chart-table', () => {
'rgba(172, 225, 196, 1)',
);
});
test('render color with column color formatter to entire row', () => {
render(
ProviderWrapper({
children: (
<TableChart
{...transformProps({
...testData.advanced,
rawFormData: {
...testData.advanced.rawFormData,
conditional_formatting: [
{
colorScheme: '#ACE1C4',
column: 'sum__num',
operator: '>',
targetValue: 2467,
toAllRow: true,
},
],
},
})}
/>
),
}),
);
expect(getComputedStyle(screen.getByText('Michael')).background).toBe(
'rgba(172, 225, 196, 1)',
);
expect(getComputedStyle(screen.getByTitle('2467063')).background).toBe(
'rgba(172, 225, 196, 1)',
);
expect(getComputedStyle(screen.getByTitle('0.123456')).background).toBe(
'rgba(172, 225, 196, 1)',
);
});
test('display text color using column color formatter', () => {
render(
ProviderWrapper({
children: (
<TableChart
{...transformProps({
...testData.advanced,
rawFormData: {
...testData.advanced.rawFormData,
conditional_formatting: [
{
colorScheme: '#ACE1C4',
column: 'sum__num',
operator: '>',
targetValue: 2467,
toTextColor: true,
},
],
},
})}
/>
),
}),
);
expect(getComputedStyle(screen.getByTitle('2467063')).color).toBe(
'rgba(172, 225, 196, 1)',
);
expect(getComputedStyle(screen.getByTitle('2467')).color).toBe(
'rgba(0, 0, 0, 0.88)',
);
});
test('display text color using column color formatter for entire row', () => {
render(
ProviderWrapper({
children: (
<TableChart
{...transformProps({
...testData.advanced,
rawFormData: {
...testData.advanced.rawFormData,
conditional_formatting: [
{
colorScheme: '#ACE1C4',
column: 'sum__num',
operator: '>',
targetValue: 2467,
toAllRow: true,
toTextColor: true,
},
],
},
})}
/>
),
}),
);
expect(getComputedStyle(screen.getByText('Michael')).color).toBe(
'rgba(172, 225, 196, 1)',
);
expect(getComputedStyle(screen.getByTitle('2467063')).color).toBe(
'rgba(172, 225, 196, 1)',
);
expect(getComputedStyle(screen.getByTitle('0.123456')).color).toBe(
'rgba(172, 225, 196, 1)',
);
});
});
});
});

View File

@@ -20,12 +20,12 @@ import {
ChartDataResponseResult,
ChartProps,
DatasourceType,
GenericDataType,
QueryMode,
supersetTheme,
ComparisonType,
VizType,
} from '@superset-ui/core';
import { supersetTheme } from '@apache-superset/core/ui';
import { GenericDataType } from '@apache-superset/core/api/core';
import { TableChartProps, TableChartFormData } from '../src/types';
const basicFormData: TableChartFormData = {

View File

@@ -17,11 +17,11 @@
* under the License.
*/
import {
EmotionCacheProvider,
createEmotionCache,
supersetTheme,
ThemeProvider,
} from '@superset-ui/core';
EmotionCacheProvider,
createEmotionCache,
} from '@apache-superset/core/ui';
const emotionCache = createEmotionCache({
key: 'test',

View File

@@ -2,18 +2,8 @@
"compilerOptions": {
"composite": false,
"emitDeclarationOnly": false,
"noEmit": true,
"rootDir": "../../../"
},
"extends": "../../../tsconfig.json",
"include": [
"**/*",
"../types/**/*",
"../../../types/**/*"
],
"references": [
{
"path": ".."
}
]
"include": ["**/*", "../types/**/*", "../../../types/**/*"]
}

View File

@@ -1,17 +1,25 @@
{
"extends": "../../tsconfig.base.json",
"extends": "../../tsconfig.json",
"compilerOptions": {
"composite": true,
"rootDir": "src",
// Path Resolution: Override baseUrl to maintain correct path mappings from parent config
// (e.g., "@apache-superset/core" -> "./packages/superset-core/src")
"baseUrl": "../..",
// Directory Overrides: Parent config paths are relative to frontend root,
// but packages need paths relative to their own directory
"outDir": "lib",
"baseUrl": ".",
"paths": {
"@superset-ui/core/components": ["../../packages/superset-ui-core/src/components"]
}
"rootDir": "src",
"declarationDir": "lib"
},
"include": ["src/**/*", "types/**/*"],
"exclude": ["lib", "test"],
"include": ["src/**/*.ts", "src/**/*.tsx", "types/**/*"],
"exclude": [
"src/**/*.js",
"src/**/*.jsx",
"src/**/*.test.*",
"src/**/*.stories.*"
],
"references": [
{ "path": "../../packages/superset-core" },
{ "path": "../../packages/superset-ui-core" },
{ "path": "../../packages/superset-ui-chart-controls" }
]