This is an automated email from the ASF dual-hosted git repository.

justinpark pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/superset.git


The following commit(s) were added to refs/heads/master by this push:
     new ddc9f06786 refactor(sqllab): nonblocking delete query editor (#29233)
ddc9f06786 is described below

commit ddc9f06786a71dd16d89c0e74c9608b5432b2de9
Author: JUST.in DO IT <[email protected]>
AuthorDate: Fri Jun 14 12:25:55 2024 -0700

    refactor(sqllab): nonblocking delete query editor (#29233)
---
 superset-frontend/src/SqlLab/actions/sqlLab.js     | 28 ++++---------------
 .../src/SqlLab/actions/sqlLab.test.js              | 10 ++-----
 .../EditorAutoSync/EditorAutoSync.test.tsx         | 23 ++++++++++++++++
 .../src/SqlLab/components/EditorAutoSync/index.tsx | 32 +++++++++++++++++++++-
 superset-frontend/src/SqlLab/fixtures.ts           |  1 +
 .../middlewares/persistSqlLabStateEnhancer.js      | 10 ++++++-
 .../src/SqlLab/reducers/getInitialState.test.ts    | 15 +++++++++-
 .../src/SqlLab/reducers/getInitialState.ts         | 12 ++++++++
 superset-frontend/src/SqlLab/reducers/sqlLab.js    |  9 ++++++
 .../src/SqlLab/reducers/sqlLab.test.js             | 17 ++++++++++++
 superset-frontend/src/SqlLab/types.ts              |  1 +
 .../src/hooks/apiResources/sqlEditorTabs.test.ts   | 21 ++++++++++++++
 .../src/hooks/apiResources/sqlEditorTabs.ts        |  8 ++++++
 13 files changed, 155 insertions(+), 32 deletions(-)

diff --git a/superset-frontend/src/SqlLab/actions/sqlLab.js 
b/superset-frontend/src/SqlLab/actions/sqlLab.js
index aa81f8429e..18b74d2afc 100644
--- a/superset-frontend/src/SqlLab/actions/sqlLab.js
+++ b/superset-frontend/src/SqlLab/actions/sqlLab.js
@@ -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;
@@ -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) {
diff --git a/superset-frontend/src/SqlLab/actions/sqlLab.test.js 
b/superset-frontend/src/SqlLab/actions/sqlLab.test.js
index db051da5ea..11ab424512 100644
--- a/superset-frontend/src/SqlLab/actions/sqlLab.test.js
+++ b/superset-frontend/src/SqlLab/actions/sqlLab.test.js
@@ -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 = [
@@ -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);
       });
     });
 
diff --git 
a/superset-frontend/src/SqlLab/components/EditorAutoSync/EditorAutoSync.test.tsx
 
b/superset-frontend/src/SqlLab/components/EditorAutoSync/EditorAutoSync.test.tsx
index 6ccae610a1..121ca07521 100644
--- 
a/superset-frontend/src/SqlLab/components/EditorAutoSync/EditorAutoSync.test.tsx
+++ 
b/superset-frontend/src/SqlLab/components/EditorAutoSync/EditorAutoSync.test.tsx
@@ -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);
diff --git a/superset-frontend/src/SqlLab/components/EditorAutoSync/index.tsx 
b/superset-frontend/src/SqlLab/components/EditorAutoSync/index.tsx
index 5477c4fa13..0b9173d368 100644
--- a/superset-frontend/src/SqlLab/components/EditorAutoSync/index.tsx
+++ b/superset-frontend/src/SqlLab/components/EditorAutoSync/index.tsx
@@ -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';
 
@@ -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,
@@ -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() {
@@ -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);
diff --git a/superset-frontend/src/SqlLab/fixtures.ts 
b/superset-frontend/src/SqlLab/fixtures.ts
index 54f1c278f8..95d45ba760 100644
--- a/superset-frontend/src/SqlLab/fixtures.ts
+++ b/superset-frontend/src/SqlLab/fixtures.ts
@@ -681,6 +681,7 @@ export const initialState = {
     queriesLastUpdate: 0,
     activeSouthPaneTab: 'Results',
     unsavedQueryEditor: {},
+    destroyedQueryEditors: {},
   },
   messageToasts: [],
   user,
