mirror of
https://github.com/apache/superset.git
synced 2026-04-25 02:55:07 +00:00
refactor(sqllab): nonblocking new query editor (#28795)
This commit is contained in:
@@ -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(
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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(),
|
||||
);
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user