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

This commit is contained in:
Mehmet Salih Yavuz
2025-09-07 13:48:10 +03:00
1560 changed files with 124308 additions and 82169 deletions

View File

@@ -451,9 +451,7 @@ export default typedMemo(function DataTable<D extends object>({
{hasGlobalControl ? (
<div ref={globalControlRef} className="form-inline dt-controls">
<StyledRow className="row">
<div
className={renderTimeComparisonDropdown ? 'col-sm-4' : 'col-sm-5'}
>
<StyledSpace size="middle">
{hasPagination ? (
<SelectPageSize
total={resultsSize}
@@ -467,23 +465,17 @@ export default typedMemo(function DataTable<D extends object>({
onChange={setPageSize}
/>
) : null}
</div>
{searchInput ? (
<StyledSpace
className={
renderTimeComparisonDropdown ? 'col-sm-7' : 'col-sm-8'
}
>
{serverPagination && (
<div className="search-select-container">
<span className="search-by-label">Search by: </span>
<SearchSelectDropdown
searchOptions={searchOptions}
value={serverPaginationData?.searchColumn || ''}
onChange={onSearchColChange}
/>
</div>
)}
{serverPagination && (
<div className="search-select-container">
<span className="search-by-label">Search by: </span>
<SearchSelectDropdown
searchOptions={searchOptions}
value={serverPaginationData?.searchColumn || ''}
onChange={onSearchColChange}
/>
</div>
)}
{searchInput && (
<GlobalFilter<D>
searchInput={
typeof searchInput === 'boolean' ? undefined : searchInput
@@ -497,16 +489,11 @@ export default typedMemo(function DataTable<D extends object>({
serverPagination={!!serverPagination}
rowCount={rowCount}
/>
</StyledSpace>
) : null}
{renderTimeComparisonDropdown ? (
<div
className="col-sm-1"
style={{ float: 'right', marginTop: '6px' }}
>
{renderTimeComparisonDropdown()}
</div>
) : null}
)}
{renderTimeComparisonDropdown
? renderTimeComparisonDropdown()
: null}
</StyledSpace>
</StyledRow>
</div>
) : null}

View File

@@ -149,7 +149,12 @@ export default styled.div`
.dt-pagination {
text-align: right;
/* use padding instead of margin so clientHeight can capture it */
padding-top: 0.5em;
padding: ${theme.paddingXXS}px 0px;
}
.dt-pagination .pagination > li {
display: inline;
margin: 0 ${theme.marginXXS}px;
}
.dt-pagination .pagination > li > a,
@@ -157,6 +162,8 @@ export default styled.div`
background-color: ${theme.colorBgBase};
color: ${theme.colorText};
border-color: ${theme.colorBorderSecondary};
padding: ${theme.paddingXXS}px ${theme.paddingXS}px;
border-radius: ${theme.borderRadius}px;
}
.dt-pagination .pagination > li.active > a,

View File

@@ -59,7 +59,6 @@ import {
Space,
RawAntdSelect as Select,
Dropdown,
Menu,
Tooltip,
} from '@superset-ui/core/components';
import {
@@ -198,7 +197,6 @@ function SearchInput({
<Space direction="horizontal" size={4} className="dt-global-filter">
{t('Search')}
<Input
size="small"
aria-label={t('Search %s records', count)}
placeholder={tn('search.num_records', count)}
value={value}
@@ -324,8 +322,10 @@ export default function TableChart<D extends DataRecord = DataRecord>(
const getValueRange = useCallback(
function getValueRange(key: string, alignPositiveNegative: boolean) {
if (typeof data?.[0]?.[key] === 'number') {
const nums = data.map(row => row[key]) as number[];
const nums = data
?.map(row => row?.[key])
.filter(value => typeof value === 'number') as number[];
if (data && nums.length === data.length) {
return (
alignPositiveNegative
? [0, d3Max(nums.map(Math.abs))]
@@ -564,52 +564,62 @@ export default function TableChart<D extends DataRecord = DataRecord>(
return (
<Dropdown
placement="bottomRight"
visible={showComparisonDropdown}
onVisibleChange={(flag: boolean) => {
open={showComparisonDropdown}
onOpenChange={(flag: boolean) => {
setShowComparisonDropdown(flag);
}}
overlay={
<Menu
multiple
onClick={handleOnClick}
onBlur={handleOnBlur}
selectedKeys={selectedComparisonColumns}
>
<div
css={css`
max-width: 242px;
padding: 0 ${theme.sizeUnit * 2}px;
color: ${theme.colorText};
font-size: ${theme.fontSizeSM}px;
`}
>
{t(
'Select columns that will be displayed in the table. You can multiselect columns.',
)}
</div>
{comparisonColumns.map(column => (
<Menu.Item key={column.key}>
<span
menu={{
multiple: true,
onClick: handleOnClick,
onBlur: handleOnBlur,
selectedKeys: selectedComparisonColumns,
items: [
{
key: 'all',
label: (
<div
css={css`
max-width: 242px;
padding: 0 ${theme.sizeUnit * 2}px;
color: ${theme.colorText};
`}
>
{column.label}
</span>
<span
css={css`
float: right;
font-size: ${theme.fontSizeSM}px;
`}
>
{selectedComparisonColumns.includes(column.key) && (
<CheckOutlined />
{t(
'Select columns that will be displayed in the table. You can multiselect columns.',
)}
</span>
</Menu.Item>
))}
</Menu>
}
</div>
),
type: 'group',
children: comparisonColumns.map(
(column: { key: string; label: string }) => ({
key: column.key,
label: (
<>
<span
css={css`
color: ${theme.colorText};
`}
>
{column.label}
</span>
<span
css={css`
float: right;
font-size: ${theme.fontSizeSM}px;
`}
>
{selectedComparisonColumns.includes(column.key) && (
<CheckOutlined />
)}
</span>
</>
),
}),
),
},
],
}}
trigger={['click']}
>
<span>
@@ -629,7 +639,11 @@ export default function TableChart<D extends DataRecord = DataRecord>(
const startPosition = value[0];
const colSpan = value.length;
// Retrieve the originalLabel from the first column in this group
const originalLabel = columnsMeta[value[0]]?.originalLabel || key;
const firstColumnInGroup = filteredColumnsMeta[startPosition];
const originalLabel = firstColumnInGroup
? columnsMeta.find(col => col.key === firstColumnInGroup.key)
?.originalLabel || key
: key;
// Add placeholder <th> for columns before this header
for (let i = currentColumnIndex; i < startPosition; i += 1) {
@@ -711,7 +725,6 @@ export default function TableChart<D extends DataRecord = DataRecord>(
const {
key,
label: originalLabel,
isNumeric,
dataType,
isMetric,
isPercentMetric,
@@ -756,7 +769,6 @@ export default function TableChart<D extends DataRecord = DataRecord>(
const { truncateLongCells } = config;
const hasColumnColorFormatters =
isNumeric &&
Array.isArray(columnColorFormatters) &&
columnColorFormatters.length > 0;

View File

@@ -17,10 +17,7 @@
* under the License.
*/
import { formatSelectOptions } from '@superset-ui/chart-controls';
import { addLocaleData, t } from '@superset-ui/core';
import i18n from './i18n';
addLocaleData(i18n);
import { t } from '@superset-ui/core';
export const PAGE_SIZE_OPTIONS = formatSelectOptions<number>([
[0, t('page_size.all')],

View File

@@ -36,16 +36,17 @@ import {
QueryModeLabel,
sections,
sharedControls,
shouldSkipMetricColumn,
isRegularMetric,
isPercentMetric,
} from '@superset-ui/chart-controls';
import {
ensureIsArray,
GenericDataType,
getMetricLabel,
isAdhocColumn,
isPhysicalColumn,
legacyValidateInteger,
QueryFormColumn,
QueryFormMetric,
QueryMode,
SMART_DATE_ID,
t,
@@ -110,8 +111,8 @@ const allColumnsControl: typeof sharedControls.groupby = {
freeForm: true,
allowAll: true,
commaChoosesOption: false,
optionRenderer: c => <ColumnOption showType column={c} />,
valueRenderer: c => <ColumnOption column={c} />,
optionRenderer: c => <ColumnOption showType column={c as ColumnMeta} />,
valueRenderer: c => <ColumnOption column={c as ColumnMeta} />,
valueKey: 'column_name',
mapStateToProps: ({ datasource, controls }, controlState) => ({
options: datasource?.columns || [],
@@ -589,15 +590,29 @@ const config: ControlPanelConfig = {
last(colname.split('__')) !== timeComparisonValue,
)
.forEach((colname, index) => {
// Skip unprefixed percent metric columns if a prefixed version exists
// But don't skip if it's also a regular metric
if (
explore.form_data.metrics?.some(
metric => getMetricLabel(metric) === colname,
) ||
explore.form_data.percent_metrics?.some(
(metric: QueryFormMetric) =>
getMetricLabel(metric) === colname,
)
shouldSkipMetricColumn({
colname,
colnames,
formData: explore.form_data,
})
) {
return;
}
const isMetric = isRegularMetric(
colname,
explore.form_data,
);
const isPercentMetricValue = isPercentMetric(
colname,
explore.form_data,
);
// Generate comparison columns for metrics (time comparison feature)
if (isMetric || isPercentMetricValue) {
const comparisonColumns =
generateComparisonColumns(colname);
comparisonColumns.forEach((name, idx) => {
@@ -646,7 +661,7 @@ const config: ControlPanelConfig = {
name: 'show_cell_bars',
config: {
type: 'CheckboxControl',
label: t('Show Cell bars'),
label: t('Show cell bars'),
renderTrigger: true,
default: true,
description: t(
@@ -674,7 +689,7 @@ const config: ControlPanelConfig = {
name: 'color_pn',
config: {
type: 'CheckboxControl',
label: t('add colors to cell bars for +/-'),
label: t('Add colors to cell bars for +/-'),
renderTrigger: true,
default: true,
description: t(
@@ -688,7 +703,7 @@ const config: ControlPanelConfig = {
name: 'comparison_color_enabled',
config: {
type: 'CheckboxControl',
label: t('basic conditional formatting'),
label: t('Basic conditional formatting'),
renderTrigger: true,
visibility: ({ controls }) =>
!isEmpty(controls?.time_compare?.value),
@@ -729,7 +744,7 @@ const config: ControlPanelConfig = {
config: {
type: 'ConditionalFormattingControl',
renderTrigger: true,
label: t('Custom Conditional Formatting'),
label: t('Custom conditional formatting'),
extraColorChoices: [
{
value: ColorSchemeEnum.Green,
@@ -757,17 +772,22 @@ const config: ControlPanelConfig = {
chart?.queriesResponse?.[0] ?? {};
const numericColumns =
Array.isArray(colnames) && Array.isArray(coltypes)
? colnames
.filter(
(colname: string, index: number) =>
coltypes[index] === GenericDataType.Numeric,
)
.map((colname: string) => ({
value: colname,
label: Array.isArray(verboseMap)
? colname
: (verboseMap[colname] ?? colname),
}))
? colnames.reduce((acc, colname, index) => {
if (
coltypes[index] === GenericDataType.Numeric ||
(!explore?.controls?.time_compare?.value &&
coltypes[index] === GenericDataType.String)
) {
acc.push({
value: colname,
label: Array.isArray(verboseMap)
? colname
: (verboseMap[colname] ?? colname),
dataType: coltypes[index],
});
}
return acc;
}, [])
: [];
const columnOptions = explore?.controls?.time_compare?.value
? processComparisonColumns(
@@ -797,6 +817,9 @@ const config: ControlPanelConfig = {
}),
visibility: isAggMode,
},
sections.matrixifyRowSection,
sections.matrixifyColumnSection,
sections.matrixifySection,
],
formDataOverrides: formData => ({
...formData,

View File

@@ -1,66 +0,0 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { Locale } from '@superset-ui/core';
const en = {
'Query Mode': [''],
Aggregate: [''],
'Raw Records': [''],
'Emit Filter Events': [''],
'Show Cell Bars': [''],
'page_size.show': ['Show'],
'page_size.all': ['All'],
'page_size.entries': ['entries'],
'table.previous_page': ['Previous'],
'table.next_page': ['Next'],
'search.num_records': ['%s record', '%s records...'],
};
const translations: Partial<Record<Locale, typeof en>> = {
en,
fr: {
'Query Mode': [''],
Aggregate: [''],
'Raw Records': [''],
'Emit Filter Events': [''],
'Show Cell Bars': [''],
'page_size.show': ['Afficher'],
'page_size.all': ['tous'],
'page_size.entries': ['entrées'],
'table.previous_page': ['Précédent'],
'table.next_page': ['Suivante'],
'search.num_records': ['%s enregistrement', '%s enregistrements...'],
},
zh: {
'Query Mode': ['查询模式'],
Aggregate: ['分组聚合'],
'Raw Records': ['原始数据'],
'Emit Filter Events': ['关联看板过滤器'],
'Show Cell Bars': ['为指标添加条状图背景'],
'page_size.show': ['每页显示'],
'page_size.all': ['全部'],
'page_size.entries': ['条'],
'table.previous_page': ['上一页'],
'table.next_page': ['下一页'],
'search.num_records': ['%s条记录...'],
},
};
export default translations;

View File

@@ -42,7 +42,7 @@ import {
getColorFormatters,
} from '@superset-ui/chart-controls';
import { isEmpty } from 'lodash';
import { isEmpty, merge } from 'lodash';
import isEqualColumns from './utils/isEqualColumns';
import DateWithFormatter from './utils/DateWithFormatter';
import {
@@ -276,7 +276,7 @@ const processColumns = memoizeOne(function processColumns(
// percent metrics have a default format
formatter = getNumberFormatter(numberFormat || PERCENT_3_POINT);
} else if (isMetric || (isNumber && (numberFormat || currency))) {
formatter = currency
formatter = currency?.symbol
? new CurrencyFormatter({
d3Format: numberFormat,
currency,
@@ -459,7 +459,7 @@ const transformProps = (
const {
height,
width,
rawFormData: formData,
rawFormData: originalFormData,
queriesData = [],
filterState,
ownState: serverPaginationData,
@@ -471,6 +471,12 @@ const transformProps = (
emitCrossFilters,
} = chartProps;
const formData = merge(
{},
originalFormData,
originalFormData.extra_form_data,
);
const {
align_pn: alignPositiveNegative = true,
color_pn: colorPositiveNegative = true,

View File

@@ -324,6 +324,27 @@ describe('plugin-chart-table', () => {
expect(cells[4]).toHaveTextContent('$ 2.47k');
});
it('render data with a bigint value in a raw record mode', () => {
render(
ProviderWrapper({
children: (
<TableChart
{...transformProps(testData.bigint)}
sticky={false}
isRawRecords
/>
),
}),
);
const cells = document.querySelectorAll('td');
expect(document.querySelectorAll('th')[0]).toHaveTextContent('name');
expect(document.querySelectorAll('th')[1]).toHaveTextContent('id');
expect(cells[0]).toHaveTextContent('Michael');
expect(cells[1]).toHaveTextContent('4312');
expect(cells[2]).toHaveTextContent('John');
expect(cells[3]).toHaveTextContent('1234567890123456789');
});
it('render raw data', () => {
const props = transformProps({
...testData.raw,
@@ -550,6 +571,191 @@ describe('plugin-chart-table', () => {
);
cells = document.querySelectorAll('td');
});
it('render color with string column color formatter(operator begins with)', () => {
render(
ProviderWrapper({
children: (
<TableChart
{...transformProps({
...testData.advanced,
rawFormData: {
...testData.advanced.rawFormData,
conditional_formatting: [
{
colorScheme: '#ACE1C4',
column: 'name',
operator: 'begins with',
targetValue: 'J',
},
],
},
})}
/>
),
}),
);
expect(getComputedStyle(screen.getByText('Joe')).background).toBe(
'rgba(172, 225, 196, 1)',
);
expect(getComputedStyle(screen.getByText('Michael')).background).toBe(
'',
);
});
it('render color with string column color formatter (operator ends with)', () => {
render(
ProviderWrapper({
children: (
<TableChart
{...transformProps({
...testData.advanced,
rawFormData: {
...testData.advanced.rawFormData,
conditional_formatting: [
{
colorScheme: '#ACE1C4',
column: 'name',
operator: 'ends with',
targetValue: 'ia',
},
],
},
})}
/>
),
}),
);
expect(getComputedStyle(screen.getByText('Maria')).background).toBe(
'rgba(172, 225, 196, 1)',
);
expect(getComputedStyle(screen.getByText('Joe')).background).toBe('');
});
it('render color with string column color formatter (operator containing)', () => {
render(
ProviderWrapper({
children: (
<TableChart
{...transformProps({
...testData.advanced,
rawFormData: {
...testData.advanced.rawFormData,
conditional_formatting: [
{
colorScheme: '#ACE1C4',
column: 'name',
operator: 'containing',
targetValue: 'c',
},
],
},
})}
/>
),
}),
);
expect(getComputedStyle(screen.getByText('Michael')).background).toBe(
'rgba(172, 225, 196, 1)',
);
expect(getComputedStyle(screen.getByText('Joe')).background).toBe('');
});
it('render color with string column color formatter (operator not containing)', () => {
render(
ProviderWrapper({
children: (
<TableChart
{...transformProps({
...testData.advanced,
rawFormData: {
...testData.advanced.rawFormData,
conditional_formatting: [
{
colorScheme: '#ACE1C4',
column: 'name',
operator: 'not containing',
targetValue: 'i',
},
],
},
})}
/>
),
}),
);
expect(getComputedStyle(screen.getByText('Joe')).background).toBe(
'rgba(172, 225, 196, 1)',
);
expect(getComputedStyle(screen.getByText('Michael')).background).toBe(
'',
);
});
it('render color with string column color formatter (operator =)', () => {
render(
ProviderWrapper({
children: (
<TableChart
{...transformProps({
...testData.advanced,
rawFormData: {
...testData.advanced.rawFormData,
conditional_formatting: [
{
colorScheme: '#ACE1C4',
column: 'name',
operator: '=',
targetValue: 'Joe',
},
],
},
})}
/>
),
}),
);
expect(getComputedStyle(screen.getByText('Joe')).background).toBe(
'rgba(172, 225, 196, 1)',
);
expect(getComputedStyle(screen.getByText('Michael')).background).toBe(
'',
);
});
it('render color with string column color formatter (operator None)', () => {
render(
ProviderWrapper({
children: (
<TableChart
{...transformProps({
...testData.advanced,
rawFormData: {
...testData.advanced.rawFormData,
conditional_formatting: [
{
colorScheme: '#ACE1C4',
column: 'name',
operator: 'None',
},
],
},
})}
/>
),
}),
);
expect(getComputedStyle(screen.getByText('Joe')).background).toBe(
'rgba(172, 225, 196, 1)',
);
expect(getComputedStyle(screen.getByText('Michael')).background).toBe(
'rgba(172, 225, 196, 1)',
);
expect(getComputedStyle(screen.getByText('Maria')).background).toBe(
'rgba(172, 225, 196, 1)',
);
});
});
});
});

View File

@@ -349,6 +349,27 @@ const empty = {
],
};
const bigint = {
...advanced,
queriesData: [
{
...basicQueryResult,
colnames: ['name', 'id'],
coltypes: [GenericDataType.String, GenericDataType.Numeric],
data: [
{
name: 'Michael',
id: 4312,
},
{
name: 'John',
id: 1234567890123456789n,
},
],
},
],
};
export default {
basic,
advanced,
@@ -357,4 +378,5 @@ export default {
comparisonWithConfig,
empty,
raw,
bigint,
};