mirror of
https://github.com/open-metadata/OpenMetadata.git
synced 2025-11-03 12:08:31 +00:00
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 <peremiquelbrull@gmail.com> Co-authored-by: Ashish Gupta <ashish@getcollate.io>
This commit is contained in:
parent
4483e183cb
commit
5b7c569ebf
@ -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() {
|
||||
|
||||
@ -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
|
||||
*/
|
||||
|
||||
@ -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',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
@ -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"]', {
|
||||
|
||||
@ -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',
|
||||
];
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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}}</0> לקבלת מידע נוסף.",
|
||||
"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}}.",
|
||||
|
||||
@ -282,6 +282,7 @@
|
||||
"constraint-plural": "制約",
|
||||
"constraint-type": "制約タイプ",
|
||||
"consumer-aligned": "消費者に合わせる",
|
||||
"contain-plural": "含む",
|
||||
"container": "コンテナ",
|
||||
"container-column": "コンテナカラム",
|
||||
"container-plural": "コンテナ",
|
||||
|
||||
@ -282,6 +282,7 @@
|
||||
"constraint-plural": "제약 조건",
|
||||
"constraint-type": "제약 조건 유형",
|
||||
"consumer-aligned": "소비자 중심",
|
||||
"contain-plural": "포함",
|
||||
"container": "컨테이너",
|
||||
"container-column": "컨테이너 열",
|
||||
"container-plural": "컨테이너들",
|
||||
|
||||
@ -282,6 +282,7 @@
|
||||
"constraint-plural": "Constraints",
|
||||
"constraint-type": "बंधन प्रकार",
|
||||
"consumer-aligned": "ग्राहक-संरेखित",
|
||||
"contain-plural": "अंतर्भूत आहे",
|
||||
"container": "कंटेनर",
|
||||
"container-column": "कंटेनर स्तंभ",
|
||||
"container-plural": "कंटेनर",
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -282,6 +282,7 @@
|
||||
"constraint-plural": "Constraints",
|
||||
"constraint-type": "نوع محدودیت",
|
||||
"consumer-aligned": "هماهنگ با مصرفکننده",
|
||||
"contain-plural": "Contém",
|
||||
"container": "ظرف",
|
||||
"container-column": "ستون ظرف",
|
||||
"container-plural": "ظرفها",
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -282,6 +282,7 @@
|
||||
"constraint-plural": "Ограничения",
|
||||
"constraint-type": "Тип ограничения",
|
||||
"consumer-aligned": "Ориентированный на потребителя",
|
||||
"contain-plural": "Содержит",
|
||||
"container": "Контейнер",
|
||||
"container-column": "Container Column",
|
||||
"container-plural": "Контейнеры",
|
||||
|
||||
@ -282,6 +282,7 @@
|
||||
"constraint-plural": "ข้อจำกัดหลายรายการ",
|
||||
"constraint-type": "ประเภทข้อจำกัด",
|
||||
"consumer-aligned": "ตรงตามความต้องการของผู้ใช้",
|
||||
"contain-plural": "มีอยู่",
|
||||
"container": "คอนเทนเนอร์",
|
||||
"container-column": "คอลัมน์คอนเทนเนอร์",
|
||||
"container-plural": "คอนเทนเนอร์",
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -282,6 +282,7 @@
|
||||
"constraint-plural": "Constraints",
|
||||
"constraint-type": "约束类型",
|
||||
"consumer-aligned": "使用者对齐",
|
||||
"contain-plural": "包含",
|
||||
"container": "存储容器",
|
||||
"container-column": "存储容器列",
|
||||
"container-plural": "存储容器",
|
||||
|
||||
@ -282,6 +282,7 @@
|
||||
"constraint-plural": "約束",
|
||||
"constraint-type": "約束類型",
|
||||
"consumer-aligned": "消費者導向",
|
||||
"contain-plural": "包含",
|
||||
"container": "容器",
|
||||
"container-column": "容器欄位",
|
||||
"container-plural": "容器",
|
||||
|
||||
@ -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,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@ -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<string, FieldOrGroup>;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user