mirror of
https://github.com/apache/superset.git
synced 2026-04-18 23:55:00 +00:00
[WiP] rename project from Caravel to Superset (#1576)
* Change in files * Renamin files and folders * cleaning up a single piece of lint * Removing boat picture from docs * add superset word mark * Update rename note in docs * Fixing images * Pinning datatables * Fixing issues with mapbox-gl * Forgot to rename one file * Linting * v0.13.0 * adding pyyaml to dev-reqs
This commit is contained in:
committed by
GitHub
parent
973537fd9a
commit
15b67b2c6c
19
superset/assets/javascripts/SqlLab/TODO.md
Normal file
19
superset/assets/javascripts/SqlLab/TODO.md
Normal file
@@ -0,0 +1,19 @@
|
||||
|
||||
# TODO
|
||||
* Figure out how to organize the left panel, integrate Search
|
||||
* collapse sql beyond 10 lines
|
||||
* Security per-database (dropdown)
|
||||
* Get a to work
|
||||
|
||||
## Cosmetic
|
||||
* Result set font is too big
|
||||
* lmiit/timer/buttons wrap
|
||||
* table label is transparent
|
||||
* SqlEditor buttons
|
||||
* use react-bootstrap-prompt for query title input
|
||||
* Make tabs look great
|
||||
|
||||
# PROJECT
|
||||
* Write Runbook
|
||||
* Confirm backups
|
||||
* merge chef branch
|
||||
296
superset/assets/javascripts/SqlLab/actions.js
Normal file
296
superset/assets/javascripts/SqlLab/actions.js
Normal file
@@ -0,0 +1,296 @@
|
||||
import shortid from 'shortid';
|
||||
import { now } from '../modules/dates';
|
||||
const $ = require('jquery');
|
||||
|
||||
export const RESET_STATE = 'RESET_STATE';
|
||||
export const ADD_QUERY_EDITOR = 'ADD_QUERY_EDITOR';
|
||||
export const CLONE_QUERY_TO_NEW_TAB = 'CLONE_QUERY_TO_NEW_TAB';
|
||||
export const REMOVE_QUERY_EDITOR = 'REMOVE_QUERY_EDITOR';
|
||||
export const MERGE_TABLE = 'MERGE_TABLE';
|
||||
export const REMOVE_TABLE = 'REMOVE_TABLE';
|
||||
export const END_QUERY = 'END_QUERY';
|
||||
export const REMOVE_QUERY = 'REMOVE_QUERY';
|
||||
export const EXPAND_TABLE = 'EXPAND_TABLE';
|
||||
export const COLLAPSE_TABLE = 'COLLAPSE_TABLE';
|
||||
export const QUERY_EDITOR_SETDB = 'QUERY_EDITOR_SETDB';
|
||||
export const QUERY_EDITOR_SET_SCHEMA = 'QUERY_EDITOR_SET_SCHEMA';
|
||||
export const QUERY_EDITOR_SET_TITLE = 'QUERY_EDITOR_SET_TITLE';
|
||||
export const QUERY_EDITOR_SET_AUTORUN = 'QUERY_EDITOR_SET_AUTORUN';
|
||||
export const QUERY_EDITOR_SET_SQL = 'QUERY_EDITOR_SET_SQL';
|
||||
export const QUERY_EDITOR_SET_SELECTED_TEXT = 'QUERY_EDITOR_SET_SELECTED_TEXT';
|
||||
export const SET_DATABASES = 'SET_DATABASES';
|
||||
export const SET_ACTIVE_QUERY_EDITOR = 'SET_ACTIVE_QUERY_EDITOR';
|
||||
export const SET_ACTIVE_SOUTHPANE_TAB = 'SET_ACTIVE_SOUTHPANE_TAB';
|
||||
export const ADD_ALERT = 'ADD_ALERT';
|
||||
export const REMOVE_ALERT = 'REMOVE_ALERT';
|
||||
export const REFRESH_QUERIES = 'REFRESH_QUERIES';
|
||||
export const SET_NETWORK_STATUS = 'SET_NETWORK_STATUS';
|
||||
export const RUN_QUERY = 'RUN_QUERY';
|
||||
export const START_QUERY = 'START_QUERY';
|
||||
export const STOP_QUERY = 'STOP_QUERY';
|
||||
export const REQUEST_QUERY_RESULTS = 'REQUEST_QUERY_RESULTS';
|
||||
export const QUERY_SUCCESS = 'QUERY_SUCCESS';
|
||||
export const QUERY_FAILED = 'QUERY_FAILED';
|
||||
export const CLEAR_QUERY_RESULTS = 'CLEAR_QUERY_RESULTS';
|
||||
export const REMOVE_DATA_PREVIEW = 'REMOVE_DATA_PREVIEW';
|
||||
export const CHANGE_DATA_PREVIEW_ID = 'CHANGE_DATA_PREVIEW_ID';
|
||||
|
||||
export function resetState() {
|
||||
return { type: RESET_STATE };
|
||||
}
|
||||
|
||||
export function startQuery(query) {
|
||||
Object.assign(query, {
|
||||
id: query.id ? query.id : shortid.generate(),
|
||||
progress: 0,
|
||||
startDttm: now(),
|
||||
state: (query.runAsync) ? 'pending' : 'running',
|
||||
cached: false,
|
||||
});
|
||||
return { type: START_QUERY, query };
|
||||
}
|
||||
|
||||
export function querySuccess(query, results) {
|
||||
return { type: QUERY_SUCCESS, query, results };
|
||||
}
|
||||
|
||||
export function queryFailed(query, msg) {
|
||||
return { type: QUERY_FAILED, query, msg };
|
||||
}
|
||||
|
||||
export function stopQuery(query) {
|
||||
return { type: STOP_QUERY, query };
|
||||
}
|
||||
|
||||
export function clearQueryResults(query) {
|
||||
return { type: CLEAR_QUERY_RESULTS, query };
|
||||
}
|
||||
|
||||
export function removeDataPreview(table) {
|
||||
return { type: REMOVE_DATA_PREVIEW, table };
|
||||
}
|
||||
|
||||
export function requestQueryResults(query) {
|
||||
return { type: REQUEST_QUERY_RESULTS, query };
|
||||
}
|
||||
|
||||
export function fetchQueryResults(query) {
|
||||
return function (dispatch) {
|
||||
dispatch(requestQueryResults(query));
|
||||
const sqlJsonUrl = `/superset/results/${query.resultsKey}/`;
|
||||
$.ajax({
|
||||
type: 'GET',
|
||||
dataType: 'json',
|
||||
url: sqlJsonUrl,
|
||||
success(results) {
|
||||
dispatch(querySuccess(query, results));
|
||||
},
|
||||
error() {
|
||||
dispatch(queryFailed(query, 'Failed at retrieving results from the results backend'));
|
||||
},
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export function runQuery(query) {
|
||||
return function (dispatch) {
|
||||
dispatch(startQuery(query));
|
||||
const sqlJsonUrl = '/superset/sql_json/';
|
||||
const sqlJsonRequest = {
|
||||
client_id: query.id,
|
||||
database_id: query.dbId,
|
||||
json: true,
|
||||
runAsync: query.runAsync,
|
||||
schema: query.schema,
|
||||
sql: query.sql,
|
||||
sql_editor_id: query.sqlEditorId,
|
||||
tab: query.tab,
|
||||
tmp_table_name: query.tempTableName,
|
||||
select_as_cta: query.ctas,
|
||||
};
|
||||
$.ajax({
|
||||
type: 'POST',
|
||||
dataType: 'json',
|
||||
url: sqlJsonUrl,
|
||||
data: sqlJsonRequest,
|
||||
success(results) {
|
||||
if (!query.runAsync) {
|
||||
dispatch(querySuccess(query, results));
|
||||
}
|
||||
},
|
||||
error(err, textStatus, errorThrown) {
|
||||
let msg;
|
||||
try {
|
||||
msg = err.responseJSON.error;
|
||||
} catch (e) {
|
||||
if (err.responseText !== undefined) {
|
||||
msg = err.responseText;
|
||||
}
|
||||
}
|
||||
if (textStatus === 'error' && errorThrown === '') {
|
||||
msg = 'Could not connect to server';
|
||||
} else if (msg === null) {
|
||||
msg = `[${textStatus}] ${errorThrown}`;
|
||||
}
|
||||
dispatch(queryFailed(query, msg));
|
||||
},
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export function setDatabases(databases) {
|
||||
return { type: SET_DATABASES, databases };
|
||||
}
|
||||
|
||||
export function addQueryEditor(queryEditor) {
|
||||
const newQe = Object.assign({}, queryEditor, { id: shortid.generate() });
|
||||
return { type: ADD_QUERY_EDITOR, queryEditor: newQe };
|
||||
}
|
||||
|
||||
export function cloneQueryToNewTab(query) {
|
||||
return { type: CLONE_QUERY_TO_NEW_TAB, query };
|
||||
}
|
||||
|
||||
export function setNetworkStatus(networkOn) {
|
||||
return { type: SET_NETWORK_STATUS, networkOn };
|
||||
}
|
||||
|
||||
export function addAlert(alert) {
|
||||
const o = Object.assign({}, alert);
|
||||
o.id = shortid.generate();
|
||||
return { type: ADD_ALERT, o };
|
||||
}
|
||||
|
||||
export function removeAlert(alert) {
|
||||
return { type: REMOVE_ALERT, alert };
|
||||
}
|
||||
|
||||
export function setActiveQueryEditor(queryEditor) {
|
||||
return { type: SET_ACTIVE_QUERY_EDITOR, queryEditor };
|
||||
}
|
||||
|
||||
export function setActiveSouthPaneTab(tabId) {
|
||||
return { type: SET_ACTIVE_SOUTHPANE_TAB, tabId };
|
||||
}
|
||||
|
||||
export function removeQueryEditor(queryEditor) {
|
||||
return { type: REMOVE_QUERY_EDITOR, queryEditor };
|
||||
}
|
||||
|
||||
export function removeQuery(query) {
|
||||
return { type: REMOVE_QUERY, query };
|
||||
}
|
||||
|
||||
export function queryEditorSetDb(queryEditor, dbId) {
|
||||
return { type: QUERY_EDITOR_SETDB, queryEditor, dbId };
|
||||
}
|
||||
|
||||
export function queryEditorSetSchema(queryEditor, schema) {
|
||||
return { type: QUERY_EDITOR_SET_SCHEMA, queryEditor, schema };
|
||||
}
|
||||
|
||||
export function queryEditorSetAutorun(queryEditor, autorun) {
|
||||
return { type: QUERY_EDITOR_SET_AUTORUN, queryEditor, autorun };
|
||||
}
|
||||
|
||||
export function queryEditorSetTitle(queryEditor, title) {
|
||||
return { type: QUERY_EDITOR_SET_TITLE, queryEditor, title };
|
||||
}
|
||||
|
||||
export function queryEditorSetSql(queryEditor, sql) {
|
||||
return { type: QUERY_EDITOR_SET_SQL, queryEditor, sql };
|
||||
}
|
||||
|
||||
export function queryEditorSetSelectedText(queryEditor, sql) {
|
||||
return { type: QUERY_EDITOR_SET_SELECTED_TEXT, queryEditor, sql };
|
||||
}
|
||||
|
||||
export function mergeTable(table, query) {
|
||||
return { type: MERGE_TABLE, table, query };
|
||||
}
|
||||
|
||||
export function addTable(query, tableName) {
|
||||
return function (dispatch) {
|
||||
let url = `/superset/table/${query.dbId}/${tableName}/${query.schema}/`;
|
||||
$.get(url, (data) => {
|
||||
const dataPreviewQuery = {
|
||||
id: shortid.generate(),
|
||||
dbId: query.dbId,
|
||||
sql: data.selectStar,
|
||||
tableName,
|
||||
sqlEditorId: null,
|
||||
tab: '',
|
||||
runAsync: false,
|
||||
ctas: false,
|
||||
};
|
||||
// Merge table to tables in state
|
||||
dispatch(mergeTable(
|
||||
Object.assign(data, {
|
||||
dbId: query.dbId,
|
||||
queryEditorId: query.id,
|
||||
schema: query.schema,
|
||||
expanded: true,
|
||||
}), dataPreviewQuery)
|
||||
);
|
||||
// Run query to get preview data for table
|
||||
dispatch(runQuery(dataPreviewQuery));
|
||||
})
|
||||
.fail(() => {
|
||||
dispatch(
|
||||
addAlert({
|
||||
msg: 'Error occurred while fetching metadata',
|
||||
bsStyle: 'danger',
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
url = `/superset/extra_table_metadata/${query.dbId}/${tableName}/${query.schema}/`;
|
||||
$.get(url, (data) => {
|
||||
const table = {
|
||||
dbId: query.dbId,
|
||||
queryEditorId: query.id,
|
||||
schema: query.schema,
|
||||
name: tableName,
|
||||
};
|
||||
Object.assign(table, data);
|
||||
dispatch(mergeTable(table));
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export function changeDataPreviewId(oldQueryId, newQuery) {
|
||||
return { type: CHANGE_DATA_PREVIEW_ID, oldQueryId, newQuery };
|
||||
}
|
||||
|
||||
export function reFetchQueryResults(query) {
|
||||
return function (dispatch) {
|
||||
const newQuery = {
|
||||
id: shortid.generate(),
|
||||
dbId: query.dbId,
|
||||
sql: query.sql,
|
||||
tableName: query.tableName,
|
||||
sqlEditorId: null,
|
||||
tab: '',
|
||||
runAsync: false,
|
||||
ctas: false,
|
||||
};
|
||||
dispatch(runQuery(newQuery));
|
||||
dispatch(changeDataPreviewId(query.id, newQuery));
|
||||
};
|
||||
}
|
||||
|
||||
export function expandTable(table) {
|
||||
return { type: EXPAND_TABLE, table };
|
||||
}
|
||||
|
||||
export function collapseTable(table) {
|
||||
return { type: COLLAPSE_TABLE, table };
|
||||
}
|
||||
|
||||
export function removeTable(table) {
|
||||
return { type: REMOVE_TABLE, table };
|
||||
}
|
||||
|
||||
export function refreshQueries(alteredQueries) {
|
||||
return { type: REFRESH_QUERIES, alteredQueries };
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
import React from 'react';
|
||||
import AceEditor from 'react-ace';
|
||||
import 'brace/mode/sql';
|
||||
import 'brace/theme/github';
|
||||
import 'brace/ext/language_tools';
|
||||
import ace from 'brace';
|
||||
import { areArraysShallowEqual } from '../../reduxUtils';
|
||||
|
||||
const langTools = ace.acequire('ace/ext/language_tools');
|
||||
|
||||
const propTypes = {
|
||||
actions: React.PropTypes.object.isRequired,
|
||||
onBlur: React.PropTypes.func,
|
||||
onAltEnter: React.PropTypes.func,
|
||||
sql: React.PropTypes.string.isRequired,
|
||||
tables: React.PropTypes.array,
|
||||
queryEditor: React.PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
const defaultProps = {
|
||||
onBlur: () => {},
|
||||
onAltEnter: () => {},
|
||||
tables: [],
|
||||
};
|
||||
|
||||
class AceEditorWrapper extends React.PureComponent {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
sql: props.sql,
|
||||
};
|
||||
}
|
||||
componentDidMount() {
|
||||
// Making sure no text is selected from previous mount
|
||||
this.props.actions.queryEditorSetSelectedText(this.props.queryEditor, null);
|
||||
this.setAutoCompleter();
|
||||
}
|
||||
componentWillReceiveProps(nextProps) {
|
||||
if (!areArraysShallowEqual(this.props.tables, nextProps.tables)) {
|
||||
this.setAutoCompleter();
|
||||
}
|
||||
}
|
||||
textChange(text) {
|
||||
this.setState({ sql: text });
|
||||
}
|
||||
onBlur() {
|
||||
this.props.onBlur(this.state.sql);
|
||||
}
|
||||
getCompletions(aceEditor, session, pos, prefix, callback) {
|
||||
callback(null, this.state.words);
|
||||
}
|
||||
onEditorLoad(editor) {
|
||||
editor.commands.addCommand({
|
||||
name: 'runQuery',
|
||||
bindKey: { win: 'Alt-enter', mac: 'Alt-enter' },
|
||||
exec: () => {
|
||||
this.props.onAltEnter();
|
||||
},
|
||||
});
|
||||
editor.$blockScrolling = Infinity; // eslint-disable-line no-param-reassign
|
||||
editor.selection.on('changeSelection', () => {
|
||||
this.props.actions.queryEditorSetSelectedText(
|
||||
this.props.queryEditor, editor.getSelectedText());
|
||||
});
|
||||
}
|
||||
setAutoCompleter() {
|
||||
// Loading table and column names as auto-completable words
|
||||
let words = [];
|
||||
const columns = {};
|
||||
const tables = this.props.tables || [];
|
||||
tables.forEach(t => {
|
||||
words.push({ name: t.name, value: t.name, score: 55, meta: 'table' });
|
||||
const cols = t.columns || [];
|
||||
cols.forEach(col => {
|
||||
columns[col.name] = null; // using an object as a unique set
|
||||
});
|
||||
});
|
||||
words = words.concat(Object.keys(columns).map(col => (
|
||||
{ name: col, value: col, score: 50, meta: 'column' }
|
||||
)));
|
||||
this.setState({ words });
|
||||
const completer = {
|
||||
getCompletions: this.getCompletions.bind(this),
|
||||
};
|
||||
if (langTools) {
|
||||
langTools.setCompleters([completer, langTools.keyWordCompleter]);
|
||||
}
|
||||
}
|
||||
render() {
|
||||
return (
|
||||
<AceEditor
|
||||
mode="sql"
|
||||
theme="github"
|
||||
onLoad={this.onEditorLoad.bind(this)}
|
||||
onBlur={this.onBlur.bind(this)}
|
||||
minLines={8}
|
||||
maxLines={30}
|
||||
onChange={this.textChange.bind(this)}
|
||||
height="200px"
|
||||
width="100%"
|
||||
editorProps={{ $blockScrolling: true }}
|
||||
enableLiveAutocompletion
|
||||
value={this.state.sql}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
AceEditorWrapper.defaultProps = defaultProps;
|
||||
AceEditorWrapper.propTypes = propTypes;
|
||||
|
||||
export default AceEditorWrapper;
|
||||
34
superset/assets/javascripts/SqlLab/components/Alerts.jsx
Normal file
34
superset/assets/javascripts/SqlLab/components/Alerts.jsx
Normal file
@@ -0,0 +1,34 @@
|
||||
import React from 'react';
|
||||
import { Alert } from 'react-bootstrap';
|
||||
|
||||
class Alerts extends React.PureComponent {
|
||||
removeAlert(alert) {
|
||||
this.props.actions.removeAlert(alert);
|
||||
}
|
||||
render() {
|
||||
const alerts = this.props.alerts.map((alert) =>
|
||||
<Alert
|
||||
key={alert.id}
|
||||
bsStyle={alert.bsStyle}
|
||||
style={{ width: '500px', textAlign: 'midddle', margin: '10px auto' }}
|
||||
>
|
||||
{alert.msg}
|
||||
<i
|
||||
className="fa fa-close pull-right"
|
||||
onClick={this.removeAlert.bind(this, alert)}
|
||||
style={{ cursor: 'pointer' }}
|
||||
/>
|
||||
</Alert>
|
||||
);
|
||||
return (
|
||||
<div>{alerts}</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Alerts.propTypes = {
|
||||
alerts: React.PropTypes.array,
|
||||
actions: React.PropTypes.object,
|
||||
};
|
||||
|
||||
export default Alerts;
|
||||
76
superset/assets/javascripts/SqlLab/components/App.jsx
Normal file
76
superset/assets/javascripts/SqlLab/components/App.jsx
Normal file
@@ -0,0 +1,76 @@
|
||||
import * as Actions from '../actions';
|
||||
import React from 'react';
|
||||
|
||||
import TabbedSqlEditors from './TabbedSqlEditors';
|
||||
import QueryAutoRefresh from './QueryAutoRefresh';
|
||||
import QuerySearch from './QuerySearch';
|
||||
import Alerts from './Alerts';
|
||||
|
||||
import { bindActionCreators } from 'redux';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
class App extends React.PureComponent {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
hash: window.location.hash,
|
||||
};
|
||||
}
|
||||
componentDidMount() {
|
||||
window.addEventListener('hashchange', this.onHashChanged.bind(this));
|
||||
}
|
||||
componentWillUnmount() {
|
||||
window.removeEventListener('hashchange', this.onHashChanged.bind(this));
|
||||
}
|
||||
onHashChanged() {
|
||||
this.setState({ hash: window.location.hash });
|
||||
}
|
||||
render() {
|
||||
let content;
|
||||
if (this.state.hash) {
|
||||
content = (
|
||||
<div className="container-fluid">
|
||||
<div className="row">
|
||||
<div className="col-md-12">
|
||||
<QuerySearch />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
content = (
|
||||
<div>
|
||||
<QueryAutoRefresh />
|
||||
<TabbedSqlEditors />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<div className="App SqlLab">
|
||||
<Alerts alerts={this.props.alerts} actions={this.props.actions} />
|
||||
<div className="container-fluid">
|
||||
{content}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
App.propTypes = {
|
||||
alerts: React.PropTypes.array,
|
||||
actions: React.PropTypes.object,
|
||||
};
|
||||
|
||||
function mapStateToProps(state) {
|
||||
return {
|
||||
alerts: state.alerts,
|
||||
};
|
||||
}
|
||||
function mapDispatchToProps(dispatch) {
|
||||
return {
|
||||
actions: bindActionCreators(Actions, dispatch),
|
||||
};
|
||||
}
|
||||
|
||||
export { App };
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(App);
|
||||
@@ -0,0 +1,59 @@
|
||||
import React from 'react';
|
||||
|
||||
import { OverlayTrigger, Tooltip } from 'react-bootstrap';
|
||||
|
||||
const propTypes = {
|
||||
column: React.PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
const iconMap = {
|
||||
pk: 'fa-key',
|
||||
fk: 'fa-link',
|
||||
index: 'fa-bookmark',
|
||||
};
|
||||
const tooltipTitleMap = {
|
||||
pk: 'Primary Key',
|
||||
fk: 'Foreign Key',
|
||||
index: 'Index',
|
||||
};
|
||||
|
||||
class ColumnElement extends React.PureComponent {
|
||||
render() {
|
||||
const col = this.props.column;
|
||||
let name = col.name;
|
||||
let icons;
|
||||
if (col.keys && col.keys.length > 0) {
|
||||
name = <strong>{col.name}</strong>;
|
||||
icons = col.keys.map((key, i) => (
|
||||
<span key={i} className="ColumnElement">
|
||||
<OverlayTrigger
|
||||
placement="right"
|
||||
overlay={
|
||||
<Tooltip id="idx-json" bsSize="lg">
|
||||
<strong>{tooltipTitleMap[key.type]}</strong>
|
||||
<hr />
|
||||
<pre className="text-small">
|
||||
{JSON.stringify(key, null, ' ')}
|
||||
</pre>
|
||||
</Tooltip>
|
||||
}
|
||||
>
|
||||
<i className={`fa text-muted m-l-2 ${iconMap[key.type]}`} />
|
||||
</OverlayTrigger>
|
||||
</span>
|
||||
));
|
||||
}
|
||||
return (
|
||||
<div className="clearfix table-column">
|
||||
<div className="pull-left m-l-10 col-name">
|
||||
{name}{icons}
|
||||
</div>
|
||||
<div className="pull-right text-muted">
|
||||
<small> {col.type}</small>
|
||||
</div>
|
||||
</div>);
|
||||
}
|
||||
}
|
||||
ColumnElement.propTypes = propTypes;
|
||||
|
||||
export default ColumnElement;
|
||||
@@ -0,0 +1,50 @@
|
||||
import React from 'react';
|
||||
import CopyToClipboard from '../../components/CopyToClipboard';
|
||||
import { getShortUrl } from '../../../utils/common';
|
||||
|
||||
const propTypes = {
|
||||
queryEditor: React.PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
export default class CopyQueryTabUrl extends React.PureComponent {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
shortUrl: '',
|
||||
};
|
||||
}
|
||||
|
||||
componentWillMount() {
|
||||
const qe = this.props.queryEditor;
|
||||
const params = [];
|
||||
if (qe.dbId) params.push('dbid=' + qe.dbId);
|
||||
if (qe.title) params.push('title=' + encodeURIComponent(qe.title));
|
||||
if (qe.schema) params.push('schema=' + encodeURIComponent(qe.schema));
|
||||
if (qe.autorun) params.push('autorun=' + qe.autorun);
|
||||
if (qe.sql) params.push('sql=' + encodeURIComponent(qe.sql));
|
||||
|
||||
const queryString = params.join('&');
|
||||
const queryLink = window.location.pathname + '?' + queryString;
|
||||
getShortUrl(queryLink, this.onShortUrlSuccess.bind(this));
|
||||
}
|
||||
|
||||
onShortUrlSuccess(data) {
|
||||
this.setState({
|
||||
shortUrl: data,
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<CopyToClipboard
|
||||
inMenu
|
||||
text={this.state.shortUrl}
|
||||
copyNode={<span>share query</span>}
|
||||
tooltipText="copy URL to clipboard"
|
||||
shouldShowText={false}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
CopyQueryTabUrl.propTypes = propTypes;
|
||||
@@ -0,0 +1,58 @@
|
||||
import * as Actions from '../actions';
|
||||
import React from 'react';
|
||||
import { bindActionCreators } from 'redux';
|
||||
import { connect } from 'react-redux';
|
||||
import { Modal } from 'react-bootstrap';
|
||||
|
||||
import ResultSet from './ResultSet';
|
||||
|
||||
const propTypes = {
|
||||
queries: React.PropTypes.object,
|
||||
actions: React.PropTypes.object,
|
||||
showDataPreviewModal: React.PropTypes.bool,
|
||||
dataPreviewQueryId: React.PropTypes.string,
|
||||
};
|
||||
|
||||
class DataPreviewModal extends React.PureComponent {
|
||||
hide() {
|
||||
this.props.actions.hideDataPreview();
|
||||
}
|
||||
render() {
|
||||
if (this.props.showDataPreviewModal && this.props.dataPreviewQueryId) {
|
||||
const query = this.props.queries[this.props.dataPreviewQueryId];
|
||||
return (
|
||||
<Modal
|
||||
show={this.props.showDataPreviewModal}
|
||||
onHide={this.hide.bind(this)}
|
||||
bsStyle="lg"
|
||||
>
|
||||
<Modal.Header closeButton>
|
||||
<Modal.Title>
|
||||
Data preview for <strong>{query.tableName}</strong>
|
||||
</Modal.Title>
|
||||
</Modal.Header>
|
||||
<Modal.Body>
|
||||
<ResultSet query={query} visualize={false} csv={false} actions={this.props.actions} />
|
||||
</Modal.Body>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
DataPreviewModal.propTypes = propTypes;
|
||||
|
||||
function mapStateToProps(state) {
|
||||
return {
|
||||
queries: state.queries,
|
||||
showDataPreviewModal: state.showDataPreviewModal,
|
||||
dataPreviewQueryId: state.dataPreviewQueryId,
|
||||
};
|
||||
}
|
||||
function mapDispatchToProps(dispatch) {
|
||||
return {
|
||||
actions: bindActionCreators(Actions, dispatch),
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(DataPreviewModal);
|
||||
@@ -0,0 +1,53 @@
|
||||
const $ = window.$ = require('jquery');
|
||||
import React from 'react';
|
||||
import Select from 'react-select';
|
||||
|
||||
class DatabaseSelect extends React.PureComponent {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
databaseLoading: false,
|
||||
databaseOptions: [],
|
||||
};
|
||||
}
|
||||
componentDidMount() {
|
||||
this.fetchDatabaseOptions();
|
||||
}
|
||||
changeDb(db) {
|
||||
this.props.onChange(db);
|
||||
}
|
||||
fetchDatabaseOptions() {
|
||||
this.setState({ databaseLoading: true });
|
||||
const url = '/databaseasync/api/read?_flt_0_expose_in_sqllab=1';
|
||||
$.get(url, (data) => {
|
||||
const options = data.result.map((db) => ({ value: db.id, label: db.database_name }));
|
||||
this.setState({ databaseOptions: options, databaseLoading: false });
|
||||
this.props.actions.setDatabases(data.result);
|
||||
});
|
||||
}
|
||||
render() {
|
||||
return (
|
||||
<div>
|
||||
<Select
|
||||
name="select-db"
|
||||
placeholder={`Select a database (${this.state.databaseOptions.length})`}
|
||||
options={this.state.databaseOptions}
|
||||
value={this.props.databaseId}
|
||||
isLoading={this.state.databaseLoading}
|
||||
autosize={false}
|
||||
onChange={this.changeDb.bind(this)}
|
||||
valueRenderer={this.props.valueRenderer}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
DatabaseSelect.propTypes = {
|
||||
onChange: React.PropTypes.func,
|
||||
actions: React.PropTypes.object,
|
||||
databaseId: React.PropTypes.number,
|
||||
valueRenderer: React.PropTypes.func,
|
||||
};
|
||||
|
||||
export default DatabaseSelect;
|
||||
@@ -0,0 +1,93 @@
|
||||
import React from 'react';
|
||||
import { Well } from 'react-bootstrap';
|
||||
import SyntaxHighlighter from 'react-syntax-highlighter';
|
||||
import { github } from 'react-syntax-highlighter/dist/styles';
|
||||
import ModalTrigger from '../../components/ModalTrigger';
|
||||
|
||||
const defaultProps = {
|
||||
maxWidth: 50,
|
||||
maxLines: 5,
|
||||
shrink: false,
|
||||
};
|
||||
|
||||
const propTypes = {
|
||||
sql: React.PropTypes.string.isRequired,
|
||||
rawSql: React.PropTypes.string,
|
||||
maxWidth: React.PropTypes.number,
|
||||
maxLines: React.PropTypes.number,
|
||||
shrink: React.PropTypes.bool,
|
||||
};
|
||||
|
||||
class HighlightedSql extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
modalBody: null,
|
||||
};
|
||||
}
|
||||
shrinkSql() {
|
||||
const props = this.props;
|
||||
const sql = props.sql || '';
|
||||
let lines = sql.split('\n');
|
||||
if (lines.length >= props.maxLines) {
|
||||
lines = lines.slice(0, props.maxLines);
|
||||
lines.push('{...}');
|
||||
}
|
||||
return lines.map((line) => {
|
||||
if (line.length > props.maxWidth) {
|
||||
return line.slice(0, props.maxWidth) + '{...}';
|
||||
}
|
||||
return line;
|
||||
})
|
||||
.join('\n');
|
||||
}
|
||||
triggerNode() {
|
||||
const props = this.props;
|
||||
let shownSql = props.shrink ? this.shrinkSql(props.sql) : props.sql;
|
||||
return (
|
||||
<Well>
|
||||
<SyntaxHighlighter language="sql" style={github}>
|
||||
{shownSql}
|
||||
</SyntaxHighlighter>
|
||||
</Well>);
|
||||
}
|
||||
generateModal() {
|
||||
const props = this.props;
|
||||
let rawSql;
|
||||
if (props.rawSql && props.rawSql !== this.props.sql) {
|
||||
rawSql = (
|
||||
<div>
|
||||
<h4>Raw SQL</h4>
|
||||
<SyntaxHighlighter language="sql" style={github}>
|
||||
{props.rawSql}
|
||||
</SyntaxHighlighter>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
this.setState({
|
||||
modalBody: (
|
||||
<div>
|
||||
<h4>Source SQL</h4>
|
||||
<SyntaxHighlighter language="sql" style={github}>
|
||||
{this.props.sql}
|
||||
</SyntaxHighlighter>
|
||||
{rawSql}
|
||||
</div>
|
||||
),
|
||||
});
|
||||
}
|
||||
render() {
|
||||
return (
|
||||
<ModalTrigger
|
||||
modalTitle="SQL"
|
||||
triggerNode={this.triggerNode()}
|
||||
modalBody={this.state.modalBody}
|
||||
beforeOpen={this.generateModal.bind(this)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
HighlightedSql.propTypes = propTypes;
|
||||
HighlightedSql.defaultProps = defaultProps;
|
||||
|
||||
export default HighlightedSql;
|
||||
58
superset/assets/javascripts/SqlLab/components/Link.jsx
Normal file
58
superset/assets/javascripts/SqlLab/components/Link.jsx
Normal file
@@ -0,0 +1,58 @@
|
||||
import React from 'react';
|
||||
import { OverlayTrigger, Tooltip } from 'react-bootstrap';
|
||||
|
||||
const propTypes = {
|
||||
children: React.PropTypes.node,
|
||||
className: React.PropTypes.string,
|
||||
href: React.PropTypes.string,
|
||||
onClick: React.PropTypes.func,
|
||||
placement: React.PropTypes.string,
|
||||
style: React.PropTypes.object,
|
||||
tooltip: React.PropTypes.string,
|
||||
};
|
||||
const defaultProps = {
|
||||
className: '',
|
||||
href: '#',
|
||||
onClick: () => {},
|
||||
placement: 'top',
|
||||
style: {},
|
||||
tooltip: null,
|
||||
};
|
||||
|
||||
|
||||
class Link extends React.PureComponent {
|
||||
render() {
|
||||
let tooltip = (
|
||||
<Tooltip id="tooltip">
|
||||
{this.props.tooltip}
|
||||
</Tooltip>
|
||||
);
|
||||
const link = (
|
||||
<a
|
||||
href={this.props.href}
|
||||
onClick={this.props.onClick}
|
||||
style={this.props.style}
|
||||
className={'Link ' + this.props.className}
|
||||
>
|
||||
{this.props.children}
|
||||
</a>
|
||||
);
|
||||
if (this.props.tooltip) {
|
||||
return (
|
||||
<OverlayTrigger
|
||||
overlay={tooltip}
|
||||
placement={this.props.placement}
|
||||
delayShow={300}
|
||||
delayHide={150}
|
||||
>
|
||||
{link}
|
||||
</OverlayTrigger>
|
||||
);
|
||||
}
|
||||
return link;
|
||||
}
|
||||
}
|
||||
Link.propTypes = propTypes;
|
||||
Link.defaultProps = defaultProps;
|
||||
|
||||
export default Link;
|
||||
@@ -0,0 +1,63 @@
|
||||
import React from 'react';
|
||||
import { bindActionCreators } from 'redux';
|
||||
import { connect } from 'react-redux';
|
||||
import * as Actions from '../actions';
|
||||
|
||||
const $ = require('jquery');
|
||||
const QUERY_UPDATE_FREQ = 1000;
|
||||
const QUERY_UPDATE_BUFFER_MS = 5000;
|
||||
|
||||
class QueryAutoRefresh extends React.PureComponent {
|
||||
componentWillMount() {
|
||||
this.startTimer();
|
||||
}
|
||||
componentWillUnmount() {
|
||||
this.stopTimer();
|
||||
}
|
||||
startTimer() {
|
||||
if (!(this.timer)) {
|
||||
this.timer = setInterval(this.stopwatch.bind(this), QUERY_UPDATE_FREQ);
|
||||
}
|
||||
}
|
||||
stopTimer() {
|
||||
clearInterval(this.timer);
|
||||
this.timer = null;
|
||||
}
|
||||
stopwatch() {
|
||||
const url = '/superset/queries/' + (this.props.queriesLastUpdate - QUERY_UPDATE_BUFFER_MS);
|
||||
// No updates in case of failure.
|
||||
$.getJSON(url, (data) => {
|
||||
if (Object.keys(data).length > 0) {
|
||||
this.props.actions.refreshQueries(data);
|
||||
}
|
||||
this.props.actions.setNetworkStatus(true);
|
||||
})
|
||||
.fail(() => {
|
||||
this.props.actions.setNetworkStatus(false);
|
||||
});
|
||||
}
|
||||
render() {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
QueryAutoRefresh.propTypes = {
|
||||
actions: React.PropTypes.object,
|
||||
queriesLastUpdate: React.PropTypes.number,
|
||||
};
|
||||
QueryAutoRefresh.defaultProps = {
|
||||
// queries: null,
|
||||
};
|
||||
|
||||
function mapStateToProps(state) {
|
||||
return {
|
||||
queriesLastUpdate: state.queriesLastUpdate,
|
||||
};
|
||||
}
|
||||
|
||||
function mapDispatchToProps(dispatch) {
|
||||
return {
|
||||
actions: bindActionCreators(Actions, dispatch),
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(QueryAutoRefresh);
|
||||
@@ -0,0 +1,32 @@
|
||||
import React from 'react';
|
||||
|
||||
import QueryTable from './QueryTable';
|
||||
import { Alert } from 'react-bootstrap';
|
||||
|
||||
const propTypes = {
|
||||
queries: React.PropTypes.array.isRequired,
|
||||
actions: React.PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
const QueryHistory = (props) => {
|
||||
if (props.queries.length > 0) {
|
||||
return (
|
||||
<QueryTable
|
||||
columns={[
|
||||
'state', 'started', 'duration', 'progress',
|
||||
'rows', 'sql', 'output', 'actions',
|
||||
]}
|
||||
queries={props.queries}
|
||||
actions={props.actions}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Alert bsStyle="info">
|
||||
No query history yet...
|
||||
</Alert>
|
||||
);
|
||||
};
|
||||
QueryHistory.propTypes = propTypes;
|
||||
|
||||
export default QueryHistory;
|
||||
203
superset/assets/javascripts/SqlLab/components/QuerySearch.jsx
Normal file
203
superset/assets/javascripts/SqlLab/components/QuerySearch.jsx
Normal file
@@ -0,0 +1,203 @@
|
||||
const $ = window.$ = require('jquery');
|
||||
import React from 'react';
|
||||
import { Button } from 'react-bootstrap';
|
||||
import Select from 'react-select';
|
||||
import QueryTable from './QueryTable';
|
||||
import DatabaseSelect from './DatabaseSelect';
|
||||
import { now, epochTimeXHoursAgo,
|
||||
epochTimeXDaysAgo, epochTimeXYearsAgo } from '../../modules/dates';
|
||||
import { STATUS_OPTIONS, TIME_OPTIONS } from '../constants';
|
||||
|
||||
const propTypes = {
|
||||
actions: React.PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
class QuerySearch extends React.PureComponent {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
userLoading: false,
|
||||
userOptions: [],
|
||||
databaseId: null,
|
||||
userId: null,
|
||||
searchText: null,
|
||||
from: null,
|
||||
to: null,
|
||||
status: 'success',
|
||||
queriesArray: [],
|
||||
};
|
||||
}
|
||||
componentWillMount() {
|
||||
this.fetchUsers();
|
||||
this.refreshQueries();
|
||||
}
|
||||
onUserClicked(userId) {
|
||||
this.setState({ userId }, () => { this.refreshQueries(); });
|
||||
}
|
||||
onDbClicked(dbId) {
|
||||
this.setState({ databaseId: dbId }, () => { this.refreshQueries(); });
|
||||
}
|
||||
onChange(db) {
|
||||
const val = (db) ? db.value : null;
|
||||
this.setState({ databaseId: val });
|
||||
}
|
||||
getTimeFromSelection(selection) {
|
||||
switch (selection) {
|
||||
case 'now':
|
||||
return now();
|
||||
case '1 hour ago':
|
||||
return epochTimeXHoursAgo(1);
|
||||
case '1 day ago':
|
||||
return epochTimeXDaysAgo(1);
|
||||
case '7 days ago':
|
||||
return epochTimeXDaysAgo(7);
|
||||
case '28 days ago':
|
||||
return epochTimeXDaysAgo(28);
|
||||
case '90 days ago':
|
||||
return epochTimeXDaysAgo(90);
|
||||
case '1 year ago':
|
||||
return epochTimeXYearsAgo(1);
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
changeFrom(user) {
|
||||
const val = (user) ? user.value : null;
|
||||
this.setState({ from: val });
|
||||
}
|
||||
changeTo(status) {
|
||||
const val = (status) ? status.value : null;
|
||||
this.setState({ to: val });
|
||||
}
|
||||
changeUser(user) {
|
||||
const val = (user) ? user.value : null;
|
||||
this.setState({ userId: val });
|
||||
}
|
||||
insertParams(baseUrl, params) {
|
||||
const validParams = params.filter(
|
||||
function (p) { return p !== ''; }
|
||||
);
|
||||
return baseUrl + '?' + validParams.join('&');
|
||||
}
|
||||
changeStatus(status) {
|
||||
const val = (status) ? status.value : null;
|
||||
this.setState({ status: val });
|
||||
}
|
||||
changeSearch(event) {
|
||||
this.setState({ searchText: event.target.value });
|
||||
}
|
||||
fetchUsers() {
|
||||
this.setState({ userLoading: true });
|
||||
const url = '/users/api/read';
|
||||
$.getJSON(url, (data, status) => {
|
||||
if (status === 'success') {
|
||||
const options = [];
|
||||
for (let i = 0; i < data.pks.length; i++) {
|
||||
options.push({ value: data.pks[i], label: data.result[i].username });
|
||||
}
|
||||
this.setState({ userOptions: options, userLoading: false });
|
||||
}
|
||||
});
|
||||
}
|
||||
refreshQueries() {
|
||||
const params = [
|
||||
this.state.userId ? `user_id=${this.state.userId}` : '',
|
||||
this.state.databaseId ? `database_id=${this.state.databaseId}` : '',
|
||||
this.state.searchText ? `search_text=${this.state.searchText}` : '',
|
||||
this.state.status ? `status=${this.state.status}` : '',
|
||||
this.state.from ? `from=${this.getTimeFromSelection(this.state.from)}` : '',
|
||||
this.state.to ? `to=${this.getTimeFromSelection(this.state.to)}` : '',
|
||||
];
|
||||
|
||||
const url = this.insertParams('/superset/search_queries', params);
|
||||
$.getJSON(url, (data, status) => {
|
||||
if (status === 'success') {
|
||||
const newQueriesArray = [];
|
||||
for (const id in data) {
|
||||
newQueriesArray.push(data[id]);
|
||||
}
|
||||
this.setState({ queriesArray: newQueriesArray });
|
||||
}
|
||||
});
|
||||
}
|
||||
render() {
|
||||
return (
|
||||
<div>
|
||||
<div className="row space-1">
|
||||
<div className="col-sm-2">
|
||||
<Select
|
||||
name="select-user"
|
||||
placeholder="[User]"
|
||||
options={this.state.userOptions}
|
||||
value={this.state.userId}
|
||||
isLoading={this.state.userLoading}
|
||||
autosize={false}
|
||||
onChange={this.changeUser.bind(this)}
|
||||
/>
|
||||
</div>
|
||||
<div className="col-sm-2">
|
||||
<DatabaseSelect
|
||||
onChange={this.onChange.bind(this)}
|
||||
databaseId={this.state.databaseId}
|
||||
/>
|
||||
</div>
|
||||
<div className="col-sm-4">
|
||||
<input
|
||||
type="text"
|
||||
onChange={this.changeSearch.bind(this)}
|
||||
className="form-control input-sm"
|
||||
placeholder="Search Results"
|
||||
/>
|
||||
</div>
|
||||
<div className="col-sm-1">
|
||||
<Select
|
||||
name="select-from"
|
||||
placeholder="[From]-"
|
||||
options={TIME_OPTIONS.
|
||||
slice(1, TIME_OPTIONS.length).map((t) => ({ value: t, label: t }))}
|
||||
value={this.state.from}
|
||||
autosize={false}
|
||||
onChange={this.changeFrom.bind(this)}
|
||||
/>
|
||||
</div>
|
||||
<div className="col-sm-1">
|
||||
<Select
|
||||
name="select-to"
|
||||
placeholder="[To]-"
|
||||
options={TIME_OPTIONS.map((t) => ({ value: t, label: t }))}
|
||||
value={this.state.to}
|
||||
autosize={false}
|
||||
onChange={this.changeTo.bind(this)}
|
||||
/>
|
||||
</div>
|
||||
<div className="col-sm-1">
|
||||
<Select
|
||||
name="select-status"
|
||||
placeholder="[Query Status]"
|
||||
options={STATUS_OPTIONS.map((s) => ({ value: s, label: s }))}
|
||||
value={this.state.status}
|
||||
isLoading={false}
|
||||
autosize={false}
|
||||
onChange={this.changeStatus.bind(this)}
|
||||
/>
|
||||
</div>
|
||||
<Button bsSize="small" bsStyle="success" onClick={this.refreshQueries.bind(this)}>
|
||||
Search
|
||||
</Button>
|
||||
</div>
|
||||
<QueryTable
|
||||
columns={[
|
||||
'state', 'db', 'user', 'date',
|
||||
'progress', 'rows', 'sql', 'querylink',
|
||||
]}
|
||||
onUserClicked={this.onUserClicked.bind(this)}
|
||||
onDbClicked={this.onDbClicked.bind(this)}
|
||||
queries={this.state.queriesArray}
|
||||
actions={this.props.actions}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
QuerySearch.propTypes = propTypes;
|
||||
export default QuerySearch;
|
||||
200
superset/assets/javascripts/SqlLab/components/QueryTable.jsx
Normal file
200
superset/assets/javascripts/SqlLab/components/QueryTable.jsx
Normal file
@@ -0,0 +1,200 @@
|
||||
import React from 'react';
|
||||
|
||||
import moment from 'moment';
|
||||
import { Table } from 'reactable';
|
||||
import { Label, ProgressBar } from 'react-bootstrap';
|
||||
import Link from './Link';
|
||||
import VisualizeModal from './VisualizeModal';
|
||||
import ResultSet from './ResultSet';
|
||||
import ModalTrigger from '../../components/ModalTrigger';
|
||||
import HighlightedSql from './HighlightedSql';
|
||||
import { STATE_BSSTYLE_MAP } from '../constants';
|
||||
import { fDuration } from '../../modules/dates';
|
||||
import { getLink } from '../../../utils/common';
|
||||
|
||||
const propTypes = {
|
||||
columns: React.PropTypes.array,
|
||||
actions: React.PropTypes.object,
|
||||
queries: React.PropTypes.array,
|
||||
onUserClicked: React.PropTypes.func,
|
||||
onDbClicked: React.PropTypes.func,
|
||||
};
|
||||
const defaultProps = {
|
||||
columns: ['started', 'duration', 'rows'],
|
||||
queries: [],
|
||||
onUserClicked: () => {},
|
||||
onDbClicked: () => {},
|
||||
};
|
||||
|
||||
|
||||
class QueryTable extends React.PureComponent {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
const uri = window.location.toString();
|
||||
const cleanUri = uri.substring(0, uri.indexOf('#'));
|
||||
this.state = {
|
||||
cleanUri,
|
||||
showVisualizeModal: false,
|
||||
activeQuery: null,
|
||||
};
|
||||
}
|
||||
getQueryLink(dbId, sql) {
|
||||
const params = ['dbid=' + dbId, 'sql=' + sql, 'title=Untitled Query'];
|
||||
const link = getLink(this.state.cleanUri, params);
|
||||
return encodeURI(link);
|
||||
}
|
||||
hideVisualizeModal() {
|
||||
this.setState({ showVisualizeModal: false });
|
||||
}
|
||||
showVisualizeModal(query) {
|
||||
this.setState({ showVisualizeModal: true });
|
||||
this.setState({ activeQuery: query });
|
||||
}
|
||||
restoreSql(query) {
|
||||
this.props.actions.queryEditorSetSql({ id: query.sqlEditorId }, query.sql);
|
||||
}
|
||||
|
||||
openQueryInNewTab(query) {
|
||||
this.props.actions.cloneQueryToNewTab(query);
|
||||
}
|
||||
openAsyncResults(query) {
|
||||
this.props.actions.fetchQueryResults(query);
|
||||
}
|
||||
clearQueryResults(query) {
|
||||
this.props.actions.clearQueryResults(query);
|
||||
}
|
||||
removeQuery(query) {
|
||||
this.props.actions.removeQuery(query);
|
||||
}
|
||||
|
||||
render() {
|
||||
const data = this.props.queries.map((query) => {
|
||||
const q = Object.assign({}, query);
|
||||
if (q.endDttm) {
|
||||
q.duration = fDuration(q.startDttm, q.endDttm);
|
||||
}
|
||||
q.date = moment(q.startDttm).format('MMM Do YYYY');
|
||||
q.user = (
|
||||
<button
|
||||
className="btn btn-link btn-xs"
|
||||
onClick={this.props.onUserClicked.bind(this, q.userId)}
|
||||
>
|
||||
{q.user}
|
||||
</button>
|
||||
);
|
||||
q.db = (
|
||||
<button
|
||||
className="btn btn-link btn-xs"
|
||||
onClick={this.props.onDbClicked.bind(this, q.dbId)}
|
||||
>
|
||||
{q.db}
|
||||
</button>
|
||||
);
|
||||
q.started = moment(q.startDttm).format('HH:mm:ss');
|
||||
q.sql = (
|
||||
<HighlightedSql sql={q.sql} rawSql={q.executedSql} shrink maxWidth={60} />
|
||||
);
|
||||
if (q.resultsKey) {
|
||||
q.output = (
|
||||
<ModalTrigger
|
||||
bsSize="large"
|
||||
className="ResultsModal"
|
||||
triggerNode={(
|
||||
<Label
|
||||
bsStyle="info"
|
||||
style={{ cursor: 'pointer' }}
|
||||
>
|
||||
view results
|
||||
</Label>
|
||||
)}
|
||||
modalTitle={'Data preview'}
|
||||
beforeOpen={this.openAsyncResults.bind(this, query)}
|
||||
onExit={this.clearQueryResults.bind(this, query)}
|
||||
modalBody={<ResultSet showSql query={query} actions={this.props.actions} />}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
q.output = q.tempTable;
|
||||
}
|
||||
q.progress = (
|
||||
<ProgressBar
|
||||
style={{ width: '75px' }}
|
||||
striped
|
||||
now={q.progress}
|
||||
label={`${q.progress}%`}
|
||||
/>
|
||||
);
|
||||
let errorTooltip;
|
||||
if (q.errorMessage) {
|
||||
errorTooltip = (
|
||||
<Link tooltip={q.errorMessage}>
|
||||
<i className="fa fa-exclamation-circle text-danger" />
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
q.state = (
|
||||
<div>
|
||||
<span className={'m-r-3 label label-' + STATE_BSSTYLE_MAP[q.state]}>
|
||||
{q.state}
|
||||
</span>
|
||||
{errorTooltip}
|
||||
</div>
|
||||
);
|
||||
q.actions = (
|
||||
<div style={{ width: '75px' }}>
|
||||
<Link
|
||||
className="fa fa-line-chart m-r-3"
|
||||
tooltip="Visualize the data out of this query"
|
||||
onClick={this.showVisualizeModal.bind(this, query)}
|
||||
/>
|
||||
<Link
|
||||
className="fa fa-pencil m-r-3"
|
||||
onClick={this.restoreSql.bind(this, query)}
|
||||
tooltip="Overwrite text in editor with a query on this table"
|
||||
placement="top"
|
||||
/>
|
||||
<Link
|
||||
className="fa fa-plus-circle m-r-3"
|
||||
onClick={this.openQueryInNewTab.bind(this, query)}
|
||||
tooltip="Run query in a new tab"
|
||||
placement="top"
|
||||
/>
|
||||
<Link
|
||||
className="fa fa-trash m-r-3"
|
||||
tooltip="Remove query from log"
|
||||
onClick={this.removeQuery.bind(this, query)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
q.querylink = (
|
||||
<div style={{ width: '100px' }}>
|
||||
<a
|
||||
href={this.getQueryLink(q.dbId, q.sql)}
|
||||
className="btn btn-primary btn-xs"
|
||||
>
|
||||
<i className="fa fa-external-link" />Open in SQL Editor
|
||||
</a>
|
||||
</div>
|
||||
);
|
||||
return q;
|
||||
}).reverse();
|
||||
return (
|
||||
<div className="QueryTable">
|
||||
<VisualizeModal
|
||||
show={this.state.showVisualizeModal}
|
||||
query={this.state.activeQuery}
|
||||
onHide={this.hideVisualizeModal.bind(this)}
|
||||
/>
|
||||
<Table
|
||||
columns={this.props.columns}
|
||||
className="table table-condensed"
|
||||
data={data}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
QueryTable.propTypes = propTypes;
|
||||
QueryTable.defaultProps = defaultProps;
|
||||
|
||||
export default QueryTable;
|
||||
228
superset/assets/javascripts/SqlLab/components/ResultSet.jsx
Normal file
228
superset/assets/javascripts/SqlLab/components/ResultSet.jsx
Normal file
@@ -0,0 +1,228 @@
|
||||
import React from 'react';
|
||||
import { Alert, Button, ButtonGroup, ProgressBar } from 'react-bootstrap';
|
||||
import { Table } from 'reactable';
|
||||
import shortid from 'shortid';
|
||||
|
||||
import VisualizeModal from './VisualizeModal';
|
||||
import HighlightedSql from './HighlightedSql';
|
||||
|
||||
const propTypes = {
|
||||
actions: React.PropTypes.object,
|
||||
csv: React.PropTypes.bool,
|
||||
query: React.PropTypes.object,
|
||||
search: React.PropTypes.bool,
|
||||
searchText: React.PropTypes.string,
|
||||
showSql: React.PropTypes.bool,
|
||||
visualize: React.PropTypes.bool,
|
||||
cache: React.PropTypes.bool,
|
||||
};
|
||||
const defaultProps = {
|
||||
search: true,
|
||||
visualize: true,
|
||||
showSql: false,
|
||||
csv: true,
|
||||
searchText: '',
|
||||
actions: {},
|
||||
cache: false,
|
||||
};
|
||||
|
||||
|
||||
class ResultSet extends React.PureComponent {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
searchText: '',
|
||||
showModal: false,
|
||||
data: [],
|
||||
};
|
||||
}
|
||||
componentWillReceiveProps(nextProps) {
|
||||
// when new results comes in, save them locally and clear in store
|
||||
if (this.props.cache && (!nextProps.query.cached)
|
||||
&& nextProps.query.results
|
||||
&& nextProps.query.results.data.length > 0) {
|
||||
this.setState(
|
||||
{ data: nextProps.query.results.data },
|
||||
this.clearQueryResults(nextProps.query)
|
||||
);
|
||||
}
|
||||
}
|
||||
getControls() {
|
||||
if (this.props.search || this.props.visualize || this.props.csv) {
|
||||
let csvButton;
|
||||
if (this.props.csv) {
|
||||
csvButton = (
|
||||
<Button bsSize="small" href={'/superset/csv/' + this.props.query.id}>
|
||||
<i className="fa fa-file-text-o" /> .CSV
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
let visualizeButton;
|
||||
if (this.props.visualize) {
|
||||
visualizeButton = (
|
||||
<Button
|
||||
bsSize="small"
|
||||
onClick={this.showModal.bind(this)}
|
||||
>
|
||||
<i className="fa fa-line-chart m-l-1" /> Visualize
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
let searchBox;
|
||||
if (this.props.search) {
|
||||
searchBox = (
|
||||
<input
|
||||
type="text"
|
||||
onChange={this.changeSearch.bind(this)}
|
||||
className="form-control input-sm"
|
||||
placeholder="Search Results"
|
||||
/>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<div className="ResultSetControls">
|
||||
<div className="clearfix">
|
||||
<div className="pull-left">
|
||||
<ButtonGroup>
|
||||
{visualizeButton}
|
||||
{csvButton}
|
||||
</ButtonGroup>
|
||||
</div>
|
||||
<div className="pull-right">
|
||||
{searchBox}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return <div className="noControls" />;
|
||||
}
|
||||
clearQueryResults(query) {
|
||||
this.props.actions.clearQueryResults(query);
|
||||
}
|
||||
popSelectStar() {
|
||||
const qe = {
|
||||
id: shortid.generate(),
|
||||
title: this.props.query.tempTable,
|
||||
autorun: false,
|
||||
dbId: this.props.query.dbId,
|
||||
sql: `SELECT * FROM ${this.props.query.tempTable}`,
|
||||
};
|
||||
this.props.actions.addQueryEditor(qe);
|
||||
}
|
||||
showModal() {
|
||||
this.setState({ showModal: true });
|
||||
}
|
||||
hideModal() {
|
||||
this.setState({ showModal: false });
|
||||
}
|
||||
changeSearch(event) {
|
||||
this.setState({ searchText: event.target.value });
|
||||
}
|
||||
fetchResults(query) {
|
||||
this.props.actions.fetchQueryResults(query);
|
||||
}
|
||||
reFetchQueryResults(query) {
|
||||
this.props.actions.reFetchQueryResults(query);
|
||||
}
|
||||
render() {
|
||||
const query = this.props.query;
|
||||
const results = query.results;
|
||||
let data;
|
||||
if (this.props.cache && query.cached) {
|
||||
data = this.state.data;
|
||||
} else {
|
||||
data = results ? results.data : [];
|
||||
}
|
||||
|
||||
let sql;
|
||||
|
||||
if (this.props.showSql) {
|
||||
sql = <HighlightedSql sql={query.sql} />;
|
||||
}
|
||||
if (['running', 'pending', 'fetching'].indexOf(query.state) > -1) {
|
||||
let progressBar;
|
||||
if (query.progress > 0 && query.state === 'running') {
|
||||
progressBar = (
|
||||
<ProgressBar
|
||||
striped
|
||||
now={query.progress}
|
||||
label={`${query.progress}%`}
|
||||
/>);
|
||||
}
|
||||
return (
|
||||
<div>
|
||||
<img className="loading" alt="Loading..." src="/static/assets/images/loading.gif" />
|
||||
{progressBar}
|
||||
</div>
|
||||
);
|
||||
} else if (query.state === 'failed') {
|
||||
return <Alert bsStyle="danger">{query.errorMessage}</Alert>;
|
||||
} else if (query.state === 'success' && query.ctas) {
|
||||
return (
|
||||
<div>
|
||||
<Alert bsStyle="info">
|
||||
Table [<strong>{query.tempTable}</strong>] was
|
||||
created
|
||||
<Button
|
||||
bsSize="small"
|
||||
className="m-r-5"
|
||||
onClick={this.popSelectStar.bind(this)}
|
||||
>
|
||||
Query in a new tab
|
||||
</Button>
|
||||
</Alert>
|
||||
</div>);
|
||||
} else if (query.state === 'success') {
|
||||
if (results && data && data.length > 0) {
|
||||
return (
|
||||
<div>
|
||||
<VisualizeModal
|
||||
show={this.state.showModal}
|
||||
query={this.props.query}
|
||||
onHide={this.hideModal.bind(this)}
|
||||
/>
|
||||
{this.getControls.bind(this)()}
|
||||
{sql}
|
||||
<div className="ResultSet">
|
||||
<Table
|
||||
data={data}
|
||||
columns={results.columns.map((col) => col.name)}
|
||||
sortable
|
||||
className="table table-condensed table-bordered"
|
||||
filterBy={this.state.searchText}
|
||||
filterable={results.columns.map((c) => c.name)}
|
||||
hideFilterInput
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
} else if (query.resultsKey) {
|
||||
return (
|
||||
<div>
|
||||
<Alert bsStyle="warning">This query was run asynchronously
|
||||
<Button bsSize="sm" onClick={this.fetchResults.bind(this, query)}>
|
||||
Fetch results
|
||||
</Button>
|
||||
</Alert>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
if (query.cached) {
|
||||
return (
|
||||
<a
|
||||
href="#"
|
||||
onClick={this.reFetchQueryResults.bind(this, query)}
|
||||
>
|
||||
click to retrieve results
|
||||
</a>
|
||||
);
|
||||
}
|
||||
return (<Alert bsStyle="warning">The query returned no data</Alert>);
|
||||
}
|
||||
}
|
||||
ResultSet.propTypes = propTypes;
|
||||
ResultSet.defaultProps = defaultProps;
|
||||
|
||||
export default ResultSet;
|
||||
105
superset/assets/javascripts/SqlLab/components/SouthPane.jsx
Normal file
105
superset/assets/javascripts/SqlLab/components/SouthPane.jsx
Normal file
@@ -0,0 +1,105 @@
|
||||
import { Alert, Tab, Tabs } from 'react-bootstrap';
|
||||
import QueryHistory from './QueryHistory';
|
||||
import ResultSet from './ResultSet';
|
||||
import { connect } from 'react-redux';
|
||||
import { bindActionCreators } from 'redux';
|
||||
import * as Actions from '../actions';
|
||||
import React from 'react';
|
||||
import { areArraysShallowEqual } from '../../reduxUtils';
|
||||
|
||||
import shortid from 'shortid';
|
||||
|
||||
/*
|
||||
editorQueries are queries executed by users passed from SqlEditor component
|
||||
dataPrebiewQueries are all queries executed for preview of table data (from SqlEditorLeft)
|
||||
*/
|
||||
const propTypes = {
|
||||
editorQueries: React.PropTypes.array.isRequired,
|
||||
dataPreviewQueries: React.PropTypes.array.isRequired,
|
||||
actions: React.PropTypes.object.isRequired,
|
||||
activeSouthPaneTab: React.PropTypes.string,
|
||||
};
|
||||
|
||||
const defaultProps = {
|
||||
activeSouthPaneTab: 'Results',
|
||||
};
|
||||
|
||||
class SouthPane extends React.PureComponent {
|
||||
switchTab(id) {
|
||||
this.props.actions.setActiveSouthPaneTab(id);
|
||||
}
|
||||
shouldComponentUpdate(nextProps) {
|
||||
return !areArraysShallowEqual(this.props.editorQueries, nextProps.editorQueries)
|
||||
|| !areArraysShallowEqual(this.props.dataPreviewQueries, nextProps.dataPreviewQueries)
|
||||
|| this.props.activeSouthPaneTab !== nextProps.activeSouthPaneTab;
|
||||
}
|
||||
render() {
|
||||
let latestQuery;
|
||||
const props = this.props;
|
||||
if (props.editorQueries.length > 0) {
|
||||
latestQuery = props.editorQueries[props.editorQueries.length - 1];
|
||||
}
|
||||
let results;
|
||||
if (latestQuery) {
|
||||
results = (
|
||||
<ResultSet showControls search query={latestQuery} actions={props.actions} />
|
||||
);
|
||||
} else {
|
||||
results = <Alert bsStyle="info">Run a query to display results here</Alert>;
|
||||
}
|
||||
|
||||
const dataPreviewTabs = props.dataPreviewQueries.map((query) => (
|
||||
<Tab
|
||||
title={`Preview for ${query.tableName}`}
|
||||
eventKey={query.id}
|
||||
key={query.id}
|
||||
>
|
||||
<ResultSet query={query} visualize={false} csv={false} actions={props.actions} cache />
|
||||
</Tab>
|
||||
));
|
||||
|
||||
return (
|
||||
<div className="SouthPane">
|
||||
<Tabs
|
||||
bsStyle="tabs"
|
||||
id={shortid.generate()}
|
||||
activeKey={this.props.activeSouthPaneTab}
|
||||
onSelect={this.switchTab.bind(this)}
|
||||
>
|
||||
<Tab
|
||||
title="Results"
|
||||
eventKey="Results"
|
||||
>
|
||||
<div style={{ overflow: 'auto' }}>
|
||||
{results}
|
||||
</div>
|
||||
</Tab>
|
||||
<Tab
|
||||
title="Query History"
|
||||
eventKey="History"
|
||||
>
|
||||
<QueryHistory queries={props.editorQueries} actions={props.actions} />
|
||||
</Tab>
|
||||
{dataPreviewTabs}
|
||||
</Tabs>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function mapStateToProps(state) {
|
||||
return {
|
||||
activeSouthPaneTab: state.activeSouthPaneTab,
|
||||
};
|
||||
}
|
||||
|
||||
function mapDispatchToProps(dispatch) {
|
||||
return {
|
||||
actions: bindActionCreators(Actions, dispatch),
|
||||
};
|
||||
}
|
||||
|
||||
SouthPane.propTypes = propTypes;
|
||||
SouthPane.defaultProps = defaultProps;
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(SouthPane);
|
||||
258
superset/assets/javascripts/SqlLab/components/SqlEditor.jsx
Normal file
258
superset/assets/javascripts/SqlLab/components/SqlEditor.jsx
Normal file
@@ -0,0 +1,258 @@
|
||||
import React from 'react';
|
||||
import {
|
||||
Button,
|
||||
ButtonGroup,
|
||||
Col,
|
||||
FormGroup,
|
||||
InputGroup,
|
||||
Form,
|
||||
FormControl,
|
||||
Label,
|
||||
OverlayTrigger,
|
||||
Row,
|
||||
Tooltip,
|
||||
Collapse,
|
||||
} from 'react-bootstrap';
|
||||
|
||||
import SouthPane from './SouthPane';
|
||||
import Timer from './Timer';
|
||||
|
||||
import SqlEditorLeftBar from './SqlEditorLeftBar';
|
||||
import AceEditorWrapper from './AceEditorWrapper';
|
||||
|
||||
const propTypes = {
|
||||
actions: React.PropTypes.object.isRequired,
|
||||
database: React.PropTypes.object,
|
||||
latestQuery: React.PropTypes.object,
|
||||
networkOn: React.PropTypes.bool,
|
||||
tables: React.PropTypes.array.isRequired,
|
||||
editorQueries: React.PropTypes.array.isRequired,
|
||||
dataPreviewQueries: React.PropTypes.array.isRequired,
|
||||
queryEditor: React.PropTypes.object.isRequired,
|
||||
hideLeftBar: React.PropTypes.bool,
|
||||
};
|
||||
|
||||
const defaultProps = {
|
||||
networkOn: true,
|
||||
database: null,
|
||||
latestQuery: null,
|
||||
hideLeftBar: false,
|
||||
};
|
||||
|
||||
|
||||
class SqlEditor extends React.PureComponent {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
autorun: props.queryEditor.autorun,
|
||||
ctas: '',
|
||||
};
|
||||
}
|
||||
componentDidMount() {
|
||||
this.onMount();
|
||||
}
|
||||
onMount() {
|
||||
if (this.state.autorun) {
|
||||
this.setState({ autorun: false });
|
||||
this.props.actions.queryEditorSetAutorun(this.props.queryEditor, false);
|
||||
this.startQuery();
|
||||
}
|
||||
}
|
||||
runQuery(runAsync = false) {
|
||||
let effectiveRunAsync = runAsync;
|
||||
if (!this.props.database.allow_run_sync) {
|
||||
effectiveRunAsync = true;
|
||||
}
|
||||
this.startQuery(effectiveRunAsync);
|
||||
}
|
||||
startQuery(runAsync = false, ctas = false) {
|
||||
const qe = this.props.queryEditor;
|
||||
const query = {
|
||||
dbId: qe.dbId,
|
||||
sql: qe.selectedText ? qe.selectedText : qe.sql,
|
||||
sqlEditorId: qe.id,
|
||||
tab: qe.title,
|
||||
schema: qe.schema,
|
||||
tempTableName: this.state.ctas,
|
||||
runAsync,
|
||||
ctas,
|
||||
};
|
||||
this.props.actions.runQuery(query);
|
||||
this.props.actions.setActiveSouthPaneTab('Results');
|
||||
}
|
||||
stopQuery() {
|
||||
this.props.actions.stopQuery(this.props.latestQuery);
|
||||
}
|
||||
createTableAs() {
|
||||
this.startQuery(true, true);
|
||||
}
|
||||
setQueryEditorSql(sql) {
|
||||
this.props.actions.queryEditorSetSql(this.props.queryEditor, sql);
|
||||
}
|
||||
ctasChanged(event) {
|
||||
this.setState({ ctas: event.target.value });
|
||||
}
|
||||
sqlEditorHeight() {
|
||||
// quick hack to make the white bg of the tab stretch full height.
|
||||
const tabNavHeight = 40;
|
||||
const navBarHeight = 56;
|
||||
const mysteryVerticalHeight = 50;
|
||||
return window.innerHeight - tabNavHeight - navBarHeight - mysteryVerticalHeight;
|
||||
}
|
||||
|
||||
render() {
|
||||
let runButtons = [];
|
||||
let runText = 'Run Query';
|
||||
let btnStyle = 'primary';
|
||||
if (this.props.queryEditor.selectedText) {
|
||||
runText = 'Run Selection';
|
||||
btnStyle = 'warning';
|
||||
}
|
||||
if (this.props.database && this.props.database.allow_run_sync) {
|
||||
runButtons.push(
|
||||
<Button
|
||||
bsSize="small"
|
||||
bsStyle={btnStyle}
|
||||
style={{ width: '100px' }}
|
||||
onClick={this.runQuery.bind(this, false)}
|
||||
disabled={!(this.props.queryEditor.dbId)}
|
||||
key="run-btn"
|
||||
>
|
||||
<i className="fa fa-table" /> {runText}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
if (this.props.database && this.props.database.allow_run_async) {
|
||||
runButtons.push(
|
||||
<Button
|
||||
bsSize="small"
|
||||
bsStyle={btnStyle}
|
||||
style={{ width: '100px' }}
|
||||
onClick={this.runQuery.bind(this, true)}
|
||||
disabled={!(this.props.queryEditor.dbId)}
|
||||
key="run-async-btn"
|
||||
>
|
||||
<i className="fa fa-table" /> Run Async
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
runButtons = (
|
||||
<ButtonGroup bsSize="small" className="inline m-r-5 pull-left">
|
||||
{runButtons}
|
||||
</ButtonGroup>
|
||||
);
|
||||
if (
|
||||
this.props.latestQuery &&
|
||||
['running', 'pending'].indexOf(this.props.latestQuery.state) > -1) {
|
||||
runButtons = (
|
||||
<ButtonGroup bsSize="small" className="inline m-r-5 pull-left">
|
||||
<Button
|
||||
bsStyle="primary"
|
||||
bsSize="small"
|
||||
style={{ width: '100px' }}
|
||||
onClick={this.stopQuery.bind(this)}
|
||||
>
|
||||
<a className="fa fa-stop" /> Stop
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
);
|
||||
}
|
||||
let limitWarning = null;
|
||||
if (this.props.latestQuery && this.props.latestQuery.limit_reached) {
|
||||
const tooltip = (
|
||||
<Tooltip id="tooltip">
|
||||
It appears that the number of rows in the query results displayed
|
||||
was limited on the server side to
|
||||
the {this.props.latestQuery.rows} limit.
|
||||
</Tooltip>
|
||||
);
|
||||
limitWarning = (
|
||||
<OverlayTrigger placement="left" overlay={tooltip}>
|
||||
<Label bsStyle="warning" className="m-r-5">LIMIT</Label>
|
||||
</OverlayTrigger>
|
||||
);
|
||||
}
|
||||
let ctasControls;
|
||||
if (this.props.database && this.props.database.allow_ctas) {
|
||||
ctasControls = (
|
||||
<FormGroup>
|
||||
<InputGroup>
|
||||
<FormControl
|
||||
type="text"
|
||||
bsSize="small"
|
||||
className="input-sm"
|
||||
placeholder="new table name"
|
||||
onChange={this.ctasChanged.bind(this)}
|
||||
/>
|
||||
<InputGroup.Button>
|
||||
<Button
|
||||
bsSize="small"
|
||||
disabled={this.state.ctas.length === 0}
|
||||
onClick={this.createTableAs.bind(this)}
|
||||
>
|
||||
<i className="fa fa-table" /> CTAS
|
||||
</Button>
|
||||
</InputGroup.Button>
|
||||
</InputGroup>
|
||||
</FormGroup>
|
||||
);
|
||||
}
|
||||
const editorBottomBar = (
|
||||
<div className="sql-toolbar clearfix">
|
||||
<div className="pull-left">
|
||||
<Form inline>
|
||||
{runButtons}
|
||||
{ctasControls}
|
||||
</Form>
|
||||
</div>
|
||||
<div className="pull-right">
|
||||
{limitWarning}
|
||||
<Timer query={this.props.latestQuery} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
return (
|
||||
<div className="SqlEditor" style={{ minHeight: this.sqlEditorHeight() }}>
|
||||
<Row>
|
||||
<Collapse
|
||||
in={!this.props.hideLeftBar}
|
||||
>
|
||||
<Col md={3}>
|
||||
<SqlEditorLeftBar
|
||||
queryEditor={this.props.queryEditor}
|
||||
tables={this.props.tables}
|
||||
networkOn={this.props.networkOn}
|
||||
actions={this.props.actions}
|
||||
/>
|
||||
</Col>
|
||||
</Collapse>
|
||||
<Col md={this.props.hideLeftBar ? 12 : 9}>
|
||||
<div className="scrollbar-container">
|
||||
<div className="scrollbar-content">
|
||||
<AceEditorWrapper
|
||||
actions={this.props.actions}
|
||||
onBlur={this.setQueryEditorSql.bind(this)}
|
||||
queryEditor={this.props.queryEditor}
|
||||
onAltEnter={this.runQuery.bind(this)}
|
||||
sql={this.props.queryEditor.sql}
|
||||
tables={this.props.tables}
|
||||
/>
|
||||
{editorBottomBar}
|
||||
<br />
|
||||
<SouthPane
|
||||
editorQueries={this.props.editorQueries}
|
||||
dataPreviewQueries={this.props.dataPreviewQueries}
|
||||
actions={this.props.actions}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Col>
|
||||
</Row>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
SqlEditor.defaultProps = defaultProps;
|
||||
SqlEditor.propTypes = propTypes;
|
||||
|
||||
export default SqlEditor;
|
||||
@@ -0,0 +1,167 @@
|
||||
const $ = window.$ = require('jquery');
|
||||
import React from 'react';
|
||||
import Select from 'react-select';
|
||||
import { Label, Button } from 'react-bootstrap';
|
||||
import TableElement from './TableElement';
|
||||
import DatabaseSelect from './DatabaseSelect';
|
||||
|
||||
const propTypes = {
|
||||
queryEditor: React.PropTypes.object.isRequired,
|
||||
tables: React.PropTypes.array,
|
||||
actions: React.PropTypes.object,
|
||||
networkOn: React.PropTypes.bool,
|
||||
};
|
||||
|
||||
const defaultProps = {
|
||||
tables: [],
|
||||
networkOn: true,
|
||||
actions: {},
|
||||
};
|
||||
|
||||
class SqlEditorLeftBar extends React.PureComponent {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
schemaLoading: false,
|
||||
schemaOptions: [],
|
||||
tableLoading: false,
|
||||
tableOptions: [],
|
||||
networkOn: true,
|
||||
};
|
||||
}
|
||||
componentWillMount() {
|
||||
this.fetchSchemas();
|
||||
this.fetchTables();
|
||||
}
|
||||
onChange(db) {
|
||||
const val = (db) ? db.value : null;
|
||||
this.setState({ schemaOptions: [] });
|
||||
this.props.actions.queryEditorSetDb(this.props.queryEditor, val);
|
||||
if (!(db)) {
|
||||
this.setState({ tableOptions: [] });
|
||||
} else {
|
||||
this.fetchTables(val, this.props.queryEditor.schema);
|
||||
this.fetchSchemas(val);
|
||||
}
|
||||
}
|
||||
resetState() {
|
||||
this.props.actions.resetState();
|
||||
}
|
||||
fetchTables(dbId, schema) {
|
||||
const actualDbId = dbId || this.props.queryEditor.dbId;
|
||||
if (actualDbId) {
|
||||
const actualSchema = schema || this.props.queryEditor.schema;
|
||||
this.setState({ tableLoading: true });
|
||||
this.setState({ tableOptions: [] });
|
||||
const url = `/superset/tables/${actualDbId}/${actualSchema}`;
|
||||
$.get(url, (data) => {
|
||||
let tableOptions = data.tables.map((s) => ({ value: s, label: s }));
|
||||
const views = data.views.map((s) => ({ value: s, label: '[view] ' + s }));
|
||||
tableOptions = [...tableOptions, ...views];
|
||||
this.setState({ tableOptions });
|
||||
this.setState({ tableLoading: false });
|
||||
});
|
||||
}
|
||||
}
|
||||
changeSchema(schemaOpt) {
|
||||
const schema = (schemaOpt) ? schemaOpt.value : null;
|
||||
this.props.actions.queryEditorSetSchema(this.props.queryEditor, schema);
|
||||
this.fetchTables(this.props.queryEditor.dbId, schema);
|
||||
}
|
||||
fetchSchemas(dbId) {
|
||||
const actualDbId = dbId || this.props.queryEditor.dbId;
|
||||
if (actualDbId) {
|
||||
this.setState({ schemaLoading: true });
|
||||
const url = `/databasetablesasync/api/read?_flt_0_id=${actualDbId}`;
|
||||
$.get(url, (data) => {
|
||||
const schemas = data.result[0].all_schema_names;
|
||||
const schemaOptions = schemas.map((s) => ({ value: s, label: s }));
|
||||
this.setState({ schemaOptions });
|
||||
this.setState({ schemaLoading: false });
|
||||
});
|
||||
}
|
||||
}
|
||||
closePopover(ref) {
|
||||
this.refs[ref].hide();
|
||||
}
|
||||
changeTable(tableOpt) {
|
||||
const tableName = tableOpt.value;
|
||||
const qe = this.props.queryEditor;
|
||||
|
||||
this.setState({ tableLoading: true });
|
||||
this.props.actions.addTable(qe, tableName);
|
||||
this.setState({ tableLoading: false });
|
||||
}
|
||||
render() {
|
||||
let networkAlert = null;
|
||||
if (!this.props.networkOn) {
|
||||
networkAlert = <p><Label bsStyle="danger">OFFLINE</Label></p>;
|
||||
}
|
||||
const shouldShowReset = window.location.search === '?reset=1';
|
||||
return (
|
||||
<div className="scrollbar-container">
|
||||
<div className="clearfix sql-toolbar scrollbar-content">
|
||||
{networkAlert}
|
||||
<div>
|
||||
<DatabaseSelect
|
||||
onChange={this.onChange.bind(this)}
|
||||
databaseId={this.props.queryEditor.dbId}
|
||||
actions={this.props.actions}
|
||||
valueRenderer={(o) => (
|
||||
<div>
|
||||
<span className="text-muted">Database:</span> {o.label}
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div className="m-t-5">
|
||||
<Select
|
||||
name="select-schema"
|
||||
placeholder={`Select a schema (${this.state.schemaOptions.length})`}
|
||||
options={this.state.schemaOptions}
|
||||
value={this.props.queryEditor.schema}
|
||||
valueRenderer={(o) => (
|
||||
<div>
|
||||
<span className="text-muted">Schema:</span> {o.label}
|
||||
</div>
|
||||
)}
|
||||
isLoading={this.state.schemaLoading}
|
||||
autosize={false}
|
||||
onChange={this.changeSchema.bind(this)}
|
||||
/>
|
||||
</div>
|
||||
<div className="m-t-5">
|
||||
<Select
|
||||
name="select-table"
|
||||
ref="selectTable"
|
||||
isLoading={this.state.tableLoading}
|
||||
placeholder={`Add a table (${this.state.tableOptions.length})`}
|
||||
autosize={false}
|
||||
onChange={this.changeTable.bind(this)}
|
||||
options={this.state.tableOptions}
|
||||
/>
|
||||
</div>
|
||||
<hr />
|
||||
<div className="m-t-5">
|
||||
{this.props.tables.map((table) => (
|
||||
<TableElement
|
||||
table={table}
|
||||
key={table.id}
|
||||
actions={this.props.actions}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
{shouldShowReset &&
|
||||
<Button bsSize="small" bsStyle="danger" onClick={this.resetState.bind(this)}>
|
||||
<i className="fa fa-bomb" /> Reset State
|
||||
</Button>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
SqlEditorLeftBar.propTypes = propTypes;
|
||||
SqlEditorLeftBar.defaultProps = defaultProps;
|
||||
|
||||
export default SqlEditorLeftBar;
|
||||
@@ -0,0 +1,225 @@
|
||||
import React from 'react';
|
||||
import { DropdownButton, MenuItem, Tab, Tabs } from 'react-bootstrap';
|
||||
import { connect } from 'react-redux';
|
||||
import { bindActionCreators } from 'redux';
|
||||
import * as Actions from '../actions';
|
||||
import SqlEditor from './SqlEditor';
|
||||
import { getParamFromQuery } from '../../../utils/common';
|
||||
import CopyQueryTabUrl from './CopyQueryTabUrl';
|
||||
import { areObjectsEqual } from '../../reduxUtils';
|
||||
|
||||
const propTypes = {
|
||||
actions: React.PropTypes.object.isRequired,
|
||||
databases: React.PropTypes.object.isRequired,
|
||||
queries: React.PropTypes.object.isRequired,
|
||||
queryEditors: React.PropTypes.array,
|
||||
tabHistory: React.PropTypes.array.isRequired,
|
||||
tables: React.PropTypes.array.isRequired,
|
||||
networkOn: React.PropTypes.bool,
|
||||
};
|
||||
const defaultProps = {
|
||||
queryEditors: [],
|
||||
networkOn: true,
|
||||
};
|
||||
|
||||
let queryCount = 1;
|
||||
|
||||
class TabbedSqlEditors extends React.PureComponent {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
const uri = window.location.toString();
|
||||
const search = window.location.search;
|
||||
const cleanUri = search ? uri.substring(0, uri.indexOf('?')) : uri;
|
||||
const query = search.substring(1);
|
||||
this.state = {
|
||||
uri,
|
||||
cleanUri,
|
||||
query,
|
||||
queriesArray: [],
|
||||
hideLeftBar: false,
|
||||
};
|
||||
}
|
||||
componentWillMount() {
|
||||
if (this.state.query) {
|
||||
queryCount++;
|
||||
const queryEditorProps = {
|
||||
title: getParamFromQuery(this.state.query, 'title'),
|
||||
dbId: parseInt(getParamFromQuery(this.state.query, 'dbid'), 10),
|
||||
schema: getParamFromQuery(this.state.query, 'schema'),
|
||||
autorun: getParamFromQuery(this.state.query, 'autorun'),
|
||||
sql: getParamFromQuery(this.state.query, 'sql'),
|
||||
};
|
||||
this.props.actions.addQueryEditor(queryEditorProps);
|
||||
// Clean the url in browser history
|
||||
window.history.replaceState({}, document.title, this.state.cleanUri);
|
||||
}
|
||||
}
|
||||
componentWillReceiveProps(nextProps) {
|
||||
const activeQeId = this.props.tabHistory[this.props.tabHistory.length - 1];
|
||||
const newActiveQeId = nextProps.tabHistory[nextProps.tabHistory.length - 1];
|
||||
if (activeQeId !== newActiveQeId || !areObjectsEqual(this.props.queries, nextProps.queries)) {
|
||||
const queriesArray = [];
|
||||
for (const id in this.props.queries) {
|
||||
if (this.props.queries[id].sqlEditorId === newActiveQeId) {
|
||||
queriesArray.push(this.props.queries[id]);
|
||||
}
|
||||
}
|
||||
this.setState({ queriesArray });
|
||||
}
|
||||
}
|
||||
renameTab(qe) {
|
||||
/* eslint no-alert: 0 */
|
||||
const newTitle = prompt('Enter a new title for the tab');
|
||||
if (newTitle) {
|
||||
this.props.actions.queryEditorSetTitle(qe, newTitle);
|
||||
}
|
||||
}
|
||||
activeQueryEditor() {
|
||||
const qeid = this.props.tabHistory[this.props.tabHistory.length - 1];
|
||||
for (let i = 0; i < this.props.queryEditors.length; i++) {
|
||||
const qe = this.props.queryEditors[i];
|
||||
if (qe.id === qeid) {
|
||||
return qe;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
newQueryEditor() {
|
||||
queryCount++;
|
||||
const activeQueryEditor = this.activeQueryEditor();
|
||||
const qe = {
|
||||
title: `Untitled Query ${queryCount}`,
|
||||
dbId: (activeQueryEditor) ? activeQueryEditor.dbId : null,
|
||||
schema: (activeQueryEditor) ? activeQueryEditor.schema : null,
|
||||
autorun: false,
|
||||
sql: 'SELECT ...',
|
||||
};
|
||||
this.props.actions.addQueryEditor(qe);
|
||||
}
|
||||
handleSelect(key) {
|
||||
if (key === 'add_tab') {
|
||||
this.newQueryEditor();
|
||||
} else {
|
||||
this.props.actions.setActiveQueryEditor({ id: key });
|
||||
}
|
||||
}
|
||||
removeQueryEditor(qe) {
|
||||
this.props.actions.removeQueryEditor(qe);
|
||||
}
|
||||
toggleLeftBar() {
|
||||
this.setState({ hideLeftBar: !this.state.hideLeftBar });
|
||||
}
|
||||
render() {
|
||||
const editors = this.props.queryEditors.map((qe, i) => {
|
||||
const isSelected = (qe.id === this.activeQueryEditor().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 dataPreviewQueries = [];
|
||||
this.props.tables.forEach((table) => {
|
||||
const queryId = table.dataPreviewQueryId;
|
||||
if (queryId && this.props.queries[queryId] && table.queryEditorId === qe.id) {
|
||||
dataPreviewQueries.push(this.props.queries[queryId]);
|
||||
}
|
||||
});
|
||||
|
||||
const tabTitle = (
|
||||
<div>
|
||||
<div className={'circle ' + state} /> {qe.title} {' '}
|
||||
<DropdownButton
|
||||
bsSize="small"
|
||||
id={'ddbtn-tab-' + i}
|
||||
title=""
|
||||
>
|
||||
<MenuItem eventKey="1" onClick={this.removeQueryEditor.bind(this, qe)}>
|
||||
<i className="fa fa-close" /> close tab
|
||||
</MenuItem>
|
||||
<MenuItem eventKey="2" onClick={this.renameTab.bind(this, qe)}>
|
||||
<i className="fa fa-i-cursor" /> rename tab
|
||||
</MenuItem>
|
||||
{qe &&
|
||||
<MenuItem eventKey="3">
|
||||
<i className="fa fa-clipboard" /> <CopyQueryTabUrl queryEditor={qe} />
|
||||
</MenuItem>
|
||||
}
|
||||
<MenuItem eventKey="4" onClick={this.toggleLeftBar.bind(this)}>
|
||||
<i className="fa fa-cogs" />
|
||||
|
||||
{this.state.hideLeftBar ? 'expand tool bar' : 'hide tool bar'}
|
||||
</MenuItem>
|
||||
</DropdownButton>
|
||||
</div>
|
||||
);
|
||||
return (
|
||||
<Tab
|
||||
key={qe.id}
|
||||
title={tabTitle}
|
||||
eventKey={qe.id}
|
||||
>
|
||||
<div className="panel panel-default">
|
||||
<div className="panel-body">
|
||||
{isSelected &&
|
||||
<SqlEditor
|
||||
tables={this.props.tables.filter((t) => (t.queryEditorId === qe.id))}
|
||||
queryEditor={qe}
|
||||
editorQueries={this.state.queriesArray}
|
||||
dataPreviewQueries={dataPreviewQueries}
|
||||
latestQuery={latestQuery}
|
||||
database={database}
|
||||
actions={this.props.actions}
|
||||
networkOn={this.props.networkOn}
|
||||
hideLeftBar={this.state.hideLeftBar}
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</Tab>);
|
||||
});
|
||||
return (
|
||||
<Tabs
|
||||
bsStyle="tabs"
|
||||
activeKey={this.props.tabHistory[this.props.tabHistory.length - 1]}
|
||||
onSelect={this.handleSelect.bind(this)}
|
||||
id="a11y-query-editor-tabs"
|
||||
>
|
||||
{editors}
|
||||
<Tab
|
||||
title={
|
||||
<div>
|
||||
<i className="fa fa-plus-circle" />
|
||||
</div>}
|
||||
eventKey="add_tab"
|
||||
/>
|
||||
</Tabs>
|
||||
);
|
||||
}
|
||||
}
|
||||
TabbedSqlEditors.propTypes = propTypes;
|
||||
TabbedSqlEditors.defaultProps = defaultProps;
|
||||
|
||||
function mapStateToProps(state) {
|
||||
return {
|
||||
databases: state.databases,
|
||||
queryEditors: state.queryEditors,
|
||||
queries: state.queries,
|
||||
tabHistory: state.tabHistory,
|
||||
networkOn: state.networkOn,
|
||||
tables: state.tables,
|
||||
};
|
||||
}
|
||||
function mapDispatchToProps(dispatch) {
|
||||
return {
|
||||
actions: bindActionCreators(Actions, dispatch),
|
||||
};
|
||||
}
|
||||
|
||||
export { TabbedSqlEditors };
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(TabbedSqlEditors);
|
||||
214
superset/assets/javascripts/SqlLab/components/TableElement.jsx
Normal file
214
superset/assets/javascripts/SqlLab/components/TableElement.jsx
Normal file
@@ -0,0 +1,214 @@
|
||||
import React from 'react';
|
||||
|
||||
import { ButtonGroup, Collapse, Well } from 'react-bootstrap';
|
||||
import shortid from 'shortid';
|
||||
|
||||
import CopyToClipboard from '../../components/CopyToClipboard';
|
||||
import Link from './Link';
|
||||
import ColumnElement from './ColumnElement';
|
||||
import ModalTrigger from '../../components/ModalTrigger';
|
||||
|
||||
const propTypes = {
|
||||
table: React.PropTypes.object,
|
||||
actions: React.PropTypes.object,
|
||||
timeout: React.PropTypes.number, // used for tests
|
||||
};
|
||||
|
||||
const defaultProps = {
|
||||
actions: {},
|
||||
table: null,
|
||||
timeout: 500,
|
||||
};
|
||||
|
||||
class TableElement extends React.PureComponent {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
sortColumns: false,
|
||||
expanded: true,
|
||||
};
|
||||
}
|
||||
|
||||
popSelectStar() {
|
||||
const qe = {
|
||||
id: shortid.generate(),
|
||||
title: this.props.table.name,
|
||||
dbId: this.props.table.dbId,
|
||||
autorun: true,
|
||||
sql: this.props.table.selectStar,
|
||||
};
|
||||
this.props.actions.addQueryEditor(qe);
|
||||
}
|
||||
|
||||
toggleTable(e) {
|
||||
e.preventDefault();
|
||||
if (this.props.table.expanded) {
|
||||
this.props.actions.collapseTable(this.props.table);
|
||||
} else {
|
||||
this.props.actions.expandTable(this.props.table);
|
||||
}
|
||||
}
|
||||
|
||||
removeTable() {
|
||||
this.setState({ expanded: false });
|
||||
this.props.actions.removeDataPreview(this.props.table);
|
||||
}
|
||||
toggleSortColumns() {
|
||||
this.setState({ sortColumns: !this.state.sortColumns });
|
||||
}
|
||||
|
||||
renderHeader() {
|
||||
const table = this.props.table;
|
||||
let header;
|
||||
if (table.partitions) {
|
||||
let partitionQuery;
|
||||
let partitionClipBoard;
|
||||
if (table.partitions.partitionQuery) {
|
||||
partitionQuery = table.partitions.partitionQuery;
|
||||
const tt = 'Copy partition query to clipboard';
|
||||
partitionClipBoard = (
|
||||
<CopyToClipboard
|
||||
text={partitionQuery}
|
||||
shouldShowText={false}
|
||||
tooltipText={tt}
|
||||
copyNode={<i className="fa fa-clipboard" />}
|
||||
/>
|
||||
);
|
||||
}
|
||||
let latest = [];
|
||||
for (const k in table.partitions.latest) {
|
||||
latest.push(`${k}=${table.partitions.latest[k]}`);
|
||||
}
|
||||
latest = latest.join('/');
|
||||
header = (
|
||||
<Well bsSize="small">
|
||||
<div>
|
||||
<small>
|
||||
latest partition: {latest}
|
||||
</small> {partitionClipBoard}
|
||||
</div>
|
||||
</Well>
|
||||
);
|
||||
}
|
||||
return header;
|
||||
}
|
||||
renderMetadata() {
|
||||
const table = this.props.table;
|
||||
let cols;
|
||||
if (table.columns) {
|
||||
cols = table.columns.slice();
|
||||
if (this.state.sortColumns) {
|
||||
cols.sort((a, b) => a.name.toUpperCase() > b.name.toUpperCase());
|
||||
}
|
||||
}
|
||||
const metadata = (
|
||||
<Collapse
|
||||
in={table.expanded}
|
||||
timeout={this.props.timeout}
|
||||
>
|
||||
<div>
|
||||
{this.renderHeader()}
|
||||
<div className="table-columns">
|
||||
{cols && cols.map(col => (
|
||||
<ColumnElement column={col} key={col.name} />
|
||||
))}
|
||||
<hr />
|
||||
</div>
|
||||
</div>
|
||||
</Collapse>
|
||||
);
|
||||
return metadata;
|
||||
}
|
||||
removeFromStore() {
|
||||
this.props.actions.removeTable(this.props.table);
|
||||
}
|
||||
|
||||
render() {
|
||||
const table = this.props.table;
|
||||
let keyLink;
|
||||
if (table.indexes && table.indexes.length > 0) {
|
||||
keyLink = (
|
||||
<ModalTrigger
|
||||
modalTitle={
|
||||
<div>
|
||||
Keys for table <strong>{table.name}</strong>
|
||||
</div>
|
||||
}
|
||||
modalBody={table.indexes.map((ix, i) => (
|
||||
<pre key={i}>{JSON.stringify(ix, null, ' ')}</pre>
|
||||
))}
|
||||
triggerNode={
|
||||
<Link
|
||||
className="fa fa-key pull-left m-l-2"
|
||||
tooltip={`View keys & indexes (${table.indexes.length})`}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Collapse
|
||||
in={this.state.expanded}
|
||||
timeout={this.props.timeout}
|
||||
transitionAppear
|
||||
onExited={this.removeFromStore.bind(this)}
|
||||
>
|
||||
<div className="TableElement">
|
||||
<div className="clearfix">
|
||||
<div className="pull-left">
|
||||
<a
|
||||
href="#"
|
||||
className="table-name"
|
||||
onClick={(e) => { this.toggleTable(e); }}
|
||||
>
|
||||
<strong>{table.name}</strong>
|
||||
<small className="m-l-5">
|
||||
<i className={`fa fa-${table.expanded ? 'minus' : 'plus'}-square-o`} />
|
||||
</small>
|
||||
</a>
|
||||
</div>
|
||||
<div className="pull-right">
|
||||
<ButtonGroup className="ws-el-controls pull-right">
|
||||
{keyLink}
|
||||
<Link
|
||||
className={
|
||||
`fa fa-sort-${!this.state.sortColumns ? 'alpha' : 'numeric'}-asc ` +
|
||||
'pull-left sort-cols m-l-2'}
|
||||
onClick={this.toggleSortColumns.bind(this)}
|
||||
tooltip={
|
||||
!this.state.sortColumns ?
|
||||
'Sort columns alphabetically' :
|
||||
'Original table column order'}
|
||||
href="#"
|
||||
/>
|
||||
{table.selectStar &&
|
||||
<CopyToClipboard
|
||||
copyNode={
|
||||
<a className="fa fa-clipboard pull-left m-l-2" />
|
||||
}
|
||||
text={table.selectStar}
|
||||
shouldShowText={false}
|
||||
tooltipText="Copy SELECT statement to clipboard"
|
||||
/>
|
||||
}
|
||||
<Link
|
||||
className="fa fa-trash table-remove pull-left m-l-2"
|
||||
onClick={this.removeTable.bind(this)}
|
||||
tooltip="Remove table preview"
|
||||
href="#"
|
||||
/>
|
||||
</ButtonGroup>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
{this.renderMetadata()}
|
||||
</div>
|
||||
</div>
|
||||
</Collapse>
|
||||
);
|
||||
}
|
||||
}
|
||||
TableElement.propTypes = propTypes;
|
||||
TableElement.defaultProps = defaultProps;
|
||||
|
||||
export default TableElement;
|
||||
61
superset/assets/javascripts/SqlLab/components/Timer.jsx
Normal file
61
superset/assets/javascripts/SqlLab/components/Timer.jsx
Normal file
@@ -0,0 +1,61 @@
|
||||
import React from 'react';
|
||||
import { now, fDuration } from '../../modules/dates';
|
||||
|
||||
import { STATE_BSSTYLE_MAP } from '../constants.js';
|
||||
|
||||
class Timer extends React.PureComponent {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
clockStr: '',
|
||||
};
|
||||
}
|
||||
componentWillMount() {
|
||||
this.startTimer();
|
||||
}
|
||||
componentWillUnmount() {
|
||||
this.stopTimer();
|
||||
}
|
||||
startTimer() {
|
||||
if (!(this.timer)) {
|
||||
this.timer = setInterval(this.stopwatch.bind(this), 30);
|
||||
}
|
||||
}
|
||||
stopTimer() {
|
||||
clearInterval(this.timer);
|
||||
this.timer = null;
|
||||
}
|
||||
stopwatch() {
|
||||
if (this.props && this.props.query) {
|
||||
const endDttm = this.props.query.endDttm || now();
|
||||
const clockStr = fDuration(this.props.query.startDttm, endDttm);
|
||||
this.setState({ clockStr });
|
||||
if (this.props.query.state !== 'running') {
|
||||
this.stopTimer();
|
||||
}
|
||||
}
|
||||
}
|
||||
render() {
|
||||
if (this.props.query && this.props.query.state === 'running') {
|
||||
this.startTimer();
|
||||
}
|
||||
let timerSpan = null;
|
||||
if (this.props && this.props.query) {
|
||||
const bsStyle = STATE_BSSTYLE_MAP[this.props.query.state];
|
||||
timerSpan = (
|
||||
<span className={'inlineBlock m-r-5 label label-' + bsStyle}>
|
||||
{this.state.clockStr}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
return timerSpan;
|
||||
}
|
||||
}
|
||||
Timer.propTypes = {
|
||||
query: React.PropTypes.object,
|
||||
};
|
||||
Timer.defaultProps = {
|
||||
query: null,
|
||||
};
|
||||
|
||||
export default Timer;
|
||||
220
superset/assets/javascripts/SqlLab/components/VisualizeModal.jsx
Normal file
220
superset/assets/javascripts/SqlLab/components/VisualizeModal.jsx
Normal file
@@ -0,0 +1,220 @@
|
||||
import React from 'react';
|
||||
import { Alert, Button, Col, Modal } from 'react-bootstrap';
|
||||
|
||||
import Select from 'react-select';
|
||||
import { Table } from 'reactable';
|
||||
import shortid from 'shortid';
|
||||
|
||||
const CHART_TYPES = [
|
||||
{ value: 'dist_bar', label: 'Distribution - Bar Chart', requiresTime: false },
|
||||
{ value: 'pie', label: 'Pie Chart', requiresTime: false },
|
||||
{ value: 'line', label: 'Time Series - Line Chart', requiresTime: true },
|
||||
{ value: 'bar', label: 'Time Series - Bar Chart', requiresTime: true },
|
||||
];
|
||||
|
||||
const propTypes = {
|
||||
onHide: React.PropTypes.func,
|
||||
query: React.PropTypes.object,
|
||||
show: React.PropTypes.bool,
|
||||
};
|
||||
const defaultProps = {
|
||||
show: false,
|
||||
query: {},
|
||||
onHide: () => {},
|
||||
};
|
||||
|
||||
class VisualizeModal extends React.PureComponent {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
const uniqueId = shortid.generate();
|
||||
this.state = {
|
||||
chartType: CHART_TYPES[0],
|
||||
datasourceName: uniqueId,
|
||||
columns: {},
|
||||
hints: [],
|
||||
};
|
||||
}
|
||||
componentWillMount() {
|
||||
this.setStateFromProps();
|
||||
}
|
||||
componentDidMount() {
|
||||
this.validate();
|
||||
}
|
||||
setStateFromProps() {
|
||||
if (
|
||||
!this.props.query ||
|
||||
!this.props.query.results ||
|
||||
!this.props.query.results.columns) {
|
||||
return;
|
||||
}
|
||||
const columns = {};
|
||||
this.props.query.results.columns.forEach((col) => {
|
||||
columns[col.name] = col;
|
||||
});
|
||||
this.setState({ columns });
|
||||
}
|
||||
validate() {
|
||||
const hints = [];
|
||||
const cols = this.mergedColumns();
|
||||
const re = /^\w+$/;
|
||||
Object.keys(cols).forEach((colName) => {
|
||||
if (!re.test(colName)) {
|
||||
hints.push(
|
||||
<div>
|
||||
"{colName}" is not right as a column name, please alias it
|
||||
(as in SELECT count(*) <strong>AS my_alias</strong>) using only
|
||||
alphanumeric characters and underscores
|
||||
</div>);
|
||||
}
|
||||
});
|
||||
if (this.state.chartType === null) {
|
||||
hints.push('Pick a chart type!');
|
||||
} else if (this.state.chartType.requiresTime) {
|
||||
let hasTime = false;
|
||||
for (const colName in cols) {
|
||||
const col = cols[colName];
|
||||
if (col.hasOwnProperty('is_date') && col.is_date) {
|
||||
hasTime = true;
|
||||
}
|
||||
}
|
||||
if (!hasTime) {
|
||||
hints.push('To use this chart type you need at least one column flagged as a date');
|
||||
}
|
||||
}
|
||||
this.setState({ hints });
|
||||
}
|
||||
changeChartType(option) {
|
||||
this.setState({ chartType: option }, this.validate);
|
||||
}
|
||||
mergedColumns() {
|
||||
const columns = Object.assign({}, this.state.columns);
|
||||
if (this.props.query && this.props.query.results.columns) {
|
||||
this.props.query.results.columns.forEach((col) => {
|
||||
if (columns[col.name] === undefined) {
|
||||
columns[col.name] = col;
|
||||
}
|
||||
});
|
||||
}
|
||||
return columns;
|
||||
}
|
||||
visualize() {
|
||||
const vizOptions = {
|
||||
chartType: this.state.chartType.value,
|
||||
datasourceName: this.state.datasourceName,
|
||||
columns: this.state.columns,
|
||||
sql: this.props.query.sql,
|
||||
dbId: this.props.query.dbId,
|
||||
};
|
||||
window.open('/superset/sqllab_viz/?data=' + JSON.stringify(vizOptions));
|
||||
}
|
||||
changeDatasourceName(event) {
|
||||
this.setState({ datasourceName: event.target.value });
|
||||
this.validate();
|
||||
}
|
||||
changeCheckbox(attr, columnName, event) {
|
||||
let columns = this.mergedColumns();
|
||||
const column = Object.assign({}, columns[columnName], { [attr]: event.target.checked });
|
||||
columns = Object.assign({}, columns, { [columnName]: column });
|
||||
this.setState({ columns }, this.validate);
|
||||
}
|
||||
changeAggFunction(columnName, option) {
|
||||
let columns = this.mergedColumns();
|
||||
const val = (option) ? option.value : null;
|
||||
const column = Object.assign({}, columns[columnName], { agg: val });
|
||||
columns = Object.assign({}, columns, { [columnName]: column });
|
||||
this.setState({ columns }, this.validate);
|
||||
}
|
||||
render() {
|
||||
if (!(this.props.query)) {
|
||||
return <div />;
|
||||
}
|
||||
const tableData = this.props.query.results.columns.map((col) => ({
|
||||
column: col.name,
|
||||
is_dimension: (
|
||||
<input
|
||||
type="checkbox"
|
||||
onChange={this.changeCheckbox.bind(this, 'is_dim', col.name)}
|
||||
checked={(this.state.columns[col.name]) ? this.state.columns[col.name].is_dim : false}
|
||||
className="form-control"
|
||||
/>
|
||||
),
|
||||
is_date: (
|
||||
<input
|
||||
type="checkbox"
|
||||
className="form-control"
|
||||
onChange={this.changeCheckbox.bind(this, 'is_date', col.name)}
|
||||
checked={(this.state.columns[col.name]) ? this.state.columns[col.name].is_date : false}
|
||||
/>
|
||||
),
|
||||
agg_func: (
|
||||
<Select
|
||||
options={[
|
||||
{ value: 'sum', label: 'SUM(x)' },
|
||||
{ value: 'min', label: 'MIN(x)' },
|
||||
{ value: 'max', label: 'MAX(x)' },
|
||||
{ value: 'avg', label: 'AVG(x)' },
|
||||
{ value: 'count_distinct', label: 'COUNT(DISTINCT x)' },
|
||||
]}
|
||||
onChange={this.changeAggFunction.bind(this, col.name)}
|
||||
value={(this.state.columns[col.name]) ? this.state.columns[col.name].agg : null}
|
||||
/>
|
||||
),
|
||||
}));
|
||||
const alerts = this.state.hints.map((hint, i) => (
|
||||
<Alert bsStyle="warning" key={i}>{hint}</Alert>
|
||||
));
|
||||
const modal = (
|
||||
<div className="VisualizeModal">
|
||||
<Modal show={this.props.show} onHide={this.props.onHide}>
|
||||
<Modal.Header closeButton>
|
||||
<Modal.Title>Visualize</Modal.Title>
|
||||
</Modal.Header>
|
||||
<Modal.Body>
|
||||
{alerts}
|
||||
<div className="row">
|
||||
<Col md={6}>
|
||||
Chart Type
|
||||
<Select
|
||||
name="select-chart-type"
|
||||
placeholder="[Chart Type]"
|
||||
options={CHART_TYPES}
|
||||
value={(this.state.chartType) ? this.state.chartType.value : null}
|
||||
autosize={false}
|
||||
onChange={this.changeChartType.bind(this)}
|
||||
/>
|
||||
</Col>
|
||||
<Col md={6}>
|
||||
Datasource Name
|
||||
<input
|
||||
type="text"
|
||||
className="form-control input-sm"
|
||||
placeholder="datasource name"
|
||||
onChange={this.changeDatasourceName.bind(this)}
|
||||
value={this.state.datasourceName}
|
||||
/>
|
||||
</Col>
|
||||
</div>
|
||||
<hr />
|
||||
<Table
|
||||
className="table table-condensed"
|
||||
columns={['column', 'is_dimension', 'is_date', 'agg_func']}
|
||||
data={tableData}
|
||||
/>
|
||||
<Button
|
||||
onClick={this.visualize.bind(this)}
|
||||
bsStyle="primary"
|
||||
disabled={(this.state.hints.length > 0)}
|
||||
>
|
||||
Visualize
|
||||
</Button>
|
||||
</Modal.Body>
|
||||
</Modal>
|
||||
</div>
|
||||
);
|
||||
return modal;
|
||||
}
|
||||
}
|
||||
VisualizeModal.propTypes = propTypes;
|
||||
VisualizeModal.defaultProps = defaultProps;
|
||||
|
||||
export default VisualizeModal;
|
||||
24
superset/assets/javascripts/SqlLab/constants.js
Normal file
24
superset/assets/javascripts/SqlLab/constants.js
Normal file
@@ -0,0 +1,24 @@
|
||||
export const STATE_BSSTYLE_MAP = {
|
||||
failed: 'danger',
|
||||
pending: 'info',
|
||||
fetching: 'info',
|
||||
running: 'warning',
|
||||
stopped: 'danger',
|
||||
success: 'success',
|
||||
};
|
||||
|
||||
export const STATUS_OPTIONS = [
|
||||
'success',
|
||||
'failed',
|
||||
'running',
|
||||
];
|
||||
|
||||
export const TIME_OPTIONS = [
|
||||
'now',
|
||||
'1 hour ago',
|
||||
'1 day ago',
|
||||
'7 days ago',
|
||||
'28 days ago',
|
||||
'90 days ago',
|
||||
'1 year ago',
|
||||
];
|
||||
29
superset/assets/javascripts/SqlLab/index.jsx
Normal file
29
superset/assets/javascripts/SqlLab/index.jsx
Normal file
@@ -0,0 +1,29 @@
|
||||
const $ = window.$ = require('jquery');
|
||||
const jQuery = window.jQuery = $; // eslint-disable-line
|
||||
require('bootstrap');
|
||||
|
||||
import React from 'react';
|
||||
import { render } from 'react-dom';
|
||||
import { initialState, sqlLabReducer } from './reducers';
|
||||
import { enhancer } from '../reduxUtils';
|
||||
import { createStore, compose, applyMiddleware } from 'redux';
|
||||
import { Provider } from 'react-redux';
|
||||
import thunkMiddleware from 'redux-thunk';
|
||||
|
||||
import App from './components/App';
|
||||
|
||||
|
||||
require('./main.css');
|
||||
|
||||
let store = createStore(
|
||||
sqlLabReducer, initialState, compose(applyMiddleware(thunkMiddleware), enhancer()));
|
||||
|
||||
// jquery hack to highlight the navbar menu
|
||||
$('a:contains("SQL Lab")').parent().addClass('active');
|
||||
|
||||
render(
|
||||
<Provider store={store}>
|
||||
<App />
|
||||
</Provider>,
|
||||
document.getElementById('app')
|
||||
);
|
||||
296
superset/assets/javascripts/SqlLab/main.css
Normal file
296
superset/assets/javascripts/SqlLab/main.css
Normal file
@@ -0,0 +1,296 @@
|
||||
body {
|
||||
overflow: hidden;
|
||||
}
|
||||
.inlineBlock {
|
||||
display: inline-block;
|
||||
}
|
||||
.valignTop {
|
||||
vertical-align: top;
|
||||
}
|
||||
.inline {
|
||||
display: inline;
|
||||
}
|
||||
.nopadding {
|
||||
padding: 0px;
|
||||
}
|
||||
.panel.nopadding .panel-body {
|
||||
padding: 0px;
|
||||
}
|
||||
|
||||
.loading {
|
||||
width: 50px;
|
||||
margin-top: 15px;
|
||||
}
|
||||
.pane-cell {
|
||||
padding: 10px;
|
||||
overflow: auto;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
.SqlEditor .header {
|
||||
padding-top: 5px;
|
||||
padding-bottom: 5px;
|
||||
}
|
||||
|
||||
.scrollbar-container {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
width: 100%;
|
||||
height: 95%;
|
||||
}
|
||||
|
||||
.scrollbar-content {
|
||||
position: absolute;
|
||||
top: 0px;
|
||||
left: 0px;
|
||||
right: 0px;
|
||||
bottom: 0px;
|
||||
overflow: scroll;
|
||||
margin-right: 0px;
|
||||
margin-bottom: 100px;
|
||||
}
|
||||
|
||||
.Workspace .btn-sm {
|
||||
box-shadow: 1px 1px 2px rgba(0, 0, 0, 0.1);
|
||||
margin-top: 2px;
|
||||
padding: 4px;
|
||||
}
|
||||
.Workspace hr {
|
||||
margin-top: 10px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
div.Workspace {
|
||||
height: 100%;
|
||||
margin: 0px;
|
||||
}
|
||||
.SqlEditor .clock {
|
||||
background-color: orange;
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
.padded {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.p-t-10 {
|
||||
padding-top: 10px;
|
||||
}
|
||||
.p-t-5 {
|
||||
padding-top: 5px;
|
||||
}
|
||||
.m-r-5 {
|
||||
margin-right: 5px;
|
||||
}
|
||||
.m-r-3 {
|
||||
margin-right: 3px;
|
||||
}
|
||||
.m-l-1 {
|
||||
margin-left: 1px;
|
||||
}
|
||||
.m-l-2 {
|
||||
margin-left: 2px;
|
||||
}
|
||||
.m-r-10 {
|
||||
margin-right: 10px;
|
||||
}
|
||||
.m-l-10 {
|
||||
margin-left: 10px;
|
||||
}
|
||||
.m-l-5 {
|
||||
margin-left: 5px;
|
||||
}
|
||||
.m-b-10 {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.m-t-5 {
|
||||
margin-top: 5px;
|
||||
}
|
||||
.m-t-10 {
|
||||
margin-top: 10px;
|
||||
}
|
||||
.p-t-10 {
|
||||
padding-top: 10px;
|
||||
}
|
||||
.sqllab-toolbar {
|
||||
padding-top: 5px;
|
||||
border-bottom: 1px solid #DDD;
|
||||
}
|
||||
.no-shadow {
|
||||
box-shadow: none;
|
||||
background-color: transparent;
|
||||
}
|
||||
.pane-west {
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
}
|
||||
.circle {
|
||||
border-radius: 50%;
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
display: inline-block;
|
||||
background-color: #ccc;
|
||||
}
|
||||
.Pane2 {
|
||||
width: 0;
|
||||
}
|
||||
.running {
|
||||
background-color: lime;
|
||||
color: black;
|
||||
}
|
||||
.success {
|
||||
background-color: #4AC15F;
|
||||
}
|
||||
.failed {
|
||||
background-color: red;
|
||||
}
|
||||
|
||||
.handle {
|
||||
cursor: move;
|
||||
}
|
||||
.window {
|
||||
z-index: 1000;
|
||||
position: absolute;
|
||||
width: 300px;
|
||||
opacity: 0.85;
|
||||
border: 1px solid #AAA;
|
||||
max-height: 600px;
|
||||
box-shadow: rgba(0, 0, 0, 0.8) 5px 5px 25px
|
||||
}
|
||||
.SqlLab pre {
|
||||
padding: 0px !important;
|
||||
margin: 0px;
|
||||
border: none;
|
||||
font-size: 12px;
|
||||
line-height: @line-height-base;
|
||||
background-color: transparent !important;
|
||||
}
|
||||
|
||||
.Resizer {
|
||||
background: #000;
|
||||
opacity: .2;
|
||||
z-index: 1;
|
||||
-moz-box-sizing: border-box;
|
||||
-webkit-box-sizing: border-box;
|
||||
box-sizing: border-box;
|
||||
-moz-background-clip: padding;
|
||||
-webkit-background-clip: padding;
|
||||
background-clip: padding-box;
|
||||
}
|
||||
|
||||
.Resizer:hover {
|
||||
-webkit-transition: all 2s ease;
|
||||
transition: all 2s ease;
|
||||
}
|
||||
|
||||
.Resizer.horizontal {
|
||||
height: 10px;
|
||||
margin: -5px 0;
|
||||
border-top: 5px solid rgba(255, 255, 255, 0);
|
||||
border-bottom: 5px solid rgba(255, 255, 255, 0);
|
||||
cursor: row-resize;
|
||||
width: 100%;
|
||||
padding: 1px;
|
||||
}
|
||||
|
||||
.Resizer.horizontal:hover {
|
||||
border-top: 5px solid rgba(0, 0, 0, 0.5);
|
||||
border-bottom: 5px solid rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
.Resizer.vertical {
|
||||
width: 9px;
|
||||
margin: 0 -5px;
|
||||
border-left: 5px solid rgba(255, 255, 255, 0);
|
||||
border-right: 5px solid rgba(255, 255, 255, 0);
|
||||
cursor: col-resize;
|
||||
}
|
||||
|
||||
.Resizer.vertical:hover {
|
||||
border-left: 5px solid rgba(0, 0, 0, 0.5);
|
||||
border-right: 5px solid rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
.Resizer.disabled {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
.Resizer.disabled:hover {
|
||||
border-color: transparent;
|
||||
}
|
||||
|
||||
.popover{
|
||||
max-width:400px;
|
||||
}
|
||||
.Select-menu-outer {
|
||||
z-index: 1000;
|
||||
}
|
||||
.table-label {
|
||||
margin-top: 5px;
|
||||
margin-right: 10px;
|
||||
float: left;
|
||||
}
|
||||
div.tablePopover {
|
||||
opacity: 0.7 !important;
|
||||
}
|
||||
div.tablePopover:hover {
|
||||
opacity: 1 !important;
|
||||
}
|
||||
.ResultSetControls {
|
||||
padding-bottom: 3px;
|
||||
padding-top: 3px;
|
||||
}
|
||||
|
||||
.ace_editor {
|
||||
border: 1px solid #ccc;
|
||||
margin: 0px 0px 10px 0px;
|
||||
}
|
||||
|
||||
.Select-menu-outer {
|
||||
min-width: 100%;
|
||||
width: inherit;
|
||||
}
|
||||
.ace_content {
|
||||
background-color: #f4f4f4;
|
||||
}
|
||||
|
||||
.SouthPane .tab-content {
|
||||
padding-top: 10px;
|
||||
}
|
||||
|
||||
.TableElement {
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.TableElement .well {
|
||||
margin-top: 5px;
|
||||
margin-bottom: 5px;
|
||||
padding: 5px 10px;
|
||||
}
|
||||
|
||||
.QueryTable .label {
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
.ResultsModal .modal-body {
|
||||
min-height: 600px;
|
||||
}
|
||||
|
||||
.modal-body {
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
a.Link {
|
||||
cursor: pointer;
|
||||
}
|
||||
.QueryTable .well {
|
||||
padding: 3px 5px;
|
||||
margin: 3px 5px;
|
||||
}
|
||||
.tooltip pre {
|
||||
background: transparent;
|
||||
border: none;
|
||||
text-align: left;
|
||||
color: white;
|
||||
font-size: 10px;
|
||||
}
|
||||
.tooltip-inner {
|
||||
max-width: 500px;
|
||||
}
|
||||
253
superset/assets/javascripts/SqlLab/reducers.js
Normal file
253
superset/assets/javascripts/SqlLab/reducers.js
Normal file
@@ -0,0 +1,253 @@
|
||||
import shortid from 'shortid';
|
||||
import * as actions from './actions';
|
||||
import { now } from '../modules/dates';
|
||||
import { addToObject, alterInObject, alterInArr, removeFromArr, getFromArr, addToArr }
|
||||
from '../reduxUtils.js';
|
||||
|
||||
export const defaultQueryEditor = {
|
||||
id: shortid.generate(),
|
||||
title: 'Untitled Query',
|
||||
sql: 'SELECT *\nFROM\nWHERE',
|
||||
selectedText: null,
|
||||
latestQueryId: null,
|
||||
autorun: false,
|
||||
dbId: null,
|
||||
};
|
||||
|
||||
export const initialState = {
|
||||
alerts: [],
|
||||
networkOn: true,
|
||||
queries: {},
|
||||
databases: {},
|
||||
queryEditors: [defaultQueryEditor],
|
||||
tabHistory: [defaultQueryEditor.id],
|
||||
tables: [],
|
||||
queriesLastUpdate: 0,
|
||||
activeSouthPaneTab: 'Results',
|
||||
};
|
||||
|
||||
export const sqlLabReducer = function (state, action) {
|
||||
const actionHandlers = {
|
||||
[actions.ADD_QUERY_EDITOR]() {
|
||||
const tabHistory = state.tabHistory.slice();
|
||||
tabHistory.push(action.queryEditor.id);
|
||||
const newState = Object.assign({}, state, { tabHistory });
|
||||
return addToArr(newState, 'queryEditors', action.queryEditor);
|
||||
},
|
||||
[actions.CLONE_QUERY_TO_NEW_TAB]() {
|
||||
const progenitor = state.queryEditors.find((qe) =>
|
||||
qe.id === state.tabHistory[state.tabHistory.length - 1]);
|
||||
const qe = {
|
||||
id: shortid.generate(),
|
||||
title: `Copy of ${progenitor.title}`,
|
||||
dbId: (action.query.dbId) ? action.query.dbId : null,
|
||||
schema: (action.query.schema) ? action.query.schema : null,
|
||||
autorun: true,
|
||||
sql: action.query.sql,
|
||||
};
|
||||
|
||||
return sqlLabReducer(state, actions.addQueryEditor(qe));
|
||||
},
|
||||
[actions.REMOVE_QUERY_EDITOR]() {
|
||||
let newState = removeFromArr(state, 'queryEditors', action.queryEditor);
|
||||
// List of remaining queryEditor ids
|
||||
const qeIds = newState.queryEditors.map((qe) => qe.id);
|
||||
const queries = {};
|
||||
Object.keys(state.queries).forEach((k) => {
|
||||
const query = state.queries[k];
|
||||
if (qeIds.indexOf(query.sqlEditorId) > -1) {
|
||||
queries[k] = query;
|
||||
}
|
||||
});
|
||||
let tabHistory = state.tabHistory.slice();
|
||||
tabHistory = tabHistory.filter((id) => qeIds.indexOf(id) > -1);
|
||||
newState = Object.assign({}, newState, { tabHistory, queries });
|
||||
return newState;
|
||||
},
|
||||
[actions.REMOVE_QUERY]() {
|
||||
const newQueries = Object.assign({}, state.queries);
|
||||
delete newQueries[action.query.id];
|
||||
return Object.assign({}, state, { queries: newQueries });
|
||||
},
|
||||
[actions.RESET_STATE]() {
|
||||
return Object.assign({}, initialState);
|
||||
},
|
||||
[actions.MERGE_TABLE]() {
|
||||
const at = Object.assign({}, action.table);
|
||||
let existingTable;
|
||||
state.tables.forEach((t) => {
|
||||
if (
|
||||
t.dbId === at.dbId &&
|
||||
t.queryEditorId === at.queryEditorId &&
|
||||
t.schema === at.schema &&
|
||||
t.name === at.name) {
|
||||
existingTable = t;
|
||||
}
|
||||
});
|
||||
if (existingTable) {
|
||||
if (action.query) {
|
||||
at.dataPreviewQueryId = action.query.id;
|
||||
}
|
||||
return alterInArr(state, 'tables', existingTable, at);
|
||||
}
|
||||
at.id = shortid.generate();
|
||||
// for new table, associate Id of query for data preview
|
||||
at.dataPreviewQueryId = null;
|
||||
let newState = addToArr(state, 'tables', at);
|
||||
if (action.query) {
|
||||
newState = alterInArr(newState, 'tables', at, { dataPreviewQueryId: action.query.id });
|
||||
}
|
||||
return newState;
|
||||
},
|
||||
[actions.EXPAND_TABLE]() {
|
||||
return alterInArr(state, 'tables', action.table, { expanded: true });
|
||||
},
|
||||
[actions.REMOVE_DATA_PREVIEW]() {
|
||||
const queries = Object.assign({}, state.queries);
|
||||
delete queries[action.table.dataPreviewQueryId];
|
||||
const newState = alterInArr(state, 'tables', action.table, { dataPreviewQueryId: null });
|
||||
return Object.assign(
|
||||
{}, newState, { queries });
|
||||
},
|
||||
[actions.CHANGE_DATA_PREVIEW_ID]() {
|
||||
const queries = Object.assign({}, state.queries);
|
||||
delete queries[action.oldQueryId];
|
||||
|
||||
const newTables = [];
|
||||
state.tables.forEach((t) => {
|
||||
if (t.dataPreviewQueryId === action.oldQueryId) {
|
||||
newTables.push(Object.assign({}, t, { dataPreviewQueryId: action.newQuery.id }));
|
||||
} else {
|
||||
newTables.push(t);
|
||||
}
|
||||
});
|
||||
return Object.assign(
|
||||
{}, state, { queries, tables: newTables, activeSouthPaneTab: action.newQuery.id });
|
||||
},
|
||||
[actions.COLLAPSE_TABLE]() {
|
||||
return alterInArr(state, 'tables', action.table, { expanded: false });
|
||||
},
|
||||
[actions.REMOVE_TABLE]() {
|
||||
return removeFromArr(state, 'tables', action.table);
|
||||
},
|
||||
[actions.START_QUERY]() {
|
||||
let newState = Object.assign({}, state);
|
||||
if (action.query.sqlEditorId) {
|
||||
const qe = getFromArr(state.queryEditors, action.query.sqlEditorId);
|
||||
if (qe.latestQueryId) {
|
||||
const q = Object.assign({}, state.queries[qe.latestQueryId], { results: null });
|
||||
const queries = Object.assign({}, state.queries, { [q.id]: q });
|
||||
newState = Object.assign({}, state, { queries });
|
||||
}
|
||||
} else {
|
||||
newState.activeSouthPaneTab = action.query.id;
|
||||
}
|
||||
newState = addToObject(newState, 'queries', action.query);
|
||||
const sqlEditor = { id: action.query.sqlEditorId };
|
||||
return alterInArr(newState, 'queryEditors', sqlEditor, { latestQueryId: action.query.id });
|
||||
},
|
||||
[actions.STOP_QUERY]() {
|
||||
return alterInObject(state, 'queries', action.query, { state: 'stopped' });
|
||||
},
|
||||
[actions.CLEAR_QUERY_RESULTS]() {
|
||||
const newResults = Object.assign({}, action.query.results);
|
||||
newResults.data = [];
|
||||
return alterInObject(state, 'queries', action.query, { results: newResults, cached: true });
|
||||
},
|
||||
[actions.REQUEST_QUERY_RESULTS]() {
|
||||
return alterInObject(state, 'queries', action.query, { state: 'fetching' });
|
||||
},
|
||||
[actions.QUERY_SUCCESS]() {
|
||||
let rows;
|
||||
if (action.results.data) {
|
||||
rows = action.results.data.length;
|
||||
}
|
||||
const alts = {
|
||||
endDttm: now(),
|
||||
progress: 100,
|
||||
results: action.results,
|
||||
rows,
|
||||
state: 'success',
|
||||
errorMessage: null,
|
||||
cached: false,
|
||||
};
|
||||
return alterInObject(state, 'queries', action.query, alts);
|
||||
},
|
||||
[actions.QUERY_FAILED]() {
|
||||
const alts = { state: 'failed', errorMessage: action.msg, endDttm: now() };
|
||||
return alterInObject(state, 'queries', action.query, alts);
|
||||
},
|
||||
[actions.SET_ACTIVE_QUERY_EDITOR]() {
|
||||
const qeIds = state.queryEditors.map((qe) => qe.id);
|
||||
if (qeIds.indexOf(action.queryEditor.id) > -1) {
|
||||
const tabHistory = state.tabHistory.slice();
|
||||
tabHistory.push(action.queryEditor.id);
|
||||
return Object.assign({}, state, { tabHistory });
|
||||
}
|
||||
return state;
|
||||
},
|
||||
[actions.SET_ACTIVE_SOUTHPANE_TAB]() {
|
||||
return Object.assign({}, state, { activeSouthPaneTab: action.tabId });
|
||||
},
|
||||
[actions.QUERY_EDITOR_SETDB]() {
|
||||
return alterInArr(state, 'queryEditors', action.queryEditor, { dbId: action.dbId });
|
||||
},
|
||||
[actions.QUERY_EDITOR_SET_SCHEMA]() {
|
||||
return alterInArr(state, 'queryEditors', action.queryEditor, { schema: action.schema });
|
||||
},
|
||||
[actions.QUERY_EDITOR_SET_TITLE]() {
|
||||
return alterInArr(state, 'queryEditors', action.queryEditor, { title: action.title });
|
||||
},
|
||||
[actions.QUERY_EDITOR_SET_SQL]() {
|
||||
return alterInArr(state, 'queryEditors', action.queryEditor, { sql: action.sql });
|
||||
},
|
||||
[actions.QUERY_EDITOR_SET_SELECTED_TEXT]() {
|
||||
return alterInArr(state, 'queryEditors', action.queryEditor, { selectedText: action.sql });
|
||||
},
|
||||
[actions.QUERY_EDITOR_SET_AUTORUN]() {
|
||||
return alterInArr(state, 'queryEditors', action.queryEditor, { autorun: action.autorun });
|
||||
},
|
||||
[actions.ADD_ALERT]() {
|
||||
return addToArr(state, 'alerts', action.alert);
|
||||
},
|
||||
[actions.SET_DATABASES]() {
|
||||
const databases = {};
|
||||
action.databases.forEach((db) => {
|
||||
databases[db.id] = db;
|
||||
});
|
||||
return Object.assign({}, state, { databases });
|
||||
},
|
||||
[actions.REMOVE_ALERT]() {
|
||||
return removeFromArr(state, 'alerts', action.alert);
|
||||
},
|
||||
[actions.SET_NETWORK_STATUS]() {
|
||||
if (state.networkOn !== action.networkOn) {
|
||||
return Object.assign({}, state, { networkOn: action.networkOn });
|
||||
}
|
||||
return state;
|
||||
},
|
||||
[actions.REFRESH_QUERIES]() {
|
||||
let newQueries = Object.assign({}, state.queries);
|
||||
// Fetch the updates to the queries present in the store.
|
||||
let change = false;
|
||||
for (const id in action.alteredQueries) {
|
||||
const changedQuery = action.alteredQueries[id];
|
||||
if (
|
||||
!state.queries.hasOwnProperty(id) ||
|
||||
state.queries[id].changedOn !== changedQuery.changedOn) {
|
||||
newQueries[id] = Object.assign({}, state.queries[id], changedQuery);
|
||||
change = true;
|
||||
}
|
||||
}
|
||||
if (!change) {
|
||||
newQueries = state.queries;
|
||||
}
|
||||
const queriesLastUpdate = now();
|
||||
return Object.assign({}, state, { queries: newQueries, queriesLastUpdate });
|
||||
},
|
||||
};
|
||||
if (action.type in actionHandlers) {
|
||||
return actionHandlers[action.type]();
|
||||
}
|
||||
return state;
|
||||
};
|
||||
Reference in New Issue
Block a user