superset/superset-frontend/src/SqlLab/components/TabbedSqlEditors.jsx

436 lines
14 KiB
JavaScript

/**
* 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 PropTypes from 'prop-types';
import { MenuItem, DropdownButton, Tab, Tabs } from 'react-bootstrap';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import URI from 'urijs';
import { t } from '@superset-ui/core';
import { isFeatureEnabled, FeatureFlag } from 'src/featureFlags';
import * as Actions from '../actions/sqlLab';
import SqlEditor from './SqlEditor';
import { areArraysShallowEqual } from '../../reduxUtils';
import TabStatusIcon from './TabStatusIcon';
import Icon from '../../components/Icon';
const propTypes = {
actions: PropTypes.object.isRequired,
defaultDbId: PropTypes.number,
displayLimit: PropTypes.number,
defaultQueryLimit: PropTypes.number.isRequired,
maxRow: PropTypes.number.isRequired,
databases: PropTypes.object.isRequired,
queries: PropTypes.object.isRequired,
queryEditors: PropTypes.array,
requestedQuery: PropTypes.object,
tabHistory: PropTypes.array.isRequired,
tables: PropTypes.array.isRequired,
offline: PropTypes.bool,
saveQueryWarning: PropTypes.string,
scheduleQueryWarning: PropTypes.string,
};
const defaultProps = {
queryEditors: [],
offline: false,
requestedQuery: null,
saveQueryWarning: null,
scheduleQueryWarning: null,
};
let queryCount = 1;
class TabbedSqlEditors extends React.PureComponent {
constructor(props) {
super(props);
const sqlLabUrl = '/superset/sqllab';
this.state = {
sqlLabUrl,
queriesArray: [],
dataPreviewQueries: [],
hideLeftBar: false,
};
this.removeQueryEditor = this.removeQueryEditor.bind(this);
this.renameTab = this.renameTab.bind(this);
this.toggleLeftBar = this.toggleLeftBar.bind(this);
this.removeAllOtherQueryEditors = this.removeAllOtherQueryEditors.bind(
this,
);
this.duplicateQueryEditor = this.duplicateQueryEditor.bind(this);
}
componentDidMount() {
// migrate query editor and associated tables state to server
if (isFeatureEnabled(FeatureFlag.SQLLAB_BACKEND_PERSISTENCE)) {
const localStorageTables = this.props.tables.filter(
table => table.inLocalStorage,
);
const localStorageQueries = Object.values(this.props.queries).filter(
query => query.inLocalStorage,
);
this.props.queryEditors
.filter(qe => qe.inLocalStorage)
.forEach(qe => {
// get all queries associated with the query editor
const queries = localStorageQueries.filter(
query => query.sqlEditorId === qe.id,
);
const tables = localStorageTables.filter(
table => table.queryEditorId === qe.id,
);
this.props.actions.migrateQueryEditorFromLocalStorage(
qe,
tables,
queries,
);
});
}
// merge post form data with GET search params
const query = {
...this.props.requestedQuery,
...URI(window.location).search(true),
};
// Popping a new tab based on the querystring
if (
query.id ||
query.sql ||
query.savedQueryId ||
query.datasourceKey ||
query.queryId
) {
if (query.id) {
this.props.actions.popStoredQuery(query.id);
} else if (query.savedQueryId) {
this.props.actions.popSavedQuery(query.savedQueryId);
} else if (query.queryId) {
this.props.actions.popQuery(query.queryId);
} else if (query.datasourceKey) {
this.props.actions.popDatasourceQuery(query.datasourceKey, query.sql);
} else if (query.sql) {
let dbId = query.dbid;
if (dbId) {
dbId = parseInt(dbId, 10);
} else {
const { databases } = this.props;
const dbName = query.dbname;
if (dbName) {
Object.keys(databases).forEach(db => {
if (databases[db].database_name === dbName) {
dbId = databases[db].id;
}
});
}
}
const newQueryEditor = {
title: query.title,
dbId,
schema: query.schema,
autorun: query.autorun,
sql: query.sql,
};
this.props.actions.addQueryEditor(newQueryEditor);
}
this.popNewTab();
} else if (this.props.queryEditors.length === 0) {
this.newQueryEditor();
} else {
const qe = this.activeQueryEditor();
const latestQuery = this.props.queries[qe.latestQueryId];
if (
isFeatureEnabled(FeatureFlag.SQLLAB_BACKEND_PERSISTENCE) &&
latestQuery &&
latestQuery.resultsKey
) {
// when results are not stored in localStorage they need to be
// fetched from the results backend (if configured)
this.props.actions.fetchQueryResults(
latestQuery,
this.props.displayLimit,
);
}
}
}
UNSAFE_componentWillReceiveProps(nextProps) {
const nextActiveQeId =
nextProps.tabHistory[nextProps.tabHistory.length - 1];
const queriesArray = [];
for (const id in nextProps.queries) {
if (nextProps.queries[id].sqlEditorId === nextActiveQeId) {
queriesArray.push(nextProps.queries[id]);
}
}
if (!areArraysShallowEqual(queriesArray, this.state.queriesArray)) {
this.setState({ queriesArray });
}
const dataPreviewQueries = [];
nextProps.tables.forEach(table => {
const queryId = table.dataPreviewQueryId;
if (
queryId &&
nextProps.queries[queryId] &&
table.queryEditorId === nextActiveQeId
) {
dataPreviewQueries.push({
...nextProps.queries[queryId],
tableName: table.name,
});
}
});
if (
!areArraysShallowEqual(dataPreviewQueries, this.state.dataPreviewQueries)
) {
this.setState({ dataPreviewQueries });
}
}
popNewTab() {
queryCount += 1;
// Clean the url in browser history
window.history.replaceState({}, document.title, this.state.sqlLabUrl);
}
renameTab(qe) {
/* eslint no-alert: 0 */
const newTitle = prompt(t('Enter a new title for the tab'));
if (newTitle) {
this.props.actions.queryEditorSetTitle(qe, newTitle);
}
}
activeQueryEditor() {
if (this.props.tabHistory.length === 0) {
return this.props.queryEditors[0];
}
const qeid = this.props.tabHistory[this.props.tabHistory.length - 1];
return this.props.queryEditors.find(qe => qe.id === qeid) || null;
}
newQueryEditor() {
queryCount += 1;
const activeQueryEditor = this.activeQueryEditor();
const firstDbId = Math.min(
...Object.values(this.props.databases).map(database => database.id),
);
const warning = isFeatureEnabled(FeatureFlag.SQLLAB_BACKEND_PERSISTENCE)
? ''
: `${t(
'-- Note: Unless you save your query, these tabs will NOT persist if you clear your cookies or change browsers.',
)}\n\n`;
const qe = {
title: t('Untitled Query %s', queryCount),
dbId:
activeQueryEditor && activeQueryEditor.dbId
? activeQueryEditor.dbId
: this.props.defaultDbId || firstDbId,
schema: activeQueryEditor ? activeQueryEditor.schema : null,
autorun: false,
sql: `${warning}SELECT ...`,
queryLimit: this.props.defaultQueryLimit,
};
this.props.actions.addQueryEditor(qe);
}
handleSelect(key) {
if (key === 'add_tab') {
this.newQueryEditor();
} else {
const qeid = this.props.tabHistory[this.props.tabHistory.length - 1];
if (key !== qeid) {
const queryEditor = this.props.queryEditors.find(qe => qe.id === key);
this.props.actions.switchQueryEditor(
queryEditor,
this.props.displayLimit,
);
}
}
}
removeQueryEditor(qe) {
this.props.actions.removeQueryEditor(qe);
}
removeAllOtherQueryEditors(cqe) {
this.props.queryEditors.forEach(
qe => qe !== cqe && this.removeQueryEditor(qe),
);
}
duplicateQueryEditor(qe) {
this.props.actions.cloneQueryToNewTab(qe, false);
}
toggleLeftBar() {
this.setState({ hideLeftBar: !this.state.hideLeftBar });
}
render() {
const editors = this.props.queryEditors.map((qe, i) => {
const isSelected =
this.activeQueryEditor() && this.activeQueryEditor().id === qe.id;
let latestQuery;
if (qe.latestQueryId) {
latestQuery = this.props.queries[qe.latestQueryId];
}
let database;
if (qe.dbId) {
database = this.props.databases[qe.dbId];
}
const state = latestQuery ? latestQuery.state : '';
const title = (
<>
{qe.title} <TabStatusIcon tabState={state} />{' '}
<Icon
role="button"
tabIndex={0}
cursor="pointer"
name="cancel-x"
onClick={() => this.removeQueryEditor(qe)}
/>
</>
);
const tabTitle = (
<>
{isSelected && (
<DropdownButton
bsSize="small"
id={`ddbtn-tab-${i}`}
title={' '}
noCaret
>
<MenuItem
className="close-btn"
eventKey="1"
onClick={() => this.removeQueryEditor(qe)}
>
<div className="icon-container">
<i className="fa fa-close" />
</div>
{t('Close tab')}
</MenuItem>
<MenuItem eventKey="2" onClick={() => this.renameTab(qe)}>
<div className="icon-container">
<i className="fa fa-i-cursor" />
</div>
{t('Rename tab')}
</MenuItem>
<MenuItem eventKey="3" onClick={this.toggleLeftBar}>
<div className="icon-container">
<i className="fa fa-cogs" />
</div>
{this.state.hideLeftBar
? t('Expand tool bar')
: t('Hide tool bar')}
</MenuItem>
<MenuItem
eventKey="4"
onClick={() => this.removeAllOtherQueryEditors(qe)}
>
<div className="icon-container">
<i className="fa fa-times-circle-o" />
</div>
{t('Close all other tabs')}
</MenuItem>
<MenuItem
eventKey="5"
onClick={() => this.duplicateQueryEditor(qe)}
>
<div className="icon-container">
<i className="fa fa-files-o" />
</div>
{t('Duplicate tab')}
</MenuItem>
</DropdownButton>
)}
<span className="ddbtn-tab">{title}</span>
</>
);
return (
<Tab key={qe.id} title={tabTitle} eventKey={qe.id}>
{isSelected && (
<SqlEditor
tables={this.props.tables.filter(
xt => xt.queryEditorId === qe.id,
)}
queryEditor={qe}
editorQueries={this.state.queriesArray}
dataPreviewQueries={this.state.dataPreviewQueries}
latestQuery={latestQuery}
database={database}
actions={this.props.actions}
hideLeftBar={this.state.hideLeftBar}
defaultQueryLimit={this.props.defaultQueryLimit}
maxRow={this.props.maxRow}
displayLimit={this.props.displayLimit}
saveQueryWarning={this.props.saveQueryWarning}
scheduleQueryWarning={this.props.scheduleQueryWarning}
/>
)}
</Tab>
);
});
return (
<Tabs
bsStyle="tabs"
animation={false}
activeKey={this.props.tabHistory[this.props.tabHistory.length - 1]}
onSelect={this.handleSelect.bind(this)}
id="a11y-query-editor-tabs"
className="SqlEditorTabs"
>
{editors}
<Tab
title={
<div>
<i className="fa fa-plus-circle" />
&nbsp;
</div>
}
className="addEditorTab"
eventKey="add_tab"
disabled={this.props.offline}
/>
</Tabs>
);
}
}
TabbedSqlEditors.propTypes = propTypes;
TabbedSqlEditors.defaultProps = defaultProps;
function mapStateToProps({ sqlLab, common, requestedQuery }) {
return {
databases: sqlLab.databases,
queryEditors: sqlLab.queryEditors,
queries: sqlLab.queries,
tabHistory: sqlLab.tabHistory,
tables: sqlLab.tables,
defaultDbId: sqlLab.defaultDbId,
displayLimit: common.conf.DISPLAY_MAX_ROW,
offline: sqlLab.offline,
defaultQueryLimit: common.conf.DEFAULT_SQLLAB_LIMIT,
maxRow: common.conf.SQL_MAX_ROW,
saveQueryWarning: common.conf.SQLLAB_SAVE_WARNING_MESSAGE,
scheduleQueryWarning: common.conf.SQLLAB_SCHEDULE_WARNING_MESSAGE,
requestedQuery,
};
}
function mapDispatchToProps(dispatch) {
return {
actions: bindActionCreators(Actions, dispatch),
};
}
export { TabbedSqlEditors };
export default connect(mapStateToProps, mapDispatchToProps)(TabbedSqlEditors);