diff --git 
a/superset-frontend/src/SqlLab/middlewares/persistSqlLabStateEnhancer.js 
b/superset-frontend/src/SqlLab/middlewares/persistSqlLabStateEnhancer.js
index d83e06d316..12c9c34853 100644
--- a/superset-frontend/src/SqlLab/middlewares/persistSqlLabStateEnhancer.js
+++ b/superset-frontend/src/SqlLab/middlewares/persistSqlLabStateEnhancer.js
@@ -50,6 +50,7 @@ const sqlLabPersistStateConfig = {
             queries,
             tabHistory,
             lastUpdatedActiveTab,
+            destroyedQueryEditors,
           } = state.sqlLab;
           const unsavedQueryEditors = filterUnsavedQueryEditorList(
             queryEditors,
@@ -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,
@@ -76,6 +83,7 @@ const sqlLabPersistStateConfig = {
               ...(hasUnsavedActiveTabState && {
                 tabHistory,
               }),
+              destroyedQueryEditors,
             };
           }
           return;
diff --git a/superset-frontend/src/SqlLab/reducers/getInitialState.test.ts 
b/superset-frontend/src/SqlLab/reducers/getInitialState.test.ts
index 1e7745f6d0..f7e5897c41 100644
--- a/superset-frontend/src/SqlLab/reducers/getInitialState.test.ts
+++ b/superset-frontend/src/SqlLab/reducers/getInitialState.test.ts
@@ -274,6 +274,9 @@ describe('getInitialState', () => {
                 name: expectedValue,
               },
             ],
+            destroyedQueryEditors: {
+              10: 12345,
+            },
           },
         }),
       );
@@ -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],
@@ -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());
diff --git a/superset-frontend/src/SqlLab/reducers/getInitialState.ts 
b/superset-frontend/src/SqlLab/reducers/getInitialState.ts
index 6a156bacfd..6052daedbd 100644
--- a/superset-frontend/src/SqlLab/reducers/getInitialState.ts
+++ b/superset-frontend/src/SqlLab/reducers/getInitialState.ts
@@ -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
@@ -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) {
@@ -253,6 +264,7 @@ export default function getInitialState({
       queryCostEstimates: {},
       unsavedQueryEditor,
       lastUpdatedActiveTab,
+      destroyedQueryEditors,
     },
     localStorageUsageInKilobytes: 0,
     common,
diff --git a/superset-frontend/src/SqlLab/reducers/sqlLab.js 
b/superset-frontend/src/SqlLab/reducers/sqlLab.js
index f290d29f6a..5f0b27e97a 100644
--- a/superset-frontend/src/SqlLab/reducers/sqlLab.js
+++ b/superset-frontend/src/SqlLab/reducers/sqlLab.js
@@ -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];
diff --git a/superset-frontend/src/SqlLab/reducers/sqlLab.test.js 
b/superset-frontend/src/SqlLab/reducers/sqlLab.test.js
index 0cf6b1d487..c3b603667b 100644
--- a/superset-frontend/src/SqlLab/reducers/sqlLab.test.js
+++ b/superset-frontend/src/SqlLab/reducers/sqlLab.test.js
@@ -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;
diff --git a/superset-frontend/src/SqlLab/types.ts 
b/superset-frontend/src/SqlLab/types.ts
index 2dee58c223..eb49ca8ac1 100644
--- a/superset-frontend/src/SqlLab/types.ts
+++ b/superset-frontend/src/SqlLab/types.ts
@@ -110,6 +110,7 @@ export type SqlLabRootState = {
     queryCostEstimates?: Record<string, QueryCostEstimate>;
     editorTabLastUpdatedAt: number;
     lastUpdatedActiveTab: string;
+    destroyedQueryEditors: Record<string, number>;
   };
   localStorageUsageInKilobytes: number;
   messageToasts: toastState[];
diff --git a/superset-frontend/src/hooks/apiResources/sqlEditorTabs.test.ts 
b/superset-frontend/src/hooks/apiResources/sqlEditorTabs.test.ts
index e2bb7c41e9..42f738f3c9 100644
--- a/superset-frontend/src/hooks/apiResources/sqlEditorTabs.test.ts
+++ b/superset-frontend/src/hooks/apiResources/sqlEditorTabs.test.ts
@@ -25,6 +25,7 @@ import {
 import { api } from 'src/hooks/apiResources/queryApi';
 import { LatestQueryEditorVersion } from 'src/SqlLab/types';
 import {
+  useDeleteSqlEditorTabMutation,
   useUpdateCurrentSqlEditorTabMutation,
   useUpdateSqlEditorTabMutation,
 } from './sqlEditorTabs';
@@ -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),
+  );
+});
diff --git a/superset-frontend/src/hooks/apiResources/sqlEditorTabs.ts 
b/superset-frontend/src/hooks/apiResources/sqlEditorTabs.ts
index 29fe102235..90f4a057c1 100644
--- a/superset-frontend/src/hooks/apiResources/sqlEditorTabs.ts
+++ b/superset-frontend/src/hooks/apiResources/sqlEditorTabs.ts
@@ -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;

Reply via email to