Implemented new add service flow with schema form (#4111)

This commit is contained in:
darth-coder00 2022-04-14 09:47:22 +05:30 committed by GitHub
parent 339968c71e
commit 786cf75171
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 112 additions and 339 deletions

View File

@ -12,33 +12,28 @@
*/
import { isUndefined } from 'lodash';
import { DynamicFormFieldType } from 'Models';
import { LoadingState } from 'Models';
import React, { useState } from 'react';
import { useHistory } from 'react-router-dom';
import {
getAddServicePath,
getServiceDetailsPath,
ONLY_NUMBER_REGEX,
ROUTES,
} from '../../constants/constants';
import { getServiceDetailsPath, ROUTES } from '../../constants/constants';
import { STEPS_FOR_ADD_SERVICE } from '../../constants/services.const';
import { PageLayoutType } from '../../enums/layout.enum';
import { ServiceCategory } from '../../enums/service.enum';
import { DashboardServiceType } from '../../generated/entity/services/dashboardService';
import { MessagingServiceType } from '../../generated/entity/services/messagingService';
import { DataObj } from '../../interface/service.interface';
import { getCurrentUserId } from '../../utils/CommonUtils';
import {
getKeyValueObject,
isIngestionSupported,
} from '../../utils/ServiceUtils';
ConfigData,
DataObj,
DataService,
} from '../../interface/service.interface';
import { getCurrentUserId } from '../../utils/CommonUtils';
import { getAddServicePath } from '../../utils/RouterUtils';
import { isIngestionSupported } from '../../utils/ServiceUtils';
import AddIngestion from '../AddIngestion/AddIngestion.component';
import SuccessScreen from '../common/success-screen/SuccessScreen';
import PageLayout from '../containers/PageLayout';
import IngestionStepper from '../IngestionStepper/IngestionStepper.component';
import ConnectionConfigForm from '../ServiceConfig/ConnectionConfigForm';
import { AddServiceProps } from './AddService.interface';
import ConfigureService from './Steps/ConfigureService';
import ConnectionDetails from './Steps/ConnectionDetails';
import SelectServiceType from './Steps/SelectServiceType';
const AddService = ({
@ -58,28 +53,8 @@ const AddService = ({
const [selectServiceType, setSelectServiceType] = useState('');
const [serviceName, setServiceName] = useState('');
const [description, setDescription] = useState('');
const [url, setUrl] = useState('');
const [port, setPort] = useState('');
const [database, setDatabase] = useState('');
const [username, setUsername] = useState('');
const [password, setPassword] = useState('');
const [warehouse, setWarehouse] = useState('');
const [account, setAccount] = useState('');
const [connectionOptions, setConnectionOptions] = useState<
DynamicFormFieldType[]
>([]);
const [connectionArguments, setConnectionArguments] = useState<
DynamicFormFieldType[]
>([]);
const [brokers, setBrokers] = useState('');
const [schemaRegistry, setSchemaRegistry] = useState('');
const [pipelineUrl, setPipelineUrl] = useState('');
const [dashboardUrl, setDashboardUrl] = useState('');
const [env, setEnv] = useState('');
const [apiVersion, setApiVersion] = useState('');
const [server, setServer] = useState('');
const [siteName, setSiteName] = useState('');
const [apiKey, setApiKey] = useState('');
const [saveServiceState, setSaveServiceState] =
useState<LoadingState>('initial');
const handleServiceTypeClick = (type: string) => {
setShowErrorMessage({ ...showErrorMessage, serviceType: false });
@ -121,173 +96,47 @@ const AddService = ({
setAddIngestion(value);
};
const handleSubmit = () => {
let dataObj: DataObj = {
description: description,
const handleConfigUpdate = (
oData: ConfigData,
serviceCat: ServiceCategory
) => {
const data = {
name: serviceName,
serviceType: selectServiceType,
};
switch (serviceCategory) {
case ServiceCategory.DATABASE_SERVICES:
{
dataObj = {
...dataObj,
databaseConnection: {
hostPort: `${url}:${port}`,
connectionArguments: getKeyValueObject(connectionArguments),
connectionOptions: getKeyValueObject(connectionOptions),
database: database,
password: password,
username: username,
},
};
}
break;
case ServiceCategory.MESSAGING_SERVICES:
{
dataObj = {
...dataObj,
brokers:
selectServiceType === MessagingServiceType.Pulsar
? [brokers]
: brokers.split(',').map((broker) => broker.trim()),
schemaRegistry: schemaRegistry,
};
}
break;
case ServiceCategory.DASHBOARD_SERVICES:
{
switch (selectServiceType) {
case DashboardServiceType.Redash:
{
dataObj = {
...dataObj,
dashboardUrl: dashboardUrl,
// eslint-disable-next-line @typescript-eslint/camelcase
api_key: apiKey,
};
}
break;
case DashboardServiceType.Tableau:
{
dataObj = {
...dataObj,
dashboardUrl: dashboardUrl,
// eslint-disable-next-line @typescript-eslint/camelcase
site_name: siteName,
username: username,
password: password,
// eslint-disable-next-line @typescript-eslint/camelcase
api_version: apiVersion,
server: server,
};
}
break;
default:
{
dataObj = {
...dataObj,
dashboardUrl: dashboardUrl,
username: username,
password: password,
};
}
break;
}
}
break;
case ServiceCategory.PIPELINE_SERVICES:
{
dataObj = {
...dataObj,
pipelineUrl: pipelineUrl,
};
}
break;
default:
break;
}
// TODO:- need to replace mockdata with actual data.
// mockdata to create service for mySQL
const mockData = {
description: description,
name: serviceName,
connection: {
config: {
type: 'MySQL',
hostPort: 'localhost:3306',
},
},
owner: {
id: getCurrentUserId(),
type: 'user',
},
serviceType: 'MySQL',
};
const configData =
serviceCat === ServiceCategory.PIPELINE_SERVICES
? { ...data, pipelineUrl: oData.pipelineUrl }
: {
...data,
connection: {
config: oData,
},
};
onAddServiceSave(mockData).then(() => {
setActiveStepperStep(4);
return new Promise<void>((resolve, reject) => {
setSaveServiceState('waiting');
onAddServiceSave(configData)
.then(() => {
setActiveStepperStep(4);
resolve();
})
.catch((err) => {
reject(err);
})
.finally(() => setSaveServiceState('initial'));
});
};
const handleConnectionDetailsSubmitClick = () => {
// validation will go here
handleSubmit();
};
const handleConnectionDetailsBackClick = () => {
setActiveStepperStep(2);
};
const addConnectionOptionFields = () => {
setConnectionOptions([...connectionOptions, { key: '', value: '' }]);
};
const removeConnectionOptionFields = (i: number) => {
const newFormValues = [...connectionOptions];
newFormValues.splice(i, 1);
setConnectionOptions(newFormValues);
};
const handleConnectionOptionFieldsChange = (
i: number,
field: keyof DynamicFormFieldType,
value: string
) => {
const newFormValues = [...connectionOptions];
newFormValues[i][field] = value;
setConnectionOptions(newFormValues);
};
const addConnectionArgumentFields = () => {
setConnectionArguments([...connectionArguments, { key: '', value: '' }]);
};
const removeConnectionArgumentFields = (i: number) => {
const newFormValues = [...connectionArguments];
newFormValues.splice(i, 1);
setConnectionArguments(newFormValues);
};
const handleConnectionArgumentFieldsChange = (
i: number,
field: keyof DynamicFormFieldType,
value: string
) => {
const newFormValues = [...connectionArguments];
newFormValues[i][field] = value;
setConnectionArguments(newFormValues);
};
const handleViewServiceClick = () => {
if (!isUndefined(newServiceData)) {
history.push(getServiceDetailsPath(newServiceData.name, serviceCategory));
@ -297,97 +146,10 @@ const AddService = ({
const handleValidation = (
event: React.ChangeEvent<HTMLInputElement | HTMLSelectElement>
) => {
const value = event.target.value;
const name = event.target.name;
switch (name) {
case 'serviceName':
setServiceName(value.trim());
setShowErrorMessage({ ...showErrorMessage, name: false });
break;
case 'url':
setUrl(value);
break;
case 'port':
if (ONLY_NUMBER_REGEX.test(value) || value === '') {
setPort(value);
}
break;
case 'database':
setDatabase(value);
break;
case 'username':
setUsername(value);
break;
case 'password':
setPassword(value);
break;
case 'warehouse':
setWarehouse(value);
break;
case 'account':
setAccount(value);
break;
case 'brokers':
setBrokers(value);
break;
case 'schemaRegistry':
setSchemaRegistry(value);
break;
case 'pipelineUrl':
setPipelineUrl(value);
break;
case 'dashboardUrl':
setDashboardUrl(value);
break;
case 'env':
setEnv(value);
break;
case 'apiVersion':
setApiVersion(value);
break;
case 'server':
setServer(value);
break;
case 'siteName':
setSiteName(value);
break;
case 'apiKey':
setApiKey(value);
break;
const value = event.target.value.trim();
setServiceName(value);
if (value) {
setShowErrorMessage({ ...showErrorMessage, name: false });
}
};
@ -430,40 +192,20 @@ const AddService = ({
)}
{activeStepperStep === 3 && (
<ConnectionDetails
account={account}
addConnectionArgumentFields={addConnectionArgumentFields}
addConnectionOptionFields={addConnectionOptionFields}
apiKey={apiKey}
apiVersion={apiVersion}
brokers={brokers}
connectionArguments={connectionArguments}
connectionOptions={connectionOptions}
dashboardUrl={dashboardUrl}
database={database}
env={env}
handleConnectionArgumentFieldsChange={
handleConnectionArgumentFieldsChange
<ConnectionConfigForm
data={
(serviceCategory !== ServiceCategory.PIPELINE_SERVICES
? {
connection: { config: { type: selectServiceType } },
}
: {}) as DataService
}
handleConnectionOptionFieldsChange={
handleConnectionOptionFieldsChange
}
handleValidation={handleValidation}
password={password}
pipelineUrl={pipelineUrl}
port={port}
removeConnectionArgumentFields={removeConnectionArgumentFields}
removeConnectionOptionFields={removeConnectionOptionFields}
schemaRegistry={schemaRegistry}
selectedService={selectServiceType}
server={server}
serviceCategory={serviceCategory}
siteName={siteName}
url={url}
username={username}
warehouse={warehouse}
onBack={handleConnectionDetailsBackClick}
onSubmit={handleConnectionDetailsSubmitClick}
status={saveServiceState}
onCancel={handleConnectionDetailsBackClick}
onSave={(e) => {
handleConfigUpdate(e.formData, serviceCategory);
}}
/>
)}

View File

@ -38,6 +38,10 @@ jest.mock('react-router-dom', () => ({
useHistory: jest.fn(),
}));
jest.mock('../ServiceConfig/ConnectionConfigForm', () => () => (
<>ConnectionConfigForm</>
));
describe('Test AddService component', () => {
it('AddService component should render', async () => {
const { container } = render(

View File

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

View File

@ -40,6 +40,7 @@ interface Props {
data: DatabaseService | MessagingService | DashboardService | PipelineService;
serviceCategory: ServiceCategory;
status: LoadingState;
onCancel?: () => void;
onSave: (data: ISubmitEvent<ConfigData>) => void;
}
@ -47,6 +48,7 @@ const ConnectionConfigForm: FunctionComponent<Props> = ({
data,
serviceCategory,
status,
onCancel,
onSave,
}: Props) => {
const config = !isNil(data)
@ -55,7 +57,7 @@ const ConnectionConfigForm: FunctionComponent<Props> = ({
? ((data as DatabaseService | MessagingService | DashboardService)
.connection.config as ConfigData)
: ({ pipelineUrl: (data as PipelineService).pipelineUrl } as ConfigData)
: {};
: ({} as ConfigData);
const getDatabaseFields = () => {
let connSch = {
schema: {},
@ -105,6 +107,7 @@ const ConnectionConfigForm: FunctionComponent<Props> = ({
schema={connSch.schema}
status={status}
uiSchema={connSch.uiSchema}
onCancel={onCancel}
onSubmit={onSave}
/>
);

View File

@ -11,20 +11,21 @@
* limitations under the License.
*/
import { ServicesData } from 'Models';
import { LoadingState, ServicesData } from 'Models';
import React, { useState } from 'react';
import { ServiceCategory } from '../../enums/service.enum';
import { DashboardService } from '../../generated/entity/services/dashboardService';
import { DatabaseService } from '../../generated/entity/services/databaseService';
import { MessagingService } from '../../generated/entity/services/messagingService';
import { PipelineService } from '../../generated/entity/services/pipelineService';
import { ConfigData } from '../../interface/service.interface';
import ConnectionConfigForm from './ConnectionConfigForm';
interface ServiceConfigProps {
serviceCategory: ServiceCategory;
data?: ServicesData;
handleUpdate: (
data: ServicesData,
data: ConfigData,
serviceCategory: ServiceCategory
) => Promise<void>;
}
@ -38,7 +39,7 @@ const ServiceConfig = ({
data,
handleUpdate,
}: ServiceConfigProps) => {
const [status] = useState<'initial' | 'waiting' | 'success'>('initial');
const [status] = useState<LoadingState>('initial');
const getDynamicFields = () => {
return (
@ -53,7 +54,7 @@ const ServiceConfig = ({
serviceCategory={serviceCategory}
status={status}
onSave={(e) => {
handleUpdate(e.formData as ServicesData, serviceCategory);
handleUpdate(e.formData, serviceCategory);
}}
/>
);

View File

@ -45,7 +45,8 @@ const PLACEHOLDER_ROUTE_DASHBOARD_FQN = ':dashboardFQN';
const PLACEHOLDER_ROUTE_DATABASE_FQN = ':databaseFQN';
const PLACEHOLDER_ROUTE_DATABASE_SCHEMA_FQN = ':databaseSchemaFQN';
const PLACEHOLDER_ROUTE_SERVICE_FQN = ':serviceFQN';
const PLACEHOLDER_ROUTE_SERVICE_CAT = ':serviceCategory';
export const PLACEHOLDER_ROUTE_SERVICE_CAT = ':serviceCategory';
const PLACEHOLDER_ROUTE_SEARCHQUERY = ':searchQuery';
const PLACEHOLDER_ROUTE_TAB = ':tab';
const PLACEHOLDER_ROUTE_TEAM = ':team';
@ -243,13 +244,6 @@ export const getServiceDetailsPath = (
return path;
};
export const getAddServicePath = (serviceCategory: string) => {
let path = ROUTES.ADD_SERVICE;
path = path.replace(PLACEHOLDER_ROUTE_SERVICE_CAT, serviceCategory);
return path;
};
export const getExplorePathWithSearch = (searchQuery = '', tab = 'tables') => {
let path = ROUTES.EXPLORE_WITH_SEARCH;
path = path

View File

@ -62,15 +62,19 @@ export type ServiceDataObj = { name: string } & Partial<DatabaseService> &
Partial<DashboardService> &
Partial<PipelineService>;
export type DataService =
| DatabaseService
| MessagingService
| DashboardService
| PipelineService;
export interface ServiceResponse {
data: Array<ServiceDataObj>;
paging: Paging;
}
export type ConfigData =
| DatabaseService['connection']
| MessagingService['connection']
| DashboardService['connection']
| {
pipelineUrl: string;
};
export type ConfigData = Partial<DatabaseService['connection']> &
Partial<MessagingService['connection']> &
Partial<DashboardService['connection']> & {
pipelineUrl: string;
};

View File

@ -45,6 +45,7 @@ import {
mockSupersetService,
mockTableauService,
} from '../../mocks/Service.mock';
import { getAddServicePath } from '../../utils/RouterUtils';
import ServicesPage from './index';
jest.mock('../../authentication/auth-provider/AuthProvider', () => {
@ -59,6 +60,10 @@ jest.mock('../../authentication/auth-provider/AuthProvider', () => {
};
});
jest.mock('../../utils/RouterUtils', () => ({
getAddServicePath: jest.fn(),
}));
jest.mock('../../axiosAPIs/serviceAPI', () => ({
deleteService: jest.fn(),
getServiceDetails: jest
@ -112,6 +117,8 @@ jest.mock('../../components/Modals/AddServiceModal/AddServiceModal', () => ({
.mockReturnValue(<p data-testid="add-service-modal">AddServiceModal</p>),
}));
const mockGetAddServicePath = jest.fn();
describe('Test Service page', () => {
it('Check if there is an element in the page', async () => {
const { container } = render(<ServicesPage />, {
@ -160,15 +167,16 @@ describe('Test Service page', () => {
});
it('OnClick of add service, AddServiceModal should open', async () => {
(getAddServicePath as jest.Mock).mockImplementationOnce(
mockGetAddServicePath
);
const { container } = render(<ServicesPage />, {
wrapper: MemoryRouter,
});
const addService = await findByTestId(container, 'add-new-service-button');
fireEvent.click(addService);
expect(
await findByTestId(container, 'add-service-modal')
).toBeInTheDocument();
expect(mockGetAddServicePath).toBeCalled();
});
it('Card details should be display properly', async () => {

View File

@ -16,7 +16,7 @@ import classNames from 'classnames';
import { isNil } from 'lodash';
import { ServiceCollection, ServiceData, ServiceTypes } from 'Models';
import React, { Fragment, useEffect, useState } from 'react';
import { Link } from 'react-router-dom';
import { Link, useHistory } from 'react-router-dom';
import { useAuthContext } from '../../authentication/auth-provider/AuthProvider';
import { addAirflowPipeline } from '../../axiosAPIs/airflowPipelineAPI';
import {
@ -69,6 +69,7 @@ import {
} from '../../utils/CommonUtils';
import { getDashboardURL } from '../../utils/DashboardServiceUtils';
import { getBrokers } from '../../utils/MessagingServiceUtils';
import { getAddServicePath } from '../../utils/RouterUtils';
import { getErrorText } from '../../utils/StringsUtils';
import SVGIcons from '../../utils/SvgUtils';
import { showErrorToast } from '../../utils/ToastUtils';
@ -98,6 +99,8 @@ export type ApiData = {
};
const ServicesPage = () => {
const history = useHistory();
const { isAdminUser } = useAuth();
const { isAuthDisabled } = useAuthContext();
const [isModalOpen, setIsModalOpen] = useState(false);
@ -211,9 +214,12 @@ const ServicesPage = () => {
}
};
const goToAddService = () => {
history.push(getAddServicePath(serviceName));
};
const handleAddService = () => {
setEditData(undefined);
setIsModalOpen(true);
goToAddService();
};
const handleClose = () => {
setIsModalOpen(false);

View File

@ -11,7 +11,11 @@
* limitations under the License.
*/
import { IN_PAGE_SEARCH_ROUTES, ROUTES } from '../constants/constants';
import {
IN_PAGE_SEARCH_ROUTES,
PLACEHOLDER_ROUTE_SERVICE_CAT,
ROUTES,
} from '../constants/constants';
export const isDashboard = (pathname: string): boolean => {
return pathname === ROUTES.FEEDS;
@ -35,3 +39,10 @@ export const inPageSearchOptions = (pathname: string): Array<string> => {
return strOptions;
};
export const getAddServicePath = (serviceCategory: string) => {
let path = ROUTES.ADD_SERVICE;
path = path.replace(PLACEHOLDER_ROUTE_SERVICE_CAT, serviceCategory);
return path;
};