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

@@ -65,7 +65,7 @@ const ContentWrapper = styled.div`
overflow: auto;
`;
const AppLayout: React.FC = ({ children }) => {
const AppLayout: React.FC<{ children?: React.ReactNode }> = ({ children }) => {
const queryEditorId = useSelector<SqlLabRootState, string>(
({ sqlLab: { tabHistory } }) => tabHistory.slice(-1)[0],
);

View File

@@ -19,16 +19,14 @@
import { isValidElement } from 'react';
import { render } from 'spec/helpers/testing-library';
import ColumnElement from 'src/SqlLab/components/ColumnElement';
import { mockedActions, table } from 'src/SqlLab/fixtures';
import { table } from 'src/SqlLab/fixtures';
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
describe('ColumnElement', () => {
const mockedProps = {
actions: mockedActions,
column: table.columns[0],
};
test('is valid with props', () => {
expect(isValidElement(<ColumnElement {...mockedProps} />)).toBe(true);
expect(isValidElement(<ColumnElement column={table.columns[0]} />)).toBe(
true,
);
});
test('renders a proper primary key', () => {
const { container } = render(<ColumnElement column={table.columns[0]} />);

View File

@@ -116,19 +116,31 @@ describe('EditorWrapper', () => {
);
});
test('skips rerendering for updating cursor position', () => {
test('skips rerendering for updating cursor position', async () => {
const store = createStore(initialState, reducerIndex);
setup(defaultQueryEditor, store);
expect(MockEditorHost).toHaveBeenCalled();
const renderCount = MockEditorHost.mock.calls.length;
await waitFor(() => expect(MockEditorHost).toHaveBeenCalled());
const renderCountBeforeCursor = MockEditorHost.mock.calls.length;
const updatedCursorPosition = { row: 1, column: 9 };
store.dispatch(
queryEditorSetCursorPosition(defaultQueryEditor, updatedCursorPosition),
act(() => {
store.dispatch(
queryEditorSetCursorPosition(defaultQueryEditor, updatedCursorPosition),
);
});
// Cursor position change should NOT trigger a re-render
expect(MockEditorHost).toHaveBeenCalledTimes(renderCountBeforeCursor);
const renderCountBeforeDb = MockEditorHost.mock.calls.length;
act(() => {
store.dispatch(queryEditorSetDb(defaultQueryEditor, 2));
});
// DB change SHOULD trigger a re-render
await waitFor(() =>
expect(MockEditorHost.mock.calls.length).toBeGreaterThan(
renderCountBeforeDb,
),
);
expect(MockEditorHost).toHaveBeenCalledTimes(renderCount);
store.dispatch(queryEditorSetDb(defaultQueryEditor, 2));
expect(MockEditorHost).toHaveBeenCalledTimes(renderCount + 1);
});
test('clears selectedText when selection becomes empty', async () => {

View File

@@ -17,7 +17,7 @@
* under the License.
*/
import fetchMock from 'fetch-mock';
import { act, renderHook } from '@testing-library/react-hooks';
import { act, renderHook, waitFor } from '@testing-library/react';
import { COMMON_ERR_MESSAGES } from '@superset-ui/core';
import {
createWrapper,
@@ -121,7 +121,7 @@ test('skips fetching validation if validator is undefined', () => {
});
test('returns validation if validator is configured', async () => {
const { result, waitFor } = initialize(true);
const { result } = initialize(true);
await waitFor(() =>
expect(fetchMock.callHistory.calls(queryValidationApiRoute)).toHaveLength(
1,
@@ -143,7 +143,7 @@ test('returns server error description', async () => {
fetchMock.post(queryValidationApiRoute, {
throws: new Error(errorMessage),
});
const { result, waitFor } = initialize(true);
const { result } = initialize(true);
await waitFor(
() =>
expect(result.current.data).toEqual([
@@ -164,7 +164,7 @@ test('returns session expire description when CSRF token expired', async () => {
fetchMock.post(queryValidationApiRoute, {
throws: new Error(errorMessage),
});
const { result, waitFor } = initialize(true);
const { result } = initialize(true);
await waitFor(
() =>
expect(result.current.data).toEqual([

View File

@@ -17,7 +17,7 @@
* under the License.
*/
import fetchMock from 'fetch-mock';
import { act, renderHook } from '@testing-library/react-hooks';
import { act, renderHook, waitFor } from '@testing-library/react';
import { getExtensionsRegistry } from '@superset-ui/core';
import {
createWrapper,
@@ -104,7 +104,7 @@ test('returns keywords including fetched function_names data', async () => {
const dbFunctionNamesApiRoute = `glob:*/api/v1/database/${expectDbId}/function_names/`;
fetchMock.get(dbFunctionNamesApiRoute, fakeFunctionNamesApiResult);
const { result, waitFor } = renderHook(
const { result } = renderHook(
() =>
useKeywords({
queryEditorId: 'testqueryid',
@@ -241,7 +241,7 @@ test('returns column keywords among selected tables', async () => {
);
});
const { result, waitFor } = renderHook(
const { result } = renderHook(
() =>
useKeywords({
queryEditorId: expectQueryEditorId,
@@ -302,7 +302,7 @@ test('returns long keywords with detail', async () => {
),
);
});
const { result, waitFor } = renderHook(
const { result } = renderHook(
() =>
useKeywords({
queryEditorId: 'testqueryid',

View File

@@ -16,7 +16,7 @@
* specific language governing permissions and limitations
* under the License.
*/
import { FC } from 'react';
import { FC, ReactNode } from 'react';
import { t } from '@apache-superset/core/translation';
import { styled, css } from '@apache-superset/core/theme';
import { ModalTrigger } from '@superset-ui/core/components';
@@ -92,7 +92,7 @@ const ShortcutCode = styled.code`
padding: ${({ theme }) => `${theme.sizeUnit}px ${theme.sizeUnit * 2}px`};
`;
const KeyboardShortcutButton: FC<{}> = ({ children }) => (
const KeyboardShortcutButton: FC<{ children?: ReactNode }> = ({ children }) => (
<ModalTrigger
modalTitle={t('Keyboard shortcuts')}
modalBody={

View File

@@ -39,7 +39,9 @@ import getBootstrapData from 'src/utils/getBootstrapData';
const SQL_LAB_URL = '/sqllab';
const PopEditorTab: React.FC = ({ children }) => {
const PopEditorTab: React.FC<{ children?: React.ReactNode }> = ({
children,
}) => {
const [isLoading, setIsLoading] = useState(false);
const [queryEditorId, setQueryEditorId] = useState<string>();
const { requestedQuery } = useLocationState();

View File

@@ -50,14 +50,23 @@ import { StaticPosition, StyledTooltip, ModalResultSetWrapper } from './styles';
interface QueryTableQuery extends Omit<
QueryResponse,
'state' | 'sql' | 'progress' | 'results' | 'duration' | 'started'
| 'state'
| 'sql'
| 'progress'
| 'results'
| 'duration'
| 'started'
| 'user'
| 'db'
> {
state?: Record<string, any>;
sql?: Record<string, any>;
progress?: Record<string, any>;
results?: Record<string, any>;
state?: ReactNode;
sql?: ReactNode;
progress?: ReactNode;
results?: ReactNode;
duration?: ReactNode;
started?: ReactNode;
user?: ReactNode;
db?: ReactNode;
}
interface QueryTableProps {
@@ -249,7 +258,7 @@ const QueryTable = ({
return queries
.map(query => {
const { state, sql, progress, ...rest } = query;
const { state, sql, progress, results: _results, ...rest } = query;
const q = rest as QueryTableQuery;
const status = statusAttributes[state] || statusAttributes.error;
@@ -265,7 +274,7 @@ const QueryTable = ({
buttonStyle="link"
onClick={() => onUserClicked(q.userId)}
>
{q.user}
{q.user as ReactNode}
</Button>
);
q.db = (
@@ -274,7 +283,7 @@ const QueryTable = ({
buttonStyle="link"
onClick={() => onDbClicked(q.dbId)}
>
{q.db}
{q.db as ReactNode}
</Button>
);
q.started = (

View File

@@ -16,13 +16,13 @@
* specific language governing permissions and limitations
* under the License.
*/
import { useMemo, FC, ReactElement } from 'react';
import { useMemo, FC, ReactElement, type ReactNode } from 'react';
import { t } from '@apache-superset/core/translation';
import { styled, useTheme, SupersetTheme } from '@apache-superset/core/theme';
import { Button, DropdownButton } from '@superset-ui/core/components';
import { IconType, Icons } from '@superset-ui/core/components/Icons';
import { Icons } from '@superset-ui/core/components/Icons';
import { detectOS } from 'src/utils/common';
import { QueryButtonProps } from 'src/SqlLab/types';
import useQueryEditor from 'src/SqlLab/hooks/useQueryEditor';
@@ -45,9 +45,9 @@ const buildTextAndIcon = (
shouldShowStopButton: boolean,
selectedText: string | undefined,
theme: SupersetTheme,
): { text: string; icon?: IconType } => {
): { text: string; icon?: ReactNode } => {
let text = t('Run');
let icon: IconType | undefined = <Icons.CaretRightOutlined />;
let icon: ReactNode = <Icons.CaretRightOutlined />;
if (selectedText) {
text = t('Run selection');
icon = <Icons.StepForwardOutlined />;

View File

@@ -17,6 +17,7 @@
* under the License.
*/
import * as reactRedux from 'react-redux';
import { act } from 'react';
import {
cleanup,
fireEvent,
@@ -136,7 +137,7 @@ describe('SaveDatasetModal', () => {
const overwriteRadioBtn = screen.getByRole('radio', {
name: /overwrite existing/i,
});
userEvent.click(overwriteRadioBtn);
await userEvent.click(overwriteRadioBtn);
// Overwrite confirmation button should be disabled at this point
const overwriteConfirmationBtn = screen.getByRole('button', {
@@ -146,15 +147,21 @@ describe('SaveDatasetModal', () => {
// Click the overwrite select component
const select = screen.getByRole('combobox', { name: /existing dataset/i })!;
userEvent.click(select);
await userEvent.click(select);
await waitFor(() =>
expect(screen.queryByText('Loading...')).not.toBeVisible(),
);
// Advance timers to flush debounced fetches in AsyncSelect
await act(async () => {
jest.runAllTimers();
});
await waitFor(() => {
const loading = screen.queryByText('Loading...');
expect(loading === null || !loading.checkVisibility()).toBe(true);
});
// Select the first "existing dataset" from the listbox
const option = screen.getAllByText('coolest table 0')[1];
userEvent.click(option);
await userEvent.click(option);
// Overwrite button should now be enabled
expect(overwriteConfirmationBtn).toBeEnabled();
@@ -168,25 +175,31 @@ describe('SaveDatasetModal', () => {
const overwriteRadioBtn = screen.getByRole('radio', {
name: /overwrite existing/i,
});
userEvent.click(overwriteRadioBtn);
await userEvent.click(overwriteRadioBtn);
// Click the overwrite select component
const select = screen.getByRole('combobox', { name: /existing dataset/i });
userEvent.click(select);
await userEvent.click(select);
await waitFor(() =>
expect(screen.queryByText('Loading...')).not.toBeVisible(),
);
// Advance timers to flush debounced fetches in AsyncSelect
await act(async () => {
jest.runAllTimers();
});
await waitFor(() => {
const loading = screen.queryByText('Loading...');
expect(loading === null || !loading.checkVisibility()).toBe(true);
});
// Select the first "existing dataset" from the listbox
const option = screen.getAllByText('coolest table 0')[1];
userEvent.click(option);
await userEvent.click(option);
// Click the overwrite button to access the confirmation screen
const overwriteConfirmationBtn = screen.getByRole('button', {
name: /overwrite/i,
});
userEvent.click(overwriteConfirmationBtn);
await userEvent.click(overwriteConfirmationBtn);
// Overwrite screen text
expect(screen.getByText(/save or overwrite dataset/i)).toBeInTheDocument();

View File

@@ -140,7 +140,7 @@ const SouthPane = ({
logAction(LOG_ACTIONS_SQLLAB_SWITCH_SOUTH_PANE_TAB, { tab: id });
};
const removeTable = useCallback(
(key, action) => {
(key: string, action: string) => {
if (action === 'remove') {
const table = pinnedTables.find(
({ dbId, catalog, schema, name }) =>

View File

@@ -292,7 +292,10 @@ const SqlEditor: FC<Props> = ({
const SqlFormExtension = extensionsRegistry.get('sqleditor.extension.form');
const startQuery = useCallback(
(ctasArg = false, ctas_method = CtasEnum.Table) => {
(
ctasArg = false,
ctas_method: (typeof CtasEnum)[keyof typeof CtasEnum] = CtasEnum.Table,
) => {
if (!database) {
return;
}

View File

@@ -18,7 +18,7 @@
*/
import configureStore from 'redux-mock-store';
import thunk from 'redux-thunk';
import { renderHook, act } from '@testing-library/react-hooks';
import { renderHook, act } from '@testing-library/react';
import { createWrapper } from 'spec/helpers/testing-library';
import { initialState, defaultQueryEditor } from 'src/SqlLab/fixtures';
import * as localStorageHelpers from 'src/utils/localStorageHelpers';

View File

@@ -19,7 +19,7 @@
import configureStore from 'redux-mock-store';
import thunk from 'redux-thunk';
import { initialState, defaultQueryEditor } from 'src/SqlLab/fixtures';
import { renderHook } from '@testing-library/react-hooks';
import { renderHook } from '@testing-library/react';
import { createWrapper } from 'spec/helpers/testing-library';
import useQueryEditor from '.';