import React from 'react'; import { dataqueryService } from '@/_services'; import { toast } from 'react-hot-toast'; import ReactTooltip from 'react-tooltip'; import { allSources, source } from './QueryEditors'; import { Transformation } from './Transformation'; import { previewQuery, getSvgIcon } from '@/_helpers/appUtils'; import { EventManager } from '../Inspector/EventManager'; import { CodeHinter } from '../CodeBuilder/CodeHinter'; import { DataSourceTypes } from '../DataSourceManager/SourceComponents'; import RunjsIcon from '../Icons/runjs.svg'; import Preview from './Preview'; import DataSourceLister from './DataSourceLister'; import _, { isEmpty, isEqual } from 'lodash'; import { Button, ButtonGroup, Dropdown } from 'react-bootstrap'; // eslint-disable-next-line import/no-unresolved import { withTranslation } from 'react-i18next'; import cx from 'classnames'; 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' }, ]; class QueryManagerComponent extends React.Component { constructor(props) { super(props); this.state = { options: {}, selectedQuery: null, selectedDataSource: null, dataSourceMeta: {}, dataQueries: [], theme: {}, isSourceSelected: false, isFieldsChanged: false, paneHeightChanged: false, showSaveConfirmation: false, restArrayValuesChanged: false, nextProps: null, buttonText: '', }; this.previewPanelRef = React.createRef(); this.queryManagerPreferences = JSON.parse(localStorage.getItem('queryManagerPreferences')); if (localStorage.getItem('queryManagerButtonConfig') === null) { this.buttonConfig = this.queryManagerPreferences?.buttonConfig ?? {}; } else { this.buttonConfig = JSON.parse(localStorage.getItem('queryManagerButtonConfig')); localStorage.setItem( 'queryManagerPreferences', JSON.stringify({ ...this.queryManagerPreferences, buttonConfig: this.buttonConfig }) ); localStorage.removeItem('queryManagerButtonConfig'); } } setStateFromProps = (props) => { const selectedQuery = props.selectedQuery; const dataSourceId = selectedQuery?.data_source_id; const source = props.dataSources.find((datasource) => datasource.id === dataSourceId); let dataSourceMeta; if (selectedQuery?.pluginId) { dataSourceMeta = selectedQuery.manifestFile.data.source; } else { dataSourceMeta = DataSourceTypes.find((source) => source.kind === selectedQuery?.kind); } const paneHeightChanged = this.state.queryPaneHeight !== props.queryPaneHeight; const dataQueries = props.dataQueries?.length ? props.dataQueries : this.state.dataQueries; const queryPaneDragged = this.state.isQueryPaneDragging !== props.isQueryPaneDragging; this.setState( { appId: props.appId, dataSources: props.dataSources, dataQueries: dataQueries, appDefinition: props.appDefinition, mode: props.mode, currentTab: 1, addingQuery: props.addingQuery, editingQuery: props.editingQuery, queryPanelHeight: props.queryPanelHeight, isQueryPaneDragging: props.isQueryPaneDragging, currentState: props.currentState, selectedSource: source, options: props.options ?? {}, dataSourceMeta, paneHeightChanged, isSourceSelected: paneHeightChanged || queryPaneDragged ? this.state.isSourceSelected : props.isSourceSelected, selectedDataSource: paneHeightChanged || queryPaneDragged ? this.state.selectedDataSource : props.selectedDataSource, queryPreviewData: this.state.selectedQuery?.id !== props.selectedQuery?.id ? undefined : props.queryPreviewData, selectedQuery: props.mode === 'create' && selectedQuery, theme: { scheme: 'bright', author: 'chris kempson (http://chriskempson.com)', base00: props.darkMode ? '#272822' : '#000000', base01: '#303030', base02: '#505050', base03: '#b0b0b0', base04: '#d0d0d0', base05: '#e0e0e0', base06: '#f5f5f5', base07: '#ffffff', base08: '#fb0120', base09: '#fc6d24', base0A: '#fda331', base0B: '#a1c659', base0C: '#76c7b7', base0D: '#6fb3d2', base0E: '#d381c3', base0F: '#be643c', }, buttonText: props.mode === 'edit' ? this.buttonConfig?.editMode?.text ?? 'Save & Run' : this.buttonConfig?.createMode?.text ?? 'Create & Run', shouldRunQuery: props.mode === 'edit' ? this.buttonConfig?.editMode?.shouldRunQuery ?? true : this.buttonConfig?.createMode?.shouldRunQuery ?? true, }, () => { 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', id: 'null', name: 'REST API' }; } } if (selectedQuery.kind === 'runjs') { if (!selectedQuery.data_source_id) { source = { kind: 'runjs', id: 'runjs', name: 'Run JavaScript code' }; } } this.setState({ options: paneHeightChanged || this.state.selectedQuery?.id === selectedQuery?.id ? this.state.options : selectedQuery.options, selectedDataSource: source, selectedQuery, queryName: selectedQuery.name, }); } } ); }; componentWillReceiveProps(nextProps) { if (nextProps.loadingDataSources) return; // const themeModeChanged = this.props.darkMode !== nextProps.darkMode; // if (!nextProps.isQueryPaneDragging && !this.state.paneHeightChanged && !themeModeChanged) { // if (this.props.mode === 'create' && this.state.isFieldsChanged) { // this.setState({ showSaveConfirmation: true, nextProps }); // return; // } else if (this.props.mode === 'edit') { // if (this.state.selectedQuery) { // const isQueryChanged = !_.isEqual( // this.removeRestKey(this.state.options), // this.removeRestKey(this.state.selectedQuery.options) // ); // if (this.state.isFieldsChanged && isQueryChanged) { // this.setState({ showSaveConfirmation: true, nextProps }); // return; // } else if ( // !isQueryChanged && // this.state.selectedQuery.kind === 'restapi' && // this.state.restArrayValuesChanged // ) { // this.setState({ showSaveConfirmation: true, nextProps }); // return; // } // } // } // } if (this.props.showQueryConfirmation && !nextProps.showQueryConfirmation) { if (this.state.isUpdating) { this.setState({ isUpdating: false, }); } if (this.state.isCreating) { this.setState({ isCreating: false, }); } } if (!isEmpty(this.state.updatedQuery)) { const query = nextProps.dataQueries.find((q) => q.id === this.state.updatedQuery.id); if (query) { const isLoading = nextProps.currentState?.queries[query.name] ? nextProps.currentState?.queries[query.name]?.isLoading : false; const prevLoading = this.state.currentState?.queries[query.name] ? this.state.currentState?.queries[query.name]?.isLoading : false; if (!isEmpty(nextProps.selectedQuery) && !isEqual(this.state.selectedQuery, nextProps.selectedQuery)) { if (query && !isLoading && !prevLoading) { this.props.runQuery(query.id, query.name); } } else if (!isLoading && prevLoading) { this.state.updatedQuery.updateQuery ? this.setState({ updatedQuery: {}, isUpdating: false }) : this.setState({ updatedQuery: {}, isCreating: false }); } } } this.setStateFromProps(nextProps); } removeRestKey = (options) => { options?.arrayValuesChanged && delete options.arrayValuesChanged; return options; }; handleBackButton = () => { this.setState({ isSourceSelected: true, options: {}, queryPreviewData: undefined, }); }; 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, mode, selectedQuery } = this.state; const { dataQueries } = this.props; 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.props; 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, shouldRunQuery } = this.state; const appVersionId = this.props.editingVersionId; const kind = selectedDataSource.kind; const dataSourceId = selectedDataSource.id === 'null' ? null : selectedDataSource.id; const pluginId = selectedDataSource.plugin_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((data) => { this.setState({ isUpdating: shouldRunQuery ? true : false, isFieldsChanged: false, restArrayValuesChanged: false, updatedQuery: shouldRunQuery ? { ...data, updateQuery: true } : {}, }); this.props.dataQueriesChanged(); this.props.setStateOfUnsavedQueries(false); localStorage.removeItem('transformation'); }) .catch(({ error }) => { this.setState({ isUpdating: false, isFieldsChanged: false, restArrayValuesChanged: false }); this.props.setStateOfUnsavedQueries(false); toast.error(error); }); } else { this.setState({ isCreating: true }); dataqueryService .create(appId, appVersionId, queryName, kind, options, dataSourceId, pluginId) .then((data) => { toast.success('Query Added'); this.setState({ isCreating: shouldRunQuery ? true : false, isFieldsChanged: false, restArrayValuesChanged: false, updatedQuery: shouldRunQuery ? { ...data, updateQuery: false } : {}, }); this.props.dataQueriesChanged(); this.props.setStateOfUnsavedQueries(false); }) .catch(({ error }) => { this.setState({ isCreating: false, isFieldsChanged: false, restArrayValuesChanged: false }); this.props.setStateOfUnsavedQueries(false); toast.error(error); }); } }; validateNewOptions = (newOptions) => { const headersChanged = newOptions.arrayValuesChanged ?? false; let isFieldsChanged = false; if (this.state.selectedQuery) { const isQueryChanged = !_.isEqual( this.removeRestKey(newOptions), this.removeRestKey(this.state.selectedQuery.options) ); if (isQueryChanged) { isFieldsChanged = true; } else if (this.state.selectedQuery.kind === 'restapi' && headersChanged) { isFieldsChanged = true; } } else if (this.props.mode === 'create') { isFieldsChanged = true; } if (isFieldsChanged) this.props.setStateOfUnsavedQueries(true); this.setState({ options: { ...this.state.options, ...newOptions }, isFieldsChanged, restArrayValuesChanged: headersChanged, }); }; optionchanged = (option, value) => { const newOptions = { ...this.state.options, [option]: value }; this.validateNewOptions(newOptions); }; optionsChanged = (newOptions) => { this.validateNewOptions(newOptions); }; toggleOption = (option) => { const currentValue = this.state.options[option] ? this.state.options[option] : false; this.optionchanged(option, !currentValue); }; // 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); }; updateButtonText = (text, shouldRunQuery) => { if (this.state.mode === 'edit') { this.buttonConfig = { ...this.buttonConfig, editMode: { text: text, shouldRunQuery: shouldRunQuery } }; localStorage.setItem( 'queryManagerPreferences', JSON.stringify({ ...this.queryManagerPreferences, buttonConfig: this.buttonConfig }) ); } else { this.buttonConfig = { ...this.buttonConfig, createMode: { text: text, shouldRunQuery: shouldRunQuery } }; localStorage.setItem( 'queryManagerPreferences', JSON.stringify({ ...this.queryManagerPreferences, buttonConfig: this.buttonConfig }) ); } this.setState({ buttonText: text, shouldRunQuery: shouldRunQuery }); }; 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] || source; } let dropDownButtonText = mode === 'edit' ? 'Save' : 'Create'; const buttonDisabled = isUpdating || isCreating; const mockDataQueryComponent = this.mockDataQueryAsComponent(); const iconFile = this?.state?.selectedDataSource?.plugin?.icon_file?.data ?? undefined; const Icon = () => getSvgIcon(this?.state?.selectedDataSource?.kind, 18, 18, iconFile, { marginLeft: 7 }); return (
{/* this.createOrUpdateDataQuery()} onCancel={() => { this.setState({ showSaveConfirmation: false, isFieldsChanged: false }); this.setStateFromProps(this.state.nextProps); this.props.setStateOfUnsavedQueries(false); }} queryConfirmationData={this.state.queryConfirmationData} /> */}
{(addingQuery || editingQuery) && selectedDataSource && ( )}
{(addingQuery || editingQuery) && selectedDataSource && (
this.setState({ queryName: e.target.value })} className="form-control-plaintext form-control-plaintext-sm mt-1" value={queryName} autoFocus={false} data-cy={'query-label-input-field'} />
)}
{selectedDataSource && (addingQuery || editingQuery) && ( )} {selectedDataSource && (addingQuery || editingQuery) && ( { this.updateButtonText(dropDownButtonText, false); }} data-cy={`query-${String(dropDownButtonText).toLocaleLowerCase()}-option`} > {this.props.t(`editor.queryManager.${dropDownButtonText}`, dropDownButtonText)} { this.updateButtonText(`${dropDownButtonText} & Run`, true); }} data-cy={`query-${String(dropDownButtonText).toLocaleLowerCase()}-and-run-option`} > {this.props.t(`editor.queryManager.${dropDownButtonText} & Run`, `${dropDownButtonText} & Run`)} )}
{(addingQuery || editingQuery) && (
{currentTab === 1 && (
{dataSources && mode === 'create' && (
{this.state.selectedDataSource !== null && (

{ this.setState({ isSourceSelected: false, selectedDataSource: null, options: {}, }); }} style={{ marginTop: '-7px' }} >

)} {!this.state.isSourceSelected && ( )}{' '} {this?.state?.selectedDataSource?.kind && (
{this.state?.selectedDataSource?.kind === 'runjs' ? ( ) : ( )}

{' '} {this.state?.selectedDataSource?.kind && this.state.selectedDataSource.kind}

{' '}
)}
{!this.state.isSourceSelected && ( )}
)} {selectedDataSource && (
{!dataSourceMeta?.disableTransformations && selectedDataSource?.kind != 'runjs' && (
)}
)}
)} {currentTab === 2 && (
this.toggleOption('runOnPageLoad')} checked={this.state.options.runOnPageLoad} data-cy={'toggle-run-query-on-page-load'} /> {this.props.t('editor.queryManager.runQueryOnPageLoad', 'Run query on application load?')}
this.toggleOption('requestConfirmation')} checked={this.state.options.requestConfirmation} data-cy={'toggle-request-confirmation-on-run'} /> {this.props.t( 'editor.queryManager.confirmBeforeQueryRun', 'Request confirmation before running query?' )}
this.toggleOption('showSuccessNotification')} checked={this.state.options.showSuccessNotification} data-cy={'toggle-show-notification'} /> {this.props.t('editor.queryManager.notificationOnSuccess', 'Show notification on success?')}
{this.state.options.showSuccessNotification && (
this.optionchanged('successMessage', value)} placeholder={this.props.t( 'editor.queryManager.queryRanSuccessfully', 'Query ran successfully' )} cyLabel={'success-message'} />
this.optionchanged('notificationDuration', e.target.value)} placeholder={5} className="form-control" value={this.state.options.notificationDuration} data-cy={'notification-duration-input-field'} />
)}
{this.props.t('editor.queryManager.events', 'Events')}
({ ...page, id })) : [] } />
)}
)}
); } } export const QueryManager = withTranslation()(React.memo(QueryManagerComponent));