mirror of https://github.com/apache/superset.git
feat(sqllab): SPA migration (#25151)
This commit is contained in:
parent
af661ceee2
commit
5ab1e7eae4
|
@ -20,7 +20,7 @@ import { selectResultsTab } from './sqllab.helper';
|
||||||
|
|
||||||
describe.skip('SqlLab datasource panel', () => {
|
describe.skip('SqlLab datasource panel', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
cy.visit('/superset/sqllab');
|
cy.visit('/sqllab');
|
||||||
});
|
});
|
||||||
|
|
||||||
// TODO the test bellow is flaky, and has been disabled for the time being
|
// TODO the test bellow is flaky, and has been disabled for the time being
|
||||||
|
|
|
@ -25,7 +25,7 @@ function parseClockStr(node: JQuery) {
|
||||||
|
|
||||||
describe('SqlLab query panel', () => {
|
describe('SqlLab query panel', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
cy.visit('/superset/sqllab');
|
cy.visit('/sqllab');
|
||||||
});
|
});
|
||||||
|
|
||||||
it.skip('supports entering and running a query', () => {
|
it.skip('supports entering and running a query', () => {
|
||||||
|
|
|
@ -19,7 +19,7 @@
|
||||||
|
|
||||||
describe('SqlLab view', () => {
|
describe('SqlLab view', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
cy.visit('/superset/sqllab');
|
cy.visit('/sqllab');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should load the SqlLab', () => {
|
it('should load the SqlLab', () => {
|
||||||
|
|
|
@ -18,7 +18,7 @@
|
||||||
*/
|
*/
|
||||||
describe('SqlLab query tabs', () => {
|
describe('SqlLab query tabs', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
cy.visit('/superset/sqllab');
|
cy.visit('/sqllab');
|
||||||
});
|
});
|
||||||
|
|
||||||
const tablistSelector = '[data-test="sql-editor-tabs"] > [role="tablist"]';
|
const tablistSelector = '[data-test="sql-editor-tabs"] > [role="tablist"]';
|
||||||
|
|
|
@ -77984,7 +77984,7 @@
|
||||||
"@mapbox/geojson-extent": "^1.0.1",
|
"@mapbox/geojson-extent": "^1.0.1",
|
||||||
"@math.gl/web-mercator": "^3.2.2",
|
"@math.gl/web-mercator": "^3.2.2",
|
||||||
"@types/d3-array": "^2.0.0",
|
"@types/d3-array": "^2.0.0",
|
||||||
"@types/mapbox__geojson-extent": "*",
|
"@types/mapbox__geojson-extent": "^1.0.0",
|
||||||
"@types/underscore": "^1.11.6",
|
"@types/underscore": "^1.11.6",
|
||||||
"@types/urijs": "^1.19.19",
|
"@types/urijs": "^1.19.19",
|
||||||
"bootstrap-slider": "^10.0.0",
|
"bootstrap-slider": "^10.0.0",
|
||||||
|
|
|
@ -29,7 +29,6 @@ import messageToasts from 'src/components/MessageToasts/reducers';
|
||||||
import saveModal from 'src/explore/reducers/saveModalReducer';
|
import saveModal from 'src/explore/reducers/saveModalReducer';
|
||||||
import explore from 'src/explore/reducers/exploreReducer';
|
import explore from 'src/explore/reducers/exploreReducer';
|
||||||
import sqlLab from 'src/SqlLab/reducers/sqlLab';
|
import sqlLab from 'src/SqlLab/reducers/sqlLab';
|
||||||
import localStorageUsageInKilobytes from 'src/SqlLab/reducers/localStorageUsage';
|
|
||||||
import reports from 'src/features/reports/ReportModal/reducer';
|
import reports from 'src/features/reports/ReportModal/reducer';
|
||||||
import getBootstrapData from 'src/utils/getBootstrapData';
|
import getBootstrapData from 'src/utils/getBootstrapData';
|
||||||
|
|
||||||
|
@ -59,7 +58,7 @@ export default {
|
||||||
saveModal,
|
saveModal,
|
||||||
explore,
|
explore,
|
||||||
sqlLab,
|
sqlLab,
|
||||||
localStorageUsageInKilobytes,
|
localStorageUsageInKilobytes: noopReducer(0),
|
||||||
reports,
|
reports,
|
||||||
common: noopReducer(common),
|
common: noopReducer(common),
|
||||||
user: noopReducer(user),
|
user: noopReducer(user),
|
||||||
|
|
|
@ -1,84 +0,0 @@
|
||||||
/**
|
|
||||||
* 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 { Provider } from 'react-redux';
|
|
||||||
import { hot } from 'react-hot-loader/root';
|
|
||||||
import {
|
|
||||||
FeatureFlag,
|
|
||||||
ThemeProvider,
|
|
||||||
initFeatureFlags,
|
|
||||||
isFeatureEnabled,
|
|
||||||
} from '@superset-ui/core';
|
|
||||||
import { GlobalStyles } from 'src/GlobalStyles';
|
|
||||||
import { setupStore, userReducer } from 'src/views/store';
|
|
||||||
import setupExtensions from 'src/setup/setupExtensions';
|
|
||||||
import getBootstrapData from 'src/utils/getBootstrapData';
|
|
||||||
import { persistSqlLabStateEnhancer } from 'src/SqlLab/middlewares/persistSqlLabStateEnhancer';
|
|
||||||
import getInitialState from './reducers/getInitialState';
|
|
||||||
import { reducers } from './reducers/index';
|
|
||||||
import App from './components/App';
|
|
||||||
import { rehydratePersistedState } from './utils/reduxStateToLocalStorageHelper';
|
|
||||||
import setupApp from '../setup/setupApp';
|
|
||||||
|
|
||||||
import '../assets/stylesheets/reactable-pagination.less';
|
|
||||||
import { theme } from '../preamble';
|
|
||||||
import { SqlLabGlobalStyles } from './SqlLabGlobalStyles';
|
|
||||||
|
|
||||||
setupApp();
|
|
||||||
setupExtensions();
|
|
||||||
|
|
||||||
const bootstrapData = getBootstrapData();
|
|
||||||
|
|
||||||
initFeatureFlags(bootstrapData.common.feature_flags);
|
|
||||||
|
|
||||||
const initialState = getInitialState(bootstrapData);
|
|
||||||
|
|
||||||
export const store = setupStore({
|
|
||||||
initialState,
|
|
||||||
rootReducers: { ...reducers, user: userReducer },
|
|
||||||
...(!isFeatureEnabled(FeatureFlag.SQLLAB_BACKEND_PERSISTENCE) && {
|
|
||||||
enhancers: [persistSqlLabStateEnhancer],
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
|
|
||||||
rehydratePersistedState(store.dispatch, initialState);
|
|
||||||
|
|
||||||
// Highlight the navbar menu
|
|
||||||
const menus = document.querySelectorAll('.nav.navbar-nav li.dropdown');
|
|
||||||
const sqlLabMenu = Array.prototype.slice
|
|
||||||
.apply(menus)
|
|
||||||
.find(element => element.innerText.trim() === 'SQL Lab');
|
|
||||||
if (sqlLabMenu) {
|
|
||||||
const classes = sqlLabMenu.getAttribute('class');
|
|
||||||
if (classes.indexOf('active') === -1) {
|
|
||||||
sqlLabMenu.setAttribute('class', `${classes} active`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const Application = () => (
|
|
||||||
<Provider store={store}>
|
|
||||||
<ThemeProvider theme={theme}>
|
|
||||||
<GlobalStyles />
|
|
||||||
<SqlLabGlobalStyles />
|
|
||||||
<App />
|
|
||||||
</ThemeProvider>
|
|
||||||
</Provider>
|
|
||||||
);
|
|
||||||
|
|
||||||
export default hot(Application);
|
|
|
@ -28,7 +28,7 @@ import { schemaApiUtil } from 'src/hooks/apiResources/schemas';
|
||||||
import { tableApiUtil } from 'src/hooks/apiResources/tables';
|
import { tableApiUtil } from 'src/hooks/apiResources/tables';
|
||||||
import { addTable } from 'src/SqlLab/actions/sqlLab';
|
import { addTable } from 'src/SqlLab/actions/sqlLab';
|
||||||
import { initialState } from 'src/SqlLab/fixtures';
|
import { initialState } from 'src/SqlLab/fixtures';
|
||||||
import { reducers } from 'src/SqlLab/reducers';
|
import reducers from 'spec/helpers/reducerIndex';
|
||||||
import {
|
import {
|
||||||
SCHEMA_AUTOCOMPLETE_SCORE,
|
SCHEMA_AUTOCOMPLETE_SCORE,
|
||||||
TABLE_AUTOCOMPLETE_SCORE,
|
TABLE_AUTOCOMPLETE_SCORE,
|
||||||
|
|
|
@ -17,12 +17,13 @@
|
||||||
* under the License.
|
* under the License.
|
||||||
*/
|
*/
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import { combineReducers } from 'redux';
|
||||||
import configureStore from 'redux-mock-store';
|
import configureStore from 'redux-mock-store';
|
||||||
import thunk from 'redux-thunk';
|
import thunk from 'redux-thunk';
|
||||||
import { render } from 'spec/helpers/testing-library';
|
import { render } from 'spec/helpers/testing-library';
|
||||||
|
|
||||||
import App from 'src/SqlLab/components/App';
|
import App from 'src/SqlLab/components/App';
|
||||||
import sqlLabReducer from 'src/SqlLab/reducers/index';
|
import reducers from 'spec/helpers/reducerIndex';
|
||||||
import { LOCALSTORAGE_MAX_USAGE_KB } from 'src/SqlLab/constants';
|
import { LOCALSTORAGE_MAX_USAGE_KB } from 'src/SqlLab/constants';
|
||||||
import { LOG_EVENT } from 'src/logger/actions';
|
import { LOG_EVENT } from 'src/logger/actions';
|
||||||
import {
|
import {
|
||||||
|
@ -37,6 +38,8 @@ jest.mock('src/SqlLab/components/QueryAutoRefresh', () => () => (
|
||||||
<div data-test="mock-query-auto-refresh" />
|
<div data-test="mock-query-auto-refresh" />
|
||||||
));
|
));
|
||||||
|
|
||||||
|
const sqlLabReducer = combineReducers(reducers);
|
||||||
|
|
||||||
describe('SqlLab App', () => {
|
describe('SqlLab App', () => {
|
||||||
const middlewares = [thunk];
|
const middlewares = [thunk];
|
||||||
const mockStore = configureStore(middlewares);
|
const mockStore = configureStore(middlewares);
|
||||||
|
|
|
@ -20,9 +20,9 @@ import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { bindActionCreators } from 'redux';
|
import { bindActionCreators } from 'redux';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
|
import { Redirect } from 'react-router-dom';
|
||||||
import { css, styled, t } from '@superset-ui/core';
|
import { css, styled, t } from '@superset-ui/core';
|
||||||
import throttle from 'lodash/throttle';
|
import throttle from 'lodash/throttle';
|
||||||
import ToastContainer from 'src/components/MessageToasts/ToastContainer';
|
|
||||||
import {
|
import {
|
||||||
LOCALSTORAGE_MAX_USAGE_KB,
|
LOCALSTORAGE_MAX_USAGE_KB,
|
||||||
LOCALSTORAGE_WARNING_THRESHOLD,
|
LOCALSTORAGE_WARNING_THRESHOLD,
|
||||||
|
@ -186,7 +186,14 @@ class App extends React.PureComponent {
|
||||||
render() {
|
render() {
|
||||||
const { queries, queriesLastUpdate } = this.props;
|
const { queries, queriesLastUpdate } = this.props;
|
||||||
if (this.state.hash && this.state.hash === '#search') {
|
if (this.state.hash && this.state.hash === '#search') {
|
||||||
return window.location.replace('/superset/sqllab/history/');
|
return (
|
||||||
|
<Redirect
|
||||||
|
to={{
|
||||||
|
pathname: '/sqllab/history/',
|
||||||
|
replace: true,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<SqlLabStyles data-test="SqlLabApp" className="App SqlLab">
|
<SqlLabStyles data-test="SqlLabApp" className="App SqlLab">
|
||||||
|
@ -195,7 +202,6 @@ class App extends React.PureComponent {
|
||||||
queriesLastUpdate={queriesLastUpdate}
|
queriesLastUpdate={queriesLastUpdate}
|
||||||
/>
|
/>
|
||||||
<TabbedSqlEditors />
|
<TabbedSqlEditors />
|
||||||
<ToastContainer />
|
|
||||||
</SqlLabStyles>
|
</SqlLabStyles>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -61,7 +61,7 @@ interface QueryTableProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
const openQuery = (id: number) => {
|
const openQuery = (id: number) => {
|
||||||
const url = `/superset/sqllab?queryId=${id}`;
|
const url = `/sqllab?queryId=${id}`;
|
||||||
window.open(url);
|
window.open(url);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -20,7 +20,7 @@ import React from 'react';
|
||||||
import { act } from 'react-dom/test-utils';
|
import { act } from 'react-dom/test-utils';
|
||||||
import { fireEvent, render, waitFor } from 'spec/helpers/testing-library';
|
import { fireEvent, render, waitFor } from 'spec/helpers/testing-library';
|
||||||
import fetchMock from 'fetch-mock';
|
import fetchMock from 'fetch-mock';
|
||||||
import { reducers } from 'src/SqlLab/reducers';
|
import reducers from 'spec/helpers/reducerIndex';
|
||||||
import SqlEditor from 'src/SqlLab/components/SqlEditor';
|
import SqlEditor from 'src/SqlLab/components/SqlEditor';
|
||||||
import { setupStore } from 'src/views/store';
|
import { setupStore } from 'src/views/store';
|
||||||
import {
|
import {
|
||||||
|
|
|
@ -26,7 +26,7 @@ import SqlEditorLeftBar from 'src/SqlLab/components/SqlEditorLeftBar';
|
||||||
import { table, initialState, defaultQueryEditor } from 'src/SqlLab/fixtures';
|
import { table, initialState, defaultQueryEditor } from 'src/SqlLab/fixtures';
|
||||||
import { api } from 'src/hooks/apiResources/queryApi';
|
import { api } from 'src/hooks/apiResources/queryApi';
|
||||||
import { setupStore } from 'src/views/store';
|
import { setupStore } from 'src/views/store';
|
||||||
import { reducers } from 'src/SqlLab/reducers';
|
import reducers from 'spec/helpers/reducerIndex';
|
||||||
|
|
||||||
const mockedProps = {
|
const mockedProps = {
|
||||||
tables: [table],
|
tables: [table],
|
||||||
|
|
|
@ -110,23 +110,17 @@ describe('TabbedSqlEditors', () => {
|
||||||
it('should handle id', async () => {
|
it('should handle id', async () => {
|
||||||
uriStub.returns({ id: 1 });
|
uriStub.returns({ id: 1 });
|
||||||
await mountWithAct();
|
await mountWithAct();
|
||||||
expect(window.history.replaceState.getCall(0).args[2]).toBe(
|
expect(window.history.replaceState.getCall(0).args[2]).toBe('/sqllab');
|
||||||
'/superset/sqllab',
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
it('should handle savedQueryId', async () => {
|
it('should handle savedQueryId', async () => {
|
||||||
uriStub.returns({ savedQueryId: 1 });
|
uriStub.returns({ savedQueryId: 1 });
|
||||||
await mountWithAct();
|
await mountWithAct();
|
||||||
expect(window.history.replaceState.getCall(0).args[2]).toBe(
|
expect(window.history.replaceState.getCall(0).args[2]).toBe('/sqllab');
|
||||||
'/superset/sqllab',
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
it('should handle sql', async () => {
|
it('should handle sql', async () => {
|
||||||
uriStub.returns({ sql: 1, dbid: 1 });
|
uriStub.returns({ sql: 1, dbid: 1 });
|
||||||
await mountWithAct();
|
await mountWithAct();
|
||||||
expect(window.history.replaceState.getCall(0).args[2]).toBe(
|
expect(window.history.replaceState.getCall(0).args[2]).toBe('/sqllab');
|
||||||
'/superset/sqllab',
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
it('should handle custom url params', async () => {
|
it('should handle custom url params', async () => {
|
||||||
uriStub.returns({
|
uriStub.returns({
|
||||||
|
@ -137,7 +131,7 @@ describe('TabbedSqlEditors', () => {
|
||||||
});
|
});
|
||||||
await mountWithAct();
|
await mountWithAct();
|
||||||
expect(window.history.replaceState.getCall(0).args[2]).toBe(
|
expect(window.history.replaceState.getCall(0).args[2]).toBe(
|
||||||
'/superset/sqllab?custom_value=str&extra_attr1=true',
|
'/sqllab?custom_value=str&extra_attr1=true',
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -29,6 +29,7 @@ import { detectOS } from 'src/utils/common';
|
||||||
import * as Actions from 'src/SqlLab/actions/sqlLab';
|
import * as Actions from 'src/SqlLab/actions/sqlLab';
|
||||||
import { EmptyStateBig } from 'src/components/EmptyState';
|
import { EmptyStateBig } from 'src/components/EmptyState';
|
||||||
import getBootstrapData from 'src/utils/getBootstrapData';
|
import getBootstrapData from 'src/utils/getBootstrapData';
|
||||||
|
import { locationContext } from 'src/pages/SqlLab/LocationContext';
|
||||||
import SqlEditor from '../SqlEditor';
|
import SqlEditor from '../SqlEditor';
|
||||||
import SqlEditorTabHeader from '../SqlEditorTabHeader';
|
import SqlEditorTabHeader from '../SqlEditorTabHeader';
|
||||||
|
|
||||||
|
@ -75,7 +76,7 @@ const userOS = detectOS();
|
||||||
class TabbedSqlEditors extends React.PureComponent {
|
class TabbedSqlEditors extends React.PureComponent {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
const sqlLabUrl = '/superset/sqllab';
|
const sqlLabUrl = '/sqllab';
|
||||||
this.state = {
|
this.state = {
|
||||||
sqlLabUrl,
|
sqlLabUrl,
|
||||||
};
|
};
|
||||||
|
@ -132,6 +133,7 @@ class TabbedSqlEditors extends React.PureComponent {
|
||||||
new: isNewQuery,
|
new: isNewQuery,
|
||||||
...urlParams
|
...urlParams
|
||||||
} = {
|
} = {
|
||||||
|
...this.context.requestedQuery,
|
||||||
...bootstrapData.requested_query,
|
...bootstrapData.requested_query,
|
||||||
...queryParameters,
|
...queryParameters,
|
||||||
};
|
};
|
||||||
|
@ -332,6 +334,7 @@ class TabbedSqlEditors extends React.PureComponent {
|
||||||
}
|
}
|
||||||
TabbedSqlEditors.propTypes = propTypes;
|
TabbedSqlEditors.propTypes = propTypes;
|
||||||
TabbedSqlEditors.defaultProps = defaultProps;
|
TabbedSqlEditors.defaultProps = defaultProps;
|
||||||
|
TabbedSqlEditors.contextType = locationContext;
|
||||||
|
|
||||||
function mapStateToProps({ sqlLab, common }) {
|
function mapStateToProps({ sqlLab, common }) {
|
||||||
return {
|
return {
|
||||||
|
|
|
@ -1,23 +0,0 @@
|
||||||
/**
|
|
||||||
* 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 ReactDOM from 'react-dom';
|
|
||||||
import App from './App';
|
|
||||||
|
|
||||||
ReactDOM.render(<App />, document.getElementById('app'));
|
|
|
@ -1,21 +0,0 @@
|
||||||
/**
|
|
||||||
* 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.
|
|
||||||
*/
|
|
||||||
export default function commonReducer(state = {}) {
|
|
||||||
return state;
|
|
||||||
}
|
|
|
@ -1,21 +0,0 @@
|
||||||
/**
|
|
||||||
* 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.
|
|
||||||
*/
|
|
||||||
export default function localStorageUsageReducer(state = 0) {
|
|
||||||
return state;
|
|
||||||
}
|
|
|
@ -39,7 +39,6 @@ import { addDangerToast } from 'src/components/MessageToasts/actions';
|
||||||
import { logEvent } from 'src/logger/actions';
|
import { logEvent } from 'src/logger/actions';
|
||||||
import { Logger, LOG_ACTIONS_LOAD_CHART } from 'src/logger/LogUtils';
|
import { Logger, LOG_ACTIONS_LOAD_CHART } from 'src/logger/LogUtils';
|
||||||
import { getClientErrorObject } from 'src/utils/getClientErrorObject';
|
import { getClientErrorObject } from 'src/utils/getClientErrorObject';
|
||||||
import { safeStringify } from 'src/utils/safeStringify';
|
|
||||||
import { allowCrossDomain as domainShardingEnabled } from 'src/utils/hostNamesConfig';
|
import { allowCrossDomain as domainShardingEnabled } from 'src/utils/hostNamesConfig';
|
||||||
import { updateDataMask } from 'src/dataMask/actions';
|
import { updateDataMask } from 'src/dataMask/actions';
|
||||||
import { waitForAsyncData } from 'src/middleware/asyncEvent';
|
import { waitForAsyncData } from 'src/middleware/asyncEvent';
|
||||||
|
@ -571,17 +570,20 @@ export function postChartFormData(
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function redirectSQLLab(formData) {
|
export function redirectSQLLab(formData, history) {
|
||||||
return dispatch => {
|
return dispatch => {
|
||||||
getChartDataRequest({ formData, resultFormat: 'json', resultType: 'query' })
|
getChartDataRequest({ formData, resultFormat: 'json', resultType: 'query' })
|
||||||
.then(({ json }) => {
|
.then(({ json }) => {
|
||||||
const redirectUrl = '/superset/sqllab/';
|
const redirectUrl = '/sqllab/';
|
||||||
const payload = {
|
const payload = {
|
||||||
datasourceKey: formData.datasource,
|
datasourceKey: formData.datasource,
|
||||||
sql: json.result[0].query,
|
sql: json.result[0].query,
|
||||||
};
|
};
|
||||||
SupersetClient.postForm(redirectUrl, {
|
history.push({
|
||||||
form_data: safeStringify(payload),
|
pathname: redirectUrl,
|
||||||
|
state: {
|
||||||
|
requestedQuery: payload,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
.catch(() =>
|
.catch(() =>
|
||||||
|
|
|
@ -17,6 +17,7 @@
|
||||||
* under the License.
|
* under the License.
|
||||||
*/
|
*/
|
||||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||||
|
import { useHistory } from 'react-router-dom';
|
||||||
import { useDispatch } from 'react-redux';
|
import { useDispatch } from 'react-redux';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { Tooltip } from 'src/components/Tooltip';
|
import { Tooltip } from 'src/components/Tooltip';
|
||||||
|
@ -151,12 +152,22 @@ export const ExploreChartHeader = ({
|
||||||
[dispatch],
|
[dispatch],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const history = useHistory();
|
||||||
|
const { redirectSQLLab } = actions;
|
||||||
|
|
||||||
|
const redirectToSQLLab = useCallback(
|
||||||
|
formData => {
|
||||||
|
redirectSQLLab(formData, history);
|
||||||
|
},
|
||||||
|
[redirectSQLLab, history],
|
||||||
|
);
|
||||||
|
|
||||||
const [menu, isDropdownVisible, setIsDropdownVisible] =
|
const [menu, isDropdownVisible, setIsDropdownVisible] =
|
||||||
useExploreAdditionalActionsMenu(
|
useExploreAdditionalActionsMenu(
|
||||||
latestQueryFormData,
|
latestQueryFormData,
|
||||||
canDownload,
|
canDownload,
|
||||||
slice,
|
slice,
|
||||||
actions.redirectSQLLab,
|
redirectToSQLLab,
|
||||||
openPropertiesModal,
|
openPropertiesModal,
|
||||||
ownState,
|
ownState,
|
||||||
metadata?.dashboards,
|
metadata?.dashboards,
|
||||||
|
|
|
@ -18,6 +18,7 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import { Route } from 'react-router-dom';
|
||||||
import fetchMock from 'fetch-mock';
|
import fetchMock from 'fetch-mock';
|
||||||
import userEvent from '@testing-library/user-event';
|
import userEvent from '@testing-library/user-event';
|
||||||
import { DatasourceType, JsonObject, SupersetClient } from '@superset-ui/core';
|
import { DatasourceType, JsonObject, SupersetClient } from '@superset-ui/core';
|
||||||
|
@ -27,6 +28,17 @@ import DatasourceControl from '.';
|
||||||
|
|
||||||
const SupersetClientGet = jest.spyOn(SupersetClient, 'get');
|
const SupersetClientGet = jest.spyOn(SupersetClient, 'get');
|
||||||
|
|
||||||
|
const mockDatasource = {
|
||||||
|
id: 25,
|
||||||
|
database: {
|
||||||
|
name: 'examples',
|
||||||
|
},
|
||||||
|
name: 'channels',
|
||||||
|
type: 'table',
|
||||||
|
columns: [],
|
||||||
|
owners: [{ first_name: 'john', last_name: 'doe', id: 1, username: 'jd' }],
|
||||||
|
sql: 'SELECT * FROM mock_datasource_sql',
|
||||||
|
};
|
||||||
const createProps = (overrides: JsonObject = {}) => ({
|
const createProps = (overrides: JsonObject = {}) => ({
|
||||||
hovered: false,
|
hovered: false,
|
||||||
type: 'DatasourceControl',
|
type: 'DatasourceControl',
|
||||||
|
@ -35,16 +47,7 @@ const createProps = (overrides: JsonObject = {}) => ({
|
||||||
description: null,
|
description: null,
|
||||||
value: '25__table',
|
value: '25__table',
|
||||||
form_data: {},
|
form_data: {},
|
||||||
datasource: {
|
datasource: mockDatasource,
|
||||||
id: 25,
|
|
||||||
database: {
|
|
||||||
name: 'examples',
|
|
||||||
},
|
|
||||||
name: 'channels',
|
|
||||||
type: 'table',
|
|
||||||
columns: [],
|
|
||||||
owners: [{ first_name: 'john', last_name: 'doe', id: 1, username: 'jd' }],
|
|
||||||
},
|
|
||||||
validationErrors: [],
|
validationErrors: [],
|
||||||
name: 'datasource',
|
name: 'datasource',
|
||||||
actions: {
|
actions: {
|
||||||
|
@ -91,20 +94,20 @@ async function openAndSaveChanges(datasource: any) {
|
||||||
|
|
||||||
test('Should render', async () => {
|
test('Should render', async () => {
|
||||||
const props = createProps();
|
const props = createProps();
|
||||||
render(<DatasourceControl {...props} />);
|
render(<DatasourceControl {...props} />, { useRouter: true });
|
||||||
expect(await screen.findByTestId('datasource-control')).toBeVisible();
|
expect(await screen.findByTestId('datasource-control')).toBeVisible();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Should have elements', async () => {
|
test('Should have elements', async () => {
|
||||||
const props = createProps();
|
const props = createProps();
|
||||||
render(<DatasourceControl {...props} />);
|
render(<DatasourceControl {...props} />, { useRouter: true });
|
||||||
expect(await screen.findByText('channels')).toBeVisible();
|
expect(await screen.findByText('channels')).toBeVisible();
|
||||||
expect(screen.getByTestId('datasource-menu-trigger')).toBeVisible();
|
expect(screen.getByTestId('datasource-menu-trigger')).toBeVisible();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Should open a menu', async () => {
|
test('Should open a menu', async () => {
|
||||||
const props = createProps();
|
const props = createProps();
|
||||||
render(<DatasourceControl {...props} />);
|
render(<DatasourceControl {...props} />, { useRouter: true });
|
||||||
|
|
||||||
expect(screen.queryByText('Edit dataset')).not.toBeInTheDocument();
|
expect(screen.queryByText('Edit dataset')).not.toBeInTheDocument();
|
||||||
expect(screen.queryByText('Swap dataset')).not.toBeInTheDocument();
|
expect(screen.queryByText('Swap dataset')).not.toBeInTheDocument();
|
||||||
|
@ -131,7 +134,7 @@ test('Should not show SQL Lab for non sql_lab role', async () => {
|
||||||
username: 'gamma',
|
username: 'gamma',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
render(<DatasourceControl {...props} />);
|
render(<DatasourceControl {...props} />, { useRouter: true });
|
||||||
|
|
||||||
userEvent.click(screen.getByTestId('datasource-menu-trigger'));
|
userEvent.click(screen.getByTestId('datasource-menu-trigger'));
|
||||||
|
|
||||||
|
@ -154,7 +157,7 @@ test('Should show SQL Lab for sql_lab role', async () => {
|
||||||
username: 'sql',
|
username: 'sql',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
render(<DatasourceControl {...props} />);
|
render(<DatasourceControl {...props} />, { useRouter: true });
|
||||||
|
|
||||||
userEvent.click(screen.getByTestId('datasource-menu-trigger'));
|
userEvent.click(screen.getByTestId('datasource-menu-trigger'));
|
||||||
|
|
||||||
|
@ -178,6 +181,7 @@ test('Click on Swap dataset option', async () => {
|
||||||
|
|
||||||
render(<DatasourceControl {...props} />, {
|
render(<DatasourceControl {...props} />, {
|
||||||
useRedux: true,
|
useRedux: true,
|
||||||
|
useRouter: true,
|
||||||
});
|
});
|
||||||
userEvent.click(screen.getByTestId('datasource-menu-trigger'));
|
userEvent.click(screen.getByTestId('datasource-menu-trigger'));
|
||||||
|
|
||||||
|
@ -198,6 +202,7 @@ test('Click on Edit dataset', async () => {
|
||||||
);
|
);
|
||||||
render(<DatasourceControl {...props} />, {
|
render(<DatasourceControl {...props} />, {
|
||||||
useRedux: true,
|
useRedux: true,
|
||||||
|
useRouter: true,
|
||||||
});
|
});
|
||||||
userEvent.click(screen.getByTestId('datasource-menu-trigger'));
|
userEvent.click(screen.getByTestId('datasource-menu-trigger'));
|
||||||
|
|
||||||
|
@ -223,6 +228,7 @@ test('Edit dataset should be disabled when user is not admin', async () => {
|
||||||
|
|
||||||
render(<DatasourceControl {...props} />, {
|
render(<DatasourceControl {...props} />, {
|
||||||
useRedux: true,
|
useRedux: true,
|
||||||
|
useRouter: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
userEvent.click(screen.getByTestId('datasource-menu-trigger'));
|
userEvent.click(screen.getByTestId('datasource-menu-trigger'));
|
||||||
|
@ -235,21 +241,41 @@ test('Edit dataset should be disabled when user is not admin', async () => {
|
||||||
|
|
||||||
test('Click on View in SQL Lab', async () => {
|
test('Click on View in SQL Lab', async () => {
|
||||||
const props = createProps();
|
const props = createProps();
|
||||||
const postFormSpy = jest.spyOn(SupersetClient, 'postForm');
|
|
||||||
postFormSpy.mockImplementation(jest.fn());
|
|
||||||
|
|
||||||
render(<DatasourceControl {...props} />, {
|
const { queryByTestId, getByTestId } = render(
|
||||||
useRedux: true,
|
<>
|
||||||
});
|
<Route
|
||||||
|
path="/sqllab"
|
||||||
|
render={({ location }) => (
|
||||||
|
<div data-test="mock-sqllab-route">
|
||||||
|
{JSON.stringify(location.state)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<DatasourceControl {...props} />
|
||||||
|
</>,
|
||||||
|
{
|
||||||
|
useRedux: true,
|
||||||
|
useRouter: true,
|
||||||
|
},
|
||||||
|
);
|
||||||
userEvent.click(screen.getByTestId('datasource-menu-trigger'));
|
userEvent.click(screen.getByTestId('datasource-menu-trigger'));
|
||||||
|
|
||||||
expect(postFormSpy).toBeCalledTimes(0);
|
expect(queryByTestId('mock-sqllab-route')).not.toBeInTheDocument();
|
||||||
|
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
userEvent.click(screen.getByText('View in SQL Lab'));
|
userEvent.click(screen.getByText('View in SQL Lab'));
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(postFormSpy).toBeCalledTimes(1);
|
expect(getByTestId('mock-sqllab-route')).toBeInTheDocument();
|
||||||
|
expect(JSON.parse(`${getByTestId('mock-sqllab-route').textContent}`)).toEqual(
|
||||||
|
{
|
||||||
|
requestedQuery: {
|
||||||
|
datasourceKey: `${mockDatasource.id}__${mockDatasource.type}`,
|
||||||
|
sql: mockDatasource.sql,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Should open a different menu when datasource=query', async () => {
|
test('Should open a different menu when datasource=query', async () => {
|
||||||
|
@ -261,7 +287,7 @@ test('Should open a different menu when datasource=query', async () => {
|
||||||
type: DatasourceType.Query,
|
type: DatasourceType.Query,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
render(<DatasourceControl {...queryProps} />);
|
render(<DatasourceControl {...queryProps} />, { useRouter: true });
|
||||||
|
|
||||||
expect(screen.queryByText('Query preview')).not.toBeInTheDocument();
|
expect(screen.queryByText('Query preview')).not.toBeInTheDocument();
|
||||||
expect(screen.queryByText('View in SQL Lab')).not.toBeInTheDocument();
|
expect(screen.queryByText('View in SQL Lab')).not.toBeInTheDocument();
|
||||||
|
@ -284,7 +310,10 @@ test('Click on Save as dataset', async () => {
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
render(<DatasourceControl {...queryProps} />, { useRedux: true });
|
render(<DatasourceControl {...queryProps} />, {
|
||||||
|
useRedux: true,
|
||||||
|
useRouter: true,
|
||||||
|
});
|
||||||
userEvent.click(screen.getByTestId('datasource-menu-trigger'));
|
userEvent.click(screen.getByTestId('datasource-menu-trigger'));
|
||||||
userEvent.click(screen.getByText('Save as dataset'));
|
userEvent.click(screen.getByText('Save as dataset'));
|
||||||
|
|
||||||
|
@ -327,6 +356,7 @@ test('should set the default temporal column', async () => {
|
||||||
};
|
};
|
||||||
render(<DatasourceControl {...props} {...overrideProps} />, {
|
render(<DatasourceControl {...props} {...overrideProps} />, {
|
||||||
useRedux: true,
|
useRedux: true,
|
||||||
|
useRouter: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
await openAndSaveChanges(overrideProps.datasource);
|
await openAndSaveChanges(overrideProps.datasource);
|
||||||
|
@ -362,6 +392,7 @@ test('should set the first available temporal column', async () => {
|
||||||
};
|
};
|
||||||
render(<DatasourceControl {...props} {...overrideProps} />, {
|
render(<DatasourceControl {...props} {...overrideProps} />, {
|
||||||
useRedux: true,
|
useRedux: true,
|
||||||
|
useRouter: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
await openAndSaveChanges(overrideProps.datasource);
|
await openAndSaveChanges(overrideProps.datasource);
|
||||||
|
@ -397,6 +428,7 @@ test('should not set the temporal column', async () => {
|
||||||
};
|
};
|
||||||
render(<DatasourceControl {...props} {...overrideProps} />, {
|
render(<DatasourceControl {...props} {...overrideProps} />, {
|
||||||
useRedux: true,
|
useRedux: true,
|
||||||
|
useRouter: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
await openAndSaveChanges(overrideProps.datasource);
|
await openAndSaveChanges(overrideProps.datasource);
|
||||||
|
@ -410,7 +442,7 @@ test('should not set the temporal column', async () => {
|
||||||
|
|
||||||
test('should show missing params state', () => {
|
test('should show missing params state', () => {
|
||||||
const props = createProps({ datasource: fallbackExploreInitialData.dataset });
|
const props = createProps({ datasource: fallbackExploreInitialData.dataset });
|
||||||
render(<DatasourceControl {...props} />, { useRedux: true });
|
render(<DatasourceControl {...props} />, { useRedux: true, useRouter: true });
|
||||||
expect(screen.getByText(/missing dataset/i)).toBeVisible();
|
expect(screen.getByText(/missing dataset/i)).toBeVisible();
|
||||||
expect(screen.getByText(/missing url parameters/i)).toBeVisible();
|
expect(screen.getByText(/missing url parameters/i)).toBeVisible();
|
||||||
expect(
|
expect(
|
||||||
|
@ -426,7 +458,7 @@ test('should show missing dataset state', () => {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
window.location = { search: '?slice_id=152' };
|
window.location = { search: '?slice_id=152' };
|
||||||
const props = createProps({ datasource: fallbackExploreInitialData.dataset });
|
const props = createProps({ datasource: fallbackExploreInitialData.dataset });
|
||||||
render(<DatasourceControl {...props} />, { useRedux: true });
|
render(<DatasourceControl {...props} />, { useRedux: true, useRouter: true });
|
||||||
expect(screen.getAllByText(/missing dataset/i)).toHaveLength(2);
|
expect(screen.getAllByText(/missing dataset/i)).toHaveLength(2);
|
||||||
expect(
|
expect(
|
||||||
screen.getByText(
|
screen.getByText(
|
||||||
|
|
|
@ -20,13 +20,7 @@
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import {
|
import { DatasourceType, styled, t, withTheme } from '@superset-ui/core';
|
||||||
DatasourceType,
|
|
||||||
SupersetClient,
|
|
||||||
styled,
|
|
||||||
t,
|
|
||||||
withTheme,
|
|
||||||
} from '@superset-ui/core';
|
|
||||||
import { getTemporalColumns } from '@superset-ui/chart-controls';
|
import { getTemporalColumns } from '@superset-ui/chart-controls';
|
||||||
import { getUrlParam } from 'src/utils/urlUtils';
|
import { getUrlParam } from 'src/utils/urlUtils';
|
||||||
import { AntdDropdown } from 'src/components';
|
import { AntdDropdown } from 'src/components';
|
||||||
|
@ -50,8 +44,8 @@ import ModalTrigger from 'src/components/ModalTrigger';
|
||||||
import ViewQueryModalFooter from 'src/explore/components/controls/ViewQueryModalFooter';
|
import ViewQueryModalFooter from 'src/explore/components/controls/ViewQueryModalFooter';
|
||||||
import ViewQuery from 'src/explore/components/controls/ViewQuery';
|
import ViewQuery from 'src/explore/components/controls/ViewQuery';
|
||||||
import { SaveDatasetModal } from 'src/SqlLab/components/SaveDatasetModal';
|
import { SaveDatasetModal } from 'src/SqlLab/components/SaveDatasetModal';
|
||||||
import { safeStringify } from 'src/utils/safeStringify';
|
|
||||||
import { isString } from 'lodash';
|
import { isString } from 'lodash';
|
||||||
|
import { Link } from 'react-router-dom';
|
||||||
|
|
||||||
const propTypes = {
|
const propTypes = {
|
||||||
actions: PropTypes.object.isRequired,
|
actions: PropTypes.object.isRequired,
|
||||||
|
@ -126,7 +120,6 @@ const Styles = styled.div`
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const CHANGE_DATASET = 'change_dataset';
|
const CHANGE_DATASET = 'change_dataset';
|
||||||
const VIEW_IN_SQL_LAB = 'view_in_sql_lab';
|
|
||||||
const EDIT_DATASET = 'edit_dataset';
|
const EDIT_DATASET = 'edit_dataset';
|
||||||
const QUERY_PREVIEW = 'query_preview';
|
const QUERY_PREVIEW = 'query_preview';
|
||||||
const SAVE_AS_DATASET = 'save_as_dataset';
|
const SAVE_AS_DATASET = 'save_as_dataset';
|
||||||
|
@ -238,19 +231,6 @@ class DatasourceControl extends React.PureComponent {
|
||||||
this.toggleEditDatasourceModal();
|
this.toggleEditDatasourceModal();
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case VIEW_IN_SQL_LAB:
|
|
||||||
{
|
|
||||||
const { datasource } = this.props;
|
|
||||||
const payload = {
|
|
||||||
datasourceKey: `${datasource.id}__${datasource.type}`,
|
|
||||||
sql: datasource.sql,
|
|
||||||
};
|
|
||||||
SupersetClient.postForm('/superset/sqllab/', {
|
|
||||||
form_data: safeStringify(payload),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
case SAVE_AS_DATASET:
|
case SAVE_AS_DATASET:
|
||||||
this.toggleSaveDatasetModal();
|
this.toggleSaveDatasetModal();
|
||||||
break;
|
break;
|
||||||
|
@ -286,6 +266,10 @@ class DatasourceControl extends React.PureComponent {
|
||||||
const canAccessSqlLab = userHasPermission(user, 'SQL Lab', 'menu_access');
|
const canAccessSqlLab = userHasPermission(user, 'SQL Lab', 'menu_access');
|
||||||
|
|
||||||
const editText = t('Edit dataset');
|
const editText = t('Edit dataset');
|
||||||
|
const requestedQuery = {
|
||||||
|
datasourceKey: `${datasource.id}__${datasource.type}`,
|
||||||
|
sql: datasource.sql,
|
||||||
|
};
|
||||||
|
|
||||||
const defaultDatasourceMenu = (
|
const defaultDatasourceMenu = (
|
||||||
<Menu onClick={this.handleMenuItemClick}>
|
<Menu onClick={this.handleMenuItemClick}>
|
||||||
|
@ -310,7 +294,16 @@ class DatasourceControl extends React.PureComponent {
|
||||||
)}
|
)}
|
||||||
<Menu.Item key={CHANGE_DATASET}>{t('Swap dataset')}</Menu.Item>
|
<Menu.Item key={CHANGE_DATASET}>{t('Swap dataset')}</Menu.Item>
|
||||||
{!isMissingDatasource && canAccessSqlLab && (
|
{!isMissingDatasource && canAccessSqlLab && (
|
||||||
<Menu.Item key={VIEW_IN_SQL_LAB}>{t('View in SQL Lab')}</Menu.Item>
|
<Menu.Item>
|
||||||
|
<Link
|
||||||
|
to={{
|
||||||
|
pathname: '/sqllab',
|
||||||
|
state: { requestedQuery },
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t('View in SQL Lab')}
|
||||||
|
</Link>
|
||||||
|
</Menu.Item>
|
||||||
)}
|
)}
|
||||||
</Menu>
|
</Menu>
|
||||||
);
|
);
|
||||||
|
@ -340,7 +333,16 @@ class DatasourceControl extends React.PureComponent {
|
||||||
/>
|
/>
|
||||||
</Menu.Item>
|
</Menu.Item>
|
||||||
{canAccessSqlLab && (
|
{canAccessSqlLab && (
|
||||||
<Menu.Item key={VIEW_IN_SQL_LAB}>{t('View in SQL Lab')}</Menu.Item>
|
<Menu.Item>
|
||||||
|
<Link
|
||||||
|
to={{
|
||||||
|
pathname: '/sqllab',
|
||||||
|
state: { requestedQuery },
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t('View in SQL Lab')}
|
||||||
|
</Link>
|
||||||
|
</Menu.Item>
|
||||||
)}
|
)}
|
||||||
<Menu.Item key={SAVE_AS_DATASET}>{t('Save as dataset')}</Menu.Item>
|
<Menu.Item key={SAVE_AS_DATASET}>{t('Save as dataset')}</Menu.Item>
|
||||||
</Menu>
|
</Menu>
|
||||||
|
|
|
@ -18,8 +18,9 @@
|
||||||
*/
|
*/
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { isObject } from 'lodash';
|
import { isObject } from 'lodash';
|
||||||
import { t, SupersetClient } from '@superset-ui/core';
|
import { t } from '@superset-ui/core';
|
||||||
import Button from 'src/components/Button';
|
import Button from 'src/components/Button';
|
||||||
|
import { useHistory } from 'react-router-dom';
|
||||||
|
|
||||||
interface SimpleDataSource {
|
interface SimpleDataSource {
|
||||||
id: string;
|
id: string;
|
||||||
|
@ -42,12 +43,18 @@ const ViewQueryModalFooter: React.FC<ViewQueryModalFooterProps> = (props: {
|
||||||
changeDatasource: () => void;
|
changeDatasource: () => void;
|
||||||
datasource: SimpleDataSource;
|
datasource: SimpleDataSource;
|
||||||
}) => {
|
}) => {
|
||||||
|
const history = useHistory();
|
||||||
const viewInSQLLab = (id: string, type: string, sql: string) => {
|
const viewInSQLLab = (id: string, type: string, sql: string) => {
|
||||||
const payload = {
|
const payload = {
|
||||||
datasourceKey: `${id}__${type}`,
|
datasourceKey: `${id}__${type}`,
|
||||||
sql,
|
sql,
|
||||||
};
|
};
|
||||||
SupersetClient.postForm('/superset/sqllab/', payload);
|
history.push({
|
||||||
|
pathname: '/sqllab',
|
||||||
|
state: {
|
||||||
|
requestedQuery: payload,
|
||||||
|
},
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const openSQL = () => {
|
const openSQL = () => {
|
||||||
|
|
|
@ -32,6 +32,7 @@ import React, {
|
||||||
useReducer,
|
useReducer,
|
||||||
Reducer,
|
Reducer,
|
||||||
} from 'react';
|
} from 'react';
|
||||||
|
import { useHistory } from 'react-router-dom';
|
||||||
import { setItem, LocalStorageKeys } from 'src/utils/localStorageHelpers';
|
import { setItem, LocalStorageKeys } from 'src/utils/localStorageHelpers';
|
||||||
import { UploadChangeParam, UploadFile } from 'antd/lib/upload/interface';
|
import { UploadChangeParam, UploadFile } from 'antd/lib/upload/interface';
|
||||||
import Tabs from 'src/components/Tabs';
|
import Tabs from 'src/components/Tabs';
|
||||||
|
@ -141,7 +142,6 @@ interface DatabaseModalProps {
|
||||||
show: boolean;
|
show: boolean;
|
||||||
databaseId: number | undefined; // If included, will go into edit mode
|
databaseId: number | undefined; // If included, will go into edit mode
|
||||||
dbEngine: string | undefined; // if included goto step 2 with engine already set
|
dbEngine: string | undefined; // if included goto step 2 with engine already set
|
||||||
history?: any;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum ActionType {
|
export enum ActionType {
|
||||||
|
@ -526,7 +526,6 @@ const DatabaseModal: FunctionComponent<DatabaseModalProps> = ({
|
||||||
show,
|
show,
|
||||||
databaseId,
|
databaseId,
|
||||||
dbEngine,
|
dbEngine,
|
||||||
history,
|
|
||||||
}) => {
|
}) => {
|
||||||
const [db, setDB] = useReducer<
|
const [db, setDB] = useReducer<
|
||||||
Reducer<Partial<DatabaseObject> | null, DBReducerActionType>
|
Reducer<Partial<DatabaseObject> | null, DBReducerActionType>
|
||||||
|
@ -627,6 +626,7 @@ const DatabaseModal: FunctionComponent<DatabaseModalProps> = ({
|
||||||
(DB: DatabaseObject) => DB.backend === engine || DB.engine === engine,
|
(DB: DatabaseObject) => DB.backend === engine || DB.engine === engine,
|
||||||
)?.parameters !== undefined;
|
)?.parameters !== undefined;
|
||||||
const showDBError = validationErrors || dbErrors;
|
const showDBError = validationErrors || dbErrors;
|
||||||
|
const history = useHistory();
|
||||||
|
|
||||||
const dbModel: DatabaseForm =
|
const dbModel: DatabaseForm =
|
||||||
availableDbs?.databases?.find(
|
availableDbs?.databases?.find(
|
||||||
|
@ -700,13 +700,7 @@ const DatabaseModal: FunctionComponent<DatabaseModalProps> = ({
|
||||||
};
|
};
|
||||||
|
|
||||||
const redirectURL = (url: string) => {
|
const redirectURL = (url: string) => {
|
||||||
/* TODO (lyndsiWilliams): This check and passing history
|
history.push(url);
|
||||||
as a prop can be removed once SQL Lab is in the SPA */
|
|
||||||
if (!isEmpty(history)) {
|
|
||||||
history?.push(url);
|
|
||||||
} else {
|
|
||||||
window.location.href = url;
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Database import logic
|
// Database import logic
|
||||||
|
@ -1583,7 +1577,7 @@ const DatabaseModal: FunctionComponent<DatabaseModalProps> = ({
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
fetchAndSetDB();
|
fetchAndSetDB();
|
||||||
redirectURL(`/superset/sqllab/?db=true`);
|
redirectURL(`/sqllab?db=true`);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{t('QUERY DATA IN SQL LAB')}
|
{t('QUERY DATA IN SQL LAB')}
|
||||||
|
|
|
@ -45,7 +45,9 @@ jest.mock(
|
||||||
|
|
||||||
describe('DatasetPanel', () => {
|
describe('DatasetPanel', () => {
|
||||||
test('renders a blank state DatasetPanel', () => {
|
test('renders a blank state DatasetPanel', () => {
|
||||||
render(<DatasetPanel hasError={false} columnList={[]} loading={false} />);
|
render(<DatasetPanel hasError={false} columnList={[]} loading={false} />, {
|
||||||
|
useRouter: true,
|
||||||
|
});
|
||||||
|
|
||||||
const blankDatasetImg = screen.getByRole('img', { name: /empty/i });
|
const blankDatasetImg = screen.getByRole('img', { name: /empty/i });
|
||||||
expect(blankDatasetImg).toBeVisible();
|
expect(blankDatasetImg).toBeVisible();
|
||||||
|
@ -73,6 +75,9 @@ describe('DatasetPanel', () => {
|
||||||
columnList={[]}
|
columnList={[]}
|
||||||
loading={false}
|
loading={false}
|
||||||
/>,
|
/>,
|
||||||
|
{
|
||||||
|
useRouter: true,
|
||||||
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
const blankDatasetImg = screen.getByRole('img', { name: /empty/i });
|
const blankDatasetImg = screen.getByRole('img', { name: /empty/i });
|
||||||
|
@ -91,6 +96,9 @@ describe('DatasetPanel', () => {
|
||||||
columnList={[]}
|
columnList={[]}
|
||||||
loading
|
loading
|
||||||
/>,
|
/>,
|
||||||
|
{
|
||||||
|
useRouter: true,
|
||||||
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
const blankDatasetImg = screen.getByAltText(ALT_LOADING);
|
const blankDatasetImg = screen.getByAltText(ALT_LOADING);
|
||||||
|
@ -107,6 +115,9 @@ describe('DatasetPanel', () => {
|
||||||
columnList={[]}
|
columnList={[]}
|
||||||
loading={false}
|
loading={false}
|
||||||
/>,
|
/>,
|
||||||
|
{
|
||||||
|
useRouter: true,
|
||||||
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
const errorTitle = screen.getByText(ERROR_TITLE);
|
const errorTitle = screen.getByText(ERROR_TITLE);
|
||||||
|
@ -124,6 +135,9 @@ describe('DatasetPanel', () => {
|
||||||
columnList={exampleColumns}
|
columnList={exampleColumns}
|
||||||
loading={false}
|
loading={false}
|
||||||
/>,
|
/>,
|
||||||
|
{
|
||||||
|
useRouter: true,
|
||||||
|
},
|
||||||
);
|
);
|
||||||
expect(await screen.findByText(tableName)).toBeVisible();
|
expect(await screen.findByText(tableName)).toBeVisible();
|
||||||
expect(screen.getByText(COLUMN_TITLE)).toBeVisible();
|
expect(screen.getByText(COLUMN_TITLE)).toBeVisible();
|
||||||
|
@ -148,6 +162,9 @@ describe('DatasetPanel', () => {
|
||||||
loading={false}
|
loading={false}
|
||||||
datasets={exampleDataset}
|
datasets={exampleDataset}
|
||||||
/>,
|
/>,
|
||||||
|
{
|
||||||
|
useRouter: true,
|
||||||
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
// This is text in the info banner
|
// This is text in the info banner
|
||||||
|
|
|
@ -20,6 +20,7 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { t, styled } from '@superset-ui/core';
|
import { t, styled } from '@superset-ui/core';
|
||||||
import { EmptyStateBig } from 'src/components/EmptyState';
|
import { EmptyStateBig } from 'src/components/EmptyState';
|
||||||
|
import { Link } from 'react-router-dom';
|
||||||
|
|
||||||
const StyledContainer = styled.div`
|
const StyledContainer = styled.div`
|
||||||
padding: ${({ theme }) => theme.gridUnit * 8}px
|
padding: ${({ theme }) => theme.gridUnit * 8}px
|
||||||
|
@ -50,15 +51,11 @@ export const VIEW_DATASET_MESSAGE = t(
|
||||||
const renderEmptyDescription = () => (
|
const renderEmptyDescription = () => (
|
||||||
<>
|
<>
|
||||||
{SELECT_MESSAGE}
|
{SELECT_MESSAGE}
|
||||||
<span
|
<Link to="/sqllab">
|
||||||
role="button"
|
<span role="button" tabIndex={0}>
|
||||||
onClick={() => {
|
{CREATE_MESSAGE}
|
||||||
window.location.href = `/superset/sqllab`;
|
</span>
|
||||||
}}
|
</Link>
|
||||||
tabIndex={0}
|
|
||||||
>
|
|
||||||
{CREATE_MESSAGE}
|
|
||||||
</span>
|
|
||||||
{VIEW_DATASET_MESSAGE}
|
{VIEW_DATASET_MESSAGE}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
|
@ -35,7 +35,7 @@ jest.mock('react-router-dom', () => ({
|
||||||
|
|
||||||
describe('DatasetLayout', () => {
|
describe('DatasetLayout', () => {
|
||||||
it('renders nothing when no components are passed in', () => {
|
it('renders nothing when no components are passed in', () => {
|
||||||
render(<DatasetLayout />);
|
render(<DatasetLayout />, { useRouter: true });
|
||||||
const layoutWrapper = screen.getByTestId('dataset-layout-wrapper');
|
const layoutWrapper = screen.getByTestId('dataset-layout-wrapper');
|
||||||
|
|
||||||
expect(layoutWrapper).toHaveTextContent('');
|
expect(layoutWrapper).toHaveTextContent('');
|
||||||
|
@ -55,7 +55,7 @@ describe('DatasetLayout', () => {
|
||||||
it('renders a LeftPanel when passed in', async () => {
|
it('renders a LeftPanel when passed in', async () => {
|
||||||
render(
|
render(
|
||||||
<DatasetLayout leftPanel={<LeftPanel setDataset={() => null} />} />,
|
<DatasetLayout leftPanel={<LeftPanel setDataset={() => null} />} />,
|
||||||
{ useRedux: true },
|
{ useRedux: true, useRouter: true },
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(
|
expect(
|
||||||
|
@ -65,7 +65,9 @@ describe('DatasetLayout', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders a DatasetPanel when passed in', () => {
|
it('renders a DatasetPanel when passed in', () => {
|
||||||
render(<DatasetLayout datasetPanel={<DatasetPanel />} />);
|
render(<DatasetLayout datasetPanel={<DatasetPanel />} />, {
|
||||||
|
useRouter: true,
|
||||||
|
});
|
||||||
|
|
||||||
const blankDatasetImg = screen.getByRole('img', { name: /empty/i });
|
const blankDatasetImg = screen.getByRole('img', { name: /empty/i });
|
||||||
const blankDatasetTitle = screen.getByText(/select dataset source/i);
|
const blankDatasetTitle = screen.getByText(/select dataset source/i);
|
||||||
|
@ -75,13 +77,16 @@ describe('DatasetLayout', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders a RightPanel when passed in', () => {
|
it('renders a RightPanel when passed in', () => {
|
||||||
render(<DatasetLayout rightPanel={RightPanel()} />);
|
render(<DatasetLayout rightPanel={RightPanel()} />, { useRouter: true });
|
||||||
|
|
||||||
expect(screen.getByText(/right panel/i)).toBeVisible();
|
expect(screen.getByText(/right panel/i)).toBeVisible();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders a Footer when passed in', () => {
|
it('renders a Footer when passed in', () => {
|
||||||
render(<DatasetLayout footer={<Footer url="" />} />, { useRedux: true });
|
render(<DatasetLayout footer={<Footer url="" />} />, {
|
||||||
|
useRedux: true,
|
||||||
|
useRouter: true,
|
||||||
|
});
|
||||||
|
|
||||||
expect(screen.getByText(/Cancel/i)).toBeVisible();
|
expect(screen.getByText(/Cancel/i)).toBeVisible();
|
||||||
});
|
});
|
||||||
|
|
|
@ -105,7 +105,7 @@ const getEntityIcon = (entity: ActivityObject) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const getEntityUrl = (entity: ActivityObject) => {
|
const getEntityUrl = (entity: ActivityObject) => {
|
||||||
if ('sql' in entity) return `/superset/sqllab?savedQueryId=${entity.id}`;
|
if ('sql' in entity) return `/sqllab?savedQueryId=${entity.id}`;
|
||||||
if ('url' in entity) return entity.url;
|
if ('url' in entity) return entity.url;
|
||||||
return entity.item_url;
|
return entity.item_url;
|
||||||
};
|
};
|
||||||
|
|
|
@ -17,6 +17,7 @@
|
||||||
* under the License.
|
* under the License.
|
||||||
*/
|
*/
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import { Link } from 'react-router-dom';
|
||||||
import Button from 'src/components/Button';
|
import Button from 'src/components/Button';
|
||||||
import { Empty } from 'src/components';
|
import { Empty } from 'src/components';
|
||||||
import { TableTab } from 'src/views/CRUD/types';
|
import { TableTab } from 'src/views/CRUD/types';
|
||||||
|
@ -81,7 +82,7 @@ export default function EmptyState({
|
||||||
const mineRedirects: Redirects = {
|
const mineRedirects: Redirects = {
|
||||||
[WelcomeTable.Charts]: '/chart/add',
|
[WelcomeTable.Charts]: '/chart/add',
|
||||||
[WelcomeTable.Dashboards]: '/dashboard/new',
|
[WelcomeTable.Dashboards]: '/dashboard/new',
|
||||||
[WelcomeTable.SavedQueries]: '/superset/sqllab?new=true',
|
[WelcomeTable.SavedQueries]: '/sqllab?new=true',
|
||||||
};
|
};
|
||||||
const favRedirects: Redirects = {
|
const favRedirects: Redirects = {
|
||||||
[WelcomeTable.Charts]: '/chart/list',
|
[WelcomeTable.Charts]: '/chart/list',
|
||||||
|
@ -140,20 +141,17 @@ export default function EmptyState({
|
||||||
>
|
>
|
||||||
{tableName !== WelcomeTable.Recents && (
|
{tableName !== WelcomeTable.Recents && (
|
||||||
<ButtonContainer>
|
<ButtonContainer>
|
||||||
<Button
|
<Link to={mineRedirects[tableName]}>
|
||||||
buttonStyle="primary"
|
<Button buttonStyle="primary">
|
||||||
onClick={() => {
|
<i className="fa fa-plus" />
|
||||||
window.location.href = mineRedirects[tableName];
|
{tableName === WelcomeTable.SavedQueries
|
||||||
}}
|
? t('SQL query')
|
||||||
>
|
: tableName
|
||||||
<i className="fa fa-plus" />
|
.split('')
|
||||||
{tableName === WelcomeTable.SavedQueries
|
.slice(0, tableName.length - 1)
|
||||||
? t('SQL query')
|
.join('')}
|
||||||
: tableName
|
</Button>
|
||||||
.split('')
|
</Link>
|
||||||
.slice(0, tableName.length - 1)
|
|
||||||
.join('')}
|
|
||||||
</Button>
|
|
||||||
</ButtonContainer>
|
</ButtonContainer>
|
||||||
)}
|
)}
|
||||||
</Empty>
|
</Empty>
|
||||||
|
|
|
@ -62,7 +62,7 @@ const dropdownItems = [
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'SQL query',
|
label: 'SQL query',
|
||||||
url: '/superset/sqllab?new=true',
|
url: '/sqllab?new=true',
|
||||||
icon: 'fa-fw fa-search',
|
icon: 'fa-fw fa-search',
|
||||||
perm: 'can_sqllab',
|
perm: 'can_sqllab',
|
||||||
view: 'Superset',
|
view: 'Superset',
|
||||||
|
|
|
@ -73,7 +73,7 @@ const dropdownItems = [
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'SQL query',
|
label: 'SQL query',
|
||||||
url: '/superset/sqllab?new=true',
|
url: '/sqllab?new=true',
|
||||||
icon: 'fa-fw fa-search',
|
icon: 'fa-fw fa-search',
|
||||||
perm: 'can_sqllab',
|
perm: 'can_sqllab',
|
||||||
view: 'Superset',
|
view: 'Superset',
|
||||||
|
|
|
@ -210,7 +210,7 @@ const RightMenu = ({
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: t('SQL query'),
|
label: t('SQL query'),
|
||||||
url: '/superset/sqllab?new=true',
|
url: '/sqllab?new=true',
|
||||||
icon: 'fa-fw fa-search',
|
icon: 'fa-fw fa-search',
|
||||||
perm: 'can_sqllab',
|
perm: 'can_sqllab',
|
||||||
view: 'Superset',
|
view: 'Superset',
|
||||||
|
|
|
@ -17,6 +17,7 @@
|
||||||
* under the License.
|
* under the License.
|
||||||
*/
|
*/
|
||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
|
import { Link } from 'react-router-dom';
|
||||||
import { styled, SupersetClient, t, useTheme } from '@superset-ui/core';
|
import { styled, SupersetClient, t, useTheme } from '@superset-ui/core';
|
||||||
import SyntaxHighlighter from 'react-syntax-highlighter/dist/cjs/light';
|
import SyntaxHighlighter from 'react-syntax-highlighter/dist/cjs/light';
|
||||||
import sql from 'react-syntax-highlighter/dist/cjs/languages/hljs/sql';
|
import sql from 'react-syntax-highlighter/dist/cjs/languages/hljs/sql';
|
||||||
|
@ -193,12 +194,8 @@ const SavedQueries = ({
|
||||||
const renderMenu = (query: Query) => (
|
const renderMenu = (query: Query) => (
|
||||||
<Menu>
|
<Menu>
|
||||||
{canEdit && (
|
{canEdit && (
|
||||||
<Menu.Item
|
<Menu.Item>
|
||||||
onClick={() => {
|
<Link to={`/sqllab?savedQueryId=${query.id}`}>{t('Edit')}</Link>
|
||||||
window.location.href = `/superset/sqllab?savedQueryId=${query.id}`;
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{t('Edit')}
|
|
||||||
</Menu.Item>
|
</Menu.Item>
|
||||||
)}
|
)}
|
||||||
<Menu.Item
|
<Menu.Item
|
||||||
|
@ -256,15 +253,12 @@ const SavedQueries = ({
|
||||||
buttons={[
|
buttons={[
|
||||||
{
|
{
|
||||||
name: (
|
name: (
|
||||||
<>
|
<Link to="/sqllab?new=true">
|
||||||
<i className="fa fa-plus" />
|
<i className="fa fa-plus" />
|
||||||
{t('SQL Query')}
|
{t('SQL Query')}
|
||||||
</>
|
</Link>
|
||||||
),
|
),
|
||||||
buttonStyle: 'tertiary',
|
buttonStyle: 'tertiary',
|
||||||
onClick: () => {
|
|
||||||
window.location.href = '/superset/sqllab?new=true';
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: t('View All »'),
|
name: t('View All »'),
|
||||||
|
@ -278,15 +272,10 @@ const SavedQueries = ({
|
||||||
{queries.length > 0 ? (
|
{queries.length > 0 ? (
|
||||||
<CardContainer showThumbnails={showThumbnails}>
|
<CardContainer showThumbnails={showThumbnails}>
|
||||||
{queries.map(q => (
|
{queries.map(q => (
|
||||||
<CardStyles
|
<CardStyles key={q.id}>
|
||||||
onClick={() => {
|
|
||||||
window.location.href = `/superset/sqllab?savedQueryId=${q.id}`;
|
|
||||||
}}
|
|
||||||
key={q.id}
|
|
||||||
>
|
|
||||||
<ListViewCard
|
<ListViewCard
|
||||||
imgURL=""
|
imgURL=""
|
||||||
url={`/superset/sqllab?savedQueryId=${q.id}`}
|
url={`/sqllab?savedQueryId=${q.id}`}
|
||||||
title={q.label}
|
title={q.label}
|
||||||
imgFallbackURL="/static/assets/images/empty-query.svg"
|
imgFallbackURL="/static/assets/images/empty-query.svg"
|
||||||
description={t('Ran %s', q.changed_on_delta_humanized)}
|
description={t('Ran %s', q.changed_on_delta_humanized)}
|
||||||
|
|
|
@ -180,7 +180,7 @@ type MenuChild = {
|
||||||
|
|
||||||
export interface ButtonProps {
|
export interface ButtonProps {
|
||||||
name: ReactNode;
|
name: ReactNode;
|
||||||
onClick: OnClickHandler;
|
onClick?: OnClickHandler;
|
||||||
'data-test'?: string;
|
'data-test'?: string;
|
||||||
buttonStyle:
|
buttonStyle:
|
||||||
| 'primary'
|
| 'primary'
|
||||||
|
|
|
@ -30,7 +30,7 @@ export const commonMenuData = {
|
||||||
{
|
{
|
||||||
name: 'Query history',
|
name: 'Query history',
|
||||||
label: t('Query history'),
|
label: t('Query history'),
|
||||||
url: '/superset/sqllab/history/',
|
url: '/sqllab/history/',
|
||||||
usesRouter: true,
|
usesRouter: true,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|
|
@ -17,7 +17,10 @@
|
||||||
* under the License.
|
* under the License.
|
||||||
*/
|
*/
|
||||||
import rison from 'rison';
|
import rison from 'rison';
|
||||||
import { getClientErrorObject } from 'src/utils/getClientErrorObject';
|
import {
|
||||||
|
ClientErrorObject,
|
||||||
|
getClientErrorObject,
|
||||||
|
} from 'src/utils/getClientErrorObject';
|
||||||
import { createApi, BaseQueryFn } from '@reduxjs/toolkit/query/react';
|
import { createApi, BaseQueryFn } from '@reduxjs/toolkit/query/react';
|
||||||
import {
|
import {
|
||||||
SupersetClient,
|
SupersetClient,
|
||||||
|
@ -35,7 +38,9 @@ export const supersetClientQuery: BaseQueryFn<
|
||||||
parseMethod?: ParseMethod;
|
parseMethod?: ParseMethod;
|
||||||
transformResponse?: (response: SupersetClientResponse) => JsonValue;
|
transformResponse?: (response: SupersetClientResponse) => JsonValue;
|
||||||
urlParams?: Record<string, number | string | undefined | boolean>;
|
urlParams?: Record<string, number | string | undefined | boolean>;
|
||||||
}
|
},
|
||||||
|
JsonValue,
|
||||||
|
ClientErrorObject
|
||||||
> = (
|
> = (
|
||||||
{
|
{
|
||||||
endpoint,
|
endpoint,
|
||||||
|
|
|
@ -31,7 +31,7 @@ jest.mock('react-router-dom', () => ({
|
||||||
|
|
||||||
describe('AddDataset', () => {
|
describe('AddDataset', () => {
|
||||||
it('renders a blank state AddDataset', async () => {
|
it('renders a blank state AddDataset', async () => {
|
||||||
render(<AddDataset />, { useRedux: true });
|
render(<AddDataset />, { useRedux: true, useRouter: true });
|
||||||
|
|
||||||
const blankeStateImgs = screen.getAllByRole('img', { name: /empty/i });
|
const blankeStateImgs = screen.getAllByRole('img', { name: /empty/i });
|
||||||
|
|
||||||
|
|
|
@ -17,6 +17,7 @@
|
||||||
* under the License.
|
* under the License.
|
||||||
*/
|
*/
|
||||||
import React, { useMemo, useState, useCallback, ReactElement } from 'react';
|
import React, { useMemo, useState, useCallback, ReactElement } from 'react';
|
||||||
|
import { Link, useHistory } from 'react-router-dom';
|
||||||
import {
|
import {
|
||||||
QueryState,
|
QueryState,
|
||||||
styled,
|
styled,
|
||||||
|
@ -102,6 +103,7 @@ function QueryList({ addDangerToast }: QueryListProps) {
|
||||||
useState<QueryObject>();
|
useState<QueryObject>();
|
||||||
|
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
|
const history = useHistory();
|
||||||
|
|
||||||
const handleQueryPreview = useCallback(
|
const handleQueryPreview = useCallback(
|
||||||
(id: number) => {
|
(id: number) => {
|
||||||
|
@ -334,9 +336,9 @@ function QueryList({ addDangerToast }: QueryListProps) {
|
||||||
},
|
},
|
||||||
}: any) => (
|
}: any) => (
|
||||||
<Tooltip title={t('Open query in SQL Lab')} placement="bottom">
|
<Tooltip title={t('Open query in SQL Lab')} placement="bottom">
|
||||||
<a href={`/superset/sqllab?queryId=${id}`}>
|
<Link to={`/sqllab?queryId=${id}`}>
|
||||||
<Icons.Full iconColor={theme.colors.grayscale.base} />
|
<Icons.Full iconColor={theme.colors.grayscale.base} />
|
||||||
</a>
|
</Link>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
|
@ -427,9 +429,7 @@ function QueryList({ addDangerToast }: QueryListProps) {
|
||||||
query={queryCurrentlyPreviewing}
|
query={queryCurrentlyPreviewing}
|
||||||
queries={queries}
|
queries={queries}
|
||||||
fetchData={handleQueryPreview}
|
fetchData={handleQueryPreview}
|
||||||
openInSqlLab={(id: number) =>
|
openInSqlLab={(id: number) => history.push(`/sqllab?queryId=${id}`)}
|
||||||
window.location.assign(`/superset/sqllab?queryId=${id}`)
|
|
||||||
}
|
|
||||||
show
|
show
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -25,6 +25,7 @@ import {
|
||||||
t,
|
t,
|
||||||
} from '@superset-ui/core';
|
} from '@superset-ui/core';
|
||||||
import React, { useState, useMemo, useCallback } from 'react';
|
import React, { useState, useMemo, useCallback } from 'react';
|
||||||
|
import { Link, useHistory } from 'react-router-dom';
|
||||||
import rison from 'rison';
|
import rison from 'rison';
|
||||||
import moment from 'moment';
|
import moment from 'moment';
|
||||||
import {
|
import {
|
||||||
|
@ -127,6 +128,7 @@ function SavedQueryList({
|
||||||
sshTunnelPrivateKeyPasswordFields,
|
sshTunnelPrivateKeyPasswordFields,
|
||||||
setSSHTunnelPrivateKeyPasswordFields,
|
setSSHTunnelPrivateKeyPasswordFields,
|
||||||
] = useState<string[]>([]);
|
] = useState<string[]>([]);
|
||||||
|
const history = useHistory();
|
||||||
|
|
||||||
const openSavedQueryImportModal = () => {
|
const openSavedQueryImportModal = () => {
|
||||||
showImportModal(true);
|
showImportModal(true);
|
||||||
|
@ -148,10 +150,6 @@ function SavedQueryList({
|
||||||
const canExport =
|
const canExport =
|
||||||
hasPerm('can_export') && isFeatureEnabled(FeatureFlag.VERSIONED_EXPORT);
|
hasPerm('can_export') && isFeatureEnabled(FeatureFlag.VERSIONED_EXPORT);
|
||||||
|
|
||||||
const openNewQuery = () => {
|
|
||||||
window.open(`${window.location.origin}/superset/sqllab?new=true`);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleSavedQueryPreview = useCallback(
|
const handleSavedQueryPreview = useCallback(
|
||||||
(id: number) => {
|
(id: number) => {
|
||||||
SupersetClient.get({
|
SupersetClient.get({
|
||||||
|
@ -187,11 +185,10 @@ function SavedQueryList({
|
||||||
|
|
||||||
subMenuButtons.push({
|
subMenuButtons.push({
|
||||||
name: (
|
name: (
|
||||||
<>
|
<Link to="/sqllab?new=true">
|
||||||
<i className="fa fa-plus" /> {t('Query')}
|
<i className="fa fa-plus" /> {t('Query')}
|
||||||
</>
|
</Link>
|
||||||
),
|
),
|
||||||
onClick: openNewQuery,
|
|
||||||
buttonStyle: 'primary',
|
buttonStyle: 'primary',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -217,15 +214,13 @@ function SavedQueryList({
|
||||||
|
|
||||||
// Action methods
|
// Action methods
|
||||||
const openInSqlLab = (id: number) => {
|
const openInSqlLab = (id: number) => {
|
||||||
window.open(`${window.location.origin}/superset/sqllab?savedQueryId=${id}`);
|
history.push(`/sqllab?savedQueryId=${id}`);
|
||||||
};
|
};
|
||||||
|
|
||||||
const copyQueryLink = useCallback(
|
const copyQueryLink = useCallback(
|
||||||
(id: number) => {
|
(id: number) => {
|
||||||
copyTextToClipboard(() =>
|
copyTextToClipboard(() =>
|
||||||
Promise.resolve(
|
Promise.resolve(`${window.location.origin}/sqllab?savedQueryId=${id}`),
|
||||||
`${window.location.origin}/superset/sqllab?savedQueryId=${id}`,
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
addSuccessToast(t('Link Copied!'));
|
addSuccessToast(t('Link Copied!'));
|
||||||
|
|
|
@ -16,17 +16,26 @@
|
||||||
* specific language governing permissions and limitations
|
* specific language governing permissions and limitations
|
||||||
* under the License.
|
* under the License.
|
||||||
*/
|
*/
|
||||||
import { combineReducers } from 'redux';
|
|
||||||
import messageToasts from 'src/components/MessageToasts/reducers';
|
|
||||||
import sqlLab from './sqlLab';
|
|
||||||
import localStorageUsageInKilobytes from './localStorageUsage';
|
|
||||||
import common from './common';
|
|
||||||
|
|
||||||
export const reducers = {
|
import React, { createContext, useContext } from 'react';
|
||||||
sqlLab,
|
import { useLocation } from 'react-router-dom';
|
||||||
localStorageUsageInKilobytes,
|
|
||||||
messageToasts,
|
export type LocationState = {
|
||||||
common,
|
requestedQuery?: Record<string, any>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default combineReducers(reducers);
|
export const locationContext = createContext<LocationState>({});
|
||||||
|
const { Provider } = locationContext;
|
||||||
|
|
||||||
|
const EMPTY_STATE: LocationState = {};
|
||||||
|
|
||||||
|
export const LocationProvider: React.FC = ({
|
||||||
|
children,
|
||||||
|
}: {
|
||||||
|
children: React.ReactNode;
|
||||||
|
}) => {
|
||||||
|
const location = useLocation<LocationState>();
|
||||||
|
return <Provider value={location.state || EMPTY_STATE}>{children}</Provider>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useLocationState = () => useContext(locationContext);
|
|
@ -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 React from 'react';
|
||||||
|
import { omit } from 'lodash';
|
||||||
|
import {
|
||||||
|
render,
|
||||||
|
act,
|
||||||
|
waitFor,
|
||||||
|
defaultStore as store,
|
||||||
|
createStore,
|
||||||
|
} from 'spec/helpers/testing-library';
|
||||||
|
import reducers from 'spec/helpers/reducerIndex';
|
||||||
|
import { api } from 'src/hooks/apiResources/queryApi';
|
||||||
|
import { DEFAULT_COMMON_BOOTSTRAP_DATA } from 'src/constants';
|
||||||
|
import getInitialState from 'src/SqlLab/reducers/getInitialState';
|
||||||
|
|
||||||
|
import SqlLab from '.';
|
||||||
|
|
||||||
|
const fakeApiResult = {
|
||||||
|
result: {
|
||||||
|
common: DEFAULT_COMMON_BOOTSTRAP_DATA,
|
||||||
|
tab_state_ids: [],
|
||||||
|
databases: [],
|
||||||
|
queries: {},
|
||||||
|
user: {
|
||||||
|
userId: 1,
|
||||||
|
username: 'some name',
|
||||||
|
isActive: true,
|
||||||
|
isAnonymous: false,
|
||||||
|
firstName: 'first name',
|
||||||
|
lastName: 'last name',
|
||||||
|
permissions: {},
|
||||||
|
roles: {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const expectedResult = fakeApiResult.result;
|
||||||
|
const sqlLabInitialStateApiRoute = `glob:*/api/v1/sqllab/`;
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
fetchMock.reset();
|
||||||
|
act(() => {
|
||||||
|
store.dispatch(api.util.resetApiState());
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fetchMock.get(sqlLabInitialStateApiRoute, fakeApiResult);
|
||||||
|
});
|
||||||
|
|
||||||
|
jest.mock('src/SqlLab/components/App', () => () => (
|
||||||
|
<div data-test="mock-sqllab-app" />
|
||||||
|
));
|
||||||
|
|
||||||
|
test('is valid', () => {
|
||||||
|
expect(React.isValidElement(<SqlLab />)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('fetches initial data and renders', async () => {
|
||||||
|
expect(fetchMock.calls(sqlLabInitialStateApiRoute).length).toBe(0);
|
||||||
|
const storeWithSqlLab = createStore({}, reducers);
|
||||||
|
const { getByTestId } = render(<SqlLab />, {
|
||||||
|
useRedux: true,
|
||||||
|
useRouter: true,
|
||||||
|
store: storeWithSqlLab,
|
||||||
|
});
|
||||||
|
|
||||||
|
await waitFor(() =>
|
||||||
|
expect(fetchMock.calls(sqlLabInitialStateApiRoute).length).toBe(1),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(getByTestId('mock-sqllab-app')).toBeInTheDocument();
|
||||||
|
const { sqlLab } = getInitialState(expectedResult);
|
||||||
|
expect(storeWithSqlLab.getState()).toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
sqlLab: expect.objectContaining(
|
||||||
|
omit(sqlLab, ['queriesLastUpdate', 'editorTabLastUpdatedAt']),
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
|
@ -0,0 +1,78 @@
|
||||||
|
/**
|
||||||
|
* 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, { useEffect } from 'react';
|
||||||
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
|
import { css } 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';
|
||||||
|
import { addDangerToast } from 'src/components/MessageToasts/actions';
|
||||||
|
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 useEffectEvent from 'src/hooks/useEffectEvent';
|
||||||
|
import { LocationProvider } from './LocationContext';
|
||||||
|
|
||||||
|
export default function SqlLab() {
|
||||||
|
const editorTabLastUpdatedAt = useSelector<SqlLabRootState, number>(
|
||||||
|
state => state.sqlLab.editorTabLastUpdatedAt || 0,
|
||||||
|
);
|
||||||
|
const { data, isLoading, isError, error, fulfilledTimeStamp } =
|
||||||
|
useSqlLabInitialState();
|
||||||
|
const shouldInitialize = editorTabLastUpdatedAt <= (fulfilledTimeStamp || 0);
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
|
const initBootstrapData = useEffectEvent(
|
||||||
|
(sqlLabInitialState: InitialState) => {
|
||||||
|
if (shouldInitialize) {
|
||||||
|
dispatch(resetState(sqlLabInitialState));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (data) {
|
||||||
|
initBootstrapData(data);
|
||||||
|
}
|
||||||
|
}, [data, initBootstrapData]);
|
||||||
|
|
||||||
|
if (isLoading || shouldInitialize) return <Loading />;
|
||||||
|
|
||||||
|
if (isError && error?.message) {
|
||||||
|
dispatch(addDangerToast(error?.message));
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<LocationProvider>
|
||||||
|
<div
|
||||||
|
css={css`
|
||||||
|
flex: 1 1 auto;
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
`}
|
||||||
|
>
|
||||||
|
<SqlLabGlobalStyles />
|
||||||
|
<App />
|
||||||
|
</div>
|
||||||
|
</LocationProvider>
|
||||||
|
);
|
||||||
|
}
|
|
@ -674,9 +674,7 @@ export const copyQueryLink = (
|
||||||
addSuccessToast: (arg0: string) => void,
|
addSuccessToast: (arg0: string) => void,
|
||||||
) => {
|
) => {
|
||||||
copyTextToClipboard(() =>
|
copyTextToClipboard(() =>
|
||||||
Promise.resolve(
|
Promise.resolve(`${window.location.origin}/sqllab?savedQueryId=${id}`),
|
||||||
`${window.location.origin}/superset/sqllab?savedQueryId=${id}`,
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
addSuccessToast(t('Link Copied!'));
|
addSuccessToast(t('Link Copied!'));
|
||||||
|
|
|
@ -104,6 +104,10 @@ const SavedQueryList = lazy(
|
||||||
import(/* webpackChunkName: "SavedQueryList" */ 'src/pages/SavedQueryList'),
|
import(/* webpackChunkName: "SavedQueryList" */ 'src/pages/SavedQueryList'),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const SqlLab = lazy(
|
||||||
|
() => import(/* webpackChunkName: "SqlLab" */ 'src/pages/SqlLab'),
|
||||||
|
);
|
||||||
|
|
||||||
const AllEntities = lazy(
|
const AllEntities = lazy(
|
||||||
() => import(/* webpackChunkName: "AllEntities" */ 'src/pages/AllEntities'),
|
() => import(/* webpackChunkName: "AllEntities" */ 'src/pages/AllEntities'),
|
||||||
);
|
);
|
||||||
|
@ -176,7 +180,7 @@ export const routes: Routes = [
|
||||||
Component: AnnotationList,
|
Component: AnnotationList,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/superset/sqllab/history/',
|
path: '/sqllab/history/',
|
||||||
Component: QueryHistoryList,
|
Component: QueryHistoryList,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -225,6 +229,10 @@ export const routes: Routes = [
|
||||||
path: '/profile',
|
path: '/profile',
|
||||||
Component: Profile,
|
Component: Profile,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: '/sqllab/',
|
||||||
|
Component: SqlLab,
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
if (isFeatureEnabled(FeatureFlag.TAGGING_SYSTEM)) {
|
if (isFeatureEnabled(FeatureFlag.TAGGING_SYSTEM)) {
|
||||||
|
|
|
@ -211,7 +211,6 @@ const config = {
|
||||||
menu: addPreamble('src/views/menu.tsx'),
|
menu: addPreamble('src/views/menu.tsx'),
|
||||||
spa: addPreamble('/src/views/index.tsx'),
|
spa: addPreamble('/src/views/index.tsx'),
|
||||||
embedded: addPreamble('/src/embedded/index.tsx'),
|
embedded: addPreamble('/src/embedded/index.tsx'),
|
||||||
sqllab: addPreamble('/src/SqlLab/index.tsx'),
|
|
||||||
},
|
},
|
||||||
output,
|
output,
|
||||||
stats: 'minimal',
|
stats: 'minimal',
|
||||||
|
|
|
@ -192,6 +192,7 @@ class SupersetAppInitializer: # pylint: disable=too-many-public-methods
|
||||||
TableSchemaView,
|
TableSchemaView,
|
||||||
TabStateView,
|
TabStateView,
|
||||||
)
|
)
|
||||||
|
from superset.views.sqllab import SqllabView
|
||||||
from superset.views.tags import TagModelView, TagView
|
from superset.views.tags import TagModelView, TagView
|
||||||
from superset.views.users.api import CurrentUserRestApi
|
from superset.views.users.api import CurrentUserRestApi
|
||||||
|
|
||||||
|
@ -316,6 +317,7 @@ class SupersetAppInitializer: # pylint: disable=too-many-public-methods
|
||||||
appbuilder.add_view_no_menu(SavedQueryViewApi)
|
appbuilder.add_view_no_menu(SavedQueryViewApi)
|
||||||
appbuilder.add_view_no_menu(SliceAsync)
|
appbuilder.add_view_no_menu(SliceAsync)
|
||||||
appbuilder.add_view_no_menu(SqlLab)
|
appbuilder.add_view_no_menu(SqlLab)
|
||||||
|
appbuilder.add_view_no_menu(SqllabView)
|
||||||
appbuilder.add_view_no_menu(SqlMetricInlineView)
|
appbuilder.add_view_no_menu(SqlMetricInlineView)
|
||||||
appbuilder.add_view_no_menu(Superset)
|
appbuilder.add_view_no_menu(Superset)
|
||||||
appbuilder.add_view_no_menu(TableColumnInlineView)
|
appbuilder.add_view_no_menu(TableColumnInlineView)
|
||||||
|
@ -347,7 +349,7 @@ class SupersetAppInitializer: # pylint: disable=too-many-public-methods
|
||||||
appbuilder.add_link(
|
appbuilder.add_link(
|
||||||
"SQL Editor",
|
"SQL Editor",
|
||||||
label=__("SQL Lab"),
|
label=__("SQL Lab"),
|
||||||
href="/superset/sqllab/",
|
href="/sqllab/",
|
||||||
category_icon="fa-flask",
|
category_icon="fa-flask",
|
||||||
icon="fa-flask",
|
icon="fa-flask",
|
||||||
category="SQL Lab",
|
category="SQL Lab",
|
||||||
|
@ -364,7 +366,7 @@ class SupersetAppInitializer: # pylint: disable=too-many-public-methods
|
||||||
appbuilder.add_link(
|
appbuilder.add_link(
|
||||||
"Query Search",
|
"Query Search",
|
||||||
label=__("Query History"),
|
label=__("Query History"),
|
||||||
href="/superset/sqllab/history/",
|
href="/sqllab/history/",
|
||||||
icon="fa-search",
|
icon="fa-search",
|
||||||
category_icon="fa-flask",
|
category_icon="fa-flask",
|
||||||
category="SQL Lab",
|
category="SQL Lab",
|
||||||
|
|
|
@ -157,7 +157,7 @@ class ExtraCache:
|
||||||
|
|
||||||
When in SQL Lab, it's possible to add arbitrary URL "query string" parameters,
|
When in SQL Lab, it's possible to add arbitrary URL "query string" parameters,
|
||||||
and use those in your SQL code. For instance you can alter your url and add
|
and use those in your SQL code. For instance you can alter your url and add
|
||||||
`?foo=bar`, as in `{domain}/superset/sqllab?foo=bar`. Then if your query is
|
`?foo=bar`, as in `{domain}/sqllab?foo=bar`. Then if your query is
|
||||||
something like SELECT * FROM foo = '{{ url_param('foo') }}', it will be parsed
|
something like SELECT * FROM foo = '{{ url_param('foo') }}', it will be parsed
|
||||||
at runtime and replaced by the value in the URL.
|
at runtime and replaced by the value in the URL.
|
||||||
|
|
||||||
|
|
|
@ -492,7 +492,7 @@ class Database(
|
||||||
source = utils.QuerySource.DASHBOARD
|
source = utils.QuerySource.DASHBOARD
|
||||||
elif "/explore/" in request.referrer:
|
elif "/explore/" in request.referrer:
|
||||||
source = utils.QuerySource.CHART
|
source = utils.QuerySource.CHART
|
||||||
elif "/superset/sqllab" in request.referrer:
|
elif "/sqllab/" in request.referrer:
|
||||||
source = utils.QuerySource.SQL_LAB
|
source = utils.QuerySource.SQL_LAB
|
||||||
|
|
||||||
sqlalchemy_url, params = DB_CONNECTION_MUTATOR(
|
sqlalchemy_url, params = DB_CONNECTION_MUTATOR(
|
||||||
|
|
|
@ -408,7 +408,7 @@ class SavedQuery(Model, AuditMixinNullable, ExtraJSONMixin, ImportExportMixin):
|
||||||
def pop_tab_link(self) -> Markup:
|
def pop_tab_link(self) -> Markup:
|
||||||
return Markup(
|
return Markup(
|
||||||
f"""
|
f"""
|
||||||
<a href="/superset/sqllab?savedQueryId={self.id}">
|
<a href="/sqllab?savedQueryId={self.id}">
|
||||||
<i class="fa fa-link"></i>
|
<i class="fa fa-link"></i>
|
||||||
</a>
|
</a>
|
||||||
"""
|
"""
|
||||||
|
@ -423,7 +423,7 @@ class SavedQuery(Model, AuditMixinNullable, ExtraJSONMixin, ImportExportMixin):
|
||||||
return self.database.sqlalchemy_uri
|
return self.database.sqlalchemy_uri
|
||||||
|
|
||||||
def url(self) -> str:
|
def url(self) -> str:
|
||||||
return f"/superset/sqllab?savedQueryId={self.id}"
|
return f"/sqllab?savedQueryId={self.id}"
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def sql_tables(self) -> list[Table]:
|
def sql_tables(self) -> list[Table]:
|
||||||
|
|
|
@ -25,6 +25,7 @@ from flask_appbuilder.models.sqla.interface import SQLAInterface
|
||||||
from marshmallow import ValidationError
|
from marshmallow import ValidationError
|
||||||
|
|
||||||
from superset import app, is_feature_enabled
|
from superset import app, is_feature_enabled
|
||||||
|
from superset.constants import MODEL_API_RW_METHOD_PERMISSION_MAP
|
||||||
from superset.daos.database import DatabaseDAO
|
from superset.daos.database import DatabaseDAO
|
||||||
from superset.daos.query import QueryDAO
|
from superset.daos.query import QueryDAO
|
||||||
from superset.extensions import event_logger
|
from superset.extensions import event_logger
|
||||||
|
@ -67,6 +68,7 @@ logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class SqlLabRestApi(BaseSupersetApi):
|
class SqlLabRestApi(BaseSupersetApi):
|
||||||
|
method_permission_name = MODEL_API_RW_METHOD_PERMISSION_MAP
|
||||||
datamodel = SQLAInterface(Query)
|
datamodel = SQLAInterface(Query)
|
||||||
|
|
||||||
resource_name = "sqllab"
|
resource_name = "sqllab"
|
||||||
|
|
|
@ -72,7 +72,6 @@ from superset.models.dashboard import Dashboard
|
||||||
from superset.models.slice import Slice
|
from superset.models.slice import Slice
|
||||||
from superset.models.sql_lab import Query
|
from superset.models.sql_lab import Query
|
||||||
from superset.models.user_attributes import UserAttribute
|
from superset.models.user_attributes import UserAttribute
|
||||||
from superset.sqllab.utils import bootstrap_sqllab_data
|
|
||||||
from superset.superset_typing import FlaskResponse
|
from superset.superset_typing import FlaskResponse
|
||||||
from superset.utils import core as utils
|
from superset.utils import core as utils
|
||||||
from superset.utils.cache import etag_cache
|
from superset.utils.cache import etag_cache
|
||||||
|
@ -982,28 +981,18 @@ class Superset(BaseSupersetView): # pylint: disable=too-many-public-methods
|
||||||
"POST",
|
"POST",
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
@deprecated(new_target="/sqllab")
|
||||||
def sqllab(self) -> FlaskResponse:
|
def sqllab(self) -> FlaskResponse:
|
||||||
"""SQL Editor"""
|
"""SQL Editor"""
|
||||||
payload = {
|
url = "/sqllab"
|
||||||
"common": common_bootstrap_payload(g.user),
|
if url_params := request.args:
|
||||||
**bootstrap_sqllab_data(get_user_id()),
|
params = parse.urlencode(url_params)
|
||||||
}
|
url = f"{url}?{params}"
|
||||||
|
return redirect(url)
|
||||||
if form_data := request.form.get("form_data"):
|
|
||||||
with contextlib.suppress(json.JSONDecodeError):
|
|
||||||
payload["requested_query"] = json.loads(form_data)
|
|
||||||
payload["user"] = bootstrap_user_data(g.user, include_perms=True)
|
|
||||||
bootstrap_data = json.dumps(
|
|
||||||
payload, default=utils.pessimistic_json_iso_dttm_ser
|
|
||||||
)
|
|
||||||
|
|
||||||
return self.render_template(
|
|
||||||
"superset/basic.html", entry="sqllab", bootstrap_data=bootstrap_data
|
|
||||||
)
|
|
||||||
|
|
||||||
@has_access
|
@has_access
|
||||||
@event_logger.log_this
|
@event_logger.log_this
|
||||||
@expose("/sqllab/history/", methods=("GET",))
|
@expose("/sqllab/history/", methods=("GET",))
|
||||||
@event_logger.log_this
|
@deprecated(new_target="/sqllab/history")
|
||||||
def sqllab_history(self) -> FlaskResponse:
|
def sqllab_history(self) -> FlaskResponse:
|
||||||
return super().render_app_template()
|
return redirect("/sqllab/history")
|
||||||
|
|
|
@ -0,0 +1,46 @@
|
||||||
|
# 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.
|
||||||
|
from flask_appbuilder import permission_name
|
||||||
|
from flask_appbuilder.api import expose
|
||||||
|
from flask_appbuilder.security.decorators import has_access
|
||||||
|
|
||||||
|
from superset import event_logger
|
||||||
|
from superset.constants import MODEL_API_RW_METHOD_PERMISSION_MAP
|
||||||
|
from superset.superset_typing import FlaskResponse
|
||||||
|
|
||||||
|
from .base import BaseSupersetView
|
||||||
|
|
||||||
|
|
||||||
|
class SqllabView(BaseSupersetView):
|
||||||
|
route_base = "/sqllab"
|
||||||
|
class_permission_name = "SQLLab"
|
||||||
|
|
||||||
|
method_permission_name = MODEL_API_RW_METHOD_PERMISSION_MAP
|
||||||
|
|
||||||
|
@expose("/")
|
||||||
|
@has_access
|
||||||
|
@permission_name("read")
|
||||||
|
@event_logger.log_this
|
||||||
|
def root(self) -> FlaskResponse:
|
||||||
|
return self.render_app_template()
|
||||||
|
|
||||||
|
@expose("/history/", methods=("GET",))
|
||||||
|
@has_access
|
||||||
|
@permission_name("read")
|
||||||
|
@event_logger.log_this
|
||||||
|
def history(self) -> FlaskResponse:
|
||||||
|
return self.render_app_template()
|
|
@ -49,7 +49,6 @@ from superset.models.dashboard import Dashboard
|
||||||
from superset.models.slice import Slice
|
from superset.models.slice import Slice
|
||||||
from superset.models.sql_lab import Query
|
from superset.models.sql_lab import Query
|
||||||
from superset.result_set import SupersetResultSet
|
from superset.result_set import SupersetResultSet
|
||||||
from superset.sqllab.utils import bootstrap_sqllab_data
|
|
||||||
from superset.utils import core as utils
|
from superset.utils import core as utils
|
||||||
from superset.utils.core import backend
|
from superset.utils.core import backend
|
||||||
from superset.utils.database import get_example_database
|
from superset.utils.database import get_example_database
|
||||||
|
@ -956,7 +955,6 @@ class TestCore(SupersetTestCase):
|
||||||
dash_id = db.session.query(Dashboard.id).first()[0]
|
dash_id = db.session.query(Dashboard.id).first()[0]
|
||||||
tbl_id = self.table_ids.get("wb_health_population")
|
tbl_id = self.table_ids.get("wb_health_population")
|
||||||
urls = [
|
urls = [
|
||||||
"/superset/sqllab",
|
|
||||||
"/superset/welcome",
|
"/superset/welcome",
|
||||||
f"/superset/dashboard/{dash_id}/",
|
f"/superset/dashboard/{dash_id}/",
|
||||||
"/superset/profile/",
|
"/superset/profile/",
|
||||||
|
@ -1161,6 +1159,25 @@ class TestCore(SupersetTestCase):
|
||||||
resp = self.client.get("/superset/profile/")
|
resp = self.client.get("/superset/profile/")
|
||||||
assert resp.status_code == 302
|
assert resp.status_code == 302
|
||||||
|
|
||||||
|
def test_redirect_new_sqllab(self):
|
||||||
|
self.login(username="admin")
|
||||||
|
resp = self.client.get(
|
||||||
|
"/superset/sqllab?savedQueryId=1&testParams=2",
|
||||||
|
follow_redirects=True,
|
||||||
|
)
|
||||||
|
assert resp.request.path == "/sqllab/"
|
||||||
|
assert (
|
||||||
|
resp.request.query_string.decode("utf-8") == "savedQueryId=1&testParams=2"
|
||||||
|
)
|
||||||
|
|
||||||
|
resp = self.client.post("/superset/sqllab/")
|
||||||
|
assert resp.status_code == 302
|
||||||
|
|
||||||
|
def test_redirect_new_sqllab_history(self):
|
||||||
|
self.login(username="admin")
|
||||||
|
resp = self.client.get("/superset/sqllab/history/")
|
||||||
|
assert resp.status_code == 302
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
unittest.main()
|
unittest.main()
|
||||||
|
|
|
@ -259,7 +259,7 @@ class TestSqlLab(SupersetTestCase):
|
||||||
def test_sqllab_has_access(self):
|
def test_sqllab_has_access(self):
|
||||||
for username in ("admin", "gamma_sqllab"):
|
for username in ("admin", "gamma_sqllab"):
|
||||||
self.login(username)
|
self.login(username)
|
||||||
for endpoint in ("/superset/sqllab/", "/superset/sqllab/history/"):
|
for endpoint in ("/sqllab/", "/sqllab/history/"):
|
||||||
resp = self.client.get(endpoint)
|
resp = self.client.get(endpoint)
|
||||||
self.assertEqual(200, resp.status_code)
|
self.assertEqual(200, resp.status_code)
|
||||||
|
|
||||||
|
@ -267,7 +267,7 @@ class TestSqlLab(SupersetTestCase):
|
||||||
|
|
||||||
def test_sqllab_no_access(self):
|
def test_sqllab_no_access(self):
|
||||||
self.login("gamma")
|
self.login("gamma")
|
||||||
for endpoint in ("/superset/sqllab/", "/superset/sqllab/history/"):
|
for endpoint in ("/sqllab/", "/sqllab/history/"):
|
||||||
resp = self.client.get(endpoint)
|
resp = self.client.get(endpoint)
|
||||||
# Redirects to the main page
|
# Redirects to the main page
|
||||||
self.assertEqual(302, resp.status_code)
|
self.assertEqual(302, resp.status_code)
|
||||||
|
|
Loading…
Reference in New Issue