chore: Upgrade to React 18 (#38563)

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Evan Rusackas <evan@preset.io>
This commit is contained in:
Mehmet Salih Yavuz
2026-05-04 19:19:36 +03:00
committed by GitHub
parent 28239c18d4
commit 41a22d7918
183 changed files with 5035 additions and 7225 deletions

View File

@@ -16,17 +16,26 @@
* specific language governing permissions and limitations
* under the License.
*/
import React, { Component } from 'react';
import React, { useCallback, useMemo } from 'react';
import { IconTooltip, List } from '@superset-ui/core/components';
import { nanoid } from 'nanoid';
import { t } from '@apache-superset/core/translation';
import { withTheme, type SupersetTheme } from '@apache-superset/core/theme';
import { useTheme, type SupersetTheme } from '@apache-superset/core/theme';
import {
SortableContainer,
SortableHandle,
SortableElement,
DndContext,
closestCenter,
useSensor,
useSensors,
PointerSensor,
type DragEndEvent,
} from '@dnd-kit/core';
import {
SortableContext,
verticalListSortingStrategy,
useSortable,
arrayMove,
} from 'react-sortable-hoc';
} from '@dnd-kit/sortable';
import { CSS } from '@dnd-kit/utilities';
import { Icons } from '@superset-ui/core/components/Icons';
import {
HeaderContainer,
@@ -54,161 +63,243 @@ interface CollectionControlProps {
isFloat?: boolean;
isInt?: boolean;
controlName: string;
theme: SupersetTheme;
}
const defaultProps: Partial<CollectionControlProps> = {
label: null,
description: null,
onChange: () => {},
placeholder: t('Empty collection'),
itemGenerator: () => ({ key: nanoid(11) }),
keyAccessor: (o: CollectionItem) => o.key ?? '',
value: [],
addTooltip: t('Add an item'),
};
const SortableListItem = SortableElement(CustomListItem);
const SortableList = SortableContainer(List);
const SortableDragger = SortableHandle(() => (
<Icons.MenuOutlined
role="img"
aria-label={t('Drag to reorder')}
className="text-primary"
style={{ cursor: 'ns-resize' }}
/>
));
function DragHandle() {
return (
<Icons.MenuOutlined
role="img"
aria-label={t('Drag to reorder')}
className="text-primary"
style={{ cursor: 'ns-resize' }}
/>
);
}
class CollectionControl extends Component<CollectionControlProps> {
static defaultProps = defaultProps;
interface SortableItemProps {
id: string;
index: number;
item: CollectionItem;
controlProps: Omit<CollectionControlProps, 'label'>;
onChangeItem: (index: number, value: CollectionItem) => void;
onRemoveItem: (index: number) => void;
}
constructor(props: CollectionControlProps) {
super(props);
this.onAdd = this.onAdd.bind(this);
}
function SortableItem({
id,
index,
item,
controlProps,
onChangeItem,
onRemoveItem,
}: SortableItemProps) {
const { attributes, listeners, setNodeRef, transform, transition } =
useSortable({ id });
const style = {
transform: CSS.Transform.toString(transform),
transition: transition ?? undefined,
};
const Control = (controlMap as Record<string, React.ComponentType<any>>)[
controlProps.controlName
];
onChange(i: number, value: CollectionItem) {
const currentValue = this.props.value ?? [];
const newValue = [...currentValue];
newValue[i] = { ...currentValue[i], ...value };
this.props.onChange?.(newValue);
}
onAdd() {
const currentValue = this.props.value ?? [];
const newItem = this.props.itemGenerator?.();
// Cast needed: original JS allowed undefined items from itemGenerator
this.props.onChange?.(
currentValue.concat([newItem] as unknown as CollectionItem[]),
);
}
onSortEnd({ oldIndex, newIndex }: { oldIndex: number; newIndex: number }) {
const currentValue = this.props.value ?? [];
this.props.onChange?.(arrayMove(currentValue, oldIndex, newIndex));
}
removeItem(i: number) {
const currentValue = this.props.value ?? [];
this.props.onChange?.(currentValue.filter((o, ix) => i !== ix));
}
renderList() {
const currentValue = this.props.value ?? [];
if (currentValue.length === 0) {
return <div className="text-muted">{this.props.placeholder}</div>;
}
const Control = (controlMap as Record<string, React.ComponentType<any>>)[
this.props.controlName
];
const keyAccessor =
this.props.keyAccessor ?? ((o: CollectionItem) => o.key ?? '');
return (
<SortableList
useDragHandle
lockAxis="y"
onSortEnd={this.onSortEnd.bind(this)}
bordered
return (
<CustomListItem
ref={setNodeRef}
style={style}
selectable={false}
className="clearfix"
css={(theme: SupersetTheme) => ({
alignItems: 'center',
justifyContent: 'flex-start',
display: 'flex',
paddingInline: theme.sizeUnit * 6,
})}
>
<span {...attributes} {...listeners}>
<DragHandle />
</span>
<div
css={(theme: SupersetTheme) => ({
borderRadius: theme.borderRadius,
flex: 1,
marginLeft: theme.sizeUnit * 2,
marginRight: theme.sizeUnit * 2,
})}
>
{currentValue.map((o: CollectionItem, i: number) => {
// label relevant only for header, not here
const { label, theme, ...commonProps } = this.props;
return (
<SortableListItem
selectable={false}
className="clearfix"
css={(theme: SupersetTheme) => ({
alignItems: 'center',
justifyContent: 'flex-start',
display: 'flex',
paddingInline: theme.sizeUnit * 6,
})}
key={keyAccessor(o)}
index={i}
>
<SortableDragger />
<div
css={(theme: SupersetTheme) => ({
flex: 1,
marginLeft: theme.sizeUnit * 2,
marginRight: theme.sizeUnit * 2,
})}
>
<Control
{...commonProps}
{...o}
onChange={this.onChange.bind(this, i)}
/>
</div>
<IconTooltip
className="pointer"
placement="right"
onClick={this.removeItem.bind(this, i)}
tooltip={t('Remove item')}
mouseEnterDelay={0}
mouseLeaveDelay={0}
css={(theme: SupersetTheme) => ({
padding: 0,
minWidth: 'auto',
height: 'auto',
lineHeight: 1,
cursor: 'pointer',
'& svg path': {
fill: theme.colorIcon,
transition: `fill ${theme.motionDurationMid} ease-out`,
},
'&:hover svg path': {
fill: theme.colorError,
},
})}
>
<Icons.CloseOutlined iconSize="s" />
</IconTooltip>
</SortableListItem>
);
})}
</SortableList>
);
}
render() {
return (
<div data-test="CollectionControl" className="CollectionControl">
<HeaderContainer>
<ControlHeader {...this.props} />
<AddIconButton onClick={this.onAdd}>
<Icons.PlusOutlined
iconSize="s"
iconColor={this.props.theme.colorTextLightSolid}
/>
</AddIconButton>
</HeaderContainer>
{this.renderList()}
<Control
{...controlProps}
{...item}
onChange={(value: CollectionItem) => onChangeItem(index, value)}
/>
</div>
);
}
<IconTooltip
className="pointer"
placement="right"
onClick={() => onRemoveItem(index)}
tooltip={t('Remove item')}
mouseEnterDelay={0}
mouseLeaveDelay={0}
css={(theme: SupersetTheme) => ({
padding: 0,
minWidth: 'auto',
height: 'auto',
lineHeight: 1,
cursor: 'pointer',
'& svg path': {
fill: theme.colorIcon,
transition: `fill ${theme.motionDurationMid} ease-out`,
},
'&:hover svg path': {
fill: theme.colorError,
},
})}
>
<Icons.CloseOutlined iconSize="s" />
</IconTooltip>
</CustomListItem>
);
}
export default withTheme(CollectionControl);
const defaultKeyAccessor = (o: CollectionItem) => o.key ?? '';
const defaultItemGenerator = () => ({ key: nanoid(11) });
function CollectionControl({
name,
label = null,
description = null,
placeholder = t('Empty collection'),
addTooltip = t('Add an item'),
itemGenerator = defaultItemGenerator,
keyAccessor = defaultKeyAccessor,
onChange,
value = [],
isFloat,
isInt,
controlName,
}: CollectionControlProps) {
const theme = useTheme();
const sensors = useSensors(
useSensor(PointerSensor, {
activationConstraint: { distance: 5 },
}),
);
const itemIds = useMemo(
() => value.map((item, i) => keyAccessor(item) || String(i)),
[value, keyAccessor],
);
const onAdd = useCallback(() => {
const newItem = itemGenerator();
onChange?.(value.concat([newItem] as unknown as CollectionItem[]));
}, [value, itemGenerator, onChange]);
const onChangeItem = useCallback(
(i: number, itemValue: CollectionItem) => {
const newValue = [...value];
newValue[i] = { ...value[i], ...itemValue };
onChange?.(newValue);
},
[value, onChange],
);
const onRemoveItem = useCallback(
(i: number) => {
onChange?.(value.filter((_, ix) => i !== ix));
},
[value, onChange],
);
const handleDragEnd = useCallback(
(event: DragEndEvent) => {
const { active, over } = event;
if (over && active.id !== over.id) {
const oldIndex = itemIds.indexOf(String(active.id));
const newIndex = itemIds.indexOf(String(over.id));
onChange?.(arrayMove(value, oldIndex, newIndex));
}
},
[value, itemIds, onChange],
);
const controlProps = useMemo(
() => ({
name,
description,
placeholder,
addTooltip,
itemGenerator,
keyAccessor,
onChange,
value,
isFloat,
isInt,
controlName,
}),
[
name,
description,
placeholder,
addTooltip,
itemGenerator,
keyAccessor,
onChange,
value,
isFloat,
isInt,
controlName,
],
);
const renderList = () => {
if (value.length === 0) {
return <div className="text-muted">{placeholder}</div>;
}
return (
<DndContext
sensors={sensors}
collisionDetection={closestCenter}
onDragEnd={handleDragEnd}
>
<SortableContext items={itemIds} strategy={verticalListSortingStrategy}>
<List
bordered
css={(theme: SupersetTheme) => ({
borderRadius: theme.borderRadius,
})}
>
{value.map((item, i) => (
<SortableItem
key={itemIds[i]}
id={itemIds[i]}
index={i}
item={item}
controlProps={controlProps}
onChangeItem={onChangeItem}
onRemoveItem={onRemoveItem}
/>
))}
</List>
</SortableContext>
</DndContext>
);
};
return (
<div data-test="CollectionControl" className="CollectionControl">
<HeaderContainer>
<ControlHeader name={name} label={label} description={description} />
<AddIconButton onClick={onAdd}>
<Icons.PlusOutlined
iconSize="s"
iconColor={theme.colorTextLightSolid}
/>
</AddIconButton>
</HeaderContainer>
{renderList()}
</div>
);
}
export default CollectionControl;

View File

@@ -16,39 +16,48 @@
* specific language governing permissions and limitations
* under the License.
*/
import { forwardRef, type ReactNode } from 'react';
import { useTheme } from '@apache-superset/core/theme';
import { List, type ListItemProps } from '@superset-ui/core/components';
export interface CustomListItemProps extends ListItemProps {
selectable: boolean;
children?: ReactNode;
}
export default function CustomListItem(props: CustomListItemProps) {
const { selectable, children, ...rest } = props;
const theme = useTheme();
const css: Record<string, Record<string, Record<string, number> | string>> = {
'&.ant-list-item': {
':first-of-type': {
borderTopLeftRadius: theme.borderRadius,
borderTopRightRadius: theme.borderRadius,
const CustomListItem = forwardRef<HTMLDivElement, CustomListItemProps>(
function CustomListItem(props, ref) {
const { selectable, children, ...rest } = props;
const theme = useTheme();
const css: Record<
string,
Record<string, Record<string, number> | string>
> = {
'&.ant-list-item': {
':first-of-type': {
borderTopLeftRadius: theme.borderRadius,
borderTopRightRadius: theme.borderRadius,
},
':last-of-type': {
borderBottomLeftRadius: theme.borderRadius,
borderBottomRightRadius: theme.borderRadius,
},
},
':last-of-type': {
borderBottomLeftRadius: theme.borderRadius,
borderBottomRightRadius: theme.borderRadius,
},
},
};
if (selectable) {
css['&:hover'] = {
cursor: 'pointer',
backgroundColor: theme.colorFillSecondary,
};
}
return (
<List.Item {...rest} css={css}>
{children}
</List.Item>
);
}
if (selectable) {
css['&:hover'] = {
cursor: 'pointer',
backgroundColor: theme.colorFillSecondary,
};
}
return (
<List.Item ref={ref} {...rest} css={css}>
{children}
</List.Item>
);
},
);
export default CustomListItem;

View File

@@ -203,7 +203,7 @@ const ColumnSelectPopover = ({
);
const onSqlExpressionChange = useCallback(
sqlExpression => {
(sqlExpression: string) => {
setAdhocColumn({ label, sqlExpression, expressionType: 'SQL' });
setSelectedSimpleColumn(undefined);
setSelectedCalculatedColumn(undefined);
@@ -213,7 +213,7 @@ const ColumnSelectPopover = ({
);
const onCalculatedColumnChange = useCallback(
selectedColumnName => {
(selectedColumnName: string) => {
const selectedColumn = calculatedColumns.find(
col => col.column_name === selectedColumnName,
);
@@ -229,7 +229,7 @@ const ColumnSelectPopover = ({
);
const onSimpleColumnChange = useCallback(
selectedColumnName => {
(selectedColumnName: string) => {
const selectedColumn = simpleColumns.find(
col => col.column_name === selectedColumnName,
);
@@ -245,7 +245,7 @@ const ColumnSelectPopover = ({
);
const onSimpleMetricChange = useCallback(
selectedMetricName => {
(selectedMetricName: string) => {
const selectedMetric = availableMetrics.find(
metric => metric.metric_name === selectedMetricName,
);
@@ -261,7 +261,7 @@ const ColumnSelectPopover = ({
);
const onSimpleItemChange = useCallback(
selectedValue => {
(selectedValue: string) => {
const selectedColumn = columnMap[selectedValue];
if (selectedColumn) {
onSimpleColumnChange(selectedValue);
@@ -349,7 +349,7 @@ const ColumnSelectPopover = ({
]);
const onTabChange = useCallback(
tab => {
(tab: string) => {
getCurrentTab(tab);
setSelectedTab(tab);
sqlEditorRef.current?.focus();

View File

@@ -63,7 +63,7 @@ export const DndColumnSelectPopoverTitle = ({
}, []);
const onInputBlur = useCallback(
e => {
(e: React.FocusEvent<HTMLInputElement>) => {
if (e.target.value === '') {
onChange(e);
}

View File

@@ -139,7 +139,7 @@ const DndMetricSelect = (props: any) => {
);
const handleChange = useCallback(
opts => {
(opts: ValueType | ValueType[] | null) => {
// if clear out options
if (opts === null) {
onChange(null);
@@ -150,7 +150,11 @@ const DndMetricSelect = (props: any) => {
const optionValues = transformedOpts
.map(option => {
// pre-defined metric
if (option.metric_name) {
if (
typeof option === 'object' &&
'metric_name' in option &&
option.metric_name
) {
return option.metric_name;
}
return option;
@@ -263,7 +267,7 @@ const DndMetricSelect = (props: any) => {
);
const getSavedMetricOptionsForMetric = useCallback(
index =>
(index: number) =>
getOptionsForSavedMetrics(
props.savedMetrics,
props.value,

View File

@@ -44,7 +44,7 @@ export default function Option({
}: OptionProps) {
const theme = useTheme();
const onClickClose = useCallback(
e => {
(e: React.MouseEvent) => {
e.stopPropagation();
clickClose(index);
},

View File

@@ -16,7 +16,7 @@
* specific language governing permissions and limitations
* under the License.
*/
import { PureComponent } from 'react';
import { PureComponent, ReactNode } from 'react';
import { OptionSortType } from 'src/explore/types';
import AdhocFilterEditPopover from 'src/explore/components/controls/FilterControl/AdhocFilterEditPopover';
import AdhocFilter from 'src/explore/components/controls/FilterControl/AdhocFilter';
@@ -37,6 +37,7 @@ interface AdhocFilterPopoverTriggerProps {
togglePopover?: (visible: boolean) => void;
closePopover?: () => void;
requireSave?: boolean;
children?: ReactNode;
}
interface AdhocFilterPopoverTriggerState {

View File

@@ -16,7 +16,7 @@
* specific language governing permissions and limitations
* under the License.
*/
import { renderHook, cleanup } from '@testing-library/react-hooks';
import { renderHook, cleanup } from '@testing-library/react';
import { TestDataset } from '@superset-ui/chart-controls';
import { useDatePickerInAdhocFilter } from './useDatePickerInAdhocFilter';

View File

@@ -16,7 +16,7 @@
* specific language governing permissions and limitations
* under the License.
*/
import { renderHook } from '@testing-library/react-hooks';
import { renderHook, waitFor } from '@testing-library/react';
import { NO_TIME_RANGE, fetchTimeRange } from '@superset-ui/core';
import { Operators } from 'src/explore/constants';
import { useGetTimeRangeLabel } from './useGetTimeRangeLabel';
@@ -80,10 +80,12 @@ test('should get actualTimeRange and title', async () => {
clause: Clauses.Where,
});
const { result } = await renderHook(() => useGetTimeRangeLabel(adhocFilter));
expect(result.current).toEqual({
actualTimeRange: 'MOCK TIME',
title: 'Last week',
const { result } = renderHook(() => useGetTimeRangeLabel(adhocFilter));
await waitFor(() => {
expect(result.current).toEqual({
actualTimeRange: 'MOCK TIME',
title: 'Last week',
});
});
});
@@ -98,9 +100,11 @@ test('should get actualTimeRange and title when gets an error', async () => {
clause: Clauses.Where,
});
const { result } = await renderHook(() => useGetTimeRangeLabel(adhocFilter));
expect(result.current).toEqual({
actualTimeRange: 'temporal column (Last week)',
title: 'MOCK ERROR',
const { result } = renderHook(() => useGetTimeRangeLabel(adhocFilter));
await waitFor(() => {
expect(result.current).toEqual({
actualTimeRange: 'temporal column (Last week)',
title: 'MOCK ERROR',
});
});
});

View File

@@ -107,7 +107,7 @@ test('accepts an edited metric from an AdhocMetricEditPopover', async () => {
userEvent.click(metricLabel);
await screen.findByText('aggregate');
selectOption('AVG', 'Select aggregate options');
await selectOption('AVG', 'Select aggregate options');
await screen.findByText('AVG(value)');