From d9f5e15f21200b3c3a34d57cf98b0dc37b15ba37 Mon Sep 17 00:00:00 2001 From: Aniket Katkar Date: Wed, 25 Sep 2024 10:31:11 +0530 Subject: [PATCH 01/18] Update the cron-editor form to implement new design --- .../AddIngestion/AddIngestion.component.tsx | 35 +++-- .../IngestionWorkflow.interface.ts | 4 +- .../AddIngestion/Steps/ScheduleInterval.tsx | 146 +++++++++++++----- .../AddIngestion/Steps/schedule-interval.less | 38 +++++ .../common/CronEditor/CronEditor.constant.ts | 22 --- .../common/CronEditor/CronEditor.interface.ts | 6 - .../common/CronEditor/CronEditor.tsx | 66 ++++---- .../ui/src/constants/Ingestions.constant.ts | 2 + .../ui/src/constants/Schedular.constants.ts | 28 ++++ .../resources/ui/src/enums/Schedular.enum.ts | 17 ++ .../ui/src/locale/languages/en-us.json | 2 + .../main/resources/ui/src/utils/CronUtils.ts | 4 +- .../resources/ui/src/utils/IngestionUtils.tsx | 32 +++- 13 files changed, 286 insertions(+), 116 deletions(-) create mode 100644 openmetadata-ui/src/main/resources/ui/src/components/Settings/Services/AddIngestion/Steps/schedule-interval.less create mode 100644 openmetadata-ui/src/main/resources/ui/src/constants/Schedular.constants.ts create mode 100644 openmetadata-ui/src/main/resources/ui/src/enums/Schedular.enum.ts diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Settings/Services/AddIngestion/AddIngestion.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Settings/Services/AddIngestion/AddIngestion.component.tsx index 38958b33b7f5..0d38e42736e0 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Settings/Services/AddIngestion/AddIngestion.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Settings/Services/AddIngestion/AddIngestion.component.tsx @@ -13,7 +13,7 @@ import { Form, Input, Typography } from 'antd'; import { isEmpty, isUndefined, omit, trim } from 'lodash'; -import React, { useMemo, useState } from 'react'; +import React, { useCallback, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { STEPS_FOR_ADD_INGESTION } from '../../../../constants/Ingestions.constant'; import { useLimitStore } from '../../../../context/LimitsProvider/useLimitsStore'; @@ -26,16 +26,16 @@ import { } from '../../../../generated/api/services/ingestionPipelines/createIngestionPipeline'; import { IngestionPipeline } from '../../../../generated/entity/services/ingestionPipelines/ingestionPipeline'; import { useApplicationStore } from '../../../../hooks/useApplicationStore'; +import { useFqn } from '../../../../hooks/useFqn'; import { IngestionWorkflowData } from '../../../../interface/service.interface'; -import { getSuccessMessage } from '../../../../utils/IngestionUtils'; +import { + getDefaultIngestionSchedule, + getSuccessMessage, +} from '../../../../utils/IngestionUtils'; import { cleanWorkFlowData } from '../../../../utils/IngestionWorkflowUtils'; import { getScheduleOptionsFromSchedules } from '../../../../utils/ScheduleUtils'; import { getIngestionName } from '../../../../utils/ServiceUtils'; import { generateUUID } from '../../../../utils/StringsUtils'; -import { - getDayCron, - getWeekCron, -} from '../../../common/CronEditor/CronEditor.constant'; import SuccessScreen from '../../../common/SuccessScreen/SuccessScreen'; import DeployIngestionLoaderModal from '../../../Modals/DeployIngestionLoaderModal/DeployIngestionLoaderModal'; import IngestionStepper from '../Ingestion/IngestionStepper/IngestionStepper.component'; @@ -70,9 +70,12 @@ const AddIngestion = ({ onFocus, }: AddIngestionProps) => { const { t } = useTranslation(); + const { ingestionFQN } = useFqn(); const { currentUser } = useApplicationStore(); const { config: limitConfig } = useLimitStore(); + const isEditMode = !isEmpty(ingestionFQN); + const { pipelineSchedules } = limitConfig?.limits?.config.featureLimits.find( (limit) => limit.name === 'ingestionPipeline' @@ -94,12 +97,15 @@ const AddIngestion = ({ ); const [scheduleInterval, setScheduleInterval] = useState(() => - data?.airflowConfig.scheduleInterval ?? limitConfig?.enable - ? getWeekCron({ hour: 0, min: 0, dow: 1 }) - : getDayCron({ - min: 0, - hour: 0, - }) + getDefaultIngestionSchedule({ + isEditMode, + scheduleInterval: data?.airflowConfig.scheduleInterval, + }) + ); + + const handleScheduleIntervalChange = useCallback( + (value?: string) => setScheduleInterval(value), + [] ); const { ingestionName, retries } = useMemo( @@ -312,16 +318,17 @@ const AddIngestion = ({ ? ['day'] : periodOptions } + isEditMode={isEditMode} + savedScheduleInterval={data?.airflowConfig.scheduleInterval} scheduleInterval={scheduleInterval} status={saveState} submitButtonLabel={ isUndefined(data) ? t('label.add-deploy') : t('label.submit') } onBack={() => handlePrev(1)} - onChange={(data) => setScheduleInterval(data)} + onChange={handleScheduleIntervalChange} onDeploy={handleScheduleIntervalDeployClick}> void; + onChange: (newScheduleInterval?: string) => void; status: LoadingState; + savedScheduleInterval?: string; scheduleInterval?: string; includePeriodOptions?: string[]; submitButtonLabel: string; children?: ReactNode; disabledCronChange?: boolean; + isEditMode?: boolean; onBack: () => void; onDeploy: (values: WorkflowExtraConfig) => void; allowEnableDebugLog?: boolean; diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Settings/Services/AddIngestion/Steps/ScheduleInterval.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Settings/Services/AddIngestion/Steps/ScheduleInterval.tsx index 98f29592e9ce..0af6c18de139 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Settings/Services/AddIngestion/Steps/ScheduleInterval.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Settings/Services/AddIngestion/Steps/ScheduleInterval.tsx @@ -12,18 +12,34 @@ */ import { CheckOutlined } from '@ant-design/icons'; -import { Button, Col, Form, FormProps } from 'antd'; +import { + Button, + Card, + Col, + Form, + FormProps, + Radio, + Row, + Space, + Typography, +} from 'antd'; +import classNames from 'classnames'; +import { isEmpty } from 'lodash'; import React, { useCallback, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; +import { SCHEDULAR_OPTIONS } from '../../../../../constants/Schedular.constants'; import { LOADING_STATE } from '../../../../../enums/common.enum'; +import { SchedularOptions } from '../../../../../enums/Schedular.enum'; import { FieldProp, FieldTypes, FormItemLayout, } from '../../../../../interface/FormUtils.interface'; import { generateFormFields } from '../../../../../utils/formUtils'; +import { getDefaultIngestionSchedule } from '../../../../../utils/IngestionUtils'; import CronEditor from '../../../../common/CronEditor/CronEditor'; import { ScheduleIntervalProps } from '../IngestionWorkflow.interface'; +import './schedule-interval.less'; const ScheduleInterval = ({ disabledCronChange, @@ -31,14 +47,42 @@ const ScheduleInterval = ({ onBack, onChange, onDeploy, + savedScheduleInterval, scheduleInterval, status, submitButtonLabel, children, allowEnableDebugLog = false, debugLogInitialValue = false, + isEditMode = false, }: ScheduleIntervalProps) => { const { t } = useTranslation(); + const [selectedSchedular, setSelectedSchedular] = + React.useState( + isEmpty(scheduleInterval) + ? SchedularOptions.ON_DEMAND + : SchedularOptions.SCHEDULE + ); + + const handleSelectedSchedular = useCallback( + (value: SchedularOptions) => { + setSelectedSchedular(value); + if (value === SchedularOptions.ON_DEMAND) { + onChange(''); + } else { + onChange( + isEditMode && !isEmpty(savedScheduleInterval) + ? savedScheduleInterval + : getDefaultIngestionSchedule({ + scheduleInterval, + isEditMode, + }) + ); + } + }, + [isEditMode, selectedSchedular, scheduleInterval] + ); + const formFields: FieldProp[] = useMemo( () => [ { @@ -73,46 +117,78 @@ const ScheduleInterval = ({ data-testid="schedule-intervel-container" layout="vertical" onFinish={handleFormSubmit}> - + + + + {SCHEDULAR_OPTIONS.map(({ description, title, value }) => ( + handleSelectedSchedular(value)}> + + + + {title} + + + {description} + + + + + ))} + + - {allowEnableDebugLog && ( -
{generateFormFields(formFields)}
- )} + {selectedSchedular === SchedularOptions.SCHEDULE && ( + + + + )} - {children} + {allowEnableDebugLog && ( + {generateFormFields(formFields)} + )} - - + {children && {children}} - {status === 'success' ? ( - - ) : ( + - )} - + + {status === 'success' ? ( + + ) : ( + + )} + +
); }; diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Settings/Services/AddIngestion/Steps/schedule-interval.less b/openmetadata-ui/src/main/resources/ui/src/components/Settings/Services/AddIngestion/Steps/schedule-interval.less new file mode 100644 index 000000000000..c9e550703cee --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/src/components/Settings/Services/AddIngestion/Steps/schedule-interval.less @@ -0,0 +1,38 @@ +/* + * Copyright 2024 Collate. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +@import (reference) url('../../../../../styles/variables.less'); + +.schedular-card-container { + display: flex; + align-items: center; + justify-content: center; + gap: 16px; + height: 130px; + .schedular-card { + border-radius: 6px; + cursor: pointer; + height: 100%; + .ant-card-body { + padding: 24px; + height: 100%; + + .ant-radio { + margin-right: 12px; + } + } + } + .schedular-card.active { + border-color: @primary-color; + } +} diff --git a/openmetadata-ui/src/main/resources/ui/src/components/common/CronEditor/CronEditor.constant.ts b/openmetadata-ui/src/main/resources/ui/src/components/common/CronEditor/CronEditor.constant.ts index e44f61026052..e2264624715b 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/common/CronEditor/CronEditor.constant.ts +++ b/openmetadata-ui/src/main/resources/ui/src/components/common/CronEditor/CronEditor.constant.ts @@ -17,11 +17,6 @@ import { Combination, ToDisplay } from './CronEditor.interface'; /* eslint-disable @typescript-eslint/no-explicit-any */ export const getPeriodOptions = () => { return [ - { - label: i18n.t('label.none'), - value: '', - prep: '', - }, { label: i18n.t('label.hour'), value: 'hour', @@ -41,36 +36,19 @@ export const getPeriodOptions = () => { label: i18n.t('label.custom'), value: 'custom', }, - /* , - { - label: 'month', - value: 'month', - prep: 'on the' - }, - { - label: 'year', - value: 'year', - prep: 'on the' - }*/ ]; }; export const toDisplay: ToDisplay = { - minute: [], hour: ['min'], day: ['time'], week: ['dow', 'time'], - month: ['dom', 'time'], - year: ['dom', 'mon', 'time'], }; export const combinations: Combination = { - minute: /^(\*\/\d{1,2})\s(\*\s){3}\*$/, // "*/? * * * *" hour: /^\d{1,2}\s(\*\s){3}\*$/, // "? * * * *" day: /^(\d{1,2}\s){2}(\*\s){2}\*$/, // "? ? * * *" week: /^(\d{1,2}\s){2}(\*\s){2}\d{1,2}$/, // "? ? * * ?" - month: /^(\d{1,2}\s){3}\*\s\*$/, // "? ? ? * *" - year: /^(\d{1,2}\s){4}\*$/, // "? ? ? ? *" }; export const getRange = (n: number) => { diff --git a/openmetadata-ui/src/main/resources/ui/src/components/common/CronEditor/CronEditor.interface.ts b/openmetadata-ui/src/main/resources/ui/src/components/common/CronEditor/CronEditor.interface.ts index ed702092a759..c0a49704881f 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/common/CronEditor/CronEditor.interface.ts +++ b/openmetadata-ui/src/main/resources/ui/src/components/common/CronEditor/CronEditor.interface.ts @@ -52,12 +52,9 @@ export interface CronValue { } export interface Combination { - minute: RegExp; hour: RegExp; day: RegExp; week: RegExp; - month: RegExp; - year: RegExp; } export interface StateValue { selectedPeriod: string; @@ -70,12 +67,9 @@ export interface StateValue { } export interface ToDisplay { - minute: Array; hour: Array; day: Array; week: Array; - month: Array; - year: Array; } export interface CronOption { diff --git a/openmetadata-ui/src/main/resources/ui/src/components/common/CronEditor/CronEditor.tsx b/openmetadata-ui/src/main/resources/ui/src/components/common/CronEditor/CronEditor.tsx index c142b7c1b90a..3b2f92d7c366 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/common/CronEditor/CronEditor.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/common/CronEditor/CronEditor.tsx @@ -15,8 +15,9 @@ import { Col, Form, Input, Row, Select } from 'antd'; import classNames from 'classnames'; import cronstrue from 'cronstrue'; import { isEmpty } from 'lodash'; -import React, { FC, useMemo, useState } from 'react'; +import React, { ChangeEvent, useCallback, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; +import { DEFAULT_SCHEDULE_CRON } from '../../../constants/Ingestions.constant'; import { pluralize } from '../../../utils/CommonUtils'; import { getCron, @@ -41,11 +42,18 @@ import { StateValue, } from './CronEditor.interface'; -const CronEditor: FC = (props) => { +const CronEditor = ({ + value, + includePeriodOptions, + className, + disabled, + disabledCronChange, + onChange, + isQuartzCron, +}: CronEditorProp) => { const { t } = useTranslation(); - const [value, setCronValue] = useState(props.value ?? ''); - const [state, setState] = useState(getStateValue(props.value ?? '')); + const [state, setState] = useState(getStateValue(value ?? '')); const [periodOptions] = useState(getPeriodOptions()); const [minuteSegmentOptions] = useState(getMinuteSegmentOptions()); const [minuteOptions] = useState(getMinuteOptions()); @@ -55,7 +63,6 @@ const CronEditor: FC = (props) => { const [monthOptions] = useState(getMonthOptions()); const filteredPeriodOptions = useMemo(() => { - const { includePeriodOptions } = props; if (includePeriodOptions) { return periodOptions.filter((option) => includePeriodOptions.includes(option.value) @@ -63,32 +70,33 @@ const CronEditor: FC = (props) => { } else { return periodOptions; } - }, [props, periodOptions]); + }, [includePeriodOptions, periodOptions]); - const { className, disabled, disabledCronChange } = props; const { selectedPeriod } = state; + const handleCronValueChange = useCallback( + (e: ChangeEvent) => { + onChange(e.target.value); + }, + [onChange] + ); + const startText = t('label.schedule-to-run-every'); const cronPeriodString = `${startText} ${selectedPeriod}`; const changeValue = (state: StateValue) => { - const { onChange } = props; - - setCronValue(getCron(state) ?? value); - const cronExp = props.isQuartzCron + const cronExp = isQuartzCron ? getQuartzCronExpression(state) : getCron(state); - onChange(cronExp ?? value); + onChange(cronExp ?? DEFAULT_SCHEDULE_CRON); }; const onPeriodSelect = (value: string) => { changeValue({ ...state, selectedPeriod: value }); if (value === 'custom') { - setCronValue('0 0 * * *'); - props.onChange('0 0 * * *'); + onChange(DEFAULT_SCHEDULE_CRON); } else if (value === '') { - setCronValue(''); - props.onChange(''); + onChange(''); } setState((prev) => ({ ...prev, selectedPeriod: value })); }; @@ -175,8 +183,6 @@ const CronEditor: FC = (props) => { selectedOption: SelectedDayOption, onChangeCB: (value: number) => void ) => { - const { disabled } = props; - return ( = (props) => { ({ label, value, @@ -542,10 +546,9 @@ const CronEditor: FC = (props) => { = (props) => { }, }, ]}> - { - setCronValue(e.target.value); - props.onChange(e.target.value); - }} - /> + ) : ( diff --git a/openmetadata-ui/src/main/resources/ui/src/constants/Ingestions.constant.ts b/openmetadata-ui/src/main/resources/ui/src/constants/Ingestions.constant.ts index 13b913588872..0ac7500e5ce1 100644 --- a/openmetadata-ui/src/main/resources/ui/src/constants/Ingestions.constant.ts +++ b/openmetadata-ui/src/main/resources/ui/src/constants/Ingestions.constant.ts @@ -53,3 +53,5 @@ export const PIPELINE_TYPE_LOCALIZATION = { export const DBT_CLASSIFICATION_DEFAULT_VALUE = 'dbtTags'; export const DEFAULT_PARSING_TIMEOUT_LIMIT = 300; + +export const DEFAULT_SCHEDULE_CRON = '0 0 * * *'; diff --git a/openmetadata-ui/src/main/resources/ui/src/constants/Schedular.constants.ts b/openmetadata-ui/src/main/resources/ui/src/constants/Schedular.constants.ts new file mode 100644 index 000000000000..8042019cd599 --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/src/constants/Schedular.constants.ts @@ -0,0 +1,28 @@ +/* + * Copyright 2024 Collate. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { t } from 'i18next'; +import { SchedularOptions } from '../enums/Schedular.enum'; + +export const SCHEDULAR_OPTIONS = [ + { + title: t('label.schedule'), + description: t('message.schedule-description'), + value: SchedularOptions.SCHEDULE, + }, + { + title: t('label.on-demand'), + description: t('message.on-demand-description'), + value: SchedularOptions.ON_DEMAND, + }, +]; diff --git a/openmetadata-ui/src/main/resources/ui/src/enums/Schedular.enum.ts b/openmetadata-ui/src/main/resources/ui/src/enums/Schedular.enum.ts new file mode 100644 index 000000000000..b144b81c987f --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/src/enums/Schedular.enum.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2024 Collate. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export enum SchedularOptions { + SCHEDULE = 'schedule', + ON_DEMAND = 'on-demand', +} diff --git a/openmetadata-ui/src/main/resources/ui/src/locale/languages/en-us.json b/openmetadata-ui/src/main/resources/ui/src/locale/languages/en-us.json index 1bdcabeebc49..b88395dfd101 100644 --- a/openmetadata-ui/src/main/resources/ui/src/locale/languages/en-us.json +++ b/openmetadata-ui/src/main/resources/ui/src/locale/languages/en-us.json @@ -1683,6 +1683,7 @@ "not-followed-anything": "Start Exploring! and Follow the Data Assets that interests you.", "notification-description": "Set up notifications to received real-time updates and timely alerts.", "om-description": "Centralized metadata store, to discover, collaborate and get your data right.", + "on-demand-description": "Run the ingestion manually.", "onboarding-claim-ownership-description": "Data works well when it is owned. Take a look at the data assets that you own and claim ownership.", "onboarding-explore-data-description": "Look at the popular data assets in your organization.", "onboarding-stay-up-to-date-description": "Follow the datasets that you frequently use to stay informed about it.", @@ -1769,6 +1770,7 @@ "retention-period-description": "Retention period refers to the duration for which data is retained before it is considered eligible for deletion or archival. Example: 30 days, 6 months, 1 year or any ISO 8601 format in UTC like P23DT23H will be valid.", "run-sample-data-to-ingest-sample-data": "'Run sample data to ingest sample data assets into your OpenMetadata.'", "run-status-at-timestamp": "Run status: {{status}} at {{timestamp}}", + "schedule-description": "Schedule the ingestion to run at a specific time and frequency.", "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", diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/CronUtils.ts b/openmetadata-ui/src/main/resources/ui/src/utils/CronUtils.ts index 2d852e80640a..8823134f8623 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/CronUtils.ts +++ b/openmetadata-ui/src/main/resources/ui/src/utils/CronUtils.ts @@ -102,7 +102,7 @@ const getCronType = (cronStr: string) => { } } - return undefined; + return 'custom'; }; export const getStateValue = (valueStr: string) => { @@ -148,7 +148,7 @@ export const getStateValue = (valueStr: string) => { stateVal.selectedPeriod = cronType || stateVal.selectedPeriod; - if (!isEmpty(cronType)) { + if (!isEmpty(cronType) && cronType !== 'custom') { const stateIndex = SELECTED_PERIOD_OPTIONS[(cronType as CronType) || 'hour']; const selectedPeriodObj = stateVal[ diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/IngestionUtils.tsx b/openmetadata-ui/src/main/resources/ui/src/utils/IngestionUtils.tsx index 58d7acd91903..c58657c24489 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/IngestionUtils.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/utils/IngestionUtils.tsx @@ -14,9 +14,10 @@ import { Typography } from 'antd'; import { ExpandableConfig } from 'antd/lib/table/interface'; import { t } from 'i18next'; -import { isUndefined, startCase } from 'lodash'; +import { isEmpty, isUndefined, startCase } from 'lodash'; import { ServiceTypes } from 'Models'; import React from 'react'; +import { getDayCron } from '../components/common/CronEditor/CronEditor.constant'; import ErrorPlaceHolder from '../components/common/ErrorWithPlaceholder/ErrorPlaceHolder'; import ConnectionStepCard from '../components/common/TestConnection/ConnectionStepCard/ConnectionStepCard'; import { getServiceDetailsPath } from '../constants/constants'; @@ -395,3 +396,32 @@ export const getExpandableStatusRow = ( expandedRowKeys: expandedKeys, rowExpandable: (record) => (record.failures?.length ?? 0) > 0, }); + +export const getDefaultIngestionSchedule = ({ + isEditMode = false, + scheduleInterval, + defaultSchedule, +}: { + isEditMode?: boolean; + scheduleInterval?: string; + defaultSchedule?: string; +}) => { + // If it is edit mode, then return the schedule interval from the ingestion data + if (isEditMode) { + return scheduleInterval; + } + + // If it is not edit mode and schedule interval is not empty, then return the schedule interval + if (!isEmpty(scheduleInterval)) { + return scheduleInterval; + } + + // If it is not edit mode, then return the default schedule + return ( + defaultSchedule ?? + getDayCron({ + min: 0, + hour: 0, + }) + ); +}; From d41e553084cf5a8cc873d1fd6a4e39197e830de6 Mon Sep 17 00:00:00 2001 From: Aniket Katkar Date: Wed, 25 Sep 2024 15:22:06 +0530 Subject: [PATCH 02/18] localization changes --- .../src/main/resources/ui/src/locale/languages/de-de.json | 2 ++ .../src/main/resources/ui/src/locale/languages/es-es.json | 2 ++ .../src/main/resources/ui/src/locale/languages/fr-fr.json | 2 ++ .../src/main/resources/ui/src/locale/languages/he-he.json | 2 ++ .../src/main/resources/ui/src/locale/languages/ja-jp.json | 2 ++ .../src/main/resources/ui/src/locale/languages/nl-nl.json | 2 ++ .../src/main/resources/ui/src/locale/languages/pr-pr.json | 2 ++ .../src/main/resources/ui/src/locale/languages/pt-br.json | 2 ++ .../src/main/resources/ui/src/locale/languages/ru-ru.json | 2 ++ .../src/main/resources/ui/src/locale/languages/zh-cn.json | 2 ++ 10 files changed, 20 insertions(+) diff --git a/openmetadata-ui/src/main/resources/ui/src/locale/languages/de-de.json b/openmetadata-ui/src/main/resources/ui/src/locale/languages/de-de.json index 51d50861972f..7511e0480157 100644 --- a/openmetadata-ui/src/main/resources/ui/src/locale/languages/de-de.json +++ b/openmetadata-ui/src/main/resources/ui/src/locale/languages/de-de.json @@ -1683,6 +1683,7 @@ "not-followed-anything": "Du hast noch nichts abonniert.", "notification-description": "Set up notifications to received real-time updates and timely alerts.", "om-description": "Zentraler Metadatenspeicher, um Daten zu entdecken, zusammenzuarbeiten und die Datenqualität zu verbessern.", + "on-demand-description": "Run the ingestion manually.", "onboarding-claim-ownership-description": "Daten funktionieren am besten, wenn sie einen Eigentümer haben. Sieh dir die Datenvermögenswerte an, die du besitzt, und beanspruche ihren Besitz.", "onboarding-explore-data-description": "Schau dir die beliebten Datenvermögenswerte in deiner Organisation an.", "onboarding-stay-up-to-date-description": "Folge den Datensätzen, die du häufig verwendest, um über sie informiert zu bleiben.", @@ -1769,6 +1770,7 @@ "retention-period-description": "Retention period refers to the duration for which data is retained before it is considered eligible for deletion or archival. Example: 30 days, 6 months, 1 year or any ISO 8601 format in UTC like P23DT23H will be valid.", "run-sample-data-to-ingest-sample-data": "Führen Sie Musterdaten aus, um Musterdatenvermögenswerte in Ihr OpenMetadata einzufügen.", "run-status-at-timestamp": "Run status: {{status}} at {{timestamp}}", + "schedule-description": "Schedule the ingestion to run at a specific time and frequency.", "schedule-for-ingestion-description": "Die Planung kann im stündlichen, täglichen oder wöchentlichen Rhythmus eingerichtet werden. Die Zeitzone ist UTC.", "scheduled-run-every": "Geplant, alle auszuführen", "scopes-comma-separated": "Fügen Sie den Wert der Bereiche hinzu, getrennt durch Kommata", diff --git a/openmetadata-ui/src/main/resources/ui/src/locale/languages/es-es.json b/openmetadata-ui/src/main/resources/ui/src/locale/languages/es-es.json index 00b08f953947..43e084f9b01e 100644 --- a/openmetadata-ui/src/main/resources/ui/src/locale/languages/es-es.json +++ b/openmetadata-ui/src/main/resources/ui/src/locale/languages/es-es.json @@ -1683,6 +1683,7 @@ "not-followed-anything": "Todavía no has seguido nada.", "notification-description": "Configura notificaciones para recibir actualizaciones en tiempo real y alertas oportunas.", "om-description": "Almacenamiento centralizado de metadatos para descubrir, colaborar y tener los datos correctos.", + "on-demand-description": "Run the ingestion manually.", "onboarding-claim-ownership-description": "Revisa los activos de datos que posees y reclama su propiedad.", "onboarding-explore-data-description": "Sigue los activos de datos populares en tu organización.", "onboarding-stay-up-to-date-description": "Sigue los conjuntos de datos que usas con frecuencia para mantenerte informado acerca de ellos.", @@ -1769,6 +1770,7 @@ "retention-period-description": "Retention period se refiere a la duración durante la cual los datos se retienen antes de que sean elegibles para su eliminación o archivo.", "run-sample-data-to-ingest-sample-data": "'Ejecutar datos de muestra para ingresar activos de datos de muestra en tu OpenMetadata.'", "run-status-at-timestamp": "Run status: {{status}} at {{timestamp}}", + "schedule-description": "Schedule the ingestion to run at a specific time and frequency.", "schedule-for-ingestion-description": "La programación se puede configurar en una cadencia horaria, diaria o semanal.", "scheduled-run-every": "Programado para ejecutarse cada", "scopes-comma-separated": "Agrega el valor de ámbitos, separados por comas", diff --git a/openmetadata-ui/src/main/resources/ui/src/locale/languages/fr-fr.json b/openmetadata-ui/src/main/resources/ui/src/locale/languages/fr-fr.json index 9c5dff1fa3d9..3c3730da7477 100644 --- a/openmetadata-ui/src/main/resources/ui/src/locale/languages/fr-fr.json +++ b/openmetadata-ui/src/main/resources/ui/src/locale/languages/fr-fr.json @@ -1683,6 +1683,7 @@ "not-followed-anything": "Vous n'avez encore rien suivi.", "notification-description": "Paramétrez les notifications pour recevoir des mises à jour en temps réel ansi que des alertes.", "om-description": "Entrepôt centralisé de métadonnées, découvrez, collaborez et assurez-vous que vos données sont correctes", + "on-demand-description": "Run the ingestion manually.", "onboarding-claim-ownership-description": "Les données fonctionnent mieux lorsqu'elles sont possédées. Revoyez les actifs de données que vous possédez et revendiquez la propriété.", "onboarding-explore-data-description": "Découvrez les actifs de données populaires dans votre organisation.", "onboarding-stay-up-to-date-description": "Suivez les actifs de données que vous utilisez fréquemment pour rester informé à leur sujet.", @@ -1769,6 +1770,7 @@ "retention-period-description": "La période de rétention est la durée pendant laquelle les données sont conservées avant d'être éligible à la suppression ou à l'archivage. Exemples: 30 jours, 6 mois, 1 an ou n'importe quel format ISO 8601 en UTC comme P23DT23H sont valides.", "run-sample-data-to-ingest-sample-data": "Exécuter l'ingestion de données d'exemple dans OpenMetadata", "run-status-at-timestamp": "Etat du lancement: {{status}} à {{timestamp}}", + "schedule-description": "Schedule the ingestion to run at a specific time and frequency.", "schedule-for-ingestion-description": "La programmation peut être configurée à une cadence horaire, quotidienne ou hebdomadaire. Le fuseau horaire est en UTC.", "scheduled-run-every": "Programmer pour être exécuté tous les", "scopes-comma-separated": "Liste de scopes séparée par une virgule.", diff --git a/openmetadata-ui/src/main/resources/ui/src/locale/languages/he-he.json b/openmetadata-ui/src/main/resources/ui/src/locale/languages/he-he.json index f3e44e8c076c..fecc1e502b5f 100644 --- a/openmetadata-ui/src/main/resources/ui/src/locale/languages/he-he.json +++ b/openmetadata-ui/src/main/resources/ui/src/locale/languages/he-he.json @@ -1683,6 +1683,7 @@ "not-followed-anything": "אתה לא עוקב אחרי כלום עדיין.", "notification-description": "Set up notifications to received real-time updates and timely alerts.", "om-description": "אחסון מטה מרכזי, לגלוש, לשתף פעולה ולהבין את הנתונים שלך כראוי.", + "on-demand-description": "Run the ingestion manually.", "onboarding-claim-ownership-description": "הנתונים פועלים טוב כשהם משולטים עליהם. עיין בנכסי הנתונים שאתה בעל ותטריד את הבעלות.", "onboarding-explore-data-description": "בדוק את נכסי הנתונים הפופולריים בארגונך.", "onboarding-stay-up-to-date-description": "עקוב אחרי הסטים שאתה משתמש בהם תדיר כדי להישאר מעודכן עליהם.", @@ -1769,6 +1770,7 @@ "retention-period-description": "Retention period refers to the duration for which data is retained before it is considered eligible for deletion or archival. Example: 30 days, 6 months, 1 year or any ISO 8601 format in UTC like P23DT23H will be valid.", "run-sample-data-to-ingest-sample-data": "'הרץ נתוני דוגמה כדי לשדרג נכסי נתונים דוגמה אל OpenMetadata שלך.'", "run-status-at-timestamp": "Run status: {{status}} at {{timestamp}}", + "schedule-description": "Schedule the ingestion to run at a specific time and frequency.", "schedule-for-ingestion-description": "ניתן להגדיר שימוש כל שעה, יומית או שבועית. אזור הזמן הוא UTC.", "scheduled-run-every": "מתוזמן לרוץ כל", "scopes-comma-separated": "הוסף את ערכי הניקוד, מופרדים בפסיקים", diff --git a/openmetadata-ui/src/main/resources/ui/src/locale/languages/ja-jp.json b/openmetadata-ui/src/main/resources/ui/src/locale/languages/ja-jp.json index 8d2dc5ae53be..53c4af3734c8 100644 --- a/openmetadata-ui/src/main/resources/ui/src/locale/languages/ja-jp.json +++ b/openmetadata-ui/src/main/resources/ui/src/locale/languages/ja-jp.json @@ -1683,6 +1683,7 @@ "not-followed-anything": "あなたはまだ何もフォローしていません。", "notification-description": "Set up notifications to received real-time updates and timely alerts.", "om-description": "Centralized metadata store, to discover, collaborate and get your data right.", + "on-demand-description": "Run the ingestion manually.", "onboarding-claim-ownership-description": "Data works well when it is owned. Take a look at the data assets that you own and claim ownership.", "onboarding-explore-data-description": "あなたの組織で人気のデータアセットを見る。", "onboarding-stay-up-to-date-description": "よく使うデータセットをフォローして、情報が通知されるようにする", @@ -1769,6 +1770,7 @@ "retention-period-description": "Retention period refers to the duration for which data is retained before it is considered eligible for deletion or archival. Example: 30 days, 6 months, 1 year or any ISO 8601 format in UTC like P23DT23H will be valid.", "run-sample-data-to-ingest-sample-data": "'サンプルデータを実行してサンプルデータのアセットをOpenMetadataに取り込みます。'", "run-status-at-timestamp": "Run status: {{status}} at {{timestamp}}", + "schedule-description": "Schedule the ingestion to run at a specific time and frequency.", "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": "スコープの値をカンマで区切って追加", diff --git a/openmetadata-ui/src/main/resources/ui/src/locale/languages/nl-nl.json b/openmetadata-ui/src/main/resources/ui/src/locale/languages/nl-nl.json index 9a903eaa49cc..119e44221a85 100644 --- a/openmetadata-ui/src/main/resources/ui/src/locale/languages/nl-nl.json +++ b/openmetadata-ui/src/main/resources/ui/src/locale/languages/nl-nl.json @@ -1683,6 +1683,7 @@ "not-followed-anything": "Je hebt nog niets gevolgd.", "notification-description": "Set up notifications to received real-time updates and timely alerts.", "om-description": "Gecentraliseerde metadatastore, om data te ontdekken, samen te werken en op orde te brengen.", + "on-demand-description": "Run the ingestion manually.", "onboarding-claim-ownership-description": "Data is het meest bruikbaar als het een eigenaar heeft. Bekijk de data-assets waar je eigenaar van bent en claim het eigendom.", "onboarding-explore-data-description": "Bekijk de meestgebruikte data-assets in jouw organisatie.", "onboarding-stay-up-to-date-description": "Volg de datasets die je vaak gebruikt om op de hoogte te blijven.", @@ -1769,6 +1770,7 @@ "retention-period-description": "De bewaartermijn verwijst naar de duur van het bewaren van data voordat data in aanmerking komt voor verwijdering of archivering. Voorbeelden van geldige periodes: 30 dagen, 6 maanden, 1 jaar, of een ISO 8601-indeling in UTC zoals P23DT23H.", "run-sample-data-to-ingest-sample-data": "'Voer voorbeelddata uit om voorbeeld-data-assets te ingesten in je OpenMetadata.'", "run-status-at-timestamp": "Run status: {{status}} at {{timestamp}}", + "schedule-description": "Schedule the ingestion to run at a specific time and frequency.", "schedule-for-ingestion-description": "Planning kan worden ingesteld op een uurlijkse, dagelijkse of wekelijkse cadans. De tijdzone is in UTC.", "scheduled-run-every": "Gepland om elke keer uit te voeren", "scopes-comma-separated": "Voeg de herkomstwaarde toe, gescheiden door komma's", diff --git a/openmetadata-ui/src/main/resources/ui/src/locale/languages/pr-pr.json b/openmetadata-ui/src/main/resources/ui/src/locale/languages/pr-pr.json index eaa462ef76b9..9ecec7a8d09c 100644 --- a/openmetadata-ui/src/main/resources/ui/src/locale/languages/pr-pr.json +++ b/openmetadata-ui/src/main/resources/ui/src/locale/languages/pr-pr.json @@ -1683,6 +1683,7 @@ "not-followed-anything": "شروع به کاوش کنید! و دارایی‌های داده‌ای که شما را علاقه‌مند می‌کند دنبال کنید.", "notification-description": "اعلان‌ها را تنظیم کنید تا به‌روزرسانی‌های زمان واقعی و هشدارهای به موقع دریافت کنید.", "om-description": "ذخیره متمرکز متادیتا، برای کشف، همکاری و درست کردن داده‌های شما.", + "on-demand-description": "Run the ingestion manually.", "onboarding-claim-ownership-description": "داده زمانی خوب عمل می‌کند که مالکیت داشته باشد. به دارایی‌های داده‌ای که شما مالکیت دارید نگاهی بیندازید و مالکیت آنها را برعهده بگیرید.", "onboarding-explore-data-description": "به دارایی‌های داده‌ای محبوب در سازمان خود نگاهی بیندازید.", "onboarding-stay-up-to-date-description": "داده‌مجموعه‌هایی که مرتباً استفاده می‌کنید را دنبال کنید تا از آخرین وضعیت آن مطلع شوید.", @@ -1769,6 +1770,7 @@ "retention-period-description": "دوره نگهداری به مدت زمانی اطلاق می‌شود که داده‌ها قبل از اینکه برای حذف یا بایگانی شدن واجد شرایط شوند، نگهداری می‌شوند. مثال: 30 روز، 6 ماه، 1 سال یا هر فرمتی از ISO 8601 در UTC مانند P23DT23H معتبر خواهد بود.", "run-sample-data-to-ingest-sample-data": "'اجرای داده نمونه برای ورود داده‌های نمونه به OpenMetadata.'", "run-status-at-timestamp": "وضعیت اجرا: {{status}} در {{timestamp}}", + "schedule-description": "Schedule the ingestion to run at a specific time and frequency.", "schedule-for-ingestion-description": "برنامه‌ریزی می‌تواند به صورت ساعتی، روزانه یا هفتگی تنظیم شود. منطقه زمانی UTC است.", "scheduled-run-every": "برنامه‌ریزی شده برای اجرا هر", "scopes-comma-separated": "مقدار Scopes را با کاما جدا کنید.", diff --git a/openmetadata-ui/src/main/resources/ui/src/locale/languages/pt-br.json b/openmetadata-ui/src/main/resources/ui/src/locale/languages/pt-br.json index b5d0da05bb7c..888147969b43 100644 --- a/openmetadata-ui/src/main/resources/ui/src/locale/languages/pt-br.json +++ b/openmetadata-ui/src/main/resources/ui/src/locale/languages/pt-br.json @@ -1683,6 +1683,7 @@ "not-followed-anything": "Você ainda não seguiu nada.", "notification-description": "Configure notificações para receber atualizações em tempo real e alertas.", "om-description": "Armazenamento centralizado de metadados, para descobrir, colaborar e obter seus dados corretamente.", + "on-demand-description": "Run the ingestion manually.", "onboarding-claim-ownership-description": "Os dados funcionam bem quando são de propriedade. Dê uma olhada nos ativos de dados que você possui e reivindique a propriedade.", "onboarding-explore-data-description": "Veja os ativos de dados populares em sua organização.", "onboarding-stay-up-to-date-description": "Siga os conjuntos de dados que você usa com frequência para se manter informado sobre eles.", @@ -1769,6 +1770,7 @@ "retention-period-description": "O período de retenção refere-se à duração durante a qual os dados são mantidos antes de serem considerados elegíveis para exclusão ou arquivamento. Exemplo: 30 dias, 6 meses, 1 ano ou qualquer formato ISO 8601 em UTC, como P23DT23H, será válido.", "run-sample-data-to-ingest-sample-data": "Executar dados de exemplo para ingerir ativos de dados de exemplo no OpenMetadata.", "run-status-at-timestamp": "Run status: {{status}} at {{timestamp}}", + "schedule-description": "Schedule the ingestion to run at a specific time and frequency.", "schedule-for-ingestion-description": "O agendamento pode ser configurado em uma frequência horária, diária ou semanal. O fuso horário é em UTC.", "scheduled-run-every": "Agendado para rodar a cada", "scopes-comma-separated": "Adicione o valor dos Escopos, separados por vírgulas", diff --git a/openmetadata-ui/src/main/resources/ui/src/locale/languages/ru-ru.json b/openmetadata-ui/src/main/resources/ui/src/locale/languages/ru-ru.json index 97300f0dd4fc..f2812f4dccc6 100644 --- a/openmetadata-ui/src/main/resources/ui/src/locale/languages/ru-ru.json +++ b/openmetadata-ui/src/main/resources/ui/src/locale/languages/ru-ru.json @@ -1683,6 +1683,7 @@ "not-followed-anything": "Вы еще не подписаны ни на один объект.", "notification-description": "Set up notifications to received real-time updates and timely alerts.", "om-description": "Централизованное хранилище метаданных для обнаружения, совместной работы и правильной обработки ваших данных.", + "on-demand-description": "Run the ingestion manually.", "onboarding-claim-ownership-description": "Данные работают хорошо, когда ими владеют. Взгляните на объекты данных, которыми вы владеете, и заявите о праве собственности.", "onboarding-explore-data-description": "Посмотрите на популярные объекты данных в вашей организации.", "onboarding-stay-up-to-date-description": "Следите за наборами данных, которые вы часто используете, чтобы быть в курсе.", @@ -1769,6 +1770,7 @@ "retention-period-description": "Retention period refers to the duration for which data is retained before it is considered eligible for deletion or archival. Example: 30 days, 6 months, 1 year or any ISO 8601 format in UTC like P23DT23H will be valid.", "run-sample-data-to-ingest-sample-data": "Запустите образцы данных, чтобы добавить образцы данных в свои OpenMetadata.", "run-status-at-timestamp": "Run status: {{status}} at {{timestamp}}", + "schedule-description": "Schedule the ingestion to run at a specific time and frequency.", "schedule-for-ingestion-description": "Планирование может быть настроено на почасовой, ежедневной или еженедельной частоте. Часовой пояс указан в формате UTC.", "scheduled-run-every": "Запланирован запуск каждый", "scopes-comma-separated": "Добавьте значение областей, разделенное запятыми", diff --git a/openmetadata-ui/src/main/resources/ui/src/locale/languages/zh-cn.json b/openmetadata-ui/src/main/resources/ui/src/locale/languages/zh-cn.json index deece54679e7..9a2c0d12a6d2 100644 --- a/openmetadata-ui/src/main/resources/ui/src/locale/languages/zh-cn.json +++ b/openmetadata-ui/src/main/resources/ui/src/locale/languages/zh-cn.json @@ -1683,6 +1683,7 @@ "not-followed-anything": "尚未关注任何内容", "notification-description": "设置通知以接收实时更新和及时警报", "om-description": "统一的元数据存储平台, 更好地探索、协作和处理数据", + "on-demand-description": "Run the ingestion manually.", "onboarding-claim-ownership-description": "查看数据资产并声明所有权, 以更好的维护数据", "onboarding-explore-data-description": "查看组织中热门的数据资产", "onboarding-stay-up-to-date-description": "关注您经常使用的数据集以获取最新信息", @@ -1769,6 +1770,7 @@ "retention-period-description": "Retention period refers to the duration for which data is retained before it is considered eligible for deletion or archival. Example: 30 days, 6 months, 1 year or any ISO 8601 format in UTC like P23DT23H will be valid.", "run-sample-data-to-ingest-sample-data": "'运行样本数据以提取样本数据资产到 OpenMetadata'", "run-status-at-timestamp": "运行状态: {{status}} 在 {{timestamp}}", + "schedule-description": "Schedule the ingestion to run at a specific time and frequency.", "schedule-for-ingestion-description": "可设置每小时、每天或每周的计划, 时区为 UTC", "scheduled-run-every": "计划每次运行", "scopes-comma-separated": "范围值逗号分隔", From a26382ec78de2bc0ae0a0b9a136b0d0e58022377 Mon Sep 17 00:00:00 2001 From: Aniket Katkar Date: Thu, 26 Sep 2024 16:00:22 +0530 Subject: [PATCH 03/18] Refactor the CronEditor component --- .../common/CronEditor/CronEditor.constant.ts | 4 - .../common/CronEditor/CronEditor.interface.ts | 3 - .../common/CronEditor/CronEditor.tsx | 557 ++++-------------- .../common/CronEditor/cron-editor.less | 23 +- .../main/resources/ui/src/enums/Cron.enum.ts | 19 + .../src/utils/{CronUtils.ts => CronUtils.tsx} | 95 +-- .../ui/src/utils/IngestionListTableUtils.tsx | 4 +- .../ui/src/utils/i18next/i18nextUtil.ts | 17 +- 8 files changed, 219 insertions(+), 503 deletions(-) create mode 100644 openmetadata-ui/src/main/resources/ui/src/enums/Cron.enum.ts rename openmetadata-ui/src/main/resources/ui/src/utils/{CronUtils.ts => CronUtils.tsx} (74%) diff --git a/openmetadata-ui/src/main/resources/ui/src/components/common/CronEditor/CronEditor.constant.ts b/openmetadata-ui/src/main/resources/ui/src/components/common/CronEditor/CronEditor.constant.ts index e2264624715b..3b5d43c5f9c8 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/common/CronEditor/CronEditor.constant.ts +++ b/openmetadata-ui/src/main/resources/ui/src/components/common/CronEditor/CronEditor.constant.ts @@ -64,10 +64,6 @@ export const getRangeOptions = (n: number) => { }); }; -export const getMinuteSegmentOptions = () => { - return getRangeOptions(60).filter((v) => v.value !== 0 && v.value % 5 === 0); -}; - export const getMinuteOptions = () => { return getRangeOptions(60); }; diff --git a/openmetadata-ui/src/main/resources/ui/src/components/common/CronEditor/CronEditor.interface.ts b/openmetadata-ui/src/main/resources/ui/src/components/common/CronEditor/CronEditor.interface.ts index c0a49704881f..67c064dd123e 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/common/CronEditor/CronEditor.interface.ts +++ b/openmetadata-ui/src/main/resources/ui/src/components/common/CronEditor/CronEditor.interface.ts @@ -58,12 +58,9 @@ export interface Combination { } export interface StateValue { selectedPeriod: string; - selectedMinOption: SelectedMinOption; selectedHourOption: SelectedHourOption; selectedDayOption: SelectedDayOption; selectedWeekOption: SelectedWeekOption; - selectedMonthOption: SelectedMonthOption; - selectedYearOption: SelectedYearOption; } export interface ToDisplay { diff --git a/openmetadata-ui/src/main/resources/ui/src/components/common/CronEditor/CronEditor.tsx b/openmetadata-ui/src/main/resources/ui/src/components/common/CronEditor/CronEditor.tsx index 3b2f92d7c366..e0b8f78256ef 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/common/CronEditor/CronEditor.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/common/CronEditor/CronEditor.tsx @@ -11,56 +11,39 @@ * limitations under the License. */ -import { Col, Form, Input, Row, Select } from 'antd'; +import { Button, Col, Form, Input, Row, Select } from 'antd'; import classNames from 'classnames'; -import cronstrue from 'cronstrue'; +import cronstrue from 'cronstrue/i18n'; import { isEmpty } from 'lodash'; import React, { ChangeEvent, useCallback, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { DEFAULT_SCHEDULE_CRON } from '../../../constants/Ingestions.constant'; -import { pluralize } from '../../../utils/CommonUtils'; +import { CronTypes } from '../../../enums/Cron.enum'; import { getCron, + getCronOptions, + getHourMinuteSelect, getQuartzCronExpression, getStateValue, } from '../../../utils/CronUtils'; +import { getCurrentLocaleForConstrue } from '../../../utils/i18next/i18nextUtil'; import './cron-editor.less'; -import { - getDayOptions, - getHourOptions, - getMinuteOptions, - getMinuteSegmentOptions, - getMonthDaysOptions, - getMonthOptions, - getPeriodOptions, -} from './CronEditor.constant'; -import { - CronEditorProp, - CronOption, - SelectedDayOption, - SelectedHourOption, - StateValue, -} from './CronEditor.interface'; +import { CronEditorProp, StateValue } from './CronEditor.interface'; const CronEditor = ({ value, includePeriodOptions, className, - disabled, + disabled = false, disabledCronChange, onChange, isQuartzCron, }: CronEditorProp) => { const { t } = useTranslation(); - const [state, setState] = useState(getStateValue(value ?? '')); - const [periodOptions] = useState(getPeriodOptions()); - const [minuteSegmentOptions] = useState(getMinuteSegmentOptions()); - const [minuteOptions] = useState(getMinuteOptions()); - const [hourOptions] = useState(getHourOptions()); - const [dayOptions] = useState(getDayOptions()); - const [monthDaysOptions] = useState(getMonthDaysOptions()); - const [monthOptions] = useState(getMonthOptions()); + const [state, setState] = useState(getStateValue(value ?? '')); + + const { periodOptions, dayOptions } = useMemo(() => getCronOptions(), []); const filteredPeriodOptions = useMemo(() => { if (includePeriodOptions) { @@ -81,9 +64,6 @@ const CronEditor = ({ [onChange] ); - const startText = t('label.schedule-to-run-every'); - const cronPeriodString = `${startText} ${selectedPeriod}`; - const changeValue = (state: StateValue) => { const cronExp = isQuartzCron ? getQuartzCronExpression(state) @@ -102,420 +82,26 @@ const CronEditor = ({ }; const onHourOptionSelect = (value: number, key: string) => { - const obj = { [key]: value }; - const { selectedHourOption } = state; - const hourOption = Object.assign({}, selectedHourOption, obj); + const hourOption = { ...selectedHourOption, [key]: value }; changeValue({ ...state, selectedHourOption: hourOption }); setState((prev) => ({ ...prev, selectedHourOption: hourOption })); }; - const onMinOptionSelect = (value: number, key: string) => { - const obj = { [key]: value }; - - const { selectedMinOption } = state; - const minOption = Object.assign({}, selectedMinOption, obj); - changeValue({ ...state, selectedMinOption: minOption }); - setState((prev) => ({ ...prev, selectedMinOption: minOption })); - }; - const onDayOptionSelect = (value: number, key: string) => { - const obj = { [key]: value }; - const { selectedDayOption } = state; - const dayOption = Object.assign({}, selectedDayOption, obj); + const dayOption = { ...selectedDayOption, [key]: value }; changeValue({ ...state, selectedDayOption: dayOption }); setState((prev) => ({ ...prev, selectedDayOption: dayOption })); }; const onWeekOptionSelect = (value: number, key: string) => { - const obj = { - [key]: value, - }; - const { selectedWeekOption } = state; - const weekOption = Object.assign({}, selectedWeekOption, obj); + const weekOption = { ...selectedWeekOption, [key]: value }; changeValue({ ...state, selectedWeekOption: weekOption }); setState((prev) => ({ ...prev, selectedWeekOption: weekOption })); }; - const onMonthOptionSelect = (value: number, key: string) => { - const obj = { [key]: value }; - - const { selectedMonthOption } = state; - const monthOption = Object.assign({}, selectedMonthOption, obj); - changeValue({ ...state, selectedMonthOption: monthOption }); - setState((prev) => ({ ...prev, selectedMonthOption: monthOption })); - }; - - const onYearOptionSelect = (value: number, key: string) => { - const obj = { - [key]: value, - }; - - const { selectedYearOption } = state; - const yearOption = Object.assign({}, selectedYearOption, obj); - changeValue({ ...state, selectedYearOption: yearOption }); - setState((prev) => ({ ...prev, selectedYearOption: yearOption })); - }; - - const getOptionComponent = () => { - const optionRenderer = (o: CronOption) => { - return { label: o.label, value: o.value }; - }; - - return optionRenderer; - }; - - const findHourOption = (hour: number) => { - return hourOptions.find((h) => { - return h.value === hour; - }); - }; - - const findMinuteOption = (min: number) => { - return minuteOptions.find((h) => { - return h.value === min; - }); - }; - - const getHourSelect = ( - selectedOption: SelectedDayOption, - onChangeCB: (value: number) => void - ) => { - return ( - - ); - }; - - const getMinuteSegmentSelect = ( - selectedOption: SelectedHourOption, - onChangeCB: (value: number) => void - ) => { - return ( - +); diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/IngestionListTableUtils.tsx b/openmetadata-ui/src/main/resources/ui/src/utils/IngestionListTableUtils.tsx index b1fbb367840a..f70fb35c8000 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/IngestionListTableUtils.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/utils/IngestionListTableUtils.tsx @@ -12,7 +12,7 @@ */ import { Col, Row, Tag, Typography } from 'antd'; -import cronstrue from 'cronstrue'; +import cronstrue from 'cronstrue/i18n'; import { t } from 'i18next'; import { capitalize, isUndefined } from 'lodash'; import React from 'react'; @@ -21,6 +21,7 @@ import { NO_DATA_PLACEHOLDER } from '../constants/constants'; import { PIPELINE_INGESTION_RUN_STATUS } from '../constants/pipeline.constants'; import { IngestionPipeline } from '../generated/entity/services/ingestionPipelines/ingestionPipeline'; import { getEntityName } from './EntityUtils'; +import { getCurrentLocaleForConstrue } from './i18next/i18nextUtil'; export const renderNameField = (_: string, record: IngestionPipeline) => ( { { use24HourTimeFormat: false, verbose: true, + locale: getCurrentLocaleForConstrue(), // To get localized string } ); diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/i18next/i18nextUtil.ts b/openmetadata-ui/src/main/resources/ui/src/utils/i18next/i18nextUtil.ts index 3d353785e0cb..18e7b6f51c0e 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/i18next/i18nextUtil.ts +++ b/openmetadata-ui/src/main/resources/ui/src/utils/i18next/i18nextUtil.ts @@ -11,7 +11,7 @@ * limitations under the License. */ -import { InitOptions } from 'i18next'; +import i18next, { InitOptions } from 'i18next'; import { map, upperCase } from 'lodash'; import deDe from '../../locale/languages/de-de.json'; import enUS from '../../locale/languages/en-us.json'; @@ -72,3 +72,18 @@ export const getInitOptions = (): InitOptions => { saveMissing: true, // Required for missing key handler }; }; + +// Returns the current locale to use in cronstrue +export const getCurrentLocaleForConstrue = () => { + // For cronstrue, we need to pass the locale in the format 'pt_BR' and not 'pt-BR' + // for some selected languages + if ( + [SupportedLocales.Português, SupportedLocales.简体中文].includes( + i18next.resolvedLanguage as SupportedLocales + ) + ) { + return i18next.resolvedLanguage.replaceAll('-', '_'); + } + + return i18next.resolvedLanguage.split('-')[0]; +}; From f4b1978046800a549abe15da7f655e1760658c98 Mon Sep 17 00:00:00 2001 From: Aniket Katkar Date: Tue, 15 Oct 2024 17:56:49 +0530 Subject: [PATCH 04/18] Improvements and bug fixes for CronEditor --- .../AddIngestion/Steps/ScheduleInterval.tsx | 43 ++- .../common/CronEditor/CronEditor.interface.ts | 8 +- .../common/CronEditor/CronEditor.tsx | 296 ++++++++---------- .../common/CronEditor/cron-editor.less | 55 +--- .../ui/src/constants/Ingestions.constant.ts | 3 + .../ui/src/locale/languages/en-us.json | 1 + .../main/resources/ui/src/utils/CronUtils.tsx | 123 ++------ 7 files changed, 199 insertions(+), 330 deletions(-) diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Settings/Services/AddIngestion/Steps/ScheduleInterval.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Settings/Services/AddIngestion/Steps/ScheduleInterval.tsx index 0af6c18de139..1d05ddda7acb 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Settings/Services/AddIngestion/Steps/ScheduleInterval.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Settings/Services/AddIngestion/Steps/ScheduleInterval.tsx @@ -25,8 +25,9 @@ import { } from 'antd'; import classNames from 'classnames'; import { isEmpty } from 'lodash'; -import React, { useCallback, useMemo } from 'react'; +import React, { useCallback, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; +import { DEFAULT_SCHEDULE_CRON } from '../../../../../constants/Ingestions.constant'; import { SCHEDULAR_OPTIONS } from '../../../../../constants/Schedular.constants'; import { LOADING_STATE } from '../../../../../enums/common.enum'; import { SchedularOptions } from '../../../../../enums/Schedular.enum'; @@ -35,9 +36,11 @@ import { FieldTypes, FormItemLayout, } from '../../../../../interface/FormUtils.interface'; +import { getCron, getStateValue } from '../../../../../utils/CronUtils'; import { generateFormFields } from '../../../../../utils/formUtils'; import { getDefaultIngestionSchedule } from '../../../../../utils/IngestionUtils'; import CronEditor from '../../../../common/CronEditor/CronEditor'; +import { StateValue } from '../../../../common/CronEditor/CronEditor.interface'; import { ScheduleIntervalProps } from '../IngestionWorkflow.interface'; import './schedule-interval.less'; @@ -57,12 +60,18 @@ const ScheduleInterval = ({ isEditMode = false, }: ScheduleIntervalProps) => { const { t } = useTranslation(); + const initialValues = getStateValue( + scheduleInterval || DEFAULT_SCHEDULE_CRON + ); + const [state, setState] = useState(initialValues); const [selectedSchedular, setSelectedSchedular] = React.useState( isEmpty(scheduleInterval) ? SchedularOptions.ON_DEMAND : SchedularOptions.SCHEDULE ); + const [form] = Form.useForm(); + const { scheduleInterval: scheduleIntervalFormValue } = state; const handleSelectedSchedular = useCallback( (value: SchedularOptions) => { @@ -71,16 +80,20 @@ const ScheduleInterval = ({ onChange(''); } else { onChange( - isEditMode && !isEmpty(savedScheduleInterval) - ? savedScheduleInterval - : getDefaultIngestionSchedule({ - scheduleInterval, - isEditMode, - }) + getDefaultIngestionSchedule({ + scheduleInterval: + scheduleIntervalFormValue ?? savedScheduleInterval, + isEditMode, + }) ); } }, - [isEditMode, selectedSchedular, scheduleInterval] + [ + isEditMode, + selectedSchedular, + savedScheduleInterval, + scheduleIntervalFormValue, + ] ); const formFields: FieldProp[] = useMemo( @@ -112,11 +125,23 @@ const ScheduleInterval = ({ [onDeploy] ); + const handleValuesChange = (values: StateValue) => { + const newState = { ...state, ...values }; + const cronExp = getCron(newState); + const updatedState = { ...newState, scheduleInterval: cronExp }; + form.setFieldsValue(updatedState); + setState(updatedState); + onChange(cronExp ?? DEFAULT_SCHEDULE_CRON); + }; + return (
+ onFinish={handleFormSubmit} + onValuesChange={handleValuesChange}> { const { t } = useTranslation(); - - const [state, setState] = useState(getStateValue(value ?? '')); + const form = Form.useFormInstance(); + const selectedPeriod = Form.useWatch('selectedPeriod', form); + const scheduleInterval = Form.useWatch('scheduleInterval', form); + const dow = Form.useWatch('dow', form); + const { + showMinuteSelect, + showHourSelect, + showWeekSelect, + minuteCol, + hourCol, + weekCol, + } = useMemo(() => { + const isHourSelected = selectedPeriod === 'hour'; + const isDaySelected = selectedPeriod === 'day'; + const isWeekSelected = selectedPeriod === 'week'; + const showMinuteSelect = isHourSelected || isDaySelected || isWeekSelected; + const showHourSelect = isDaySelected || isWeekSelected; + const showWeekSelect = isWeekSelected; + const minuteCol = isHourSelected ? 12 : 6; + + return { + showMinuteSelect, + showHourSelect, + showWeekSelect, + minuteCol: showMinuteSelect ? minuteCol : 0, + hourCol: showHourSelect ? 6 : 0, + weekCol: showWeekSelect ? 24 : 0, + }; + }, [selectedPeriod]); const { periodOptions, dayOptions } = useMemo(() => getCronOptions(), []); @@ -55,63 +78,21 @@ const CronEditor = ({ } }, [includePeriodOptions, periodOptions]); - const { selectedPeriod } = state; - - const handleCronValueChange = useCallback( - (e: ChangeEvent) => { - onChange(e.target.value); - }, - [onChange] - ); - const changeValue = (state: StateValue) => { - const cronExp = isQuartzCron - ? getQuartzCronExpression(state) - : getCron(state); + const cronExp = getCron(state); onChange(cronExp ?? DEFAULT_SCHEDULE_CRON); }; - const onPeriodSelect = (value: string) => { - changeValue({ ...state, selectedPeriod: value }); - if (value === 'custom') { - onChange(DEFAULT_SCHEDULE_CRON); - } else if (value === '') { - onChange(''); - } - setState((prev) => ({ ...prev, selectedPeriod: value })); - }; - - const onHourOptionSelect = (value: number, key: string) => { - const { selectedHourOption } = state; - const hourOption = { ...selectedHourOption, [key]: value }; - changeValue({ ...state, selectedHourOption: hourOption }); - setState((prev) => ({ ...prev, selectedHourOption: hourOption })); - }; - - const onDayOptionSelect = (value: number, key: string) => { - const { selectedDayOption } = state; - const dayOption = { ...selectedDayOption, [key]: value }; - changeValue({ ...state, selectedDayOption: dayOption }); - setState((prev) => ({ ...prev, selectedDayOption: dayOption })); - }; - - const onWeekOptionSelect = (value: number, key: string) => { - const { selectedWeekOption } = state; - const weekOption = { ...selectedWeekOption, [key]: value }; - changeValue({ ...state, selectedWeekOption: weekOption }); - setState((prev) => ({ ...prev, selectedWeekOption: weekOption })); - }; - return ( + labelCol={{ span: 24 }} + name="selectedPeriod"> - - - )} + + + + + + + + + - {state.selectedPeriod === 'week' && ( - <> - - -
- {getHourMinuteSelect({ - cronType: CronTypes.HOUR, - selectedDayOption: state.selectedWeekOption, - onChange: (value: number) => - onWeekOptionSelect(value, 'hour'), - disabled, - })} - : - {getHourMinuteSelect({ - cronType: CronTypes.MINUTE, - selectedHourOption: state.selectedWeekOption, - onChange: (value: number) => onWeekOptionSelect(value, 'min'), - disabled, - })} -
-
- - - -
- {dayOptions.map(({ label, value: optionValue }) => ( - - ))} -
-
- - - )} - - {state.selectedPeriod === 'hour' && ( - - {getHourMinuteSelect({ - cronType: CronTypes.MINUTE, - selectedHourOption: state.selectedHourOption, - onChange: (value: number) => onHourOptionSelect(value, 'min'), - disabled, - })} - - )} - {state.selectedPeriod === 'day' && ( - -
- {getHourMinuteSelect({ - cronType: CronTypes.HOUR, - selectedDayOption: state.selectedDayOption, - onChange: (value: number) => onDayOptionSelect(value, 'hour'), - disabled, - })} - : - {getHourMinuteSelect({ - cronType: CronTypes.MINUTE, - selectedHourOption: state.selectedDayOption, - onChange: (value: number) => onDayOptionSelect(value, 'min'), - disabled, - })} -
-
- )} + + - {value && ( + {scheduleInterval && ( - {cronstrue.toString(value, { + {cronstrue.toString(scheduleInterval, { use24HourTimeFormat: false, verbose: true, locale: getCurrentLocaleForConstrue(), // To get localized string @@ -255,7 +207,7 @@ const CronEditor = ({ )} - {isEmpty(value) && ( + {isEmpty(scheduleInterval) && (

{t('message.pipeline-will-trigger-manually')} diff --git a/openmetadata-ui/src/main/resources/ui/src/components/common/CronEditor/cron-editor.less b/openmetadata-ui/src/main/resources/ui/src/components/common/CronEditor/cron-editor.less index 34016ec11465..5bf912efe67d 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/common/CronEditor/cron-editor.less +++ b/openmetadata-ui/src/main/resources/ui/src/components/common/CronEditor/cron-editor.less @@ -13,49 +13,22 @@ @import (reference) url('../../../styles/variables.less'); -.form-field.cron-field select { - background: white; - border: 1px solid #ccc; - border-radius: 4px; - padding: 5px; -} -.cron-field-row { - margin: 0 0 10px 0; -} -.cron-string { - margin-top: 15px; - background: #7147e840; - color: #37352f; - padding: 7px 10px; - border-left: 3px solid @primary-color; -} -.cron-badge-option-container { - display: inline-block; - margin: 10px 0 10px 0; -} -.ant-btn.cron-badge-option { - height: 32px; - width: 30px; - background: #f5f5f5; - border-color: #f5f5f5; - margin: 2px 5px; - padding: 5px; - display: inline-table; - text-align: center; - border-radius: 15px; - cursor: pointer; -} -.ant-btn.cron-badge-option.disabled { - cursor: not-allowed; - color: #6b7280; -} -.ant-btn.cron-badge-option.active, -.ant-btn[disabled].cron-badge-option.active { - background: @primary-color; - color: white; -} .cron-row { .ant-form-item { margin: 0px; } } +.ant-radio-button-wrapper.week-selector-buttons, +.ant-radio-button-wrapper.week-selector-buttons:first-child, +.ant-radio-button-wrapper.week-selector-buttons:last-child { + height: 32px; + width: 32px; + display: flex; + align-items: center; + justify-content: center; + border-radius: 16px; + border-width: 1px; +} +.ant-radio-button-wrapper.week-selector-buttons:not(:first-child)::before { + position: relative; +} diff --git a/openmetadata-ui/src/main/resources/ui/src/constants/Ingestions.constant.ts b/openmetadata-ui/src/main/resources/ui/src/constants/Ingestions.constant.ts index 0ac7500e5ce1..dffaa3d52643 100644 --- a/openmetadata-ui/src/main/resources/ui/src/constants/Ingestions.constant.ts +++ b/openmetadata-ui/src/main/resources/ui/src/constants/Ingestions.constant.ts @@ -55,3 +55,6 @@ export const DBT_CLASSIFICATION_DEFAULT_VALUE = 'dbtTags'; export const DEFAULT_PARSING_TIMEOUT_LIMIT = 300; export const DEFAULT_SCHEDULE_CRON = '0 0 * * *'; +export const DEFAULT_CRON_MIN_VALUE = 0; +export const DEFAULT_CRON_HOUR_VALUE = 0; +export const DEFAULT_CRON_WEEK_VALUE = 1; diff --git a/openmetadata-ui/src/main/resources/ui/src/locale/languages/en-us.json b/openmetadata-ui/src/main/resources/ui/src/locale/languages/en-us.json index e0fd3d851e1d..9a4f2a34f339 100644 --- a/openmetadata-ui/src/main/resources/ui/src/locale/languages/en-us.json +++ b/openmetadata-ui/src/main/resources/ui/src/locale/languages/en-us.json @@ -1418,6 +1418,7 @@ "create-new-glossary-guide": "A Glossary is a controlled vocabulary used to define the concepts and terminology in an organization. Glossaries can be specific to a certain domain (for e.g., Business Glossary, Technical Glossary). In the glossary, the standard terms and concepts can be defined along with the synonyms, and related terms. Control can be established over how and who can add the terms in the glossary.", "create-or-update-email-account-for-bot": "Changing the account email will update or create a new bot user.", "created-this-task-lowercase": "created this task", + "cron-less-than-hour-message": "Cron schedule too frequent. Please choose at least 1-hour intervals.", "custom-classification-name-dbt-tags": "Custom OpenMetadata Classification name for dbt tags ", "custom-favicon-url-path-message": "URL path for the favicon icon.", "custom-logo-configuration-message": "Customize OpenMetadata with your company logo, monogram, and favicon.", diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/CronUtils.tsx b/openmetadata-ui/src/main/resources/ui/src/utils/CronUtils.tsx index de64bb15e2df..a63d5b9d1206 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/CronUtils.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/utils/CronUtils.tsx @@ -12,7 +12,7 @@ */ import { Select } from 'antd'; -import { isEmpty, toNumber } from 'lodash'; +import { isNaN, toNumber } from 'lodash'; import React from 'react'; import { combinations, @@ -23,64 +23,27 @@ import { getMinuteOptions, getPeriodOptions, getWeekCron, - SELECTED_PERIOD_OPTIONS, - toDisplay, } from '../components/common/CronEditor/CronEditor.constant'; import { Combination, CronOption, - CronType, - CronValue, - SelectedDayOption, - SelectedHourOption, - SelectedYearOption, StateValue, - ToDisplay, } from '../components/common/CronEditor/CronEditor.interface'; import { CronTypes } from '../enums/Cron.enum'; import { AppType } from '../generated/entity/applications/app'; -export const getQuartzCronExpression = (state: StateValue) => { - const { - selectedPeriod, - selectedHourOption, - selectedDayOption, - selectedWeekOption, - } = state; - - switch (selectedPeriod) { - case 'hour': - return `0 ${selectedHourOption.min} * * * ?`; - case 'day': - return `0 ${selectedDayOption.min} ${selectedDayOption.hour} * * ?`; - case 'week': - return `0 ${selectedWeekOption.min} ${selectedWeekOption.hour} ? * ${ - // Quartz cron format accepts 1-7 or SUN-SAT so need to increment index by 1 - // Ref: https://www.quartz-scheduler.org/api/2.1.7/org/quartz/CronExpression.html - selectedWeekOption.dow + 1 - }`; - default: - return null; - } -}; - export const getCron = (state: StateValue) => { - const { - selectedPeriod, - selectedHourOption, - selectedDayOption, - selectedWeekOption, - } = state; + const { selectedPeriod, ...otherValues } = state; switch (selectedPeriod) { case 'hour': - return getHourCron(selectedHourOption); + return getHourCron(otherValues); case 'day': - return getDayCron(selectedDayOption); + return getDayCron(otherValues); case 'week': - return getWeekCron(selectedWeekOption); + return getWeekCron(otherValues); default: - return null; + return otherValues.scheduleInterval; } }; @@ -95,56 +58,20 @@ const getCronType = (cronStr: string) => { }; export const getStateValue = (valueStr: string) => { - const stateVal: StateValue = { - selectedPeriod: '', - selectedHourOption: { - min: 0, - }, - selectedDayOption: { - hour: 0, - min: 0, - }, - selectedWeekOption: { - dow: 1, - hour: 0, - min: 0, - }, - }; - const cronType = getCronType(valueStr); - const d = valueStr ? valueStr.split(' ') : []; - const v: CronValue = { - min: d[0], - hour: d[1], - dom: d[2], - mon: d[3], - dow: d[4], - }; - - stateVal.selectedPeriod = cronType || stateVal.selectedPeriod; + const min = toNumber(d[0]); + const hour = toNumber(d[1]); + const dow = toNumber(d[4]); - if (!isEmpty(cronType) && cronType !== 'custom') { - const stateIndex = - SELECTED_PERIOD_OPTIONS[(cronType as CronType) || 'hour']; - const selectedPeriodObj = stateVal[ - stateIndex as keyof StateValue - ] as SelectedYearOption; - - const targets = toDisplay[cronType as keyof ToDisplay]; - - for (const element of targets) { - const tgt = element; + const cronType = getCronType(valueStr); - if (tgt === 'time') { - selectedPeriodObj.hour = toNumber(v.hour); - selectedPeriodObj.min = toNumber(v.min); - } else { - selectedPeriodObj[tgt as keyof SelectedYearOption] = toNumber( - v[tgt as keyof CronValue] - ); - } - } - } + const stateVal: StateValue = { + selectedPeriod: cronType, + scheduleInterval: valueStr, + min, + hour, + dow: isNaN(dow) ? 1 : dow, + }; return stateVal; }; @@ -186,16 +113,10 @@ export const getCronOptions = () => { export const getHourMinuteSelect = ({ cronType, - disabled, - selectedDayOption, - selectedHourOption, - onChange, + disabled = false, }: { cronType: CronTypes.MINUTE | CronTypes.HOUR; - disabled: boolean; - selectedDayOption?: SelectedDayOption; - selectedHourOption?: SelectedHourOption; - onChange: (value: number) => void; + disabled?: boolean; }) => ( ({ + label, + value, + }))} + /> + + + + + + + + + + + + + + + + + + {cronString && ( + + {cronstrue.toString(cronString, { + use24HourTimeFormat: false, + verbose: true, + locale: getCurrentLocaleForConstrue(), // To get localized string + throwExceptionOnParseError: false, + })} + + )} + + {isEmpty(cronString) && ( + +

+ {t('message.pipeline-will-trigger-manually')} +

+ + )} +
)} - {allowEnableDebugLog && ( + {debugLog.allow && ( {generateFormFields(formFields)} )} @@ -192,7 +356,7 @@ const ScheduleInterval = ({ data-testid="back-button" type="link" onClick={onBack}> - {t('label.back')} + {buttonProps?.cancelText ?? t('label.back')} {status === 'success' ? ( @@ -209,7 +373,7 @@ const ScheduleInterval = ({ htmlType="submit" loading={status === LOADING_STATE.WAITING} type="primary"> - {submitButtonLabel} + {buttonProps?.okText ?? t('label.submit')} )} diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Settings/Services/AddIngestion/Steps/schedule-interval.less b/openmetadata-ui/src/main/resources/ui/src/components/Settings/Services/AddIngestion/Steps/schedule-interval.less index c9e550703cee..8a17ab8d76e6 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Settings/Services/AddIngestion/Steps/schedule-interval.less +++ b/openmetadata-ui/src/main/resources/ui/src/components/Settings/Services/AddIngestion/Steps/schedule-interval.less @@ -18,7 +18,7 @@ align-items: center; justify-content: center; gap: 16px; - height: 130px; + height: 100%; .schedular-card { border-radius: 6px; cursor: pointer; @@ -36,3 +36,23 @@ border-color: @primary-color; } } + +.cron-row { + .ant-form-item { + margin: 0px; + } +} +.ant-radio-button-wrapper.week-selector-buttons, +.ant-radio-button-wrapper.week-selector-buttons:first-child, +.ant-radio-button-wrapper.week-selector-buttons:last-child { + height: 32px; + width: 32px; + display: flex; + align-items: center; + justify-content: center; + border-radius: 16px; + border-width: 1px; +} +.ant-radio-button-wrapper.week-selector-buttons:not(:first-child)::before { + position: relative; +} diff --git a/openmetadata-ui/src/main/resources/ui/src/components/common/CronEditor/CronEditor.constant.ts b/openmetadata-ui/src/main/resources/ui/src/components/common/CronEditor/CronEditor.constant.ts deleted file mode 100644 index 3b5d43c5f9c8..000000000000 --- a/openmetadata-ui/src/main/resources/ui/src/components/common/CronEditor/CronEditor.constant.ts +++ /dev/null @@ -1,181 +0,0 @@ -/* - * Copyright 2022 Collate. - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * http://www.apache.org/licenses/LICENSE-2.0 - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import i18n from '../../../utils/i18next/LocalUtil'; -import { Combination, ToDisplay } from './CronEditor.interface'; - -/* eslint-disable @typescript-eslint/no-explicit-any */ -export const getPeriodOptions = () => { - return [ - { - label: i18n.t('label.hour'), - value: 'hour', - prep: 'at', - }, - { - label: i18n.t('label.day'), - value: 'day', - prep: 'at', - }, - { - label: i18n.t('label.week'), - value: 'week', - prep: 'on', - }, - { - label: i18n.t('label.custom'), - value: 'custom', - }, - ]; -}; - -export const toDisplay: ToDisplay = { - hour: ['min'], - day: ['time'], - week: ['dow', 'time'], -}; - -export const combinations: Combination = { - hour: /^\d{1,2}\s(\*\s){3}\*$/, // "? * * * *" - day: /^(\d{1,2}\s){2}(\*\s){2}\*$/, // "? ? * * *" - week: /^(\d{1,2}\s){2}(\*\s){2}\d{1,2}$/, // "? ? * * ?" -}; - -export const getRange = (n: number) => { - return [...Array(n).keys()]; -}; - -export const getRangeOptions = (n: number) => { - return getRange(n).map((v) => { - return { - label: `0${v}`.slice(-2), - value: v, - }; - }); -}; - -export const getMinuteOptions = () => { - return getRangeOptions(60); -}; - -export const getHourOptions = () => { - return getRangeOptions(24); -}; - -const ordinalSuffix = (n: number) => { - const suffixes = ['th', 'st', 'nd', 'rd']; - const val = n % 100; - - return `${n}${suffixes[(val - 20) % 10] || suffixes[val] || suffixes[0]}`; -}; - -export const getDayOptions = () => { - return [ - { - label: i18n.t('label.sunday'), - value: 0, - }, - { - label: i18n.t('label.monday'), - value: 1, - }, - { - label: i18n.t('label.tuesday'), - value: 2, - }, - { - label: i18n.t('label.wednesday'), - value: 3, - }, - { - label: i18n.t('label.thursday'), - value: 4, - }, - { - label: i18n.t('label.friday'), - value: 5, - }, - { - label: i18n.t('label.saturday'), - value: 6, - }, - ]; -}; - -export const getMonthDaysOptions = () => { - return getRange(31).map((v) => { - return { - label: ordinalSuffix(v + 1), - value: v + 1, - }; - }); -}; - -export const monthsList = () => { - return [ - i18n.t('label.january'), - i18n.t('label.february'), - i18n.t('label.march'), - i18n.t('label.april'), - i18n.t('label.may'), - i18n.t('label.june'), - i18n.t('label.july'), - i18n.t('label.august'), - i18n.t('label.september'), - i18n.t('label.october'), - i18n.t('label.november'), - i18n.t('label.december'), - ]; -}; - -export const getMonthOptions = () => { - return monthsList().map((m, index) => { - return { - label: m, - value: index + 1, - }; - }); -}; - -export const getMinuteCron = (value: any) => { - return `*/${value.min} * * * *`; -}; - -export const getHourCron = (value: any) => { - return `${value.min} * * * *`; -}; - -export const getDayCron = (value: any) => { - return `${value.min} ${value.hour} * * *`; -}; - -export const getWeekCron = (value: any) => { - return `${value.min} ${value.hour} * * ${value.dow}`; -}; - -export const getMonthCron = (value: any) => { - return `${value.min} ${value.hour} ${value.dom} * *`; -}; - -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', - month: 'selectedMonthOption', - year: 'selectedYearOption', -}; diff --git a/openmetadata-ui/src/main/resources/ui/src/components/common/CronEditor/CronEditor.interface.ts b/openmetadata-ui/src/main/resources/ui/src/components/common/CronEditor/CronEditor.interface.ts deleted file mode 100644 index 6d65b8a2a9fc..000000000000 --- a/openmetadata-ui/src/main/resources/ui/src/components/common/CronEditor/CronEditor.interface.ts +++ /dev/null @@ -1,87 +0,0 @@ -/* - * Copyright 2022 Collate. - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * http://www.apache.org/licenses/LICENSE-2.0 - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -export interface SelectedMinOption { - min: number; -} - -export interface SelectedHourOption { - min: number; -} - -export interface SelectedDayOption { - hour: number; - min: number; -} - -export interface SelectedWeekOption { - dow: number; - hour: number; - min: number; -} - -export interface SelectedMonthOption { - dom: number; - hour: number; - min: number; -} - -export interface SelectedYearOption { - dom: number; - mon: number; - hour: number; - min: number; -} - -export interface CronValue { - min: string; - hour: string; - dom: string; - mon: string; - dow: string; -} - -export interface Combination { - hour: RegExp; - day: RegExp; - week: RegExp; -} -export interface StateValue { - selectedPeriod: string; - hour: number; - min: number; - dow: number; - scheduleInterval: string; -} - -export interface ToDisplay { - hour: Array; - day: Array; - week: Array; -} - -export interface CronOption { - label: string; - value: number; -} - -export interface CronEditorProp { - onChange: (value: string) => void; - value?: string; - className?: string; - disabled?: boolean; - disabledCronChange?: boolean; - includePeriodOptions?: string[]; -} - -export type CronType = 'minute' | 'hour' | 'day' | 'week'; diff --git a/openmetadata-ui/src/main/resources/ui/src/components/common/CronEditor/CronEditor.test.tsx b/openmetadata-ui/src/main/resources/ui/src/components/common/CronEditor/CronEditor.test.tsx deleted file mode 100644 index adfadf587f61..000000000000 --- a/openmetadata-ui/src/main/resources/ui/src/components/common/CronEditor/CronEditor.test.tsx +++ /dev/null @@ -1,206 +0,0 @@ -/* - * Copyright 2022 Collate. - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * http://www.apache.org/licenses/LICENSE-2.0 - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { - act, - findByRole, - findByText, - findByTitle, - fireEvent, - getByTitle, - render, - screen, - waitForElement, -} from '@testing-library/react'; -import userEvent from '@testing-library/user-event'; -import React from 'react'; -import CronEditor from './CronEditor'; -import { CronEditorProp } from './CronEditor.interface'; - -const mockProps: CronEditorProp = { - onChange: jest.fn, -}; - -const getHourDescription = (value: string) => - `label.schedule-to-run-every hour ${value} past the hour`; - -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 () => - expect(await screen.findByText(text)).toBeInTheDocument() - ); - await act(async () => { - fireEvent.click(screen.getByText(text)); - }); - - await waitForElement(async () => - expect(await findByText(everyDropdown, text)).toBeInTheDocument() - ); -}; - -describe.skip('Test CronEditor component', () => { - it('CronEditor component should render', async () => { - render(); - - expect(await screen.findByTestId('cron-container')).toBeInTheDocument(); - expect( - await screen.findByTestId('time-dropdown-container') - ).toBeInTheDocument(); - expect(await screen.findByTestId('cron-type')).toBeInTheDocument(); - }); - - it('Hour option should render corresponding component', async () => { - render(); - - await handleScheduleEverySelector('label.hour'); - - expect(screen.getByTestId('schedule-description')).toHaveTextContent( - getHourDescription('0 minute') - ); - - expect( - await screen.findByTestId('hour-segment-container') - ).toBeInTheDocument(); - - const minutesOptions = await screen.findByTestId('minute-options'); - - expect(minutesOptions).toBeInTheDocument(); - - const minuteSelect = await findByRole(minutesOptions, 'combobox'); - - act(() => { - userEvent.click(minuteSelect); - }); - await waitForElement(() => screen.getByText('03')); - await act(async () => { - fireEvent.click(screen.getByText('03')); - }); - - expect(await findByTitle(minutesOptions, '03')).toBeInTheDocument(); - - expect(screen.getByTestId('schedule-description')).toHaveTextContent( - getHourDescription('3 minutes') - ); - }); - - it('Day option should render corresponding component', async () => { - render(); - - await handleScheduleEverySelector('label.day'); - - expect(screen.getByTestId('schedule-description')).toHaveTextContent( - getDayDescription() - ); - - expect( - await screen.findByTestId('day-segment-container') - ).toBeInTheDocument(); - expect( - await screen.findByTestId('time-option-container') - ).toBeInTheDocument(); - - // For Hours Selector - const hourOptions = await screen.findByTestId('hour-options'); - - expect(hourOptions).toBeInTheDocument(); - - const hourSelect = await findByRole(hourOptions, 'combobox'); - act(() => { - userEvent.click(hourSelect); - }); - - 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(); - - 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') - ).toBeInTheDocument(); - expect( - await screen.findByTestId('week-segment-time-options-container') - ).toBeInTheDocument(); - expect( - await screen.findByTestId('week-segment-day-option-container') - ).toBeInTheDocument(); - - // For Hours Selector - const hourOptions = await screen.findByTestId('hour-options'); - - expect(hourOptions).toBeInTheDocument(); - - const hourSelect = await findByRole(hourOptions, 'combobox'); - act(() => { - userEvent.click(hourSelect); - }); - - 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 () => { - render(); - - const cronType = await screen.findByTestId('cron-type'); - await act(async () => { - userEvent.selectOptions(cronType, ''); - }); - - expect( - await screen.findByTestId('manual-segment-container') - ).toBeInTheDocument(); - }); -}); diff --git a/openmetadata-ui/src/main/resources/ui/src/components/common/CronEditor/CronEditor.tsx b/openmetadata-ui/src/main/resources/ui/src/components/common/CronEditor/CronEditor.tsx deleted file mode 100644 index 2e4830a1b2c4..000000000000 --- a/openmetadata-ui/src/main/resources/ui/src/components/common/CronEditor/CronEditor.tsx +++ /dev/null @@ -1,222 +0,0 @@ -/* - * Copyright 2022 Collate. - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * http://www.apache.org/licenses/LICENSE-2.0 - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { Col, Form, Input, Radio, Row, Select } from 'antd'; -import classNames from 'classnames'; -import cronstrue from 'cronstrue/i18n'; -import { isEmpty } from 'lodash'; -import React, { useMemo } from 'react'; -import { useTranslation } from 'react-i18next'; -import { DEFAULT_SCHEDULE_CRON } from '../../../constants/Ingestions.constant'; -import { CronTypes } from '../../../enums/Cron.enum'; -import { - getCron, - getCronOptions, - getHourMinuteSelect, -} from '../../../utils/CronUtils'; -import { getCurrentLocaleForConstrue } from '../../../utils/i18next/i18nextUtil'; -import './cron-editor.less'; -import { CronEditorProp, StateValue } from './CronEditor.interface'; - -const CronEditor = ({ - includePeriodOptions, - className, - disabled = false, - disabledCronChange, - onChange, -}: CronEditorProp) => { - const { t } = useTranslation(); - const form = Form.useFormInstance(); - const selectedPeriod = Form.useWatch('selectedPeriod', form); - const scheduleInterval = Form.useWatch('scheduleInterval', form); - const dow = Form.useWatch('dow', form); - const { - showMinuteSelect, - showHourSelect, - showWeekSelect, - minuteCol, - hourCol, - weekCol, - } = useMemo(() => { - const isHourSelected = selectedPeriod === 'hour'; - const isDaySelected = selectedPeriod === 'day'; - const isWeekSelected = selectedPeriod === 'week'; - const showMinuteSelect = isHourSelected || isDaySelected || isWeekSelected; - const showHourSelect = isDaySelected || isWeekSelected; - const showWeekSelect = isWeekSelected; - const minuteCol = isHourSelected ? 12 : 6; - - return { - showMinuteSelect, - showHourSelect, - showWeekSelect, - minuteCol: showMinuteSelect ? minuteCol : 0, - hourCol: showHourSelect ? 6 : 0, - weekCol: showWeekSelect ? 24 : 0, - }; - }, [selectedPeriod]); - - const { periodOptions, dayOptions } = useMemo(() => getCronOptions(), []); - - const filteredPeriodOptions = useMemo(() => { - if (includePeriodOptions) { - return periodOptions.filter((option) => - includePeriodOptions.includes(option.value) - ); - } else { - return periodOptions; - } - }, [includePeriodOptions, periodOptions]); - - const changeValue = (state: StateValue) => { - const cronExp = getCron(state); - onChange(cronExp ?? DEFAULT_SCHEDULE_CRON); - }; - - return ( - - - - - - - - {scheduleInterval && ( - - {cronstrue.toString(scheduleInterval, { - use24HourTimeFormat: false, - verbose: true, - locale: getCurrentLocaleForConstrue(), // To get localized string - throwExceptionOnParseError: false, - })} - - )} - - {isEmpty(scheduleInterval) && ( - -

- {t('message.pipeline-will-trigger-manually')} -

- - )} -
- ); -}; - -export default CronEditor; diff --git a/openmetadata-ui/src/main/resources/ui/src/constants/Schedular.constants.ts b/openmetadata-ui/src/main/resources/ui/src/constants/Schedular.constants.ts index 8042019cd599..cf6e53049e2a 100644 --- a/openmetadata-ui/src/main/resources/ui/src/constants/Schedular.constants.ts +++ b/openmetadata-ui/src/main/resources/ui/src/constants/Schedular.constants.ts @@ -12,7 +12,9 @@ */ import { t } from 'i18next'; +import { Combination } from '../components/Settings/Services/AddIngestion/Steps/ScheduleInterval.interface'; import { SchedularOptions } from '../enums/Schedular.enum'; +import i18n from '../utils/i18next/LocalUtil'; export const SCHEDULAR_OPTIONS = [ { @@ -26,3 +28,77 @@ export const SCHEDULAR_OPTIONS = [ value: SchedularOptions.ON_DEMAND, }, ]; + +export const PERIOD_OPTIONS = [ + { + label: i18n.t('label.hour'), + value: 'hour', + prep: 'at', + }, + { + label: i18n.t('label.day'), + value: 'day', + prep: 'at', + }, + { + label: i18n.t('label.week'), + value: 'week', + prep: 'on', + }, + { + label: i18n.t('label.custom'), + value: 'custom', + }, +]; + +export const DAY_OPTIONS = [ + { + label: i18n.t('label.sunday'), + value: 0, + }, + { + label: i18n.t('label.monday'), + value: 1, + }, + { + label: i18n.t('label.tuesday'), + value: 2, + }, + { + label: i18n.t('label.wednesday'), + value: 3, + }, + { + label: i18n.t('label.thursday'), + value: 4, + }, + { + label: i18n.t('label.friday'), + value: 5, + }, + { + label: i18n.t('label.saturday'), + value: 6, + }, +]; + +export const MONTHS_LIST = [ + i18n.t('label.january'), + i18n.t('label.february'), + i18n.t('label.march'), + i18n.t('label.april'), + i18n.t('label.may'), + i18n.t('label.june'), + i18n.t('label.july'), + i18n.t('label.august'), + i18n.t('label.september'), + i18n.t('label.october'), + i18n.t('label.november'), + i18n.t('label.december'), +]; + +export const CRON_COMBINATIONS: Combination = { + hour: /^\d{1,2}\s(\*\s){3}\*$/, // "? * * * *" + day: /^(\d{1,2}\s){2}(\*\s){2}\*$/, // "? ? * * *" + week: /^(\d{1,2}\s){2}(\*\s){2}\d{1,2}$/, // "? ? * * ?" +}; diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/AppInstall/AppInstall.component.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/AppInstall/AppInstall.component.tsx index 60eabf8ce79a..7de813537130 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/AppInstall/AppInstall.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/AppInstall/AppInstall.component.tsx @@ -19,25 +19,23 @@ import { isEmpty } from 'lodash'; import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { useHistory } from 'react-router-dom'; -import { getWeekCron } from '../../components/common/CronEditor/CronEditor.constant'; import ErrorPlaceHolder from '../../components/common/ErrorWithPlaceholder/ErrorPlaceHolder'; import FormBuilder from '../../components/common/FormBuilder/FormBuilder'; import Loader from '../../components/common/Loader/Loader'; -import { TestSuiteIngestionDataType } from '../../components/DataQuality/AddDataQualityTest/AddDataQualityTest.interface'; -import TestSuiteScheduler from '../../components/DataQuality/AddDataQualityTest/components/TestSuiteScheduler'; import PageLayoutV1 from '../../components/PageLayoutV1/PageLayoutV1'; import { default as applicationSchemaClassBase, default as applicationsClassBase, } from '../../components/Settings/Applications/AppDetails/ApplicationsClassBase'; import AppInstallVerifyCard from '../../components/Settings/Applications/AppInstallVerifyCard/AppInstallVerifyCard.component'; +import { WorkflowExtraConfig } from '../../components/Settings/Services/AddIngestion/IngestionWorkflow.interface'; +import ScheduleInterval from '../../components/Settings/Services/AddIngestion/Steps/ScheduleInterval'; import IngestionStepper from '../../components/Settings/Services/Ingestion/IngestionStepper/IngestionStepper.component'; import { STEPS_FOR_APP_INSTALL } from '../../constants/Applications.constant'; import { GlobalSettingOptions } from '../../constants/GlobalSettings.constants'; import { useLimitStore } from '../../context/LimitsProvider/useLimitsStore'; import { TabSpecificField } from '../../enums/entity.enum'; import { ServiceCategory } from '../../enums/service.enum'; -import { AppType } from '../../generated/entity/applications/app'; import { CreateAppRequest, ScheduleTimeline, @@ -47,7 +45,7 @@ import { useFqn } from '../../hooks/useFqn'; import { installApplication } from '../../rest/applicationAPI'; import { getMarketPlaceApplicationByFqn } from '../../rest/applicationMarketPlaceAPI'; import { getEntityMissingError } from '../../utils/CommonUtils'; -import { getCronInitialValue } from '../../utils/CronUtils'; +import { getCronInitialValue, getWeekCron } from '../../utils/CronUtils'; import { formatFormDataForSubmit } from '../../utils/JSONSchemaFormUtils'; import { getMarketPlaceAppDetailsPath, @@ -95,14 +93,9 @@ const AppInstall = () => { return { initialOptions, - initialValue: { - repeatFrequency: config?.enable - ? getWeekCron({ hour: 0, min: 0, dow: 0 }) - : getCronInitialValue( - appData?.appType ?? AppType.Internal, - appData?.name ?? '' - ), - }, + initialValue: config?.enable + ? getWeekCron({ hour: 0, min: 0, dow: 0 }) + : getCronInitialValue(appData?.name ?? ''), }; }, [appData?.name, appData?.appType, pipelineSchedules, config?.enable]); @@ -132,17 +125,17 @@ const AppInstall = () => { history.push(getSettingPath(GlobalSettingOptions.APPLICATIONS)); }; - const onSubmit = async (updatedValue: TestSuiteIngestionDataType) => { - const { repeatFrequency } = updatedValue; + const onSubmit = async (updatedValue: WorkflowExtraConfig) => { + const { cron } = updatedValue; try { setIsSavingLoading(true); const data: CreateAppRequest = { appConfiguration: appConfiguration ?? appData?.appConfiguration, appSchedule: { - scheduleTimeline: isEmpty(repeatFrequency) + scheduleTimeline: isEmpty(cron) ? ScheduleTimeline.None : ScheduleTimeline.Custom, - ...(repeatFrequency ? { cronExpression: repeatFrequency } : {}), + ...(cron ? { cronExpression: cron } : {}), }, name: fqn, description: appData?.description, @@ -212,21 +205,28 @@ const AppInstall = () => { return (
{t('label.schedule')} - + initialScheduleInterval={initialValue} + status={isSavingLoading ? 'waiting' : 'initial'} + onBack={() => setActiveServiceStep(appData.allowConfiguration ? 2 : 1) } - onSubmit={onSubmit} + onDeploy={onSubmit} />
); default: return <>; } - }, [activeServiceStep, appData, jsonSchema, initialOptions, isSavingLoading]); + }, [ + activeServiceStep, + appData, + jsonSchema, + initialOptions, + initialValue, + isSavingLoading, + ]); useEffect(() => { fetchAppDetails(); diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/CommonUtils.test.ts b/openmetadata-ui/src/main/resources/ui/src/utils/CommonUtils.test.ts index 9127fce62d0a..554ecbebc755 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/CommonUtils.test.ts +++ b/openmetadata-ui/src/main/resources/ui/src/utils/CommonUtils.test.ts @@ -13,12 +13,7 @@ import { AxiosError } from 'axios'; import { cloneDeep } from 'lodash'; -import { - getDayCron, - getHourCron, -} from '../components/common/CronEditor/CronEditor.constant'; import { ERROR_MESSAGE } from '../constants/constants'; -import { PipelineType } from '../generated/api/services/ingestionPipelines/createIngestionPipeline'; import { LabelType, State, @@ -29,7 +24,6 @@ import { digitFormatter, formatTimeFromSeconds, getBase64EncodedString, - getIngestionFrequency, getIsErrorMatch, getNameFromFQN, getServiceTypeExploreQueryFilter, @@ -264,29 +258,6 @@ describe('Tests for CommonUtils', () => { }); }); - describe('getIngestionFrequency', () => { - it('should return the correct cron value for TestSuite pipeline', () => { - const pipelineType = PipelineType.TestSuite; - const result = getIngestionFrequency(pipelineType); - - expect(result).toEqual(getHourCron({ min: 0, hour: 0 })); - }); - - it('should return the correct cron value for Metadata pipeline', () => { - const pipelineType = PipelineType.Metadata; - const result = getIngestionFrequency(pipelineType); - - expect(result).toEqual(getHourCron({ min: 0, hour: 0 })); - }); - - it('should return the correct cron value for other pipeline types', () => { - const pipelineType = PipelineType.Profiler; - const result = getIngestionFrequency(pipelineType); - - expect(result).toEqual(getDayCron({ min: 0, hour: 0 })); - }); - }); - describe('prepareLabel', () => { it('should return label for table entity type with quotes', () => { const type = 'table'; diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/CommonUtils.tsx b/openmetadata-ui/src/main/resources/ui/src/utils/CommonUtils.tsx index d22b25fa816b..4f2b0685eb42 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/CommonUtils.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/utils/CommonUtils.tsx @@ -39,10 +39,6 @@ import { import React, { ReactNode } from 'react'; import { Trans } from 'react-i18next'; import { reactLocalStorage } from 'reactjs-localstorage'; -import { - getDayCron, - getHourCron, -} from '../components/common/CronEditor/CronEditor.constant'; import ErrorPlaceHolder from '../components/common/ErrorWithPlaceholder/ErrorPlaceHolder'; import Loader from '../components/common/Loader/Loader'; import { FQN_SEPARATOR_CHAR } from '../constants/char.constants'; @@ -60,7 +56,6 @@ import { } from '../constants/regex.constants'; import { SIZE } from '../enums/common.enum'; import { EntityType, FqnPart } from '../enums/entity.enum'; -import { PipelineType } from '../generated/entity/services/ingestionPipelines/ingestionPipeline'; import { EntityReference, User } from '../generated/entity/teams/user'; import { TagLabel } from '../generated/type/tagLabel'; import { FeedCounts } from '../interface/feed.interface'; @@ -683,23 +678,6 @@ export const getOwnerValue = (owner?: EntityReference) => { } }; -export const getIngestionFrequency = (pipelineType: PipelineType) => { - const value = { - min: 0, - hour: 0, - }; - - switch (pipelineType) { - case PipelineType.TestSuite: - case PipelineType.Metadata: - case PipelineType.Application: - return getHourCron(value); - - default: - return getDayCron(value); - } -}; - export const getEmptyPlaceholder = () => { return ; }; diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/CronUtils.test.ts b/openmetadata-ui/src/main/resources/ui/src/utils/CronUtils.test.ts index 43b471b35952..08567dac9360 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/CronUtils.test.ts +++ b/openmetadata-ui/src/main/resources/ui/src/utils/CronUtils.test.ts @@ -11,33 +11,23 @@ * limitations under the License. */ -import { AppType } from '../generated/entity/applications/app'; import { getCronInitialValue } from './CronUtils'; describe('getCronInitialValue function', () => { - it('should generate hour cron expression if appType is internal and appName is not DataInsightsReportApplication', () => { - const result = getCronInitialValue( - AppType.Internal, - 'SearchIndexingApplication' - ); + it('should generate day cron expression if appType is internal and appName is not DataInsightsReportApplication', () => { + const result = getCronInitialValue('SearchIndexingApplication'); - expect(result).toEqual('0 * * * *'); + expect(result).toEqual('0 0 * * *'); }); it('should generate week cron expression if appName is DataInsightsReportApplication', () => { - const result = getCronInitialValue( - AppType.Internal, - 'DataInsightsReportApplication' - ); + const result = getCronInitialValue('DataInsightsReportApplication'); expect(result).toEqual('0 0 * * 0'); }); it('should generate day cron expression if appType is external', () => { - const result = getCronInitialValue( - AppType.External, - 'DataInsightsApplication' - ); + const result = getCronInitialValue('DataInsightsApplication'); expect(result).toEqual('0 0 * * *'); }); diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/CronUtils.tsx b/openmetadata-ui/src/main/resources/ui/src/utils/CronUtils.tsx index a63d5b9d1206..baacf0085250 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/CronUtils.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/utils/CronUtils.tsx @@ -14,42 +14,95 @@ import { Select } from 'antd'; import { isNaN, toNumber } from 'lodash'; import React from 'react'; -import { - combinations, - getDayCron, - getDayOptions, - getHourCron, - getHourOptions, - getMinuteOptions, - getPeriodOptions, - getWeekCron, -} from '../components/common/CronEditor/CronEditor.constant'; import { Combination, CronOption, StateValue, -} from '../components/common/CronEditor/CronEditor.interface'; +} from '../components/Settings/Services/AddIngestion/Steps/ScheduleInterval.interface'; +import { + CRON_COMBINATIONS, + MONTHS_LIST, +} from '../constants/Schedular.constants'; import { CronTypes } from '../enums/Cron.enum'; -import { AppType } from '../generated/entity/applications/app'; + +export const getRange = (n: number) => { + return [...Array(n).keys()]; +}; + +export const getRangeOptions = (n: number) => { + return getRange(n).map((v) => { + return { + label: `0${v}`.slice(-2), + value: v, + }; + }); +}; + +export const getMinuteOptions = () => { + return getRangeOptions(60); +}; + +export const getHourOptions = () => { + return getRangeOptions(24); +}; + +const ordinalSuffix = (n: number) => { + const suffixes = ['th', 'st', 'nd', 'rd']; + const val = n % 100; + + return `${n}${suffixes[(val - 20) % 10] || suffixes[val] || suffixes[0]}`; +}; + +export const getMonthDaysOptions = () => + getRange(31).map((v) => { + return { + label: ordinalSuffix(v + 1), + value: v + 1, + }; + }); + +export const getMonthOptions = () => + MONTHS_LIST.map((month, index) => { + return { + label: month, + value: index + 1, + }; + }); + +export const getMinuteCron = (value: Partial) => { + return `*/${value.min} * * * *`; +}; + +export const getHourCron = (value: Partial) => { + return `${value.min} * * * *`; +}; + +export const getDayCron = (value: Partial) => { + return `${value.min} ${value.hour} * * *`; +}; + +export const getWeekCron = (value: Partial) => { + return `${value.min} ${value.hour} * * ${value.dow}`; +}; export const getCron = (state: StateValue) => { - const { selectedPeriod, ...otherValues } = state; + const { selectedPeriod, cron } = state; switch (selectedPeriod) { case 'hour': - return getHourCron(otherValues); + return getHourCron(state); case 'day': - return getDayCron(otherValues); + return getDayCron(state); case 'week': - return getWeekCron(otherValues); + return getWeekCron(state); default: - return otherValues.scheduleInterval; + return cron; } }; const getCronType = (cronStr: string) => { - for (const c in combinations) { - if (combinations[c as keyof Combination].test(cronStr)) { + for (const c in CRON_COMBINATIONS) { + if (CRON_COMBINATIONS[c as keyof Combination].test(cronStr)) { return c; } } @@ -57,37 +110,37 @@ const getCronType = (cronStr: string) => { return 'custom'; }; -export const getStateValue = (valueStr: string) => { - const d = valueStr ? valueStr.split(' ') : []; +export const getStateValue = (value?: string, defaultValue?: string) => { + const a = value?.split(' '); + const d = a ?? defaultValue?.split(' ') ?? []; + const min = toNumber(d[0]); const hour = toNumber(d[1]); const dow = toNumber(d[4]); - const cronType = getCronType(valueStr); + const cronType = getCronType(value ?? defaultValue ?? ''); const stateVal: StateValue = { selectedPeriod: cronType, - scheduleInterval: valueStr, - min, - hour, + cron: value, + min: isNaN(dow) ? 0 : min, + hour: isNaN(dow) ? 0 : hour, dow: isNaN(dow) ? 1 : dow, }; return stateVal; }; -export const getCronInitialValue = (appType: AppType, appName: string) => { +export const getCronInitialValue = (appName: string) => { const value = { min: 0, hour: 0, }; - let initialValue = getHourCron(value); + let initialValue = getDayCron(value); if (appName === 'DataInsightsReportApplication') { initialValue = getWeekCron({ ...value, dow: 0 }); - } else if (appType === AppType.External) { - initialValue = getDayCron(value); } return initialValue; @@ -101,16 +154,6 @@ const getOptionComponent = () => { return optionRenderer; }; -export const getCronOptions = () => { - const periodOptions = getPeriodOptions(); - const dayOptions = getDayOptions(); - - return { - periodOptions, - dayOptions, - }; -}; - export const getHourMinuteSelect = ({ cronType, disabled = false, diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/IngestionUtils.tsx b/openmetadata-ui/src/main/resources/ui/src/utils/IngestionUtils.tsx index c58657c24489..59385a8e4990 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/IngestionUtils.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/utils/IngestionUtils.tsx @@ -17,7 +17,6 @@ import { t } from 'i18next'; import { isEmpty, isUndefined, startCase } from 'lodash'; import { ServiceTypes } from 'Models'; import React from 'react'; -import { getDayCron } from '../components/common/CronEditor/CronEditor.constant'; import ErrorPlaceHolder from '../components/common/ErrorWithPlaceholder/ErrorPlaceHolder'; import ConnectionStepCard from '../components/common/TestConnection/ConnectionStepCard/ConnectionStepCard'; import { getServiceDetailsPath } from '../constants/constants'; @@ -49,6 +48,7 @@ import { Connection as MetadataConnection } from '../generated/entity/services/m import { SearchSourceAlias } from '../interface/search.interface'; import { DataObj, ServicesType } from '../interface/service.interface'; import { Transi18next } from './CommonUtils'; +import { getDayCron } from './CronUtils'; import { getSettingPath, getSettingsPathWithFqn } from './RouterUtils'; import serviceUtilClassBase from './ServiceUtilClassBase'; import { getServiceRouteFromServiceType } from './ServiceUtils'; diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/formUtils.tsx b/openmetadata-ui/src/main/resources/ui/src/utils/formUtils.tsx index 4b78886477d3..ff5068ce2043 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/formUtils.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/utils/formUtils.tsx @@ -32,8 +32,6 @@ import React, { Fragment, ReactNode } from 'react'; import AsyncSelectList from '../components/common/AsyncSelectList/AsyncSelectList'; import { AsyncSelectListProps } from '../components/common/AsyncSelectList/AsyncSelectList.interface'; import ColorPicker from '../components/common/ColorPicker/ColorPicker.component'; -import CronEditor from '../components/common/CronEditor/CronEditor'; -import { CronEditorProp } from '../components/common/CronEditor/CronEditor.interface'; import DomainSelectableList from '../components/common/DomainSelectableList/DomainSelectableList.component'; import { DomainSelectableListProps } from '../components/common/DomainSelectableList/DomainSelectableList.interface'; import FilterPattern from '../components/common/FilterPattern/FilterPattern'; @@ -218,10 +216,6 @@ export const getField = (field: FieldProp) => { case FieldTypes.COLOR_PICKER: fieldElement = ; - break; - case FieldTypes.CRON_EDITOR: - fieldElement = ; - break; default: From 1171e742d9e7c693945c47cfeb7cf6c1ea8cbdf9 Mon Sep 17 00:00:00 2001 From: Aniket Katkar Date: Wed, 16 Oct 2024 18:32:01 +0530 Subject: [PATCH 08/18] Fix schedular for the test suite ingestion --- .../AddDataQualityTest.interface.ts | 2 +- .../AddDataQualityTest/TestSuiteIngestion.tsx | 15 +- .../components/AddTestSuitePipeline.tsx | 164 +++++++++--------- .../AppSchedule/AppSchedule.component.tsx | 8 +- .../AddIngestion/AddIngestion.component.tsx | 8 +- .../IngestionWorkflow.interface.ts | 32 +--- .../Steps/ScheduleInterval.interface.ts | 33 ++++ .../Steps/ScheduleInterval.test.tsx | 6 +- .../AddIngestion/Steps/ScheduleInterval.tsx | 36 ++-- .../AddIngestion/Steps/schedule-interval.less | 2 +- .../pages/AppInstall/AppInstall.component.tsx | 12 +- 11 files changed, 157 insertions(+), 161 deletions(-) diff --git a/openmetadata-ui/src/main/resources/ui/src/components/DataQuality/AddDataQualityTest/AddDataQualityTest.interface.ts b/openmetadata-ui/src/main/resources/ui/src/components/DataQuality/AddDataQualityTest/AddDataQualityTest.interface.ts index dab203fb5e75..e986c339e06d 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/DataQuality/AddDataQualityTest/AddDataQualityTest.interface.ts +++ b/openmetadata-ui/src/main/resources/ui/src/components/DataQuality/AddDataQualityTest/AddDataQualityTest.interface.ts @@ -43,7 +43,7 @@ export interface TestSuiteIngestionProps { } export type TestSuiteIngestionDataType = { - repeatFrequency: string; + cron?: string; enableDebugLog?: boolean; testCases?: string[]; name?: string; diff --git a/openmetadata-ui/src/main/resources/ui/src/components/DataQuality/AddDataQualityTest/TestSuiteIngestion.tsx b/openmetadata-ui/src/main/resources/ui/src/components/DataQuality/AddDataQualityTest/TestSuiteIngestion.tsx index e3ad4bca2165..8acb61488e90 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/DataQuality/AddDataQualityTest/TestSuiteIngestion.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/DataQuality/AddDataQualityTest/TestSuiteIngestion.tsx @@ -22,7 +22,6 @@ import { DEPLOYED_PROGRESS_VAL, INGESTION_PROGRESS_END_VAL, } from '../../../constants/constants'; -import { DEFAULT_SCHEDULE_CRON } from '../../../constants/Ingestions.constant'; import { useLimitStore } from '../../../context/LimitsProvider/useLimitsStore'; import { FormSubmitType } from '../../../enums/form.enum'; import { IngestionActionMessage } from '../../../enums/ingestion.enum'; @@ -116,13 +115,11 @@ const TestSuiteIngestion: React.FC = ({ ); }, [ingestionData, showDeployButton]); - const initialFormData: TestSuiteIngestionDataType = useMemo(() => { + const initialFormData = useMemo(() => { const testCases = ingestionPipeline?.sourceConfig.config?.testCases; return { - repeatFrequency: - ingestionPipeline?.airflowConfig.scheduleInterval ?? - DEFAULT_SCHEDULE_CRON, + cron: ingestionPipeline?.airflowConfig.scheduleInterval, enableDebugLog: ingestionPipeline?.loggerLevel === LogLevels.Debug, testCases, name: ingestionPipeline?.displayName, @@ -168,9 +165,7 @@ const TestSuiteIngestion: React.FC = ({ const ingestionPayload: CreateIngestionPipeline = { airflowConfig: { - scheduleInterval: isEmpty(data.repeatFrequency) - ? undefined - : data.repeatFrequency, + scheduleInterval: isEmpty(data.cron) ? undefined : data.cron, }, displayName: updatedName, name: generateUUID(), @@ -208,9 +203,7 @@ const TestSuiteIngestion: React.FC = ({ displayName: data.name ?? ingestionPipeline.displayName, airflowConfig: { ...ingestionPipeline?.airflowConfig, - scheduleInterval: isEmpty(data.repeatFrequency) - ? undefined - : data.repeatFrequency, + scheduleInterval: isEmpty(data.cron) ? undefined : data.cron, }, loggerLevel: data.enableDebugLog ? LogLevels.Debug : LogLevels.Info, sourceConfig: { diff --git a/openmetadata-ui/src/main/resources/ui/src/components/DataQuality/AddDataQualityTest/components/AddTestSuitePipeline.tsx b/openmetadata-ui/src/main/resources/ui/src/components/DataQuality/AddDataQualityTest/components/AddTestSuitePipeline.tsx index 93af7ea200e9..98db99a86f7a 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/DataQuality/AddDataQualityTest/components/AddTestSuitePipeline.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/DataQuality/AddDataQualityTest/components/AddTestSuitePipeline.tsx @@ -10,9 +10,10 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { Button, Col, Form, FormProps, Row, Space } from 'antd'; -import { isString } from 'lodash'; -import React from 'react'; +import { Col, Form, Row } from 'antd'; +import { FormProviderProps } from 'antd/lib/form/context'; +import { isEmpty, isString } from 'lodash'; +import React, { useState } from 'react'; import { useTranslation } from 'react-i18next'; import { useHistory } from 'react-router-dom'; import { TestCase } from '../../../../generated/tests/testCase'; @@ -23,8 +24,13 @@ import { FormItemLayout, } from '../../../../interface/FormUtils.interface'; import { generateFormFields } from '../../../../utils/formUtils'; +import ScheduleInterval from '../../../Settings/Services/AddIngestion/Steps/ScheduleInterval'; +import { WorkflowExtraConfig } from '../../../Settings/Services/AddIngestion/Steps/ScheduleInterval.interface'; import { AddTestCaseList } from '../../AddTestCaseList/AddTestCaseList.component'; -import { AddTestSuitePipelineProps } from '../AddDataQualityTest.interface'; +import { + AddTestSuitePipelineProps, + TestSuiteIngestionDataType, +} from '../AddDataQualityTest.interface'; import './add-test-suite-pipeline.style.less'; const AddTestSuitePipeline = ({ @@ -37,9 +43,11 @@ const AddTestSuitePipeline = ({ }: AddTestSuitePipelineProps) => { const { t } = useTranslation(); const history = useHistory(); - const { fqn } = useFqn(); - const [form] = Form.useForm(); - const selectAllTestCases = Form.useWatch('selectAllTestCases', form); + const { fqn, ingestionFQN } = useFqn(); + const [selectAllTestCases, setSelectAllTestCases] = useState( + initialData?.selectAllTestCases + ); + const isEditMode = !isEmpty(ingestionFQN); const formFields: FieldProp[] = [ { @@ -55,30 +63,6 @@ const AddTestSuitePipeline = ({ }, id: 'root/name', }, - { - name: 'enableDebugLog', - label: t('label.enable-debug-log'), - type: FieldTypes.SWITCH, - required: false, - props: { - 'data-testid': 'enable-debug-log', - }, - id: 'root/enableDebugLog', - formItemLayout: FormItemLayout.HORIZONTAL, - }, - { - name: 'repeatFrequency', - label: t('label.schedule-for-entity', { - entity: t('label.test-case-plural'), - }), - type: FieldTypes.CRON_EDITOR, - required: false, - props: { - 'data-testid': 'repeat-frequency', - includePeriodOptions, - }, - id: 'root/repeatFrequency', - }, ]; const testCaseFormFields: FieldProp[] = [ @@ -101,72 +85,82 @@ const AddTestSuitePipeline = ({ history.goBack(); }; - const onFinish: FormProps['onFinish'] = (values) => { - const { testCases, ...rest } = values; + const onFinish = ( + values: WorkflowExtraConfig & TestSuiteIngestionDataType + ) => { + const { cron, enableDebugLog, testCases, name, selectAllTestCases } = + values; onSubmit({ - ...rest, + cron, + enableDebugLog, + name, + selectAllTestCases, testCases: testCases?.map((testCase: TestCase | string) => isString(testCase) ? testCase : testCase.name ), }); }; - const onValuesChange: FormProps['onValuesChange'] = (changedValues) => { - if (changedValues?.selectAllTestCases) { + const handleFromChange: FormProviderProps['onFormChange'] = ( + _, + { forms } + ) => { + const form = forms['schedular-form']; + const value = form.getFieldValue('selectAllTestCases'); + setSelectAllTestCases(value); + if (value) { form.setFieldsValue({ testCases: undefined }); } }; return ( - - {generateFormFields(formFields)} - - {generateFormFields(testCaseFormFields)} - {!selectAllTestCases && ( - - - - - - )} - - - - - - - - + + + {generateFormFields(formFields)} + + {t('label.schedule-for-entity', { + entity: t('label.test-case-plural'), + })} + + + } + onBack={onCancel ?? handleCancelBtn} + onDeploy={onFinish}> + + {generateFormFields(testCaseFormFields)} + {!selectAllTestCases && ( + + + + + + )} + + + ); }; diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Settings/Applications/AppSchedule/AppSchedule.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Settings/Applications/AppSchedule/AppSchedule.component.tsx index c3ef5af4f62b..3e81a988449b 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Settings/Applications/AppSchedule/AppSchedule.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Settings/Applications/AppSchedule/AppSchedule.component.tsx @@ -30,8 +30,8 @@ import { import { getIngestionPipelineByFqn } from '../../../../rest/ingestionPipelineAPI'; import { getCronInitialValue, getWeekCron } from '../../../../utils/CronUtils'; import Loader from '../../../common/Loader/Loader'; -import { WorkflowExtraConfig } from '../../Services/AddIngestion/IngestionWorkflow.interface'; import ScheduleInterval from '../../Services/AddIngestion/Steps/ScheduleInterval'; +import { WorkflowExtraConfig } from '../../Services/AddIngestion/Steps/ScheduleInterval.interface'; import applicationsClassBase from '../AppDetails/ApplicationsClassBase'; import AppRunsHistory from '../AppRunsHistory/AppRunsHistory.component'; import { AppRunsHistoryRef } from '../AppRunsHistory/AppRunsHistory.interface'; @@ -259,9 +259,9 @@ const AppSchedule = ({ : getCronInitialValue(appData?.name ?? '') } includePeriodOptions={initialOptions} - initialScheduleInterval={ - (appData.appSchedule as AppScheduleClass)?.cronExpression - } + initialData={{ + cron: (appData.appSchedule as AppScheduleClass)?.cronExpression, + }} status={isSaveLoading ? 'waiting' : 'initial'} onBack={onDialogCancel} onDeploy={onDialogSave} diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Settings/Services/AddIngestion/AddIngestion.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Settings/Services/AddIngestion/AddIngestion.component.tsx index f240615e23a6..9d4ab6e5d026 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Settings/Services/AddIngestion/AddIngestion.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Settings/Services/AddIngestion/AddIngestion.component.tsx @@ -37,12 +37,12 @@ import SuccessScreen from '../../../common/SuccessScreen/SuccessScreen'; import DeployIngestionLoaderModal from '../../../Modals/DeployIngestionLoaderModal/DeployIngestionLoaderModal'; import IngestionStepper from '../Ingestion/IngestionStepper/IngestionStepper.component'; import IngestionWorkflowForm from '../Ingestion/IngestionWorkflowForm/IngestionWorkflowForm'; +import { AddIngestionProps } from './IngestionWorkflow.interface'; +import ScheduleInterval from './Steps/ScheduleInterval'; import { - AddIngestionProps, IngestionExtraConfig, WorkflowExtraConfig, -} from './IngestionWorkflow.interface'; -import ScheduleInterval from './Steps/ScheduleInterval'; +} from './Steps/ScheduleInterval.interface'; const AddIngestion = ({ activeIngestionStep, @@ -311,7 +311,7 @@ const AddIngestion = ({ ? ['day'] : periodOptions } - initialScheduleInterval={data?.airflowConfig.scheduleInterval} + initialData={{ cron: data?.airflowConfig.scheduleInterval }} isEditMode={isEditMode} status={saveState} onBack={() => handlePrev(1)} diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Settings/Services/AddIngestion/IngestionWorkflow.interface.ts b/openmetadata-ui/src/main/resources/ui/src/components/Settings/Services/AddIngestion/IngestionWorkflow.interface.ts index 8cde399ae199..786c88b60eea 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Settings/Services/AddIngestion/IngestionWorkflow.interface.ts +++ b/openmetadata-ui/src/main/resources/ui/src/components/Settings/Services/AddIngestion/IngestionWorkflow.interface.ts @@ -11,8 +11,7 @@ * limitations under the License. */ -import { LoadingState, ServicesUpdateRequest } from 'Models'; -import { ReactNode } from 'react'; +import { ServicesUpdateRequest } from 'Models'; import { FormSubmitType } from '../../../../enums/form.enum'; import { ServiceCategory } from '../../../../enums/service.enum'; import { CreateIngestionPipeline } from '../../../../generated/api/services/ingestionPipelines/createIngestionPipeline'; @@ -50,32 +49,3 @@ export interface AddIngestionProps { handleViewServiceClick?: () => void; onFocus: (fieldName: string) => void; } - -export type ScheduleIntervalProps = { - status: LoadingState; - initialScheduleInterval?: string; - defaultSchedule?: string; - includePeriodOptions?: string[]; - children?: ReactNode; - disabled?: boolean; - isEditMode?: boolean; - onBack: () => void; - onDeploy: (values: WorkflowExtraConfig & T) => void; - buttonProps?: { - okText?: string; - cancelText?: string; - }; - debugLog?: { - allow?: boolean; - initialValue?: boolean; - }; -}; - -export interface WorkflowExtraConfig { - cron?: string; - enableDebugLog?: boolean; -} - -export interface IngestionExtraConfig { - retries?: number; -} diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Settings/Services/AddIngestion/Steps/ScheduleInterval.interface.ts b/openmetadata-ui/src/main/resources/ui/src/components/Settings/Services/AddIngestion/Steps/ScheduleInterval.interface.ts index 47513ce24e5e..c7f737e5c9d6 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Settings/Services/AddIngestion/Steps/ScheduleInterval.interface.ts +++ b/openmetadata-ui/src/main/resources/ui/src/components/Settings/Services/AddIngestion/Steps/ScheduleInterval.interface.ts @@ -11,6 +11,39 @@ * limitations under the License. */ +import { LoadingState } from 'Models'; +import { ReactNode } from 'react'; + +export type ScheduleIntervalProps = { + status: LoadingState; + initialData?: WorkflowExtraConfig & T; + defaultSchedule?: string; + includePeriodOptions?: string[]; + children?: ReactNode; + disabled?: boolean; + isEditMode?: boolean; + onBack: () => void; + onDeploy: (values: WorkflowExtraConfig & T) => void; + buttonProps?: { + okText?: string; + cancelText?: string; + }; + debugLog?: { + allow?: boolean; + initialValue?: boolean; + }; + topChildren?: ReactNode; +}; + +export interface WorkflowExtraConfig { + cron?: string; + enableDebugLog?: boolean; +} + +export interface IngestionExtraConfig { + retries?: number; +} + export interface Combination { hour: RegExp; day: RegExp; diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Settings/Services/AddIngestion/Steps/ScheduleInterval.test.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Settings/Services/AddIngestion/Steps/ScheduleInterval.test.tsx index bec728d6fd0f..68cb835e2944 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Settings/Services/AddIngestion/Steps/ScheduleInterval.test.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Settings/Services/AddIngestion/Steps/ScheduleInterval.test.tsx @@ -18,16 +18,16 @@ import { screen, } from '@testing-library/react'; import React from 'react'; -import { ScheduleIntervalProps } from '../IngestionWorkflow.interface'; import ScheduleInterval from './ScheduleInterval'; +import { ScheduleIntervalProps } from './ScheduleInterval.interface'; jest.mock('../../../../common/CronEditor/CronEditor', () => { return jest.fn().mockImplementation(() =>
CronEditor.component
); }); -const mockScheduleIntervalProps: ScheduleIntervalProps = { +const mockScheduleIntervalProps: ScheduleIntervalProps<{ cron: string }> = { status: 'initial', - initialScheduleInterval: '', + initialData: { cron: '' }, onBack: jest.fn(), onDeploy: jest.fn(), buttonProps: { diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Settings/Services/AddIngestion/Steps/ScheduleInterval.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Settings/Services/AddIngestion/Steps/ScheduleInterval.tsx index ac8fa80f75a9..e57f68667f8b 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Settings/Services/AddIngestion/Steps/ScheduleInterval.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Settings/Services/AddIngestion/Steps/ScheduleInterval.tsx @@ -51,19 +51,19 @@ import { } from '../../../../../utils/CronUtils'; import { generateFormFields } from '../../../../../utils/formUtils'; import { getCurrentLocaleForConstrue } from '../../../../../utils/i18next/i18nextUtil'; +import './schedule-interval.less'; import { ScheduleIntervalProps, + StateValue, WorkflowExtraConfig, -} from '../IngestionWorkflow.interface'; -import './schedule-interval.less'; -import { StateValue } from './ScheduleInterval.interface'; +} from './ScheduleInterval.interface'; const ScheduleInterval = ({ disabled, includePeriodOptions, onBack, onDeploy, - initialScheduleInterval, + initialData, status, children, debugLog = { @@ -73,16 +73,20 @@ const ScheduleInterval = ({ isEditMode = false, buttonProps, defaultSchedule = DEFAULT_SCHEDULE_CRON, + topChildren, }: ScheduleIntervalProps) => { const { t } = useTranslation(); - const initialSchedule = isEditMode - ? initialScheduleInterval - : initialScheduleInterval || defaultSchedule; - const initialValues = getStateValue(initialSchedule, defaultSchedule); + const initialCron = isEditMode + ? initialData?.cron + : initialData?.cron || defaultSchedule; + const initialValues = { + ...initialData, + ...getStateValue(initialCron, defaultSchedule), + }; const [state, setState] = useState(initialValues); const [selectedSchedular, setSelectedSchedular] = React.useState( - isEmpty(initialSchedule) + isEmpty(initialCron) ? SchedularOptions.ON_DEMAND : SchedularOptions.SCHEDULE ); @@ -118,7 +122,7 @@ const ScheduleInterval = ({ const handleSelectedSchedular = useCallback( (value: SchedularOptions) => { setSelectedSchedular(value); - let newState = getStateValue(initialScheduleInterval ?? defaultSchedule); + let newState = getStateValue(initialData?.cron ?? defaultSchedule); if (value === SchedularOptions.ON_DEMAND) { newState = { ...newState, @@ -128,7 +132,7 @@ const ScheduleInterval = ({ setState(newState); form.setFieldsValue(newState); }, - [isEditMode, initialScheduleInterval, defaultSchedule] + [isEditMode, initialData?.cron, defaultSchedule] ); const formFields: FieldProp[] = useMemo( @@ -158,7 +162,7 @@ const ScheduleInterval = ({ [onDeploy] ); - const handleValuesChange = (values: StateValue) => { + const handleValuesChange = (values: StateValue & WorkflowExtraConfig & T) => { const newState = { ...state, ...values }; const cronExp = getCron(newState); const updatedState = { ...newState, cron: cronExp }; @@ -178,13 +182,16 @@ const ScheduleInterval = ({ return (
+ {topChildren} ({ {selectedSchedular === SchedularOptions.SCHEDULE && ( - + { return { initialOptions, - initialValue: config?.enable - ? getWeekCron({ hour: 0, min: 0, dow: 0 }) - : getCronInitialValue(appData?.name ?? ''), + initialValue: { + cron: config?.enable + ? getWeekCron({ hour: 0, min: 0, dow: 0 }) + : getCronInitialValue(appData?.name ?? ''), + }, }; }, [appData?.name, appData?.appType, pipelineSchedules, config?.enable]); @@ -207,7 +209,7 @@ const AppInstall = () => { {t('label.schedule')} setActiveServiceStep(appData.allowConfiguration ? 2 : 1) From 8e34c0270ddb3d00c2a60f0b869c3f02b546d561 Mon Sep 17 00:00:00 2001 From: Aniket Katkar Date: Wed, 16 Oct 2024 20:37:37 +0530 Subject: [PATCH 09/18] Fix the unit tests --- .../components/AddTestSuitePipeline.test.tsx | 140 +++++++++--------- .../AppSchedule/AppSchedule.test.tsx | 22 ++- .../AddIngestion/AddIngestion.test.tsx | 8 + .../Steps/ScheduleInterval.test.tsx | 88 +++++------ .../AddIngestion/Steps/ScheduleInterval.tsx | 1 + .../IngestionListTable.test.tsx | 9 ++ .../src/pages/AppInstall/AppInstall.test.tsx | 32 ++-- 7 files changed, 156 insertions(+), 144 deletions(-) diff --git a/openmetadata-ui/src/main/resources/ui/src/components/DataQuality/AddDataQualityTest/components/AddTestSuitePipeline.test.tsx b/openmetadata-ui/src/main/resources/ui/src/components/DataQuality/AddDataQualityTest/components/AddTestSuitePipeline.test.tsx index 7d9ec3ff938a..a97ecd31ace4 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/DataQuality/AddDataQualityTest/components/AddTestSuitePipeline.test.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/DataQuality/AddDataQualityTest/components/AddTestSuitePipeline.test.tsx @@ -11,6 +11,7 @@ * limitations under the License. */ import { act, fireEvent, render, screen } from '@testing-library/react'; +import { Form } from 'antd'; import React from 'react'; import { AddTestSuitePipelineProps } from '../AddDataQualityTest.interface'; import AddTestSuitePipeline from './AddTestSuitePipeline'; @@ -25,6 +26,21 @@ jest.mock('../../AddTestCaseList/AddTestCaseList.component', () => ({ .fn() .mockImplementation(() =>
AddTestCaseList.component
), })); +jest.mock( + '../../../Settings/Services/AddIngestion/Steps/ScheduleInterval', + () => + jest + .fn() + .mockImplementation(({ children, topChildren, onDeploy, onBack }) => ( +
+ ScheduleInterval + {topChildren} + {children} +
submit
+
cancel
+
+ )) +); jest.mock('react-router-dom', () => ({ useHistory: jest.fn().mockImplementation(() => mockUseHistory), })); @@ -36,126 +52,102 @@ const mockProps: AddTestSuitePipelineProps = { describe('AddTestSuitePipeline', () => { it('renders form fields', () => { - render(); + render( + + + + ); // Assert that the form fields are rendered expect(screen.getByTestId('pipeline-name')).toBeInTheDocument(); - expect(screen.getByTestId('enable-debug-log')).toBeInTheDocument(); - expect(screen.getByTestId('cron-container')).toBeInTheDocument(); expect(screen.getByTestId('select-all-test-cases')).toBeInTheDocument(); - expect(screen.getByTestId('deploy-button')).toBeInTheDocument(); - expect(screen.getByTestId('cancel')).toBeInTheDocument(); + expect(screen.getByText('submit')).toBeInTheDocument(); + expect(screen.getByText('cancel')).toBeInTheDocument(); }); it('calls onSubmit when submit button is clicked', async () => { - render(); + render( +
+ + + ); fireEvent.change(screen.getByTestId('pipeline-name'), { target: { value: 'Test Suite pipeline' }, }); await act(async () => { - await fireEvent.click(screen.getByTestId('enable-debug-log')); + fireEvent.click(screen.getByTestId('select-all-test-cases')); }); await act(async () => { - await fireEvent.click(screen.getByTestId('select-all-test-cases')); - }); - await act(async () => { - await fireEvent.click(screen.getByTestId('deploy-button')); + fireEvent.click(screen.getByText('submit')); }); // Assert that onSubmit is called with the correct values - expect(mockProps.onSubmit).toHaveBeenCalledWith({ - enableDebugLog: true, - name: 'Test Suite pipeline', - period: '', - repeatFrequency: undefined, - selectAllTestCases: true, - testCases: undefined, - }); + expect(mockProps.onSubmit).toHaveBeenCalled(); }); it('calls onCancel when cancel button is clicked and onCancel button is provided', async () => { const mockOnCancel = jest.fn(); - render(); + render( +
+ + + ); await act(async () => { - await fireEvent.click(screen.getByTestId('cancel')); + fireEvent.click(screen.getByText('cancel')); }); expect(mockOnCancel).toHaveBeenCalled(); }); it('calls history.goBack when cancel button is clicked and onCancel button is not provided', async () => { - render(); + render( +
+ + + ); await act(async () => { - await fireEvent.click(screen.getByTestId('cancel')); + fireEvent.click(screen.getByText('cancel')); }); expect(mockUseHistory.goBack).toHaveBeenCalled(); }); it('Hide AddTestCaseList after clicking on select-all-test-cases switch', async () => { - render(); + jest.spyOn(Form, 'Provider').mockImplementation( + jest.fn().mockImplementation(({ onFormChange, children }) => ( +
+ onFormChange('', { + forms: { + ['schedular-form']: { + getFieldValue: jest.fn().mockImplementation(() => true), + setFieldsValue: jest.fn(), + }, + }, + }) + }> + {children} +
+ )) + ); + render( +
+ + + ); // Assert that AddTestCaseList.component is now visible expect(screen.getByText('AddTestCaseList.component')).toBeInTheDocument(); // Click on the select-all-test-cases switch await act(async () => { - await fireEvent.click(screen.getByTestId('select-all-test-cases')); + fireEvent.click(screen.getByTestId('select-all-test-cases')); }); // Assert that AddTestCaseList.component is not initially visible expect(screen.queryByText('AddTestCaseList.component')).toBeNull(); }); - - it('renders with initial data', () => { - const initialData = { - enableDebugLog: true, - name: 'Initial Test Suite', - repeatFrequency: '* 0 0 0', - selectAllTestCases: true, - testCases: ['test-case-1', 'test-case-2'], - }; - - render(); - - // Assert that the form fields are rendered with the initial data - expect(screen.getByTestId('pipeline-name')).toHaveValue(initialData.name); - expect(screen.getByTestId('enable-debug-log')).toBeChecked(); - expect(screen.getByTestId('select-all-test-cases')).toBeChecked(); - expect(screen.getByTestId('deploy-button')).toBeInTheDocument(); - expect(screen.getByTestId('cancel')).toBeInTheDocument(); - }); - - it('testCases removal should work', async () => { - const initialData = { - enableDebugLog: true, - name: 'Initial Test Suite', - repeatFrequency: '* 0 0 0', - selectAllTestCases: false, - testCases: ['test-case-1', 'test-case-2'], - }; - - render(); - - await act(async () => { - await fireEvent.click(screen.getByTestId('select-all-test-cases')); - }); - - expect(screen.queryByText('AddTestCaseList.component')).toBeNull(); - - await act(async () => { - await fireEvent.click(screen.getByTestId('deploy-button')); - }); - - // Assert that onSubmit is called with the initial data - expect(mockProps.onSubmit).toHaveBeenCalledWith({ - ...initialData, - period: '', - selectAllTestCases: true, - testCases: undefined, - }); - }); }); diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Settings/Applications/AppSchedule/AppSchedule.test.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Settings/Applications/AppSchedule/AppSchedule.test.tsx index 53599ded2cc0..bba0560854bf 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Settings/Applications/AppSchedule/AppSchedule.test.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Settings/Applications/AppSchedule/AppSchedule.test.tsx @@ -39,16 +39,14 @@ jest.mock('../../../../rest/ingestionPipelineAPI', () => ({ .mockImplementation((...args) => mockGetIngestionPipelineByFqn(...args)), })); -jest.mock( - '../../../DataQuality/AddDataQualityTest/components/TestSuiteScheduler', - () => - jest.fn().mockImplementation(({ onSubmit, onCancel }) => ( -
- TestSuiteScheduler - - -
- )) +jest.mock('../../Services/AddIngestion/Steps/ScheduleInterval', () => + jest.fn().mockImplementation(({ onDeploy, onBack }) => ( +
+ ScheduleInterval + + +
+ )) ); jest.mock('../../../common/Loader/Loader', () => { @@ -171,13 +169,13 @@ describe('AppSchedule component', () => { expect(screen.getByText('Modal is open')).toBeInTheDocument(); userEvent.click( - screen.getByRole('button', { name: 'Submit TestSuiteSchedular' }) + screen.getByRole('button', { name: 'Submit ScheduleInterval' }) ); expect(mockOnSave).toHaveBeenCalled(); userEvent.click( - screen.getByRole('button', { name: 'Cancel TestSuiteSchedular' }) + screen.getByRole('button', { name: 'Cancel ScheduleInterval' }) ); expect(screen.getByText('Modal is close')).toBeInTheDocument(); diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Settings/Services/AddIngestion/AddIngestion.test.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Settings/Services/AddIngestion/AddIngestion.test.tsx index a4c6230fa7d0..a0d32f0d3776 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Settings/Services/AddIngestion/AddIngestion.test.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Settings/Services/AddIngestion/AddIngestion.test.tsx @@ -41,6 +41,10 @@ const mockAddIngestionProps: AddIngestionProps = { onFocus: jest.fn(), }; +jest.mock('../../../../hooks/useFqn', () => ({ + useFqn: jest.fn().mockReturnValue({ ingestionFQN: 'test' }), +})); + jest.mock('@rjsf/core', () => ({ Form: jest.fn().mockImplementation(() =>
RJSF_Form.component
), })); @@ -49,6 +53,10 @@ jest.mock('../Ingestion/IngestionStepper/IngestionStepper.component', () => { return jest.fn().mockImplementation(() =>
IngestionStepper
); }); +jest.mock('./Steps/ScheduleInterval', () => { + return jest.fn().mockImplementation(() =>
ScheduleInterval
); +}); + jest.mock('../Ingestion/IngestionWorkflowForm/IngestionWorkflowForm', () => { return jest.fn().mockImplementation(() =>
Ingestion workflow form
); }); diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Settings/Services/AddIngestion/Steps/ScheduleInterval.test.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Settings/Services/AddIngestion/Steps/ScheduleInterval.test.tsx index 68cb835e2944..2eb062e30fb3 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Settings/Services/AddIngestion/Steps/ScheduleInterval.test.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Settings/Services/AddIngestion/Steps/ScheduleInterval.test.tsx @@ -11,20 +11,11 @@ * limitations under the License. */ -import { - findByTestId, - findByText, - render, - screen, -} from '@testing-library/react'; +import { act, render, screen } from '@testing-library/react'; import React from 'react'; import ScheduleInterval from './ScheduleInterval'; import { ScheduleIntervalProps } from './ScheduleInterval.interface'; -jest.mock('../../../../common/CronEditor/CronEditor', () => { - return jest.fn().mockImplementation(() =>
CronEditor.component
); -}); - const mockScheduleIntervalProps: ScheduleIntervalProps<{ cron: string }> = { status: 'initial', initialData: { cron: '' }, @@ -35,63 +26,76 @@ const mockScheduleIntervalProps: ScheduleIntervalProps<{ cron: string }> = { }, }; +jest.mock('../../../../../utils/i18next/i18nextUtil', () => ({ + getCurrentLocaleForConstrue: jest.fn().mockReturnValue('en-US'), +})); + describe('Test ScheduleInterval component', () => { it('ScheduleInterval component should render', async () => { - const { container } = render( - - ); + await act(async () => { + render(); + }); - const scheduleIntervelContainer = await findByTestId( - container, + const scheduleIntervelContainer = screen.getByTestId( 'schedule-intervel-container' ); - const backButton = await findByTestId(container, 'back-button'); - const deployButton = await findByTestId(container, 'deploy-button'); - const cronEditor = await findByText(container, 'CronEditor.component'); + const backButton = screen.getByTestId('back-button'); + const deployButton = screen.getByTestId('deploy-button'); + const scheduleCardContainer = screen.getByTestId( + 'schedular-card-container' + ); expect(scheduleIntervelContainer).toBeInTheDocument(); - expect(cronEditor).toBeInTheDocument(); + expect(scheduleCardContainer).toBeInTheDocument(); expect(backButton).toBeInTheDocument(); expect(deployButton).toBeInTheDocument(); }); - it('should not render debug log switch when allowEnableDebugLog is false', () => { - render(); + it('should not render debug log switch when allowEnableDebugLog is false', async () => { + await act(async () => { + render(); + }); expect(screen.queryByTestId('enable-debug-log')).toBeNull(); }); - it('should render enable debug log switch when allowEnableDebugLog is true', () => { - render( - - ); + it('should render enable debug log switch when allowEnableDebugLog is true', async () => { + await act(async () => { + render( + + ); + }); expect(screen.getByTestId('enable-debug-log')).toBeInTheDocument(); }); - it('debug log switch should be initially checked when debugLogInitialValue is true', () => { - render( - - ); + it('debug log switch should be initially checked when debugLogInitialValue is true', async () => { + await act(async () => { + render( + + ); + }); expect(screen.getByTestId('enable-debug-log')).toHaveClass( 'ant-switch-checked' ); }); - it('debug log switch should not be initially checked when debugLogInitialValue is false', () => { - render( - - ); + it('debug log switch should not be initially checked when debugLogInitialValue is false', async () => { + await act(async () => { + render( + + ); + }); expect(screen.getByTestId('enable-debug-log')).not.toHaveClass( 'ant-switch-checked' diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Settings/Services/AddIngestion/Steps/ScheduleInterval.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Settings/Services/AddIngestion/Steps/ScheduleInterval.tsx index e57f68667f8b..bc0e1c4911c1 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Settings/Services/AddIngestion/Steps/ScheduleInterval.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Settings/Services/AddIngestion/Steps/ScheduleInterval.tsx @@ -195,6 +195,7 @@ const ScheduleInterval = ({ {SCHEDULAR_OPTIONS.map(({ description, title, value }) => ( jest.fn().mockImplementation(() =>
NextPrevious
) ); +jest.mock('../../../../../utils/IngestionListTableUtils', () => ({ + renderNameField: jest.fn().mockImplementation(() =>
nameField
), + renderScheduleField: jest + .fn() + .mockImplementation(() =>
scheduleField
), + renderStatusField: jest.fn().mockImplementation(() =>
statusField
), + renderTypeField: jest.fn().mockImplementation(() =>
typeField
), +})); + describe('Ingestion', () => { it('should render custom emptyPlaceholder if passed', async () => { await act(async () => { diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/AppInstall/AppInstall.test.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/AppInstall/AppInstall.test.tsx index e09ce66954ee..2a224ee56181 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/AppInstall/AppInstall.test.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/AppInstall/AppInstall.test.tsx @@ -36,14 +36,14 @@ jest.mock('react-router-dom', () => ({ })); jest.mock( - '../../components/DataQuality/AddDataQualityTest/components/TestSuiteScheduler', + '../../components/Settings/Services/AddIngestion/Steps/ScheduleInterval', () => - jest.fn().mockImplementation(({ onSubmit, onCancel }) => ( - <> - TestSuiteScheduler - - - + jest.fn().mockImplementation(({ onDeploy, onBack }) => ( +
+ ScheduleInterval + + +
)) ); @@ -159,13 +159,13 @@ describe('AppInstall component', () => { ); }); - expect(screen.getByText('TestSuiteScheduler')).toBeInTheDocument(); + expect(screen.getByText('ScheduleInterval')).toBeInTheDocument(); expect(screen.queryByText('AppInstallVerifyCard')).not.toBeInTheDocument(); - // TestSuiteScheduler + // ScheduleInterval await act(async () => { userEvent.click( - screen.getByRole('button', { name: 'Submit TestSuiteScheduler' }) + screen.getByRole('button', { name: 'Submit ScheduleInterval' }) ); }); @@ -177,7 +177,7 @@ describe('AppInstall component', () => { // change ActiveServiceStep to 1 act(() => { userEvent.click( - screen.getByRole('button', { name: 'Cancel TestSuiteScheduler' }) + screen.getByRole('button', { name: 'Cancel ScheduleInterval' }) ); }); @@ -187,7 +187,7 @@ describe('AppInstall component', () => { screen.getByRole('button', { name: 'Cancel AppInstallVerifyCard' }) ); - // will call for Submit TestSuiteScheduler and Cancel AppInstallVerifyCard + // will call for Submit ScheduleInterval and Cancel AppInstallVerifyCard expect(mockPush).toHaveBeenCalledTimes(1); }); @@ -215,12 +215,12 @@ describe('AppInstall component', () => { ); }); - expect(screen.getByText('TestSuiteScheduler')).toBeInTheDocument(); + expect(screen.getByText('ScheduleInterval')).toBeInTheDocument(); // change ActiveServiceStep to 2 act(() => { userEvent.click( - screen.getByRole('button', { name: 'Cancel TestSuiteScheduler' }) + screen.getByRole('button', { name: 'Cancel ScheduleInterval' }) ); }); @@ -259,11 +259,11 @@ describe('AppInstall component', () => { ); }); - expect(screen.getByText('TestSuiteScheduler')).toBeInTheDocument(); + expect(screen.getByText('ScheduleInterval')).toBeInTheDocument(); await act(async () => { userEvent.click( - screen.getByRole('button', { name: 'Submit TestSuiteScheduler' }) + screen.getByRole('button', { name: 'Submit ScheduleInterval' }) ); }); From 05aa83b06a7a2b2fb1082d751c94cf152f0f0765 Mon Sep 17 00:00:00 2001 From: Aniket Katkar Date: Thu, 17 Oct 2024 15:42:46 +0530 Subject: [PATCH 10/18] Fix playwright tests --- .../Features/TestSuiteMultiPipeline.spec.ts | 7 +++- .../DataInsightReportApplication.spec.ts | 16 ++++++-- .../e2e/Pages/SearchIndexApplication.spec.ts | 7 +++- .../entity/ingestion/ServiceBaseClass.ts | 39 ++++++++++++++++--- 4 files changed, 56 insertions(+), 13 deletions(-) diff --git a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/TestSuiteMultiPipeline.spec.ts b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/TestSuiteMultiPipeline.spec.ts index 8bc297a543c6..1e737fc92abf 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/TestSuiteMultiPipeline.spec.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/TestSuiteMultiPipeline.spec.ts @@ -55,8 +55,11 @@ test( await page.getByTestId('add-ingestion-button').click(); await page.getByTestId('select-all-test-cases').click(); - await page.getByTestId('cron-type').getByText('Hour').click(); - await page.getByTitle('Day').click(); + + await expect( + page.getByTestId('cron-type').getByText('Day') + ).toBeAttached(); + await page.getByTestId('deploy-button').click(); await expect(page.getByTestId('view-service-button')).toBeVisible(); diff --git a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/DataInsightReportApplication.spec.ts b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/DataInsightReportApplication.spec.ts index bd702fb47683..a8d0b634a683 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/DataInsightReportApplication.spec.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/DataInsightReportApplication.spec.ts @@ -61,8 +61,15 @@ test.describe.serial('Data Insight Report Application', () => { await page.click('[data-testid="install-application"]'); await page.click('[data-testid="save-button"]'); await page.click('[data-testid="submit-btn"]'); - await page.click('[data-testid="cron-type"]'); - await page.click('[data-value="5"]'); + + await expect( + page.getByTestId('cron-type').getByText('Week') + ).toBeAttached(); + + await page + .locator('#schedular-form_dow .week-selector-buttons') + .getByText('F') + .click(); await page.click('[data-testid="deploy-button"]'); await toastNotification(page, 'Application installed successfully'); @@ -79,7 +86,10 @@ test.describe.serial('Data Insight Report Application', () => { await page.click('[data-testid="edit-button"]'); await page.click('[data-testid="cron-type"]'); - await page.click('[data-value="3"]'); + await page + .locator('#schedular-form_dow .week-selector-buttons') + .getByText('W') + .click(); await page.click('[data-testid="hour-options"]'); await page.click('[title="01"]'); await page.click('.ant-modal-body [data-testid="deploy-button"]'); diff --git a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/SearchIndexApplication.spec.ts b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/SearchIndexApplication.spec.ts index f8ade277ef41..3ef600bf147f 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/SearchIndexApplication.spec.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/SearchIndexApplication.spec.ts @@ -89,8 +89,11 @@ test('Search Index Application', async ({ page }) => { await test.step('Edit application', async () => { await page.click('[data-testid="edit-button"]'); - await page.click('[data-testid="cron-type"]'); - await page.click('.rc-virtual-list [title="None"]'); + await page.waitForSelector('[data-testid="schedular-card-container"]'); + await page + .getByTestId('schedular-card-container') + .getByText('On Demand') + .click(); const deployResponse = page.waitForResponse('/api/v1/apps/*'); await page.click('.ant-modal-body [data-testid="deploy-button"]'); diff --git a/openmetadata-ui/src/main/resources/ui/playwright/support/entity/ingestion/ServiceBaseClass.ts b/openmetadata-ui/src/main/resources/ui/playwright/support/entity/ingestion/ServiceBaseClass.ts index 2962c525ebfd..c8af4bf18cd3 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/support/entity/ingestion/ServiceBaseClass.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/support/entity/ingestion/ServiceBaseClass.ts @@ -204,10 +204,13 @@ class ServiceBaseClass { async scheduleIngestion(page: Page) { // Schedule & Deploy - await page.waitForSelector('[data-testid="cron-type"]'); - await page.click('[data-testid="cron-type"]'); - await page.waitForSelector('.ant-select-item-option-content'); - await page.click('.ant-select-item-option-content:has-text("None")'); + await page.waitForSelector('[data-testid="schedular-card-container"]'); + await page + .getByTestId('schedular-card-container') + .getByText('On Demand') + .click(); + + await expect(page.locator('[data-testid="cron-type"')).not.toBeAttached(); const deployPipelinePromise = page.waitForRequest( `/api/v1/services/ingestionPipelines/deploy/**` @@ -240,7 +243,7 @@ class ServiceBaseClass { .then((res) => res.json()); const workflowData = response.data.filter( - (d) => d.pipelineType === ingestionType + (d: { pipelineType: string }) => d.pipelineType === ingestionType )[0]; const oneHourBefore = Date.now() - 86400000; @@ -388,7 +391,31 @@ class ServiceBaseClass { await page.click('[data-testid="submit-btn"]'); await page.click('[data-testid="cron-type"]'); await page.click('.ant-select-item-option-content:has-text("Custom")'); - await page.fill('#cron', '* * * 2 6'); + + // Check validation error thrown for a cron that is too frequent + // i.e. having interval less than 1 hour + await page.locator('#schedular-form_cron').fill('* * * 2 6'); + await page.click('[data-testid="deploy-button"]'); + + await expect( + page.getByText( + 'Cron schedule too frequent. Please choose at least 1-hour intervals.' + ) + ).toBeAttached(); + + // Check validation error thrown for a cron that is invalid + await page.locator('#schedular-form_cron').clear(); + await page.click('[data-testid="deploy-button"]'); + await page.locator('#schedular-form_cron').fill('* * * 2 '); + + await expect( + page.getByText( + 'Error: Expression has only 4 parts. At least 5 parts are required.' + ) + ).toBeAttached(); + + await page.locator('#schedular-form_cron').clear(); + await page.locator('#schedular-form_cron').fill('0 * * 2 6'); await page.click('[data-testid="deploy-button"]'); await page.click('[data-testid="view-service-button"]'); From ba35a2d2fb516b58cd780a31ecb691ca95985125 Mon Sep 17 00:00:00 2001 From: Aniket Katkar Date: Thu, 17 Oct 2024 17:04:21 +0530 Subject: [PATCH 11/18] Fix the failing playwright tests --- .../playwright/e2e/Pages/SearchIndexApplication.spec.ts | 9 ++++++--- .../support/entity/ingestion/ServiceBaseClass.ts | 2 +- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/SearchIndexApplication.spec.ts b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/SearchIndexApplication.spec.ts index 3ef600bf147f..22b232f4cf00 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/SearchIndexApplication.spec.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/SearchIndexApplication.spec.ts @@ -160,10 +160,13 @@ test('Search Index Application', async ({ page }) => { await page.click('[data-testid="install-application"]'); await page.click('[data-testid="save-button"]'); await page.click('[data-testid="submit-btn"]'); - await page.click('[data-testid="cron-type"]'); - await page.click('.rc-virtual-list [title="None"]'); + await page.waitForSelector('[data-testid="schedular-card-container"]'); + await page + .getByTestId('schedular-card-container') + .getByText('On Demand') + .click(); - expect(await page.innerText('[data-testid="cron-type"]')).toContain('None'); + await expect(page.locator('[data-testid="cron-type"')).not.toBeVisible(); const installApplicationResponse = page.waitForResponse('api/v1/apps'); await page.click('[data-testid="deploy-button"]'); diff --git a/openmetadata-ui/src/main/resources/ui/playwright/support/entity/ingestion/ServiceBaseClass.ts b/openmetadata-ui/src/main/resources/ui/playwright/support/entity/ingestion/ServiceBaseClass.ts index c8af4bf18cd3..6c209e1de3e2 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/support/entity/ingestion/ServiceBaseClass.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/support/entity/ingestion/ServiceBaseClass.ts @@ -210,7 +210,7 @@ class ServiceBaseClass { .getByText('On Demand') .click(); - await expect(page.locator('[data-testid="cron-type"')).not.toBeAttached(); + await expect(page.locator('[data-testid="cron-type"')).not.toBeVisible(); const deployPipelinePromise = page.waitForRequest( `/api/v1/services/ingestionPipelines/deploy/**` From c118197201ba4812354c415beb9c02dee747eff8 Mon Sep 17 00:00:00 2001 From: Aniket Katkar Date: Thu, 17 Oct 2024 17:58:27 +0530 Subject: [PATCH 12/18] Fix the ingestion search issue --- .../ServiceDetailsPage/ServiceDetailsPage.tsx | 6 +++++- .../resources/ui/src/utils/ServiceUtils.tsx | 20 +++++++++++++++++++ 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/ServiceDetailsPage/ServiceDetailsPage.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/ServiceDetailsPage/ServiceDetailsPage.tsx index ef9e3e98bd41..61bb517933a4 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/ServiceDetailsPage/ServiceDetailsPage.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/ServiceDetailsPage/ServiceDetailsPage.tsx @@ -118,6 +118,7 @@ import { getCountLabel, getEntityTypeFromServiceCategory, getResourceEntityFromServiceCategory, + getServiceDisplayNameQueryFilter, shouldTestConnection, } from '../../utils/ServiceUtils'; import { @@ -338,6 +339,9 @@ const ServiceDetailsPage: FunctionComponent = () => { index < SERVICE_INGESTION_PIPELINE_TYPES.length - 1 ? 'OR' : '' }` ).join(' ')})`, + queryFilter: getServiceDisplayNameQueryFilter( + getEntityName(serviceDetails) + ), }); const pipelines = res.hits.hits.map((hit) => hit._source); const total = res?.hits?.total.value ?? 0; @@ -348,7 +352,7 @@ const ServiceDetailsPage: FunctionComponent = () => { setIsIngestionPipelineLoading(false); } }, - [ingestionPageSize, handleIngestionPagingChange] + [ingestionPageSize, handleIngestionPagingChange, serviceDetails] ); const include = useMemo( diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/ServiceUtils.tsx b/openmetadata-ui/src/main/resources/ui/src/utils/ServiceUtils.tsx index 0dc5e7279e0f..9372387c1de2 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/ServiceUtils.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/utils/ServiceUtils.tsx @@ -501,3 +501,23 @@ export const getLinkForFqn = (serviceCategory: ServiceTypes, fqn: string) => { return entityUtilClassBase.getEntityLink(EntityType.DATABASE, fqn); } }; + +export const getServiceDisplayNameQueryFilter = (displayName: string) => ({ + query: { + bool: { + must: [ + { + bool: { + should: [ + { + term: { + 'service.displayName.keyword': displayName, + }, + }, + ], + }, + }, + ], + }, + }, +}); From ecf47f060d9c2f4311ffaa0af6d4f2eb7f17f9ca Mon Sep 17 00:00:00 2001 From: Aniket Katkar Date: Thu, 17 Oct 2024 22:21:17 +0530 Subject: [PATCH 13/18] worked on comments and fixed playwright tests --- .../e2e/Pages/SearchIndexApplication.spec.ts | 2 +- .../entity/ingestion/ServiceBaseClass.ts | 2 +- .../AddDataQualityTest/TestSuiteIngestion.tsx | 2 +- .../AddIngestion/AddIngestion.component.tsx | 6 ++---- .../AddIngestion/Steps/ScheduleInterval.tsx | 11 +++++++---- .../ui/src/constants/Ingestions.constant.ts | 5 ----- .../ui/src/constants/Schedular.constants.ts | 2 ++ .../main/resources/ui/src/enums/Cron.enum.ts | 19 ------------------- .../resources/ui/src/enums/Schedular.enum.ts | 7 +++++++ .../main/resources/ui/src/utils/CronUtils.tsx | 2 +- 10 files changed, 22 insertions(+), 36 deletions(-) delete mode 100644 openmetadata-ui/src/main/resources/ui/src/enums/Cron.enum.ts diff --git a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/SearchIndexApplication.spec.ts b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/SearchIndexApplication.spec.ts index 22b232f4cf00..651c811ef6ee 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/SearchIndexApplication.spec.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/SearchIndexApplication.spec.ts @@ -166,7 +166,7 @@ test('Search Index Application', async ({ page }) => { .getByText('On Demand') .click(); - await expect(page.locator('[data-testid="cron-type"')).not.toBeVisible(); + await expect(page.locator('[data-testid="cron-type"]')).not.toBeVisible(); const installApplicationResponse = page.waitForResponse('api/v1/apps'); await page.click('[data-testid="deploy-button"]'); diff --git a/openmetadata-ui/src/main/resources/ui/playwright/support/entity/ingestion/ServiceBaseClass.ts b/openmetadata-ui/src/main/resources/ui/playwright/support/entity/ingestion/ServiceBaseClass.ts index 6c209e1de3e2..cd559b1e15ad 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/support/entity/ingestion/ServiceBaseClass.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/support/entity/ingestion/ServiceBaseClass.ts @@ -210,7 +210,7 @@ class ServiceBaseClass { .getByText('On Demand') .click(); - await expect(page.locator('[data-testid="cron-type"')).not.toBeVisible(); + await expect(page.locator('[data-testid="cron-type"]')).not.toBeVisible(); const deployPipelinePromise = page.waitForRequest( `/api/v1/services/ingestionPipelines/deploy/**` diff --git a/openmetadata-ui/src/main/resources/ui/src/components/DataQuality/AddDataQualityTest/TestSuiteIngestion.tsx b/openmetadata-ui/src/main/resources/ui/src/components/DataQuality/AddDataQualityTest/TestSuiteIngestion.tsx index 8acb61488e90..0402c099e9e9 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/DataQuality/AddDataQualityTest/TestSuiteIngestion.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/DataQuality/AddDataQualityTest/TestSuiteIngestion.tsx @@ -165,7 +165,7 @@ const TestSuiteIngestion: React.FC = ({ const ingestionPayload: CreateIngestionPipeline = { airflowConfig: { - scheduleInterval: isEmpty(data.cron) ? undefined : data.cron, + scheduleInterval: data.cron, }, displayName: updatedName, name: generateUUID(), diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Settings/Services/AddIngestion/AddIngestion.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Settings/Services/AddIngestion/AddIngestion.component.tsx index 9d4ab6e5d026..52bd6531e7f3 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Settings/Services/AddIngestion/AddIngestion.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Settings/Services/AddIngestion/AddIngestion.component.tsx @@ -162,7 +162,7 @@ const AddIngestion = ({ const ingestionDetails: CreateIngestionPipeline = { airflowConfig: { - scheduleInterval: isEmpty(extraData.cron) ? undefined : extraData.cron, + scheduleInterval: extraData.cron, startDate: date, retries: extraData.retries, }, @@ -214,9 +214,7 @@ const AddIngestion = ({ ...data, airflowConfig: { ...data.airflowConfig, - scheduleInterval: isEmpty(extraData.cron) - ? undefined - : extraData.cron, + scheduleInterval: extraData.cron, retries: extraData.retries, }, displayName: workflowData?.displayName, diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Settings/Services/AddIngestion/Steps/ScheduleInterval.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Settings/Services/AddIngestion/Steps/ScheduleInterval.tsx index bc0e1c4911c1..6fe76c19b621 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Settings/Services/AddIngestion/Steps/ScheduleInterval.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Settings/Services/AddIngestion/Steps/ScheduleInterval.tsx @@ -30,15 +30,17 @@ import cronstrue from 'cronstrue/i18n'; import { isEmpty } from 'lodash'; import React, { useCallback, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; -import { DEFAULT_SCHEDULE_CRON } from '../../../../../constants/Ingestions.constant'; import { DAY_OPTIONS, + DEFAULT_SCHEDULE_CRON, PERIOD_OPTIONS, SCHEDULAR_OPTIONS, } from '../../../../../constants/Schedular.constants'; import { LOADING_STATE } from '../../../../../enums/common.enum'; -import { CronTypes } from '../../../../../enums/Cron.enum'; -import { SchedularOptions } from '../../../../../enums/Schedular.enum'; +import { + CronTypes, + SchedularOptions, +} from '../../../../../enums/Schedular.enum'; import { FieldProp, FieldTypes, @@ -157,7 +159,8 @@ const ScheduleInterval = ({ const handleFormSubmit: FormProps['onFinish'] = useCallback( (data: WorkflowExtraConfig & T) => { - onDeploy(data); + // Remove cron if it is empty + onDeploy({ ...data, cron: isEmpty(data.cron) ? undefined : data.cron }); }, [onDeploy] ); diff --git a/openmetadata-ui/src/main/resources/ui/src/constants/Ingestions.constant.ts b/openmetadata-ui/src/main/resources/ui/src/constants/Ingestions.constant.ts index dffaa3d52643..13b913588872 100644 --- a/openmetadata-ui/src/main/resources/ui/src/constants/Ingestions.constant.ts +++ b/openmetadata-ui/src/main/resources/ui/src/constants/Ingestions.constant.ts @@ -53,8 +53,3 @@ export const PIPELINE_TYPE_LOCALIZATION = { export const DBT_CLASSIFICATION_DEFAULT_VALUE = 'dbtTags'; export const DEFAULT_PARSING_TIMEOUT_LIMIT = 300; - -export const DEFAULT_SCHEDULE_CRON = '0 0 * * *'; -export const DEFAULT_CRON_MIN_VALUE = 0; -export const DEFAULT_CRON_HOUR_VALUE = 0; -export const DEFAULT_CRON_WEEK_VALUE = 1; diff --git a/openmetadata-ui/src/main/resources/ui/src/constants/Schedular.constants.ts b/openmetadata-ui/src/main/resources/ui/src/constants/Schedular.constants.ts index cf6e53049e2a..1afb83368311 100644 --- a/openmetadata-ui/src/main/resources/ui/src/constants/Schedular.constants.ts +++ b/openmetadata-ui/src/main/resources/ui/src/constants/Schedular.constants.ts @@ -102,3 +102,5 @@ export const CRON_COMBINATIONS: Combination = { day: /^(\d{1,2}\s){2}(\*\s){2}\*$/, // "? ? * * *" week: /^(\d{1,2}\s){2}(\*\s){2}\d{1,2}$/, // "? ? * * ?" }; + +export const DEFAULT_SCHEDULE_CRON = '0 0 * * *'; diff --git a/openmetadata-ui/src/main/resources/ui/src/enums/Cron.enum.ts b/openmetadata-ui/src/main/resources/ui/src/enums/Cron.enum.ts deleted file mode 100644 index 0f86e34e6324..000000000000 --- a/openmetadata-ui/src/main/resources/ui/src/enums/Cron.enum.ts +++ /dev/null @@ -1,19 +0,0 @@ -/* - * Copyright 2024 Collate. - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * http://www.apache.org/licenses/LICENSE-2.0 - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -export enum CronTypes { - MINUTE = 'minute', - HOUR = 'hour', - DAY = 'day', - WEEK = 'week', -} diff --git a/openmetadata-ui/src/main/resources/ui/src/enums/Schedular.enum.ts b/openmetadata-ui/src/main/resources/ui/src/enums/Schedular.enum.ts index b144b81c987f..bf5a38469d6e 100644 --- a/openmetadata-ui/src/main/resources/ui/src/enums/Schedular.enum.ts +++ b/openmetadata-ui/src/main/resources/ui/src/enums/Schedular.enum.ts @@ -11,6 +11,13 @@ * limitations under the License. */ +export enum CronTypes { + MINUTE = 'minute', + HOUR = 'hour', + DAY = 'day', + WEEK = 'week', +} + export enum SchedularOptions { SCHEDULE = 'schedule', ON_DEMAND = 'on-demand', diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/CronUtils.tsx b/openmetadata-ui/src/main/resources/ui/src/utils/CronUtils.tsx index baacf0085250..85a7482212a8 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/CronUtils.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/utils/CronUtils.tsx @@ -23,7 +23,7 @@ import { CRON_COMBINATIONS, MONTHS_LIST, } from '../constants/Schedular.constants'; -import { CronTypes } from '../enums/Cron.enum'; +import { CronTypes } from '../enums/Schedular.enum'; export const getRange = (n: number) => { return [...Array(n).keys()]; From cf840bb9574dc75beb67fdd2922a77000b4615ee Mon Sep 17 00:00:00 2001 From: Aniket Katkar Date: Thu, 17 Oct 2024 22:32:18 +0530 Subject: [PATCH 14/18] Remove the unnecessary isEmpty check for the cron string --- .../Settings/Services/AddIngestion/Steps/ScheduleInterval.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Settings/Services/AddIngestion/Steps/ScheduleInterval.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Settings/Services/AddIngestion/Steps/ScheduleInterval.tsx index 6fe76c19b621..1167c462edd4 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Settings/Services/AddIngestion/Steps/ScheduleInterval.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Settings/Services/AddIngestion/Steps/ScheduleInterval.tsx @@ -160,7 +160,7 @@ const ScheduleInterval = ({ const handleFormSubmit: FormProps['onFinish'] = useCallback( (data: WorkflowExtraConfig & T) => { // Remove cron if it is empty - onDeploy({ ...data, cron: isEmpty(data.cron) ? undefined : data.cron }); + onDeploy(data); }, [onDeploy] ); From c4e376a1709596177630a3b80877be8a882152e1 Mon Sep 17 00:00:00 2001 From: Aniket Katkar Date: Thu, 17 Oct 2024 22:38:17 +0530 Subject: [PATCH 15/18] Fix the cron values for hour and min not showing correctly --- openmetadata-ui/src/main/resources/ui/src/utils/CronUtils.tsx | 4 ++-- .../src/main/resources/ui/src/utils/TagClassBase.test.ts | 1 - 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/CronUtils.tsx b/openmetadata-ui/src/main/resources/ui/src/utils/CronUtils.tsx index 85a7482212a8..0a5362418e0a 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/CronUtils.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/utils/CronUtils.tsx @@ -123,8 +123,8 @@ export const getStateValue = (value?: string, defaultValue?: string) => { const stateVal: StateValue = { selectedPeriod: cronType, cron: value, - min: isNaN(dow) ? 0 : min, - hour: isNaN(dow) ? 0 : hour, + min: isNaN(min) ? 0 : min, + hour: isNaN(hour) ? 0 : hour, dow: isNaN(dow) ? 1 : dow, }; diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/TagClassBase.test.ts b/openmetadata-ui/src/main/resources/ui/src/utils/TagClassBase.test.ts index a393d968c229..396cce6ac06b 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/TagClassBase.test.ts +++ b/openmetadata-ui/src/main/resources/ui/src/utils/TagClassBase.test.ts @@ -16,7 +16,6 @@ import tagClassBase, { TagClassBase } from './TagClassBase'; jest.mock('../rest/searchAPI'); - jest.mock('./StringsUtils', () => ({ getEncodedFqn: jest.fn().mockReturnValue('test'), escapeESReservedCharacters: jest.fn().mockReturnValue('test'), From 1c20a82637e60e249233ac3eea03aa4f9edca8d8 Mon Sep 17 00:00:00 2001 From: Aniket Katkar Date: Thu, 17 Oct 2024 23:18:00 +0530 Subject: [PATCH 16/18] Remove the isEmpty check for cron --- .../DataQuality/AddDataQualityTest/TestSuiteIngestion.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openmetadata-ui/src/main/resources/ui/src/components/DataQuality/AddDataQualityTest/TestSuiteIngestion.tsx b/openmetadata-ui/src/main/resources/ui/src/components/DataQuality/AddDataQualityTest/TestSuiteIngestion.tsx index 0402c099e9e9..47888be0d6e5 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/DataQuality/AddDataQualityTest/TestSuiteIngestion.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/DataQuality/AddDataQualityTest/TestSuiteIngestion.tsx @@ -203,7 +203,7 @@ const TestSuiteIngestion: React.FC = ({ displayName: data.name ?? ingestionPipeline.displayName, airflowConfig: { ...ingestionPipeline?.airflowConfig, - scheduleInterval: isEmpty(data.cron) ? undefined : data.cron, + scheduleInterval: data.cron, }, loggerLevel: data.enableDebugLog ? LogLevels.Debug : LogLevels.Info, sourceConfig: { From 6c5e87f6a551c0a39db1b634f16e33de4eebd83c Mon Sep 17 00:00:00 2001 From: Aniket Katkar Date: Fri, 18 Oct 2024 12:23:23 +0530 Subject: [PATCH 17/18] Move the cron error checks to the create ingestion logic --- .../entity/ingestion/ServiceBaseClass.ts | 51 ++++++++++--------- 1 file changed, 27 insertions(+), 24 deletions(-) diff --git a/openmetadata-ui/src/main/resources/ui/playwright/support/entity/ingestion/ServiceBaseClass.ts b/openmetadata-ui/src/main/resources/ui/playwright/support/entity/ingestion/ServiceBaseClass.ts index cd559b1e15ad..a4a3f06bf679 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/support/entity/ingestion/ServiceBaseClass.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/support/entity/ingestion/ServiceBaseClass.ts @@ -203,7 +203,32 @@ class ServiceBaseClass { } async scheduleIngestion(page: Page) { - // Schedule & Deploy + await page.click('[data-testid="cron-type"]'); + await page.click('.ant-select-item-option-content:has-text("Custom")'); + // Check validation error thrown for a cron that is too frequent + // i.e. having interval less than 1 hour + await page.locator('#schedular-form_cron').fill('* * * 2 6'); + await page.click('[data-testid="deploy-button"]'); + + await expect( + page.getByText( + 'Cron schedule too frequent. Please choose at least 1-hour intervals.' + ) + ).toBeAttached(); + + // Check validation error thrown for a cron that is invalid + await page.locator('#schedular-form_cron').clear(); + await page.click('[data-testid="deploy-button"]'); + await page.locator('#schedular-form_cron').fill('* * * 2 '); + + await expect( + page.getByText( + 'Error: Expression has only 4 parts. At least 5 parts are required.' + ) + ).toBeAttached(); + + await page.locator('#schedular-form_cron').clear(); + await page.waitForSelector('[data-testid="schedular-card-container"]'); await page .getByTestId('schedular-card-container') @@ -392,29 +417,7 @@ class ServiceBaseClass { await page.click('[data-testid="cron-type"]'); await page.click('.ant-select-item-option-content:has-text("Custom")'); - // Check validation error thrown for a cron that is too frequent - // i.e. having interval less than 1 hour - await page.locator('#schedular-form_cron').fill('* * * 2 6'); - await page.click('[data-testid="deploy-button"]'); - - await expect( - page.getByText( - 'Cron schedule too frequent. Please choose at least 1-hour intervals.' - ) - ).toBeAttached(); - - // Check validation error thrown for a cron that is invalid - await page.locator('#schedular-form_cron').clear(); - await page.click('[data-testid="deploy-button"]'); - await page.locator('#schedular-form_cron').fill('* * * 2 '); - - await expect( - page.getByText( - 'Error: Expression has only 4 parts. At least 5 parts are required.' - ) - ).toBeAttached(); - - await page.locator('#schedular-form_cron').clear(); + // Schedule & Deploy await page.locator('#schedular-form_cron').fill('0 * * 2 6'); await page.click('[data-testid="deploy-button"]'); From 0f92a2cd59621030dd8a10806eba6d804a409d66 Mon Sep 17 00:00:00 2001 From: Aniket Katkar Date: Fri, 18 Oct 2024 16:40:46 +0530 Subject: [PATCH 18/18] Fix the failing playwright --- .../support/entity/ingestion/ServiceBaseClass.ts | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/openmetadata-ui/src/main/resources/ui/playwright/support/entity/ingestion/ServiceBaseClass.ts b/openmetadata-ui/src/main/resources/ui/playwright/support/entity/ingestion/ServiceBaseClass.ts index a4a3f06bf679..6b0afb54bcb4 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/support/entity/ingestion/ServiceBaseClass.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/support/entity/ingestion/ServiceBaseClass.ts @@ -343,6 +343,11 @@ class ServiceBaseClass { await page.click('[data-testid="submit-btn"]'); // select schedule + await page.waitForSelector('[data-testid="schedular-card-container"]'); + await page + .getByTestId('schedular-card-container') + .getByText('Schedule', { exact: true }) + .click(); await page.click('[data-testid="cron-type"]'); await page .locator('.ant-select-item-option-content', { hasText: 'Hour' }) @@ -393,7 +398,10 @@ class ServiceBaseClass { await page.click('[data-testid="submit-btn"]'); await page.click('[data-testid="cron-type"]'); await page.click('.ant-select-item-option-content:has-text("Week")'); - await page.click('[data-value="6"]'); + await page + .locator('#schedular-form_dow .week-selector-buttons') + .getByText('W') + .click(); await page.click('[data-testid="hour-options"]'); await page.click('#hour-select_list + .rc-virtual-list [title="05"]'); await page.click('[data-testid="minute-options"]'); @@ -407,7 +415,7 @@ class ServiceBaseClass { 'At 05:05 AM' ); await expect(page.getByTestId('schedule-secondary-details')).toHaveText( - 'Only on saturday' + 'Only on wednesday' ); // click and edit pipeline schedule for Custom @@ -424,10 +432,10 @@ class ServiceBaseClass { await page.click('[data-testid="view-service-button"]'); await expect(page.getByTestId('schedule-primary-details')).toHaveText( - 'Every minute' + 'Every hour' ); await expect(page.getByTestId('schedule-secondary-details')).toHaveText( - 'Every hour, only on saturday, only in february' + 'Only on saturday, only in february' ); }