feat(sqllab): non-blocking persistence mode (#24539)

Co-authored-by: Justin Park <justinpark@apache.org>
This commit is contained in:
JUST.in DO IT 2023-11-20 11:13:54 -08:00 committed by GitHub
parent 628cd345f2
commit e2bfb1216b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 746 additions and 433 deletions

View File

@ -99,6 +99,8 @@ export const CREATE_DATASOURCE_STARTED = 'CREATE_DATASOURCE_STARTED';
export const CREATE_DATASOURCE_SUCCESS = 'CREATE_DATASOURCE_SUCCESS';
export const CREATE_DATASOURCE_FAILED = 'CREATE_DATASOURCE_FAILED';
export const SET_EDITOR_TAB_LAST_UPDATE = 'SET_EDITOR_TAB_LAST_UPDATE';
export const addInfoToast = addInfoToastAction;
export const addSuccessToast = addSuccessToastAction;
export const addDangerToast = addDangerToastAction;
@ -160,6 +162,10 @@ export function updateQueryEditor(alterations) {
return { type: UPDATE_QUERY_EDITOR, alterations };
}
export function setEditorTabLastUpdate(timestamp) {
return { type: SET_EDITOR_TAB_LAST_UPDATE, timestamp };
}
export function scheduleQuery(query) {
return dispatch =>
SupersetClient.post({
@ -237,44 +243,11 @@ export function startQuery(query) {
}
export function querySuccess(query, results) {
return function (dispatch) {
const sqlEditorId = results?.query?.sqlEditorId;
const sync =
sqlEditorId &&
!query.isDataPreview &&
isFeatureEnabled(FeatureFlag.SQLLAB_BACKEND_PERSISTENCE)
? SupersetClient.put({
endpoint: encodeURI(`/tabstateview/${sqlEditorId}`),
postPayload: { latest_query_id: query.id },
})
: Promise.resolve();
return sync
.then(() => dispatch({ type: QUERY_SUCCESS, query, results }))
.catch(() =>
dispatch(
addDangerToast(
t(
'An error occurred while storing the latest query id in the backend. ' +
'Please contact your administrator if this problem persists.',
),
),
),
);
};
return { type: QUERY_SUCCESS, query, results };
}
export function queryFailed(query, msg, link, errors) {
return function (dispatch) {
const sync =
!query.isDataPreview &&
isFeatureEnabled(FeatureFlag.SQLLAB_BACKEND_PERSISTENCE)
? SupersetClient.put({
endpoint: encodeURI(`/tabstateview/${query.sqlEditorId}`),
postPayload: { latest_query_id: query.id },
})
: Promise.resolve();
const eventData = {
has_err: true,
start_offset: query.startDttm,
@ -295,22 +268,7 @@ export function queryFailed(query, msg, link, errors) {
});
});
return (
sync
.catch(() =>
dispatch(
addDangerToast(
t(
'An error occurred while storing the latest query id in the backend. ' +
'Please contact your administrator if this problem persists.',
),
),
),
)
// We should always show the error message, even if we couldn't sync the
// state to the backend
.then(() => dispatch({ type: QUERY_FAILED, query, msg, link, errors }))
);
dispatch({ type: QUERY_FAILED, query, msg, link, errors });
};
}
@ -557,14 +515,15 @@ export function addQueryEditor(queryEditor) {
? SupersetClient.post({
endpoint: '/tabstateview/',
postPayload: { queryEditor },
})
: Promise.resolve({ json: { id: shortid.generate() } });
}).then(({ json }) => ({ ...json, loaded: true }))
: Promise.resolve({ id: shortid.generate() });
return sync
.then(({ json }) => {
.then(({ id, loaded }) => {
const newQueryEditor = {
...queryEditor,
id: json.id.toString(),
id: id.toString(),
loaded,
};
return dispatch({
type: ADD_QUERY_EDITOR,
@ -736,11 +695,6 @@ export function switchQueryEditor(queryEditor, displayLimit) {
schema: json.schema,
queryLimit: json.query_limit,
remoteId: json.saved_query?.id,
validationResult: {
id: null,
errors: [],
completed: false,
},
hideLeftBar: json.hide_left_bar,
};
dispatch(loadQueryEditor(loadedQueryEditor));
@ -770,31 +724,10 @@ export function setActiveSouthPaneTab(tabId) {
export function toggleLeftBar(queryEditor) {
const hideLeftBar = !queryEditor.hideLeftBar;
return function (dispatch) {
const sync = isFeatureEnabled(FeatureFlag.SQLLAB_BACKEND_PERSISTENCE)
? SupersetClient.put({
endpoint: encodeURI(`/tabstateview/${queryEditor.id}`),
postPayload: { hide_left_bar: hideLeftBar },
})
: Promise.resolve();
return sync
.then(() =>
dispatch({
type: QUERY_EDITOR_TOGGLE_LEFT_BAR,
queryEditor,
hideLeftBar,
}),
)
.catch(() =>
dispatch(
addDangerToast(
t(
'An error occurred while hiding the left bar. Please contact your administrator.',
),
),
),
);
return {
type: QUERY_EDITOR_TOGGLE_LEFT_BAR,
queryEditor,
hideLeftBar,
};
}
@ -856,110 +789,26 @@ export function removeQuery(query) {
}
export function queryEditorSetDb(queryEditor, dbId) {
return function (dispatch) {
const sync = isFeatureEnabled(FeatureFlag.SQLLAB_BACKEND_PERSISTENCE)
? SupersetClient.put({
endpoint: encodeURI(`/tabstateview/${queryEditor.id}`),
postPayload: { database_id: dbId },
})
: Promise.resolve();
return sync
.then(() => dispatch({ type: QUERY_EDITOR_SETDB, queryEditor, dbId }))
.catch(() =>
dispatch(
addDangerToast(
t(
'An error occurred while setting the tab database ID. Please contact your administrator.',
),
),
),
);
};
return { type: QUERY_EDITOR_SETDB, queryEditor, dbId };
}
export function queryEditorSetSchema(queryEditor, schema) {
return function (dispatch) {
const sync =
isFeatureEnabled(FeatureFlag.SQLLAB_BACKEND_PERSISTENCE) &&
typeof queryEditor === 'object'
? SupersetClient.put({
endpoint: encodeURI(`/tabstateview/${queryEditor.id}`),
postPayload: { schema },
})
: Promise.resolve();
return sync
.then(() =>
dispatch({
type: QUERY_EDITOR_SET_SCHEMA,
queryEditor: queryEditor || {},
schema,
}),
)
.catch(() =>
dispatch(
addDangerToast(
t(
'An error occurred while setting the tab schema. Please contact your administrator.',
),
),
),
);
return {
type: QUERY_EDITOR_SET_SCHEMA,
queryEditor: queryEditor || {},
schema,
};
}
export function queryEditorSetAutorun(queryEditor, autorun) {
return function (dispatch) {
const sync = isFeatureEnabled(FeatureFlag.SQLLAB_BACKEND_PERSISTENCE)
? SupersetClient.put({
endpoint: encodeURI(`/tabstateview/${queryEditor.id}`),
postPayload: { autorun },
})
: Promise.resolve();
return sync
.then(() =>
dispatch({ type: QUERY_EDITOR_SET_AUTORUN, queryEditor, autorun }),
)
.catch(() =>
dispatch(
addDangerToast(
t(
'An error occurred while setting the tab autorun. Please contact your administrator.',
),
),
),
);
};
return { type: QUERY_EDITOR_SET_AUTORUN, queryEditor, autorun };
}
export function queryEditorSetTitle(queryEditor, name, id) {
return function (dispatch) {
const sync = isFeatureEnabled(FeatureFlag.SQLLAB_BACKEND_PERSISTENCE)
? SupersetClient.put({
endpoint: encodeURI(`/tabstateview/${id}`),
postPayload: { label: name },
})
: Promise.resolve();
return sync
.then(() =>
dispatch({
type: QUERY_EDITOR_SET_TITLE,
queryEditor: { ...queryEditor, id },
name,
}),
)
.catch(() =>
dispatch(
addDangerToast(
t(
'An error occurred while setting the tab name. Please contact your administrator.',
),
),
),
);
return {
type: QUERY_EDITOR_SET_TITLE,
queryEditor: { ...queryEditor, id },
name,
};
}
@ -1029,32 +878,19 @@ export function updateSavedQuery(query, clientId) {
.then(() => dispatch(updateQueryEditor(query)));
}
export function queryEditorSetSql(queryEditor, sql) {
return { type: QUERY_EDITOR_SET_SQL, queryEditor, sql };
export function queryEditorSetSql(queryEditor, sql, queryId) {
return { type: QUERY_EDITOR_SET_SQL, queryEditor, sql, queryId };
}
export function formatQuery(queryEditor) {
return function (dispatch, getState) {
const { sql } = getUpToDateQuery(getState(), queryEditor);
return SupersetClient.post({
endpoint: `/api/v1/sqllab/format_sql/`,
body: JSON.stringify({ sql }),
headers: { 'Content-Type': 'application/json' },
}).then(({ json }) => {
dispatch(queryEditorSetSql(queryEditor, json.result));
});
};
}
export function queryEditorSetAndSaveSql(targetQueryEditor, sql) {
export function queryEditorSetAndSaveSql(targetQueryEditor, sql, queryId) {
return function (dispatch, getState) {
const queryEditor = getUpToDateQuery(getState(), targetQueryEditor);
// saved query and set tab state use this action
dispatch(queryEditorSetSql(queryEditor, sql));
dispatch(queryEditorSetSql(queryEditor, sql, queryId));
if (isFeatureEnabled(FeatureFlag.SQLLAB_BACKEND_PERSISTENCE)) {
return SupersetClient.put({
endpoint: encodeURI(`/tabstateview/${queryEditor.id}`),
postPayload: { sql, latest_query_id: queryEditor.latestQueryId },
postPayload: { sql, latest_query_id: queryId },
}).catch(() =>
dispatch(
addDangerToast(
@ -1071,59 +907,32 @@ export function queryEditorSetAndSaveSql(targetQueryEditor, sql) {
};
}
export function queryEditorSetQueryLimit(queryEditor, queryLimit) {
return function (dispatch) {
const sync = isFeatureEnabled(FeatureFlag.SQLLAB_BACKEND_PERSISTENCE)
? SupersetClient.put({
endpoint: encodeURI(`/tabstateview/${queryEditor.id}`),
postPayload: { query_limit: queryLimit },
})
: Promise.resolve();
export function formatQuery(queryEditor) {
return function (dispatch, getState) {
const { sql } = getUpToDateQuery(getState(), queryEditor);
return SupersetClient.post({
endpoint: `/api/v1/sqllab/format_sql/`,
body: JSON.stringify({ sql }),
headers: { 'Content-Type': 'application/json' },
}).then(({ json }) => {
dispatch(queryEditorSetSql(queryEditor, json.result));
});
};
}
return sync
.then(() =>
dispatch({
type: QUERY_EDITOR_SET_QUERY_LIMIT,
queryEditor,
queryLimit,
}),
)
.catch(() =>
dispatch(
addDangerToast(
t(
'An error occurred while setting the tab name. Please contact your administrator.',
),
),
),
);
export function queryEditorSetQueryLimit(queryEditor, queryLimit) {
return {
type: QUERY_EDITOR_SET_QUERY_LIMIT,
queryEditor,
queryLimit,
};
}
export function queryEditorSetTemplateParams(queryEditor, templateParams) {
return function (dispatch) {
dispatch({
type: QUERY_EDITOR_SET_TEMPLATE_PARAMS,
queryEditor,
templateParams,
});
const sync = isFeatureEnabled(FeatureFlag.SQLLAB_BACKEND_PERSISTENCE)
? SupersetClient.put({
endpoint: encodeURI(`/tabstateview/${queryEditor.id}`),
postPayload: { template_params: templateParams },
})
: Promise.resolve();
return sync.catch(() =>
dispatch(
addDangerToast(
t(
'An error occurred while setting the tab template parameters. ' +
'Please contact your administrator.',
),
),
),
);
return {
type: QUERY_EDITOR_SET_TEMPLATE_PARAMS,
queryEditor,
templateParams,
};
}

View File

@ -32,7 +32,6 @@ import {
initialState,
queryId,
} from 'src/SqlLab/fixtures';
import { QueryState } from '@superset-ui/core';
const middlewares = [thunk];
const mockStore = configureMockStore(middlewares);
@ -531,88 +530,6 @@ describe('async actions', () => {
afterEach(fetchMock.resetHistory);
describe('querySuccess', () => {
it('updates the tab state in the backend', () => {
expect.assertions(2);
const store = mockStore({});
const results = { query: { sqlEditorId: 'abcd' } };
const expectedActions = [
{
type: actions.QUERY_SUCCESS,
query,
results,
},
];
return store.dispatch(actions.querySuccess(query, results)).then(() => {
expect(store.getActions()).toEqual(expectedActions);
expect(fetchMock.calls(updateTabStateEndpoint)).toHaveLength(1);
});
});
});
describe('fetchQueryResults', () => {
it('updates the tab state in the backend', () => {
expect.assertions(2);
const results = {
data: mockBigNumber,
query: { sqlEditorId: 'abcd' },
status: QueryState.SUCCESS,
query_id: 'efgh',
};
fetchMock.get(fetchQueryEndpoint, JSON.stringify(results), {
overwriteRoutes: true,
});
const store = mockStore({});
const expectedActions = [
{
type: actions.REQUEST_QUERY_RESULTS,
query,
},
// missing below
{
type: actions.QUERY_SUCCESS,
query,
results,
},
];
return store.dispatch(actions.fetchQueryResults(query)).then(() => {
expect(store.getActions()).toEqual(expectedActions);
expect(fetchMock.calls(updateTabStateEndpoint)).toHaveLength(1);
});
});
it("doesn't update the tab state in the backend on stoppped query", () => {
expect.assertions(2);
const results = {
status: QueryState.STOPPED,
query_id: 'efgh',
};
fetchMock.get(fetchQueryEndpoint, JSON.stringify(results), {
overwriteRoutes: true,
});
const store = mockStore({});
const expectedActions = [
{
type: actions.REQUEST_QUERY_RESULTS,
query,
},
// missing below
{
type: actions.QUERY_SUCCESS,
query,
results,
},
];
return store.dispatch(actions.fetchQueryResults(query)).then(() => {
expect(store.getActions()).toEqual(expectedActions);
expect(fetchMock.calls(updateTabStateEndpoint)).toHaveLength(0);
});
});
});
describe('addQueryEditor', () => {
it('updates the tab state in the backend', () => {
expect.assertions(2);
@ -621,7 +538,7 @@ describe('async actions', () => {
const expectedActions = [
{
type: actions.ADD_QUERY_EDITOR,
queryEditor: { ...queryEditor, id: '1' },
queryEditor: { ...queryEditor, id: '1', loaded: true },
},
];
return store.dispatch(actions.addQueryEditor(queryEditor)).then(() => {
@ -673,7 +590,7 @@ describe('async actions', () => {
describe('queryEditorSetDb', () => {
it('updates the tab state in the backend', () => {
expect.assertions(2);
expect.assertions(1);
const dbId = 42;
const store = mockStore({});
@ -684,18 +601,14 @@ describe('async actions', () => {
dbId,
},
];
return store
.dispatch(actions.queryEditorSetDb(queryEditor, dbId))
.then(() => {
expect(store.getActions()).toEqual(expectedActions);
expect(fetchMock.calls(updateTabStateEndpoint)).toHaveLength(1);
});
store.dispatch(actions.queryEditorSetDb(queryEditor, dbId));
expect(store.getActions()).toEqual(expectedActions);
});
});
describe('queryEditorSetSchema', () => {
it('updates the tab state in the backend', () => {
expect.assertions(2);
expect.assertions(1);
const schema = 'schema';
const store = mockStore({});
@ -706,18 +619,14 @@ describe('async actions', () => {
schema,
},
];
return store
.dispatch(actions.queryEditorSetSchema(queryEditor, schema))
.then(() => {
expect(store.getActions()).toEqual(expectedActions);
expect(fetchMock.calls(updateTabStateEndpoint)).toHaveLength(1);
});
store.dispatch(actions.queryEditorSetSchema(queryEditor, schema));
expect(store.getActions()).toEqual(expectedActions);
});
});
describe('queryEditorSetAutorun', () => {
it('updates the tab state in the backend', () => {
expect.assertions(2);
expect.assertions(1);
const autorun = true;
const store = mockStore({});
@ -728,18 +637,14 @@ describe('async actions', () => {
autorun,
},
];
return store
.dispatch(actions.queryEditorSetAutorun(queryEditor, autorun))
.then(() => {
expect(store.getActions()).toEqual(expectedActions);
expect(fetchMock.calls(updateTabStateEndpoint)).toHaveLength(1);
});
store.dispatch(actions.queryEditorSetAutorun(queryEditor, autorun));
expect(store.getActions()).toEqual(expectedActions);
});
});
describe('queryEditorSetTitle', () => {
it('updates the tab state in the backend', () => {
expect.assertions(2);
expect.assertions(1);
const name = 'name';
const store = mockStore({});
@ -750,14 +655,10 @@ describe('async actions', () => {
name,
},
];
return store
.dispatch(
actions.queryEditorSetTitle(queryEditor, name, queryEditor.id),
)
.then(() => {
expect(store.getActions()).toEqual(expectedActions);
expect(fetchMock.calls(updateTabStateEndpoint)).toHaveLength(1);
});
store.dispatch(
actions.queryEditorSetTitle(queryEditor, name, queryEditor.id),
);
expect(store.getActions()).toEqual(expectedActions);
});
});
@ -803,7 +704,7 @@ describe('async actions', () => {
describe('queryEditorSetQueryLimit', () => {
it('updates the tab state in the backend', () => {
expect.assertions(2);
expect.assertions(1);
const queryLimit = 10;
const store = mockStore({});
@ -814,18 +715,16 @@ describe('async actions', () => {
queryLimit,
},
];
return store
.dispatch(actions.queryEditorSetQueryLimit(queryEditor, queryLimit))
.then(() => {
expect(store.getActions()).toEqual(expectedActions);
expect(fetchMock.calls(updateTabStateEndpoint)).toHaveLength(1);
});
store.dispatch(
actions.queryEditorSetQueryLimit(queryEditor, queryLimit),
);
expect(store.getActions()).toEqual(expectedActions);
});
});
describe('queryEditorSetTemplateParams', () => {
it('updates the tab state in the backend', () => {
expect.assertions(2);
expect.assertions(1);
const templateParams = '{"foo": "bar"}';
const store = mockStore({});
@ -836,14 +735,11 @@ describe('async actions', () => {
templateParams,
},
];
return store
.dispatch(
actions.queryEditorSetTemplateParams(queryEditor, templateParams),
)
.then(() => {
expect(store.getActions()).toEqual(expectedActions);
expect(fetchMock.calls(updateTabStateEndpoint)).toHaveLength(1);
});
store.dispatch(
actions.queryEditorSetTemplateParams(queryEditor, templateParams),
);
expect(store.getActions()).toEqual(expectedActions);
});
});

View File

@ -0,0 +1,137 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import React from 'react';
import fetchMock from 'fetch-mock';
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 '.';
jest.mock('@superset-ui/core', () => ({
...jest.requireActual('@superset-ui/core'),
logging: {
warn: jest.fn(),
},
}));
const editorTabLastUpdatedAt = Date.now();
const unsavedSqlLabState = {
...initialState.sqlLab,
unsavedQueryEditor: {
id: defaultQueryEditor.id,
name: 'updated tab name',
updatedAt: editorTabLastUpdatedAt + 100,
},
editorTabLastUpdatedAt,
};
beforeAll(() => {
jest.useFakeTimers();
});
afterAll(() => {
jest.useRealTimers();
});
test('sync the unsaved editor tab state when there are new changes since the last update', async () => {
const updateEditorTabState = `glob:*/tabstateview/${defaultQueryEditor.id}`;
fetchMock.put(updateEditorTabState, 200);
expect(fetchMock.calls(updateEditorTabState)).toHaveLength(0);
render(<EditorAutoSync />, {
useRedux: true,
initialState: {
...initialState,
sqlLab: unsavedSqlLabState,
},
});
await waitFor(() => jest.runAllTimers());
expect(fetchMock.calls(updateEditorTabState)).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);
expect(fetchMock.calls(updateEditorTabState)).toHaveLength(0);
render(<EditorAutoSync />, {
useRedux: true,
initialState: {
...initialState,
sqlLab: {
...initialState.sqlLab,
unsavedQueryEditor: {
id: defaultQueryEditor.id,
name: 'updated tab name',
updatedAt: editorTabLastUpdatedAt - 100,
},
editorTabLastUpdatedAt,
},
},
});
await waitFor(() => jest.runAllTimers());
expect(fetchMock.calls(updateEditorTabState)).toHaveLength(0);
fetchMock.restore();
});
test('renders an error toast when the sync failed', async () => {
const updateEditorTabState = `glob:*/tabstateview/${defaultQueryEditor.id}`;
fetchMock.put(updateEditorTabState, {
throws: new Error('errorMessage'),
});
expect(fetchMock.calls(updateEditorTabState)).toHaveLength(0);
render(
<>
<EditorAutoSync />
<ToastContainer />
</>,
{
useRedux: true,
initialState: {
...initialState,
sqlLab: unsavedSqlLabState,
},
},
);
await waitFor(() => jest.runAllTimers());
expect(logging.warn).toHaveBeenCalledTimes(1);
expect(logging.warn).toHaveBeenCalledWith(
'An error occurred while saving your editor state.',
expect.anything(),
);
fetchMock.restore();
});

View File

@ -0,0 +1,106 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import React, { useRef, useEffect } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { logging } from '@superset-ui/core';
import {
SqlLabRootState,
QueryEditor,
UnsavedQueryEditor,
} from 'src/SqlLab/types';
import { useUpdateSqlEditorTabMutation } from 'src/hooks/apiResources/sqlEditorTabs';
import { useDebounceValue } from 'src/hooks/useDebounceValue';
import { setEditorTabLastUpdate } from 'src/SqlLab/actions/sqlLab';
const INTERVAL = 5000;
function hasUnsavedChanges(
queryEditor: QueryEditor,
lastSavedTimestamp: number,
) {
return (
queryEditor.inLocalStorage ||
(queryEditor.updatedAt && queryEditor.updatedAt > lastSavedTimestamp)
);
}
export function filterUnsavedQueryEditorList(
queryEditors: QueryEditor[],
unsavedQueryEditor: UnsavedQueryEditor,
lastSavedTimestamp: number,
) {
return queryEditors
.map(queryEditor => ({
...queryEditor,
...(unsavedQueryEditor.id === queryEditor.id && unsavedQueryEditor),
}))
.filter(queryEditor => hasUnsavedChanges(queryEditor, lastSavedTimestamp));
}
const EditorAutoSync: React.FC = () => {
const queryEditors = useSelector<SqlLabRootState, QueryEditor[]>(
state => state.sqlLab.queryEditors,
);
const unsavedQueryEditor = useSelector<SqlLabRootState, UnsavedQueryEditor>(
state => state.sqlLab.unsavedQueryEditor,
);
const editorTabLastUpdatedAt = useSelector<SqlLabRootState, number>(
state => state.sqlLab.editorTabLastUpdatedAt,
);
const dispatch = useDispatch();
const lastSavedTimestampRef = useRef<number>(editorTabLastUpdatedAt);
const [updateSqlEditor, { error }] = useUpdateSqlEditorTabMutation();
const debouncedUnsavedQueryEditor = useDebounceValue(
unsavedQueryEditor,
INTERVAL,
);
useEffect(() => {
const unsaved = filterUnsavedQueryEditorList(
queryEditors,
debouncedUnsavedQueryEditor,
lastSavedTimestampRef.current,
);
Promise.all(
unsaved
// TODO: Migrate migrateQueryEditorFromLocalStorage
// in TabbedSqlEditors logic by addSqlEditor mutation later
.filter(({ inLocalStorage }) => !inLocalStorage)
.map(queryEditor => updateSqlEditor({ queryEditor })),
).then(resolvers => {
if (!resolvers.some(result => 'error' in result)) {
lastSavedTimestampRef.current = Date.now();
dispatch(setEditorTabLastUpdate(lastSavedTimestampRef.current));
}
});
}, [debouncedUnsavedQueryEditor, dispatch, queryEditors, updateSqlEditor]);
useEffect(() => {
if (error) {
logging.warn('An error occurred while saving your editor state.', error);
}
}, [dispatch, error]);
return null;
};
export default EditorAutoSync;

View File

@ -25,7 +25,7 @@ import { t, useTheme, QueryResponse } from '@superset-ui/core';
import { useDispatch, useSelector } from 'react-redux';
import {
queryEditorSetAndSaveSql,
queryEditorSetSql,
cloneQueryToNewTab,
fetchQueryResults,
clearQueryResults,
@ -109,7 +109,9 @@ const QueryTable = ({
const data = useMemo(() => {
const restoreSql = (query: QueryResponse) => {
dispatch(queryEditorSetAndSaveSql({ id: query.sqlEditorId }, query.sql));
dispatch(
queryEditorSetSql({ id: query.sqlEditorId }, query.sql, query.id),
);
};
const openQueryInNewTab = (query: QueryResponse) => {

View File

@ -557,10 +557,9 @@ const SqlEditor: React.FC<Props> = ({
[setQueryEditorAndSaveSql],
);
const onSqlChanged = (sql: string) => {
const onSqlChanged = useEffectEvent((sql: string) => {
dispatch(queryEditorSetSql(queryEditor, sql));
setQueryEditorAndSaveSqlWithDebounce(sql);
};
});
// Return the heights for the ace editor and the south pane as an object
// given the height of the sql editor, north pane percent and south pane percent.
@ -785,7 +784,7 @@ const SqlEditor: React.FC<Props> = ({
)}
<AceEditorWrapper
autocomplete={autocompleteEnabled}
onBlur={setQueryEditorAndSaveSql}
onBlur={onSqlChanged}
onChange={onSqlChanged}
queryEditorId={queryEditor.id}
height={`${aceEditorHeight}px`}

View File

@ -25,6 +25,7 @@ import {
QueryResponse,
QueryState,
} from '@superset-ui/core';
import { LatestQueryEditorVersion } from 'src/SqlLab/types';
import { ISaveableDatasource } from 'src/SqlLab/components/SaveDatasetModal';
export const mockedActions = sinon.stub({ ...actions });
@ -181,6 +182,7 @@ export const table = {
};
export const defaultQueryEditor = {
version: LatestQueryEditorVersion,
id: 'dfsadfs',
autorun: false,
dbId: undefined,

View File

@ -18,6 +18,9 @@
*/
// TODO: requires redux-localstorage > 1.0 for typescript support
import persistState from 'redux-localstorage';
import { pickBy } from 'lodash';
import { isFeatureEnabled, FeatureFlag } from '@superset-ui/core';
import { filterUnsavedQueryEditorList } from 'src/SqlLab/components/EditorAutoSync';
import {
emptyTablePersistData,
emptyQueryResults,
@ -38,6 +41,39 @@ const sqlLabPersistStateConfig = {
slicer: paths => state => {
const subset = {};
paths.forEach(path => {
if (isFeatureEnabled(FeatureFlag.SQLLAB_BACKEND_PERSISTENCE)) {
const {
queryEditors,
editorTabLastUpdatedAt,
unsavedQueryEditor,
tables,
queries,
tabHistory,
} = state.sqlLab;
const unsavedQueryEditors = filterUnsavedQueryEditorList(
queryEditors,
unsavedQueryEditor,
editorTabLastUpdatedAt,
);
if (unsavedQueryEditors.length > 0) {
const hasFinishedMigrationFromLocalStorage =
unsavedQueryEditors.every(
({ inLocalStorage }) => !inLocalStorage,
);
subset.sqlLab = {
queryEditors: unsavedQueryEditors,
...(!hasFinishedMigrationFromLocalStorage && {
tabHistory,
tables: tables.filter(table => table.inLocalStorage),
queries: pickBy(
queries,
query => query.inLocalStorage && !query.isDataPreview,
),
}),
};
}
return;
}
// this line is used to remove old data from browser localStorage.
// we used to persist all redux state into localStorage, but
// it caused configurations passed from server-side got override.

View File

@ -54,6 +54,10 @@ const apiDataWithTabState = {
},
};
describe('getInitialState', () => {
afterEach(() => {
localStorage.clear();
});
it('should output the user that is passed in', () => {
expect(getInitialState(apiData).user?.userId).toEqual(1);
});
@ -134,10 +138,6 @@ describe('getInitialState', () => {
});
describe('dedupe tables schema', () => {
afterEach(() => {
localStorage.clear();
});
it('should dedupe the table schema', () => {
localStorage.setItem(
'redux',
@ -245,4 +245,109 @@ describe('getInitialState', () => {
);
});
});
describe('restore unsaved changes for PERSISTENCE mode', () => {
const lastUpdatedTime = Date.now();
const expectedValue = 'updated editor value';
beforeEach(() => {
localStorage.setItem(
'redux',
JSON.stringify({
sqlLab: {
queryEditors: [
{
// restore cached value since updates are after server update time
id: '1',
name: expectedValue,
updatedAt: lastUpdatedTime + 100,
},
{
// no update required given that last updated time comes before server update time
id: '2',
name: expectedValue,
updatedAt: lastUpdatedTime - 100,
},
{
// no update required given that there's no updatedAt
id: '3',
name: expectedValue,
},
],
},
}),
);
});
it('restore unsaved changes for PERSISTENCE mode', () => {
const apiDataWithLocalStorage = {
...apiData,
active_tab: {
...apiDataWithTabState.active_tab,
id: 1,
label: 'persisted tab',
table_schemas: [],
extra_json: {
updatedAt: lastUpdatedTime,
},
},
tab_state_ids: [{ id: 1, label: '' }],
};
expect(
getInitialState(apiDataWithLocalStorage).sqlLab.queryEditors[0],
).toEqual(
expect.objectContaining({
id: '1',
name: expectedValue,
}),
);
});
it('skip unsaved changes for expired data', () => {
const apiDataWithLocalStorage = {
...apiData,
active_tab: {
...apiDataWithTabState.active_tab,
id: 2,
label: 'persisted tab',
table_schemas: [],
extra_json: {
updatedAt: lastUpdatedTime,
},
},
tab_state_ids: [{ id: 2, label: '' }],
};
expect(
getInitialState(apiDataWithLocalStorage).sqlLab.queryEditors[1],
).toEqual(
expect.objectContaining({
id: '2',
name: apiDataWithLocalStorage.active_tab.label,
}),
);
});
it('skip unsaved changes for legacy cache data', () => {
const apiDataWithLocalStorage = {
...apiData,
active_tab: {
...apiDataWithTabState.active_tab,
id: 3,
label: 'persisted tab',
table_schemas: [],
extra_json: {
updatedAt: lastUpdatedTime,
},
},
tab_state_ids: [{ id: 3, label: '' }],
};
expect(
getInitialState(apiDataWithLocalStorage).sqlLab.queryEditors[2],
).toEqual(
expect.objectContaining({
id: '3',
name: apiDataWithLocalStorage.active_tab.label,
}),
);
});
});
});

View File

@ -20,11 +20,13 @@ import { t } from '@superset-ui/core';
import getToastsFromPyFlashMessages from 'src/components/MessageToasts/getToastsFromPyFlashMessages';
import type { BootstrapData } from 'src/types/bootstrapTypes';
import type { InitialState } from 'src/hooks/apiResources/sqlLab';
import type {
import {
QueryEditor,
UnsavedQueryEditor,
SqlLabRootState,
Table,
LatestQueryEditorVersion,
QueryEditorVersion,
} from 'src/SqlLab/types';
export function dedupeTabHistory(tabHistory: string[]) {
@ -53,6 +55,7 @@ export default function getInitialState({
*/
let queryEditors: Record<string, QueryEditor> = {};
const defaultQueryEditor = {
version: LatestQueryEditorVersion,
loaded: true,
name: t('Untitled query'),
sql: 'SELECT *\nFROM\nWHERE',
@ -73,6 +76,7 @@ export default function getInitialState({
let queryEditor: QueryEditor;
if (activeTab && activeTab.id === id) {
queryEditor = {
version: activeTab.extra_json?.version ?? QueryEditorVersion.v1,
id: id.toString(),
loaded: true,
name: activeTab.label,
@ -88,6 +92,7 @@ export default function getInitialState({
schema: activeTab.schema,
queryLimit: activeTab.query_limit,
hideLeftBar: activeTab.hide_left_bar,
updatedAt: activeTab.extra_json?.updatedAt,
};
} else {
// dummy state, actual state will be loaded on tab switch
@ -103,11 +108,12 @@ export default function getInitialState({
[queryEditor.id]: queryEditor,
};
});
const tabHistory = activeTab ? [activeTab.id.toString()] : [];
let tables = {} as Record<string, Table>;
const editorTabLastUpdatedAt = Date.now();
let editorTabLastUpdatedAt = Date.now();
if (activeTab) {
editorTabLastUpdatedAt =
activeTab.extra_json?.updatedAt || editorTabLastUpdatedAt;
activeTab.table_schemas
.filter(tableSchema => tableSchema.description !== null)
.forEach(tableSchema => {
@ -153,37 +159,57 @@ export default function getInitialState({
// add query editors and tables to state with a special flag so they can
// be migrated if the `SQLLAB_BACKEND_PERSISTENCE` feature flag is on
sqlLab.queryEditors.forEach(qe => {
const hasConflictFromBackend = Boolean(queryEditors[qe.id]);
const unsavedUpdatedAt = queryEditors[qe.id]?.updatedAt;
const hasUnsavedUpdateSinceLastSave =
qe.updatedAt &&
(!unsavedUpdatedAt || qe.updatedAt > unsavedUpdatedAt);
const cachedQueryEditor: UnsavedQueryEditor =
!hasConflictFromBackend || hasUnsavedUpdateSinceLastSave ? qe : {};
queryEditors = {
...queryEditors,
[qe.id]: {
...queryEditors[qe.id],
...qe,
name: qe.title || qe.name,
...(unsavedQueryEditor.id === qe.id && unsavedQueryEditor),
inLocalStorage: true,
...cachedQueryEditor,
name:
cachedQueryEditor.title ||
cachedQueryEditor.name ||
queryEditors[qe.id]?.name,
...(cachedQueryEditor.id &&
unsavedQueryEditor.id === qe.id &&
unsavedQueryEditor),
inLocalStorage: !hasConflictFromBackend,
loaded: true,
},
};
});
const expandedTables = new Set();
tables = sqlLab.tables.reduce((merged, table) => {
const expanded = !expandedTables.has(table.queryEditorId);
if (expanded) {
expandedTables.add(table.queryEditorId);
}
return {
...merged,
[table.id]: {
...tables[table.id],
...table,
expanded,
},
};
}, tables);
Object.values(sqlLab.queries).forEach(query => {
queries[query.id] = { ...query, inLocalStorage: true };
});
tabHistory.push(...sqlLab.tabHistory);
if (sqlLab.tables) {
tables = sqlLab.tables.reduce((merged, table) => {
const expanded = !expandedTables.has(table.queryEditorId);
if (expanded) {
expandedTables.add(table.queryEditorId);
}
return {
...merged,
[table.id]: {
...tables[table.id],
...table,
expanded,
inLocalStorage: true,
},
};
}, tables);
}
if (sqlLab.queries) {
Object.values(sqlLab.queries).forEach(query => {
queries[query.id] = { ...query, inLocalStorage: true };
});
}
if (sqlLab.tabHistory) {
tabHistory.push(...sqlLab.tabHistory);
}
}
}
} catch (error) {

View File

@ -29,7 +29,7 @@ import {
extendArr,
} from '../../reduxUtils';
function alterUnsavedQueryEditorState(state, updatedState, id) {
function alterUnsavedQueryEditorState(state, updatedState, id, silent = false) {
if (state.tabHistory[state.tabHistory.length - 1] !== id) {
const { queryEditors } = alterInArr(
state,
@ -45,6 +45,7 @@ function alterUnsavedQueryEditorState(state, updatedState, id) {
unsavedQueryEditor: {
...(state.unsavedQueryEditor.id === id && state.unsavedQueryEditor),
...(id ? { id, ...updatedState } : state.unsavedQueryEditor),
...(!silent && { updatedAt: new Date().getTime() }),
},
};
}
@ -64,7 +65,10 @@ export default function sqlLabReducer(state = {}, action) {
...mergeUnsavedState,
tabHistory: [...state.tabHistory, action.queryEditor.id],
};
return addToArr(newState, 'queryEditors', action.queryEditor);
return addToArr(newState, 'queryEditors', {
...action.queryEditor,
updatedAt: new Date().getTime(),
});
},
[actions.QUERY_EDITOR_SAVED]() {
const { query, result, clientId } = action;
@ -308,6 +312,7 @@ export default function sqlLabReducer(state = {}, action) {
latestQueryId: action.query.id,
},
action.query.sqlEditorId,
action.query.isDataPreview,
),
};
},
@ -378,14 +383,12 @@ export default function sqlLabReducer(state = {}, action) {
qeIds.indexOf(action.queryEditor?.id) > -1 &&
state.tabHistory[state.tabHistory.length - 1] !== action.queryEditor.id
) {
const mergeUnsavedState = alterInArr(
state,
'queryEditors',
state.unsavedQueryEditor,
{
const mergeUnsavedState = {
...alterInArr(state, 'queryEditors', state.unsavedQueryEditor, {
...state.unsavedQueryEditor,
},
);
}),
unsavedQueryEditor: {},
};
return {
...(action.queryEditor.id === state.unsavedQueryEditor.id
? alterInArr(
@ -522,12 +525,20 @@ export default function sqlLabReducer(state = {}, action) {
};
},
[actions.QUERY_EDITOR_SET_SQL]() {
const { unsavedQueryEditor } = state;
if (
unsavedQueryEditor?.id === action.queryEditor.id &&
unsavedQueryEditor.sql === action.sql
) {
return state;
}
return {
...state,
...alterUnsavedQueryEditorState(
state,
{
sql: action.sql,
...(action.queryId && { latestQueryId: action.queryId }),
},
action.queryEditor.id,
),
@ -566,6 +577,7 @@ export default function sqlLabReducer(state = {}, action) {
selectedText: action.sql,
},
action.queryEditor.id,
true,
),
};
},
@ -708,6 +720,9 @@ export default function sqlLabReducer(state = {}, action) {
[actions.CREATE_DATASOURCE_FAILED]() {
return { ...state, isDatasourceLoading: false, errorMessage: action.err };
},
[actions.SET_EDITOR_TAB_LAST_UPDATE]() {
return { ...state, editorTabLastUpdatedAt: action.timestamp };
},
};
if (action.type in actionHandlers) {
return actionHandlers[action.type]();

View File

@ -29,7 +29,14 @@ export type QueryDictionary = {
[id: string]: QueryResponse;
};
export enum QueryEditorVersion {
v1 = 1,
}
export const LatestQueryEditorVersion = QueryEditorVersion.v1;
export interface QueryEditor {
version: QueryEditorVersion;
id: string;
dbId?: number;
name: string;
@ -48,6 +55,7 @@ export interface QueryEditor {
inLocalStorage?: boolean;
northPercent?: number;
southPercent?: number;
updatedAt?: number;
}
export type toastState = {
@ -86,7 +94,7 @@ export type SqlLabRootState = {
errorMessage: string | null;
unsavedQueryEditor: UnsavedQueryEditor;
queryCostEstimates?: Record<string, QueryCostEstimate>;
editorTabLastUpdatedAt?: number;
editorTabLastUpdatedAt: number;
};
localStorageUsageInKilobytes: number;
messageToasts: toastState[];

View File

@ -83,10 +83,11 @@ describe('reduxStateToLocalStorageHelper', () => {
});
it('should only return selected keys for query editor', () => {
const queryEditors = [defaultQueryEditor];
expect(Object.keys(queryEditors[0])).toContain('schema');
const queryEditors = [{ ...defaultQueryEditor, dummy: 'value' }];
expect(Object.keys(queryEditors[0])).toContain('dummy');
const clearedQueryEditors = clearQueryEditors(queryEditors);
expect(Object.keys(clearedQueryEditors)[0]).not.toContain('schema');
expect(Object.keys(clearedQueryEditors[0])).toContain('version');
expect(Object.keys(clearedQueryEditors[0])).not.toContain('dummy');
});
});

View File

@ -26,6 +26,7 @@ import {
} from '../constants';
const PERSISTENT_QUERY_EDITOR_KEYS = new Set([
'version',
'remoteId',
'autorun',
'dbId',

View File

@ -0,0 +1,99 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import fetchMock from 'fetch-mock';
import { act, renderHook } from '@testing-library/react-hooks';
import {
createWrapper,
defaultStore as store,
} from 'spec/helpers/testing-library';
import { api } from 'src/hooks/apiResources/queryApi';
import { LatestQueryEditorVersion } from 'src/SqlLab/types';
import { useUpdateSqlEditorTabMutation } from './sqlEditorTabs';
const expectedQueryEditor = {
version: LatestQueryEditorVersion,
id: '123',
dbId: 456,
name: 'tab 1',
sql: 'SELECT * from example_table',
schema: 'my_schema',
templateParams: '{"a": 1, "v": "str"}',
queryLimit: 1000,
remoteId: null,
autorun: false,
hideLeftBar: false,
updatedAt: Date.now(),
};
afterEach(() => {
fetchMock.reset();
act(() => {
store.dispatch(api.util.resetApiState());
});
});
test('puts api request with formData', async () => {
const tabStateMutationApiRoute = `glob:*/tabstateview/${expectedQueryEditor.id}`;
fetchMock.put(tabStateMutationApiRoute, 200);
const { result, waitFor } = renderHook(
() => useUpdateSqlEditorTabMutation(),
{
wrapper: createWrapper({
useRedux: true,
store,
}),
},
);
act(() => {
result.current[0]({
queryEditor: expectedQueryEditor,
});
});
await waitFor(() =>
expect(fetchMock.calls(tabStateMutationApiRoute).length).toBe(1),
);
const formData = fetchMock.calls(tabStateMutationApiRoute)[0][1]
?.body as FormData;
expect(formData.get('database_id')).toBe(`${expectedQueryEditor.dbId}`);
expect(formData.get('schema')).toBe(
JSON.stringify(`${expectedQueryEditor.schema}`),
);
expect(formData.get('sql')).toBe(
JSON.stringify(`${expectedQueryEditor.sql}`),
);
expect(formData.get('label')).toBe(
JSON.stringify(`${expectedQueryEditor.name}`),
);
expect(formData.get('query_limit')).toBe(`${expectedQueryEditor.queryLimit}`);
expect(formData.has('latest_query_id')).toBe(false);
expect(formData.get('template_params')).toBe(
JSON.stringify(`${expectedQueryEditor.templateParams}`),
);
expect(formData.get('hide_left_bar')).toBe(
`${expectedQueryEditor.hideLeftBar}`,
);
expect(formData.get('extra_json')).toBe(
JSON.stringify(
JSON.stringify({
updatedAt: expectedQueryEditor.updatedAt,
version: LatestQueryEditorVersion,
}),
),
);
});

View File

@ -0,0 +1,70 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { pickBy } from 'lodash';
import { QueryEditor, LatestQueryEditorVersion } from 'src/SqlLab/types';
import { api, JsonResponse } from './queryApi';
export type EditorMutationParams = {
queryEditor: QueryEditor;
extra?: Record<string, any>;
};
const sqlEditorApi = api.injectEndpoints({
endpoints: builder => ({
updateSqlEditorTab: builder.mutation<JsonResponse, EditorMutationParams>({
query: ({
queryEditor: {
version = LatestQueryEditorVersion,
id,
dbId,
schema,
queryLimit,
sql,
name,
latestQueryId,
hideLeftBar,
templateParams,
autorun,
updatedAt,
},
extra,
}) => ({
method: 'PUT',
endpoint: encodeURI(`/tabstateview/${id}`),
postPayload: pickBy(
{
database_id: dbId,
schema,
sql,
label: name,
query_limit: queryLimit,
latest_query_id: latestQueryId,
template_params: templateParams,
hide_left_bar: hideLeftBar,
autorun,
extra_json: JSON.stringify({ updatedAt, version, ...extra }),
},
value => value !== undefined,
),
}),
}),
}),
});
export const { useUpdateSqlEditorTabMutation } = sqlEditorApi;

View File

@ -50,7 +50,7 @@ export type InitialState = {
template_params: string | null;
hide_left_bar?: boolean;
saved_query: { id: number } | null;
extra_json?: object;
extra_json?: Record<string, any>;
};
databases: object[];
queries: Record<

View File

@ -19,8 +19,8 @@
import { useState, useEffect } from 'react';
import { FAST_DEBOUNCE } from 'src/constants';
export function useDebounceValue(value: string, delay = FAST_DEBOUNCE) {
const [debouncedValue, setDebouncedValue] = useState(value);
export function useDebounceValue<T>(value: T, delay = FAST_DEBOUNCE) {
const [debouncedValue, setDebouncedValue] = useState<T>(value);
useEffect(() => {
const handler: NodeJS.Timeout = setTimeout(() => {

View File

@ -18,7 +18,7 @@
*/
import React, { useEffect } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { css } from '@superset-ui/core';
import { css, isFeatureEnabled, FeatureFlag } from '@superset-ui/core';
import { useSqlLabInitialState } from 'src/hooks/apiResources/sqlLab';
import type { InitialState } from 'src/hooks/apiResources/sqlLab';
import { resetState } from 'src/SqlLab/actions/sqlLab';
@ -27,16 +27,17 @@ import type { SqlLabRootState } from 'src/SqlLab/types';
import { SqlLabGlobalStyles } from 'src/SqlLab//SqlLabGlobalStyles';
import App from 'src/SqlLab/components/App';
import Loading from 'src/components/Loading';
import EditorAutoSync from 'src/SqlLab/components/EditorAutoSync';
import useEffectEvent from 'src/hooks/useEffectEvent';
import { LocationProvider } from './LocationContext';
export default function SqlLab() {
const editorTabLastUpdatedAt = useSelector<SqlLabRootState, number>(
state => state.sqlLab.editorTabLastUpdatedAt || 0,
const lastInitializedAt = useSelector<SqlLabRootState, number>(
state => state.sqlLab.queriesLastUpdate || 0,
);
const { data, isLoading, isError, error, fulfilledTimeStamp } =
useSqlLabInitialState();
const shouldInitialize = editorTabLastUpdatedAt <= (fulfilledTimeStamp || 0);
const shouldInitialize = lastInitializedAt <= (fulfilledTimeStamp || 0);
const dispatch = useDispatch();
const initBootstrapData = useEffectEvent(
@ -72,6 +73,9 @@ export default function SqlLab() {
>
<SqlLabGlobalStyles />
<App />
{isFeatureEnabled(FeatureFlag.SQLLAB_BACKEND_PERSISTENCE) && (
<EditorAutoSync />
)}
</div>
</LocationProvider>
);

View File

@ -38,7 +38,6 @@ import logger from 'src/middleware/loggerMiddleware';
import saveModal from 'src/explore/reducers/saveModalReducer';
import explore from 'src/explore/reducers/exploreReducer';
import exploreDatasources from 'src/explore/reducers/datasourcesReducer';
import { FeatureFlag, isFeatureEnabled } from '@superset-ui/core';
import { persistSqlLabStateEnhancer } from 'src/SqlLab/middlewares/persistSqlLabStateEnhancer';
import sqlLabReducer from 'src/SqlLab/reducers/sqlLab';
@ -167,9 +166,7 @@ export function setupStore({
},
middleware: getMiddleware,
devTools: process.env.WEBPACK_MODE === 'development' && !disableDebugger,
...(!isFeatureEnabled(FeatureFlag.SQLLAB_BACKEND_PERSISTENCE) && {
enhancers: [persistSqlLabStateEnhancer as StoreEnhancer],
}),
enhancers: [persistSqlLabStateEnhancer as StoreEnhancer],
...overrides,
});
}