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 = () => {
// Schedule & Deploy
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();
// check success

View File

@ -35,7 +35,10 @@ describe('Kafka Ingestion', () => {
goToAddNewServicePage(SERVICE_TYPE.Messaging);
// 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 = () => {
cy.get('#root_bootstrapServers').type(

View File

@ -35,7 +35,10 @@ describe('Metabase Ingestion', () => {
goToAddNewServicePage(SERVICE_TYPE.Dashboard);
// 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 = () => {
cy.get('#root_username').type(Cypress.env('metabaseUsername'));

View File

@ -35,7 +35,10 @@ describe('Superset Ingestion', () => {
goToAddNewServicePage(SERVICE_TYPE.Dashboard);
// 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 = () => {
cy.get('#root_username').type(Cypress.env('supersetUsername'));

View File

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

View File

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

View File

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

View File

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

View File

@ -196,3 +196,10 @@ export const getMonthCron = (value: any) => {
export const getYearCron = (value: any) => {
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;
includePeriodOptions?: string[];
}
export type CronType = 'minute' | 'hour' | 'day' | 'week';

View File

@ -11,7 +11,16 @@
* 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 React from 'react';
import CronEditor from './CronEditor';
@ -21,6 +30,35 @@ const mockProps: CronEditorProp = {
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', () => {
it('CronEditor component should render', async () => {
render(<CronEditor {...mockProps} />);
@ -35,46 +73,79 @@ describe('Test CronEditor component', () => {
it('Hour option should render corresponding component', async () => {
render(<CronEditor disabled={false} onChange={jest.fn} />);
const cronType = await screen.findByTestId('cron-type');
userEvent.selectOptions(cronType, 'hour');
await handleScheduleEverySelector('label.hour');
expect(screen.getByTestId('schedule-description')).toHaveTextContent(
getHourDescription('0 minute')
);
expect(
await screen.findByTestId('hour-segment-container')
).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'));
});
expect(await getByTitle(minutesOptions, '03')).toBeInTheDocument();
expect(screen.getByTestId('schedule-description')).toHaveTextContent(
getHourDescription('3 minutes')
);
});
it('Minute option should render corrosponding component', async () => {
it('Minute option should render corresponding component', async () => {
render(<CronEditor disabled={false} onChange={jest.fn} />);
const cronType = await screen.findByTestId('cron-type');
userEvent.selectOptions(cronType, 'minute');
await handleScheduleEverySelector('label.minute-plural');
expect(screen.getByTestId('schedule-description')).toHaveTextContent(
getMinuteDescription('5')
);
expect(
await screen.findByTestId('minute-segment-container')
).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 () => {
render(<CronEditor disabled={false} onChange={jest.fn} />);
const cronType = await screen.findByTestId('cron-type');
userEvent.selectOptions(cronType, 'day');
await handleScheduleEverySelector('label.day');
expect(screen.getByTestId('schedule-description')).toHaveTextContent(
getDayDescription()
);
expect(
await screen.findByTestId('day-segment-container')
@ -83,24 +154,37 @@ describe('Test CronEditor component', () => {
await screen.findByTestId('time-option-container')
).toBeInTheDocument();
const minutOptions = await screen.findByTestId('minute-options');
// For Hours Selector
const hourOptions = await screen.findByTestId('hour-options');
expect(minutOptions).toBeInTheDocument();
expect(hourOptions).toBeInTheDocument();
userEvent.selectOptions(minutOptions, '10');
userEvent.selectOptions(hourOptions, '2');
const hourSelect = await findByRole(hourOptions, 'combobox');
act(() => {
userEvent.click(hourSelect);
});
expect(await screen.findAllByText('10')).toHaveLength(2);
expect(await screen.findAllByText('02')).toHaveLength(2);
await waitForElement(() => screen.getByText('01'));
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 () => {
render(<CronEditor disabled={false} onChange={jest.fn} />);
const cronType = await screen.findByTestId('cron-type');
userEvent.selectOptions(cronType, 'week');
await handleScheduleEverySelector('label.week');
expect(screen.getByTestId('schedule-description')).toHaveTextContent(
'label.schedule-to-run-every week on label.monday at 00:00'
);
expect(
await screen.findByTestId('week-segment-time-container')
@ -112,17 +196,35 @@ describe('Test CronEditor component', () => {
await screen.findByTestId('week-segment-day-option-container')
).toBeInTheDocument();
const minutOptions = await screen.findByTestId('minute-options');
// For Hours Selector
const hourOptions = await screen.findByTestId('hour-options');
expect(minutOptions).toBeInTheDocument();
expect(hourOptions).toBeInTheDocument();
userEvent.selectOptions(minutOptions, '10');
userEvent.selectOptions(hourOptions, '2');
const hourSelect = await findByRole(hourOptions, 'combobox');
act(() => {
userEvent.click(hourSelect);
});
expect(await screen.findAllByText('10')).toHaveLength(2);
expect(await screen.findAllByText('02')).toHaveLength(2);
await waitForElement(() => screen.getByText('10'));
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 () => {

View File

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

View File

@ -11,6 +11,7 @@
* limitations under the License.
*/
import { map, startCase } from 'lodash';
import { ServiceTypes } from 'Models';
import i18n from 'utils/i18next/LocalUtil';
import addPlaceHolder from '../assets/img/add-placeholder.svg';
@ -240,3 +241,8 @@ export const COMMON_UI_SCHEMA = {
export const OPENMETADATA = 'OpenMetadata';
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",
"min": "Min",
"minor": "Minor",
"minute": "Minute",
"minute-lowercase": "minute",
"minute-plural": "Minutes",
"ml-model": "ML Model",
@ -1078,6 +1079,7 @@
"pipeline-description-message": "Description of the pipeline.",
"pipeline-trigger-success-message": "Pipeline triggered successfully!",
"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.",
"profile-sample-percentage-message": "Set the Profiler value as percentage",
"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.",
"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.",
"scheduled-run-every": "Scheduled to run every",
"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-ingestion": "Search for ingestion",