Skip to content

Commit

Permalink
refactor(sqllab): nonblocking delete query editor (#29233)
Browse files Browse the repository at this point in the history
  • Loading branch information
justinpark committed Jun 14, 2024
1 parent 7ddea62 commit ddc9f06
Show file tree
Hide file tree
Showing 13 changed files with 155 additions and 32 deletions.
28 changes: 6 additions & 22 deletions superset-frontend/src/SqlLab/actions/sqlLab.js
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@ export const CREATE_DATASOURCE_FAILED = 'CREATE_DATASOURCE_FAILED';

export const SET_EDITOR_TAB_LAST_UPDATE = 'SET_EDITOR_TAB_LAST_UPDATE';
export const SET_LAST_UPDATED_ACTIVE_TAB = 'SET_LAST_UPDATED_ACTIVE_TAB';
export const CLEAR_DESTROYED_QUERY_EDITOR = 'CLEAR_DESTROYED_QUERY_EDITOR';

export const addInfoToast = addInfoToastAction;
export const addSuccessToast = addSuccessToastAction;
Expand Down Expand Up @@ -715,29 +716,12 @@ export function toggleLeftBar(queryEditor) {
};
}

export function removeQueryEditor(queryEditor) {
return function (dispatch) {
const sync = isFeatureEnabled(FeatureFlag.SqllabBackendPersistence)
? SupersetClient.delete({
endpoint: encodeURI(`/tabstateview/${queryEditor.id}`),
})
: Promise.resolve();
export function clearDestoryedQueryEditor(queryEditorId) {
return { type: CLEAR_DESTROYED_QUERY_EDITOR, queryEditorId };
}

return sync
.then(() => dispatch({ type: REMOVE_QUERY_EDITOR, queryEditor }))
.catch(({ status }) => {
if (status !== 404) {
return dispatch(
addDangerToast(
t(
'An error occurred while removing tab. Please contact your administrator.',
),
),
);
}
return dispatch({ type: REMOVE_QUERY_EDITOR, queryEditor });
});
};
export function removeQueryEditor(queryEditor) {
return { type: REMOVE_QUERY_EDITOR, queryEditor };
}

export function removeAllOtherQueryEditors(queryEditor) {
Expand Down
10 changes: 3 additions & 7 deletions superset-frontend/src/SqlLab/actions/sqlLab.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -610,7 +610,7 @@ describe('async actions', () => {

describe('removeQueryEditor', () => {
it('updates the tab state in the backend', () => {
expect.assertions(2);
expect.assertions(1);

const store = mockStore({});
const expectedActions = [
Expand All @@ -619,12 +619,8 @@ describe('async actions', () => {
queryEditor,
},
];
return store
.dispatch(actions.removeQueryEditor(queryEditor))
.then(() => {
expect(store.getActions()).toEqual(expectedActions);
expect(fetchMock.calls(updateTabStateEndpoint)).toHaveLength(1);
});
store.dispatch(actions.removeQueryEditor(queryEditor));
expect(store.getActions()).toEqual(expectedActions);
});
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,29 @@ test('sync the active editor id when there are updates in tab history', async ()
expect(fetchMock.calls(updateActiveEditorTabState)).toHaveLength(1);
});

test('sync the destroyed editor id when there are updates in destroyed editors', async () => {
const removeId = 'removed-tab-id';
const deleteEditorState = `glob:*/tabstateview/${removeId}`;
fetchMock.delete(deleteEditorState, { id: removeId });
expect(fetchMock.calls(deleteEditorState)).toHaveLength(0);
render(<EditorAutoSync />, {
useRedux: true,
initialState: {
...initialState,
sqlLab: {
...initialState.sqlLab,
destroyedQueryEditors: {
[removeId]: 123,
},
},
},
});
await waitFor(() => jest.advanceTimersByTime(INTERVAL));
expect(fetchMock.calls(deleteEditorState)).toHaveLength(1);
await waitFor(() => jest.advanceTimersByTime(INTERVAL));
expect(fetchMock.calls(deleteEditorState)).toHaveLength(1);
});

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);
Expand Down
32 changes: 31 additions & 1 deletion superset-frontend/src/SqlLab/components/EditorAutoSync/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,12 +29,14 @@ import {
import {
useUpdateCurrentSqlEditorTabMutation,
useUpdateSqlEditorTabMutation,
useDeleteSqlEditorTabMutation,
} from 'src/hooks/apiResources/sqlEditorTabs';
import { useDebounceValue } from 'src/hooks/useDebounceValue';
import {
syncQueryEditor,
setEditorTabLastUpdate,
setLastUpdatedActiveTab,
clearDestoryedQueryEditor,
} from 'src/SqlLab/actions/sqlLab';
import useEffectEvent from 'src/hooks/useEffectEvent';

Expand Down Expand Up @@ -82,8 +84,13 @@ const EditorAutoSync: FC = () => {
const lastUpdatedActiveTab = useSelector<SqlLabRootState, string>(
({ sqlLab }) => sqlLab.lastUpdatedActiveTab,
);
const destroyedQueryEditors = useSelector<
SqlLabRootState,
Record<string, number>
>(({ sqlLab }) => sqlLab.destroyedQueryEditors);
const [updateSqlEditor, { error }] = useUpdateSqlEditorTabMutation();
const [updateCurrentSqlEditor] = useUpdateCurrentSqlEditorTabMutation();
const [deleteSqlEditor] = useDeleteSqlEditorTabMutation();

const debouncedUnsavedQueryEditor = useDebounceValue(
unsavedQueryEditor,
Expand Down Expand Up @@ -119,6 +126,22 @@ const EditorAutoSync: FC = () => {
}
});

const syncDeletedQueryEditor = useEffectEvent(() => {
if (Object.keys(destroyedQueryEditors).length > 0) {
Object.keys(destroyedQueryEditors).forEach(id => {
deleteSqlEditor(id)
.then(() => {
dispatch(clearDestoryedQueryEditor(id));
})
.catch(({ status }) => {
if (status === 404) {
dispatch(clearDestoryedQueryEditor(id));
}
});
});
}
});

useEffect(() => {
let saveTimer: NodeJS.Timeout;
function saveUnsavedQueryEditor() {
Expand All @@ -131,11 +154,18 @@ const EditorAutoSync: FC = () => {
}
const syncTimer = setInterval(syncCurrentQueryEditor, INTERVAL);
saveTimer = setTimeout(saveUnsavedQueryEditor, INTERVAL);
const clearQueryEditorTimer = setInterval(syncDeletedQueryEditor, INTERVAL);
return () => {
clearTimeout(saveTimer);
clearInterval(syncTimer);
clearInterval(clearQueryEditorTimer);
};
}, [getUnsavedNewQueryEditor, syncCurrentQueryEditor, dispatch]);
}, [
getUnsavedNewQueryEditor,
syncCurrentQueryEditor,
syncDeletedQueryEditor,
dispatch,
]);

useEffect(() => {
const unsaved = getUnsavedItems(debouncedUnsavedQueryEditor);
Expand Down
1 change: 1 addition & 0 deletions superset-frontend/src/SqlLab/fixtures.ts
Original file line number Diff line number Diff line change
Expand Up @@ -681,6 +681,7 @@ export const initialState = {
queriesLastUpdate: 0,
activeSouthPaneTab: 'Results',
unsavedQueryEditor: {},
destroyedQueryEditors: {},
},
messageToasts: [],
user,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ const sqlLabPersistStateConfig = {
queries,
tabHistory,
lastUpdatedActiveTab,
destroyedQueryEditors,
} = state.sqlLab;
const unsavedQueryEditors = filterUnsavedQueryEditorList(
queryEditors,
Expand All @@ -58,7 +59,13 @@ const sqlLabPersistStateConfig = {
);
const hasUnsavedActiveTabState =
tabHistory.slice(-1)[0] !== lastUpdatedActiveTab;
if (unsavedQueryEditors.length > 0 || hasUnsavedActiveTabState) {
const hasUnsavedDeletedQueryEditors =
Object.keys(destroyedQueryEditors).length > 0;
if (
unsavedQueryEditors.length > 0 ||
hasUnsavedActiveTabState ||
hasUnsavedDeletedQueryEditors
) {
const hasFinishedMigrationFromLocalStorage =
unsavedQueryEditors.every(
({ inLocalStorage }) => !inLocalStorage,
Expand All @@ -76,6 +83,7 @@ const sqlLabPersistStateConfig = {
...(hasUnsavedActiveTabState && {
tabHistory,
}),
destroyedQueryEditors,
};
}
return;
Expand Down
15 changes: 14 additions & 1 deletion superset-frontend/src/SqlLab/reducers/getInitialState.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -274,6 +274,9 @@ describe('getInitialState', () => {
name: expectedValue,
},
],
destroyedQueryEditors: {
10: 12345,
},
},
}),
);
Expand All @@ -291,7 +294,10 @@ describe('getInitialState', () => {
updatedAt: lastUpdatedTime,
},
},
tab_state_ids: [{ id: 1, label: '' }],
tab_state_ids: [
{ id: 1, label: '' },
{ id: 10, label: 'removed' },
],
};
expect(
getInitialState(apiDataWithLocalStorage).sqlLab.queryEditors[0],
Expand All @@ -301,6 +307,13 @@ describe('getInitialState', () => {
name: expectedValue,
}),
);
expect(
getInitialState(apiDataWithLocalStorage).sqlLab.queryEditors,
).not.toContainEqual(
expect.objectContaining({
id: '10',
}),
);
expect(
getInitialState(apiDataWithLocalStorage).sqlLab.lastUpdatedActiveTab,
).toEqual(apiDataWithTabState.active_tab.id.toString());
Expand Down
12 changes: 12 additions & 0 deletions superset-frontend/src/SqlLab/reducers/getInitialState.ts
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,8 @@ export default function getInitialState({
}),
};

const destroyedQueryEditors = {};

/**
* If the `SQLLAB_BACKEND_PERSISTENCE` feature flag is off, or if the user
* hasn't used SQL Lab after it has been turned on, the state will be stored
Expand Down Expand Up @@ -219,6 +221,15 @@ export default function getInitialState({
tabHistory.push(...sqlLab.tabHistory);
}
lastUpdatedActiveTab = tabHistory.slice(tabHistory.length - 1)[0] || '';

if (sqlLab.destroyedQueryEditors) {
Object.entries(sqlLab.destroyedQueryEditors).forEach(([id, ts]) => {
if (queryEditors[id]) {
destroyedQueryEditors[id] = ts;
delete queryEditors[id];
}
});
}
}
}
} catch (error) {
Expand Down Expand Up @@ -253,6 +264,7 @@ export default function getInitialState({
queryCostEstimates: {},
unsavedQueryEditor,
lastUpdatedActiveTab,
destroyedQueryEditors,
},
localStorageUsageInKilobytes: 0,
common,
Expand Down
9 changes: 9 additions & 0 deletions superset-frontend/src/SqlLab/reducers/sqlLab.js
Original file line number Diff line number Diff line change
Expand Up @@ -163,9 +163,18 @@ export default function sqlLabReducer(state = {}, action) {
...(action.queryEditor.id !== state.unsavedQueryEditor.id &&
state.unsavedQueryEditor),
},
destroyedQueryEditors: {
...newState.destroyedQueryEditors,
[queryEditor.id]: Date.now(),
},
};
return newState;
},
[actions.CLEAR_DESTROYED_QUERY_EDITOR]() {
const destroyedQueryEditors = { ...state.destroyedQueryEditors };
delete destroyedQueryEditors[action.queryEditorId];
return { ...state, destroyedQueryEditors };
},
[actions.REMOVE_QUERY]() {
const newQueries = { ...state.queries };
delete newQueries[action.query.id];
Expand Down
17 changes: 17 additions & 0 deletions superset-frontend/src/SqlLab/reducers/sqlLab.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -267,6 +267,23 @@ describe('sqlLabReducer', () => {
expect(newState.tabHistory).toContain('updatedNewId');
expect(newState.tabHistory).not.toContain(qe.id);
});
it('should clear the destroyed query editors', () => {
const expectedQEId = '1233289';
const action = {
type: actions.CLEAR_DESTROYED_QUERY_EDITOR,
queryEditorId: expectedQEId,
};
newState = sqlLabReducer(
{
...newState,
destroyedQueryEditors: {
[expectedQEId]: Date.now(),
},
},
action,
);
expect(newState.destroyedQueryEditors).toEqual({});
});
});
describe('Tables', () => {
let newState;
Expand Down
1 change: 1 addition & 0 deletions superset-frontend/src/SqlLab/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@ export type SqlLabRootState = {
queryCostEstimates?: Record<string, QueryCostEstimate>;
editorTabLastUpdatedAt: number;
lastUpdatedActiveTab: string;
destroyedQueryEditors: Record<string, number>;
};
localStorageUsageInKilobytes: number;
messageToasts: toastState[];
Expand Down
21 changes: 21 additions & 0 deletions superset-frontend/src/hooks/apiResources/sqlEditorTabs.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import {
import { api } from 'src/hooks/apiResources/queryApi';
import { LatestQueryEditorVersion } from 'src/SqlLab/types';
import {
useDeleteSqlEditorTabMutation,
useUpdateCurrentSqlEditorTabMutation,
useUpdateSqlEditorTabMutation,
} from './sqlEditorTabs';
Expand Down Expand Up @@ -120,3 +121,23 @@ test('posts activate request with queryEditorId', async () => {
expect(fetchMock.calls(tabStateMutationApiRoute).length).toBe(1),
);
});

test('deletes destoryed query editors', async () => {
const tabStateMutationApiRoute = `glob:*/tabstateview/${expectedQueryEditor.id}`;
fetchMock.delete(tabStateMutationApiRoute, 200);
const { result, waitFor } = renderHook(
() => useDeleteSqlEditorTabMutation(),
{
wrapper: createWrapper({
useRedux: true,
store,
}),
},
);
act(() => {
result.current[0](expectedQueryEditor.id);
});
await waitFor(() =>
expect(fetchMock.calls(tabStateMutationApiRoute).length).toBe(1),
);
});
8 changes: 8 additions & 0 deletions superset-frontend/src/hooks/apiResources/sqlEditorTabs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,10 +73,18 @@ const sqlEditorApi = api.injectEndpoints({
transformResponse: () => queryEditorId,
}),
}),
deleteSqlEditorTab: builder.mutation<void, string>({
query: queryEditorId => ({
method: 'DELETE',
endpoint: encodeURI(`/tabstateview/${queryEditorId}`),
transformResponse: () => queryEditorId,
}),
}),
}),
});

export const {
useUpdateSqlEditorTabMutation,
useUpdateCurrentSqlEditorTabMutation,
useDeleteSqlEditorTabMutation,
} = sqlEditorApi;

0 comments on commit ddc9f06

Please sign in to comment.