From 5b7c569ebf2b0141878d2af67ba29b3ea104bf76 Mon Sep 17 00:00:00 2001 From: Chirag Madlani <12962843+chirag-madlani@users.noreply.github.com> Date: Mon, 8 Sep 2025 14:56:37 +0530 Subject: [PATCH] feat(ui): support custom operator for array object contains (#23248) * feat(ui): support custom operator for array object contains * add contains logicOps * added playwright test and supported contains in tags and glossary as well * fix sonar issue --------- Co-authored-by: Pere Miquel Brull Co-authored-by: Ashish Gupta --- .../openmetadata/service/rules/LogicOps.java | 26 +++ .../service/rules/RuleEngineTests.java | 102 +++++++++++ .../ui/playwright/constant/dataContracts.ts | 19 ++ .../e2e/Pages/DataContracts.spec.ts | 169 +++++++++++++++++- .../src/constants/DataContract.constants.ts | 6 + .../ui/src/locale/languages/de-de.json | 1 + .../ui/src/locale/languages/en-us.json | 1 + .../ui/src/locale/languages/es-es.json | 8 +- .../ui/src/locale/languages/fr-fr.json | 1 + .../ui/src/locale/languages/gl-es.json | 1 + .../ui/src/locale/languages/he-he.json | 7 +- .../ui/src/locale/languages/ja-jp.json | 1 + .../ui/src/locale/languages/ko-kr.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/tr-tr.json | 1 + .../ui/src/locale/languages/zh-cn.json | 1 + .../ui/src/locale/languages/zh-tw.json | 1 + .../utils/DataContract/DataContractUtils.ts | 26 ++- .../ui/src/utils/JSONLogicSearchClassBase.ts | 25 ++- 25 files changed, 382 insertions(+), 22 deletions(-) diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/rules/LogicOps.java b/openmetadata-service/src/main/java/org/openmetadata/service/rules/LogicOps.java index fb65def68fd..37eb7c33c65 100644 --- a/openmetadata-service/src/main/java/org/openmetadata/service/rules/LogicOps.java +++ b/openmetadata-service/src/main/java/org/openmetadata/service/rules/LogicOps.java @@ -28,6 +28,7 @@ public class LogicOps { public enum CustomLogicOps { LENGTH("length"), + CONTAINS("contains"), IS_REVIEWER("isReviewer"), IS_OWNER("isOwner"); @@ -55,6 +56,31 @@ public class LogicOps { return args.length; }); + jsonLogic.addOperation( + "contains", + (args) -> { + if (nullOrEmpty(args) || args.length < 2) { + return false; + } + + Object value = args[0]; + Object container = args[1]; + + // If either value or container is null/empty, the rule is broken + if (CommonUtil.nullOrEmpty(value) || CommonUtil.nullOrEmpty(container)) { + return false; + } + + if (container instanceof List list) { + return list.contains(value); + } else if (container.getClass().isArray()) { + Object[] array = (Object[]) container; + return Arrays.asList(array).contains(value); + } else { + return container.toString().contains(value.toString()); + } + }); + // {"isReviewer": { var: "updatedBy"} } jsonLogic.addOperation( new JsonLogicExpression() { diff --git a/openmetadata-service/src/test/java/org/openmetadata/service/rules/RuleEngineTests.java b/openmetadata-service/src/test/java/org/openmetadata/service/rules/RuleEngineTests.java index 11518a01168..6ae248cfe0d 100644 --- a/openmetadata-service/src/test/java/org/openmetadata/service/rules/RuleEngineTests.java +++ b/openmetadata-service/src/test/java/org/openmetadata/service/rules/RuleEngineTests.java @@ -476,6 +476,108 @@ public class RuleEngineTests extends OpenMetadataApplicationTest { RuleEngine.getInstance().evaluate(table, List.of(piiRule), false, false); } + @Test + @Execution(ExecutionMode.CONCURRENT) + void testContainsOperator(TestInfo test) { + Table table = getMockTable(test); + + // Test case 1: Empty tags - rule should fail (no tags to check) + table.withTags(List.of()); + SemanticsRule containsRule = + new SemanticsRule() + .withName("Tag FQN must be in allowed list") + .withDescription("Validates that tagFQN is contained in the allowed list") + .withRule( + "{\"and\":[{\"some\":[{\"var\":\"tags\"},{\"contains\":[{\"var\":\"tagFQN\"},[\"Tier.Tier3\",\"Tier.Tier2\"]]}]}]}"); + + assertThrows( + RuleValidationException.class, + () -> RuleEngine.getInstance().evaluate(table, List.of(containsRule), false, false)); + + // Test case 2: Tags with matching FQN - rule should pass + TagLabel allowedTag = + new TagLabel().withTagFQN("Tier.Tier2").withSource(TagLabel.TagSource.CLASSIFICATION); + table.withTags(List.of(allowedTag)); + RuleEngine.getInstance().evaluate(table, List.of(containsRule), false, false); + + // Test case 3: Tags with non-matching FQN - rule should fail + TagLabel disallowedTag = + new TagLabel().withTagFQN("Tier.Tier1").withSource(TagLabel.TagSource.CLASSIFICATION); + table.withTags(List.of(disallowedTag)); + assertThrows( + RuleValidationException.class, + () -> RuleEngine.getInstance().evaluate(table, List.of(containsRule), false, false)); + + // Test case 4: Multiple tags with at least one matching FQN - rule should pass + table.withTags(List.of(disallowedTag, allowedTag)); + RuleEngine.getInstance().evaluate(table, List.of(containsRule), false, false); + + // Test case 5: Multiple allowed tags - rule should pass + TagLabel anotherAllowedTag = + new TagLabel().withTagFQN("Tier.Tier3").withSource(TagLabel.TagSource.CLASSIFICATION); + table.withTags(List.of(allowedTag, anotherAllowedTag)); + RuleEngine.getInstance().evaluate(table, List.of(containsRule), false, false); + } + + @Test + @Execution(ExecutionMode.CONCURRENT) + void testContainsOperatorEdgeCases(TestInfo test) { + Table table = getMockTable(test); + + // Test case 1: Rule with single allowed value + SemanticsRule singleValueRule = + new SemanticsRule() + .withName("Tag FQN must be specific value") + .withDescription("Validates that tagFQN equals specific value using contains") + .withRule( + "{\"and\":[{\"some\":[{\"var\":\"tags\"},{\"contains\":[{\"var\":\"tagFQN\"},[\"Tier.Tier1\"]]}]}]}"); + + TagLabel exactMatch = + new TagLabel().withTagFQN("Tier.Tier1").withSource(TagLabel.TagSource.CLASSIFICATION); + table.withTags(List.of(exactMatch)); + RuleEngine.getInstance().evaluate(table, List.of(singleValueRule), false, false); + + TagLabel noMatch = + new TagLabel().withTagFQN("Tier.Tier2").withSource(TagLabel.TagSource.CLASSIFICATION); + table.withTags(List.of(noMatch)); + assertThrows( + RuleValidationException.class, + () -> RuleEngine.getInstance().evaluate(table, List.of(singleValueRule), false, false)); + + // Test case 2: Mixed tag sources (Glossary and Classification) + SemanticsRule mixedSourceRule = + new SemanticsRule() + .withName("Allow mixed tag sources") + .withDescription("Validates that either glossary or classification tags are allowed") + .withRule( + "{\"and\":[{\"some\":[{\"var\":\"tags\"},{\"contains\":[{\"var\":\"tagFQN\"},[\"Glossary.Term1\",\"Tier.Tier1\"]]}]}]}"); + + TagLabel glossaryTag = + new TagLabel().withTagFQN("Glossary.Term1").withSource(TagLabel.TagSource.GLOSSARY); + TagLabel classificationTag = + new TagLabel().withTagFQN("Tier.Tier1").withSource(TagLabel.TagSource.CLASSIFICATION); + + // Test with glossary tag + table.withTags(List.of(glossaryTag)); + RuleEngine.getInstance().evaluate(table, List.of(mixedSourceRule), false, false); + + // Test with classification tag + table.withTags(List.of(classificationTag)); + RuleEngine.getInstance().evaluate(table, List.of(mixedSourceRule), false, false); + + // Test with both + table.withTags(List.of(glossaryTag, classificationTag)); + RuleEngine.getInstance().evaluate(table, List.of(mixedSourceRule), false, false); + + // Test with neither allowed + TagLabel disallowedTag = + new TagLabel().withTagFQN("NotAllowed.Tag").withSource(TagLabel.TagSource.CLASSIFICATION); + table.withTags(List.of(disallowedTag)); + assertThrows( + RuleValidationException.class, + () -> RuleEngine.getInstance().evaluate(table, List.of(mixedSourceRule), false, false)); + } + /** * Helper method to create a real Domain entity for testing */ diff --git a/openmetadata-ui/src/main/resources/ui/playwright/constant/dataContracts.ts b/openmetadata-ui/src/main/resources/ui/playwright/constant/dataContracts.ts index 978eee03cce..c3979eb9780 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/constant/dataContracts.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/constant/dataContracts.ts @@ -52,3 +52,22 @@ export const NEW_TABLE_TEST_CASE = { value: '1000', description: 'New table test case for TableColumnCountToEqual', }; + +export const DATA_CONTRACT_CONTAIN_SEMANTICS = { + name: `data_contract_container_semantic_${uuid()}`, + description: 'new data contract semantic contains description ', + rules: [ + { + field: 'Tier', + operator: 'Contains', + }, + { + field: 'Tags', + operator: 'Contains', + }, + { + field: 'Glossary Term', + operator: 'Contains', + }, + ], +}; diff --git a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/DataContracts.spec.ts b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/DataContracts.spec.ts index 9c685b6cd45..8e1ef2a18c1 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/DataContracts.spec.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/DataContracts.spec.ts @@ -12,6 +12,7 @@ */ import { test as base, expect, Page } from '@playwright/test'; import { + DATA_CONTRACT_CONTAIN_SEMANTICS, DATA_CONTRACT_DETAILS, DATA_CONTRACT_SEMANTICS1, DATA_CONTRACT_SEMANTICS2, @@ -39,7 +40,13 @@ import { validateDataContractInsideBundleTestSuites, waitForDataContractExecution, } from '../../utils/dataContracts'; -import { addOwner, addOwnerWithoutValidation } from '../../utils/entity'; +import { + addOwner, + addOwnerWithoutValidation, + assignGlossaryTerm, + assignTag, + assignTier, +} from '../../utils/entity'; import { settingClick } from '../../utils/sidebar'; const adminUser = new UserClass(); @@ -1124,6 +1131,164 @@ test.describe('Data Contracts', () => { } }); + test('Semantic with Contains Operator should work for Tier, Tag and Glossary', async ({ + page, + }) => { + await redirectToHomePage(page); + await table.visitEntityPage(page); + await page.click('[data-testid="contract"]'); + await page.getByTestId('add-contract-button').click(); + + await expect(page.getByTestId('add-contract-card')).toBeVisible(); + + await expect(page.getByTestId('add-contract-card')).toBeVisible(); + + await page.getByTestId('contract-name').fill(DATA_CONTRACT_DETAILS.name); + + await page.getByRole('tab', { name: 'Semantics' }).click(); + + await expect(page.getByTestId('add-semantic-button')).toBeDisabled(); + + await page.fill('#semantics_0_name', DATA_CONTRACT_CONTAIN_SEMANTICS.name); + await page.fill( + '#semantics_0_description', + DATA_CONTRACT_CONTAIN_SEMANTICS.description + ); + const ruleLocator = page.locator('.group').nth(0); + await selectOption( + page, + ruleLocator.locator('.group--field .ant-select'), + DATA_CONTRACT_CONTAIN_SEMANTICS.rules[0].field, + true + ); + await selectOption( + page, + ruleLocator.locator('.rule--operator .ant-select'), + DATA_CONTRACT_CONTAIN_SEMANTICS.rules[0].operator + ); + await selectOption( + page, + ruleLocator.locator('.rule--value .ant-select'), + 'Tier.Tier1', + true + ); + await page.getByRole('button', { name: 'Add New Rule' }).click(); + + await expect(page.locator('.group--conjunctions')).toBeVisible(); + + const ruleLocator2 = page.locator('.rule').nth(1); + await selectOption( + page, + ruleLocator2.locator('.rule--field .ant-select'), + DATA_CONTRACT_CONTAIN_SEMANTICS.rules[1].field, + true + ); + await selectOption( + page, + ruleLocator2.locator('.rule--operator .ant-select'), + DATA_CONTRACT_CONTAIN_SEMANTICS.rules[1].operator + ); + + await selectOption( + page, + ruleLocator2.locator('.rule--value .ant-select'), + testTag.responseData.name, + true + ); + + await page.getByRole('button', { name: 'Add New Rule' }).click(); + + await expect(page.locator('.group--conjunctions')).toBeVisible(); + + const ruleLocator3 = page.locator('.rule').nth(2); + await selectOption( + page, + ruleLocator3.locator('.rule--field .ant-select'), + DATA_CONTRACT_CONTAIN_SEMANTICS.rules[2].field, + true + ); + await selectOption( + page, + ruleLocator3.locator('.rule--operator .ant-select'), + DATA_CONTRACT_CONTAIN_SEMANTICS.rules[2].operator + ); + + await selectOption( + page, + ruleLocator3.locator('.rule--value .ant-select'), + testGlossaryTerm.responseData.name, + true + ); + + await page.getByTestId('save-semantic-button').click(); + + await expect( + page + .getByTestId('contract-semantics-card-0') + .locator('.semantic-form-item-title') + ).toContainText(DATA_CONTRACT_CONTAIN_SEMANTICS.name); + await expect( + page + .getByTestId('contract-semantics-card-0') + .locator('.semantic-form-item-description') + ).toContainText(DATA_CONTRACT_CONTAIN_SEMANTICS.description); + + await page.locator('.expand-collapse-icon').click(); + + await expect(page.locator('.semantic-rule-editor-view-only')).toBeVisible(); + + // save and trigger contract validation + await saveAndTriggerDataContractValidation(page, true); + + await expect( + page.getByTestId('contract-card-title-container').filter({ + hasText: 'Contract Status', + }) + ).toBeVisible(); + await expect( + page.getByTestId('contract-status-card-item-Semantics-status') + ).toContainText('Failed'); + await expect( + page.getByTestId('data-contract-latest-result-btn') + ).toContainText('Contract Failed'); + + await page.getByTestId('schema').click(); + + // Add the data in the Table Entity which Semantic Required + await assignTier(page, 'Tier1', EntityTypeEndpoint.Table); + await assignTag( + page, + testTag.responseData.displayName, + 'Add', + EntityTypeEndpoint.Table, + 'KnowledgePanel.Tags', + testTag.responseData.fullyQualifiedName + ); + await assignGlossaryTerm(page, testGlossaryTerm.responseData); + + await page.click('[data-testid="contract"]'); + + const runNowResponse = page.waitForResponse( + '/api/v1/dataContracts/*/validate' + ); + + await page.getByTestId('contract-run-now-button').click(); + await runNowResponse; + + await toastNotification(page, 'Contract validation trigger successfully.'); + + await page.reload(); + + await page.waitForLoadState('networkidle'); + await page.waitForSelector('[data-testid="loader"]', { + state: 'detached', + }); + + await expect( + page.getByTestId('contract-status-card-item-Semantics-status') + ).toContainText('Passed'); + }); + test('Nested Column should not be selectable', async ({ page }) => { const entityFQN = table.entityResponseData.fullyQualifiedName; await redirectToHomePage(page); @@ -1133,6 +1298,8 @@ test.describe('Data Contracts', () => { await expect(page.getByTestId('add-contract-card')).toBeVisible(); + await page.getByTestId('contract-name').fill(DATA_CONTRACT_DETAILS.name); + await page.getByRole('button', { name: 'Schema' }).click(); await page.waitForSelector('[data-testid="loader"]', { diff --git a/openmetadata-ui/src/main/resources/ui/src/constants/DataContract.constants.ts b/openmetadata-ui/src/main/resources/ui/src/constants/DataContract.constants.ts index 973b7480ed8..1d6c5bec6f5 100644 --- a/openmetadata-ui/src/main/resources/ui/src/constants/DataContract.constants.ts +++ b/openmetadata-ui/src/main/resources/ui/src/constants/DataContract.constants.ts @@ -12,6 +12,7 @@ */ import { EntityReferenceFields } from '../enums/AdvancedSearch.enum'; +import jsonLogicSearchClassBase from '../utils/JSONLogicSearchClassBase'; export enum DataContractMode { YAML, @@ -41,3 +42,8 @@ export const DATA_ASSET_RULE_FIELDS_NOT_TO_RENDER = [ EntityReferenceFields.DISPLAY_NAME, EntityReferenceFields.DELETED, ]; + +export const SEMANTIC_OPERATORS = [ + ...(jsonLogicSearchClassBase.defaultSelectOperators ?? []), + 'array_contains', +]; 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 f887d6b3b6d..92320a8bcff 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 @@ -282,6 +282,7 @@ "constraint-plural": "Einschränkungen", "constraint-type": "Einschränkungstyp", "consumer-aligned": "Consumer-aligned", + "contain-plural": "Enthält", "container": "Container", "container-column": "Container Column", "container-plural": "Container", 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 ead98765f01..8ac111896bb 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 @@ -282,6 +282,7 @@ "constraint-plural": "Constraints", "constraint-type": "Constraint Type", "consumer-aligned": "Consumer-aligned", + "contain-plural": "Contains", "container": "Container", "container-column": "Container Column", "container-plural": "Containers", 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 4f148ef3d0b..3d2ae2becd5 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 @@ -282,6 +282,7 @@ "constraint-plural": "Restricciones", "constraint-type": "Tipo de restricción", "consumer-aligned": "Alineado con el Consumidor", + "contain-plural": "Contiene", "container": "Contenedor", "container-column": "Contenedor Columna", "container-plural": "Contenedores", @@ -2501,13 +2502,6 @@ "welcome-screen-message": "Descubre todos tus datos en un solo lugar y colabora sin problemas con tu equipo en datos en los que puedas confiar.", "welcome-to-om": "¡Bienvenido a OpenMetadata!", "welcome-to-open-metadata": "¡Bienvenido a OpenMetadata!", - "workflow-status-exception": "AutoPilot aplicación encontró una excepción.", - "workflow-status-failure": "AutoPilot aplicación falló.", - "workflow-status-failure-description": "Por favor, revise los registros de ejecución de la aplicación para obtener más detalles. También puede volver a activar la aplicación desde la página de la aplicación.", - "workflow-status-finished": "AutoPilot aplicación completada correctamente.", - "workflow-status-finished-description": "La aplicación ha finalizado la ejecución correctamente. Ahora puede ver los insights para su servicio.", - "workflow-status-running": "AutoPilot aplicación está activada y en ejecución.", - "workflow-status-running-description": "La aplicación está actualmente en ejecución. Por favor, espere mientras obtenemos los insights para su servicio.", "would-like-to-start-adding-some": "¿Te gustaría comenzar a agregar algunos?", "write-your-announcement-lowercase": "escribe tu anuncio", "write-your-description": "Escribe tu descripción", 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 890110baf30..99bf933fa9f 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 @@ -282,6 +282,7 @@ "constraint-plural": "Constraints", "constraint-type": "Type de contrainte", "consumer-aligned": "Consumer-aligned", + "contain-plural": "Contient", "container": "Conteneur", "container-column": "Container Column", "container-plural": "Conteneurs", 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 bf4d5ef8349..0b8e03801b7 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 @@ -282,6 +282,7 @@ "constraint-plural": "Restrición", "constraint-type": "Tipo de Restrición", "consumer-aligned": "Aliñado co consumidor", + "contain-plural": "Contains", "container": "Contedor", "container-column": "Columna do contedor", "container-plural": "Contedores", 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 907025c8951..ba94ccea28f 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 @@ -282,6 +282,7 @@ "constraint-plural": "Constraints", "constraint-type": "סוג מגבלה", "consumer-aligned": "מכוון לצרכן", + "contain-plural": "Contains", "container": "אחסון", "container-column": "Container Column", "container-plural": "אחסון (Storage)", @@ -2037,7 +2038,7 @@ "error-self-signup-disabled": "Self-signup is currently disabled. To proceed, please reach out to your administrator for further assistance or to request access.", "error-team-transfer-message": "You cannot move to this team as Team Type {{dragTeam}} can't be {{dropTeam}} children", "error-while-fetching-access-token": "שגיאה בעת קבלת טוקן גישה.", - "expected-schema-structure-of-this-asset": "מבנה סכמה צפוי של נכס זה", + "expected-schema-structure-of-this-asset": "מבנה סchema צפוי של נכס זה", "explore-our-guide-here": "explore our guide here.", "export-entity-help": "הורד את כל הישויות שלך בפורמט קובץ CSV ושתף עם הצוות שלך.", "external-destination-selection": "Only external destinations can be tested.", @@ -2347,8 +2348,8 @@ "redirect-message": "אנא המתן בזמן שאתה מועבר.", "redirecting-to-home-page": "מועבר לדף הבית", "refer-to-our-doc": "זקוק.ה לעזרה נוספת? בקר.י ב-<0>{{doc}} לקבלת מידע נוסף.", - "remove-default-persona-confirmation": "האם אתה בטוח שברצונך להסיר את {{persona}} כפרסונה ברירת מחדל? לא תוגדר פרסונה ברירת מחדל למשתמשים.", - "remove-default-persona-description": "לא תוגדר פרסונה ברירת מחדל למשתמשים", + "remove-default-persona-confirmation": "האם אתה בטוח שברצונך להסיר את {{persona}} כפרסונה ברירת מחדל? לא תוגדר פרסונה ברירת המחדל למשתמשים.", + "remove-default-persona-description": "לא תוגדר פרסונה ברירת המחדל למשתמשים", "remove-edge-between-source-and-target": "האם אתה בטוח שברצונך להסיר את הקשת בין \"{{sourceDisplayName}} ו-{{targetDisplayName}}\"?", "remove-lineage-edge": "הסר קישוריות", "rename-entity": "שנה את השם והשם לתצוגה של {{entity}}.", 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 43adc74faf9..118ad360b5d 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 @@ -282,6 +282,7 @@ "constraint-plural": "制約", "constraint-type": "制約タイプ", "consumer-aligned": "消費者に合わせる", + "contain-plural": "含む", "container": "コンテナ", "container-column": "コンテナカラム", "container-plural": "コンテナ", diff --git a/openmetadata-ui/src/main/resources/ui/src/locale/languages/ko-kr.json b/openmetadata-ui/src/main/resources/ui/src/locale/languages/ko-kr.json index c45de4b7e4c..f9750284bb2 100644 --- a/openmetadata-ui/src/main/resources/ui/src/locale/languages/ko-kr.json +++ b/openmetadata-ui/src/main/resources/ui/src/locale/languages/ko-kr.json @@ -282,6 +282,7 @@ "constraint-plural": "제약 조건", "constraint-type": "제약 조건 유형", "consumer-aligned": "소비자 중심", + "contain-plural": "포함", "container": "컨테이너", "container-column": "컨테이너 열", "container-plural": "컨테이너들", 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 f4a38afe855..29505d9c80b 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 @@ -282,6 +282,7 @@ "constraint-plural": "Constraints", "constraint-type": "बंधन प्रकार", "consumer-aligned": "ग्राहक-संरेखित", + "contain-plural": "अंतर्भूत आहे", "container": "कंटेनर", "container-column": "कंटेनर स्तंभ", "container-plural": "कंटेनर", 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 8b5f5ec97d9..7388f248a25 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 @@ -282,6 +282,7 @@ "constraint-plural": "Constraints", "constraint-type": "Beperkings type", "consumer-aligned": "Consumentafgestemd", + "contain-plural": "Bevat", "container": "Container", "container-column": "Container Column", "container-plural": "Containers", 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 7a652c003b0..63217cf7959 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 @@ -282,6 +282,7 @@ "constraint-plural": "Constraints", "constraint-type": "نوع محدودیت", "consumer-aligned": "هماهنگ با مصرف‌کننده", + "contain-plural": "Contém", "container": "ظرف", "container-column": "ستون ظرف", "container-plural": "ظرف‌ها", 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 3ce9ec6aa61..adf3bf55b51 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 @@ -282,6 +282,7 @@ "constraint-plural": "Restrições", "constraint-type": "Tipo de restrição", "consumer-aligned": "Alinhado ao Consumidor", + "contain-plural": "Contém", "container": "Contêiner", "container-column": "Coluna de contêiner", "container-plural": "Contêineres", 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 51f2d89fdf9..6f10a27472e 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 @@ -282,6 +282,7 @@ "constraint-plural": "Constraints", "constraint-type": "Tipo de restrição", "consumer-aligned": "Alinhado ao Consumidor", + "contain-plural": "Contém", "container": "Contentor", "container-column": "Container Column", "container-plural": "Contentores", 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 0d876ea9057..834a3e667e9 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 @@ -282,6 +282,7 @@ "constraint-plural": "Ограничения", "constraint-type": "Тип ограничения", "consumer-aligned": "Ориентированный на потребителя", + "contain-plural": "Содержит", "container": "Контейнер", "container-column": "Container Column", "container-plural": "Контейнеры", 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 46ec5885fa4..a4566e80bac 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 @@ -282,6 +282,7 @@ "constraint-plural": "ข้อจำกัดหลายรายการ", "constraint-type": "ประเภทข้อจำกัด", "consumer-aligned": "ตรงตามความต้องการของผู้ใช้", + "contain-plural": "มีอยู่", "container": "คอนเทนเนอร์", "container-column": "คอลัมน์คอนเทนเนอร์", "container-plural": "คอนเทนเนอร์", diff --git a/openmetadata-ui/src/main/resources/ui/src/locale/languages/tr-tr.json b/openmetadata-ui/src/main/resources/ui/src/locale/languages/tr-tr.json index efc83dd5a42..9dbd5ab6078 100644 --- a/openmetadata-ui/src/main/resources/ui/src/locale/languages/tr-tr.json +++ b/openmetadata-ui/src/main/resources/ui/src/locale/languages/tr-tr.json @@ -282,6 +282,7 @@ "constraint-plural": "Kısıtlamalar", "constraint-type": "Kısıtlama Türü", "consumer-aligned": "Tüketici Odaklı", + "contain-plural": "İçerir", "container": "Konteyner", "container-column": "Konteyner Sütunu", "container-plural": "Konteynerler", 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 54bb7ab16dd..2b7f11bbf25 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 @@ -282,6 +282,7 @@ "constraint-plural": "Constraints", "constraint-type": "约束类型", "consumer-aligned": "使用者对齐", + "contain-plural": "包含", "container": "存储容器", "container-column": "存储容器列", "container-plural": "存储容器", diff --git a/openmetadata-ui/src/main/resources/ui/src/locale/languages/zh-tw.json b/openmetadata-ui/src/main/resources/ui/src/locale/languages/zh-tw.json index e7f17dc2332..c265edc4a3e 100644 --- a/openmetadata-ui/src/main/resources/ui/src/locale/languages/zh-tw.json +++ b/openmetadata-ui/src/main/resources/ui/src/locale/languages/zh-tw.json @@ -282,6 +282,7 @@ "constraint-plural": "約束", "constraint-type": "約束類型", "consumer-aligned": "消費者導向", + "contain-plural": "包含", "container": "容器", "container-column": "容器欄位", "container-plural": "容器", diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/DataContract/DataContractUtils.ts b/openmetadata-ui/src/main/resources/ui/src/utils/DataContract/DataContractUtils.ts index d3bbe36684e..f4cd0040961 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/DataContract/DataContractUtils.ts +++ b/openmetadata-ui/src/main/resources/ui/src/utils/DataContract/DataContractUtils.ts @@ -26,6 +26,7 @@ import { RED_3, YELLOW_2, } from '../../constants/Color.constants'; +import { SEMANTIC_OPERATORS } from '../../constants/DataContract.constants'; import { EntityReferenceFields } from '../../enums/AdvancedSearch.enum'; import { SearchIndex } from '../../enums/search.enum'; import { TestCaseType } from '../../enums/TestSuite.enum'; @@ -239,7 +240,7 @@ export const getSematicRuleFields = () => { label: 'Tags', type: 'select', mainWidgetProps: jsonLogicSearchClassBase.mainWidgetProps, - operators: jsonLogicSearchClassBase.defaultSelectOperators, + operators: SEMANTIC_OPERATORS, fieldSettings: { asyncFetch: jsonLogicSearchClassBase.searchAutocomplete({ searchIndex: SearchIndex.TAG, @@ -265,7 +266,7 @@ export const getSematicRuleFields = () => { label: 'Tags', type: 'select', mainWidgetProps: jsonLogicSearchClassBase.mainWidgetProps, - operators: jsonLogicSearchClassBase.defaultSelectOperators, + operators: SEMANTIC_OPERATORS, fieldSettings: { asyncFetch: jsonLogicSearchClassBase.searchAutocomplete({ searchIndex: SearchIndex.GLOSSARY_TERM, @@ -280,13 +281,22 @@ export const getSematicRuleFields = () => { const tierField = { label: t('label.tier'), - type: 'select', + type: '!group', + mode: 'some', fieldName: 'tags', - mainWidgetProps: jsonLogicSearchClassBase.mainWidgetProps, - operators: jsonLogicSearchClassBase.defaultSelectOperators, - fieldSettings: { - asyncFetch: jsonLogicSearchClassBase.autoCompleteTier, - useAsyncSearch: true, + defaultField: 'tagFQN', + subfields: { + tagFQN: { + label: 'Tags', + type: 'multiselect', + mainWidgetProps: jsonLogicSearchClassBase.mainWidgetProps, + operators: SEMANTIC_OPERATORS, + fieldSettings: { + asyncFetch: jsonLogicSearchClassBase.autoCompleteTier, + useAsyncSearch: true, + listValues: jsonLogicSearchClassBase.autoCompleteTier, + }, + }, }, }; diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/JSONLogicSearchClassBase.ts b/openmetadata-ui/src/main/resources/ui/src/utils/JSONLogicSearchClassBase.ts index 9d35560f5df..3aaa339da98 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/JSONLogicSearchClassBase.ts +++ b/openmetadata-ui/src/main/resources/ui/src/utils/JSONLogicSearchClassBase.ts @@ -55,6 +55,13 @@ class JSONLogicSearchClassBase { text: { operators: ['like', 'not_like', 'regexp'], }, + multiselect: { + operators: [ + ...(this.baseConfig.types.multiselect?.widgets?.multiselect + ?.operators || []), + 'array_contains', + ], + }, }, // Limits source to user input values, not other fields valueSources: ['value'], @@ -66,6 +73,12 @@ class JSONLogicSearchClassBase { text: { operators: ['like', 'not_like', 'regexp'], }, + select: { + operators: [ + ...(this.baseConfig.types.select?.widgets?.select?.operators || []), + 'array_contains', + ], + }, }, valueSources: ['value'], }, @@ -100,7 +113,7 @@ class JSONLogicSearchClassBase { ...this.baseConfig.widgets.text, }, }; - configOperators = { + configOperators: Config['operators'] = { ...this.baseConfig.operators, like: { ...this.baseConfig.operators.like, @@ -141,7 +154,6 @@ class JSONLogicSearchClassBase { label: t('label.is-entity', { entity: t('label.reviewer') }), labelForFormat: t('label.is-entity', { entity: t('label.reviewer') }), cardinality: 0, - unary: true, jsonLogic: 'isReviewer', sqlOp: 'IS REVIEWER', }, @@ -149,10 +161,17 @@ class JSONLogicSearchClassBase { label: t('label.is-entity', { entity: t('label.owner') }), labelForFormat: t('label.is-entity', { entity: t('label.owner') }), cardinality: 0, - unary: true, jsonLogic: 'isOwner', sqlOp: 'IS OWNER', }, + array_contains: { + label: t('label.contain-plural'), + labelForFormat: t('label.contain-plural'), + valueTypes: ['multiselect', 'select'], + cardinality: 1, + valueSources: ['value'], + jsonLogic: 'contains', + }, }; mapFields: Record;