perf(sqllab): Rendering perf improvement using immutable state (#20877)

* perf(sqllab): Rendering perf improvement using immutable state

- keep queryEditors immutable during active state
- add unsavedQueryEditor to store all active changes
- refactor each component to subscribe the related unsaved editor state only

* revert ISaveableDatasource type cast

* missing trigger prop

* a default of an empty object and optional operator
This commit is contained in:
JUST.in DO IT
2022-08-23 08:17:19 -07:00
committed by GitHub
parent 4ca4a5c7cb
commit f77b910e2c
32 changed files with 1929 additions and 606 deletions

View File

@@ -17,38 +17,100 @@
* under the License.
*/
import React, { ReactNode } from 'react';
import React from 'react';
import configureStore from 'redux-mock-store';
import thunk from 'redux-thunk';
import { Store } from 'redux';
import {
render,
fireEvent,
getByText,
waitFor,
} from 'spec/helpers/testing-library';
import { ThemeProvider, supersetTheme } from '@superset-ui/core';
import { initialState, defaultQueryEditor } from 'src/SqlLab/fixtures';
import TemplateParamsEditor from 'src/SqlLab/components/TemplateParamsEditor';
import TemplateParamsEditor, {
Props,
} from 'src/SqlLab/components/TemplateParamsEditor';
const ThemeWrapper = ({ children }: { children: ReactNode }) => (
<ThemeProvider theme={supersetTheme}>{children}</ThemeProvider>
);
jest.mock('src/components/Select', () => () => (
<div data-test="mock-deprecated-select" />
));
jest.mock('src/components/Select/Select', () => () => (
<div data-test="mock-deprecated-select-select" />
));
jest.mock('src/components/Select/AsyncSelect', () => () => (
<div data-test="mock-async-select" />
));
jest.mock('src/components/AsyncAceEditor', () => ({
ConfigEditor: ({ value }: { value: string }) => (
<div data-test="mock-async-ace-editor">{value}</div>
),
}));
const middlewares = [thunk];
const mockStore = configureStore(middlewares);
const setup = (otherProps: Partial<Props> = {}, store?: Store) =>
render(
<TemplateParamsEditor
language="json"
onChange={() => {}}
queryEditor={defaultQueryEditor}
{...otherProps}
/>,
{
useRedux: true,
store: mockStore(initialState),
...(store && { store }),
},
);
describe('TemplateParamsEditor', () => {
it('should render with a title', () => {
const { container } = render(
<TemplateParamsEditor code="FOO" language="json" onChange={() => {}} />,
{ wrapper: ThemeWrapper },
);
const { container } = setup();
expect(container.querySelector('div[role="button"]')).toBeInTheDocument();
});
it('should open a modal with the ace editor', async () => {
const { container, baseElement } = render(
<TemplateParamsEditor code="FOO" language="json" onChange={() => {}} />,
{ wrapper: ThemeWrapper },
const { container, getByTestId } = setup();
fireEvent.click(getByText(container, 'Parameters'));
await waitFor(() => {
expect(getByTestId('mock-async-ace-editor')).toBeInTheDocument();
});
});
it('renders templateParams', async () => {
const { container, getByTestId } = setup();
fireEvent.click(getByText(container, 'Parameters'));
await waitFor(() => {
expect(getByTestId('mock-async-ace-editor')).toBeInTheDocument();
});
expect(getByTestId('mock-async-ace-editor')).toHaveTextContent(
defaultQueryEditor.templateParams,
);
});
it('renders code from unsaved changes', async () => {
const expectedCode = 'custom code value';
const { container, getByTestId } = setup(
{},
mockStore({
...initialState,
sqlLab: {
...initialState.sqlLab,
unsavedQueryEditor: {
id: defaultQueryEditor.id,
templateParams: expectedCode,
},
},
}),
);
fireEvent.click(getByText(container, 'Parameters'));
await waitFor(() => {
expect(baseElement.querySelector('#ace-editor')).toBeInTheDocument();
expect(getByTestId('mock-async-ace-editor')).toBeInTheDocument();
});
expect(getByTestId('mock-async-ace-editor')).toHaveTextContent(
expectedCode,
);
});
});

View File

@@ -26,6 +26,9 @@ import ModalTrigger from 'src/components/ModalTrigger';
import { ConfigEditor } from 'src/components/AsyncAceEditor';
import { FAST_DEBOUNCE } from 'src/constants';
import { Tooltip } from 'src/components/Tooltip';
import { useSelector } from 'react-redux';
import { QueryEditor, SqlLabRootState } from 'src/SqlLab/types';
import { getUpToDateQuery } from 'src/SqlLab/actions/sqlLab';
const StyledConfigEditor = styled(ConfigEditor)`
&.ace_editor {
@@ -33,17 +36,24 @@ const StyledConfigEditor = styled(ConfigEditor)`
}
`;
function TemplateParamsEditor({
code = '{}',
language,
onChange = () => {},
}: {
code: string;
export type Props = {
queryEditor: QueryEditor;
language: 'yaml' | 'json';
onChange: () => void;
}) {
};
function TemplateParamsEditor({
queryEditor,
language,
onChange = () => {},
}: Props) {
const [parsedJSON, setParsedJSON] = useState({});
const [isValid, setIsValid] = useState(true);
const code = useSelector<SqlLabRootState, string>(
rootState =>
(getUpToDateQuery(rootState, queryEditor) as unknown as QueryEditor)
.templateParams || '{}',
);
useEffect(() => {
try {