443 lines
14 KiB
JavaScript
443 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 = Object.values(nextProps.queries).filter(
|
|
query => query.sqlEditorId === nextActiveQeId,
|
|
);
|
|
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" />
|
|
|
|
</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 default connect(mapStateToProps, mapDispatchToProps)(TabbedSqlEditors);
|