refactor(sql_lab): SQL Lab Persistent Saved State (#17771)

* a lot of console logs

* testing

* test

* added saved_query to remoteId

* created useEffect so that title properly changes in modal

* Update superset-frontend/src/SqlLab/actions/sqlLab.js

Co-authored-by: Lyndsi Kay Williams <55605634+lyndsiWilliams@users.noreply.github.com>

Co-authored-by: Lyndsi Kay Williams <55605634+lyndsiWilliams@users.noreply.github.com>
This commit is contained in:
AAfghahi 2022-01-18 13:10:31 -05:00 committed by GitHub
parent 5bfe2d47b0
commit 88db2cc0ab
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 52 additions and 8 deletions

View File

@ -636,12 +636,13 @@ export function switchQueryEditor(queryEditor, displayLimit) {
title: json.label, title: json.label,
sql: json.sql, sql: json.sql,
selectedText: null, selectedText: null,
latestQueryId: json.latest_query ? json.latest_query.id : null, latestQueryId: json.latest_query?.id,
autorun: json.autorun, autorun: json.autorun,
dbId: json.database_id, dbId: json.database_id,
templateParams: json.template_params, templateParams: json.template_params,
schema: json.schema, schema: json.schema,
queryLimit: json.query_limit, queryLimit: json.query_limit,
remoteId: json.saved_query?.id,
validationResult: { validationResult: {
id: null, id: null,
errors: [], errors: [],
@ -864,19 +865,38 @@ export function saveQuery(query) {
stringify: false, stringify: false,
}) })
.then(result => { .then(result => {
const savedQuery = convertQueryToClient(result.json.item);
dispatch({ dispatch({
type: QUERY_EDITOR_SAVED, type: QUERY_EDITOR_SAVED,
query, query,
result: convertQueryToClient(result.json.item), result: savedQuery,
}); });
dispatch(addSuccessToast(t('Your query was saved')));
dispatch(queryEditorSetTitle(query, query.title)); dispatch(queryEditorSetTitle(query, query.title));
return savedQuery;
}) })
.catch(() => .catch(() =>
dispatch(addDangerToast(t('Your query could not be saved'))), dispatch(addDangerToast(t('Your query could not be saved'))),
); );
} }
export const addSavedQueryToTabState =
(queryEditor, savedQuery) => dispatch => {
const sync = isFeatureEnabled(FeatureFlag.SQLLAB_BACKEND_PERSISTENCE)
? SupersetClient.put({
endpoint: `/tabstateview/${queryEditor.id}`,
postPayload: { saved_query_id: savedQuery.remoteId },
})
: Promise.resolve();
return sync
.catch(() => {
dispatch(addDangerToast(t('Your query was not properly saved')));
})
.then(() => {
dispatch(addSuccessToast(t('Your query was saved')));
});
};
export function updateSavedQuery(query) { export function updateSavedQuery(query) {
return dispatch => return dispatch =>
SupersetClient.put({ SupersetClient.put({

View File

@ -23,7 +23,6 @@ import configureMockStore from 'redux-mock-store';
import thunk from 'redux-thunk'; import thunk from 'redux-thunk';
import shortid from 'shortid'; import shortid from 'shortid';
import * as featureFlags from 'src/featureFlags'; import * as featureFlags from 'src/featureFlags';
import { ADD_TOAST } from 'src/components/MessageToasts/actions';
import * as actions from 'src/SqlLab/actions/sqlLab'; import * as actions from 'src/SqlLab/actions/sqlLab';
import { defaultQueryEditor, query } from '../fixtures'; import { defaultQueryEditor, query } from '../fixtures';
@ -93,7 +92,7 @@ describe('async actions', () => {
expect.assertions(1); expect.assertions(1);
return makeRequest().then(() => { return makeRequest().then(() => {
expect(dispatch.callCount).toBe(3); expect(dispatch.callCount).toBe(2);
}); });
}); });
@ -111,7 +110,6 @@ describe('async actions', () => {
const store = mockStore({}); const store = mockStore({});
const expectedActionTypes = [ const expectedActionTypes = [
actions.QUERY_EDITOR_SAVED, actions.QUERY_EDITOR_SAVED,
ADD_TOAST,
actions.QUERY_EDITOR_SET_TITLE, actions.QUERY_EDITOR_SET_TITLE,
]; ];
return store.dispatch(actions.saveQuery(query)).then(() => { return store.dispatch(actions.saveQuery(query)).then(() => {

View File

@ -16,7 +16,7 @@
* specific language governing permissions and limitations * specific language governing permissions and limitations
* under the License. * under the License.
*/ */
import React, { useState } from 'react'; import React, { useState, useEffect } from 'react';
import { Row, Col, Input, TextArea } from 'src/common/components'; import { Row, Col, Input, TextArea } from 'src/common/components';
import { t, styled } from '@superset-ui/core'; import { t, styled } from '@superset-ui/core';
import Button from 'src/components/Button'; import Button from 'src/components/Button';
@ -90,6 +90,11 @@ export default function SaveQuery({
description, description,
}); });
useEffect(() => {
if (!isSaved) {
setLabel(defaultLabel);
}
}, [defaultLabel]);
const close = () => { const close = () => {
setShowSave(false); setShowSave(false);
}; };

View File

@ -52,6 +52,7 @@ import {
queryEditorSetTemplateParams, queryEditorSetTemplateParams,
runQuery, runQuery,
saveQuery, saveQuery,
addSavedQueryToTabState,
scheduleQuery, scheduleQuery,
setActiveSouthPaneTab, setActiveSouthPaneTab,
updateSavedQuery, updateSavedQuery,
@ -192,6 +193,7 @@ class SqlEditor extends React.PureComponent {
this.canValidateQuery = this.canValidateQuery.bind(this); this.canValidateQuery = this.canValidateQuery.bind(this);
this.runQuery = this.runQuery.bind(this); this.runQuery = this.runQuery.bind(this);
this.stopQuery = this.stopQuery.bind(this); this.stopQuery = this.stopQuery.bind(this);
this.saveQuery = this.saveQuery.bind(this);
this.onSqlChanged = this.onSqlChanged.bind(this); this.onSqlChanged = this.onSqlChanged.bind(this);
this.setQueryEditorSql = this.setQueryEditorSql.bind(this); this.setQueryEditorSql = this.setQueryEditorSql.bind(this);
this.setQueryEditorSqlWithDebounce = debounce( this.setQueryEditorSqlWithDebounce = debounce(
@ -592,6 +594,12 @@ class SqlEditor extends React.PureComponent {
); );
} }
async saveQuery(query) {
const { queryEditor: qe, actions } = this.props;
const savedQuery = await actions.saveQuery(query);
actions.addSavedQueryToTabState(qe, savedQuery);
}
renderEditorBottomBar() { renderEditorBottomBar() {
const { queryEditor: qe } = this.props; const { queryEditor: qe } = this.props;
@ -630,6 +638,7 @@ class SqlEditor extends React.PureComponent {
)} )}
</Menu> </Menu>
); );
return ( return (
<StyledToolbar className="sql-toolbar" id="js-sql-toolbar"> <StyledToolbar className="sql-toolbar" id="js-sql-toolbar">
<div className="leftItems"> <div className="leftItems">
@ -693,7 +702,7 @@ class SqlEditor extends React.PureComponent {
<SaveQuery <SaveQuery
query={qe} query={qe}
defaultLabel={qe.title || qe.description} defaultLabel={qe.title || qe.description}
onSave={this.props.actions.saveQuery} onSave={this.saveQuery}
onUpdate={this.props.actions.updateSavedQuery} onUpdate={this.props.actions.updateSavedQuery}
saveQueryWarning={this.props.saveQueryWarning} saveQueryWarning={this.props.saveQueryWarning}
/> />
@ -809,6 +818,7 @@ function mapDispatchToProps(dispatch) {
queryEditorSetTemplateParams, queryEditorSetTemplateParams,
runQuery, runQuery,
saveQuery, saveQuery,
addSavedQueryToTabState,
scheduleQuery, scheduleQuery,
setActiveSouthPaneTab, setActiveSouthPaneTab,
updateSavedQuery, updateSavedQuery,

View File

@ -79,6 +79,7 @@ export default function getInitialState({
latestQueryId: activeTab.latest_query latestQueryId: activeTab.latest_query
? activeTab.latest_query.id ? activeTab.latest_query.id
: null, : null,
remoteId: activeTab.saved_query?.id,
autorun: activeTab.autorun, autorun: activeTab.autorun,
templateParams: activeTab.template_params || undefined, templateParams: activeTab.template_params || undefined,
dbId: activeTab.database_id, dbId: activeTab.database_id,

View File

@ -211,6 +211,11 @@ class SavedQuery(Model, AuditMixinNullable, ExtraJSONMixin, ImportExportMixin):
def __repr__(self) -> str: def __repr__(self) -> str:
return str(self.label) return str(self.label)
def to_dict(self) -> Dict[str, Any]:
return {
"id": self.id,
}
@property @property
def pop_tab_link(self) -> Markup: def pop_tab_link(self) -> Markup:
return Markup( return Markup(
@ -285,6 +290,10 @@ class TabState(Model, AuditMixinNullable, ExtraJSONMixin):
template_params = Column(Text) template_params = Column(Text)
hide_left_bar = Column(Boolean, default=False) hide_left_bar = Column(Boolean, default=False)
# any saved queries that are associated with the Tab State
saved_query_id = Column(Integer, ForeignKey("saved_query.id"), nullable=True)
saved_query = relationship("SavedQuery", foreign_keys=[saved_query_id])
def to_dict(self) -> Dict[str, Any]: def to_dict(self) -> Dict[str, Any]:
return { return {
"id": self.id, "id": self.id,
@ -300,6 +309,7 @@ class TabState(Model, AuditMixinNullable, ExtraJSONMixin):
"autorun": self.autorun, "autorun": self.autorun,
"template_params": self.template_params, "template_params": self.template_params,
"hide_left_bar": self.hide_left_bar, "hide_left_bar": self.hide_left_bar,
"saved_query": self.saved_query.to_dict() if self.saved_query else None,
} }