refactor(sqllab): nonblocking new query editor (#28795)

This commit is contained in:
JUST.in DO IT
2024-06-04 18:56:50 -07:00
committed by GitHub
parent 1a52c6a3b8
commit 8a8ce16a1f
8 changed files with 212 additions and 126 deletions

View File

@@ -40,7 +40,7 @@ import { render, waitFor } from 'spec/helpers/testing-library';
import ToastContainer from 'src/components/MessageToasts/ToastContainer';
import { initialState, defaultQueryEditor } from 'src/SqlLab/fixtures';
import { logging } from '@superset-ui/core';
import EditorAutoSync from '.';
import EditorAutoSync, { INTERVAL } from '.';
jest.mock('@superset-ui/core', () => ({
...jest.requireActual('@superset-ui/core'),
@@ -78,11 +78,37 @@ test('sync the unsaved editor tab state when there are new changes since the las
sqlLab: unsavedSqlLabState,
},
});
await waitFor(() => jest.runAllTimers());
await waitFor(() => jest.advanceTimersByTime(INTERVAL));
expect(fetchMock.calls(updateEditorTabState)).toHaveLength(1);
fetchMock.restore();
});
test('sync the unsaved NEW editor state when there are new in local storage', async () => {
const createEditorTabState = `glob:*/tabstateview/`;
fetchMock.post(createEditorTabState, { id: 123 });
expect(fetchMock.calls(createEditorTabState)).toHaveLength(0);
render(<EditorAutoSync />, {
useRedux: true,
initialState: {
...initialState,
sqlLab: {
...initialState.sqlLab,
queryEditors: [
...initialState.sqlLab.queryEditors,
{
id: 'rnd-new-id',
name: 'new tab name',
inLocalStorage: true,
},
],
},
},
});
await waitFor(() => jest.advanceTimersByTime(INTERVAL));
expect(fetchMock.calls(createEditorTabState)).toHaveLength(1);
fetchMock.restore();
});
test('skip syncing the unsaved editor tab state when the updates are already synced', async () => {
const updateEditorTabState = `glob:*/tabstateview/${defaultQueryEditor.id}`;
fetchMock.put(updateEditorTabState, 200);
@@ -102,7 +128,7 @@ test('skip syncing the unsaved editor tab state when the updates are already syn
},
},
});
await waitFor(() => jest.runAllTimers());
await waitFor(() => jest.advanceTimersByTime(INTERVAL));
expect(fetchMock.calls(updateEditorTabState)).toHaveLength(0);
fetchMock.restore();
});
@@ -126,7 +152,7 @@ test('renders an error toast when the sync failed', async () => {
},
},
);
await waitFor(() => jest.runAllTimers());
await waitFor(() => jest.advanceTimersByTime(INTERVAL));
expect(logging.warn).toHaveBeenCalledTimes(1);
expect(logging.warn).toHaveBeenCalledWith(

View File

@@ -27,9 +27,13 @@ import {
} from 'src/SqlLab/types';
import { useUpdateSqlEditorTabMutation } from 'src/hooks/apiResources/sqlEditorTabs';
import { useDebounceValue } from 'src/hooks/useDebounceValue';
import { setEditorTabLastUpdate } from 'src/SqlLab/actions/sqlLab';
import {
syncQueryEditor,
setEditorTabLastUpdate,
} from 'src/SqlLab/actions/sqlLab';
import useEffectEvent from 'src/hooks/useEffectEvent';
const INTERVAL = 5000;
export const INTERVAL = 5000;
function hasUnsavedChanges(
queryEditor: QueryEditor,
@@ -73,17 +77,43 @@ const EditorAutoSync: React.FC = () => {
INTERVAL,
);
useEffect(() => {
const unsaved = filterUnsavedQueryEditorList(
const getUnsavedItems = useEffectEvent(unsavedQE =>
filterUnsavedQueryEditorList(
queryEditors,
debouncedUnsavedQueryEditor,
unsavedQE,
lastSavedTimestampRef.current,
);
),
);
const getUnsavedNewQueryEditor = useEffectEvent(() =>
filterUnsavedQueryEditorList(
queryEditors,
unsavedQueryEditor,
lastSavedTimestampRef.current,
).find(({ inLocalStorage }) => Boolean(inLocalStorage)),
);
useEffect(() => {
let timer: NodeJS.Timeout;
function saveUnsavedQueryEditor() {
const firstUnsavedQueryEditor = getUnsavedNewQueryEditor();
if (firstUnsavedQueryEditor) {
dispatch(syncQueryEditor(firstUnsavedQueryEditor));
}
timer = setTimeout(saveUnsavedQueryEditor, INTERVAL);
}
timer = setTimeout(saveUnsavedQueryEditor, INTERVAL);
return () => {
clearTimeout(timer);
};
}, [getUnsavedNewQueryEditor, dispatch]);
useEffect(() => {
const unsaved = getUnsavedItems(debouncedUnsavedQueryEditor);
Promise.all(
unsaved
// TODO: Migrate migrateQueryEditorFromLocalStorage
// in TabbedSqlEditors logic by addSqlEditor mutation later
.filter(({ inLocalStorage }) => !inLocalStorage)
.map(queryEditor => updateSqlEditor({ queryEditor })),
).then(resolvers => {
@@ -92,7 +122,7 @@ const EditorAutoSync: React.FC = () => {
dispatch(setEditorTabLastUpdate(lastSavedTimestampRef.current));
}
});
}, [debouncedUnsavedQueryEditor, dispatch, queryEditors, updateSqlEditor]);
}, [debouncedUnsavedQueryEditor, getUnsavedItems, dispatch, updateSqlEditor]);
useEffect(() => {
if (error) {

View File

@@ -165,8 +165,8 @@ test('should disable new tab when offline', () => {
});
expect(queryAllByLabelText('Add tab').length).toEqual(0);
});
test('should have an empty state when query editors is empty', () => {
const { getByText } = setup(undefined, {
test('should have an empty state when query editors is empty', async () => {
const { getByText, getByRole } = setup(undefined, {
...initialState,
sqlLab: {
...initialState.sqlLab,
@@ -174,5 +174,12 @@ test('should have an empty state when query editors is empty', () => {
tabHistory: [],
},
});
expect(getByText('Add a new tab to create SQL Query')).toBeInTheDocument();
// Clear the new tab applied in componentDidMount and check the state of the empty tab
const removeTabButton = getByRole('button', { name: 'remove' });
fireEvent.click(removeTabButton);
await waitFor(() =>
expect(getByText('Add a new tab to create SQL Query')).toBeInTheDocument(),
);
});

View File

@@ -70,32 +70,6 @@ class TabbedSqlEditors extends React.PureComponent<TabbedSqlEditorsProps> {
}
componentDidMount() {
// migrate query editor and associated tables state to server
if (isFeatureEnabled(FeatureFlag.SqllabBackendPersistence)) {
const localStorageTables = this.props.tables.filter(
table => table.inLocalStorage,
);
const localStorageQueries = Object.values(this.props.queries).filter(
query => query.inLocalStorage,
);
this.props.queryEditors
.filter(qe => qe.inLocalStorage)
.forEach(qe => {
// get all queries associated with the query editor
const queries = localStorageQueries.filter(
query => query.sqlEditorId === qe.id,
);
const tables = localStorageTables.filter(
table => table.queryEditorId === qe.id,
);
this.props.actions.migrateQueryEditorFromLocalStorage(
qe,
tables,
queries,
);
});
}
// merge post form data with GET search params
// Hack: this data should be coming from getInitialState
// but for some reason this data isn't being passed properly through
@@ -322,7 +296,6 @@ export function mapStateToProps({ sqlLab, common }: SqlLabRootState) {
queryEditors: sqlLab.queryEditors ?? DEFAULT_PROPS.queryEditors,
queries: sqlLab.queries,
tabHistory: sqlLab.tabHistory,
tables: sqlLab.tables,
defaultDbId: common.conf.SQLLAB_DEFAULT_DBID,
displayLimit: common.conf.DISPLAY_MAX_ROW,
offline: sqlLab.offline ?? DEFAULT_PROPS.offline,