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', () => {
|
||||
beforeEach(() => {
|
||||
cy.visit('/superset/sqllab');
|
||||
cy.visit('/sqllab');
|
||||
});
|
||||
|
||||
// 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', () => {
|
||||
beforeEach(() => {
|
||||
cy.visit('/superset/sqllab');
|
||||
cy.visit('/sqllab');
|
||||
});
|
||||
|
||||
it.skip('supports entering and running a query', () => {
|
||||
|
|
|
@ -19,7 +19,7 @@
|
|||
|
||||
describe('SqlLab view', () => {
|
||||
beforeEach(() => {
|
||||
cy.visit('/superset/sqllab');
|
||||
cy.visit('/sqllab');
|
||||
});
|
||||
|
||||
it('should load the SqlLab', () => {
|
||||
|
|
|
@ -18,7 +18,7 @@
|
|||
*/
|
||||
describe('SqlLab query tabs', () => {
|
||||
beforeEach(() => {
|
||||
cy.visit('/superset/sqllab');
|
||||
cy.visit('/sqllab');
|
||||
});
|
||||
|
||||
const tablistSelector = '[data-test="sql-editor-tabs"] > [role="tablist"]';
|
||||
|
|
|
@ -77984,7 +77984,7 @@
|
|||
"@mapbox/geojson-extent": "^1.0.1",
|
||||
"@math.gl/web-mercator": "^3.2.2",
|
||||
"@types/d3-array": "^2.0.0",
|
||||
"@types/mapbox__geojson-extent": "*",
|
||||
"@types/mapbox__geojson-extent": "^1.0.0",
|
||||
"@types/underscore": "^1.11.6",
|
||||
"@types/urijs": "^1.19.19",
|
||||
"bootstrap-slider": "^10.0.0",
|
||||
|
|
|
@ -29,7 +29,6 @@ import messageToasts from 'src/components/MessageToasts/reducers';
|
|||
import saveModal from 'src/explore/reducers/saveModalReducer';
|
||||
import explore from 'src/explore/reducers/exploreReducer';
|
||||
import sqlLab from 'src/SqlLab/reducers/sqlLab';
|
||||
import localStorageUsageInKilobytes from 'src/SqlLab/reducers/localStorageUsage';
|
||||
import reports from 'src/features/reports/ReportModal/reducer';
|
||||
import getBootstrapData from 'src/utils/getBootstrapData';
|
||||
|
||||
|
@ -59,7 +58,7 @@ export default {
|
|||
saveModal,
|
||||
explore,
|
||||
sqlLab,
|
||||
localStorageUsageInKilobytes,
|
||||
localStorageUsageInKilobytes: noopReducer(0),
|
||||
reports,
|
||||
common: noopReducer(common),
|
||||
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 { addTable } from 'src/SqlLab/actions/sqlLab';
|
||||
import { initialState } from 'src/SqlLab/fixtures';
|
||||
import { reducers } from 'src/SqlLab/reducers';
|
||||
import reducers from 'spec/helpers/reducerIndex';
|
||||
import {
|
||||
SCHEMA_AUTOCOMPLETE_SCORE,
|
||||
TABLE_AUTOCOMPLETE_SCORE,
|
||||
|
|
|
@ -17,12 +17,13 @@
|
|||
* under the License.
|
||||
*/
|
||||
import React from 'react';
|
||||
import { combineReducers } from 'redux';
|
||||
import configureStore from 'redux-mock-store';
|
||||
import thunk from 'redux-thunk';
|
||||
import { render } from 'spec/helpers/testing-library';
|
||||
|
||||
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 { LOG_EVENT } from 'src/logger/actions';
|
||||
import {
|
||||
|
@ -37,6 +38,8 @@ jest.mock('src/SqlLab/components/QueryAutoRefresh', () => () => (
|
|||
<div data-test="mock-query-auto-refresh" />
|
||||
));
|
||||
|
||||
const sqlLabReducer = combineReducers(reducers);
|
||||
|
||||
describe('SqlLab App', () => {
|
||||
const middlewares = [thunk];
|
||||
const mockStore = configureStore(middlewares);
|
||||
|
|
|
@ -20,9 +20,9 @@ import React from 'react';
|
|||
import PropTypes from 'prop-types';
|
||||
import { bindActionCreators } from 'redux';
|
||||
import { connect } from 'react-redux';
|
||||
import { Redirect } from 'react-router-dom';
|
||||
import { css, styled, t } from '@superset-ui/core';
|
||||
import throttle from 'lodash/throttle';
|
||||
import ToastContainer from 'src/components/MessageToasts/ToastContainer';
|
||||
import {
|
||||
LOCALSTORAGE_MAX_USAGE_KB,
|
||||
LOCALSTORAGE_WARNING_THRESHOLD,
|
||||
|
@ -186,7 +186,14 @@ class App extends React.PureComponent {
|
|||
render() {
|
||||
const { queries, queriesLastUpdate } = this.props;
|
||||
if (this.state.hash && this.state.hash === '#search') {
|
||||
return window.location.replace('/superset/sqllab/history/');
|
||||
return (
|
||||
<Redirect
|
||||
to={{
|
||||
pathname: '/sqllab/history/',
|
||||
replace: true,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<SqlLabStyles data-test="SqlLabApp" className="App SqlLab">
|
||||
|
@ -195,7 +202,6 @@ class App extends React.PureComponent {
|
|||
queriesLastUpdate={queriesLastUpdate}
|
||||
/>
|
||||
<TabbedSqlEditors />
|
||||
<ToastContainer />
|
||||
</SqlLabStyles>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -61,7 +61,7 @@ interface QueryTableProps {
|
|||
}
|
||||
|
||||
const openQuery = (id: number) => {
|
||||
const url = `/superset/sqllab?queryId=${id}`;
|
||||
const url = `/sqllab?queryId=${id}`;
|
||||
window.open(url);
|
||||
};
|
||||
|
||||
|
|
|
@ -20,7 +20,7 @@ import React from 'react';
|
|||
import { act } from 'react-dom/test-utils';
|
||||
import { fireEvent, render, waitFor } from 'spec/helpers/testing-library';
|
||||
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 { setupStore } from 'src/views/store';
|
||||
import {
|
||||
|
|
|
@ -26,7 +26,7 @@ import SqlEditorLeftBar from 'src/SqlLab/components/SqlEditorLeftBar';
|
|||
import { table, initialState, defaultQueryEditor } from 'src/SqlLab/fixtures';
|
||||
import { api } from 'src/hooks/apiResources/queryApi';
|
||||
import { setupStore } from 'src/views/store';
|
||||
import { reducers } from 'src/SqlLab/reducers';
|
||||
import reducers from 'spec/helpers/reducerIndex';
|
||||
|
||||
const mockedProps = {
|
||||
tables: [table],
|
||||
|
|
|
@ -110,23 +110,17 @@ describe('TabbedSqlEditors', () => {
|
|||
it('should handle id', async () => {
|
||||
uriStub.returns({ id: 1 });
|
||||
await mountWithAct();
|
||||
expect(window.history.replaceState.getCall(0).args[2]).toBe(
|
||||
'/superset/sqllab',
|
||||
);
|
||||
expect(window.history.replaceState.getCall(0).args[2]).toBe('/sqllab');
|
||||
});
|
||||
it('should handle savedQueryId', async () => {
|
||||
uriStub.returns({ savedQueryId: 1 });
|
||||
await mountWithAct();
|
||||
expect(window.history.replaceState.getCall(0).args[2]).toBe(
|
||||
'/superset/sqllab',
|
||||
);
|
||||
expect(window.history.replaceState.getCall(0).args[2]).toBe('/sqllab');
|
||||
});
|
||||
it('should handle sql', async () => {
|
||||
uriStub.returns({ sql: 1, dbid: 1 });
|
||||
await mountWithAct();
|
||||
expect(window.history.replaceState.getCall(0).args[2]).toBe(
|
||||
'/superset/sqllab',
|
||||
);
|
||||
expect(window.history.replaceState.getCall(0).args[2]).toBe('/sqllab');
|
||||
});
|
||||
it('should handle custom url params', async () => {
|
||||
uriStub.returns({
|
||||
|
@ -137,7 +131,7 @@ describe('TabbedSqlEditors', () => {
|
|||
});
|
||||
await mountWithAct();
|
||||
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 { EmptyStateBig } from 'src/components/EmptyState';
|
||||
import getBootstrapData from 'src/utils/getBootstrapData';
|
||||
import { locationContext } from 'src/pages/SqlLab/LocationContext';
|
||||
import SqlEditor from '../SqlEditor';
|
||||
import SqlEditorTabHeader from '../SqlEditorTabHeader';
|
||||
|
||||
|
@ -75,7 +76,7 @@ const userOS = detectOS();
|
|||
class TabbedSqlEditors extends React.PureComponent {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
const sqlLabUrl = '/superset/sqllab';
|
||||
const sqlLabUrl = '/sqllab';
|
||||
this.state = {
|
||||
sqlLabUrl,
|
||||
};
|
||||
|
@ -132,6 +133,7 @@ class TabbedSqlEditors extends React.PureComponent {
|
|||
new: isNewQuery,
|
||||
...urlParams
|
||||
} = {
|
||||
...this.context.requestedQuery,
|
||||
...bootstrapData.requested_query,
|
||||
...queryParameters,
|
||||
};
|
||||
|
@ -332,6 +334,7 @@ class TabbedSqlEditors extends React.PureComponent {
|
|||
}
|
||||
TabbedSqlEditors.propTypes = propTypes;
|
||||
TabbedSqlEditors.defaultProps = defaultProps;
|
||||
TabbedSqlEditors.contextType = locationContext;
|
||||
|
||||
function mapStateToProps({ sqlLab, common }) {
|
||||
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 { Logger, LOG_ACTIONS_LOAD_CHART } from 'src/logger/LogUtils';
|
||||
import { getClientErrorObject } from 'src/utils/getClientErrorObject';
|
||||
import { safeStringify } from 'src/utils/safeStringify';
|
||||
import { allowCrossDomain as domainShardingEnabled } from 'src/utils/hostNamesConfig';
|
||||
import { updateDataMask } from 'src/dataMask/actions';
|
||||
import { waitForAsyncData } from 'src/middleware/asyncEvent';
|
||||
|
@ -571,17 +570,20 @@ export function postChartFormData(
|
|||
);
|
||||
}
|
||||
|
||||
export function redirectSQLLab(formData) {
|
||||
export function redirectSQLLab(formData, history) {
|
||||
return dispatch => {
|
||||
getChartDataRequest({ formData, resultFormat: 'json', resultType: 'query' })
|
||||
.then(({ json }) => {
|
||||
const redirectUrl = '/superset/sqllab/';
|
||||
const redirectUrl = '/sqllab/';
|
||||
const payload = {
|
||||
datasourceKey: formData.datasource,
|
||||
sql: json.result[0].query,
|
||||
};
|
||||
SupersetClient.postForm(redirectUrl, {
|
||||
form_data: safeStringify(payload),
|
||||
history.push({
|
||||
pathname: redirectUrl,
|
||||
state: {
|
||||
requestedQuery: payload,
|
||||
},
|
||||
});
|
||||
})
|
||||
.catch(() =>
|
||||
|
|
|
@ -17,6 +17,7 @@
|
|||
* under the License.
|
||||
*/
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Tooltip } from 'src/components/Tooltip';
|
||||
|
@ -151,12 +152,22 @@ export const ExploreChartHeader = ({
|
|||
[dispatch],
|
||||
);
|
||||
|
||||
const history = useHistory();
|
||||
const { redirectSQLLab } = actions;
|
||||
|
||||
const redirectToSQLLab = useCallback(
|
||||
formData => {
|
||||
redirectSQLLab(formData, history);
|
||||
},
|
||||
[redirectSQLLab, history],
|
||||
);
|
||||
|
||||
const [menu, isDropdownVisible, setIsDropdownVisible] =
|
||||
useExploreAdditionalActionsMenu(
|
||||
latestQueryFormData,
|
||||
canDownload,
|
||||
slice,
|
||||
actions.redirectSQLLab,
|
||||
redirectToSQLLab,
|
||||
openPropertiesModal,
|
||||
ownState,
|
||||
metadata?.dashboards,
|
||||
|
|
|
@ -18,6 +18,7 @@
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { Route } from 'react-router-dom';
|
||||
import fetchMock from 'fetch-mock';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { DatasourceType, JsonObject, SupersetClient } from '@superset-ui/core';
|
||||
|
@ -27,6 +28,17 @@ import DatasourceControl from '.';
|
|||
|
||||
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 = {}) => ({
|
||||
hovered: false,
|
||||
type: 'DatasourceControl',
|
||||
|
@ -35,16 +47,7 @@ const createProps = (overrides: JsonObject = {}) => ({
|
|||
description: null,
|
||||
value: '25__table',
|
||||
form_data: {},
|
||||
datasource: {
|
||||
id: 25,
|
||||
database: {
|
||||
name: 'examples',
|
||||
},
|
||||
name: 'channels',
|
||||
type: 'table',
|
||||
columns: [],
|
||||
owners: [{ first_name: 'john', last_name: 'doe', id: 1, username: 'jd' }],
|
||||
},
|
||||
datasource: mockDatasource,
|
||||
validationErrors: [],
|
||||
name: 'datasource',
|
||||
actions: {
|
||||
|
@ -91,20 +94,20 @@ async function openAndSaveChanges(datasource: any) {
|
|||
|
||||
test('Should render', async () => {
|
||||
const props = createProps();
|
||||
render(<DatasourceControl {...props} />);
|
||||
render(<DatasourceControl {...props} />, { useRouter: true });
|
||||
expect(await screen.findByTestId('datasource-control')).toBeVisible();
|
||||
});
|
||||
|
||||
test('Should have elements', async () => {
|
||||
const props = createProps();
|
||||
render(<DatasourceControl {...props} />);
|
||||
render(<DatasourceControl {...props} />, { useRouter: true });
|
||||
expect(await screen.findByText('channels')).toBeVisible();
|
||||
expect(screen.getByTestId('datasource-menu-trigger')).toBeVisible();
|
||||
});
|
||||
|
||||
test('Should open a menu', async () => {
|
||||
const props = createProps();
|
||||
render(<DatasourceControl {...props} />);
|
||||
render(<DatasourceControl {...props} />, { useRouter: true });
|
||||
|
||||
expect(screen.queryByText('Edit 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',
|
||||
},
|
||||
});
|
||||
render(<DatasourceControl {...props} />);
|
||||
render(<DatasourceControl {...props} />, { useRouter: true });
|
||||
|
||||
userEvent.click(screen.getByTestId('datasource-menu-trigger'));
|
||||
|
||||
|
@ -154,7 +157,7 @@ test('Should show SQL Lab for sql_lab role', async () => {
|
|||
username: 'sql',
|
||||
},
|
||||
});
|
||||
render(<DatasourceControl {...props} />);
|
||||
render(<DatasourceControl {...props} />, { useRouter: true });
|
||||
|
||||
userEvent.click(screen.getByTestId('datasource-menu-trigger'));
|
||||
|
||||
|
@ -178,6 +181,7 @@ test('Click on Swap dataset option', async () => {
|
|||
|
||||
render(<DatasourceControl {...props} />, {
|
||||
useRedux: true,
|
||||
useRouter: true,
|
||||
});
|
||||
userEvent.click(screen.getByTestId('datasource-menu-trigger'));
|
||||
|
||||
|
@ -198,6 +202,7 @@ test('Click on Edit dataset', async () => {
|
|||
);
|
||||
render(<DatasourceControl {...props} />, {
|
||||
useRedux: true,
|
||||
useRouter: true,
|
||||
});
|
||||
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} />, {
|
||||
useRedux: true,
|
||||
useRouter: true,
|
||||
});
|
||||
|
||||
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 () => {
|
||||
const props = createProps();
|
||||
const postFormSpy = jest.spyOn(SupersetClient, 'postForm');
|
||||
postFormSpy.mockImplementation(jest.fn());
|
||||
|
||||
render(<DatasourceControl {...props} />, {
|
||||
useRedux: true,
|
||||
});
|
||||
const { queryByTestId, getByTestId } = render(
|
||||
<>
|
||||
<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'));
|
||||
|
||||
expect(postFormSpy).toBeCalledTimes(0);
|
||||
expect(queryByTestId('mock-sqllab-route')).not.toBeInTheDocument();
|
||||
|
||||
await act(async () => {
|
||||
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 () => {
|
||||
|
@ -261,7 +287,7 @@ test('Should open a different menu when datasource=query', async () => {
|
|||
type: DatasourceType.Query,
|
||||
},
|
||||
};
|
||||
render(<DatasourceControl {...queryProps} />);
|
||||
render(<DatasourceControl {...queryProps} />, { useRouter: true });
|
||||
|
||||
expect(screen.queryByText('Query preview')).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.getByText('Save as dataset'));
|
||||
|
||||
|
@ -327,6 +356,7 @@ test('should set the default temporal column', async () => {
|
|||
};
|
||||
render(<DatasourceControl {...props} {...overrideProps} />, {
|
||||
useRedux: true,
|
||||
useRouter: true,
|
||||
});
|
||||
|
||||
await openAndSaveChanges(overrideProps.datasource);
|
||||
|
@ -362,6 +392,7 @@ test('should set the first available temporal column', async () => {
|
|||
};
|
||||
render(<DatasourceControl {...props} {...overrideProps} />, {
|
||||
useRedux: true,
|
||||
useRouter: true,
|
||||
});
|
||||
|
||||
await openAndSaveChanges(overrideProps.datasource);
|
||||
|
@ -397,6 +428,7 @@ test('should not set the temporal column', async () => {
|
|||
};
|
||||
render(<DatasourceControl {...props} {...overrideProps} />, {
|
||||
useRedux: true,
|
||||
useRouter: true,
|
||||
});
|
||||
|
||||
await openAndSaveChanges(overrideProps.datasource);
|
||||
|
@ -410,7 +442,7 @@ test('should not set the temporal column', async () => {
|
|||
|
||||
test('should show missing params state', () => {
|
||||
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 url parameters/i)).toBeVisible();
|
||||
expect(
|
||||
|
@ -426,7 +458,7 @@ test('should show missing dataset state', () => {
|
|||
// @ts-ignore
|
||||
window.location = { search: '?slice_id=152' };
|
||||
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.getByText(
|
||||
|
|
|
@ -20,13 +20,7 @@
|
|||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {
|
||||
DatasourceType,
|
||||
SupersetClient,
|
||||
styled,
|
||||
t,
|
||||
withTheme,
|
||||
} from '@superset-ui/core';
|
||||
import { DatasourceType, styled, t, withTheme } from '@superset-ui/core';
|
||||
import { getTemporalColumns } from '@superset-ui/chart-controls';
|
||||
import { getUrlParam } from 'src/utils/urlUtils';
|
||||
import { AntdDropdown } from 'src/components';
|
||||
|
@ -50,8 +44,8 @@ import ModalTrigger from 'src/components/ModalTrigger';
|
|||
import ViewQueryModalFooter from 'src/explore/components/controls/ViewQueryModalFooter';
|
||||
import ViewQuery from 'src/explore/components/controls/ViewQuery';
|
||||
import { SaveDatasetModal } from 'src/SqlLab/components/SaveDatasetModal';
|
||||
import { safeStringify } from 'src/utils/safeStringify';
|
||||
import { isString } from 'lodash';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
const propTypes = {
|
||||
actions: PropTypes.object.isRequired,
|
||||
|
@ -126,7 +120,6 @@ const Styles = styled.div`
|
|||
`;
|
||||
|
||||
const CHANGE_DATASET = 'change_dataset';
|
||||
const VIEW_IN_SQL_LAB = 'view_in_sql_lab';
|
||||
const EDIT_DATASET = 'edit_dataset';
|
||||
const QUERY_PREVIEW = 'query_preview';
|
||||
const SAVE_AS_DATASET = 'save_as_dataset';
|
||||
|
@ -238,19 +231,6 @@ class DatasourceControl extends React.PureComponent {
|
|||
this.toggleEditDatasourceModal();
|
||||
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:
|
||||
this.toggleSaveDatasetModal();
|
||||
break;
|
||||
|
@ -286,6 +266,10 @@ class DatasourceControl extends React.PureComponent {
|
|||
const canAccessSqlLab = userHasPermission(user, 'SQL Lab', 'menu_access');
|
||||
|
||||
const editText = t('Edit dataset');
|
||||
const requestedQuery = {
|
||||
datasourceKey: `${datasource.id}__${datasource.type}`,
|
||||
sql: datasource.sql,
|
||||
};
|
||||
|
||||
const defaultDatasourceMenu = (
|
||||
<Menu onClick={this.handleMenuItemClick}>
|
||||
|
@ -310,7 +294,16 @@ class DatasourceControl extends React.PureComponent {
|
|||
)}
|
||||
<Menu.Item key={CHANGE_DATASET}>{t('Swap dataset')}</Menu.Item>
|
||||
{!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>
|
||||
);
|
||||
|
@ -340,7 +333,16 @@ class DatasourceControl extends React.PureComponent {
|
|||
/>
|
||||
</Menu.Item>
|
||||
{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>
|
||||
|
|
|
@ -18,8 +18,9 @@
|
|||
*/
|
||||
import React from 'react';
|
||||
import { isObject } from 'lodash';
|
||||
import { t, SupersetClient } from '@superset-ui/core';
|
||||
import { t } from '@superset-ui/core';
|
||||
import Button from 'src/components/Button';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
|
||||
interface SimpleDataSource {
|
||||
id: string;
|
||||
|
@ -42,12 +43,18 @@ const ViewQueryModalFooter: React.FC<ViewQueryModalFooterProps> = (props: {
|
|||
changeDatasource: () => void;
|
||||
datasource: SimpleDataSource;
|
||||
}) => {
|
||||
const history = useHistory();
|
||||
const viewInSQLLab = (id: string, type: string, sql: string) => {
|
||||
const payload = {
|
||||
datasourceKey: `${id}__${type}`,
|
||||
sql,
|
||||
};
|
||||
SupersetClient.postForm('/superset/sqllab/', payload);
|
||||
history.push({
|
||||
pathname: '/sqllab',
|
||||
state: {
|
||||
requestedQuery: payload,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const openSQL = () => {
|
||||
|
|
|
@ -32,6 +32,7 @@ import React, {
|
|||
useReducer,
|
||||
Reducer,
|
||||
} from 'react';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import { setItem, LocalStorageKeys } from 'src/utils/localStorageHelpers';
|
||||
import { UploadChangeParam, UploadFile } from 'antd/lib/upload/interface';
|
||||
import Tabs from 'src/components/Tabs';
|
||||
|
@ -141,7 +142,6 @@ interface DatabaseModalProps {
|
|||
show: boolean;
|
||||
databaseId: number | undefined; // If included, will go into edit mode
|
||||
dbEngine: string | undefined; // if included goto step 2 with engine already set
|
||||
history?: any;
|
||||
}
|
||||
|
||||
export enum ActionType {
|
||||
|
@ -526,7 +526,6 @@ const DatabaseModal: FunctionComponent<DatabaseModalProps> = ({
|
|||
show,
|
||||
databaseId,
|
||||
dbEngine,
|
||||
history,
|
||||
}) => {
|
||||
const [db, setDB] = useReducer<
|
||||
Reducer<Partial<DatabaseObject> | null, DBReducerActionType>
|
||||
|
@ -627,6 +626,7 @@ const DatabaseModal: FunctionComponent<DatabaseModalProps> = ({
|
|||
(DB: DatabaseObject) => DB.backend === engine || DB.engine === engine,
|
||||
)?.parameters !== undefined;
|
||||
const showDBError = validationErrors || dbErrors;
|
||||
const history = useHistory();
|
||||
|
||||
const dbModel: DatabaseForm =
|
||||
availableDbs?.databases?.find(
|
||||
|
@ -700,13 +700,7 @@ const DatabaseModal: FunctionComponent<DatabaseModalProps> = ({
|
|||
};
|
||||
|
||||
const redirectURL = (url: string) => {
|
||||
/* TODO (lyndsiWilliams): This check and passing history
|
||||
as a prop can be removed once SQL Lab is in the SPA */
|
||||
if (!isEmpty(history)) {
|
||||
history?.push(url);
|
||||
} else {
|
||||
window.location.href = url;
|
||||
}
|
||||
history.push(url);
|
||||
};
|
||||
|
||||
// Database import logic
|
||||
|
@ -1583,7 +1577,7 @@ const DatabaseModal: FunctionComponent<DatabaseModalProps> = ({
|
|||
onClick={() => {
|
||||
setLoading(true);
|
||||
fetchAndSetDB();
|
||||
redirectURL(`/superset/sqllab/?db=true`);
|
||||
redirectURL(`/sqllab?db=true`);
|
||||
}}
|
||||
>
|
||||
{t('QUERY DATA IN SQL LAB')}
|
||||
|
|
|
@ -45,7 +45,9 @@ jest.mock(
|
|||
|
||||
describe('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 });
|
||||
expect(blankDatasetImg).toBeVisible();
|
||||
|
@ -73,6 +75,9 @@ describe('DatasetPanel', () => {
|
|||
columnList={[]}
|
||||
loading={false}
|
||||
/>,
|
||||
{
|
||||
useRouter: true,
|
||||
},
|
||||
);
|
||||
|
||||
const blankDatasetImg = screen.getByRole('img', { name: /empty/i });
|
||||
|
@ -91,6 +96,9 @@ describe('DatasetPanel', () => {
|
|||
columnList={[]}
|
||||
loading
|
||||
/>,
|
||||
{
|
||||
useRouter: true,
|
||||
},
|
||||
);
|
||||
|
||||
const blankDatasetImg = screen.getByAltText(ALT_LOADING);
|
||||
|
@ -107,6 +115,9 @@ describe('DatasetPanel', () => {
|
|||
columnList={[]}
|
||||
loading={false}
|
||||
/>,
|
||||
{
|
||||
useRouter: true,
|
||||
},
|
||||
);
|
||||
|
||||
const errorTitle = screen.getByText(ERROR_TITLE);
|
||||
|
@ -124,6 +135,9 @@ describe('DatasetPanel', () => {
|
|||
columnList={exampleColumns}
|
||||
loading={false}
|
||||
/>,
|
||||
{
|
||||
useRouter: true,
|
||||
},
|
||||
);
|
||||
expect(await screen.findByText(tableName)).toBeVisible();
|
||||
expect(screen.getByText(COLUMN_TITLE)).toBeVisible();
|
||||
|
@ -148,6 +162,9 @@ describe('DatasetPanel', () => {
|
|||
loading={false}
|
||||
datasets={exampleDataset}
|
||||
/>,
|
||||
{
|
||||
useRouter: true,
|
||||
},
|
||||
);
|
||||
|
||||
// This is text in the info banner
|
||||
|
|
|
@ -20,6 +20,7 @@
|
|||
import React from 'react';
|
||||
import { t, styled } from '@superset-ui/core';
|
||||
import { EmptyStateBig } from 'src/components/EmptyState';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
const StyledContainer = styled.div`
|
||||
padding: ${({ theme }) => theme.gridUnit * 8}px
|
||||
|
@ -50,15 +51,11 @@ export const VIEW_DATASET_MESSAGE = t(
|
|||
const renderEmptyDescription = () => (
|
||||
<>
|
||||
{SELECT_MESSAGE}
|
||||
<span
|
||||
role="button"
|
||||
onClick={() => {
|
||||
window.location.href = `/superset/sqllab`;
|
||||
}}
|
||||
tabIndex={0}
|
||||
>
|
||||
{CREATE_MESSAGE}
|
||||
</span>
|
||||
<Link to="/sqllab">
|
||||
<span role="button" tabIndex={0}>
|
||||
{CREATE_MESSAGE}
|
||||
</span>
|
||||
</Link>
|
||||
{VIEW_DATASET_MESSAGE}
|
||||
</>
|
||||
);
|
||||
|
|
|
@ -35,7 +35,7 @@ jest.mock('react-router-dom', () => ({
|
|||
|
||||
describe('DatasetLayout', () => {
|
||||
it('renders nothing when no components are passed in', () => {
|
||||
render(<DatasetLayout />);
|
||||
render(<DatasetLayout />, { useRouter: true });
|
||||
const layoutWrapper = screen.getByTestId('dataset-layout-wrapper');
|
||||
|
||||
expect(layoutWrapper).toHaveTextContent('');
|
||||
|
@ -55,7 +55,7 @@ describe('DatasetLayout', () => {
|
|||
it('renders a LeftPanel when passed in', async () => {
|
||||
render(
|
||||
<DatasetLayout leftPanel={<LeftPanel setDataset={() => null} />} />,
|
||||
{ useRedux: true },
|
||||
{ useRedux: true, useRouter: true },
|
||||
);
|
||||
|
||||
expect(
|
||||
|
@ -65,7 +65,9 @@ describe('DatasetLayout', () => {
|
|||
});
|
||||
|
||||
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 blankDatasetTitle = screen.getByText(/select dataset source/i);
|
||||
|
@ -75,13 +77,16 @@ describe('DatasetLayout', () => {
|
|||
});
|
||||
|
||||
it('renders a RightPanel when passed in', () => {
|
||||
render(<DatasetLayout rightPanel={RightPanel()} />);
|
||||
render(<DatasetLayout rightPanel={RightPanel()} />, { useRouter: true });
|
||||
|
||||
expect(screen.getByText(/right panel/i)).toBeVisible();
|
||||
});
|
||||
|
||||
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();
|
||||
});
|
||||
|
|
|
@ -105,7 +105,7 @@ const getEntityIcon = (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;
|
||||
return entity.item_url;
|
||||
};
|
||||
|
|
|
@ -17,6 +17,7 @@
|
|||
* under the License.
|
||||
*/
|
||||
import React from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import Button from 'src/components/Button';
|
||||
import { Empty } from 'src/components';
|
||||
import { TableTab } from 'src/views/CRUD/types';
|
||||
|
@ -81,7 +82,7 @@ export default function EmptyState({
|
|||
const mineRedirects: Redirects = {
|
||||
[WelcomeTable.Charts]: '/chart/add',
|
||||
[WelcomeTable.Dashboards]: '/dashboard/new',
|
||||
[WelcomeTable.SavedQueries]: '/superset/sqllab?new=true',
|
||||
[WelcomeTable.SavedQueries]: '/sqllab?new=true',
|
||||
};
|
||||
const favRedirects: Redirects = {
|
||||
[WelcomeTable.Charts]: '/chart/list',
|
||||
|
@ -140,20 +141,17 @@ export default function EmptyState({
|
|||
>
|
||||
{tableName !== WelcomeTable.Recents && (
|
||||
<ButtonContainer>
|
||||
<Button
|
||||
buttonStyle="primary"
|
||||
onClick={() => {
|
||||
window.location.href = mineRedirects[tableName];
|
||||
}}
|
||||
>
|
||||
<i className="fa fa-plus" />
|
||||
{tableName === WelcomeTable.SavedQueries
|
||||
? t('SQL query')
|
||||
: tableName
|
||||
.split('')
|
||||
.slice(0, tableName.length - 1)
|
||||
.join('')}
|
||||
</Button>
|
||||
<Link to={mineRedirects[tableName]}>
|
||||
<Button buttonStyle="primary">
|
||||
<i className="fa fa-plus" />
|
||||
{tableName === WelcomeTable.SavedQueries
|
||||
? t('SQL query')
|
||||
: tableName
|
||||
.split('')
|
||||
.slice(0, tableName.length - 1)
|
||||
.join('')}
|
||||
</Button>
|
||||
</Link>
|
||||
</ButtonContainer>
|
||||
)}
|
||||
</Empty>
|
||||
|
|
|
@ -62,7 +62,7 @@ const dropdownItems = [
|
|||
},
|
||||
{
|
||||
label: 'SQL query',
|
||||
url: '/superset/sqllab?new=true',
|
||||
url: '/sqllab?new=true',
|
||||
icon: 'fa-fw fa-search',
|
||||
perm: 'can_sqllab',
|
||||
view: 'Superset',
|
||||
|
|
|
@ -73,7 +73,7 @@ const dropdownItems = [
|
|||
},
|
||||
{
|
||||
label: 'SQL query',
|
||||
url: '/superset/sqllab?new=true',
|
||||
url: '/sqllab?new=true',
|
||||
icon: 'fa-fw fa-search',
|
||||
perm: 'can_sqllab',
|
||||
view: 'Superset',
|
||||
|
|
|
@ -210,7 +210,7 @@ const RightMenu = ({
|
|||
},
|
||||
{
|
||||
label: t('SQL query'),
|
||||
url: '/superset/sqllab?new=true',
|
||||
url: '/sqllab?new=true',
|
||||
icon: 'fa-fw fa-search',
|
||||
perm: 'can_sqllab',
|
||||
view: 'Superset',
|
||||
|
|
|
@ -17,6 +17,7 @@
|
|||
* under the License.
|
||||
*/
|
||||
import React, { useState } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { styled, SupersetClient, t, useTheme } from '@superset-ui/core';
|
||||
import SyntaxHighlighter from 'react-syntax-highlighter/dist/cjs/light';
|
||||
import sql from 'react-syntax-highlighter/dist/cjs/languages/hljs/sql';
|
||||
|
@ -193,12 +194,8 @@ const SavedQueries = ({
|
|||
const renderMenu = (query: Query) => (
|
||||
<Menu>
|
||||
{canEdit && (
|
||||
<Menu.Item
|
||||
onClick={() => {
|
||||
window.location.href = `/superset/sqllab?savedQueryId=${query.id}`;
|
||||
}}
|
||||
>
|
||||
{t('Edit')}
|
||||
<Menu.Item>
|
||||
<Link to={`/sqllab?savedQueryId=${query.id}`}>{t('Edit')}</Link>
|
||||
</Menu.Item>
|
||||
)}
|
||||
<Menu.Item
|
||||
|
@ -256,15 +253,12 @@ const SavedQueries = ({
|
|||
buttons={[
|
||||
{
|
||||
name: (
|
||||
<>
|
||||
<Link to="/sqllab?new=true">
|
||||
<i className="fa fa-plus" />
|
||||
{t('SQL Query')}
|
||||
</>
|
||||
</Link>
|
||||
),
|
||||
buttonStyle: 'tertiary',
|
||||
onClick: () => {
|
||||
window.location.href = '/superset/sqllab?new=true';
|
||||
},
|
||||
},
|
||||
{
|
||||
name: t('View All »'),
|
||||
|
@ -278,15 +272,10 @@ const SavedQueries = ({
|
|||
{queries.length > 0 ? (
|
||||
<CardContainer showThumbnails={showThumbnails}>
|
||||
{queries.map(q => (
|
||||
<CardStyles
|
||||
onClick={() => {
|
||||
window.location.href = `/superset/sqllab?savedQueryId=${q.id}`;
|
||||
}}
|
||||
key={q.id}
|
||||
>
|
||||
<CardStyles key={q.id}>
|
||||
<ListViewCard
|
||||
imgURL=""
|
||||
url={`/superset/sqllab?savedQueryId=${q.id}`}
|
||||
url={`/sqllab?savedQueryId=${q.id}`}
|
||||
title={q.label}
|
||||
imgFallbackURL="/static/assets/images/empty-query.svg"
|
||||
description={t('Ran %s', q.changed_on_delta_humanized)}
|
||||
|
|
|
@ -180,7 +180,7 @@ type MenuChild = {
|
|||
|
||||
export interface ButtonProps {
|
||||
name: ReactNode;
|
||||
onClick: OnClickHandler;
|
||||
onClick?: OnClickHandler;
|
||||
'data-test'?: string;
|
||||
buttonStyle:
|
||||
| 'primary'
|
||||
|
|
|
@ -30,7 +30,7 @@ export const commonMenuData = {
|
|||
{
|
||||
name: 'Query history',
|
||||
label: t('Query history'),
|
||||
url: '/superset/sqllab/history/',
|
||||
url: '/sqllab/history/',
|
||||
usesRouter: true,
|
||||
},
|
||||
],
|
||||
|
|
|
@ -17,7 +17,10 @@
|
|||
* under the License.
|
||||
*/
|
||||
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 {
|
||||
SupersetClient,
|
||||
|
@ -35,7 +38,9 @@ export const supersetClientQuery: BaseQueryFn<
|
|||
parseMethod?: ParseMethod;
|
||||
transformResponse?: (response: SupersetClientResponse) => JsonValue;
|
||||
urlParams?: Record<string, number | string | undefined | boolean>;
|
||||
}
|
||||
},
|
||||
JsonValue,
|
||||
ClientErrorObject
|
||||
> = (
|
||||
{
|
||||
endpoint,
|
||||
|
|
|
@ -31,7 +31,7 @@ jest.mock('react-router-dom', () => ({
|
|||
|
||||
describe('AddDataset', () => {
|
||||
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 });
|
||||
|
||||
|
|
|
@ -17,6 +17,7 @@
|
|||
* under the License.
|
||||
*/
|
||||
import React, { useMemo, useState, useCallback, ReactElement } from 'react';
|
||||
import { Link, useHistory } from 'react-router-dom';
|
||||
import {
|
||||
QueryState,
|
||||
styled,
|
||||
|
@ -102,6 +103,7 @@ function QueryList({ addDangerToast }: QueryListProps) {
|
|||
useState<QueryObject>();
|
||||
|
||||
const theme = useTheme();
|
||||
const history = useHistory();
|
||||
|
||||
const handleQueryPreview = useCallback(
|
||||
(id: number) => {
|
||||
|
@ -334,9 +336,9 @@ function QueryList({ addDangerToast }: QueryListProps) {
|
|||
},
|
||||
}: any) => (
|
||||
<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} />
|
||||
</a>
|
||||
</Link>
|
||||
</Tooltip>
|
||||
),
|
||||
},
|
||||
|
@ -427,9 +429,7 @@ function QueryList({ addDangerToast }: QueryListProps) {
|
|||
query={queryCurrentlyPreviewing}
|
||||
queries={queries}
|
||||
fetchData={handleQueryPreview}
|
||||
openInSqlLab={(id: number) =>
|
||||
window.location.assign(`/superset/sqllab?queryId=${id}`)
|
||||
}
|
||||
openInSqlLab={(id: number) => history.push(`/sqllab?queryId=${id}`)}
|
||||
show
|
||||
/>
|
||||
)}
|
||||
|
|
|
@ -25,6 +25,7 @@ import {
|
|||
t,
|
||||
} from '@superset-ui/core';
|
||||
import React, { useState, useMemo, useCallback } from 'react';
|
||||
import { Link, useHistory } from 'react-router-dom';
|
||||
import rison from 'rison';
|
||||
import moment from 'moment';
|
||||
import {
|
||||
|
@ -127,6 +128,7 @@ function SavedQueryList({
|
|||
sshTunnelPrivateKeyPasswordFields,
|
||||
setSSHTunnelPrivateKeyPasswordFields,
|
||||
] = useState<string[]>([]);
|
||||
const history = useHistory();
|
||||
|
||||
const openSavedQueryImportModal = () => {
|
||||
showImportModal(true);
|
||||
|
@ -148,10 +150,6 @@ function SavedQueryList({
|
|||
const canExport =
|
||||
hasPerm('can_export') && isFeatureEnabled(FeatureFlag.VERSIONED_EXPORT);
|
||||
|
||||
const openNewQuery = () => {
|
||||
window.open(`${window.location.origin}/superset/sqllab?new=true`);
|
||||
};
|
||||
|
||||
const handleSavedQueryPreview = useCallback(
|
||||
(id: number) => {
|
||||
SupersetClient.get({
|
||||
|
@ -187,11 +185,10 @@ function SavedQueryList({
|
|||
|
||||
subMenuButtons.push({
|
||||
name: (
|
||||
<>
|
||||
<Link to="/sqllab?new=true">
|
||||
<i className="fa fa-plus" /> {t('Query')}
|
||||
</>
|
||||
</Link>
|
||||
),
|
||||
onClick: openNewQuery,
|
||||
buttonStyle: 'primary',
|
||||
});
|
||||
|
||||
|
@ -217,15 +214,13 @@ function SavedQueryList({
|
|||
|
||||
// Action methods
|
||||
const openInSqlLab = (id: number) => {
|
||||
window.open(`${window.location.origin}/superset/sqllab?savedQueryId=${id}`);
|
||||
history.push(`/sqllab?savedQueryId=${id}`);
|
||||
};
|
||||
|
||||
const copyQueryLink = useCallback(
|
||||
(id: number) => {
|
||||
copyTextToClipboard(() =>
|
||||
Promise.resolve(
|
||||
`${window.location.origin}/superset/sqllab?savedQueryId=${id}`,
|
||||
),
|
||||
Promise.resolve(`${window.location.origin}/sqllab?savedQueryId=${id}`),
|
||||
)
|
||||
.then(() => {
|
||||
addSuccessToast(t('Link Copied!'));
|
||||
|
|
|
@ -16,17 +16,26 @@
|
|||
* specific language governing permissions and limitations
|
||||
* 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 = {
|
||||
sqlLab,
|
||||
localStorageUsageInKilobytes,
|
||||
messageToasts,
|
||||
common,
|
||||
import React, { createContext, useContext } from 'react';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
|
||||
export type LocationState = {
|
||||
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,
|
||||
) => {
|
||||
copyTextToClipboard(() =>
|
||||
Promise.resolve(
|
||||
`${window.location.origin}/superset/sqllab?savedQueryId=${id}`,
|
||||
),
|
||||
Promise.resolve(`${window.location.origin}/sqllab?savedQueryId=${id}`),
|
||||
)
|
||||
.then(() => {
|
||||
addSuccessToast(t('Link Copied!'));
|
||||
|
|
|
@ -104,6 +104,10 @@ const SavedQueryList = lazy(
|
|||
import(/* webpackChunkName: "SavedQueryList" */ 'src/pages/SavedQueryList'),
|
||||
);
|
||||
|
||||
const SqlLab = lazy(
|
||||
() => import(/* webpackChunkName: "SqlLab" */ 'src/pages/SqlLab'),
|
||||
);
|
||||
|
||||
const AllEntities = lazy(
|
||||
() => import(/* webpackChunkName: "AllEntities" */ 'src/pages/AllEntities'),
|
||||
);
|
||||
|
@ -176,7 +180,7 @@ export const routes: Routes = [
|
|||
Component: AnnotationList,
|
||||
},
|
||||
{
|
||||
path: '/superset/sqllab/history/',
|
||||
path: '/sqllab/history/',
|
||||
Component: QueryHistoryList,
|
||||
},
|
||||
{
|
||||
|
@ -225,6 +229,10 @@ export const routes: Routes = [
|
|||
path: '/profile',
|
||||
Component: Profile,
|
||||
},
|
||||
{
|
||||
path: '/sqllab/',
|
||||
Component: SqlLab,
|
||||
},
|
||||
];
|
||||
|
||||
if (isFeatureEnabled(FeatureFlag.TAGGING_SYSTEM)) {
|
||||
|
|
|
@ -211,7 +211,6 @@ const config = {
|
|||
menu: addPreamble('src/views/menu.tsx'),
|
||||
spa: addPreamble('/src/views/index.tsx'),
|
||||
embedded: addPreamble('/src/embedded/index.tsx'),
|
||||
sqllab: addPreamble('/src/SqlLab/index.tsx'),
|
||||
},
|
||||
output,
|
||||
stats: 'minimal',
|
||||
|
|
|
@ -192,6 +192,7 @@ class SupersetAppInitializer: # pylint: disable=too-many-public-methods
|
|||
TableSchemaView,
|
||||
TabStateView,
|
||||
)
|
||||
from superset.views.sqllab import SqllabView
|
||||
from superset.views.tags import TagModelView, TagView
|
||||
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(SliceAsync)
|
||||
appbuilder.add_view_no_menu(SqlLab)
|
||||
appbuilder.add_view_no_menu(SqllabView)
|
||||
appbuilder.add_view_no_menu(SqlMetricInlineView)
|
||||
appbuilder.add_view_no_menu(Superset)
|
||||
appbuilder.add_view_no_menu(TableColumnInlineView)
|
||||
|
@ -347,7 +349,7 @@ class SupersetAppInitializer: # pylint: disable=too-many-public-methods
|
|||
appbuilder.add_link(
|
||||
"SQL Editor",
|
||||
label=__("SQL Lab"),
|
||||
href="/superset/sqllab/",
|
||||
href="/sqllab/",
|
||||
category_icon="fa-flask",
|
||||
icon="fa-flask",
|
||||
category="SQL Lab",
|
||||
|
@ -364,7 +366,7 @@ class SupersetAppInitializer: # pylint: disable=too-many-public-methods
|
|||
appbuilder.add_link(
|
||||
"Query Search",
|
||||
label=__("Query History"),
|
||||
href="/superset/sqllab/history/",
|
||||
href="/sqllab/history/",
|
||||
icon="fa-search",
|
||||
category_icon="fa-flask",
|
||||
category="SQL Lab",
|
||||
|
|
|
@ -157,7 +157,7 @@ class ExtraCache:
|
|||
|
||||
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
|
||||
`?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
|
||||
at runtime and replaced by the value in the URL.
|
||||
|
||||
|
|
|
@ -492,7 +492,7 @@ class Database(
|
|||
source = utils.QuerySource.DASHBOARD
|
||||
elif "/explore/" in request.referrer:
|
||||
source = utils.QuerySource.CHART
|
||||
elif "/superset/sqllab" in request.referrer:
|
||||
elif "/sqllab/" in request.referrer:
|
||||
source = utils.QuerySource.SQL_LAB
|
||||
|
||||
sqlalchemy_url, params = DB_CONNECTION_MUTATOR(
|
||||
|
|
|
@ -408,7 +408,7 @@ class SavedQuery(Model, AuditMixinNullable, ExtraJSONMixin, ImportExportMixin):
|
|||
def pop_tab_link(self) -> Markup:
|
||||
return Markup(
|
||||
f"""
|
||||
<a href="/superset/sqllab?savedQueryId={self.id}">
|
||||
<a href="/sqllab?savedQueryId={self.id}">
|
||||
<i class="fa fa-link"></i>
|
||||
</a>
|
||||
"""
|
||||
|
@ -423,7 +423,7 @@ class SavedQuery(Model, AuditMixinNullable, ExtraJSONMixin, ImportExportMixin):
|
|||
return self.database.sqlalchemy_uri
|
||||
|
||||
def url(self) -> str:
|
||||
return f"/superset/sqllab?savedQueryId={self.id}"
|
||||
return f"/sqllab?savedQueryId={self.id}"
|
||||
|
||||
@property
|
||||
def sql_tables(self) -> list[Table]:
|
||||
|
|
|
@ -25,6 +25,7 @@ from flask_appbuilder.models.sqla.interface import SQLAInterface
|
|||
from marshmallow import ValidationError
|
||||
|
||||
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.query import QueryDAO
|
||||
from superset.extensions import event_logger
|
||||
|
@ -67,6 +68,7 @@ logger = logging.getLogger(__name__)
|
|||
|
||||
|
||||
class SqlLabRestApi(BaseSupersetApi):
|
||||
method_permission_name = MODEL_API_RW_METHOD_PERMISSION_MAP
|
||||
datamodel = SQLAInterface(Query)
|
||||
|
||||
resource_name = "sqllab"
|
||||
|
|
|
@ -72,7 +72,6 @@ from superset.models.dashboard import Dashboard
|
|||
from superset.models.slice import Slice
|
||||
from superset.models.sql_lab import Query
|
||||
from superset.models.user_attributes import UserAttribute
|
||||
from superset.sqllab.utils import bootstrap_sqllab_data
|
||||
from superset.superset_typing import FlaskResponse
|
||||
from superset.utils import core as utils
|
||||
from superset.utils.cache import etag_cache
|
||||
|
@ -982,28 +981,18 @@ class Superset(BaseSupersetView): # pylint: disable=too-many-public-methods
|
|||
"POST",
|
||||
),
|
||||
)
|
||||
@deprecated(new_target="/sqllab")
|
||||
def sqllab(self) -> FlaskResponse:
|
||||
"""SQL Editor"""
|
||||
payload = {
|
||||
"common": common_bootstrap_payload(g.user),
|
||||
**bootstrap_sqllab_data(get_user_id()),
|
||||
}
|
||||
|
||||
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
|
||||
)
|
||||
url = "/sqllab"
|
||||
if url_params := request.args:
|
||||
params = parse.urlencode(url_params)
|
||||
url = f"{url}?{params}"
|
||||
return redirect(url)
|
||||
|
||||
@has_access
|
||||
@event_logger.log_this
|
||||
@expose("/sqllab/history/", methods=("GET",))
|
||||
@event_logger.log_this
|
||||
@deprecated(new_target="/sqllab/history")
|
||||
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.sql_lab import Query
|
||||
from superset.result_set import SupersetResultSet
|
||||
from superset.sqllab.utils import bootstrap_sqllab_data
|
||||
from superset.utils import core as utils
|
||||
from superset.utils.core import backend
|
||||
from superset.utils.database import get_example_database
|
||||
|
@ -956,7 +955,6 @@ class TestCore(SupersetTestCase):
|
|||
dash_id = db.session.query(Dashboard.id).first()[0]
|
||||
tbl_id = self.table_ids.get("wb_health_population")
|
||||
urls = [
|
||||
"/superset/sqllab",
|
||||
"/superset/welcome",
|
||||
f"/superset/dashboard/{dash_id}/",
|
||||
"/superset/profile/",
|
||||
|
@ -1161,6 +1159,25 @@ class TestCore(SupersetTestCase):
|
|||
resp = self.client.get("/superset/profile/")
|
||||
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__":
|
||||
unittest.main()
|
||||
|
|
|
@ -259,7 +259,7 @@ class TestSqlLab(SupersetTestCase):
|
|||
def test_sqllab_has_access(self):
|
||||
for username in ("admin", "gamma_sqllab"):
|
||||
self.login(username)
|
||||
for endpoint in ("/superset/sqllab/", "/superset/sqllab/history/"):
|
||||
for endpoint in ("/sqllab/", "/sqllab/history/"):
|
||||
resp = self.client.get(endpoint)
|
||||
self.assertEqual(200, resp.status_code)
|
||||
|
||||
|
@ -267,7 +267,7 @@ class TestSqlLab(SupersetTestCase):
|
|||
|
||||
def test_sqllab_no_access(self):
|
||||
self.login("gamma")
|
||||
for endpoint in ("/superset/sqllab/", "/superset/sqllab/history/"):
|
||||
for endpoint in ("/sqllab/", "/sqllab/history/"):
|
||||
resp = self.client.get(endpoint)
|
||||
# Redirects to the main page
|
||||
self.assertEqual(302, resp.status_code)
|
||||
|
|
Loading…
Reference in New Issue