ToolJet/frontend/src/Editor/QueryManager/QueryManager.jsx

428 lines
18 KiB
React
Raw Normal View History

import React from 'react';
import { dataqueryService, authenticationService } from '@/_services';
import { ToastContainer, toast } from 'react-toastify';
import 'react-toastify/dist/ReactToastify.css';
import { Restapi } from './Restapi';
import { Mysql } from './Mysql';
import { Postgresql } from './Postgresql';
2021-04-08 01:20:30 +00:00
import { Stripe } from './Stripe';
import { Firestore } from './Firestore';
2021-04-19 16:49:57 +00:00
import { Redis } from './Redis';
2021-04-22 10:59:14 +00:00
import { Googlesheets } from './Googlesheets';
import SelectSearch, { fuzzySearch } from 'react-select-search';
import ReactTooltip from 'react-tooltip';
2021-04-28 08:06:14 +00:00
import { Elasticsearch } from './Elasticsearch';
const allSources = {
Restapi,
Mysql,
2021-04-08 01:20:30 +00:00
Postgresql,
Stripe,
2021-04-19 16:49:57 +00:00
Firestore,
2021-04-22 10:59:14 +00:00
Redis,
2021-04-28 08:06:14 +00:00
Googlesheets,
Elasticsearch
}
2021-04-04 17:07:03 +00:00
const queryNameRegex = new RegExp("^[A-Za-z0-9_-]*$");
2021-04-04 17:07:03 +00:00
const staticDataSources = [
{ kind: 'restapi', id: 'restapi', name: 'REST API' },
]
2021-04-08 01:20:30 +00:00
const defaultOptions = {
2021-04-04 17:07:03 +00:00
'postgresql': {
2021-04-19 16:49:57 +00:00
},
'redis': {
query: 'PING'
2021-04-08 01:20:30 +00:00
},
'mysql': {
},
'firestore': {
path: '',
2021-04-28 08:06:14 +00:00
},
'elasticsearch': {
query: '',
2021-04-04 17:07:03 +00:00
},
'restapi': {
method: 'GET',
url: null,
url_params: [ ['', ''] ],
headers: [ ['', ''] ],
body: [ ['', ''] ],
2021-04-08 01:20:30 +00:00
},
'stripe': {
2021-04-22 10:59:14 +00:00
},
'googlesheets': {
operation: 'read'
2021-04-04 17:07:03 +00:00
}
}
class QueryManager extends React.Component {
constructor(props) {
super(props);
this.state = {
2021-04-07 05:02:44 +00:00
};
}
2021-04-07 05:02:44 +00:00
setStateFromProps = (props) => {
const selectedQuery = props.selectedQuery;
2021-04-07 04:14:40 +00:00
2021-04-07 05:02:44 +00:00
this.setState({
appId: props.appId,
dataSources: props.dataSources,
dataQueries: props.dataQueries,
mode: props.mode,
currentTab: 1,
addingQuery: props.addingQuery,
editingQuery: props.editingQuery,
queryPaneHeight: props.queryPaneHeight,
currentState: props.currentState
}, () => {
if(this.props.mode === 'edit') {
const source = props.dataSources.find(source => source.id === selectedQuery.data_source_id)
this.setState({
options: selectedQuery.options,
selectedDataSource: source,
selectedQuery,
queryName: selectedQuery.name
})
} else {
this.setState({
options: {},
selectedDataSource: null,
selectedQuery: null,
})
}
2021-04-07 05:02:44 +00:00
});
}
2021-04-07 04:14:40 +00:00
2021-04-07 05:02:44 +00:00
componentWillReceiveProps(nextProps) {
this.setStateFromProps(nextProps);
}
componentDidMount() {
this.setStateFromProps(this.props);
}
changeDataSource = (sourceId) => {
2021-04-04 17:07:03 +00:00
const source = [...this.state.dataSources, ...staticDataSources].find(source => source.id === sourceId);
this.setState({ selectedDataSource: source, options: defaultOptions[source.kind], queryName: this.computeQueryName(source.kind) });
}
switchCurrentTab = (tab) => {
this.setState({
currentTab: tab
});
}
validateQueryName = () => {
const { queryName, dataQueries, mode, selectedQuery} = this.state;
if(mode === 'create') {
return (dataQueries.find(query => query.name === queryName) === undefined) && queryNameRegex.test(queryName);
} else {
const existingQuery = dataQueries.find(query => query.name === queryName);
if (existingQuery) {
return (existingQuery.id === selectedQuery.id) && queryNameRegex.test(queryName);
} else {
queryNameRegex.test(queryName);
}
}
}
2021-04-04 06:26:23 +00:00
computeQueryName = (kind) => {
const { dataQueries } = this.state;
const currentQueriesForKind = dataQueries.filter(query => query.kind === kind);
let found = false;
let name = '';
let currentNumber = currentQueriesForKind.length + 1;
2021-04-06 03:14:52 +00:00
2021-04-04 06:26:23 +00:00
while(!found) {
name = `${kind}${currentNumber}`;
if(dataQueries.find(query => query.name === name) === undefined) {
found = true;
}
2021-04-06 03:14:52 +00:00
currentNumber = currentNumber + 1
2021-04-04 06:26:23 +00:00
}
return name;
}
2021-04-07 04:14:40 +00:00
createOrUpdateDataQuery = () => {
const { appId, options, selectedDataSource, mode, queryName } = this.state;
const kind = selectedDataSource.kind;
2021-04-06 03:14:52 +00:00
const dataSourceId = selectedDataSource.id;
2021-04-07 04:14:40 +00:00
const isQueryNameValid = this.validateQueryName();
if(!isQueryNameValid) {
toast.error('Invalid query name. Should be unique and only include letters, numbers and underscore.', { hideProgressBar: true, position: "bottom-center", });
return;
}
2021-04-07 04:14:40 +00:00
if ( mode === 'edit') {
this.setState({ isUpdating: true });
dataqueryService.update(this.state.selectedQuery.id, queryName, options).then((data) => {
toast.success('Query Updated', { hideProgressBar: true, position: "bottom-center", });
this.setState({ isUpdating: false });
2021-04-07 04:14:40 +00:00
this.props.dataQueriesChanged();
});
} else {
this.setState({ isCreating: true });
dataqueryService.create(appId, queryName, kind, options, dataSourceId).then((data) => {
toast.success('Query Added', { hideProgressBar: true, position: "bottom-center", });
this.setState({ isCreating: false });
2021-04-07 04:14:40 +00:00
this.props.dataQueriesChanged();
});
}
}
optionchanged = (option, value) => {
this.setState( { options: { ...this.state.options, [option]: value } } );
}
2021-04-04 17:07:03 +00:00
optionsChanged = (newOptions) => {
this.setState({ options: newOptions });
}
2021-04-07 07:03:03 +00:00
toggleOption = (option) => {
const currentValue = this.state.options[option] ? this.state.options[option] : false;
this.optionchanged(option, !currentValue);
}
renderDataSourceOption = (props, option, snapshot, className) => {
return (
<button {...props} className={className} type="button">
<div className="row">
<div className="col-md-9">
<span className="text-muted mx-2">{option.name}</span>
</div>
</div>
</button>
);
}
render() {
const {
dataSources,
selectedDataSource,
mode,
currentTab,
isUpdating,
isCreating,
addingQuery,
editingQuery,
selectedQuery,
queryPaneHeight,
currentState,
queryName
} = this.state;
let ElementToRender = '';
if(selectedDataSource) {
const sourcecomponentName = selectedDataSource.kind.charAt(0).toUpperCase() + selectedDataSource.kind.slice(1);
ElementToRender = allSources[sourcecomponentName];
}
let buttonText = mode === 'edit' ? 'Save' : 'Create';
const buttonDisabled = isUpdating || isCreating;
if(isUpdating) { buttonText = 'Saving...'}
if(isCreating) { buttonText = 'Creating...'}
return (
<div className="query-manager" key={selectedQuery ? selectedQuery.id : ''}>
<ReactTooltip type="dark" effect="solid" delayShow={250} />
<div className="row header">
<div className="col">
{(addingQuery || editingQuery) &&
<div className="nav-header">
<ul className="nav nav-tabs" data-bs-toggle="tabs">
<li className="nav-item">
<a
onClick={() => this.switchCurrentTab(1)}
className={currentTab === 1 ? 'nav-link active' : 'nav-link'}
>
&nbsp; General
</a>
</li>
<li className="nav-item">
<a
onClick={() => this.switchCurrentTab(2)}
className={currentTab === 2 ? 'nav-link active' : 'nav-link'}
>
&nbsp; Advanced
</a>
</li>
</ul>
</div>
}
</div>
{(addingQuery || editingQuery) &&
2021-04-29 16:56:33 +00:00
<div className="col query-name-field">
<div className="input-icon" style={{width: '160px'}}>
<input
type="text"
onChange={(e) => this.setState({ queryName: e.target.value })}
class="form-control-plaintext form-control-plaintext-sm mt-1"
value={queryName}
style={{width: '160px'}}
autoFocus
/>
<span class="input-icon-addon">
<img src="https://www.svgrepo.com/show/149235/edit.svg" width="12" height="12"/>
</span>
</div>
</div>
}
<div className="col-auto">
{((addingQuery || editingQuery) && selectedQuery) &&
<span
data-tip="NOTE: Query should be saved before running."
onClick={() => this.props.runQuery(selectedQuery.id, selectedQuery.name)}
className={`btn btn-secondary m-1 float-right1 ${currentState.queries[selectedQuery.name].isLoading === true ? ' btn-loading' : '' }`}
>
Run
</span>
}
{(addingQuery || editingQuery) &&
<button onClick={this.createOrUpdateDataQuery} disabled={buttonDisabled} className="btn btn-primary m-1 float-right">
{ buttonText }
</button>
}
{queryPaneHeight === '30%' ?
<span className="btn btn-light m-1" onClick={this.props.toggleQueryPaneHeight} data-tip="Maximize query editor">
<img src="https://www.svgrepo.com/show/129993/expand.svg" width="12" height="12"/>
</span>
:
<span className="btn btn-light m-1" onClick={this.props.toggleQueryPaneHeight} data-tip="Minimize query editor">
<img src="https://www.svgrepo.com/show/310476/arrow-minimize.svg" width="12" height="12"/>
</span>
}
</div>
</div>
{(addingQuery || editingQuery) &&
<div>
{currentTab === 1 &&
<div className="row row-deck p-3">
{(dataSources && mode ==='create') &&
<div className="datasource-picker mb-2">
<label className="form-label col-md-2">Datasource</label>
<SelectSearch
options={[
...dataSources.map(source => { return { name: source.name, value: source.id } }),
...staticDataSources.map(source => { return {name: source.name, value: source.id} }),
]}
value={selectedDataSource ? selectedDataSource.id : ''}
search={true}
onChange={(value) => this.changeDataSource(value) }
filterOptions={fuzzySearch}
renderOption={this.renderDataSourceOption}
placeholder="Select a data source"
/>
</div>
}
{selectedDataSource &&
<div>
<ElementToRender
options={this.state.options}
optionsChanged={this.optionsChanged}
/>
</div>
}
</div>
}
{currentTab === 2 &&
<div className="advanced-options-container p-2 m-2">
<label className="form-check form-switch">
<input
className="form-check-input"
type="checkbox"
onClick={() => this.toggleOption('runOnPageLoad')}
checked={this.state.options.runOnPageLoad}
/>
<span className="form-check-label">Run this query on page load?</span>
</label>
<label className="form-check form-switch">
<input
className="form-check-input"
type="checkbox"
onClick={() => this.toggleOption('requestConfirmation')}
checked={this.state.options.requestConfirmation}
/>
<span className="form-check-label">Request confirmation before running query?</span>
</label>
<hr/>
<label className="form-check form-switch">
<input
className="form-check-input"
type="checkbox"
onClick={() => this.toggleOption('showSuccessNotification')}
checked={this.state.options.showSuccessNotification}
/>
<span className="form-check-label">Show notification on success?</span>
</label>
<div class="row mt-3">
<div class="col-auto">
<label class="form-label p-2">Success Message</label>
</div>
<div class="col">
<input
type="text"
disabled={!this.state.options.showSuccessNotification}
value={this.state.options.successMessage}
onChange={(e) => this.optionchanged('successMessage', e.target.value)}
placeholder="Query ran successfully"
class="form-control"
value={this.state.options.successMessage}
/>
</div>
</div>
<hr/>
<div class="row mt-3">
<div class="col-auto">
<label class="form-label p-2">Notification duration (s)</label>
</div>
<div class="col">
<input
type="number"
disabled={!this.state.options.showSuccessNotification}
value={this.state.options.notificationDuration}
onChange={(e) => this.optionchanged('notificationDuration', e.target.value)}
placeholder={5}
class="form-control"
value={this.state.options.notificationDuration}
/>
</div>
</div>
</div>
}
</div>
}
</div>
)
}
}
2021-04-04 06:46:53 +00:00
export { QueryManager };