UI :- Change normal select to ANTD select in Service Page (#9710)

* Change normal select to ANTD select in Service Page

* fix cypress and unit test issue

* remove empty space

* fix cypress issue

* code optimization

* fix tests

* optimize the code and changes made as per comments

* fix unit test issue
This commit is contained in:
Ashish Gupta 2023-03-02 11:16:30 +05:30 committed by GitHub
parent 6a4df5f460
commit b59de5c7a5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 371 additions and 325 deletions

View File

@ -123,7 +123,8 @@ export const handleIngestionRetry = (
export const scheduleIngestion = () => { export const scheduleIngestion = () => {
// Schedule & Deploy // Schedule & Deploy
cy.contains('Schedule for Ingestion').should('be.visible'); cy.contains('Schedule for Ingestion').should('be.visible');
cy.get('[data-testid="cron-type"]').should('be.visible').select('hour'); cy.get('[data-testid="cron-type"]').should('be.visible').click();
cy.get('.ant-select-item-option-content').contains('Hour').click();
cy.get('[data-testid="deploy-button"]').should('be.visible').click(); cy.get('[data-testid="deploy-button"]').should('be.visible').click();
// check success // check success

View File

@ -35,7 +35,10 @@ describe('Kafka Ingestion', () => {
goToAddNewServicePage(SERVICE_TYPE.Messaging); goToAddNewServicePage(SERVICE_TYPE.Messaging);
// Select Dashboard services // Select Dashboard services
cy.get('[data-testid="service-category"]').select('messagingServices'); cy.get('[data-testid="service-category"]').should('be.visible').click();
cy.get('.ant-select-item-option-content')
.contains('Messaging Services')
.click();
const connectionInput = () => { const connectionInput = () => {
cy.get('#root_bootstrapServers').type( cy.get('#root_bootstrapServers').type(

View File

@ -35,7 +35,10 @@ describe('Metabase Ingestion', () => {
goToAddNewServicePage(SERVICE_TYPE.Dashboard); goToAddNewServicePage(SERVICE_TYPE.Dashboard);
// Select Dashboard services // Select Dashboard services
cy.get('[data-testid="service-category"]').select('dashboardServices'); cy.get('[data-testid="service-category"]').should('be.visible').click();
cy.get('.ant-select-item-option-content')
.contains('Dashboard Services')
.click();
const connectionInput = () => { const connectionInput = () => {
cy.get('#root_username').type(Cypress.env('metabaseUsername')); cy.get('#root_username').type(Cypress.env('metabaseUsername'));

View File

@ -35,7 +35,10 @@ describe('Superset Ingestion', () => {
goToAddNewServicePage(SERVICE_TYPE.Dashboard); goToAddNewServicePage(SERVICE_TYPE.Dashboard);
// Select Dashboard services // Select Dashboard services
cy.get('[data-testid="service-category"]').select('dashboardServices'); cy.get('[data-testid="service-category"]').should('be.visible').click();
cy.get('.ant-select-item-option-content')
.contains('Dashboard Services')
.click();
const connectionInput = () => { const connectionInput = () => {
cy.get('#root_username').type(Cypress.env('supersetUsername')); cy.get('#root_username').type(Cypress.env('supersetUsername'));

View File

@ -11,8 +11,8 @@
* limitations under the License. * limitations under the License.
*/ */
import { LOADING_STATE } from 'enums/common.enum';
import { isEmpty, isUndefined, omit, trim } from 'lodash'; import { isEmpty, isUndefined, omit, trim } from 'lodash';
import { LoadingState } from 'Models';
import React, { import React, {
Reducer, Reducer,
useCallback, useCallback,
@ -92,7 +92,6 @@ const AddIngestion = ({
status, status,
}: AddIngestionProps) => { }: AddIngestionProps) => {
const { t } = useTranslation(); const { t } = useTranslation();
console.log('data:', data);
const { sourceConfig, sourceConfigType } = useMemo( const { sourceConfig, sourceConfigType } = useMemo(
() => ({ () => ({
sourceConfig: data?.sourceConfig.config as ConfigClass, sourceConfig: data?.sourceConfig.config as ConfigClass,
@ -204,7 +203,9 @@ const AddIngestion = ({
Reducer<AddIngestionState, Partial<AddIngestionState>> Reducer<AddIngestionState, Partial<AddIngestionState>>
>(reducerWithoutAction, initialState); >(reducerWithoutAction, initialState);
const [saveState, setSaveState] = useState<LoadingState>('initial'); const [saveState, setSaveState] = useState<LOADING_STATE>(
LOADING_STATE.INITIAL
);
const [showDeployModal, setShowDeployModal] = useState(false); const [showDeployModal, setShowDeployModal] = useState(false);
const handleStateChange = useCallback( const handleStateChange = useCallback(
@ -494,6 +495,7 @@ const AddIngestion = ({
}; };
const createNewIngestion = () => { const createNewIngestion = () => {
setSaveState(LOADING_STATE.WAITING);
const { repeatFrequency, enableDebugLog, ingestionName } = state; const { repeatFrequency, enableDebugLog, ingestionName } = state;
const ingestionDetails: CreateIngestionPipeline = { const ingestionDetails: CreateIngestionPipeline = {
airflowConfig: { airflowConfig: {
@ -534,6 +536,7 @@ const AddIngestion = ({
// ignore since error is displayed in toast in the parent promise // ignore since error is displayed in toast in the parent promise
}) })
.finally(() => { .finally(() => {
setTimeout(() => setSaveState(LOADING_STATE.INITIAL), 500);
setTimeout(() => setShowDeployModal(false), 500); setTimeout(() => setShowDeployModal(false), 500);
}); });
} }
@ -560,11 +563,11 @@ const AddIngestion = ({
}; };
if (onUpdateIngestion) { if (onUpdateIngestion) {
setSaveState('waiting'); setSaveState(LOADING_STATE.WAITING);
setShowDeployModal(true); setShowDeployModal(true);
onUpdateIngestion(updatedData, data, data.id as string, data.name) onUpdateIngestion(updatedData, data, data.id as string, data.name)
.then(() => { .then(() => {
setSaveState('success'); setSaveState(LOADING_STATE.SUCCESS);
if (showSuccessScreen) { if (showSuccessScreen) {
handleNext(); handleNext();
} else { } else {
@ -572,7 +575,7 @@ const AddIngestion = ({
} }
}) })
.finally(() => { .finally(() => {
setTimeout(() => setSaveState('initial'), 500); setTimeout(() => setSaveState(LOADING_STATE.INITIAL), 500);
setTimeout(() => setShowDeployModal(false), 500); setTimeout(() => setShowDeployModal(false), 500);
}); });
} }

View File

@ -11,7 +11,7 @@
* limitations under the License. * limitations under the License.
*/ */
import { Form, InputNumber, Select, Typography } from 'antd'; import { Button, Form, InputNumber, Select, Typography } from 'antd';
import { isNil } from 'lodash'; import { isNil } from 'lodash';
import React, { Fragment, useMemo, useRef } from 'react'; import React, { Fragment, useMemo, useRef } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
@ -22,7 +22,6 @@ import { ServiceCategory } from '../../../enums/service.enum';
import { ProfileSampleType } from '../../../generated/entity/data/table'; import { ProfileSampleType } from '../../../generated/entity/data/table';
import { PipelineType } from '../../../generated/entity/services/ingestionPipelines/ingestionPipeline'; import { PipelineType } from '../../../generated/entity/services/ingestionPipelines/ingestionPipeline';
import { getSeparator } from '../../../utils/CommonUtils'; import { getSeparator } from '../../../utils/CommonUtils';
import { Button } from '../../buttons/Button/Button';
import FilterPattern from '../../common/FilterPattern/FilterPattern'; import FilterPattern from '../../common/FilterPattern/FilterPattern';
import RichTextEditor from '../../common/rich-text-editor/RichTextEditor'; import RichTextEditor from '../../common/rich-text-editor/RichTextEditor';
import { EditorContentRef } from '../../common/rich-text-editor/RichTextEditor.interface'; import { EditorContentRef } from '../../common/rich-text-editor/RichTextEditor.interface';
@ -891,22 +890,19 @@ const ConfigureIngestion = ({
layout="vertical"> layout="vertical">
{getIngestionPipelineFields()} {getIngestionPipelineFields()}
<Field className="tw-flex tw-justify-end"> <Field className="d-flex justify-end">
<Button <Button
className="tw-mr-2" className="m-r-xs"
data-testid="back-button" data-testid="back-button"
size="regular" type="link"
theme="primary"
variant="text"
onClick={onCancel}> onClick={onCancel}>
<span>{t('label.cancel')}</span> <span>{t('label.cancel')}</span>
</Button> </Button>
<Button <Button
className="font-medium p-x-md p-y-xxs h-auto rounded-6"
data-testid="next-button" data-testid="next-button"
size="regular" type="primary"
theme="primary"
variant="contained"
onClick={handleNext}> onClick={handleNext}>
<span>{t('label.next')}</span> <span>{t('label.next')}</span>
</Button> </Button>

View File

@ -12,12 +12,11 @@
*/ */
import { CheckOutlined } from '@ant-design/icons'; import { CheckOutlined } from '@ant-design/icons';
import { Button, Col, Row } from 'antd';
import { LOADING_STATE } from 'enums/common.enum';
import React from 'react'; import React from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { Button } from '../../buttons/Button/Button';
import CronEditor from '../../common/CronEditor/CronEditor'; import CronEditor from '../../common/CronEditor/CronEditor';
import { Field } from '../../Field/Field';
import Loader from '../../Loader/Loader';
import { ScheduleIntervalProps } from '../addIngestion.interface'; import { ScheduleIntervalProps } from '../addIngestion.interface';
const ScheduleInterval = ({ const ScheduleInterval = ({
@ -36,8 +35,8 @@ const ScheduleInterval = ({
const { t } = useTranslation(); const { t } = useTranslation();
return ( return (
<div data-testid="schedule-intervel-container"> <Row data-testid="schedule-intervel-container">
<Field> <Col span={24}>
<div> <div>
<CronEditor <CronEditor
includePeriodOptions={includePeriodOptions} includePeriodOptions={includePeriodOptions}
@ -45,48 +44,35 @@ const ScheduleInterval = ({
onChange={handleRepeatFrequencyChange} onChange={handleRepeatFrequencyChange}
/> />
</div> </div>
</Field> </Col>
<Field className="tw-flex tw-justify-end tw-mt-5"> <Col className="d-flex justify-end mt-4" span={24}>
<Button <Button
className="tw-mr-2" className="m-r-xs"
data-testid="back-button" data-testid="back-button"
size="regular" type="link"
theme="primary"
variant="text"
onClick={onBack}> onClick={onBack}>
<span>{t('label.back')}</span> <span>{t('label.back')}</span>
</Button> </Button>
{status === 'waiting' ? ( {status === 'success' ? (
<Button <Button
disabled disabled
className="tw-w-16 tw-h-10 disabled:tw-opacity-100" className="w-16 opacity-100 p-x-md p-y-xxs"
size="regular" type="primary">
theme="primary"
variant="contained">
<Loader size="small" type="white" />
</Button>
) : status === 'success' ? (
<Button
disabled
className="tw-w-16 tw-h-10 disabled:tw-opacity-100"
size="regular"
theme="primary"
variant="contained">
<CheckOutlined /> <CheckOutlined />
</Button> </Button>
) : ( ) : (
<Button <Button
className="font-medium p-x-md p-y-xxs h-auto rounded-6"
data-testid="deploy-button" data-testid="deploy-button"
size="regular" loading={status === LOADING_STATE.WAITING}
theme="primary" type="primary"
variant="contained"
onClick={onDeploy}> onClick={onDeploy}>
<span>{submitButtonLabel}</span> {submitButtonLabel}
</Button> </Button>
)} )}
</Field> </Col>
</div> </Row>
); );
}; };

View File

@ -11,22 +11,22 @@
* limitations under the License. * limitations under the License.
*/ */
import { Button, Col, Row, Select } from 'antd';
import classNames from 'classnames'; import classNames from 'classnames';
import { t } from 'i18next';
import { startCase } from 'lodash'; import { startCase } from 'lodash';
import React, { useEffect, useMemo, useState } from 'react'; import React, { useEffect, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { import {
excludedService, excludedService,
serviceTypes, serviceTypes,
SERVICE_CATEGORY_OPTIONS,
} from '../../../constants/Services.constant'; } from '../../../constants/Services.constant';
import { ServiceCategory } from '../../../enums/service.enum'; import { ServiceCategory } from '../../../enums/service.enum';
import { MetadataServiceType } from '../../../generated/entity/services/metadataService'; import { MetadataServiceType } from '../../../generated/entity/services/metadataService';
import { MlModelServiceType } from '../../../generated/entity/services/mlmodelService'; import { MlModelServiceType } from '../../../generated/entity/services/mlmodelService';
import { errorMsg, getServiceLogo } from '../../../utils/CommonUtils'; import { errorMsg, getServiceLogo } from '../../../utils/CommonUtils';
import SVGIcons, { Icons } from '../../../utils/SvgUtils'; import SVGIcons, { Icons } from '../../../utils/SvgUtils';
import { Button } from '../../buttons/Button/Button';
import Searchbar from '../../common/searchbar/Searchbar'; import Searchbar from '../../common/searchbar/Searchbar';
import { Field } from '../../Field/Field';
import { SelectServiceTypeProps } from './Steps.interface'; import { SelectServiceTypeProps } from './Steps.interface';
const SelectServiceType = ({ const SelectServiceType = ({
@ -38,6 +38,7 @@ const SelectServiceType = ({
onCancel, onCancel,
onNext, onNext,
}: SelectServiceTypeProps) => { }: SelectServiceTypeProps) => {
const { t } = useTranslation();
const [category, setCategory] = useState(''); const [category, setCategory] = useState('');
const [connectorSearchTerm, setConnectorSearchTerm] = useState(''); const [connectorSearchTerm, setConnectorSearchTerm] = useState('');
const [selectedConnectors, setSelectedConnectors] = useState<string[]>([]); const [selectedConnectors, setSelectedConnectors] = useState<string[]>([]);
@ -77,37 +78,30 @@ const SelectServiceType = ({
return ( return (
<div> <div>
<Field> <Row>
<select <Col span={24}>
className="tw-form-inputs tw-form-inputs-padding" <Select
className="w-full"
data-testid="service-category" data-testid="service-category"
id="serviceCategory" id="serviceCategory"
name="serviceCategory" options={SERVICE_CATEGORY_OPTIONS}
value={category} value={category}
onChange={(e) => { onChange={(value) => {
setConnectorSearchTerm(''); setConnectorSearchTerm('');
serviceCategoryHandler(e.target.value as ServiceCategory); serviceCategoryHandler(value as ServiceCategory);
}}> }}
{Object.values(ServiceCategory).map((option, i) => ( />
<option key={i} value={option}> </Col>
{startCase(option)} <Col className="m-t-lg" span={24}>
</option>
))}
</select>
</Field>
<Field className="tw-mt-7">
<Field>
<Searchbar <Searchbar
removeMargin removeMargin
placeholder={`${t('label.search-for-type', { placeholder={t('label.search-for-type', {
type: t('label.connector'), type: t('label.connector'),
})}...`} })}
searchValue={connectorSearchTerm} searchValue={connectorSearchTerm}
typingInterval={500} typingInterval={500}
onSearch={handleConnectorSearchTerm} onSearch={handleConnectorSearchTerm}
/> />
</Field>
<div className="tw-flex"> <div className="tw-flex">
<div <div
className="tw-grid tw-grid-cols-6 tw-grid-flow-row tw-gap-4 tw-mt-4" className="tw-grid tw-grid-cols-6 tw-grid-flow-row tw-gap-4 tw-mt-4"
@ -129,7 +123,10 @@ const SelectServiceType = ({
</div> </div>
<div className="tw-absolute tw-top-0 tw-right-1.5"> <div className="tw-absolute tw-top-0 tw-right-1.5">
{type === selectServiceType && ( {type === selectServiceType && (
<SVGIcons alt="checkbox" icon={Icons.CHECKBOX_PRIMARY} /> <SVGIcons
alt="checkbox"
icon={Icons.CHECKBOX_PRIMARY}
/>
)} )}
</div> </div>
</div> </div>
@ -146,27 +143,26 @@ const SelectServiceType = ({
fieldText: t('label.service'), fieldText: t('label.service'),
}) })
)} )}
</Field> </Col>
<Field className="tw-flex tw-justify-end tw-mt-10">
<Col className="d-flex justify-end mt-12" span={24}>
<Button <Button
className={classNames('tw-mr-2')} className="m-r-xs"
data-testid="previous-button" data-testid="previous-button"
size="regular" type="link"
theme="primary"
variant="text"
onClick={onCancel}> onClick={onCancel}>
<span>{t('label.cancel')}</span> {t('label.cancel')}
</Button> </Button>
<Button <Button
className="font-medium p-x-md p-y-xxs h-auto rounded-6"
data-testid="next-button" data-testid="next-button"
size="regular" type="primary"
theme="primary"
variant="contained"
onClick={onNext}> onClick={onNext}>
<span>{t('label.next')}</span> {t('label.next')}
</Button> </Button>
</Field> </Col>
</Row>
</div> </div>
); );
}; };

View File

@ -196,3 +196,10 @@ export const getMonthCron = (value: any) => {
export const getYearCron = (value: any) => { export const getYearCron = (value: any) => {
return `${value.min} ${value.hour} ${value.dom} ${value.mon} *`; return `${value.min} ${value.hour} ${value.dom} ${value.mon} *`;
}; };
export const SELECTED_PERIOD_OPTIONS = {
hour: 'selectedHourOption',
day: 'selectedDayOption',
week: 'selectedWeekOption',
minute: 'selectedMinuteOption',
};

View File

@ -90,3 +90,5 @@ export interface CronEditorProp {
disabled?: boolean; disabled?: boolean;
includePeriodOptions?: string[]; includePeriodOptions?: string[];
} }
export type CronType = 'minute' | 'hour' | 'day' | 'week';

View File

@ -11,7 +11,16 @@
* limitations under the License. * limitations under the License.
*/ */
import { act, render, screen } from '@testing-library/react'; import {
act,
findByRole,
fireEvent,
getByText,
getByTitle,
render,
screen,
waitForElement,
} from '@testing-library/react';
import userEvent from '@testing-library/user-event'; import userEvent from '@testing-library/user-event';
import React from 'react'; import React from 'react';
import CronEditor from './CronEditor'; import CronEditor from './CronEditor';
@ -21,6 +30,35 @@ const mockProps: CronEditorProp = {
onChange: jest.fn, onChange: jest.fn,
}; };
const getHourDescription = (value: string) =>
`label.schedule-to-run-every hour ${value} past the hour`;
const getMinuteDescription = (value: string) =>
`label.schedule-to-run-every ${value}`;
const getDayDescription = () => 'label.schedule-to-run-every day at 00:00';
const handleScheduleEverySelector = async (text: string) => {
const everyDropdown = await screen.findByTestId('time-dropdown-container');
expect(everyDropdown).toBeInTheDocument();
const cronSelect = await findByRole(everyDropdown, 'combobox');
act(() => {
userEvent.click(cronSelect);
});
await waitForElement(
async () => await expect(screen.getByText(text)).toBeInTheDocument()
);
await act(async () => {
fireEvent.click(screen.getByText(text));
});
await waitForElement(
async () => await expect(getByText(everyDropdown, text)).toBeInTheDocument()
);
};
describe('Test CronEditor component', () => { describe('Test CronEditor component', () => {
it('CronEditor component should render', async () => { it('CronEditor component should render', async () => {
render(<CronEditor {...mockProps} />); render(<CronEditor {...mockProps} />);
@ -35,46 +73,79 @@ describe('Test CronEditor component', () => {
it('Hour option should render corresponding component', async () => { it('Hour option should render corresponding component', async () => {
render(<CronEditor disabled={false} onChange={jest.fn} />); render(<CronEditor disabled={false} onChange={jest.fn} />);
const cronType = await screen.findByTestId('cron-type'); await handleScheduleEverySelector('label.hour');
userEvent.selectOptions(cronType, 'hour');
expect(screen.getByTestId('schedule-description')).toHaveTextContent(
getHourDescription('0 minute')
);
expect( expect(
await screen.findByTestId('hour-segment-container') await screen.findByTestId('hour-segment-container')
).toBeInTheDocument(); ).toBeInTheDocument();
const minutOptions = await screen.findByTestId('minute-options'); const minutesOptions = await screen.findByTestId('minute-options');
expect(minutOptions).toBeInTheDocument(); expect(minutesOptions).toBeInTheDocument();
userEvent.selectOptions(minutOptions, '10'); const minuteSelect = await findByRole(minutesOptions, 'combobox');
expect(await screen.findByText('10')).toBeInTheDocument(); act(() => {
userEvent.click(minuteSelect);
});
await waitForElement(() => screen.getByText('03'));
await act(async () => {
fireEvent.click(screen.getByText('03'));
}); });
it('Minute option should render corrosponding component', async () => { expect(await getByTitle(minutesOptions, '03')).toBeInTheDocument();
expect(screen.getByTestId('schedule-description')).toHaveTextContent(
getHourDescription('3 minutes')
);
});
it('Minute option should render corresponding component', async () => {
render(<CronEditor disabled={false} onChange={jest.fn} />); render(<CronEditor disabled={false} onChange={jest.fn} />);
const cronType = await screen.findByTestId('cron-type'); await handleScheduleEverySelector('label.minute-plural');
userEvent.selectOptions(cronType, 'minute');
expect(screen.getByTestId('schedule-description')).toHaveTextContent(
getMinuteDescription('5')
);
expect( expect(
await screen.findByTestId('minute-segment-container') await screen.findByTestId('minute-segment-container')
).toBeInTheDocument(); ).toBeInTheDocument();
const minutOptions = await screen.findByTestId('minute-segment-options'); const minutesOptions = await screen.findByTestId('minute-segment-options');
expect(minutOptions).toBeInTheDocument(); expect(minutesOptions).toBeInTheDocument();
userEvent.selectOptions(minutOptions, '10'); const minuteSelect = await findByRole(minutesOptions, 'combobox');
expect(await screen.findByText('10')).toBeInTheDocument(); act(() => {
userEvent.click(minuteSelect);
});
await waitForElement(() => screen.getByText('15'));
await act(async () => {
fireEvent.click(screen.getByText('15'));
});
expect(await screen.getAllByText('15')).toHaveLength(2);
expect(screen.getByTestId('schedule-description')).toHaveTextContent(
getMinuteDescription('15')
);
}); });
it('Day option should render corresponding component', async () => { it('Day option should render corresponding component', async () => {
render(<CronEditor disabled={false} onChange={jest.fn} />); render(<CronEditor disabled={false} onChange={jest.fn} />);
const cronType = await screen.findByTestId('cron-type'); await handleScheduleEverySelector('label.day');
userEvent.selectOptions(cronType, 'day');
expect(screen.getByTestId('schedule-description')).toHaveTextContent(
getDayDescription()
);
expect( expect(
await screen.findByTestId('day-segment-container') await screen.findByTestId('day-segment-container')
@ -83,24 +154,37 @@ describe('Test CronEditor component', () => {
await screen.findByTestId('time-option-container') await screen.findByTestId('time-option-container')
).toBeInTheDocument(); ).toBeInTheDocument();
const minutOptions = await screen.findByTestId('minute-options'); // For Hours Selector
const hourOptions = await screen.findByTestId('hour-options'); const hourOptions = await screen.findByTestId('hour-options');
expect(minutOptions).toBeInTheDocument();
expect(hourOptions).toBeInTheDocument(); expect(hourOptions).toBeInTheDocument();
userEvent.selectOptions(minutOptions, '10'); const hourSelect = await findByRole(hourOptions, 'combobox');
userEvent.selectOptions(hourOptions, '2'); act(() => {
userEvent.click(hourSelect);
});
expect(await screen.findAllByText('10')).toHaveLength(2); await waitForElement(() => screen.getByText('01'));
expect(await screen.findAllByText('02')).toHaveLength(2); await act(async () => {
fireEvent.click(screen.getByText('01'));
});
expect(await getByTitle(hourOptions, '01')).toBeInTheDocument();
// For Minute Selector
const minutesOptions = await screen.findByTestId('minute-options');
expect(minutesOptions).toBeInTheDocument();
}); });
it('week option should render corresponding component', async () => { it('week option should render corresponding component', async () => {
render(<CronEditor disabled={false} onChange={jest.fn} />); render(<CronEditor disabled={false} onChange={jest.fn} />);
const cronType = await screen.findByTestId('cron-type'); await handleScheduleEverySelector('label.week');
userEvent.selectOptions(cronType, 'week');
expect(screen.getByTestId('schedule-description')).toHaveTextContent(
'label.schedule-to-run-every week on label.monday at 00:00'
);
expect( expect(
await screen.findByTestId('week-segment-time-container') await screen.findByTestId('week-segment-time-container')
@ -112,17 +196,35 @@ describe('Test CronEditor component', () => {
await screen.findByTestId('week-segment-day-option-container') await screen.findByTestId('week-segment-day-option-container')
).toBeInTheDocument(); ).toBeInTheDocument();
const minutOptions = await screen.findByTestId('minute-options'); // For Hours Selector
const hourOptions = await screen.findByTestId('hour-options'); const hourOptions = await screen.findByTestId('hour-options');
expect(minutOptions).toBeInTheDocument();
expect(hourOptions).toBeInTheDocument(); expect(hourOptions).toBeInTheDocument();
userEvent.selectOptions(minutOptions, '10'); const hourSelect = await findByRole(hourOptions, 'combobox');
userEvent.selectOptions(hourOptions, '2'); act(() => {
userEvent.click(hourSelect);
});
expect(await screen.findAllByText('10')).toHaveLength(2); await waitForElement(() => screen.getByText('10'));
expect(await screen.findAllByText('02')).toHaveLength(2); await act(async () => {
fireEvent.click(screen.getByText('10'));
});
expect(await getByTitle(hourOptions, '10')).toBeInTheDocument();
// For Minute Selector
const minutesOptions = await screen.findByTestId('minute-options');
expect(minutesOptions).toBeInTheDocument();
// For Days Selector
const daysContainer = await screen.findByTestId(
'week-segment-day-option-container'
);
expect(daysContainer).toBeInTheDocument();
}); });
it('None option should render corresponding component', async () => { it('None option should render corresponding component', async () => {

View File

@ -11,6 +11,7 @@
* limitations under the License. * limitations under the License.
*/ */
import { Select } from 'antd';
import { isEmpty, toNumber } from 'lodash'; import { isEmpty, toNumber } from 'lodash';
import React, { FC, useMemo, useState } from 'react'; import React, { FC, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
@ -25,12 +26,14 @@ import {
getMonthDaysOptions, getMonthDaysOptions,
getMonthOptions, getMonthOptions,
getPeriodOptions, getPeriodOptions,
SELECTED_PERIOD_OPTIONS,
toDisplay, toDisplay,
} from './CronEditor.constant'; } from './CronEditor.constant';
import { import {
Combination, Combination,
CronEditorProp, CronEditorProp,
CronOption, CronOption,
CronType,
CronValue, CronValue,
SelectedDayOption, SelectedDayOption,
SelectedHourOption, SelectedHourOption,
@ -41,7 +44,6 @@ import {
const CronEditor: FC<CronEditorProp> = (props) => { const CronEditor: FC<CronEditorProp> = (props) => {
const { t } = useTranslation(); const { t } = useTranslation();
const getCronType = (cronStr: string) => { const getCronType = (cronStr: string) => {
for (const c in combinations) { for (const c in combinations) {
if (combinations[c as keyof Combination].test(cronStr)) { if (combinations[c as keyof Combination].test(cronStr)) {
@ -94,10 +96,9 @@ const CronEditor: FC<CronEditorProp> = (props) => {
stateVal.selectedPeriod = cronType || stateVal.selectedPeriod; stateVal.selectedPeriod = cronType || stateVal.selectedPeriod;
if (!isEmpty(t)) { if (!isEmpty(cronType)) {
const stateIndex = `${t('label.selected-lowercase')}${cronType const stateIndex =
?.charAt(0) SELECTED_PERIOD_OPTIONS[(cronType as CronType) || 'hour'];
.toUpperCase()}${cronType?.substring(1)}${t('label.option')}`;
const selectedPeriodObj = stateVal[ const selectedPeriodObj = stateVal[
stateIndex as keyof StateValue stateIndex as keyof StateValue
] as SelectedYearOption; ] as SelectedYearOption;
@ -154,16 +155,12 @@ const CronEditor: FC<CronEditorProp> = (props) => {
onChange(getCron(state) ?? ''); onChange(getCron(state) ?? '');
}; };
const onPeriodSelect = (event: React.ChangeEvent<HTMLSelectElement>) => { const onPeriodSelect = (value: string) => {
changeValue({ ...state, selectedPeriod: event.target.value }); changeValue({ ...state, selectedPeriod: value });
setState((prev) => ({ ...prev, selectedPeriod: event.target.value })); setState((prev) => ({ ...prev, selectedPeriod: value }));
}; };
const onHourOptionSelect = ( const onHourOptionSelect = (value: number, key: string) => {
event: React.ChangeEvent<HTMLSelectElement>,
key: string
) => {
const value = event.target.value;
const obj = { [key]: value }; const obj = { [key]: value };
const { selectedHourOption } = state; const { selectedHourOption } = state;
@ -172,12 +169,8 @@ const CronEditor: FC<CronEditorProp> = (props) => {
setState((prev) => ({ ...prev, selectedHourOption: hourOption })); setState((prev) => ({ ...prev, selectedHourOption: hourOption }));
}; };
const onMinOptionSelect = ( const onMinOptionSelect = (value: number, key: string) => {
event: React.ChangeEvent<HTMLSelectElement>, const obj = { [key]: value };
key: string
) => {
const selectedValue = event.target.value;
const obj = { [key]: selectedValue };
const { selectedMinOption } = state; const { selectedMinOption } = state;
const minOption = Object.assign({}, selectedMinOption, obj); const minOption = Object.assign({}, selectedMinOption, obj);
@ -185,11 +178,7 @@ const CronEditor: FC<CronEditorProp> = (props) => {
setState((prev) => ({ ...prev, selectedMinOption: minOption })); setState((prev) => ({ ...prev, selectedMinOption: minOption }));
}; };
const onDayOptionSelect = ( const onDayOptionSelect = (value: number, key: string) => {
event: React.ChangeEvent<HTMLSelectElement>,
key: string
) => {
const value = parseInt(event.target.value);
const obj = { [key]: value }; const obj = { [key]: value };
const { selectedDayOption } = state; const { selectedDayOption } = state;
@ -198,13 +187,10 @@ const CronEditor: FC<CronEditorProp> = (props) => {
setState((prev) => ({ ...prev, selectedDayOption: dayOption })); setState((prev) => ({ ...prev, selectedDayOption: dayOption }));
}; };
const onWeekOptionSelect = ( const onWeekOptionSelect = (value: number, key: string) => {
event: React.ChangeEvent<HTMLSelectElement>, const obj = {
key: string [key]: value,
) => { };
const value = event.target.value || event.target.dataset.value;
const numberValue = value ? parseInt(value) : '';
const obj = { [key]: numberValue };
const { selectedWeekOption } = state; const { selectedWeekOption } = state;
const weekOption = Object.assign({}, selectedWeekOption, obj); const weekOption = Object.assign({}, selectedWeekOption, obj);
@ -212,11 +198,7 @@ const CronEditor: FC<CronEditorProp> = (props) => {
setState((prev) => ({ ...prev, selectedWeekOption: weekOption })); setState((prev) => ({ ...prev, selectedWeekOption: weekOption }));
}; };
const onMonthOptionSelect = ( const onMonthOptionSelect = (value: number, key: string) => {
event: React.ChangeEvent<HTMLSelectElement>,
key: string
) => {
const value = event.target.value || event.target.dataset.value;
const obj = { [key]: value }; const obj = { [key]: value };
const { selectedMonthOption } = state; const { selectedMonthOption } = state;
@ -225,12 +207,10 @@ const CronEditor: FC<CronEditorProp> = (props) => {
setState((prev) => ({ ...prev, selectedMonthOption: monthOption })); setState((prev) => ({ ...prev, selectedMonthOption: monthOption }));
}; };
const onYearOptionSelect = ( const onYearOptionSelect = (value: number, key: string) => {
event: React.ChangeEvent<HTMLSelectElement>, const obj = {
key: string [key]: value,
) => { };
const value = event.target.value || event.target.dataset.value;
const obj = { [key]: value };
const { selectedYearOption } = state; const { selectedYearOption } = state;
const yearOption = Object.assign({}, selectedYearOption, obj); const yearOption = Object.assign({}, selectedYearOption, obj);
@ -238,20 +218,16 @@ const CronEditor: FC<CronEditorProp> = (props) => {
setState((prev) => ({ ...prev, selectedYearOption: yearOption })); setState((prev) => ({ ...prev, selectedYearOption: yearOption }));
}; };
const getOptionComponent = (key: string) => { const getOptionComponent = () => {
const optionRenderer = (o: CronOption, i: number) => { const optionRenderer = (o: CronOption) => {
return ( return { label: o.label, value: o.value };
<option key={`${key}_${i}`} value={o.value}>
{o.label}
</option>
);
}; };
return optionRenderer; return optionRenderer;
}; };
const getTextComp = (str: string) => { const getTextComp = (str: string) => {
return <div>{str}</div>; return <div data-testid="schedule-description">{str}</div>;
}; };
const findHourOption = (hour: number) => { const findHourOption = (hour: number) => {
@ -268,61 +244,55 @@ const CronEditor: FC<CronEditorProp> = (props) => {
const getHourSelect = ( const getHourSelect = (
selectedOption: SelectedDayOption, selectedOption: SelectedDayOption,
onChangeCB: (e: React.ChangeEvent<HTMLSelectElement>) => void onChangeCB: (value: number) => void
) => { ) => {
const { disabled } = props; const { disabled } = props;
return ( return (
<select <Select
className="tw-form-inputs tw-py-1 tw-px-1" className="w-full"
data-testid="hour-options" data-testid="hour-options"
disabled={disabled} disabled={disabled}
id="hour-select"
options={hourOptions.map(getOptionComponent())}
value={selectedOption.hour} value={selectedOption.hour}
onChange={(e) => { onChange={onChangeCB}
e.persist(); />
onChangeCB(e);
}}>
{hourOptions.map(getOptionComponent('hour_option'))}
</select>
); );
}; };
const getMinuteSelect = ( const getMinuteSelect = (
selectedOption: SelectedHourOption, selectedOption: SelectedHourOption,
onChangeCB: (e: React.ChangeEvent<HTMLSelectElement>) => void onChangeCB: (value: number) => void
) => { ) => {
const { disabled } = props; const { disabled } = props;
return ( return (
<select <Select
className="tw-form-inputs tw-py-1 tw-px-1" className="w-full"
data-testid="minute-options" data-testid="minute-options"
disabled={disabled} disabled={disabled}
id="minute-select"
options={minuteOptions.map(getOptionComponent())}
value={selectedOption.min} value={selectedOption.min}
onChange={(e) => { onChange={onChangeCB}
e.persist(); />
onChangeCB(e);
}}>
{minuteOptions.map(getOptionComponent('minute_option'))}
</select>
); );
}; };
const getMinuteSegmentSelect = ( const getMinuteSegmentSelect = (
selectedOption: SelectedHourOption, selectedOption: SelectedHourOption,
onChangeCB: (e: React.ChangeEvent<HTMLSelectElement>) => void onChangeCB: (value: number) => void
) => { ) => {
return ( return (
<select <Select
className="tw-form-inputs tw-py-1 tw-px-1" className="w-full"
data-testid="minute-segment-options" data-testid="minute-segment-options"
disabled={props.disabled} disabled={props.disabled}
id="minute-segment-select"
options={minuteSegmentOptions.map(getOptionComponent())}
value={selectedOption.min} value={selectedOption.min}
onChange={(e) => { onChange={onChangeCB}
e.persist(); />
onChangeCB(e);
}}>
{minuteSegmentOptions.map(getOptionComponent('minute_option'))}
</select>
); );
}; };
@ -330,36 +300,30 @@ const CronEditor: FC<CronEditorProp> = (props) => {
options: CronOption[], options: CronOption[],
value: number, value: number,
substrVal: number, substrVal: number,
// eslint-disable-next-line @typescript-eslint/no-explicit-any onClick: (value: number) => void
onClick: (e: any) => void ) =>
) => { options.map(({ label, value: optionValue }, index) => {
const { disabled } = props; let strVal = label;
const optionComps: JSX.Element[] = [];
options.forEach((o, i) => {
let strVal = o.label;
if (substrVal) { if (substrVal) {
strVal = strVal.substr(0, substrVal); strVal = strVal.substr(0, substrVal);
} }
const comp = (
return (
<span <span
className={`cron-badge-option ${o.value === value ? 'active' : ''} ${ className={`cron-badge-option ${
disabled || !onClick ? 'disabled' : '' optionValue === value ? 'active' : ''
}`} } ${props.disabled || !onClick ? 'disabled' : ''}`}
data-value={o.value} data-value={optionValue}
key={i} key={index}
onClick={(e) => onClick?.(e)}> onClick={() => {
onClick?.(Number(optionValue));
}}>
{strVal} {strVal}
</span> </span>
); );
optionComps.push(comp);
}); });
return optionComps;
};
const getMinuteComponent = (cronPeriodString: string) => { const getMinuteComponent = (cronPeriodString: string) => {
const { selectedMinOption } = state; const { selectedMinOption } = state;
@ -367,11 +331,9 @@ const CronEditor: FC<CronEditorProp> = (props) => {
state.selectedPeriod === 'minute' && ( state.selectedPeriod === 'minute' && (
<> <>
<div className="tw-mb-1.5" data-testid="minute-segment-container"> <div className="tw-mb-1.5" data-testid="minute-segment-container">
<label>{`${t('label.minute-lowercase')}:`}</label> <label>{`${t('label.minute')}:`}</label>
{getMinuteSegmentSelect( {getMinuteSegmentSelect(selectedMinOption, (value: number) =>
selectedMinOption, onMinOptionSelect(value, 'min')
(e: React.ChangeEvent<HTMLSelectElement>) =>
onMinOptionSelect(e, 'min')
)} )}
</div> </div>
<div className="tw-col-span-2"> <div className="tw-col-span-2">
@ -391,11 +353,9 @@ const CronEditor: FC<CronEditorProp> = (props) => {
state.selectedPeriod === 'hour' && ( state.selectedPeriod === 'hour' && (
<> <>
<div className="tw-mb-1.5" data-testid="hour-segment-container"> <div className="tw-mb-1.5" data-testid="hour-segment-container">
<label>{`${t('label.minute-lowercase')}:`}</label> <label>{`${t('label.minute')}:`}</label>
{getMinuteSelect( {getMinuteSelect(selectedHourOption, (value: number) =>
selectedHourOption, onHourOptionSelect(value, 'min')
(e: React.ChangeEvent<HTMLSelectElement>) =>
onHourOptionSelect(e, 'min')
)} )}
</div> </div>
<div className="tw-col-span-2"> <div className="tw-col-span-2">
@ -423,16 +383,12 @@ const CronEditor: FC<CronEditorProp> = (props) => {
<div className="tw-mb-1.5" data-testid="day-segment-container"> <div className="tw-mb-1.5" data-testid="day-segment-container">
<label>{`${t('label.time')}:`}</label> <label>{`${t('label.time')}:`}</label>
<div className="tw-flex" data-testid="time-option-container"> <div className="tw-flex" data-testid="time-option-container">
{getHourSelect( {getHourSelect(selectedDayOption, (value: number) =>
selectedDayOption, onDayOptionSelect(value, 'hour')
(e: React.ChangeEvent<HTMLSelectElement>) =>
onDayOptionSelect(e, 'hour')
)} )}
<span className="tw-mx-2 tw-self-center">:</span> <span className="tw-mx-2 tw-self-center">:</span>
{getMinuteSelect( {getMinuteSelect(selectedDayOption, (value: number) =>
selectedDayOption, onDayOptionSelect(value, 'min')
(e: React.ChangeEvent<HTMLSelectElement>) =>
onDayOptionSelect(e, 'min')
)} )}
</div> </div>
</div> </div>
@ -462,16 +418,12 @@ const CronEditor: FC<CronEditorProp> = (props) => {
<div <div
className="tw-flex" className="tw-flex"
data-testid="week-segment-time-options-container"> data-testid="week-segment-time-options-container">
{getHourSelect( {getHourSelect(selectedWeekOption, (value: number) =>
selectedWeekOption, onWeekOptionSelect(value, 'hour')
(e: React.ChangeEvent<HTMLSelectElement>) =>
onWeekOptionSelect(e, 'hour')
)} )}
<span className="tw-mx-2 tw-self-center">:</span> <span className="tw-mx-2 tw-self-center">:</span>
{getMinuteSelect( {getMinuteSelect(selectedWeekOption, (value: number) =>
selectedWeekOption, onWeekOptionSelect(value, 'min')
(e: React.ChangeEvent<HTMLSelectElement>) =>
onWeekOptionSelect(e, 'min')
)} )}
</div> </div>
</div> </div>
@ -484,8 +436,7 @@ const CronEditor: FC<CronEditorProp> = (props) => {
dayOptions, dayOptions,
selectedWeekOption.dow, selectedWeekOption.dow,
1, 1,
(e: React.ChangeEvent<HTMLSelectElement>) => (value: number) => onWeekOptionSelect(value, 'dow')
onWeekOptionSelect(e, 'dow')
)} )}
</div> </div>
</div> </div>
@ -519,20 +470,17 @@ const CronEditor: FC<CronEditorProp> = (props) => {
monthDaysOptions, monthDaysOptions,
selectedMonthOption.dom, selectedMonthOption.dom,
0, 0,
(e: React.ChangeEvent<HTMLSelectElement>) => (value: number) => onMonthOptionSelect(value, 'dom')
onMonthOptionSelect(e, 'dom')
)} )}
</div> </div>
</div> </div>
<div className="cron-field-row"> <div className="cron-field-row">
<span className="m-l-xs">{`${t('label.time')}:`}</span> <span className="m-l-xs">{`${t('label.time')}:`}</span>
{`${getHourSelect( {`${getHourSelect(selectedMonthOption, (e: number) =>
selectedMonthOption,
(e: React.ChangeEvent<HTMLSelectElement>) =>
onMonthOptionSelect(e, 'hour') onMonthOptionSelect(e, 'hour')
)} : ${getMinuteSelect( )}
selectedMonthOption, :
(e: React.ChangeEvent<HTMLSelectElement>) => ${getMinuteSelect(selectedMonthOption, (e: number) =>
onMonthOptionSelect(e, 'min') onMonthOptionSelect(e, 'min')
)}`} )}`}
</div> </div>
@ -567,8 +515,7 @@ const CronEditor: FC<CronEditorProp> = (props) => {
monthOptions, monthOptions,
selectedYearOption.mon, selectedYearOption.mon,
3, 3,
(e: React.ChangeEvent<HTMLSelectElement>) => (value: number) => onYearOptionSelect(value, 'mon')
onYearOptionSelect(e, 'mon')
)} )}
</div> </div>
</div> </div>
@ -579,23 +526,18 @@ const CronEditor: FC<CronEditorProp> = (props) => {
monthDaysOptions, monthDaysOptions,
selectedYearOption.dom, selectedYearOption.dom,
0, 0,
(e: React.ChangeEvent<HTMLSelectElement>) => (value: number) => onYearOptionSelect(value, 'dom')
onYearOptionSelect(e, 'dom')
)} )}
</div> </div>
</div> </div>
<div className="cron-field-row"> <div className="cron-field-row">
<span className="m-l-xs">{`${t('label.time')}:`}</span> <span className="m-l-xs">{`${t('label.time')}:`}</span>
{`${getHourSelect( {`${getHourSelect(selectedYearOption, (value: number) =>
selectedYearOption, onYearOptionSelect(value, 'hour')
(e: React.ChangeEvent<HTMLSelectElement>) =>
onYearOptionSelect(e, 'hour')
)} )}
: :
${getMinuteSelect( ${getMinuteSelect(selectedYearOption, (value: number) =>
selectedYearOption, onYearOptionSelect(value, 'min')
(e: React.ChangeEvent<HTMLSelectElement>) =>
onYearOptionSelect(e, 'min')
)}`} )}`}
</div> </div>
{getTextComp( {getTextComp(
@ -612,25 +554,18 @@ const CronEditor: FC<CronEditorProp> = (props) => {
<div className="tw-grid tw-grid-cols-2 tw-gap-4"> <div className="tw-grid tw-grid-cols-2 tw-gap-4">
<div className="tw-mb-1.5" data-testid="time-dropdown-container"> <div className="tw-mb-1.5" data-testid="time-dropdown-container">
<label htmlFor="cronType">{`${t('label.every')}:`}</label> <label htmlFor="cronType">{`${t('label.every')}:`}</label>
<select <Select
className="tw-form-inputs tw-px-3 tw-py-1" className="w-full"
data-testid="cron-type" data-testid="cron-type"
disabled={disabled} disabled={disabled}
id="cronType" id="cronType"
name="cronType" options={filteredPeriodOptions.map(({ label, value }) => ({
label,
value,
}))}
value={selectedPeriod} value={selectedPeriod}
onChange={(e) => { onChange={onPeriodSelect}
e.persist(); />
onPeriodSelect(e);
}}>
{filteredPeriodOptions.map((t, index) => {
return (
<option key={`period_option_${index}`} value={t.value}>
{t.label}
</option>
);
})}
</select>
</div> </div>
{getMinuteComponent(startText)} {getMinuteComponent(startText)}

View File

@ -11,6 +11,7 @@
* limitations under the License. * limitations under the License.
*/ */
import { map, startCase } from 'lodash';
import { ServiceTypes } from 'Models'; import { ServiceTypes } from 'Models';
import i18n from 'utils/i18next/LocalUtil'; import i18n from 'utils/i18next/LocalUtil';
import addPlaceHolder from '../assets/img/add-placeholder.svg'; import addPlaceHolder from '../assets/img/add-placeholder.svg';
@ -240,3 +241,8 @@ export const COMMON_UI_SCHEMA = {
export const OPENMETADATA = 'OpenMetadata'; export const OPENMETADATA = 'OpenMetadata';
export const JWT_CONFIG = 'openMetadataJWTClientConfig'; export const JWT_CONFIG = 'openMetadataJWTClientConfig';
export const SERVICE_CATEGORY_OPTIONS = map(ServiceCategory, (value) => ({
label: startCase(value),
value,
}));

View File

@ -455,6 +455,7 @@
"metrics-summary": "Metrics Summary", "metrics-summary": "Metrics Summary",
"min": "Min", "min": "Min",
"minor": "Minor", "minor": "Minor",
"minute": "Minute",
"minute-lowercase": "minute", "minute-lowercase": "minute",
"minute-plural": "Minutes", "minute-plural": "Minutes",
"ml-model": "ML Model", "ml-model": "ML Model",
@ -1078,6 +1079,7 @@
"pipeline-description-message": "Description of the pipeline.", "pipeline-description-message": "Description of the pipeline.",
"pipeline-trigger-success-message": "Pipeline triggered successfully!", "pipeline-trigger-success-message": "Pipeline triggered successfully!",
"pipeline-will-trigger-manually": "Pipeline will only be triggered manually.", "pipeline-will-trigger-manually": "Pipeline will only be triggered manually.",
"pipeline-will-triggered-manually": "Pipeline will only be triggered manually",
"process-pii-sensitive-column-message": "Check column names to auto tag PII Senstive/nonSensitive columns.", "process-pii-sensitive-column-message": "Check column names to auto tag PII Senstive/nonSensitive columns.",
"profile-sample-percentage-message": "Set the Profiler value as percentage", "profile-sample-percentage-message": "Set the Profiler value as percentage",
"profile-sample-row-count-message": " Set the Profiler value as row count", "profile-sample-row-count-message": " Set the Profiler value as row count",
@ -1099,6 +1101,7 @@
"result-limit-message": "Configuration to set the limit for query logs.", "result-limit-message": "Configuration to set the limit for query logs.",
"run-sample-data-to-ingest-sample-data": "'Run sample data to ingest sample data assets into your OpenMetadata.'", "run-sample-data-to-ingest-sample-data": "'Run sample data to ingest sample data assets into your OpenMetadata.'",
"schedule-for-ingestion-description": "Scheduling can be set up at an hourly, daily, or weekly cadence. The timezone is in UTC.", "schedule-for-ingestion-description": "Scheduling can be set up at an hourly, daily, or weekly cadence. The timezone is in UTC.",
"scheduled-run-every": "Scheduled to run every",
"scopes-comma-separated": "Add the Scopes value, separated by commas", "scopes-comma-separated": "Add the Scopes value, separated by commas",
"search-for-entity-types": "Search for Tables, Topics, Dashboards, Pipelines and ML Models.", "search-for-entity-types": "Search for Tables, Topics, Dashboards, Pipelines and ML Models.",
"search-for-ingestion": "Search for ingestion", "search-for-ingestion": "Search for ingestion",