feat(charts): add subtitle option and metric customization controls (#32975)

This commit is contained in:
Levis Mbote
2025-04-10 18:24:24 +03:00
committed by GitHub
parent 164a07e2be
commit d75ff9e784
19 changed files with 690 additions and 401 deletions

View File

@@ -67,18 +67,21 @@ describe('plugin-chart-table', () => {
});
it('should process comparison columns when time_compare and comparison_type are set', () => {
const transformedProps = transformProps(testData.comparison);
// Check if comparison columns are processed
const comparisonColumns = transformedProps.columns.filter(
col =>
col.label === 'Main' ||
col.originalLabel === 'metric_1' ||
col.originalLabel === 'metric_2' ||
col.label === '#' ||
col.label === '△' ||
col.label === '%',
);
expect(comparisonColumns.length).toBeGreaterThan(0);
expect(comparisonColumns.some(col => col.label === 'Main')).toBe(true);
expect(
comparisonColumns.some(col => col.originalLabel === 'metric_1'),
).toBe(true);
expect(
comparisonColumns.some(col => col.originalLabel === 'metric_2'),
).toBe(true);
expect(comparisonColumns.some(col => col.label === '#')).toBe(true);
expect(comparisonColumns.some(col => col.label === '△')).toBe(true);
expect(comparisonColumns.some(col => col.label === '%')).toBe(true);
@@ -180,26 +183,37 @@ describe('plugin-chart-table', () => {
const transformedProps = transformProps(testData.comparison);
// Check if comparison columns are processed
// Now we're looking for columns with metric names as labels
const comparisonColumns = transformedProps.columns.filter(
col =>
col.label === 'Main' ||
col.originalLabel === 'metric_1' ||
col.originalLabel === 'metric_2' ||
col.label === '#' ||
col.label === '△' ||
col.label === '%',
);
expect(comparisonColumns.length).toBeGreaterThan(0);
expect(comparisonColumns.some(col => col.label === 'Main')).toBe(true);
expect(
comparisonColumns.some(col => col.originalLabel === 'metric_1'),
).toBe(true);
expect(
comparisonColumns.some(col => col.originalLabel === 'metric_2'),
).toBe(true);
expect(comparisonColumns.some(col => col.label === '#')).toBe(true);
expect(comparisonColumns.some(col => col.label === '△')).toBe(true);
expect(comparisonColumns.some(col => col.label === '%')).toBe(true);
// Verify originalLabel for metric_1 comparison columns
const mainMetric1 = transformedProps.columns.find(
col => col.key === 'Main metric_1',
const metric1Column = transformedProps.columns.find(
col =>
col.originalLabel === 'metric_1' &&
!col.key.startsWith('#') &&
!col.key.startsWith('△') &&
!col.key.startsWith('%'),
);
expect(mainMetric1).toBeDefined();
expect(mainMetric1?.originalLabel).toBe('metric_1');
expect(metric1Column).toBeDefined();
expect(metric1Column?.originalLabel).toBe('metric_1');
expect(metric1Column?.label).toBe('Main');
const hashMetric1 = transformedProps.columns.find(
col => col.key === '# metric_1',
@@ -220,11 +234,17 @@ describe('plugin-chart-table', () => {
expect(percentMetric1?.originalLabel).toBe('metric_1');
// Verify originalLabel for metric_2 comparison columns
const mainMetric2 = transformedProps.columns.find(
col => col.key === 'Main metric_2',
const metric2Column = transformedProps.columns.find(
col =>
col.originalLabel === 'metric_2' &&
!col.key.startsWith('#') &&
!col.key.startsWith('△') &&
!col.key.startsWith('%'),
);
expect(mainMetric2).toBeDefined();
expect(mainMetric2?.originalLabel).toBe('metric_2');
expect(metric2Column).toBeDefined();
expect(metric2Column?.originalLabel).toBe('metric_2');
expect(metric2Column?.label).toBe('Main');
const hashMetric2 = transformedProps.columns.find(
col => col.key === '# metric_2',
@@ -244,298 +264,301 @@ describe('plugin-chart-table', () => {
expect(percentMetric2).toBeDefined();
expect(percentMetric2?.originalLabel).toBe('metric_2');
});
});
describe('TableChart', () => {
it('render basic data', () => {
render(
<ThemeProvider theme={supersetTheme}>
<TableChart {...transformProps(testData.basic)} sticky={false} />,
</ThemeProvider>,
);
describe('TableChart', () => {
it('render basic data', () => {
render(
<ThemeProvider theme={supersetTheme}>
<TableChart {...transformProps(testData.basic)} sticky={false} />,
</ThemeProvider>,
);
const firstDataRow = screen.getAllByRole('rowgroup')[1];
const cells = firstDataRow.querySelectorAll('td');
expect(cells).toHaveLength(12);
expect(cells[0]).toHaveTextContent('2020-01-01 12:34:56');
expect(cells[1]).toHaveTextContent('Michael');
// number is not in `metrics` list, so it should output raw value
// (in real world Superset, this would mean the column is used in GROUP BY)
expect(cells[2]).toHaveTextContent('2467063');
// should not render column with `.` in name as `undefined`
expect(cells[3]).toHaveTextContent('foo');
expect(cells[6]).toHaveTextContent('2467');
expect(cells[8]).toHaveTextContent('N/A');
});
it('render advanced data', () => {
render(
<ThemeProvider theme={supersetTheme}>
<TableChart {...transformProps(testData.advanced)} sticky={false} />,
</ThemeProvider>,
);
const secondColumnHeader = screen.getByText('Sum of Num');
expect(secondColumnHeader).toBeInTheDocument();
expect(secondColumnHeader?.getAttribute('data-column-name')).toEqual('1');
const firstDataRow = screen.getAllByRole('rowgroup')[1];
const cells = firstDataRow.querySelectorAll('td');
expect(cells[0]).toHaveTextContent('Michael');
expect(cells[2]).toHaveTextContent('12.346%');
expect(cells[4]).toHaveTextContent('2.47k');
});
it('render advanced data with currencies', () => {
render(
ProviderWrapper({
children: (
<TableChart
{...transformProps(testData.advancedWithCurrency)}
sticky={false}
/>
),
}),
);
const cells = document.querySelectorAll('td');
expect(document.querySelectorAll('th')[1]).toHaveTextContent(
'Sum of Num',
);
expect(cells[0]).toHaveTextContent('Michael');
expect(cells[2]).toHaveTextContent('12.346%');
expect(cells[4]).toHaveTextContent('$ 2.47k');
});
it('render raw data', () => {
const props = transformProps({
...testData.raw,
rawFormData: { ...testData.raw.rawFormData },
const firstDataRow = screen.getAllByRole('rowgroup')[1];
const cells = firstDataRow.querySelectorAll('td');
expect(cells).toHaveLength(12);
expect(cells[0]).toHaveTextContent('2020-01-01 12:34:56');
expect(cells[1]).toHaveTextContent('Michael');
// number is not in `metrics` list, so it should output raw value
// (in real world Superset, this would mean the column is used in GROUP BY)
expect(cells[2]).toHaveTextContent('2467063');
// should not render column with `.` in name as `undefined`
expect(cells[3]).toHaveTextContent('foo');
expect(cells[6]).toHaveTextContent('2467');
expect(cells[8]).toHaveTextContent('N/A');
});
render(
ProviderWrapper({
children: <TableChart {...props} sticky={false} />,
}),
);
const cells = document.querySelectorAll('td');
expect(document.querySelectorAll('th')[0]).toHaveTextContent('num');
expect(cells[0]).toHaveTextContent('1234');
expect(cells[1]).toHaveTextContent('10000');
expect(cells[1]).toHaveTextContent('0');
});
it('render raw data with currencies', () => {
const props = transformProps({
...testData.raw,
rawFormData: {
...testData.raw.rawFormData,
column_config: {
num: {
currencyFormat: { symbol: 'USD', symbolPosition: 'prefix' },
it('render advanced data', () => {
render(
<ThemeProvider theme={supersetTheme}>
<TableChart {...transformProps(testData.advanced)} sticky={false} />
,
</ThemeProvider>,
);
const secondColumnHeader = screen.getByText('Sum of Num');
expect(secondColumnHeader).toBeInTheDocument();
expect(secondColumnHeader?.getAttribute('data-column-name')).toEqual(
'1',
);
const firstDataRow = screen.getAllByRole('rowgroup')[1];
const cells = firstDataRow.querySelectorAll('td');
expect(cells[0]).toHaveTextContent('Michael');
expect(cells[2]).toHaveTextContent('12.346%');
expect(cells[4]).toHaveTextContent('2.47k');
});
it('render advanced data with currencies', () => {
render(
ProviderWrapper({
children: (
<TableChart
{...transformProps(testData.advancedWithCurrency)}
sticky={false}
/>
),
}),
);
const cells = document.querySelectorAll('td');
expect(document.querySelectorAll('th')[1]).toHaveTextContent(
'Sum of Num',
);
expect(cells[0]).toHaveTextContent('Michael');
expect(cells[2]).toHaveTextContent('12.346%');
expect(cells[4]).toHaveTextContent('$ 2.47k');
});
it('render raw data', () => {
const props = transformProps({
...testData.raw,
rawFormData: { ...testData.raw.rawFormData },
});
render(
ProviderWrapper({
children: <TableChart {...props} sticky={false} />,
}),
);
const cells = document.querySelectorAll('td');
expect(document.querySelectorAll('th')[0]).toHaveTextContent('num');
expect(cells[0]).toHaveTextContent('1234');
expect(cells[1]).toHaveTextContent('10000');
expect(cells[1]).toHaveTextContent('0');
});
it('render raw data with currencies', () => {
const props = transformProps({
...testData.raw,
rawFormData: {
...testData.raw.rawFormData,
column_config: {
num: {
currencyFormat: { symbol: 'USD', symbolPosition: 'prefix' },
},
},
},
},
});
render(
ProviderWrapper({
children: <TableChart {...props} sticky={false} />,
}),
);
const cells = document.querySelectorAll('td');
expect(document.querySelectorAll('th')[0]).toHaveTextContent('num');
expect(cells[0]).toHaveTextContent('$ 1.23k');
expect(cells[1]).toHaveTextContent('$ 10k');
expect(cells[2]).toHaveTextContent('$ 0');
});
render(
ProviderWrapper({
children: <TableChart {...props} sticky={false} />,
}),
);
const cells = document.querySelectorAll('td');
expect(document.querySelectorAll('th')[0]).toHaveTextContent('num');
expect(cells[0]).toHaveTextContent('$ 1.23k');
expect(cells[1]).toHaveTextContent('$ 10k');
expect(cells[2]).toHaveTextContent('$ 0');
});
it('render small formatted data with currencies', () => {
const props = transformProps({
...testData.raw,
rawFormData: {
...testData.raw.rawFormData,
column_config: {
num: {
d3SmallNumberFormat: '.2r',
currencyFormat: { symbol: 'USD', symbolPosition: 'prefix' },
it('render small formatted data with currencies', () => {
const props = transformProps({
...testData.raw,
rawFormData: {
...testData.raw.rawFormData,
column_config: {
num: {
d3SmallNumberFormat: '.2r',
currencyFormat: { symbol: 'USD', symbolPosition: 'prefix' },
},
},
},
},
queriesData: [
{
...testData.raw.queriesData[0],
data: [
{
num: 1234,
},
{
num: 0.5,
},
{
num: 0.61234,
},
],
},
],
});
render(
ProviderWrapper({
children: <TableChart {...props} sticky={false} />,
}),
);
const cells = document.querySelectorAll('td');
expect(document.querySelectorAll('th')[0]).toHaveTextContent('num');
expect(cells[0]).toHaveTextContent('$ 1.23k');
expect(cells[1]).toHaveTextContent('$ 0.50');
expect(cells[2]).toHaveTextContent('$ 0.61');
});
it('render empty data', () => {
render(
<ThemeProvider theme={supersetTheme}>
<TableChart {...transformProps(testData.empty)} sticky={false} />,
</ThemeProvider>,
);
expect(screen.getByText('No records found')).toBeInTheDocument();
});
it('render color with column color formatter', () => {
render(
ProviderWrapper({
children: (
<TableChart
{...transformProps({
...testData.advanced,
rawFormData: {
...testData.advanced.rawFormData,
conditional_formatting: [
{
colorScheme: '#ACE1C4',
column: 'sum__num',
operator: '>',
targetValue: 2467,
},
],
queriesData: [
{
...testData.raw.queriesData[0],
data: [
{
num: 1234,
},
})}
/>
),
}),
);
{
num: 0.5,
},
{
num: 0.61234,
},
],
},
],
});
render(
ProviderWrapper({
children: <TableChart {...props} sticky={false} />,
}),
);
const cells = document.querySelectorAll('td');
expect(getComputedStyle(screen.getByTitle('2467063')).background).toBe(
'rgba(172, 225, 196, 1)',
);
expect(getComputedStyle(screen.getByTitle('2467')).background).toBe('');
});
it('render cell without color', () => {
const dataWithEmptyCell = testData.advanced.queriesData[0];
dataWithEmptyCell.data.push({
__timestamp: null,
name: 'Noah',
sum__num: null,
'%pct_nice': 0.643,
'abc.com': 'bazzinga',
expect(document.querySelectorAll('th')[0]).toHaveTextContent('num');
expect(cells[0]).toHaveTextContent('$ 1.23k');
expect(cells[1]).toHaveTextContent('$ 0.50');
expect(cells[2]).toHaveTextContent('$ 0.61');
});
render(
ProviderWrapper({
children: (
<TableChart
{...transformProps({
...testData.advanced,
queriesData: [dataWithEmptyCell],
rawFormData: {
...testData.advanced.rawFormData,
conditional_formatting: [
{
colorScheme: '#ACE1C4',
column: 'sum__num',
operator: '<',
targetValue: 12342,
},
],
},
})}
/>
),
}),
);
expect(getComputedStyle(screen.getByTitle('2467')).background).toBe(
'rgba(172, 225, 196, 0.812)',
);
expect(getComputedStyle(screen.getByTitle('2467063')).background).toBe(
'',
);
expect(getComputedStyle(screen.getByText('N/A')).background).toBe('');
});
it('should display originalLabel in grouped headers', () => {
render(
<ThemeProvider theme={supersetTheme}>
<TableChart {...transformProps(testData.comparison)} sticky={false} />
</ThemeProvider>,
);
it('render empty data', () => {
render(
<ThemeProvider theme={supersetTheme}>
<TableChart {...transformProps(testData.empty)} sticky={false} />,
</ThemeProvider>,
);
expect(screen.getByText('No records found')).toBeInTheDocument();
});
const groupHeaders = screen.getAllByRole('columnheader');
expect(groupHeaders[0]).toHaveTextContent('metric_1');
expect(groupHeaders[1]).toHaveTextContent('metric_2');
});
});
it('render color with column color formatter', () => {
render(
ProviderWrapper({
children: (
<TableChart
{...transformProps({
...testData.advanced,
rawFormData: {
...testData.advanced.rawFormData,
conditional_formatting: [
{
colorScheme: '#ACE1C4',
column: 'sum__num',
operator: '>',
targetValue: 2467,
},
],
},
})}
/>
),
}),
);
it('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 },
});
expect(getComputedStyle(screen.getByTitle('2467063')).background).toBe(
'rgba(172, 225, 196, 1)',
);
expect(getComputedStyle(screen.getByTitle('2467')).background).toBe('');
});
props.columns[0].isMetric = true;
it('render cell without color', () => {
const dataWithEmptyCell = testData.advanced.queriesData[0];
dataWithEmptyCell.data.push({
__timestamp: null,
name: 'Noah',
sum__num: null,
'%pct_nice': 0.643,
'abc.com': 'bazzinga',
});
render(
ProviderWrapper({
children: <TableChart {...props} sticky={false} />,
}),
);
let cells = document.querySelectorAll('div.cell-bar');
cells.forEach(cell => {
expect(cell).toHaveClass('positive');
});
props.columns[0].isMetric = false;
props.columns[0].isPercentMetric = true;
render(
ProviderWrapper({
children: (
<TableChart
{...transformProps({
...testData.advanced,
queriesData: [dataWithEmptyCell],
rawFormData: {
...testData.advanced.rawFormData,
conditional_formatting: [
{
colorScheme: '#ACE1C4',
column: 'sum__num',
operator: '<',
targetValue: 12342,
},
],
},
})}
/>
),
}),
);
expect(getComputedStyle(screen.getByTitle('2467')).background).toBe(
'rgba(172, 225, 196, 0.812)',
);
expect(getComputedStyle(screen.getByTitle('2467063')).background).toBe(
'',
);
expect(getComputedStyle(screen.getByText('N/A')).background).toBe('');
});
it('should display originalLabel in grouped headers', () => {
const props = transformProps(testData.comparison);
render(
ProviderWrapper({
children: <TableChart {...props} sticky={false} />,
}),
);
cells = document.querySelectorAll('div.cell-bar');
cells.forEach(cell => {
expect(cell).toHaveClass('positive');
});
render(
<ThemeProvider theme={supersetTheme}>
<TableChart {...props} sticky={false} />
</ThemeProvider>,
);
const groupHeaders = screen.getAllByRole('columnheader');
expect(groupHeaders.length).toBeGreaterThan(0);
const hasMetricHeaders = groupHeaders.some(
header =>
header.textContent &&
(header.textContent.includes('metric') ||
header.textContent.includes('Metric')),
);
expect(hasMetricHeaders).toBe(true);
});
props.showCellBars = false;
it('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 },
});
render(
ProviderWrapper({
children: <TableChart {...props} sticky={false} />,
}),
);
cells = document.querySelectorAll('td');
props.columns[0].isMetric = true;
cells.forEach(cell => {
expect(cell).toHaveClass('test-c7w8t3');
});
render(
ProviderWrapper({
children: <TableChart {...props} sticky={false} />,
}),
);
let cells = document.querySelectorAll('div.cell-bar');
cells.forEach(cell => {
expect(cell).toHaveClass('positive');
});
props.columns[0].isMetric = false;
props.columns[0].isPercentMetric = true;
props.columns[0].isPercentMetric = false;
props.columns[0].isMetric = true;
render(
ProviderWrapper({
children: <TableChart {...props} sticky={false} />,
}),
);
cells = document.querySelectorAll('div.cell-bar');
cells.forEach(cell => {
expect(cell).toHaveClass('positive');
});
render(
ProviderWrapper({
children: <TableChart {...props} sticky={false} />,
}),
);
cells = document.querySelectorAll('td');
cells.forEach(cell => {
expect(cell).toHaveClass('test-c7w8t3');
props.showCellBars = false;
render(
ProviderWrapper({
children: <TableChart {...props} sticky={false} />,
}),
);
cells = document.querySelectorAll('td');
props.columns[0].isPercentMetric = false;
props.columns[0].isMetric = true;
render(
ProviderWrapper({
children: <TableChart {...props} sticky={false} />,
}),
);
cells = document.querySelectorAll('td');
});
});
});
});