2024-10-22 08:01:21 +00:00
|
|
|
import React from 'react';
|
|
|
|
|
import Skeleton from 'react-loading-skeleton';
|
|
|
|
|
import { datasourceService, pluginsService, globalDatasourceService, libraryAppService } from '@/_services';
|
|
|
|
|
import cx from 'classnames';
|
|
|
|
|
import { Modal, Button, Tab, Row, Col, ListGroup, ModalBody } from 'react-bootstrap';
|
|
|
|
|
import { toast } from 'react-hot-toast';
|
|
|
|
|
import { getSvgIcon } from '@/_helpers/appUtils';
|
|
|
|
|
import { TestConnection } from './TestConnection';
|
2025-02-25 06:52:50 +00:00
|
|
|
import { getWorkspaceId, deepEqual, returnDevelopmentEnv, decodeEntities } from '@/_helpers/utils';
|
2024-10-22 08:01:21 +00:00
|
|
|
import { getSubpath } from '@/_helpers/routes';
|
|
|
|
|
import {
|
|
|
|
|
DataBaseSources,
|
|
|
|
|
ApiSources,
|
|
|
|
|
DataSourceTypes,
|
|
|
|
|
SourceComponent,
|
|
|
|
|
SourceComponents,
|
|
|
|
|
CloudStorageSources,
|
2025-02-25 06:52:50 +00:00
|
|
|
} from '../../../common/components/DataSourceComponents';
|
2024-10-22 08:01:21 +00:00
|
|
|
import { CopyToClipboard } from 'react-copy-to-clipboard';
|
|
|
|
|
import config from 'config';
|
|
|
|
|
import { capitalize, isEmpty } from 'lodash';
|
|
|
|
|
import { Card } from '@/_ui/Card';
|
|
|
|
|
import { withTranslation, useTranslation } from 'react-i18next';
|
2025-05-07 07:00:49 +00:00
|
|
|
import { camelizeKeys, decamelizeKeys, decamelize } from 'humps';
|
2024-10-22 08:01:21 +00:00
|
|
|
import { ButtonSolid } from '@/_ui/AppButton/AppButton';
|
|
|
|
|
import SolidIcon from '@/_ui/Icon/SolidIcons';
|
|
|
|
|
import { useAppVersionStore } from '@/_stores/appVersionStore';
|
|
|
|
|
import { ConfirmDialog, ToolTip } from '@/_components';
|
|
|
|
|
import { shallow } from 'zustand/shallow';
|
2025-02-25 06:52:50 +00:00
|
|
|
import { useDataSourcesStore } from '@/_stores/dataSourcesStore';
|
2024-10-22 08:01:21 +00:00
|
|
|
import { withRouter } from '@/_hoc/withRouter';
|
|
|
|
|
import useGlobalDatasourceUnsavedChanges from '@/_hooks/useGlobalDatasourceUnsavedChanges';
|
|
|
|
|
import { LicenseTooltip } from '@/LicenseTooltip';
|
|
|
|
|
import { DATA_SOURCE_TYPE } from '@/_helpers/constants';
|
|
|
|
|
import './dataSourceManager.theme.scss';
|
2025-02-25 06:52:50 +00:00
|
|
|
import { canUpdateDataSource } from '@/_helpers';
|
2025-04-03 08:17:49 +00:00
|
|
|
import DataSourceSchemaManager from '@/_helpers/dataSourceSchemaManager';
|
2025-02-25 06:52:50 +00:00
|
|
|
import MultiEnvTabs from './MultiEnvTabs';
|
2025-05-09 04:48:07 +00:00
|
|
|
import { generateCypressDataCy } from '../../../common/helpers/cypressHelpers';
|
2025-07-10 13:23:54 +00:00
|
|
|
import posthogHelper from '@/modules/common/helpers/posthogHelper';
|
2024-10-22 08:01:21 +00:00
|
|
|
|
|
|
|
|
class DataSourceManagerComponent extends React.Component {
|
|
|
|
|
constructor(props) {
|
|
|
|
|
super(props);
|
|
|
|
|
|
|
|
|
|
let selectedDataSource = null;
|
|
|
|
|
let dataSourceSchema = null;
|
|
|
|
|
let selectedDataSourceIcon = null;
|
|
|
|
|
let options = {};
|
|
|
|
|
let dataSourceMeta = {};
|
|
|
|
|
let datasourceName = '';
|
|
|
|
|
if (props.selectedDataSource) {
|
|
|
|
|
selectedDataSource = props.selectedDataSource;
|
|
|
|
|
options = selectedDataSource.options;
|
|
|
|
|
dataSourceMeta = this.getDataSourceMeta(selectedDataSource);
|
|
|
|
|
dataSourceSchema = props.selectedDataSource?.plugin?.manifestFile?.data;
|
|
|
|
|
selectedDataSourceIcon = props.selectDataSource?.plugin?.iconFile.data;
|
|
|
|
|
datasourceName = props.selectedDataSource?.name;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
this.state = {
|
|
|
|
|
showModal: true,
|
|
|
|
|
appId: props.appId,
|
|
|
|
|
selectedDataSource,
|
|
|
|
|
dataSourceSchema,
|
|
|
|
|
selectedDataSourceIcon,
|
|
|
|
|
options,
|
|
|
|
|
dataSourceMeta,
|
|
|
|
|
isSaving: false,
|
|
|
|
|
isCopied: false,
|
|
|
|
|
queryString: null,
|
|
|
|
|
plugins: [],
|
|
|
|
|
filteredDatasources: [],
|
|
|
|
|
activeDatasourceList: '#alldatasources',
|
|
|
|
|
suggestingDatasources: false,
|
|
|
|
|
scope: props?.scope,
|
|
|
|
|
modalProps: props?.modalProps ?? {},
|
|
|
|
|
showBackButton: props?.showBackButton ?? true,
|
|
|
|
|
defaultOptions: {},
|
|
|
|
|
pluginsLoaded: false,
|
2025-07-14 11:22:44 +00:00
|
|
|
dataSourceConfirmModalProps: { isOpen: false, dataSource: null, category: null },
|
2024-10-22 08:01:21 +00:00
|
|
|
addingDataSource: false,
|
|
|
|
|
createdDataSource: null,
|
|
|
|
|
unsavedChangesModal: false,
|
|
|
|
|
datasourceName,
|
|
|
|
|
creatingApp: false,
|
2025-04-03 08:17:49 +00:00
|
|
|
validationError: [],
|
|
|
|
|
validationMessages: {},
|
2025-04-28 04:53:25 +00:00
|
|
|
showValidationErrors: false,
|
2024-10-22 08:01:21 +00:00
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
componentDidMount() {
|
|
|
|
|
this.setState({
|
|
|
|
|
appId: this.props.appId,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
pluginsService
|
|
|
|
|
.findAll()
|
|
|
|
|
.then(({ data = [] }) => {
|
|
|
|
|
this.setState({ plugins: data, pluginsLoaded: true });
|
|
|
|
|
})
|
|
|
|
|
.catch((error) => {
|
|
|
|
|
this.setState({ pluginsLoaded: true });
|
|
|
|
|
toast.error(error?.message || 'failed to fetch plugins');
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
componentDidUpdate(prevProps) {
|
|
|
|
|
this.props.setGlobalDataSourceStatus({ saveAction: this.createDataSource });
|
|
|
|
|
if (prevProps.selectedDataSource !== this.props.selectedDataSource) {
|
|
|
|
|
let dataSourceMeta = this.getDataSourceMeta(this.props.selectedDataSource);
|
|
|
|
|
this.setState({
|
|
|
|
|
selectedDataSource: this.props.selectedDataSource,
|
|
|
|
|
options: this.props.selectedDataSource?.options,
|
|
|
|
|
dataSourceMeta,
|
|
|
|
|
dataSourceSchema: this.props.selectedDataSource?.plugin?.manifestFile?.data,
|
|
|
|
|
selectedDataSourceIcon: this.props.selectedDataSource?.plugin?.iconFile?.data,
|
|
|
|
|
connectionTestError: null,
|
|
|
|
|
datasourceName: this.props.selectedDataSource?.name,
|
2025-07-07 12:20:50 +00:00
|
|
|
validationMessages: {},
|
|
|
|
|
validationError: [],
|
|
|
|
|
showValidationErrors: false,
|
2024-10-22 08:01:21 +00:00
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
getDataSourceMeta = (dataSource) => {
|
|
|
|
|
if (!dataSource) return {};
|
|
|
|
|
|
|
|
|
|
if (dataSource?.pluginId) {
|
|
|
|
|
let dataSourceMeta = camelizeKeys(dataSource?.plugin?.manifestFile?.data.source);
|
|
|
|
|
dataSourceMeta.options = decamelizeKeys(dataSourceMeta.options);
|
|
|
|
|
|
|
|
|
|
return dataSourceMeta;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return DataSourceTypes.find((source) => source?.kind === dataSource?.kind);
|
|
|
|
|
};
|
|
|
|
|
|
2025-07-14 11:22:44 +00:00
|
|
|
selectDataSource = (source, category) => {
|
2025-07-10 13:23:54 +00:00
|
|
|
posthogHelper.captureEvent('choose_datasource', {
|
2025-07-14 11:22:44 +00:00
|
|
|
dataSource: source?.kind,
|
|
|
|
|
category,
|
2025-07-10 13:23:54 +00:00
|
|
|
appId: this.state.appId,
|
|
|
|
|
});
|
2024-10-22 08:01:21 +00:00
|
|
|
this.hideModal();
|
|
|
|
|
this.setState(
|
|
|
|
|
{
|
|
|
|
|
dataSourceMeta: source.manifestFile?.data?.source ?? source,
|
|
|
|
|
selectedDataSource: source.manifestFile?.data?.source ?? source,
|
|
|
|
|
options: source?.defaults ?? source?.options,
|
|
|
|
|
selectedDataSourceIcon: source.iconFile?.data,
|
|
|
|
|
name: source.manifestFile?.data?.source?.kind ?? source.kind,
|
|
|
|
|
dataSourceSchema: source.manifestFile?.data,
|
|
|
|
|
selectedDataSourcePluginId: source.id,
|
|
|
|
|
datasourceName: source.name,
|
2025-07-07 12:20:50 +00:00
|
|
|
validationMessages: {},
|
|
|
|
|
validationError: [],
|
|
|
|
|
showValidationErrors: false,
|
2024-10-22 08:01:21 +00:00
|
|
|
},
|
|
|
|
|
() => this.createDataSource()
|
|
|
|
|
);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
onNameChanged = (newName) => {
|
|
|
|
|
this.setState({
|
|
|
|
|
selectedDataSource: {
|
|
|
|
|
...this.state.selectedDataSource,
|
|
|
|
|
name: newName,
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
onExit = () => {
|
2025-02-25 06:52:50 +00:00
|
|
|
!this.state.selectedDataSource?.id && this.props.environmentChanged(returnDevelopmentEnv(this.props.environments));
|
2024-10-22 08:01:21 +00:00
|
|
|
this.setState({
|
|
|
|
|
dataSourceMeta: {},
|
|
|
|
|
selectedDataSource: null,
|
|
|
|
|
options: {},
|
|
|
|
|
connectionTestError: null,
|
|
|
|
|
queryString: null,
|
|
|
|
|
filteredDatasources: [],
|
|
|
|
|
activeDatasourceList: '#alldatasources',
|
|
|
|
|
});
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
setStateAsync = (state) => {
|
|
|
|
|
return new Promise((resolve) => {
|
|
|
|
|
this.setState(state, resolve);
|
|
|
|
|
});
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
optionchanged = (option, value) => {
|
|
|
|
|
const stateToUpdate = {
|
|
|
|
|
connectionTestError: null,
|
|
|
|
|
options: {
|
|
|
|
|
...this.state.options,
|
|
|
|
|
[option]: { value: value },
|
|
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
return this.setStateAsync(stateToUpdate);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
resetOptions = () => {
|
|
|
|
|
return this.setStateAsync({
|
|
|
|
|
connectionTestError: null,
|
|
|
|
|
options: this.state.defaultOptions,
|
|
|
|
|
});
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
hideModal = (ds = null) => {
|
|
|
|
|
this.onExit();
|
|
|
|
|
this.props.hideModal(ds);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
resetDataSourceConfirmModal = () => {
|
|
|
|
|
this.setState({
|
|
|
|
|
dataSourceConfirmModalProps: {
|
|
|
|
|
isOpen: false,
|
|
|
|
|
dataSource: null,
|
2025-07-14 11:22:44 +00:00
|
|
|
category: null,
|
2024-10-22 08:01:21 +00:00
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
createDataSource = () => {
|
2025-04-03 08:17:49 +00:00
|
|
|
const {
|
|
|
|
|
appId,
|
|
|
|
|
options,
|
|
|
|
|
selectedDataSource,
|
|
|
|
|
selectedDataSourcePluginId,
|
|
|
|
|
dataSourceMeta,
|
|
|
|
|
dataSourceSchema,
|
|
|
|
|
validationMessages,
|
2025-04-28 04:53:25 +00:00
|
|
|
validationError,
|
2025-04-03 08:17:49 +00:00
|
|
|
} = this.state;
|
|
|
|
|
|
|
|
|
|
if (!isEmpty(validationMessages)) {
|
|
|
|
|
const validationMessageArray = Object.values(validationMessages);
|
2025-04-28 04:53:25 +00:00
|
|
|
this.setState({ validationError: validationMessageArray, showValidationErrors: true });
|
|
|
|
|
|
2025-04-03 08:17:49 +00:00
|
|
|
toast.error(
|
|
|
|
|
this.props.t(
|
|
|
|
|
'editor.queryManager.dataSourceManager.toast.error.validationFailed',
|
|
|
|
|
'Validation failed. Please check your inputs.'
|
|
|
|
|
),
|
|
|
|
|
{ position: 'top-center' }
|
|
|
|
|
);
|
|
|
|
|
if (validationMessageArray.length > 0) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-08-21 13:24:05 +00:00
|
|
|
const OAuthDs = ['slack', 'zendesk', 'googlesheets', 'salesforce', 'googlecalendar', 'microsoft_graph', 'hubspot'];
|
2024-10-22 08:01:21 +00:00
|
|
|
const name = selectedDataSource.name;
|
|
|
|
|
const kind = selectedDataSource?.kind;
|
|
|
|
|
const pluginId = selectedDataSourcePluginId;
|
|
|
|
|
const appVersionId = useAppVersionStore?.getState()?.editingVersion?.id;
|
|
|
|
|
const currentAppEnvironmentId = this.props.currentAppEnvironmentId ?? this.props.currentEnvironment?.id;
|
|
|
|
|
const scope = this.state?.scope || selectedDataSource?.scope;
|
|
|
|
|
|
2025-07-10 13:23:54 +00:00
|
|
|
posthogHelper.captureEvent('save_connection_datasource', { dataSource: kind, appId }); //posthog event
|
|
|
|
|
|
2024-10-22 08:01:21 +00:00
|
|
|
const parsedOptions = Object?.keys(options)?.map((key) => {
|
2025-05-07 07:00:49 +00:00
|
|
|
let keyMeta = dataSourceMeta.options[key];
|
|
|
|
|
let isEncrypted = false;
|
|
|
|
|
if (keyMeta) {
|
|
|
|
|
isEncrypted = keyMeta.encrypted;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// to resolve any casing mis-match
|
|
|
|
|
if (decamelize(key) !== key) {
|
|
|
|
|
const newKey = decamelize(key);
|
2025-05-28 18:36:11 +00:00
|
|
|
isEncrypted = dataSourceMeta.options[newKey]?.encrypted;
|
2025-05-07 07:00:49 +00:00
|
|
|
}
|
|
|
|
|
|
2024-10-22 08:01:21 +00:00
|
|
|
return {
|
|
|
|
|
key: key,
|
|
|
|
|
value: options[key].value,
|
2025-05-07 07:00:49 +00:00
|
|
|
encrypted: isEncrypted,
|
2024-10-22 08:01:21 +00:00
|
|
|
...(!options[key]?.value && { credential_id: options[key]?.credential_id }),
|
|
|
|
|
};
|
|
|
|
|
});
|
2025-05-07 07:00:49 +00:00
|
|
|
|
2024-10-22 08:01:21 +00:00
|
|
|
if (OAuthDs.includes(kind)) {
|
|
|
|
|
const value = localStorage.getItem('OAuthCode');
|
|
|
|
|
parsedOptions.push({ key: 'code', value, encrypted: false });
|
|
|
|
|
}
|
2025-04-03 08:17:49 +00:00
|
|
|
|
2024-10-22 08:01:21 +00:00
|
|
|
if (name.trim() !== '') {
|
|
|
|
|
let service = scope === 'global' ? globalDatasourceService : datasourceService;
|
|
|
|
|
if (selectedDataSource.id) {
|
|
|
|
|
this.setState({ isSaving: true });
|
|
|
|
|
this.props.setGlobalDataSourceStatus({ isSaving: true, isEditing: false });
|
|
|
|
|
service
|
|
|
|
|
.save({
|
|
|
|
|
id: selectedDataSource.id,
|
|
|
|
|
name,
|
|
|
|
|
options: parsedOptions,
|
|
|
|
|
app_id: appId,
|
|
|
|
|
environment_id: currentAppEnvironmentId,
|
|
|
|
|
})
|
|
|
|
|
.then(() => {
|
|
|
|
|
this.props.updateSelectedDatasource && this.props.updateSelectedDatasource(selectedDataSource.name);
|
|
|
|
|
this.setState({ isSaving: false });
|
|
|
|
|
this.hideModal(selectedDataSource);
|
|
|
|
|
toast.success(
|
|
|
|
|
this.props.t('editor.queryManager.dataSourceManager.toast.success.dataSourceSaved', 'Data Source Saved'),
|
|
|
|
|
{ position: 'top-center' }
|
|
|
|
|
);
|
|
|
|
|
this.props.dataSourcesChanged(false, selectedDataSource);
|
|
|
|
|
this.props.globalDataSourcesChanged && this.props.globalDataSourcesChanged();
|
|
|
|
|
this.props.setGlobalDataSourceStatus({ isSaving: false, isEditing: false });
|
|
|
|
|
scope === 'local' && this.hideModal();
|
|
|
|
|
})
|
|
|
|
|
.catch(({ error }) => {
|
|
|
|
|
this.setState({ isSaving: false });
|
|
|
|
|
this.hideModal(selectedDataSource);
|
|
|
|
|
error && toast.error(error, { position: 'top-center' });
|
|
|
|
|
this.resetDataSourceConfirmModal();
|
|
|
|
|
this.props.setGlobalDataSourceStatus({ isSaving: false, isEditing: false });
|
|
|
|
|
});
|
|
|
|
|
} else {
|
|
|
|
|
this.setState({ isSaving: true, addingDataSource: true });
|
|
|
|
|
service
|
|
|
|
|
.create({
|
|
|
|
|
plugin_id: pluginId,
|
|
|
|
|
name,
|
|
|
|
|
kind,
|
|
|
|
|
options: parsedOptions,
|
|
|
|
|
app_id: appId,
|
|
|
|
|
app_version_id: appVersionId,
|
|
|
|
|
scope,
|
|
|
|
|
environment_id: currentAppEnvironmentId,
|
|
|
|
|
})
|
|
|
|
|
.then((data) => {
|
|
|
|
|
this.setState({ isSaving: false, addingDataSource: false });
|
|
|
|
|
this.props.updateSelectedDatasource && this.props.updateSelectedDatasource(name);
|
|
|
|
|
|
|
|
|
|
this.hideModal(selectedDataSource);
|
|
|
|
|
toast.success(
|
|
|
|
|
this.props.t('editor.queryManager.dataSourceManager.toast.success.dataSourceAdded', 'Data Source Added'),
|
|
|
|
|
{ position: 'top-center' }
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
this.props.dataSourcesChanged(false, data);
|
|
|
|
|
this.props.globalDataSourcesChanged && this.props.globalDataSourcesChanged();
|
|
|
|
|
this.resetDataSourceConfirmModal();
|
|
|
|
|
})
|
|
|
|
|
.catch(({ error }) => {
|
|
|
|
|
this.setState({ isSaving: false, addingDataSource: false });
|
|
|
|
|
this.hideModal();
|
|
|
|
|
error && toast.error(error, { position: 'top-center' });
|
|
|
|
|
this.resetDataSourceConfirmModal();
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
toast.error(
|
|
|
|
|
this.props.t(
|
|
|
|
|
'editor.queryManager.dataSourceManager.toast.error.noEmptyDsName',
|
|
|
|
|
'The name of datasource should not be empty'
|
|
|
|
|
),
|
|
|
|
|
{ position: 'top-center' }
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
handleSearch = (searchQuery, activeDatasourceList) => {
|
|
|
|
|
this.setState({ queryString: searchQuery });
|
|
|
|
|
|
|
|
|
|
const arr = [];
|
|
|
|
|
const filteredDatasources = this.datasourcesGroups().filter((group) => group.key === activeDatasourceList)[0].list;
|
|
|
|
|
|
|
|
|
|
filteredDatasources.forEach((datasource) => {
|
|
|
|
|
if (datasource.name.toLowerCase().includes(searchQuery.toLowerCase())) {
|
|
|
|
|
arr.push(datasource);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
this.setState({ filteredDatasources: arr });
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
handleBackToAllDatasources = () => {
|
|
|
|
|
this.setState({
|
|
|
|
|
queryString: null,
|
|
|
|
|
filteredDatasources: [],
|
|
|
|
|
activeDatasourceList: '#alldatasources',
|
|
|
|
|
});
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
updateSuggestedDatasources = () => {
|
|
|
|
|
this.setState({ suggestingDatasources: true, activeDatasourceList: '#' });
|
|
|
|
|
};
|
|
|
|
|
|
2025-04-28 06:31:41 +00:00
|
|
|
setValidationMessages = (errors, schema, interactedFields) => {
|
2025-04-03 08:17:49 +00:00
|
|
|
const errorMap = errors.reduce((acc, error) => {
|
|
|
|
|
// Get property name from either required error or dataPath
|
|
|
|
|
const property =
|
|
|
|
|
error.keyword === 'required'
|
|
|
|
|
? error.params.missingProperty
|
|
|
|
|
: error.dataPath?.replace(/^[./]/, '') || error.instancePath?.replace(/^[./]/, '');
|
|
|
|
|
|
|
|
|
|
if (property) {
|
|
|
|
|
const propertySchema = schema.properties?.[property];
|
|
|
|
|
const propertyTitle = propertySchema?.title;
|
|
|
|
|
acc[property] =
|
|
|
|
|
error.keyword === 'required' ? `${propertyTitle} is required` : `${propertyTitle} ${error.message}`;
|
|
|
|
|
}
|
|
|
|
|
return acc;
|
|
|
|
|
}, {});
|
|
|
|
|
this.setState({ validationMessages: errorMap });
|
2025-04-28 06:31:41 +00:00
|
|
|
const filteredValidationBanner = interactedFields
|
|
|
|
|
? Object.keys(this.state.validationMessages)
|
|
|
|
|
.filter((key) => interactedFields.has(key))
|
|
|
|
|
.reduce((result, key) => {
|
|
|
|
|
result.push(this.state.validationMessages[key]);
|
|
|
|
|
return result;
|
|
|
|
|
}, [])
|
|
|
|
|
: Object.values(this.state.validationMessages);
|
|
|
|
|
this.setState({ validationError: filteredValidationBanner });
|
2025-04-03 08:17:49 +00:00
|
|
|
};
|
|
|
|
|
|
2024-10-22 08:01:21 +00:00
|
|
|
renderSourceComponent = (kind, isPlugin = false) => {
|
2025-04-28 04:53:25 +00:00
|
|
|
const { options, isSaving, showValidationErrors } = this.state;
|
2024-10-22 08:01:21 +00:00
|
|
|
|
|
|
|
|
const sourceComponentName = kind?.charAt(0).toUpperCase() + kind?.slice(1);
|
|
|
|
|
const ComponentToRender = isPlugin ? SourceComponent : SourceComponents[sourceComponentName] || SourceComponent;
|
|
|
|
|
return (
|
|
|
|
|
<ComponentToRender
|
2025-07-07 12:20:50 +00:00
|
|
|
key={this.state.selectedDataSource?.id}
|
2024-10-22 08:01:21 +00:00
|
|
|
dataSourceSchema={this.state.dataSourceSchema}
|
|
|
|
|
optionsChanged={(options = {}) => this.setState({ options })}
|
|
|
|
|
optionchanged={this.optionchanged}
|
|
|
|
|
createDataSource={this.createDataSource}
|
|
|
|
|
options={options}
|
|
|
|
|
isSaving={isSaving}
|
|
|
|
|
hideModal={this.hideModal}
|
|
|
|
|
selectedDataSource={this.state.selectedDataSource}
|
|
|
|
|
isEditMode={!isEmpty(this.state.selectedDataSource)}
|
|
|
|
|
currentAppEnvironmentId={this.props.currentEnvironment?.id}
|
2025-04-03 08:17:49 +00:00
|
|
|
validationMessages={this.state.validationMessages}
|
|
|
|
|
setValidationMessages={this.setValidationMessages}
|
|
|
|
|
clearValidationMessages={() => this.setState({ validationMessages: {} })}
|
2024-10-22 08:01:21 +00:00
|
|
|
setDefaultOptions={this.setDefaultOptions}
|
2025-04-28 04:53:25 +00:00
|
|
|
showValidationErrors={showValidationErrors}
|
|
|
|
|
clearValidationErrorBanner={() => this.setState({ validationError: [] })}
|
2024-10-22 08:01:21 +00:00
|
|
|
/>
|
|
|
|
|
);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
setDefaultOptions = (defaults) => {
|
|
|
|
|
this.setState({
|
|
|
|
|
defaultOptions: defaults,
|
|
|
|
|
});
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
onConnectionTestFailed = (data) => {
|
|
|
|
|
this.setState({ connectionTestError: data });
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
segregateDataSources = (suggestingDatasources, darkMode) => {
|
|
|
|
|
const datasources = this.datasourcesGroups();
|
|
|
|
|
|
|
|
|
|
const handleOnSelect = (activekey) => {
|
|
|
|
|
if (suggestingDatasources) {
|
|
|
|
|
this.setState({ suggestingDatasources: false });
|
|
|
|
|
}
|
|
|
|
|
this.setState({ activeDatasourceList: activekey });
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const goBacktoAllDatasources = () => {
|
|
|
|
|
this.setState({ suggestingDatasources: false });
|
|
|
|
|
this.handleBackToAllDatasources();
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const datasourceSuggestionUI = () => {
|
|
|
|
|
return (
|
|
|
|
|
<div className="empty-state-wrapper suggestingDatasourcesWrapper">
|
|
|
|
|
<EmptyStateContainer
|
|
|
|
|
suggestionUI={true}
|
|
|
|
|
queryString={this.state.queryString}
|
|
|
|
|
handleBackToAllDatasources={goBacktoAllDatasources}
|
|
|
|
|
darkMode={this.props.darkMode}
|
|
|
|
|
placeholder={this.props.t(
|
|
|
|
|
'editor.queryManager.dataSourceManager.suggestAnIntegration',
|
|
|
|
|
'Suggest an integration'
|
|
|
|
|
)}
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<Tab.Container
|
|
|
|
|
activeKey={this.state.activeDatasourceList}
|
|
|
|
|
unmountOnExit={true}
|
|
|
|
|
onSelect={(activekey) => handleOnSelect(activekey)}
|
|
|
|
|
id="list-group-tabs-example"
|
|
|
|
|
defaultActiveKey={this.state.activeDatasourceList}
|
|
|
|
|
>
|
|
|
|
|
<Row>
|
|
|
|
|
<Col sm={6} md={4} className={`modal-sidebar ${darkMode ? 'dark' : ''}`}>
|
|
|
|
|
{this.renderSidebarList()}
|
|
|
|
|
</Col>
|
|
|
|
|
<Col style={{ left: '25%' }} className={`modal-body-content ${darkMode ? 'dark' : ''}`}>
|
|
|
|
|
<div className="selected-datasource-list-content">
|
|
|
|
|
<Tab.Content>
|
|
|
|
|
{suggestingDatasources ? (
|
|
|
|
|
<div className="suggestion-container">
|
|
|
|
|
<h4 className="justify-content-start">
|
|
|
|
|
{this.props.t('editor.queryManager.dataSourceManager.suggestDataSource', 'Suggest Datasource')}
|
|
|
|
|
</h4>
|
|
|
|
|
{datasourceSuggestionUI()}
|
|
|
|
|
</div>
|
|
|
|
|
) : (
|
|
|
|
|
<>
|
|
|
|
|
<div className="input-icon modal-searchbar">
|
|
|
|
|
<SearchBoxContainer
|
|
|
|
|
dataCy={'datasource-search-input'}
|
|
|
|
|
onChange={this.handleSearch}
|
|
|
|
|
onClear={this.handleBackToAllDatasources}
|
|
|
|
|
queryString={this.state.queryString}
|
|
|
|
|
activeDatasourceList={this.state.activeDatasourceList}
|
|
|
|
|
scope={this.state.scope}
|
|
|
|
|
className="tj-text"
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
{datasources.map((datasource) => (
|
|
|
|
|
<Tab.Pane
|
|
|
|
|
transition={false}
|
|
|
|
|
active={this.state.activeDatasourceList === datasource.key}
|
|
|
|
|
bsPrefix={`datasource-modal-${this.state.activeDatasourceList}`}
|
|
|
|
|
eventKey={datasource.key}
|
|
|
|
|
key={datasource.key}
|
|
|
|
|
>
|
|
|
|
|
{datasource.renderDatasources()}
|
|
|
|
|
</Tab.Pane>
|
|
|
|
|
))}
|
|
|
|
|
</>
|
|
|
|
|
)}
|
|
|
|
|
{!suggestingDatasources && this.state.queryString && this.state.filteredDatasources.length === 0 && (
|
|
|
|
|
<div className="empty-state-wrapper row">
|
|
|
|
|
<EmptyStateContainer
|
|
|
|
|
queryString={this.state.queryString}
|
|
|
|
|
handleBackToAllDatasources={this.handleBackToAllDatasources}
|
|
|
|
|
darkMode={this.props.darkMode}
|
|
|
|
|
placeholder={this.props.t(
|
|
|
|
|
'editor.queryManager.dataSourceManager.whatLookingFor',
|
|
|
|
|
'Tell us what you were looking for?'
|
|
|
|
|
)}
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</Tab.Content>
|
|
|
|
|
</div>
|
|
|
|
|
</Col>
|
|
|
|
|
</Row>
|
|
|
|
|
</Tab.Container>
|
|
|
|
|
);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
datasourcesGroups = () => {
|
|
|
|
|
const allDataSourcesList = {
|
|
|
|
|
databases: DataBaseSources,
|
|
|
|
|
apis: ApiSources,
|
|
|
|
|
cloudStorages: CloudStorageSources,
|
|
|
|
|
plugins: this.state.plugins,
|
|
|
|
|
filteredDatasources: this.state.filteredDatasources,
|
|
|
|
|
};
|
|
|
|
|
const dataSourceList = [
|
|
|
|
|
{
|
|
|
|
|
type: 'All Datasources',
|
|
|
|
|
key: '#alldatasources',
|
|
|
|
|
list: [
|
|
|
|
|
...allDataSourcesList.databases,
|
|
|
|
|
...allDataSourcesList.apis,
|
|
|
|
|
...allDataSourcesList.cloudStorages,
|
|
|
|
|
...allDataSourcesList.plugins,
|
|
|
|
|
],
|
|
|
|
|
renderDatasources: () => this.renderCardGroup(allDataSourcesList, 'All Datasources'),
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
type: 'Databases',
|
|
|
|
|
key: '#databases',
|
|
|
|
|
list: allDataSourcesList.databases,
|
|
|
|
|
renderDatasources: () => this.renderCardGroup(allDataSourcesList.databases, 'Databases'),
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
type: 'APIs',
|
|
|
|
|
key: '#apis',
|
|
|
|
|
list: allDataSourcesList.apis,
|
|
|
|
|
renderDatasources: () => this.renderCardGroup(allDataSourcesList.apis, 'APIs'),
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
type: 'Cloud Storage',
|
|
|
|
|
key: '#cloudstorage',
|
|
|
|
|
list: allDataSourcesList.cloudStorages,
|
|
|
|
|
renderDatasources: () => this.renderCardGroup(allDataSourcesList.cloudStorages, 'Cloud Storages'),
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
type: 'Plugins',
|
|
|
|
|
key: '#plugins',
|
|
|
|
|
list: allDataSourcesList.plugins,
|
|
|
|
|
renderDatasources: () => this.renderCardGroup(allDataSourcesList.plugins, 'Plugins'),
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
type: 'Filtered Datasources',
|
|
|
|
|
key: '#filtereddatasources',
|
|
|
|
|
list: allDataSourcesList.filteredDatasources,
|
|
|
|
|
renderDatasources: () => this.renderCardGroup(this.state.filteredDatasources, this.state.activeDatasourceList),
|
|
|
|
|
},
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
return dataSourceList;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
renderSidebarList = () => {
|
|
|
|
|
const dataSourceList = this.datasourcesGroups().splice(0, 5);
|
|
|
|
|
|
|
|
|
|
const updateSuggestionState = () => {
|
|
|
|
|
this.updateSuggestedDatasources();
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<>
|
|
|
|
|
<ListGroup className="datasource-lists-modal" variant="flush">
|
|
|
|
|
{dataSourceList.map((datasource) => (
|
|
|
|
|
<ListGroup.Item key={datasource.key} eventKey={datasource.key}>
|
|
|
|
|
{`${datasource.type} (${datasource.list.length})`}
|
|
|
|
|
</ListGroup.Item>
|
|
|
|
|
))}
|
|
|
|
|
</ListGroup>
|
|
|
|
|
<div className="datasource-modal-sidebar-footer">
|
|
|
|
|
<p>
|
|
|
|
|
<span className="footer-text">
|
|
|
|
|
{this.props.t(
|
|
|
|
|
'editor.queryManager.dataSourceManager.noResultFound',
|
|
|
|
|
`Don't see what you were looking for?`
|
|
|
|
|
)}
|
|
|
|
|
</span>
|
|
|
|
|
<br />
|
|
|
|
|
<span className="link-span" onClick={updateSuggestionState}>
|
|
|
|
|
{this.props.t('editor.queryManager.dataSourceManager.suggest', 'Suggest')}
|
|
|
|
|
</span>
|
|
|
|
|
</p>
|
|
|
|
|
</div>
|
|
|
|
|
</>
|
|
|
|
|
);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
createSampleApp = () => {
|
|
|
|
|
let _self = this;
|
|
|
|
|
_self.setState({ creatingApp: true });
|
|
|
|
|
libraryAppService
|
|
|
|
|
.createSampleApp()
|
|
|
|
|
.then((data) => {
|
|
|
|
|
const workspaceId = getWorkspaceId();
|
|
|
|
|
const subpath = getSubpath();
|
|
|
|
|
const path = subpath
|
|
|
|
|
? `${subpath}/${workspaceId}/apps/${data.app[0].id}`
|
|
|
|
|
: `/${workspaceId}/apps/${data.app[0].id}`;
|
|
|
|
|
window.open(path, '_blank');
|
|
|
|
|
toast.success('App created successfully!');
|
|
|
|
|
_self.setState({ creatingApp: false });
|
|
|
|
|
})
|
|
|
|
|
.catch((errorResponse) => {
|
|
|
|
|
_self.setState({ creatingApp: false });
|
|
|
|
|
const message = errorResponse?.error;
|
|
|
|
|
toast.error(message);
|
|
|
|
|
});
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
renderSampleDBModal = () => {
|
|
|
|
|
const { dataSourceMeta, selectedDataSourceIcon, creatingApp } = this.state;
|
|
|
|
|
return (
|
|
|
|
|
<div className="sample-db-modal-body">
|
|
|
|
|
<div className="row sample-db-title" data-cy="sample-db-postgres-title">
|
|
|
|
|
<div className="col-md-1">
|
|
|
|
|
{getSvgIcon(dataSourceMeta?.kind?.toLowerCase(), 35, 35, selectedDataSourceIcon)}
|
|
|
|
|
</div>
|
|
|
|
|
<div className="col-md-1">PostgreSQL</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div className={'sample-db-description'} data-cy="sample-db-description">
|
|
|
|
|
<p className={`p ${this.props.darkMode ? 'dark' : ''}`}>
|
|
|
|
|
This PostgreSQL data source is a shared resource and may show varying data
|
|
|
|
|
<br /> due to real-time updates. It's reset daily for some consistency, but please note <br />
|
|
|
|
|
it's designed for user exploration, not production use.
|
|
|
|
|
</p>
|
|
|
|
|
</div>
|
|
|
|
|
<div className="create-btn-cont">
|
|
|
|
|
<ButtonSolid
|
|
|
|
|
className={`create-app-btn`}
|
|
|
|
|
data-cy="create-sample-app-button"
|
|
|
|
|
isLoading={creatingApp}
|
|
|
|
|
// disabled={isSaving || this.props.isVersionReleased || isSaveDisabled}
|
|
|
|
|
variant="primary"
|
|
|
|
|
onClick={this.createSampleApp}
|
|
|
|
|
fill={this.props.darkMode && this.props.isVersionReleased ? '#4c5155' : '#FDFDFE'}
|
|
|
|
|
>
|
|
|
|
|
Create sample application
|
|
|
|
|
</ButtonSolid>
|
|
|
|
|
</div>
|
|
|
|
|
<div className="image-container">
|
|
|
|
|
<img
|
|
|
|
|
src="assets/images/Sample data source.png"
|
|
|
|
|
className="img-sample-db"
|
|
|
|
|
alt="Sample data source"
|
|
|
|
|
data-cy="sample-db-image"
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
renderCardGroup = (source, type) => {
|
|
|
|
|
const openDataSourceConfirmModal = (dataSource) =>
|
|
|
|
|
this.setState({
|
|
|
|
|
dataSourceConfirmModalProps: {
|
|
|
|
|
isOpen: true,
|
|
|
|
|
dataSource,
|
2025-07-14 11:22:44 +00:00
|
|
|
category: type,
|
2024-10-22 08:01:21 +00:00
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (this.state.queryString && this.state.queryString.length > 0) {
|
|
|
|
|
const filteredDatasources = this.state.filteredDatasources.map((datasource) => {
|
|
|
|
|
const src = datasource?.iconFile?.data
|
|
|
|
|
? `data:image/svg+xml;base64,${datasource.iconFile?.data}`
|
|
|
|
|
: datasource?.kind?.toLowerCase();
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
...datasource,
|
|
|
|
|
src,
|
|
|
|
|
title: datasource.name,
|
|
|
|
|
};
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<>
|
|
|
|
|
<div className="row row-deck mt-4 ">
|
|
|
|
|
<h4 className="mb-2">{type}</h4>
|
|
|
|
|
{filteredDatasources.map((item) => (
|
|
|
|
|
<Card
|
|
|
|
|
key={item.key}
|
|
|
|
|
title={item.title}
|
|
|
|
|
src={item.src}
|
|
|
|
|
handleClick={() => openDataSourceConfirmModal(item)}
|
|
|
|
|
usePluginIcon={isEmpty(item?.iconFile?.data)}
|
|
|
|
|
height="35px"
|
|
|
|
|
width="35px"
|
|
|
|
|
/>
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
</>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (type === 'All Datasources') {
|
|
|
|
|
const databases = source.databases.map((datasource) => {
|
|
|
|
|
return {
|
|
|
|
|
...datasource,
|
|
|
|
|
src: datasource?.kind?.toLowerCase(),
|
|
|
|
|
title: datasource.name,
|
|
|
|
|
};
|
|
|
|
|
});
|
|
|
|
|
const apis = source.apis.map((datasource) => {
|
|
|
|
|
return {
|
|
|
|
|
...datasource,
|
|
|
|
|
src: datasource?.kind?.toLowerCase(),
|
|
|
|
|
title: datasource.name,
|
|
|
|
|
};
|
|
|
|
|
});
|
|
|
|
|
const cloudStorages = source.cloudStorages.map((datasource) => {
|
|
|
|
|
return {
|
|
|
|
|
...datasource,
|
|
|
|
|
src: datasource?.kind?.toLowerCase(),
|
|
|
|
|
title: datasource.name,
|
|
|
|
|
};
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<>
|
|
|
|
|
<div>
|
|
|
|
|
<div className="row row-deck mt-4">
|
|
|
|
|
<h4 className="mb-2">{'Databases'}</h4>
|
|
|
|
|
{databases.map((item) => (
|
|
|
|
|
<Card
|
|
|
|
|
key={item.key}
|
|
|
|
|
title={item.title}
|
|
|
|
|
src={item.src}
|
|
|
|
|
handleClick={() => openDataSourceConfirmModal(item)}
|
|
|
|
|
usePluginIcon={true}
|
|
|
|
|
height="35px"
|
|
|
|
|
width="35px"
|
|
|
|
|
/>
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div>
|
|
|
|
|
<div className="row row-deck mt-4">
|
|
|
|
|
<h4 className="mb-2">{'APIs'}</h4>
|
|
|
|
|
{apis.map((item) => (
|
|
|
|
|
<Card
|
|
|
|
|
key={item.key}
|
|
|
|
|
title={item.title}
|
|
|
|
|
src={item.src}
|
|
|
|
|
handleClick={() => openDataSourceConfirmModal(item)}
|
|
|
|
|
usePluginIcon={true}
|
|
|
|
|
height="35px"
|
|
|
|
|
width="35px"
|
|
|
|
|
/>
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div>
|
|
|
|
|
<div className="row row-deck mt-4">
|
|
|
|
|
<h4 className="mb-2">{'Cloud Storages'}</h4>
|
|
|
|
|
{cloudStorages.map((item) => (
|
|
|
|
|
<Card
|
|
|
|
|
key={item.key}
|
|
|
|
|
title={item.title}
|
|
|
|
|
src={item.src}
|
|
|
|
|
handleClick={() => openDataSourceConfirmModal(item)}
|
|
|
|
|
usePluginIcon={true}
|
|
|
|
|
height="35px"
|
|
|
|
|
width="35px"
|
|
|
|
|
/>
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const datasources = source.map((datasource) => {
|
|
|
|
|
const src = datasource?.iconFile?.data
|
|
|
|
|
? `data:image/svg+xml;base64,${datasource.iconFile?.data}`
|
|
|
|
|
: datasource?.kind?.toLowerCase();
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
...datasource,
|
|
|
|
|
src,
|
|
|
|
|
title: datasource.name,
|
|
|
|
|
};
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<>
|
|
|
|
|
<div className="row row-deck mt-4">
|
|
|
|
|
<h4 className="mb-2">{type}</h4>
|
|
|
|
|
{datasources.map((item) => (
|
|
|
|
|
<Card
|
|
|
|
|
key={item.key}
|
|
|
|
|
title={item.title}
|
|
|
|
|
src={item?.src}
|
|
|
|
|
handleClick={() => openDataSourceConfirmModal(item)}
|
|
|
|
|
usePluginIcon={isEmpty(item?.iconFile?.data)}
|
|
|
|
|
height="35px"
|
|
|
|
|
width="35px"
|
|
|
|
|
/>
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
</>
|
|
|
|
|
);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
renderEnvironmentsTab = (selectedDataSource) => {
|
|
|
|
|
const multiEnvironmentEnabled = this.props?.featureAccess?.multiEnvironment;
|
|
|
|
|
const isTrial = this.props?.featureAccess?.licenseStatus?.licenseType === 'trial';
|
|
|
|
|
const licenseValid =
|
|
|
|
|
!this.props?.featureAccess?.licenseStatus?.isExpired && this.props?.featureAccess?.licenseStatus?.isLicenseValid;
|
|
|
|
|
return (
|
|
|
|
|
selectedDataSource &&
|
|
|
|
|
this.props.environments?.length > 1 && (
|
|
|
|
|
<nav className="nav nav-tabs mt-3">
|
|
|
|
|
{this.props?.environments.map((env, key) => {
|
|
|
|
|
const Wrapper = ({ children }) =>
|
|
|
|
|
!env?.enabled ? (
|
|
|
|
|
<LicenseTooltip
|
|
|
|
|
placement="bottom"
|
|
|
|
|
feature={'multi-environments'}
|
|
|
|
|
isAvailable={env?.enabled}
|
|
|
|
|
noTooltipIfValid={true}
|
|
|
|
|
customMessage={
|
|
|
|
|
!this.props?.featureAccess?.isLicenseValid || this.props?.featureAccess?.isExpired
|
|
|
|
|
? 'Multi-environments are available only in paid plans'
|
|
|
|
|
: 'Multi-environments are not included in your current plan'
|
|
|
|
|
}
|
|
|
|
|
>
|
|
|
|
|
{children}
|
|
|
|
|
</LicenseTooltip>
|
|
|
|
|
) : (
|
|
|
|
|
<>{children}</>
|
|
|
|
|
);
|
|
|
|
|
return (
|
|
|
|
|
<Wrapper key={key}>
|
|
|
|
|
<a
|
|
|
|
|
key={env?.id}
|
|
|
|
|
onClick={() =>
|
|
|
|
|
this.props.handleActions(() => {
|
|
|
|
|
if (env?.enabled) {
|
|
|
|
|
!selectedDataSource?.id && this.resetOptions();
|
|
|
|
|
this.props.environmentChanged(env, selectedDataSource?.id);
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
disabled={!env?.enabled}
|
|
|
|
|
className={cx('nav-item nav-link', { active: this.props.currentEnvironment?.name === env.name })}
|
|
|
|
|
data-cy={`${env.name}-label`}
|
|
|
|
|
>
|
|
|
|
|
<ToolTip
|
|
|
|
|
message={'Multi-environments is a paid plan feature'}
|
|
|
|
|
show={isTrial && licenseValid}
|
|
|
|
|
placement="bottom"
|
|
|
|
|
>
|
|
|
|
|
<div className="d-flex align-items-center">
|
|
|
|
|
{capitalize(env.name)}
|
|
|
|
|
{env.priority > 1 && (!multiEnvironmentEnabled || isTrial) && (
|
|
|
|
|
<SolidIcon className="mx-1" name="enterprisesmall" />
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
</ToolTip>
|
|
|
|
|
</a>
|
|
|
|
|
</Wrapper>
|
|
|
|
|
);
|
|
|
|
|
})}
|
|
|
|
|
</nav>
|
|
|
|
|
)
|
|
|
|
|
);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
render() {
|
|
|
|
|
const {
|
|
|
|
|
dataSourceMeta,
|
|
|
|
|
selectedDataSource,
|
|
|
|
|
selectedDataSourceIcon,
|
|
|
|
|
options,
|
|
|
|
|
isSaving,
|
|
|
|
|
connectionTestError,
|
|
|
|
|
isCopied,
|
|
|
|
|
dataSourceSchema,
|
|
|
|
|
pluginsLoaded,
|
|
|
|
|
dataSourceConfirmModalProps,
|
|
|
|
|
addingDataSource,
|
|
|
|
|
datasourceName,
|
2025-04-03 08:17:49 +00:00
|
|
|
validationError,
|
2025-04-28 04:53:25 +00:00
|
|
|
validationMessages,
|
2024-10-22 08:01:21 +00:00
|
|
|
} = this.state;
|
|
|
|
|
const isPlugin = dataSourceSchema ? true : false;
|
2025-07-14 11:22:44 +00:00
|
|
|
const createSelectedDataSource = (dataSource, category) => {
|
|
|
|
|
this.selectDataSource(dataSource, category);
|
2024-10-22 08:01:21 +00:00
|
|
|
};
|
|
|
|
|
const isSampleDb = selectedDataSource?.type === DATA_SOURCE_TYPE.SAMPLE;
|
|
|
|
|
const sampleDBmodalBodyStyle = isSampleDb ? { paddingBottom: '0px', borderBottom: '1px solid #E6E8EB' } : {};
|
|
|
|
|
const sampleDBmodalFooterStyle = isSampleDb ? { paddingTop: '8px' } : {};
|
|
|
|
|
const isSaveDisabled = selectedDataSource
|
2025-04-28 04:53:25 +00:00
|
|
|
? (deepEqual(options, selectedDataSource?.options, ['encrypted']) &&
|
|
|
|
|
selectedDataSource?.name === datasourceName) ||
|
|
|
|
|
!isEmpty(validationMessages)
|
2024-10-22 08:01:21 +00:00
|
|
|
: true;
|
|
|
|
|
this.props.setGlobalDataSourceStatus({ isEditing: !isSaveDisabled });
|
|
|
|
|
const docLink = isSampleDb
|
2025-06-25 07:10:07 +00:00
|
|
|
? 'https://docs.tooljet.ai/docs/data-sources/sample-data-sources'
|
2024-10-22 08:01:21 +00:00
|
|
|
: selectedDataSource?.pluginId && selectedDataSource.pluginId.trim() !== ''
|
2025-06-25 07:10:07 +00:00
|
|
|
? `https://docs.tooljet.ai/docs/marketplace/plugins/marketplace-plugin-${selectedDataSource?.kind}/`
|
|
|
|
|
: `https://docs.tooljet.ai/docs/data-sources/${selectedDataSource?.kind}`;
|
2025-08-11 13:29:49 +00:00
|
|
|
const OAuthDs = [
|
|
|
|
|
'slack',
|
|
|
|
|
'zendesk',
|
|
|
|
|
'googlesheets',
|
|
|
|
|
'salesforce',
|
|
|
|
|
'googlecalendar',
|
|
|
|
|
'snowflake',
|
|
|
|
|
'microsoft_graph',
|
2025-08-21 13:24:05 +00:00
|
|
|
'hubspot',
|
2025-08-11 13:29:49 +00:00
|
|
|
];
|
2024-10-22 08:01:21 +00:00
|
|
|
return (
|
|
|
|
|
pluginsLoaded && (
|
|
|
|
|
<div>
|
|
|
|
|
<Modal
|
|
|
|
|
show={this.props.showDataSourceManagerModal}
|
|
|
|
|
size={selectedDataSource ? 'lg' : 'xl'}
|
|
|
|
|
onEscapeKeyDown={this.hideModal}
|
|
|
|
|
className={selectedDataSource ? 'animation-fade' : 'select-datasource-list-modal animation-fade'}
|
|
|
|
|
contentClassName={`${this.props.darkMode ? 'dark-theme' : ''}`}
|
|
|
|
|
animation={false}
|
|
|
|
|
onExit={this.onExit}
|
|
|
|
|
container={this.props.container}
|
|
|
|
|
{...this.props.modalProps}
|
|
|
|
|
>
|
|
|
|
|
<Modal.Header className={'d-block'}>
|
2025-02-25 06:52:50 +00:00
|
|
|
<div className="d-flex align-items-center justify-content-between">
|
|
|
|
|
<div className="d-flex">
|
|
|
|
|
{selectedDataSource && this.props.showBackButton && (
|
|
|
|
|
<div
|
|
|
|
|
className={`back-btn me-3 mt-3 ${this.props.darkMode ? 'dark' : ''}`}
|
|
|
|
|
role="button"
|
|
|
|
|
onClick={() => this.setState({ selectedDataSource: false }, () => this.onExit())}
|
|
|
|
|
>
|
|
|
|
|
<img
|
|
|
|
|
data-cy="button-back-ds-connection-modal"
|
|
|
|
|
className="m-0"
|
|
|
|
|
src="assets/images/icons/back.svg"
|
|
|
|
|
width="30"
|
|
|
|
|
height="30"
|
|
|
|
|
/>
|
2024-10-22 08:01:21 +00:00
|
|
|
</div>
|
2025-02-25 06:52:50 +00:00
|
|
|
)}
|
|
|
|
|
<Modal.Title className="mt-3">
|
|
|
|
|
{selectedDataSource && !isSampleDb ? (
|
2025-08-14 12:47:49 +00:00
|
|
|
<div className="row selected-ds img-container">
|
2025-02-25 06:52:50 +00:00
|
|
|
{getSvgIcon(dataSourceMeta?.kind?.toLowerCase(), 35, 35, selectedDataSourceIcon)}
|
|
|
|
|
<div className="input-icon" style={{ width: '160px' }}>
|
|
|
|
|
<input
|
|
|
|
|
type="text"
|
|
|
|
|
onChange={(e) => this.onNameChanged(e.target.value)}
|
2025-07-07 11:50:55 +00:00
|
|
|
className="form-control-plaintext form-control-plaintext-sm color-slate12 tw-border-x tw-border-y"
|
2025-02-25 06:52:50 +00:00
|
|
|
value={decodeEntities(selectedDataSource.name)}
|
|
|
|
|
style={{ width: '160px' }}
|
2025-03-19 08:26:33 +00:00
|
|
|
data-cy="data-source-name-input-field"
|
2025-02-25 06:52:50 +00:00
|
|
|
autoFocus
|
|
|
|
|
autoComplete="off"
|
|
|
|
|
disabled={!canUpdateDataSource(selectedDataSource.id)}
|
|
|
|
|
/>
|
|
|
|
|
{!this.props.isEditing && (
|
|
|
|
|
<span className="input-icon-addon">
|
|
|
|
|
<img src="assets/images/icons/edit-source.svg" width="12" height="12" />
|
|
|
|
|
</span>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
2024-10-22 08:01:21 +00:00
|
|
|
</div>
|
2025-02-25 06:52:50 +00:00
|
|
|
) : (
|
|
|
|
|
<div className="row">
|
|
|
|
|
<div className="col-md-2">
|
2025-08-06 10:40:05 +00:00
|
|
|
<img src="assets/images/tj-logo.svg" />
|
2025-02-25 06:52:50 +00:00
|
|
|
</div>
|
|
|
|
|
<div className="col-md-10" data-cy="sample-data-source-title">
|
|
|
|
|
{' '}
|
|
|
|
|
Sample data source
|
|
|
|
|
</div>
|
2024-10-22 08:01:21 +00:00
|
|
|
</div>
|
2025-02-25 06:52:50 +00:00
|
|
|
)}
|
|
|
|
|
{!selectedDataSource && (
|
|
|
|
|
<span className="" data-cy="title-add-new-datasource">
|
|
|
|
|
{this.props.t('editor.queryManager.dataSourceManager.addNewDataSource', 'Add new datasource')}
|
|
|
|
|
</span>
|
|
|
|
|
)}
|
|
|
|
|
</Modal.Title>
|
|
|
|
|
{!this.props.isEditing && (
|
|
|
|
|
<span
|
|
|
|
|
data-cy="button-close-ds-connection-modal"
|
|
|
|
|
className={`close-btn mx-4 mt-3 ${this.props.darkMode ? 'dark' : ''}`}
|
|
|
|
|
onClick={() => this.hideModal()}
|
|
|
|
|
>
|
|
|
|
|
<SolidIcon name="remove" width="20" fill={'var(--slate12)'} />
|
2024-10-22 08:01:21 +00:00
|
|
|
</span>
|
|
|
|
|
)}
|
2025-02-25 06:52:50 +00:00
|
|
|
</div>
|
|
|
|
|
<div className="tw-pt-[15px]">
|
|
|
|
|
{this.props.tags &&
|
|
|
|
|
this.props.tags.map((tag) => {
|
|
|
|
|
if (tag === 'AI') {
|
|
|
|
|
return (
|
|
|
|
|
<div key={tag} className="tag-container">
|
|
|
|
|
<SolidIcon name="AI-tag" />
|
|
|
|
|
<span>{tag}</span>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
})}
|
|
|
|
|
</div>
|
2024-10-22 08:01:21 +00:00
|
|
|
</div>
|
2025-02-25 06:52:50 +00:00
|
|
|
{!isSampleDb && (
|
|
|
|
|
<MultiEnvTabs
|
|
|
|
|
selectedDataSource={selectedDataSource}
|
|
|
|
|
environments={this.props.environments}
|
|
|
|
|
featureAccess={this.props.featureAccess}
|
|
|
|
|
currentEnvironment={this.props.currentEnvironment}
|
|
|
|
|
handleActions={this.props.handleActions}
|
|
|
|
|
environmentChanged={this.props.environmentChanged}
|
|
|
|
|
resetOptions={this.resetOptions}
|
|
|
|
|
/>
|
|
|
|
|
)}
|
2024-10-22 08:01:21 +00:00
|
|
|
</Modal.Header>
|
|
|
|
|
{this.props.environmentLoading ? (
|
|
|
|
|
<ModalBody>
|
|
|
|
|
<DataSourceLoader />
|
|
|
|
|
</ModalBody>
|
|
|
|
|
) : (
|
|
|
|
|
<>
|
|
|
|
|
<Modal.Body style={sampleDBmodalBodyStyle}>
|
|
|
|
|
{selectedDataSource && !isSampleDb ? (
|
|
|
|
|
<div className="dataSourceWrapper">
|
|
|
|
|
{this.renderSourceComponent(selectedDataSource.kind, isPlugin)}
|
|
|
|
|
</div>
|
|
|
|
|
) : (
|
|
|
|
|
selectedDataSource &&
|
|
|
|
|
isSampleDb && <div className="dataSourceWrapper">{this.renderSampleDBModal()}</div>
|
|
|
|
|
)}
|
|
|
|
|
{!selectedDataSource &&
|
|
|
|
|
this.segregateDataSources(this.state.suggestingDatasources, this.props.darkMode)}
|
|
|
|
|
</Modal.Body>
|
|
|
|
|
|
2025-07-14 16:48:06 +00:00
|
|
|
{selectedDataSource &&
|
|
|
|
|
!dataSourceMeta.customTesting &&
|
|
|
|
|
(!OAuthDs.includes(selectedDataSource?.kind) ||
|
2025-07-23 16:48:50 +00:00
|
|
|
!(
|
|
|
|
|
options?.auth_type?.value === 'oauth2' &&
|
|
|
|
|
options?.grant_type?.value === 'authorization_code' &&
|
|
|
|
|
options?.multiple_auth_enabled?.value !== true
|
|
|
|
|
)) && (
|
2025-07-14 16:48:06 +00:00
|
|
|
<Modal.Footer style={sampleDBmodalFooterStyle} className="modal-footer-class">
|
|
|
|
|
{selectedDataSource && !isSampleDb && (
|
|
|
|
|
<div className="row w-100">
|
|
|
|
|
<div className="card-body datasource-footer-info">
|
|
|
|
|
<div className="row">
|
|
|
|
|
<div className="col-1">
|
|
|
|
|
<SolidIcon name="information" fill="#3E63DD" />
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div className="col" style={{ maxWidth: '480px' }}>
|
|
|
|
|
<p data-cy="white-list-ip-text" className="tj-text">
|
|
|
|
|
{this.props.t(
|
|
|
|
|
'editor.queryManager.dataSourceManager.whiteListIP',
|
|
|
|
|
'Please white-list our IP address if the data source is not publicly accessible.'
|
|
|
|
|
)}
|
|
|
|
|
</p>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div className="col-auto">
|
|
|
|
|
{isCopied ? (
|
|
|
|
|
<center className="my-2">
|
|
|
|
|
<span className="copied" data-cy="label-ip-copied">
|
|
|
|
|
{this.props.t('editor.queryManager.dataSourceManager.copied', 'Copied')}
|
|
|
|
|
</span>
|
|
|
|
|
</center>
|
|
|
|
|
) : (
|
|
|
|
|
<CopyToClipboard
|
|
|
|
|
text={config.SERVER_IP}
|
|
|
|
|
onCopy={() => {
|
|
|
|
|
this.setState({ isCopied: true });
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
<ButtonSolid
|
|
|
|
|
type="button"
|
|
|
|
|
className={`datasource-copy-button`}
|
|
|
|
|
data-cy="button-copy-ip"
|
|
|
|
|
variant="tertiary"
|
|
|
|
|
leftIcon="copy"
|
|
|
|
|
iconWidth="12"
|
|
|
|
|
>
|
|
|
|
|
{this.props.t('editor.queryManager.dataSourceManager.copy', 'Copy')}
|
|
|
|
|
</ButtonSolid>
|
|
|
|
|
</CopyToClipboard>
|
2024-10-22 08:01:21 +00:00
|
|
|
)}
|
2025-07-14 16:48:06 +00:00
|
|
|
</div>
|
2024-10-22 08:01:21 +00:00
|
|
|
</div>
|
2025-07-14 16:48:06 +00:00
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
2024-10-22 08:01:21 +00:00
|
|
|
|
2025-07-14 16:48:06 +00:00
|
|
|
{connectionTestError && (
|
|
|
|
|
<div className="row w-100">
|
|
|
|
|
<div className="alert alert-danger" role="alert">
|
|
|
|
|
<div className="text-muted" data-cy="connection-alert-text">
|
|
|
|
|
{connectionTestError.message}
|
2024-10-22 08:01:21 +00:00
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
2025-07-14 16:48:06 +00:00
|
|
|
)}
|
2024-10-22 08:01:21 +00:00
|
|
|
|
2025-07-14 16:48:06 +00:00
|
|
|
{validationError && validationError.length > 0 && (
|
|
|
|
|
<div className="row w-100">
|
|
|
|
|
<div className="alert alert-danger" role="alert">
|
|
|
|
|
{validationError.map((error, index) => (
|
|
|
|
|
<div
|
|
|
|
|
key={index}
|
|
|
|
|
className="text-muted"
|
|
|
|
|
data-cy={`${generateCypressDataCy(error)}-field-alert-text`}
|
|
|
|
|
>
|
|
|
|
|
{error}
|
|
|
|
|
</div>
|
|
|
|
|
))}
|
2024-10-22 08:01:21 +00:00
|
|
|
</div>
|
|
|
|
|
</div>
|
2025-07-14 16:48:06 +00:00
|
|
|
)}
|
2025-04-03 08:17:49 +00:00
|
|
|
|
2025-07-14 16:48:06 +00:00
|
|
|
<div className="col">
|
|
|
|
|
<SolidIcon name="logs" fill="#3E63DD" width="20" style={{ marginRight: '8px' }} />
|
|
|
|
|
<a
|
|
|
|
|
className="color-primary tj-docs-link tj-text-sm"
|
|
|
|
|
href={docLink}
|
|
|
|
|
target="_blank"
|
|
|
|
|
rel="noreferrer"
|
|
|
|
|
data-cy="link-read-documentation"
|
|
|
|
|
>
|
|
|
|
|
{this.props.t('globals.readDocumentation', 'Read documentation')}
|
|
|
|
|
</a>
|
2025-04-03 08:17:49 +00:00
|
|
|
</div>
|
2025-07-14 16:48:06 +00:00
|
|
|
<div
|
|
|
|
|
className={!isSampleDb ? `col-auto` : 'col-auto test-connection-sample-db'}
|
|
|
|
|
data-cy="button-test-connection"
|
2024-10-22 08:01:21 +00:00
|
|
|
>
|
2025-07-14 16:48:06 +00:00
|
|
|
<TestConnection
|
|
|
|
|
kind={selectedDataSource?.kind}
|
|
|
|
|
pluginId={selectedDataSource?.pluginId ?? this.state.selectedDataSourcePluginId}
|
|
|
|
|
options={options}
|
|
|
|
|
onConnectionTestFailed={this.onConnectionTestFailed}
|
|
|
|
|
darkMode={this.props.darkMode}
|
|
|
|
|
environmentId={this.props.currentEnvironment?.id}
|
|
|
|
|
dataSourceId={selectedDataSource?.id}
|
|
|
|
|
dataSourceType={selectedDataSource?.type}
|
2025-07-15 06:11:17 +00:00
|
|
|
appId={this.state.appId}
|
2025-07-14 16:48:06 +00:00
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
{!isSampleDb && (
|
|
|
|
|
<div className="col-auto" data-cy="db-connection-save-button">
|
|
|
|
|
<ButtonSolid
|
|
|
|
|
className={`m-2 ${isSaving ? 'btn-loading' : ''}`}
|
|
|
|
|
isLoading={isSaving}
|
|
|
|
|
disabled={isSaving || this.props.isVersionReleased || isSaveDisabled}
|
|
|
|
|
variant="primary"
|
|
|
|
|
onClick={this.createDataSource}
|
|
|
|
|
leftIcon="floppydisk"
|
|
|
|
|
fill={this.props.darkMode && this.props.isVersionReleased ? '#4c5155' : '#FDFDFE'}
|
|
|
|
|
>
|
|
|
|
|
{this.props.t('globals.save', 'Save')}
|
|
|
|
|
</ButtonSolid>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</Modal.Footer>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
{!dataSourceMeta?.hideSave &&
|
|
|
|
|
selectedDataSource &&
|
|
|
|
|
dataSourceMeta.customTesting &&
|
|
|
|
|
(!OAuthDs.includes(selectedDataSource?.kind) ||
|
2025-07-23 16:48:50 +00:00
|
|
|
!(
|
|
|
|
|
options?.auth_type?.value === 'oauth2' &&
|
|
|
|
|
options?.grant_type?.value === 'authorization_code' &&
|
|
|
|
|
options?.multiple_auth_enabled?.value !== true
|
|
|
|
|
)) && (
|
2025-07-14 16:48:06 +00:00
|
|
|
<Modal.Footer>
|
|
|
|
|
<div className="col">
|
|
|
|
|
<SolidIcon name="logs" fill="#3E63DD" width="20" style={{ marginRight: '8px' }} />
|
|
|
|
|
<a
|
|
|
|
|
className="color-primary tj-docs-link tj-text-sm"
|
|
|
|
|
data-cy="link-read-documentation"
|
|
|
|
|
href={
|
|
|
|
|
selectedDataSource?.pluginId && selectedDataSource.pluginId.trim() !== ''
|
|
|
|
|
? `https://docs.tooljet.ai/docs/marketplace/plugins/marketplace-plugin-${selectedDataSource.kind}/`
|
|
|
|
|
: `https://docs.tooljet.ai/docs/data-sources/${selectedDataSource.kind}`
|
|
|
|
|
}
|
|
|
|
|
target="_blank"
|
|
|
|
|
rel="noreferrer"
|
|
|
|
|
>
|
|
|
|
|
{this.props.t('globals.readDocumentation', 'Read documentation')}
|
|
|
|
|
</a>
|
|
|
|
|
</div>
|
2024-10-22 08:01:21 +00:00
|
|
|
<div className="col-auto" data-cy="db-connection-save-button">
|
|
|
|
|
<ButtonSolid
|
2025-07-14 16:48:06 +00:00
|
|
|
leftIcon="floppydisk"
|
|
|
|
|
fill={'#FDFDFE'}
|
|
|
|
|
className="m-2"
|
2024-10-22 08:01:21 +00:00
|
|
|
disabled={isSaving || this.props.isVersionReleased || isSaveDisabled}
|
|
|
|
|
variant="primary"
|
|
|
|
|
onClick={this.createDataSource}
|
|
|
|
|
>
|
2025-07-14 16:48:06 +00:00
|
|
|
{isSaving
|
|
|
|
|
? this.props.t('editor.queryManager.dataSourceManager.saving' + '...', 'Saving...')
|
|
|
|
|
: this.props.t('globals.save', 'Save')}
|
2024-10-22 08:01:21 +00:00
|
|
|
</ButtonSolid>
|
|
|
|
|
</div>
|
2025-07-14 16:48:06 +00:00
|
|
|
</Modal.Footer>
|
|
|
|
|
)}
|
2024-10-22 08:01:21 +00:00
|
|
|
</>
|
|
|
|
|
)}
|
|
|
|
|
</Modal>
|
|
|
|
|
<ConfirmDialog
|
|
|
|
|
title={'Add datasource'}
|
|
|
|
|
show={dataSourceConfirmModalProps.isOpen}
|
|
|
|
|
message={`Do you want to add ${dataSourceConfirmModalProps?.dataSource?.name}`}
|
2025-07-14 11:22:44 +00:00
|
|
|
onConfirm={() =>
|
|
|
|
|
createSelectedDataSource(dataSourceConfirmModalProps.dataSource, dataSourceConfirmModalProps?.category)
|
|
|
|
|
}
|
2024-10-22 08:01:21 +00:00
|
|
|
onCancel={this.resetDataSourceConfirmModal}
|
|
|
|
|
confirmButtonText={'Add datasource'}
|
|
|
|
|
confirmButtonType="primary"
|
|
|
|
|
cancelButtonType="tertiary"
|
|
|
|
|
backdropClassName="datasource-selection-confirm-backdrop"
|
|
|
|
|
confirmButtonLoading={addingDataSource}
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
)
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const EmptyStateContainer = ({
|
|
|
|
|
suggestionUI = false,
|
|
|
|
|
queryString,
|
|
|
|
|
handleBackToAllDatasources,
|
|
|
|
|
darkMode,
|
|
|
|
|
placeholder,
|
|
|
|
|
}) => {
|
|
|
|
|
const { t } = useTranslation();
|
|
|
|
|
const [inputValue, set] = React.useState(() => '');
|
|
|
|
|
|
|
|
|
|
const [status, setStatus] = React.useState(false);
|
|
|
|
|
const handleSend = () => {
|
|
|
|
|
if (inputValue) {
|
|
|
|
|
setStatus(true);
|
|
|
|
|
//send value to backend
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
React.useEffect(() => {
|
|
|
|
|
setStatus(false);
|
|
|
|
|
}, [queryString]);
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div className="empty">
|
|
|
|
|
{queryString && !suggestionUI && (
|
|
|
|
|
<h3>
|
|
|
|
|
{t(
|
|
|
|
|
`editor.queryManager.dataSourceManager.noResultsFor + "${queryString}"`,
|
|
|
|
|
`No results for "${queryString}"`
|
|
|
|
|
)}
|
|
|
|
|
</h3>
|
|
|
|
|
)}
|
|
|
|
|
<center className={`empty-results ${suggestionUI ? 'suggestionUI-results' : ''}`}>
|
|
|
|
|
<img src="assets/images/icons/no-results.svg" width="150" height="150" />
|
|
|
|
|
{status ? (
|
|
|
|
|
<div>
|
|
|
|
|
<p className="text-success mt-2">
|
|
|
|
|
{t('editor.queryManager.dataSourceManager.noteTaken', `Thank you, we've taken a note of that!`)}
|
|
|
|
|
</p>
|
|
|
|
|
<button
|
|
|
|
|
className={`datasource-modal-button ${darkMode && 'dark-button'}`}
|
|
|
|
|
onClick={handleBackToAllDatasources}
|
|
|
|
|
>
|
|
|
|
|
{t('editor.queryManager.dataSourceManager.goToAllDatasources', 'Go to all Datasources')}
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
) : (
|
|
|
|
|
<div className="row empty-search">
|
|
|
|
|
<div className="col-9 mt-2">
|
|
|
|
|
<div className="input-icon">
|
|
|
|
|
<input
|
|
|
|
|
type="text"
|
|
|
|
|
className="form-control mb-2"
|
|
|
|
|
value={inputValue}
|
|
|
|
|
placeholder={placeholder}
|
|
|
|
|
onChange={(e) => set(e.target.value)}
|
|
|
|
|
onKeyDown={(event) => {
|
|
|
|
|
if (event.key === 'Enter') {
|
|
|
|
|
handleSend();
|
|
|
|
|
}
|
|
|
|
|
}}
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div className="col-auto">
|
|
|
|
|
<Button className="mt-2" disabled={!inputValue.length} variant="primary" onClick={handleSend}>
|
|
|
|
|
{t('editor.queryManager.dataSourceManager.send', 'Send')}
|
|
|
|
|
</Button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</center>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const SearchBoxContainer = ({ onChange, onClear, queryString, activeDatasourceList, dataCy, scope }) => {
|
|
|
|
|
const [searchText, setSearchText] = React.useState(queryString ?? '');
|
|
|
|
|
const { t } = useTranslation();
|
|
|
|
|
const handleChange = (e) => {
|
|
|
|
|
setSearchText(e.target.value);
|
|
|
|
|
onChange(e.target.value, activeDatasourceList);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const clearSearch = () => {
|
|
|
|
|
setSearchText('');
|
|
|
|
|
onClear();
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
React.useEffect(() => {
|
|
|
|
|
if (searchText.length > 0) {
|
|
|
|
|
onChange(searchText, activeDatasourceList);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
|
|
|
}, [activeDatasourceList]);
|
|
|
|
|
|
|
|
|
|
React.useEffect(() => {
|
|
|
|
|
if (queryString === null) {
|
|
|
|
|
setSearchText('');
|
|
|
|
|
}
|
|
|
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
|
|
|
}, [queryString]);
|
|
|
|
|
React.useEffect(() => {
|
|
|
|
|
if (searchText === '') {
|
|
|
|
|
onClear();
|
|
|
|
|
}
|
|
|
|
|
let element = document.querySelector('.input-icon .form-control:not(:first-child)');
|
|
|
|
|
|
|
|
|
|
if (scope === 'global') {
|
|
|
|
|
element = document.querySelector('.input-icon .form-control');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (searchText && element) {
|
|
|
|
|
element.style.paddingLeft = '0.5rem';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return () => {
|
|
|
|
|
element && (element.style.paddingLeft = '2.5rem');
|
|
|
|
|
};
|
|
|
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
|
|
|
}, [searchText]);
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div className="search-box-wrapper">
|
|
|
|
|
<div style={{ height: '36px' }} className="input-icon d-flex">
|
|
|
|
|
{searchText.length === 0 && (
|
|
|
|
|
<span className="search-icon mt-2 mx-2">
|
|
|
|
|
<svg
|
|
|
|
|
xmlns="http://www.w3.org/2000/svg"
|
|
|
|
|
className="icon"
|
|
|
|
|
width="24"
|
|
|
|
|
height="24"
|
|
|
|
|
viewBox="0 0 24 24"
|
|
|
|
|
strokeWidth="2"
|
|
|
|
|
stroke="currentColor"
|
|
|
|
|
fill="none"
|
|
|
|
|
strokeLinecap="round"
|
|
|
|
|
strokeLinejoin="round"
|
|
|
|
|
>
|
|
|
|
|
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
|
|
|
|
<circle cx="10" cy="10" r="7" />
|
|
|
|
|
<line x1="21" y1="21" x2="15" y2="15" />
|
|
|
|
|
</svg>
|
|
|
|
|
</span>
|
|
|
|
|
)}
|
|
|
|
|
<input
|
|
|
|
|
type="text"
|
|
|
|
|
value={searchText}
|
|
|
|
|
onChange={handleChange}
|
|
|
|
|
className="form-control"
|
|
|
|
|
placeholder={t('globals.search', 'Search')}
|
|
|
|
|
autoFocus
|
|
|
|
|
data-cy={dataCy}
|
|
|
|
|
/>
|
|
|
|
|
{searchText.length > 0 && (
|
|
|
|
|
<span className="clear-icon mt-2" onClick={clearSearch}>
|
|
|
|
|
<svg
|
|
|
|
|
xmlns="http://www.w3.org/2000/svg"
|
|
|
|
|
className="icon icon-tabler icon-tabler-circle-x"
|
|
|
|
|
width="24"
|
|
|
|
|
height="24"
|
|
|
|
|
viewBox="0 0 24 24"
|
|
|
|
|
strokeWidth="2"
|
|
|
|
|
stroke="currentColor"
|
|
|
|
|
fill="none"
|
|
|
|
|
strokeLinecap="round"
|
|
|
|
|
strokeLinejoin="round"
|
|
|
|
|
>
|
|
|
|
|
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
|
|
|
|
|
<circle cx="12" cy="12" r="9"></circle>
|
|
|
|
|
<path d="M10 10l4 4m0 -4l-4 4"></path>
|
|
|
|
|
</svg>
|
|
|
|
|
</span>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const DataSourceLoader = () => {
|
|
|
|
|
const generateLoaders = (max = 7) => {
|
|
|
|
|
const arr = [];
|
|
|
|
|
for (let i = 1; i < max; i++) {
|
|
|
|
|
arr.push(
|
|
|
|
|
<div key={i}>
|
|
|
|
|
<Skeleton className="label-loader" />
|
|
|
|
|
<Skeleton className="input-loader" />
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
return arr;
|
|
|
|
|
};
|
|
|
|
|
return <div className="data-source-loader-container">{generateLoaders()}</div>;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const withStore = (Component) => (props) => {
|
|
|
|
|
const { setGlobalDataSourceStatus } = useDataSourcesStore(
|
|
|
|
|
(state) => ({
|
|
|
|
|
setGlobalDataSourceStatus: state.actions.setGlobalDataSourceStatus,
|
|
|
|
|
}),
|
|
|
|
|
shallow
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const { handleActions } = useGlobalDatasourceUnsavedChanges();
|
|
|
|
|
|
|
|
|
|
return <Component {...props} setGlobalDataSourceStatus={setGlobalDataSourceStatus} handleActions={handleActions} />;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
export const DataSourceManager = withTranslation()(withRouter(withStore(DataSourceManagerComponent)));
|