Fixed #4249: Test Connection button when adding/updating a service (#4298)

This commit is contained in:
darth-coder00 2022-04-21 02:59:31 +05:30 committed by GitHub
parent 67fb846dd9
commit 6c00b80f1d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 168 additions and 36 deletions

View File

@ -14,6 +14,7 @@
import { AxiosResponse } from 'axios'; import { AxiosResponse } from 'axios';
import { isNil } from 'lodash'; import { isNil } from 'lodash';
import { ServiceOption } from 'Models'; import { ServiceOption } from 'Models';
import { ConfigData } from '../interface/service.interface';
import { getURLWithQueryFields } from '../utils/APIUtils'; import { getURLWithQueryFields } from '../utils/APIUtils';
import APIClient from './index'; import APIClient from './index';
@ -79,3 +80,15 @@ export const deleteService: Function = (
): Promise<AxiosResponse> => { ): Promise<AxiosResponse> => {
return APIClient.delete(`/services/${serviceName}/${id}`); return APIClient.delete(`/services/${serviceName}/${id}`);
}; };
export const TestConnection = (
data: ConfigData,
type: string
): Promise<AxiosResponse> => {
const payload = {
connection: { config: data },
connectionType: type,
};
return APIClient.post(`/services/ingestionPipelines/testConnection`, payload);
};

View File

