[sql lab] allow users to save their queries (#2528)

* Allow users to save their queries

Fixing tests .

* Adding placeholder for Query Description

* initJQueryCSRF -> initJQueryAjaxCSRF
This commit is contained in:
Maxime Beauchemin
2017-04-04 20:15:19 -07:00
committed by GitHub
parent c1d9918abe
commit 122891c29b
31 changed files with 656 additions and 279 deletions

View File

@@ -1,34 +0,0 @@
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;

View File

@@ -0,0 +1,18 @@
import React from 'react';
import AlertContainer from 'react-alert';
export default class AlertsWrapper extends React.PureComponent {
render() {
return (
<AlertContainer
ref={ref => {
global.notify = ref;
}}
offset={14}
position="top right"
theme="dark"
time={5000}
transition="fade"
/>);
}
}

View File

@@ -5,7 +5,7 @@ import React from 'react';
import TabbedSqlEditors from './TabbedSqlEditors';
import QueryAutoRefresh from './QueryAutoRefresh';
import QuerySearch from './QuerySearch';
import Alerts from './Alerts';
import AlertsWrapper from './AlertsWrapper';
import { bindActionCreators } from 'redux';
import { connect } from 'react-redux';
@@ -64,7 +64,7 @@ class App extends React.PureComponent {
}
return (
<div className="App SqlLab">
<Alerts id="sqllab-alerts" alerts={this.props.alerts} actions={this.props.actions} />
<AlertsWrapper />
<div className="container-fluid">
{content}
</div>

View File

@@ -31,7 +31,7 @@ export default function RunQueryActionButton(props) {
onClick={() => props.runQuery(false)}
key="run-btn"
>
<i className="fa fa-table" /> {runBtnText}
<i className="fa fa-refresh" /> {runBtnText}
</Button>
);

View File

@@ -0,0 +1,131 @@
/* global notify */
import React from 'react';
import { FormControl, FormGroup, Overlay, Popover, Row, Col } from 'react-bootstrap';
import Button from '../../components/Button';
const propTypes = {
defaultLabel: React.PropTypes.string,
sql: React.PropTypes.string,
schema: React.PropTypes.string,
dbId: React.PropTypes.number,
animation: React.PropTypes.bool,
onSave: React.PropTypes.func,
};
const defaultProps = {
defaultLabel: 'Undefined',
animation: true,
onSave: () => {},
};
class SaveQuery extends React.PureComponent {
constructor(props) {
super(props);
this.state = {
description: '',
label: props.defaultLabel,
showSave: false,
};
this.toggleSave = this.toggleSave.bind(this);
this.onSave = this.onSave.bind(this);
this.onCancel = this.onCancel.bind(this);
this.onLabelChange = this.onLabelChange.bind(this);
this.onDescriptionChange = this.onDescriptionChange.bind(this);
}
onSave() {
const query = {
label: this.state.label,
description: this.state.description,
db_id: this.props.dbId,
schema: this.props.schema,
sql: this.props.sql,
};
this.props.onSave(query);
this.setState({ showSave: false });
}
onCancel() {
this.setState({ showSave: false });
}
onLabelChange(e) {
this.setState({ label: e.target.value });
}
onDescriptionChange(e) {
this.setState({ description: e.target.value });
}
renderPopover() {
return (
<Popover id="embed-code-popover">
<FormGroup bsSize="small" style={{ width: '350px' }}>
<Row>
<Col md={12}>
<small>
<label className="control-label" htmlFor="embed-height">
Label
</label>
</small>
<FormControl
type="text"
placeholder="Label for your query"
value={this.state.label}
onChange={this.onLabelChange}
/>
</Col>
</Row>
<br />
<Row>
<Col md={12}>
<small>
<label className="control-label" htmlFor="embed-height">Description</label>
</small>
<FormControl
componentClass="textarea"
placeholder="Write a description for your query"
value={this.state.description}
onChange={this.onDescriptionChange}
/>
</Col>
</Row>
<br />
<Row>
<Col md={12}>
<Button
bsStyle="primary"
onClick={this.onSave}
className="m-r-3"
>
Save
</Button>
<Button onClick={this.onCancel} className="cancelQuery">
Cancel
</Button>
</Col>
</Row>
</FormGroup>
</Popover>
);
}
toggleSave(e) {
this.setState({ target: e.target, showSave: !this.state.showSave });
}
render() {
return (
<span className="SaveQuery">
<Overlay
trigger="click"
target={this.state.target}
show={this.state.showSave}
placement="bottom"
animation={this.props.animation}
>
{this.renderPopover()}
</Overlay>
<Button bsSize="small" className="toggleSave" onClick={this.toggleSave}>
<i className="fa fa-save" /> Save Query
</Button>
</span>
);
}
}
SaveQuery.propTypes = propTypes;
SaveQuery.defaultProps = defaultProps;
export default SaveQuery;

View File

@@ -15,6 +15,7 @@ import {
import Button from '../../components/Button';
import SouthPane from './SouthPane';
import SaveQuery from './SaveQuery';
import Timer from '../../components/Timer';
import SqlEditorLeftBar from './SqlEditorLeftBar';
import AceEditorWrapper from './AceEditorWrapper';
@@ -101,6 +102,7 @@ class SqlEditor extends React.PureComponent {
}
render() {
const qe = this.props.queryEditor;
let limitWarning = null;
if (this.props.latestQuery && this.props.latestQuery.limit_reached) {
const tooltip = (
@@ -149,12 +151,19 @@ class SqlEditor extends React.PureComponent {
<Form inline>
<RunQueryActionButton
allowAsync={this.props.database ? this.props.database.allow_run_async : false}
dbId={this.props.queryEditor.dbId}
dbId={qe.dbId}
queryState={this.props.latestQuery && this.props.latestQuery.state}
runQuery={this.runQuery.bind(this)}
selectedText={this.props.queryEditor.selectedText}
selectedText={qe.selectedText}
stopQuery={this.stopQuery.bind(this)}
/>
<SaveQuery
defaultLabel={qe.title}
sql={qe.sql}
onSave={this.props.actions.saveQuery}
schema={qe.schema}
dbId={qe.dbId}
/>
{ctasControls}
</Form>
</div>

View File

@@ -6,7 +6,7 @@ import * as Actions from '../actions';
import SqlEditor from './SqlEditor';
import CopyQueryTabUrl from './CopyQueryTabUrl';
import { areArraysShallowEqual } from '../../reduxUtils';
import { getParamFromQuery } from '../../../utils/common';
import URI from 'urijs';
const propTypes = {
actions: React.PropTypes.object.isRequired,
@@ -35,19 +35,19 @@ class TabbedSqlEditors extends React.PureComponent {
};
}
componentDidMount() {
const search = window.location.search;
if (search) {
const queryString = search.substring(1);
const urlId = getParamFromQuery(queryString, 'id');
if (urlId) {
this.props.actions.popStoredQuery(urlId);
} else {
let dbId = getParamFromQuery(queryString, 'dbid');
const query = URI(window.location).search(true);
if (query.id || query.sql || query.savedQueryId) {
if (query.id) {
this.props.actions.popStoredQuery(query.id);
} else if (query.savedQueryId) {
this.props.actions.popSavedQuery(query.savedQueryId);
} else if (query.sql) {
let dbId = query.dbid;
if (dbId) {
dbId = parseInt(dbId, 10);
} else {
const databases = this.props.databases;
const dbName = getParamFromQuery(queryString, 'dbname');
const dbName = query.dbname;
if (dbName) {
Object.keys(databases).forEach((db) => {
if (databases[db].database_name === dbName) {
@@ -57,11 +57,11 @@ class TabbedSqlEditors extends React.PureComponent {
}
}
const newQueryEditor = {
title: getParamFromQuery(queryString, 'title'),
title: query.title,
dbId,
schema: getParamFromQuery(queryString, 'schema'),
autorun: getParamFromQuery(queryString, 'autorun'),
sql: getParamFromQuery(queryString, 'sql'),
schema: query.schema,
autorun: query.autorun,
sql: query.sql,
};
this.props.actions.addQueryEditor(newQueryEditor);
}