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

440 lines
15 KiB
React
Raw Normal View History

import React from 'react';
import { dataqueryService } from '@/_services';
import { toast } from 'react-toastify';
import 'react-toastify/dist/ReactToastify.css';
import SelectSearch, { fuzzySearch } from 'react-select-search';
import ReactTooltip from 'react-tooltip';
import { allSources } from './QueryEditors';
import { Transformation } from './Transformation';
import { defaultOptions } from './constants';
2021-05-22 11:29:27 +00:00
import ReactJson from 'react-json-view';
2021-04-04 17:07:03 +00:00
2021-04-30 06:31:32 +00:00
const queryNameRegex = new RegExp('^[A-Za-z0-9_-]*$');
2021-04-30 06:31:32 +00:00
const staticDataSources = [{ kind: 'restapi', id: 'restapi', name: 'REST API' }];
2021-04-04 17:07:03 +00:00
class QueryManager extends React.Component {
2021-04-30 06:31:32 +00:00
constructor(props) {
super(props);
this.state = {};
}
setStateFromProps = (props) => {
const selectedQuery = props.selectedQuery;
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,
2021-05-22 11:29:27 +00:00
currentState: props.currentState,
previewLoading: props.previewLoading,
queryPreviewData: props.queryPreviewData
2021-04-30 06:31:32 +00:00
},
() => {
if (this.props.mode === 'edit') {
let source = props.dataSources.find((datasource) => datasource.id === selectedQuery.data_source_id);
if(selectedQuery.kind === 'restapi') source = { kind: 'restapi' };
2021-04-30 06:31:32 +00:00
this.setState({
options: selectedQuery.options,
selectedDataSource: source,
selectedQuery,
queryName: selectedQuery.name
2021-04-30 06:31:32 +00:00
});
} else {
2021-04-30 06:31:32 +00:00
this.setState({
options: {},
selectedQuery: null
2021-04-30 06:31:32 +00:00
});
}
2021-04-30 06:31:32 +00:00
}
);
};
componentWillReceiveProps(nextProps) {
this.setStateFromProps(nextProps);
}
componentDidMount() {
this.setStateFromProps(this.props);
}
changeDataSource = (sourceId) => {
const source = [...this.state.dataSources, ...staticDataSources].find((datasource) => datasource.id === sourceId);
2021-04-30 06:31:32 +00:00
this.setState({
selectedDataSource: source,
options: defaultOptions[source.kind],
queryName: this.computeQueryName(source.kind)
2021-04-30 06:31:32 +00:00
});
};
switchCurrentTab = (tab) => {
this.setState({
currentTab: tab
2021-04-30 06:31:32 +00:00
});
};
validateQueryName = () => {
const {
queryName, dataQueries, mode, selectedQuery
} = this.state;
2021-04-30 06:31:32 +00:00
if (mode === 'create') {
return dataQueries.find((query) => query.name === queryName) === undefined && queryNameRegex.test(queryName);
}
const existingQuery = dataQueries.find((query) => query.name === queryName);
if (existingQuery) {
return existingQuery.id === selectedQuery.id && queryNameRegex.test(queryName);
}
return queryNameRegex.test(queryName);
2021-04-30 06:31:32 +00:00
};
computeQueryName = (kind) => {
const { dataQueries } = this.state;
const currentQueriesForKind = dataQueries.filter((query) => query.kind === kind);
let found = false;
let newName = '';
2021-04-30 06:31:32 +00:00
let currentNumber = currentQueriesForKind.length + 1;
while (!found) {
newName = `${kind}${currentNumber}`;
if (dataQueries.find((query) => query.name === newName) === undefined) {
2021-04-30 06:31:32 +00:00
found = true;
}
currentNumber += 1;
2021-04-04 06:26:23 +00:00
}
return newName;
2021-04-30 06:31:32 +00:00
};
createOrUpdateDataQuery = () => {
const {
appId, options, selectedDataSource, mode, queryName
} = this.state;
2021-04-30 06:31:32 +00:00
const kind = selectedDataSource.kind;
const dataSourceId = selectedDataSource.id;
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'
2021-04-30 06:31:32 +00:00
});
return;
}
2021-04-30 06:31:32 +00:00
if (mode === 'edit') {
this.setState({ isUpdating: true });
dataqueryService.update(this.state.selectedQuery.id, queryName, options).then(() => {
2021-04-30 06:31:32 +00:00
toast.success('Query Updated', { hideProgressBar: true, position: 'bottom-center' });
this.setState({ isUpdating: false });
this.props.dataQueriesChanged();
}).catch((error) => {
this.setState({ isUpdating: false });
toast.error(error, { hideProgressBar: true, position: 'bottom-center' });
2021-04-30 06:31:32 +00:00
});
} else {
this.setState({ isCreating: true });
dataqueryService.create(appId, queryName, kind, options, dataSourceId).then(() => {
2021-04-30 06:31:32 +00:00
toast.success('Query Added', { hideProgressBar: true, position: 'bottom-center' });
this.setState({ isCreating: false });
this.props.dataQueriesChanged();
}).catch((error) => {
this.setState({ isCreating: false });
toast.error(error, { hideProgressBar: true, position: 'bottom-center' });
2021-04-30 06:31:32 +00:00
});
}
2021-04-30 06:31:32 +00:00
};
optionchanged = (option, value) => {
this.setState({ options: { ...this.state.options, [option]: value } });
};
optionsChanged = (newOptions) => {
this.setState({ options: newOptions });
};
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,
2021-05-22 11:50:55 +00:00
options,
2021-04-30 06:31:32 +00:00
currentTab,
isUpdating,
isCreating,
addingQuery,
editingQuery,
selectedQuery,
queryPaneHeight,
currentState,
2021-05-22 11:29:27 +00:00
queryName,
previewLoading,
queryPreviewData
2021-04-30 06:31:32 +00:00
} = this.state;
let ElementToRender = '';
if (selectedDataSource) {
const sourcecomponentName = selectedDataSource.kind.charAt(0).toUpperCase() + selectedDataSource.kind.slice(1);
ElementToRender = allSources[sourcecomponentName];
2021-04-04 17:07:03 +00:00
}
2021-04-30 06:31:32 +00:00
let buttonText = mode === 'edit' ? 'Save' : 'Create';
const buttonDisabled = isUpdating || isCreating;
2021-04-07 07:03:03 +00:00
2021-04-30 06:31:32 +00:00
if (isUpdating) {
buttonText = 'Saving...';
}
if (isCreating) {
buttonText = 'Creating...';
}
2021-04-30 06:31:32 +00:00
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 query-manager-header" data-bs-toggle="tabs">
2021-04-30 06:31:32 +00:00
<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) && selectedDataSource) && (
2021-04-30 06:31:32 +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 })}
className="form-control-plaintext form-control-plaintext-sm mt-1"
2021-04-30 06:31:32 +00:00
value={queryName}
style={{ width: '160px' }}
autoFocus={false}
2021-04-30 06:31:32 +00:00
/>
<span className="input-icon-addon">
2021-04-30 06:31:32 +00:00
<img src="https://www.svgrepo.com/show/149235/edit.svg" width="12" height="12" />
</span>
</div>
</div>
)}
<div className="col-auto">
2021-05-22 11:50:55 +00:00
{(addingQuery || editingQuery) && (
2021-05-22 11:29:27 +00:00
<span
2021-05-22 11:50:55 +00:00
onClick={() => this.props.previewQuery(addingQuery ? { data_source_id: selectedDataSource.id, options: options } : selectedQuery)}
2021-05-22 11:29:27 +00:00
className={`btn btn-secondary m-1 float-right1 ${
previewLoading ? ' btn-loading' : ''
}`}
>
Preview
</span>
)}
2021-04-30 06:31:32 +00:00
{(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"
>
2021-05-20 05:06:32 +00:00
<img src="https://www.svgrepo.com/show/310311/arrow-maximize.svg" width="12" height="12" />
2021-04-30 06:31:32 +00:00
</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 className="py-2">
2021-04-30 06:31:32 +00:00
{currentTab === 1 && (
2021-05-13 07:25:54 +00:00
<div className="row row-deck px-2">
2021-04-30 06:31:32 +00:00
{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 };
})
2021-04-30 06:31:32 +00:00
]}
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} currentState={currentState}/>
<hr></hr>
<div className="mb-3 mt-2">
2021-05-03 14:27:32 +00:00
<Transformation changeOption={this.optionchanged} options={this.state.options} currentState={currentState}/>
</div>
2021-05-22 11:29:27 +00:00
<div className="row header border-top">
<div className="py-2">
Preview
</div>
</div>
<div className="mb-3 mt-2">
{previewLoading && <div class="spinner-border text-azure" role="status"></div>}
{previewLoading === false &&
<div>
<ReactJson
name={false}
style={{ fontSize: '0.7rem' }}
enableClipboard={false}
src={queryPreviewData}
displayDataTypes={false}
collapsed={true}
displayObjectSize={false}
quotesOnKeys={false}
sortKeys={true}
indentWidth={1}
/>
</div>
}
</div>
2021-04-30 06:31:32 +00:00
</div>
)}
</div>
)}
{currentTab === 2 && (
<div className="advanced-options-container m-2">
2021-04-30 06:31:32 +00:00
<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 className="row mt-3">
<div className="col-auto">
<label className="form-label p-2">Success Message</label>
2021-04-30 06:31:32 +00:00
</div>
<div className="col">
2021-04-30 06:31:32 +00:00
<input
type="text"
disabled={!this.state.options.showSuccessNotification}
onChange={(e) => this.optionchanged('successMessage', e.target.value)}
placeholder="Query ran successfully"
className="form-control"
2021-04-30 06:31:32 +00:00
value={this.state.options.successMessage}
/>
</div>
</div>
2021-04-30 06:31:32 +00:00
<hr />
<div className="row mt-3">
<div className="col-auto">
<label className="form-label p-2">Notification duration (s)</label>
2021-04-30 06:31:32 +00:00
</div>
<div className="col">
2021-04-30 06:31:32 +00:00
<input
type="number"
disabled={!this.state.options.showSuccessNotification}
onChange={(e) => this.optionchanged('notificationDuration', e.target.value)}
placeholder={5}
className="form-control"
2021-04-30 06:31:32 +00:00
value={this.state.options.notificationDuration}
/>
</div>
</div>
2021-04-30 06:31:32 +00:00
</div>
)}
</div>
)}
</div>
);
}
}
2021-04-04 06:46:53 +00:00
export { QueryManager };