import React from 'react'; import { dataqueryService } from '@/_services'; import toast from 'react-hot-toast'; import SelectSearch, { fuzzySearch } from 'react-select-search'; import ReactTooltip from 'react-tooltip'; import { allSources } from './QueryEditors'; import { Transformation } from './Transformation'; import ReactJson from 'react-json-view'; import { previewQuery } from '@/_helpers/appUtils'; import { EventManager } from '../Inspector/EventManager'; import { CodeHinter } from '../CodeBuilder/CodeHinter'; import { DataSourceTypes } from '../DataSourceManager/SourceComponents'; const queryNameRegex = new RegExp('^[A-Za-z0-9_-]*$'); const staticDataSources = [ { kind: 'restapi', id: 'null', name: 'REST API' }, { kind: 'runjs', id: 'runjs', name: 'Run JavaScript code' }, ]; let QueryManager = class QueryManager extends React.Component { constructor(props) { super(props); this.state = { options: {}, selectedQuery: null, selectedDataSource: null, dataSourceMeta: {}, }; this.previewPanelRef = React.createRef(); } setStateFromProps = (props) => { const selectedQuery = props.selectedQuery; const dataSourceId = selectedQuery?.data_source_id; const source = props.dataSources.find((datasource) => datasource.id === dataSourceId); let dataSourceMeta = DataSourceTypes.find((source) => source.kind === selectedQuery?.kind); const paneHeightChanged = this.state.queryPaneHeight !== props.queryPaneHeight; 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, selectedSource: source, dataSourceMeta, selectedDataSource: paneHeightChanged ? this.state.selectedDataSource : props.selectedDataSource, }, () => { if (this.props.mode === 'edit') { let source = props.dataSources.find((datasource) => datasource.id === selectedQuery.data_source_id); if (selectedQuery.kind === 'restapi') { if (!selectedQuery.data_source_id) { source = { kind: 'restapi' }; } } if (selectedQuery.kind === 'runjs') { if (!selectedQuery.data_source_id) { source = { kind: 'runjs' }; } } this.setState({ options: paneHeightChanged ? this.state.options : selectedQuery.options, selectedDataSource: source, selectedQuery, queryName: selectedQuery.name, }); } // } else { // this.setState({ // options: {}, // selectedQuery: null, // selectedDataSource: paneHeightChanged ? this.state.selectedDataSource : props.selectedDataSource, // }); // } } ); }; componentWillReceiveProps(nextProps) { this.setStateFromProps(nextProps); } componentDidMount() { this.setStateFromProps(this.props); } changeDataSource = (sourceId) => { const source = [...this.state.dataSources, ...staticDataSources].find((datasource) => datasource.id === sourceId); const isSchemaUnavailable = ['restapi', 'stripe', 'runjs'].includes(source.kind); const schemaUnavailableOptions = { restapi: { method: 'get', url: null, url_params: [], headers: [], body: [], }, stripe: {}, runjs: {}, }; this.setState({ selectedDataSource: source, selectedSource: source, queryName: this.computeQueryName(source.kind), ...(isSchemaUnavailable && { options: schemaUnavailableOptions[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); } const existingQuery = dataQueries.find((query) => query.name === queryName); if (existingQuery) { return existingQuery.id === selectedQuery.id && queryNameRegex.test(queryName); } return queryNameRegex.test(queryName); }; computeQueryName = (kind) => { const { dataQueries } = this.state; const currentQueriesForKind = dataQueries.filter((query) => query.kind === kind); let found = false; let newName = ''; let currentNumber = currentQueriesForKind.length + 1; while (!found) { newName = `${kind}${currentNumber}`; if (dataQueries.find((query) => query.name === newName) === undefined) { found = true; } currentNumber += 1; } return newName; }; createOrUpdateDataQuery = () => { const { appId, options, selectedDataSource, mode, queryName } = this.state; const kind = selectedDataSource.kind; const dataSourceId = selectedDataSource.id === 'null' ? null : selectedDataSource.id; const isQueryNameValid = this.validateQueryName(); if (!isQueryNameValid) { toast.error('Invalid query name. Should be unique and only include letters, numbers and underscore.'); return; } if (mode === 'edit') { this.setState({ isUpdating: true }); dataqueryService .update(this.state.selectedQuery.id, queryName, options) .then(() => { toast.success('Query Updated'); this.setState({ isUpdating: false }); this.props.dataQueriesChanged(); }) .catch(({ error }) => { this.setState({ isUpdating: false }); toast.error(error); }); } else { this.setState({ isCreating: true }); dataqueryService .create(appId, queryName, kind, options, dataSourceId) .then(() => { toast.success('Query Added'); this.setState({ isCreating: false }); this.props.dataQueriesChanged(); }) .catch(({ error }) => { this.setState({ isCreating: false }); toast.error(error); }); } }; 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 ( ); }; // Here we have mocked data query in format of a component to be usable by event manager // TODO: Refactor EventManager to be generic mockDataQueryAsComponent = () => { const dataQueryEvents = this.state.options?.events || []; return { component: { component: { definition: { events: dataQueryEvents } } }, componentMeta: { events: { onDataQuerySuccess: { displayName: 'Query Success' }, onDataQueryFailure: { displayName: 'Query Failure' }, }, }, }; }; eventsChanged = (events) => { this.optionchanged('events', events); }; render() { const { dataSources, selectedDataSource, mode, options, currentTab, isUpdating, isCreating, addingQuery, editingQuery, selectedQuery, currentState, queryName, previewLoading, queryPreviewData, dataSourceMeta, } = 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; const mockDataQueryComponent = this.mockDataQueryAsComponent(); return (
{(addingQuery || editingQuery) && (
)}
{(addingQuery || editingQuery) && selectedDataSource && (
this.setState({ queryName: e.target.value })} className="form-control-plaintext form-control-plaintext-sm mt-1" value={queryName} style={{ width: '160px' }} autoFocus={false} />
)}
{(addingQuery || editingQuery) && ( { const _options = { ...options }; const query = { data_source_id: selectedDataSource.id === 'null' ? null : selectedDataSource.id, options: _options, kind: selectedDataSource.kind, }; previewQuery(this, query) .then(() => { this.previewPanelRef.current.scrollIntoView(); }) .catch(({ error, data }) => { console.log(error, data); }); }} className={`btn btn-secondary m-1 float-right1 ${previewLoading ? ' btn-loading' : ''}`} > Preview )} {(addingQuery || editingQuery) && ( )}
{(addingQuery || editingQuery) && (
{currentTab === 1 && (
{dataSources && mode === 'create' && (
{ 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" />
)} {selectedDataSource && (
{!dataSourceMeta?.disableTransformations && (

)}
Preview
{previewLoading && (
)} {previewLoading === false && (
)}
)}
)} {currentTab === 2 && (
{this.state.options.showSuccessNotification && (
this.optionchanged('successMessage', value)} placeholder="Query ran successfully" />
this.optionchanged('notificationDuration', e.target.value)} placeholder={5} className="form-control" value={this.state.options.notificationDuration} />
)}
Events
)}
)}
); } }; QueryManager = React.memo(QueryManager); export { QueryManager };