From 48c81a6277d6e2e8aabe1382bbef9e36df4ccba8 Mon Sep 17 00:00:00 2001 From: Aniket Katkar Date: Mon, 20 Jan 2025 10:32:53 +0530 Subject: [PATCH] Minor: Improve cron expression validations (#19426) --- .../src/main/resources/ui/package.json | 2 +- .../e2e/Features/CronValidations.spec.ts | 182 ++++++++++++++++++ .../AddIngestion/Steps/ScheduleInterval.tsx | 18 +- .../ui/src/locale/languages/de-de.json | 1 + .../ui/src/locale/languages/en-us.json | 1 + .../ui/src/locale/languages/es-es.json | 1 + .../ui/src/locale/languages/fr-fr.json | 1 + .../ui/src/locale/languages/gl-es.json | 3 +- .../ui/src/locale/languages/he-he.json | 1 + .../ui/src/locale/languages/ja-jp.json | 1 + .../ui/src/locale/languages/mr-in.json | 1 + .../ui/src/locale/languages/nl-nl.json | 1 + .../ui/src/locale/languages/pr-pr.json | 1 + .../ui/src/locale/languages/pt-br.json | 1 + .../ui/src/locale/languages/pt-pt.json | 1 + .../ui/src/locale/languages/ru-ru.json | 1 + .../ui/src/locale/languages/th-th.json | 1 + .../ui/src/locale/languages/zh-cn.json | 1 + .../ui/src/utils/SchedularUtils.test.tsx | 83 ++++++++ .../resources/ui/src/utils/SchedularUtils.tsx | 49 +++++ .../src/main/resources/ui/yarn.lock | 8 +- 21 files changed, 337 insertions(+), 22 deletions(-) create mode 100644 openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/CronValidations.spec.ts diff --git a/openmetadata-ui/src/main/resources/ui/package.json b/openmetadata-ui/src/main/resources/ui/package.json index 162e0c2d705..007ed2df820 100644 --- a/openmetadata-ui/src/main/resources/ui/package.json +++ b/openmetadata-ui/src/main/resources/ui/package.json @@ -78,7 +78,7 @@ "classnames": "^2.3.1", "codemirror": "^5.65.16", "cookie-storage": "^6.1.0", - "cronstrue": "^1.122.0", + "cronstrue": "^2.53.0", "crypto-random-string-with-promisify-polyfill": "^5.0.0", "diff": "^5.0.0", "dompurify": "^3.1.5", diff --git a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/CronValidations.spec.ts b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/CronValidations.spec.ts new file mode 100644 index 00000000000..788ad7a5840 --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/CronValidations.spec.ts @@ -0,0 +1,182 @@ +/* + * Copyright 2025 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 { expect, Page, test } from '@playwright/test'; +import { GlobalSettingOptions } from '../../constant/settings'; +import { redirectToHomePage } from '../../utils/common'; +import { settingClick } from '../../utils/sidebar'; + +const inputCronExpression = async (page: Page, cron: string) => { + await page + .locator('[data-testid="cron-container"] #schedular-form_cron') + .click(); + await page + .locator('[data-testid="cron-container"] #schedular-form_cron') + .clear(); + await page + .locator('[data-testid="cron-container"] #schedular-form_cron') + .fill(cron); +}; + +// use the admin user to login +test.use({ + storageState: 'playwright/.auth/admin.json', +}); + +test.describe('Cron Validations', () => { + test('Validate different cron expressions', async ({ page }) => { + await redirectToHomePage(page); + + await settingClick(page, GlobalSettingOptions.APPLICATIONS); + + const applicationResponse = page.waitForResponse( + '/api/v1/apps/name/SearchIndexingApplication/status?offset=0&limit=1' + ); + + await page + .locator( + '[data-testid="search-indexing-application-card"] [data-testid="config-btn"]' + ) + .click(); + + await applicationResponse; + + await page.click('[data-testid="edit-button"]'); + await page.waitForSelector('[data-testid="schedular-card-container"]'); + await page + .getByTestId('schedular-card-container') + .getByText('Schedule', { exact: true }) + .click(); + + await page + .getByTestId('time-dropdown-container') + .getByTestId('cron-type') + .click(); + + await page.click('.ant-select-dropdown:visible [title="Custom"]'); + + await page.waitForSelector( + '[data-testid="cron-container"] #schedular-form_cron' + ); + + // Check Valid Crons + + // Check '0 0 * * *' to be valid + await inputCronExpression(page, '0 0 * * *'); + + await expect( + page.getByTestId('cron-container').getByText('At 12:00 AM, every day') + ).toBeAttached(); + await expect(page.locator('#schedular-form_cron_help')).not.toBeAttached(); + + // Check '0 0 1/3 * * 1' to be valid + await inputCronExpression(page, '0 0 1/3 * * 1'); + + await expect( + page + .getByTestId('cron-container') + .getByText( + 'At 0 minutes past the hour, every 3 hours, starting at 01:00 AM, only on Monday' + ) + ).toBeAttached(); + await expect(page.locator('#schedular-form_cron_help')).not.toBeAttached(); + + // Check '0 0 * * 1-6' to be valid + await inputCronExpression(page, '0 0 * * 1-6'); + + await expect( + page + .getByTestId('cron-container') + .getByText('At 12:00 AM, Monday through Saturday') + ).toBeAttached(); + await expect(page.locator('#schedular-form_cron_help')).not.toBeAttached(); + + // Check Invalid crons + + // Check every minute frequency throws an error + await inputCronExpression(page, '0/1 0 * * *'); + + await expect( + page + .locator('#schedular-form_cron_help') + .getByText( + 'Cron schedule too frequent. Please choose at least 1-hour intervals.' + ) + ).toBeAttached(); + + // Check every second frequency throws an error + await inputCronExpression(page, '0/1 0 * * * 1'); + + await expect( + page + .locator('#schedular-form_cron_help') + .getByText( + 'Cron schedule too frequent. Please choose at least 1-hour intervals.' + ) + ).toBeAttached(); + + // Check '0 0 * * 7' to be invalid + await inputCronExpression(page, '0 0 * * 7'); + + await expect( + page + .locator('#schedular-form_cron_help') + .getByText('DOW part must be >= 0 and <= 6') + ).toBeAttached(); + + // Check '0 0 * * 1 7' to be invalid + await inputCronExpression(page, '0 0 * * 1 7'); + + await expect( + page + .locator('#schedular-form_cron_help') + .getByText('DOW part must be >= 0 and <= 6') + ).toBeAttached(); + + // Check '0 0 * * 1 7 67' to be invalid + await inputCronExpression(page, '0 0 * * 1 7 67'); + + await expect( + page + .locator('#schedular-form_cron_help') + .getByText('DOW part must be >= 0 and <= 6') + ).toBeAttached(); + + // Check '0 0 * * 0-7' to be invalid + await inputCronExpression(page, '0 0 * * 0-7'); + + await expect( + page + .locator('#schedular-form_cron_help') + .getByText('DOW part must be >= 0 and <= 6') + ).toBeAttached(); + + // Check '0 0 * * 7-9' to be invalid + await inputCronExpression(page, '0 0 * * 7-9'); + + await expect( + page + .locator('#schedular-form_cron_help') + .getByText('DOW part must be >= 0 and <= 6') + ).toBeAttached(); + + // Check '0 0 * * -1-9' to be invalid + await inputCronExpression(page, '0 0 * * -1-9'); + + await expect( + page + .locator('#schedular-form_cron_help') + .getByText('Error: DOW part must be >= 0 and <= 6') + ).toBeAttached(); + }); +}); 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 61c88355151..d586974b506 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 @@ -49,6 +49,7 @@ import { import { generateFormFields } from '../../../../../utils/formUtils'; import { getCurrentLocaleForConstrue } from '../../../../../utils/i18next/i18nextUtil'; import { + cronValidator, getCron, getDefaultScheduleValue, getHourMinuteSelect, @@ -360,22 +361,7 @@ const ScheduleInterval = ({ }), }, { - validator: async (_, value) => { - // Check if cron is valid and get the description - const description = cronstrue.toString(value); - - // Check if cron has a frequency of less than an hour - const isFrequencyInMinutes = /Every \d* *minute/.test( - description - ); - if (isFrequencyInMinutes) { - return Promise.reject( - t('message.cron-less-than-hour-message') - ); - } - - return Promise.resolve(); - }, + validator: cronValidator, }, ]}> 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 951e986c871..5045aaaf9a3 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 @@ -1488,6 +1488,7 @@ "create-new-glossary-guide": "Ein Glossar ist ein kontrolliertes Vokabular, das verwendet wird, um die Konzepte und Terminologie in einer Organisation zu definieren. Glossare können spezifisch für einen bestimmten Bereich sein (z. B. Business Glossar, Technisches Glossar). Im Glossar können die Standardbegriffe und Konzepte definiert werden, zusammen mit Synonymen und verwandten Begriffen. Es kann festgelegt werden, wie und von wem Begriffe im Glossar hinzugefügt werden können.", "create-or-update-email-account-for-bot": "Die Änderung der Kontaktemail aktualisiert oder erstellt einen neuen Bot-Benutzer.", "created-this-task-lowercase": "hat diese Aufgabe erstellt", + "cron-dow-validation-failure": "Der DOW-Teil muss >= 0 und <= 6 sein", "cron-less-than-hour-message": "Cron schedule too frequent. Please choose at least 1-hour intervals.", "custom-classification-name-dbt-tags": "Benutzerdefinierter OpenMetadata-Klassifikationsname für dbt-Tags ", "custom-favicon-url-path-message": "URL path for the favicon icon.", 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 12c0e1155e4..7ae350ab4d5 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 @@ -1488,6 +1488,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-dow-validation-failure": "DOW part must be >= 0 and <= 6", "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.", 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 4046d52f1f1..6d1ea630bdf 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 @@ -1488,6 +1488,7 @@ "create-new-glossary-guide": "Un glosario es un vocabulario controlado utilizado para definir los conceptos y terminología en una organización. Los glosarios pueden ser específicos para un determinado dominio (por ejemplo, glosario de negocios o técnico). En el glosario, se pueden definir los términos y conceptos estándar junto con los sinónimos y términos relacionados. Se puede establecer control sobre cómo y quién puede agregar los términos en el glosario.", "create-or-update-email-account-for-bot": "Cambiar el correo electrónico de la cuenta actualizará o creará un nuevo bot.", "created-this-task-lowercase": "creó esta tarea", + "cron-dow-validation-failure": "La parte DOW debe ser >= 0 y <= 6", "cron-less-than-hour-message": "Cron schedule too frequent. Please choose at least 1-hour intervals.", "custom-classification-name-dbt-tags": "Nombre personalizado de clasificación de OpenMetadata para tags de dbt", "custom-favicon-url-path-message": "Ruta URL para el ícono de favicon.", 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 917bdb6c1ed..27866a462ce 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 @@ -1488,6 +1488,7 @@ "create-new-glossary-guide": "Un Glossaire est un recueil de termes et vocabulaire utilisé pour définir des concepts et terminologies. Glossaires peuvent être spécifiques à certains domaines (e.g., Glossaire Business, Glossaire Technique, etc.). Dans le glossaire, les termes et concepts peuvent être définis tout en spécifiant des synonymes et des termes liés. Il est possible de contrôler qui peut ajouter des termes dans le dans le glossaire et comment ces termes peuvent être ajoutés.", "create-or-update-email-account-for-bot": "Changer l'email créera un nouveau ou mettra à jour l'agent numérique", "created-this-task-lowercase": "a créé cette tâche", + "cron-dow-validation-failure": "La partie DOW doit être >= 0 et <= 6", "cron-less-than-hour-message": "Cron schedule too frequent. Please choose at least 1-hour intervals.", "custom-classification-name-dbt-tags": "Nom personnalisé de la classification OpenMetadata pour les tags dbt ", "custom-favicon-url-path-message": "Chemin (URL) de l'icône favicon.", diff --git a/openmetadata-ui/src/main/resources/ui/src/locale/languages/gl-es.json b/openmetadata-ui/src/main/resources/ui/src/locale/languages/gl-es.json index e37ad4fa849..e357577b5fb 100644 --- a/openmetadata-ui/src/main/resources/ui/src/locale/languages/gl-es.json +++ b/openmetadata-ui/src/main/resources/ui/src/locale/languages/gl-es.json @@ -1488,7 +1488,8 @@ "create-new-glossary-guide": "Un glosario é un vocabulario controlado que se usa para definir os conceptos e a terminoloxía nunha organización. Os glosarios poden ser específicos dun determinado dominio (por exemplo, glosario empresarial, glosario técnico). No glosario, poden definirse os termos e conceptos estándar, xunto cos sinónimos e termos relacionados. Pódese establecer control sobre como e quen pode engadir termos ao glosario.", "create-or-update-email-account-for-bot": "Cambiar o correo electrónico da conta actualizará ou creará un novo usuario bot.", "created-this-task-lowercase": "creou esta tarefa", - "cron-less-than-hour-message": "Cron schedule too frequent. Please choose at least 1-hour intervals.", + "cron-dow-validation-failure": "A parte DOW debe ser >= 0 e <= 6", + "cron-less-than-hour-message": "Horario Cron demasiado frecuente. Escolle intervalos de polo menos 1 hora.", "custom-classification-name-dbt-tags": "Nome da clasificación personalizada de OpenMetadata para etiquetas dbt", "custom-favicon-url-path-message": "Ruta URL para o icono favicon.", "custom-logo-configuration-message": "Personaliza OpenMetadata co logotipo da túa empresa, monograma e favicon.", 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 e6abcfa965d..ce55ee75e97 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 @@ -1488,6 +1488,7 @@ "create-new-glossary-guide": "מדוע לא ניתן לתת למשתמש האחרון את השאלה שלו?", "create-or-update-email-account-for-bot": "שינוי כתובת האימייל של החשבון יעדכן או ייצור משתמש בוט חדש.", "created-this-task-lowercase": "יצר משימה זו", + "cron-dow-validation-failure": "חלק DOW חייב להיות >= 0 ו-<= 6", "cron-less-than-hour-message": "Cron schedule too frequent. Please choose at least 1-hour intervals.", "custom-classification-name-dbt-tags": "שם קטגוריה מותאמת אישית של OpenMetadata עבור תגי dbt ", "custom-favicon-url-path-message": "נתיב URL עבור סמל האתר.", 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 cdd8e20c5cb..f79442703c8 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 @@ -1488,6 +1488,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": "このタスクを作成する", + "cron-dow-validation-failure": "DOW部分は0以上6以下でなければなりません", "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.", diff --git a/openmetadata-ui/src/main/resources/ui/src/locale/languages/mr-in.json b/openmetadata-ui/src/main/resources/ui/src/locale/languages/mr-in.json index 02dbb8788ba..b410e7088d6 100644 --- a/openmetadata-ui/src/main/resources/ui/src/locale/languages/mr-in.json +++ b/openmetadata-ui/src/main/resources/ui/src/locale/languages/mr-in.json @@ -1488,6 +1488,7 @@ "create-new-glossary-guide": "शब्दकोश ही नियंत्रित शब्दावली आहे जी संस्थेतील संकल्पना आणि शब्दावली परिभाषित करण्यासाठी वापरली जाते. शब्दकोश विशिष्ट डोमेनसाठी विशिष्ट असू शकतात (उदा., व्यवसाय शब्दकोश, तांत्रिक शब्दकोश). शब्दकोशात, मानक संज्ञा आणि संकल्पना समानार्थी शब्द आणि संबंधित संज्ञांसह परिभाषित केल्या जाऊ शकतात. शब्दकोशात संज्ञा कशा आणि कोण जोडू शकतात यावर नियंत्रण स्थापित केले जाऊ शकते.", "create-or-update-email-account-for-bot": "खाते ईमेल बदलल्याने नवीन बॉट वापरकर्ता अद्यतनित किंवा तयार होईल.", "created-this-task-lowercase": "हे कार्य तयार केले", + "cron-dow-validation-failure": "DOW भाग >= 0 आणि <= 6 असणे आवश्यक आहे", "cron-less-than-hour-message": "क्रॉन वेळापत्रक खूप वारंवार आहे. कृपया किमान 1-तास अंतर निवडा.", "custom-classification-name-dbt-tags": "dbt टॅगसाठी सानुकूल OpenMetadata वर्गीकरण नाव", "custom-favicon-url-path-message": "फेविकॉन आयकॉनसाठी URL पथ.", 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 88fa90e449c..86db72a8f60 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 @@ -1488,6 +1488,7 @@ "create-new-glossary-guide": "Een woordenboek is een gecontroleerde woordenlijst die wordt gebruikt om concepten en terminologie van een organisatie te definiëren. Woordenboeken kunnen domeinspecifiek zijn (bijv. Bedrijfswoordenboek, Technisch woordenboek). In het woordenboek kunnen standaardtermen en concepten worden gedefinieerd, samen met synoniemen en gerelateerde termen. Hoe en wie de termen in het woordenboek kan toevoegen, kan worden beheerd.", "create-or-update-email-account-for-bot": "Het wijzigen van het account-e-mailadres zal een nieuwe botgebruiker updaten of maken.", "created-this-task-lowercase": "heeft deze taak aangemaakt", + "cron-dow-validation-failure": "DOW-deel moet >= 0 en <= 6 zijn", "cron-less-than-hour-message": "Cron schedule too frequent. Please choose at least 1-hour intervals.", "custom-classification-name-dbt-tags": "Aangepaste OpenMetadata-classificatienaam voor dbt-tags", "custom-favicon-url-path-message": "URL-pad voor het favicon-pictogram.", 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 f07f2e9025a..2dc2ae75363 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 @@ -1488,6 +1488,7 @@ "create-new-glossary-guide": "یک فرهنگ لغت واژگان کنترل‌شده‌ای است که برای تعریف مفاهیم و اصطلاحات در یک سازمان استفاده می‌شود. فرهنگ لغت‌ها می‌توانند خاص یک دامنه خاص باشند (به عنوان مثال، فرهنگ لغت تجاری، فرهنگ لغت فنی). در فرهنگ لغت، اصطلاحات و مفاهیم استاندارد می‌توانند همراه با مترادف‌ها و اصطلاحات مرتبط تعریف شوند. کنترل می‌تواند بر نحوه و افرادی که می‌توانند اصطلاحات را به فرهنگ لغت اضافه کنند، برقرار شود.", "create-or-update-email-account-for-bot": "تغییر ایمیل حساب باعث به‌روزرسانی یا ایجاد یک کاربر ربات جدید خواهد شد.", "created-this-task-lowercase": "این وظیفه را ایجاد کرد", + "cron-dow-validation-failure": "بخش DOW باید >= 0 و <= 6 باشد", "cron-less-than-hour-message": "Cron schedule too frequent. Please choose at least 1-hour intervals.", "custom-classification-name-dbt-tags": "نام دسته‌بندی سفارشی OpenMetadata برای برچسب‌های dbt", "custom-favicon-url-path-message": "مسیر URL برای آیکون favicon.", 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 301b99520e5..1c95271c777 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 @@ -1488,6 +1488,7 @@ "create-new-glossary-guide": "Um Glossário é um vocabulário controlado usado para definir os conceitos e terminologias em uma organização. Os glossários podem ser específicos para um determinado domínio (por exemplo, Glossário de Negócios, Glossário Técnico). No glossário, os termos e conceitos padrão podem ser definidos juntamente com os sinônimos e termos relacionados. O controle pode ser estabelecido sobre como e quem pode adicionar os termos no glossário.", "create-or-update-email-account-for-bot": "Alterar o e-mail da conta atualizará ou criará um novo usuário bot.", "created-this-task-lowercase": "criou esta tarefa", + "cron-dow-validation-failure": "A parte DOW deve ser >= 0 e <= 6", "cron-less-than-hour-message": "Cron schedule too frequent. Please choose at least 1-hour intervals.", "custom-classification-name-dbt-tags": "Nome de Classificação Personalizada do OpenMetadata para tags dbt", "custom-favicon-url-path-message": "Caminho da URL para o ícone favicon.", diff --git a/openmetadata-ui/src/main/resources/ui/src/locale/languages/pt-pt.json b/openmetadata-ui/src/main/resources/ui/src/locale/languages/pt-pt.json index c27c48832b2..e2c1138a79b 100644 --- a/openmetadata-ui/src/main/resources/ui/src/locale/languages/pt-pt.json +++ b/openmetadata-ui/src/main/resources/ui/src/locale/languages/pt-pt.json @@ -1488,6 +1488,7 @@ "create-new-glossary-guide": "Um Glossário é um vocabulário controlado usado para definir os conceitos e terminologias em uma organização. Os glossários podem ser específicos para um determinado domínio (por exemplo, Glossário de Negócios, Glossário Técnico). No glossário, os termos e conceitos padrão podem ser definidos juntamente com os sinónimos e termos relacionados. O controle pode ser estabelecido sobre como e quem pode adicionar os termos no glossário.", "create-or-update-email-account-for-bot": "Alterar o e-mail da conta atualizará ou criará um novo utilizador bot.", "created-this-task-lowercase": "criou esta tarefa", + "cron-dow-validation-failure": "A parte DOW deve ser >= 0 e <= 6", "cron-less-than-hour-message": "Cron schedule too frequent. Please choose at least 1-hour intervals.", "custom-classification-name-dbt-tags": "Nome de Classificação Personalizada do OpenMetadata para tags dbt", "custom-favicon-url-path-message": "Caminho da URL para o ícone favicon.", 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 31bd5c778b6..19e49bbaaee 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 @@ -1488,6 +1488,7 @@ "create-new-glossary-guide": "Глоссарий — это контролируемый словарь, используемый для определения понятий и терминологии в организации. Глоссарии могут относиться к определенному домену (например, деловой глоссарий, технический глоссарий). В глоссарии стандартные термины и понятия могут быть определены вместе с синонимами и родственными терминами. Можно установить контроль над тем, как и кто может добавлять термины в глоссарий.", "create-or-update-email-account-for-bot": "Изменение адреса электронной почты учетной записи приведет к обновлению или созданию нового пользователя-бота.", "created-this-task-lowercase": "Задача создана", + "cron-dow-validation-failure": "Часть DOW должна быть >= 0 и <= 6", "cron-less-than-hour-message": "Cron schedule too frequent. Please choose at least 1-hour intervals.", "custom-classification-name-dbt-tags": "Пользовательское имя классификации OpenMetadata для тегов dbt", "custom-favicon-url-path-message": "URL path for the favicon icon.", diff --git a/openmetadata-ui/src/main/resources/ui/src/locale/languages/th-th.json b/openmetadata-ui/src/main/resources/ui/src/locale/languages/th-th.json index 122c803d577..4e98227be81 100644 --- a/openmetadata-ui/src/main/resources/ui/src/locale/languages/th-th.json +++ b/openmetadata-ui/src/main/resources/ui/src/locale/languages/th-th.json @@ -1488,6 +1488,7 @@ "create-new-glossary-guide": "สารานุกรมคือคำศัพท์ที่ควบคุมใช้ในการกำหนดแนวคิดและคำศัพท์ในองค์กร สารานุกรมสามารถเฉพาะเจาะจงได้ตามโดเมนใดโดเมนหนึ่ง (เช่น สารานุกรมธุรกิจ, สารานุกรมเทคนิค) ในสารานุกรม คำศัพท์และแนวคิดมาตรฐานสามารถถูกกำหนดพร้อมกับคำพ้องและคำที่เกี่ยวข้อง ควบคุมสามารถถูกตั้งขึ้นเกี่ยวกับวิธีการและผู้ที่สามารถเพิ่มคำในสารานุกรม", "create-or-update-email-account-for-bot": "การเปลี่ยนแปลงอีเมลบัญชีจะอัปเดตหรือสร้างผู้ใช้บอทใหม่", "created-this-task-lowercase": "สร้างงานนี้", + "cron-dow-validation-failure": "ส่วน DOW ต้องเป็น >= 0 และ <= 6", "cron-less-than-hour-message": "กำหนดการ Cron บ่อยเกินไป กรุณาเลือกช่วงเวลาอย่างน้อย 1 ชั่วโมง", "custom-classification-name-dbt-tags": "ชื่อการจำแนกประเภท OpenMetadata ที่กำหนดเองสำหรับแท็ก dbt", "custom-favicon-url-path-message": "เส้นทาง URL สำหรับไอคอน favicon", 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 03a0f2cf8e6..2044a39de25 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 @@ -1488,6 +1488,7 @@ "create-new-glossary-guide": "术语库是用于定义组织中的概念和术语的受控词汇集合。术语库可以特定于某个域(例如, 业务术语库, 技术术语库)。在术语库中, 可以定义标准术语、概念及其同义词和相关术语。还可以控制向术语库中添加术语的人员和方式。", "create-or-update-email-account-for-bot": "更改帐号电子邮箱将更新或创建一个新的机器人用户", "created-this-task-lowercase": "创建了此任务", + "cron-dow-validation-failure": "DOW 部分必须 >= 0 且 <= 6", "cron-less-than-hour-message": "Cron schedule too frequent. Please choose at least 1-hour intervals.", "custom-classification-name-dbt-tags": "dbt 标签的自定义 OpenMetadata 分类名称", "custom-favicon-url-path-message": "网站 Favicon 指向的 URL 地址", diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/SchedularUtils.test.tsx b/openmetadata-ui/src/main/resources/ui/src/utils/SchedularUtils.test.tsx index a9c08561ec7..f82afc50568 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/SchedularUtils.test.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/utils/SchedularUtils.test.tsx @@ -20,6 +20,8 @@ import { mockOldState1, } from '../mocks/Schedular.mock'; import { + checkDOWValidity, + cronValidator, getCronDefaultValue, getScheduleOptionsFromSchedules, getUpdatedStateFromFormState, @@ -162,3 +164,84 @@ describe('getUpdatedStateFromFormState', () => { }); }); }); + +describe('checkDOWValidity', () => { + it('should not throw an error for valid day of week (0-6)', () => { + expect(() => checkDOWValidity('0')).not.toThrow(); + expect(() => checkDOWValidity('3')).not.toThrow(); + expect(() => checkDOWValidity('6')).not.toThrow(); + }); + + it('should throw an error for invalid day of week is >6)', async () => { + await expect(checkDOWValidity('7')).rejects.toMatch( + 'message.cron-dow-validation-failure' + ); + }); + + it('should not throw an error for valid day of week range (0-6)', () => { + expect(() => checkDOWValidity('0-3')).not.toThrow(); + expect(() => checkDOWValidity('4-6')).not.toThrow(); + }); + + it('should throw an error for invalid day of week range is >6', async () => { + await expect(checkDOWValidity('7-9')).rejects.toMatch( + 'message.cron-dow-validation-failure' + ); + }); + + it('should throw an error for mixed valid and invalid day of week range', async () => { + await expect(checkDOWValidity('0-7')).rejects.toMatch( + 'message.cron-dow-validation-failure' + ); + }); +}); + +describe('cronValidator', () => { + it('should resolve for valid cron expression', async () => { + await expect(cronValidator({}, '0 0 * * *')).resolves.toBeUndefined(); + }); + + it('should reject for cron expression with frequency less than an hour', async () => { + await expect(cronValidator({}, '*/30 * * * *')).rejects.toMatch( + 'message.cron-less-than-hour-message' + ); + }); + + it('should reject for invalid day of week (<0 or >6)', async () => { + await expect(cronValidator({}, '0 0 * * 7')).rejects.toMatch( + 'message.cron-dow-validation-failure' + ); + await expect(cronValidator({}, '0 0 * * -1')).rejects.toMatch( + 'Error: DOW part must be >= 0 and <= 6' + ); + }); + + it('should resolve for valid day of week (0-6)', async () => { + await expect(cronValidator({}, '0 0 * * 0')).resolves.toBeUndefined(); + await expect(cronValidator({}, '0 0 * * 3')).resolves.toBeUndefined(); + await expect(cronValidator({}, '0 0 * * 6')).resolves.toBeUndefined(); + }); + + it('should resolve for valid day of week range (0-6)', async () => { + await expect(cronValidator({}, '0 0 * * 0-3')).resolves.toBeUndefined(); + await expect(cronValidator({}, '0 0 * * 4-6')).resolves.toBeUndefined(); + }); + + it('should reject for invalid day of week range (<0 or >6)', async () => { + await expect(cronValidator({}, '0 0 * * 4-7')).rejects.toMatch( + 'message.cron-dow-validation-failure' + ); + await expect(cronValidator({}, '0 0 * * -1-3')).rejects.toMatch( + 'Error: DOW part must be >= 0 and <= 6' + ); + }); + + it('should reject for mixed valid and invalid day of week range', async () => { + await expect(cronValidator({}, '0 0 * * 0-7')).rejects.toMatch( + 'message.cron-dow-validation-failure' + ); + await expect(cronValidator({}, '0 0 * * -1-6')).rejects.toMatch( + 'Error: DOW part must be >= 0 and <= 6' + ); + }); +}); diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/SchedularUtils.tsx b/openmetadata-ui/src/main/resources/ui/src/utils/SchedularUtils.tsx index 8d50f240e86..563f47c3105 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/SchedularUtils.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/utils/SchedularUtils.tsx @@ -12,7 +12,10 @@ */ import { Select } from 'antd'; +import cronstrue from 'cronstrue/i18n'; +import { t } from 'i18next'; import { isUndefined, toNumber, toString } from 'lodash'; +import { RuleObject } from 'rc-field-form/es/interface'; import React from 'react'; import { Combination, @@ -281,3 +284,49 @@ export const getUpdatedStateFromFormState = ( return { ...currentState, ...formValues }; } }; + +export const checkDOWValidity = async (dow: string) => { + // Check if dow is valid if it is not a number between 0-6 + const isDayValid = toNumber(dow) < 0 || toNumber(dow) > 6; + + // Check if dow is a range and any of the values are not between 0-6 + const isDayRangeValid = + dow.includes('-') && + dow.split('-').some((d) => toNumber(d) < 0 || toNumber(d) > 6); + + // If dow is not valid or dow range is not valid, throw an error + if (isDayValid || isDayRangeValid) { + return Promise.reject(t('message.cron-dow-validation-failure')); + } + + return Promise.resolve(); +}; + +export const cronValidator = async (_: RuleObject, value: string) => { + // Check if cron is valid and get the description + const description = cronstrue.toString(value.trim()); + + // Check if cron has a frequency of less than an hour + const isFrequencyInMinutes = /Every \d* *minute/.test(description); + const isFrequencyInSeconds = /Every \d* *second/.test(description); + + if (isFrequencyInMinutes || isFrequencyInSeconds) { + return Promise.reject(t('message.cron-less-than-hour-message')); + } + + // Check if dow is other than 0-6 + // Adding this manual check since cronstrue accepts 7 as a valid value for dow + // which is not a valid value for argo + const cronParts = value.trim().split(' '); + + // dow is at index 4 if there is no year field or seconds field + let dow = cronParts[4]; + if (cronParts.length !== 5) { + dow = cronParts[5]; + } + + // Check if dow is valid + await checkDOWValidity(dow); + + return Promise.resolve(); +}; diff --git a/openmetadata-ui/src/main/resources/ui/yarn.lock b/openmetadata-ui/src/main/resources/ui/yarn.lock index 3e063ab4db6..7c45f9ea918 100644 --- a/openmetadata-ui/src/main/resources/ui/yarn.lock +++ b/openmetadata-ui/src/main/resources/ui/yarn.lock @@ -6515,10 +6515,10 @@ crelt@^1.0.0: resolved "https://registry.yarnpkg.com/crelt/-/crelt-1.0.6.tgz#7cc898ea74e190fb6ef9dae57f8f81cf7302df72" integrity sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g== -cronstrue@^1.122.0: - version "1.122.0" - resolved "https://registry.yarnpkg.com/cronstrue/-/cronstrue-1.122.0.tgz#bd6838077b476d28f61d381398b47b8c3912a126" - integrity sha512-PFuhZd+iPQQ0AWTXIEYX+t3nFGzBrWxmTWUKJOrsGRewaBSLKZ4I1f8s2kryU75nNxgyugZgiGh2OJsCTA/XlA== +cronstrue@^2.53.0: + version "2.53.0" + resolved "https://registry.yarnpkg.com/cronstrue/-/cronstrue-2.53.0.tgz#5bbcd7483636b99379480f624faef5056f3efbd8" + integrity sha512-CkAcaI94xL8h6N7cGxgXfR5D7oV2yVtDzB9vMZP8tIgPyEv/oc/7nq9rlk7LMxvc3N+q6LKZmNLCVxJRpyEg8A== cross-fetch@^3.1.5: version "3.1.5"