Skip to content

Commit b5011da

Browse files
committed
Merge branch 'master' into enxdev/fix/dbt-cloud-panel
2 parents 6b24475 + 37d58a4 commit b5011da

File tree

33 files changed

+1549
-223
lines changed

33 files changed

+1549
-223
lines changed

superset-frontend/packages/superset-ui-core/src/components/Tabs/Tabs.test.tsx

Lines changed: 89 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -20,25 +20,25 @@
2020
import { fireEvent, render } from '@superset-ui/core/spec';
2121
import Tabs, { EditableTabs, LineEditableTabs } from './Tabs';
2222

23-
describe('Tabs', () => {
24-
const defaultItems = [
25-
{
26-
key: '1',
27-
label: 'Tab 1',
28-
children: <div data-testid="tab1-content">Tab 1 content</div>,
29-
},
30-
{
31-
key: '2',
32-
label: 'Tab 2',
33-
children: <div data-testid="tab2-content">Tab 2 content</div>,
34-
},
35-
{
36-
key: '3',
37-
label: 'Tab 3',
38-
children: <div data-testid="tab3-content">Tab 3 content</div>,
39-
},
40-
];
23+
const defaultItems = [
24+
{
25+
key: '1',
26+
label: 'Tab 1',
27+
children: <div data-testid="tab1-content">Tab 1 content</div>,
28+
},
29+
{
30+
key: '2',
31+
label: 'Tab 2',
32+
children: <div data-testid="tab2-content">Tab 2 content</div>,
33+
},
34+
{
35+
key: '3',
36+
label: 'Tab 3',
37+
children: <div data-testid="tab3-content">Tab 3 content</div>,
38+
},
39+
];
4140

41+
describe('Tabs', () => {
4242
describe('Basic Tabs', () => {
4343
it('should render tabs with default props', () => {
4444
const { getByText, container } = render(<Tabs items={defaultItems} />);
@@ -284,6 +284,7 @@ describe('Tabs', () => {
284284
describe('Styling Integration', () => {
285285
it('should accept and apply custom CSS classes', () => {
286286
const { container } = render(
287+
// eslint-disable-next-line react/forbid-component-props
287288
<Tabs items={defaultItems} className="custom-tabs-class" />,
288289
);
289290

@@ -295,6 +296,7 @@ describe('Tabs', () => {
295296
it('should accept and apply custom styles', () => {
296297
const customStyle = { minHeight: '200px' };
297298
const { container } = render(
299+
// eslint-disable-next-line react/forbid-component-props
298300
<Tabs items={defaultItems} style={customStyle} />,
299301
);
300302

@@ -304,3 +306,72 @@ describe('Tabs', () => {
304306
});
305307
});
306308
});
309+
310+
test('fullHeight prop renders component hierarchy correctly', () => {
311+
const { container } = render(<Tabs items={defaultItems} fullHeight />);
312+
313+
const tabsElement = container.querySelector('.ant-tabs');
314+
const contentHolder = container.querySelector('.ant-tabs-content-holder');
315+
const content = container.querySelector('.ant-tabs-content');
316+
const tabPane = container.querySelector('.ant-tabs-tabpane');
317+
318+
expect(tabsElement).toBeInTheDocument();
319+
expect(contentHolder).toBeInTheDocument();
320+
expect(content).toBeInTheDocument();
321+
expect(tabPane).toBeInTheDocument();
322+
expect(tabsElement?.contains(contentHolder as Node)).toBe(true);
323+
expect(contentHolder?.contains(content as Node)).toBe(true);
324+
expect(content?.contains(tabPane as Node)).toBe(true);
325+
});
326+
327+
test('fullHeight prop maintains structure when content updates', () => {
328+
const { container, rerender } = render(
329+
<Tabs items={defaultItems} fullHeight />,
330+
);
331+
332+
const initialTabsElement = container.querySelector('.ant-tabs');
333+
334+
const newItems = [
335+
...defaultItems,
336+
{
337+
key: '4',
338+
label: 'Tab 4',
339+
children: <div data-testid="tab4-content">New tab content</div>,
340+
},
341+
];
342+
343+
rerender(<Tabs items={newItems} fullHeight />);
344+
345+
const updatedTabsElement = container.querySelector('.ant-tabs');
346+
const updatedContentHolder = container.querySelector(
347+
'.ant-tabs-content-holder',
348+
);
349+
350+
expect(updatedTabsElement).toBeInTheDocument();
351+
expect(updatedContentHolder).toBeInTheDocument();
352+
expect(initialTabsElement).toBe(updatedTabsElement);
353+
});
354+
355+
test('fullHeight prop works with allowOverflow to handle tall content', () => {
356+
const { container } = render(
357+
<Tabs items={defaultItems} fullHeight allowOverflow />,
358+
);
359+
360+
const tabsElement = container.querySelector('.ant-tabs') as HTMLElement;
361+
const contentHolder = container.querySelector(
362+
'.ant-tabs-content-holder',
363+
) as HTMLElement;
364+
365+
expect(tabsElement).toBeInTheDocument();
366+
expect(contentHolder).toBeInTheDocument();
367+
368+
// Verify overflow handling is not restricted
369+
const holderStyles = window.getComputedStyle(contentHolder);
370+
expect(holderStyles.overflow).not.toBe('hidden');
371+
});
372+
373+
test('fullHeight prop handles empty items array', () => {
374+
const { container } = render(<Tabs items={[]} fullHeight />);
375+
376+
expect(container.querySelector('.ant-tabs')).toBeInTheDocument();
377+
});

superset-frontend/packages/superset-ui-core/src/components/Tabs/Tabs.tsx

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,12 +25,14 @@ import type { SerializedStyles } from '@emotion/react';
2525

2626
export interface TabsProps extends AntdTabsProps {
2727
allowOverflow?: boolean;
28+
fullHeight?: boolean;
2829
contentStyle?: SerializedStyles;
2930
}
3031

3132
const StyledTabs = ({
3233
animated = false,
3334
allowOverflow = true,
35+
fullHeight = false,
3436
tabBarStyle,
3537
contentStyle,
3638
...props
@@ -46,9 +48,17 @@ const StyledTabs = ({
4648
tabBarStyle={mergedStyle}
4749
css={theme => css`
4850
overflow: ${allowOverflow ? 'visible' : 'hidden'};
51+
${fullHeight && 'height: 100%;'}
4952
5053
.ant-tabs-content-holder {
5154
overflow: ${allowOverflow ? 'visible' : 'auto'};
55+
${fullHeight && 'height: 100%;'}
56+
}
57+
.ant-tabs-content {
58+
${fullHeight && 'height: 100%;'}
59+
}
60+
.ant-tabs-tabpane {
61+
${fullHeight && 'height: 100%;'}
5262
${contentStyle}
5363
}
5464
.ant-tabs-tab {

superset-frontend/packages/superset-ui-core/src/query/normalizeTimeColumn.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ export function normalizeTimeColumn(
6767
sqlExpression: formData.x_axis,
6868
label: formData.x_axis,
6969
expressionType: 'SQL',
70+
isColumnReference: true,
7071
};
7172
}
7273

superset-frontend/packages/superset-ui-core/src/query/types/Column.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ export interface AdhocColumn {
2727
optionName?: string;
2828
sqlExpression: string;
2929
expressionType: 'SQL';
30+
isColumnReference?: boolean;
3031
columnType?: 'BASE_AXIS' | 'SERIES';
3132
timeGrain?: string;
3233
datasourceWarning?: boolean;
@@ -74,6 +75,10 @@ export function isAdhocColumn(column?: any): column is AdhocColumn {
7475
);
7576
}
7677

78+
export function isAdhocColumnReference(column?: any): column is AdhocColumn {
79+
return isAdhocColumn(column) && column?.isColumnReference === true;
80+
}
81+
7782
export function isQueryFormColumn(column: any): column is QueryFormColumn {
7883
return isPhysicalColumn(column) || isAdhocColumn(column);
7984
}

superset-frontend/packages/superset-ui-core/test/query/normalizeTimeColumn.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,7 @@ test('should support different columns for x-axis and granularity', () => {
8686
{
8787
timeGrain: 'P1Y',
8888
columnType: 'BASE_AXIS',
89+
isColumnReference: true,
8990
sqlExpression: 'time_column_in_x_axis',
9091
label: 'time_column_in_x_axis',
9192
expressionType: 'SQL',

superset-frontend/plugins/plugin-chart-echarts/src/controls.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ const legendTypeControl: ControlSetItem = {
6767
label: t('Type'),
6868
choices: [
6969
['scroll', t('Scroll')],
70-
['plain', t('Plain')],
70+
['plain', t('List')],
7171
],
7272
default: legendType,
7373
renderTrigger: true,

superset-frontend/plugins/plugin-chart-echarts/src/utils/series.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -482,8 +482,17 @@ export function getLegendProps(
482482
break;
483483
case LegendOrientation.Bottom:
484484
legend.bottom = 0;
485+
if (padding?.left) {
486+
legend.left = padding.left;
487+
}
485488
break;
486489
case LegendOrientation.Top:
490+
legend.top = 0;
491+
legend.right = zoomable ? TIMESERIES_CONSTANTS.legendTopRightOffset : 0;
492+
if (padding?.left) {
493+
legend.left = padding.left;
494+
}
495+
break;
487496
default:
488497
legend.top = 0;
489498
legend.right = zoomable ? TIMESERIES_CONSTANTS.legendTopRightOffset : 0;

superset-frontend/plugins/plugin-chart-echarts/test/Timeseries/buildQuery.test.ts

Lines changed: 29 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -101,36 +101,35 @@ describe('queryObject conversion', () => {
101101

102102
it('should convert queryObject', () => {
103103
const { queries } = buildQuery({ ...formData, x_axis: 'time_column' });
104-
expect(queries[0]).toEqual(
105-
expect.objectContaining({
106-
granularity: 'time_column',
107-
time_range: '1 year ago : 2013',
108-
extras: { having: '', where: '', time_grain_sqla: 'P1Y' },
109-
columns: [
110-
{
111-
columnType: 'BASE_AXIS',
112-
expressionType: 'SQL',
113-
label: 'time_column',
114-
sqlExpression: 'time_column',
115-
timeGrain: 'P1Y',
116-
},
117-
'col1',
118-
],
119-
series_columns: ['col1'],
120-
metrics: ['count(*)'],
121-
post_processing: [
122-
{
123-
operation: 'pivot',
124-
options: {
125-
aggregates: { 'count(*)': { operator: 'mean' } },
126-
columns: ['col1'],
127-
drop_missing_columns: true,
128-
index: ['time_column'],
129-
},
104+
expect(queries[0]).toMatchObject({
105+
granularity: 'time_column',
106+
time_range: '1 year ago : 2013',
107+
extras: { having: '', where: '', time_grain_sqla: 'P1Y' },
108+
columns: [
109+
{
110+
columnType: 'BASE_AXIS',
111+
expressionType: 'SQL',
112+
label: 'time_column',
113+
sqlExpression: 'time_column',
114+
timeGrain: 'P1Y',
115+
isColumnReference: true,
116+
},
117+
'col1',
118+
],
119+
series_columns: ['col1'],
120+
metrics: ['count(*)'],
121+
post_processing: [
122+
{
123+
operation: 'pivot',
124+
options: {
125+
aggregates: { 'count(*)': { operator: 'mean' } },
126+
columns: ['col1'],
127+
drop_missing_columns: true,
128+
index: ['time_column'],
130129
},
131-
{ operation: 'flatten' },
132-
],
133-
}),
134-
);
130+
},
131+
{ operation: 'flatten' },
132+
],
133+
});
135134
});
136135
});

superset-frontend/plugins/plugin-chart-table/src/TableChart.tsx

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,31 @@ function cellWidth({
139139
return perc2;
140140
}
141141

142+
/**
143+
* Sanitize a column identifier for use in HTML id attributes and CSS selectors.
144+
* Replaces characters that are invalid in CSS selectors with safe alternatives.
145+
*
146+
* Note: The returned value should be prefixed with a string (e.g., "header-")
147+
* to ensure it forms a valid HTML ID (IDs cannot start with a digit).
148+
*
149+
* Exported for testing.
150+
*/
151+
export function sanitizeHeaderId(columnId: string): string {
152+
return (
153+
columnId
154+
// Semantic replacements first: preserve meaning in IDs for readability
155+
// (e.g., '%pct_nice' → 'percentpct_nice' instead of '_pct_nice')
156+
.replace(/%/g, 'percent')
157+
.replace(/#/g, 'hash')
158+
.replace(//g, 'delta')
159+
// Generic sanitization for remaining special characters
160+
.replace(/\s+/g, '_')
161+
.replace(/[^a-zA-Z0-9_-]/g, '_')
162+
.replace(/_+/g, '_') // Collapse consecutive underscores
163+
.replace(/^_+|_+$/g, '') // Trim leading/trailing underscores
164+
);
165+
}
166+
142167
/**
143168
* Cell left margin (offset) calculation for horizontal bar chart elements
144169
* when alignPositiveNegative is not set
@@ -844,6 +869,9 @@ export default function TableChart<D extends DataRecord = DataRecord>(
844869
}
845870
}
846871

872+
// Cache sanitized header ID to avoid recomputing it multiple times
873+
const headerId = sanitizeHeaderId(column.originalLabel ?? column.key);
874+
847875
return {
848876
id: String(i), // to allow duplicate column keys
849877
// must use custom accessor to allow `.` in column names
@@ -969,7 +997,7 @@ export default function TableChart<D extends DataRecord = DataRecord>(
969997
}
970998

971999
const cellProps = {
972-
'aria-labelledby': `header-${column.key}`,
1000+
'aria-labelledby': `header-${headerId}`,
9731001
role: 'cell',
9741002
// show raw number in title in case of numeric values
9751003
title: typeof value === 'number' ? String(value) : undefined,
@@ -1056,7 +1084,7 @@ export default function TableChart<D extends DataRecord = DataRecord>(
10561084
},
10571085
Header: ({ column: col, onClick, style, onDragStart, onDrop }) => (
10581086
<th
1059-
id={`header-${column.originalLabel}`}
1087+
id={`header-${headerId}`}
10601088
title={t('Shift + Click to sort by multiple columns')}
10611089
className={[className, col.isSorted ? 'is-sorted' : ''].join(' ')}
10621090
style={{

0 commit comments

Comments
 (0)