@ -101,7 +101,7 @@ const SelectServiceType = ({
theme="primary" theme="primary"
variant="text" variant="text"
onClick={onCancel}> onClick={onCancel}>
<span>Back</span> <span>Discard</span>
</Button> </Button>
<Button <Button

View File

@ -15,6 +15,7 @@ import { ISubmitEvent } from '@rjsf/core';
import { cloneDeep, isNil } from 'lodash'; import { cloneDeep, isNil } from 'lodash';
import { LoadingState } from 'Models'; import { LoadingState } from 'Models';
import React, { FunctionComponent } from 'react'; import React, { FunctionComponent } from 'react';
import { TestConnection } from '../../axiosAPIs/serviceAPI';
import { ServiceCategory } from '../../enums/service.enum'; import { ServiceCategory } from '../../enums/service.enum';
import { import {
DashboardConnection, DashboardConnection,
@ -30,10 +31,12 @@ import {
} from '../../generated/entity/services/messagingService'; } from '../../generated/entity/services/messagingService';
import { PipelineService } from '../../generated/entity/services/pipelineService'; import { PipelineService } from '../../generated/entity/services/pipelineService';
import { ConfigData } from '../../interface/service.interface'; import { ConfigData } from '../../interface/service.interface';
import jsonData from '../../jsons/en';
import { getDashboardConfig } from '../../utils/DashboardServiceUtils'; import { getDashboardConfig } from '../../utils/DashboardServiceUtils';
import { getDatabaseConfig } from '../../utils/DatabaseServiceUtils'; import { getDatabaseConfig } from '../../utils/DatabaseServiceUtils';
import { getMessagingConfig } from '../../utils/MessagingServiceUtils'; import { getMessagingConfig } from '../../utils/MessagingServiceUtils';
import { getPipelineConfig } from '../../utils/PipelineServiceUtils'; import { getPipelineConfig } from '../../utils/PipelineServiceUtils';
import { showErrorToast, showSuccessToast } from '../../utils/ToastUtils';
import FormBuilder from '../common/FormBuilder/FormBuilder'; import FormBuilder from '../common/FormBuilder/FormBuilder';
interface Props { interface Props {
@ -62,6 +65,32 @@ const ConnectionConfigForm: FunctionComponent<Props> = ({
.connection.config as ConfigData) .connection.config as ConfigData)
: ({ pipelineUrl: (data as PipelineService).pipelineUrl } as ConfigData) : ({ pipelineUrl: (data as PipelineService).pipelineUrl } as ConfigData)
: ({} as ConfigData); : ({} as ConfigData);
const handleTestConnection = (formData: ConfigData) => {
return new Promise<void>((resolve, reject) => {
TestConnection(formData, 'Database')
.then((res) => {
// This api only responds with status 200 on success
// No data sent on api success
if (res.status === 200) {
showSuccessToast(
jsonData['api-success-messages']['test-connection-success']
);
resolve();
} else {
throw jsonData['api-error-messages']['unexpected-server-response'];
}
})
.catch((err) => {
showErrorToast(
err,
jsonData['api-error-messages']['test-connection-error']
);
reject(err);
});
});
};
const getDatabaseFields = () => { const getDatabaseFields = () => {
let connSch = { let connSch = {
schema: {}, schema: {},
@ -115,6 +144,7 @@ const ConnectionConfigForm: FunctionComponent<Props> = ({
uiSchema={connSch.uiSchema} uiSchema={connSch.uiSchema}
onCancel={onCancel} onCancel={onCancel}
onSubmit={onSave} onSubmit={onSave}
onTestConnection={handleTestConnection}
/> />
); );
}; };

View File

@ -18,8 +18,14 @@ import Form, {
ObjectFieldTemplateProps, ObjectFieldTemplateProps,
} from '@rjsf/core'; } from '@rjsf/core';
import classNames from 'classnames'; import classNames from 'classnames';
import { debounce, isEmpty, isEqual } from 'lodash';
import { LoadingState } from 'Models'; import { LoadingState } from 'Models';
import React, { Fragment, FunctionComponent } from 'react'; import React, {
Fragment,
FunctionComponent,
useCallback,
useState,
} from 'react';
import { ConfigData } from '../../../interface/service.interface'; import { ConfigData } from '../../../interface/service.interface';
import SVGIcons, { Icons } from '../../../utils/SvgUtils'; import SVGIcons, { Icons } from '../../../utils/SvgUtils';
import { Button } from '../../buttons/Button/Button'; import { Button } from '../../buttons/Button/Button';
@ -31,6 +37,7 @@ interface Props extends FormProps<ConfigData> {
showFormHeader?: boolean; showFormHeader?: boolean;
status?: LoadingState; status?: LoadingState;
onCancel?: () => void; onCancel?: () => void;
onTestConnection?: (formData: ConfigData) => Promise<void>;
} }
function ArrayFieldTemplate(props: ArrayFieldTemplateProps) { function ArrayFieldTemplate(props: ArrayFieldTemplateProps) {
@ -124,17 +131,26 @@ function ObjectFieldTemplate(props: ObjectFieldTemplateProps) {
} }
const FormBuilder: FunctionComponent<Props> = ({ const FormBuilder: FunctionComponent<Props> = ({
formData,
schema,
okText, okText,
cancelText, cancelText,
showFormHeader = false, showFormHeader = false,
status = 'initial', status = 'initial',
onCancel, onCancel,
onSubmit, onSubmit,
onTestConnection,
...props ...props
}: Props) => { }: Props) => {
let oForm: Form<ConfigData> | null; let oForm: Form<ConfigData> | null;
const [localFormData, setLocalFormData] = useState<ConfigData | undefined>(
formData
);
const [connectionTesting, setConnectionTesting] = useState<boolean>(false);
const [connectionTested, setConnectionTested] = useState<boolean>(false);
const handleCancel = () => { const handleCancel = () => {
setLocalFormData(formData);
if (onCancel) { if (onCancel) {
onCancel(); onCancel();
} }
@ -146,6 +162,40 @@ const FormBuilder: FunctionComponent<Props> = ({
} }
}; };
const handleTestConnection = () => {
if (localFormData && onTestConnection) {
setConnectionTesting(true);
onTestConnection(localFormData)
.then(() => {
setConnectionTested(true);
})
.catch(() => {
setConnectionTested(false);
})
.finally(() => {
setConnectionTesting(false);
});
}
};
const debouncedOnChange = useCallback(
(updatedData: ConfigData): void => {
setLocalFormData(updatedData);
},
[setLocalFormData]
);
const debounceOnSearch = useCallback(debounce(debouncedOnChange, 1500), [
debouncedOnChange,
]);
const handleChange = (updatedData: ConfigData) => {
debounceOnSearch(updatedData);
setConnectionTested(false);
};
const disableTestConnection = () => {
return connectionTesting || isEqual(localFormData, formData);
};
return ( return (
<Form <Form
ArrayFieldTemplate={ArrayFieldTemplate} ArrayFieldTemplate={ArrayFieldTemplate}
@ -153,48 +203,79 @@ const FormBuilder: FunctionComponent<Props> = ({
className={classNames('rjsf', props.className, { className={classNames('rjsf', props.className, {
'no-header': !showFormHeader, 'no-header': !showFormHeader,
})} })}
formData={localFormData}
ref={(form) => { ref={(form) => {
oForm = form; oForm = form;
}} }}
schema={schema}
onChange={(e) => {
handleChange(e.formData);
props.onChange && props.onChange(e);
}}
onSubmit={onSubmit} onSubmit={onSubmit}
{...props}> {...props}>
<div className="tw-mt-6 tw-text-right" data-testid="buttons"> {isEmpty(schema) && (
<Button <div className="tw-text-grey-muted tw-text-center">
size="regular" No Connection Configs available.
theme="primary" </div>
variant="text" )}
onClick={handleCancel}> <div className="tw-mt-6 tw-flex tw-justify-between">
{cancelText} <div>
</Button> {!isEmpty(schema) && onTestConnection && (
{status === 'waiting' ? ( <Button
className={classNames({
'tw-opacity-40': disableTestConnection(),
})}
disabled={disableTestConnection()}
size="regular"
theme="primary"
variant="outlined"
onClick={handleTestConnection}>
Test Connection
</Button>
)}
</div>
<div className="tw-text-right" data-testid="buttons">
<Button <Button
disabled
className="tw-w-16 tw-h-10 disabled:tw-opacity-100"
size="regular" size="regular"
theme="primary" theme="primary"
variant="contained"> variant="text"
<Loader size="small" type="white" /> onClick={handleCancel}>
{cancelText}
</Button> </Button>
) : status === 'success' ? ( {status === 'waiting' ? (
<Button <Button
disabled disabled
className="tw-w-16 tw-h-10 disabled:tw-opacity-100" className="tw-w-16 tw-h-10 disabled:tw-opacity-100"
size="regular" size="regular"
theme="primary" theme="primary"
variant="contained"> variant="contained">
<FontAwesomeIcon icon="check" /> <Loader size="small" type="white" />
</Button> </Button>
) : ( ) : status === 'success' ? (
<Button <Button
className="tw-w-16 tw-h-10" disabled
data-testid="saveManageTab" className="tw-w-16 tw-h-10 disabled:tw-opacity-100"
size="regular" size="regular"
theme="primary" theme="primary"
variant="contained" variant="contained">
onClick={handleSubmit}> <FontAwesomeIcon icon="check" />
{okText} </Button>
</Button> ) : (
)} <Button
className={classNames('tw-w-16 tw-h-10', {
'tw-opacity-40': !connectionTested && !isEmpty(schema),
})}
data-testid="saveManageTab"
disabled={!connectionTested && !isEmpty(schema)}
size="regular"
theme="primary"
variant="contained"
onClick={handleSubmit}>
{okText}
</Button>
)}
</div>
</div> </div>
</Form> </Form>
); );

View File

@ -42,7 +42,7 @@ const SuccessScreen = ({
</div> </div>
<p className="tw-mb-7" data-testid="success-line"> <p className="tw-mb-7" data-testid="success-line">
<span className="tw-mr-1 tw-font-semibold">&quot;{name}&quot;</span> <span className="tw-mr-1 tw-font-semibold">&quot;{name}&quot;</span>
<span>has been successfuly created</span> <span>has been created successfully</span>
</p> </p>
<div> <div>

View File

@ -102,6 +102,8 @@ const jsonData = {
'fetch-webhook-error': 'Error while fetching webhooks!', 'fetch-webhook-error': 'Error while fetching webhooks!',
'fetch-user-count-error': 'Error while getting users count!', 'fetch-user-count-error': 'Error while getting users count!',
'test-connection-error': 'Error while testing connection!',
'unexpected-server-response': 'Unexpected response from server!', 'unexpected-server-response': 'Unexpected response from server!',
'update-chart-error': 'Error while updating charts!', 'update-chart-error': 'Error while updating charts!',
@ -138,6 +140,7 @@ const jsonData = {
'delete-test': 'Test deleted successfully!', 'delete-test': 'Test deleted successfully!',
'delete-message': 'Message deleted successfully!', 'delete-message': 'Message deleted successfully!',
'delete-entity-success': 'Entity deleted successfully!', 'delete-entity-success': 'Entity deleted successfully!',
'test-connection-success': 'Connection tested successfully!',
}, },
'form-error-messages': { 'form-error-messages': {
'empty-email': 'Email is required.', 'empty-email': 'Email is required.',

View File

@ -449,6 +449,10 @@
width: 450px; width: 450px;
} }
.Toastify__toast {
@apply tw-overflow-y-auto tw-max-h-80vh;
}
.Toastify__toast-body { .Toastify__toast-body {
@apply tw-items-start; @apply tw-items-start;
} }

View File

@ -170,6 +170,7 @@ module.exports = {
}, },
maxHeight: { maxHeight: {
32: '8rem', 32: '8rem',
'80vh': '80vh',
'90vh': '90vh', '90vh': '90vh',
}, },
minHeight: { minHeight: {