mirror of
https://github.com/apache/superset.git
synced 2026-05-30 04:39:20 +00:00
Merge branch 'master' into msyavuz/chore/react-18
This commit is contained in:
@@ -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": "*",
|
||||
|
||||
@@ -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()}
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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"
|
||||
>
|
||||
|
||||
@@ -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`
|
||||
|
||||
@@ -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: '',
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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 |
@@ -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<
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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)',
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -2,18 +2,8 @@
|
||||
"compilerOptions": {
|
||||
"composite": false,
|
||||
"emitDeclarationOnly": false,
|
||||
"noEmit": true,
|
||||
"rootDir": "../../../"
|
||||
},
|
||||
"extends": "../../../tsconfig.json",
|
||||
"include": [
|
||||
"**/*",
|
||||
"../types/**/*",
|
||||
"../../../types/**/*"
|
||||
],
|
||||
"references": [
|
||||
{
|
||||
"path": ".."
|
||||
}
|
||||
]
|
||||
"include": ["**/*", "../types/**/*", "../../../types/**/*"]
|
||||
}
|
||||
|
||||
@@ -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" }
|
||||
]
|
||||
|
||||
Reference in New Issue
Block a user