mirror of
https://github.com/open-metadata/OpenMetadata.git
synced 2025-11-23 14:13:39 +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 {
|
public enum CustomLogicOps {
|
||||||
LENGTH("length"),
|
LENGTH("length"),
|
||||||
|
CONTAINS("contains"),
|
||||||
IS_REVIEWER("isReviewer"),
|
IS_REVIEWER("isReviewer"),
|
||||||
IS_OWNER("isOwner");
|
IS_OWNER("isOwner");
|
||||||
|
|
||||||
@ -55,6 +56,31 @@ public class LogicOps {
|
|||||||
return args.length;
|
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"} }
|
// {"isReviewer": { var: "updatedBy"} }
|
||||||
jsonLogic.addOperation(
|
jsonLogic.addOperation(
|
||||||
new JsonLogicExpression() {
|
new JsonLogicExpression() {
|
||||||
|
|||||||
@ -476,6 +476,108 @@ public class RuleEngineTests extends OpenMetadataApplicationTest {
|
|||||||
RuleEngine.getInstance().evaluate(table, List.of(piiRule), false, false);
|
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
|
* Helper method to create a real Domain entity for testing
|
||||||
*/
|
*/
|
||||||
|
|||||||
@ -52,3 +52,22 @@ export const NEW_TABLE_TEST_CASE = {
|
|||||||
value: '1000',
|
value: '1000',
|
||||||
description: 'New table test case for TableColumnCountToEqual',
|
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 { test as base, expect, Page } from '@playwright/test';
|
||||||
import {
|
import {
|
||||||
|
DATA_CONTRACT_CONTAIN_SEMANTICS,
|
||||||
DATA_CONTRACT_DETAILS,
|
DATA_CONTRACT_DETAILS,
|
||||||
DATA_CONTRACT_SEMANTICS1,
|
DATA_CONTRACT_SEMANTICS1,
|
||||||
DATA_CONTRACT_SEMANTICS2,
|
DATA_CONTRACT_SEMANTICS2,
|
||||||
@ -39,7 +40,13 @@ import {
|
|||||||
validateDataContractInsideBundleTestSuites,
|
validateDataContractInsideBundleTestSuites,
|
||||||
waitForDataContractExecution,
|
waitForDataContractExecution,
|
||||||
} from '../../utils/dataContracts';
|
} from '../../utils/dataContracts';
|
||||||
import { addOwner, addOwnerWithoutValidation } from '../../utils/entity';
|
import {
|
||||||
|
addOwner,
|
||||||
|
addOwnerWithoutValidation,
|
||||||
|
assignGlossaryTerm,
|
||||||
|
assignTag,
|
||||||
|
assignTier,
|
||||||
|
} from '../../utils/entity';
|
||||||
import { settingClick } from '../../utils/sidebar';
|
import { settingClick } from '../../utils/sidebar';
|
||||||
|
|
||||||
const adminUser = new UserClass();
|
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 }) => {
|
test('Nested Column should not be selectable', async ({ page }) => {
|
||||||
const entityFQN = table.entityResponseData.fullyQualifiedName;
|
const entityFQN = table.entityResponseData.fullyQualifiedName;
|
||||||
await redirectToHomePage(page);
|
await redirectToHomePage(page);
|
||||||
@ -1133,6 +1298,8 @@ test.describe('Data Contracts', () => {
|
|||||||
|
|
||||||
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('button', { name: 'Schema' }).click();
|
await page.getByRole('button', { name: 'Schema' }).click();
|
||||||
|
|
||||||
await page.waitForSelector('[data-testid="loader"]', {
|
await page.waitForSelector('[data-testid="loader"]', {
|
||||||
|
|||||||
@ -12,6 +12,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { EntityReferenceFields } from '../enums/AdvancedSearch.enum';
|
import { EntityReferenceFields } from '../enums/AdvancedSearch.enum';
|
||||||
|
import jsonLogicSearchClassBase from '../utils/JSONLogicSearchClassBase';
|
||||||
|
|
||||||
export enum DataContractMode {
|
export enum DataContractMode {
|
||||||
YAML,
|
YAML,
|
||||||
@ -41,3 +42,8 @@ export const DATA_ASSET_RULE_FIELDS_NOT_TO_RENDER = [
|
|||||||
EntityReferenceFields.DISPLAY_NAME,
|
EntityReferenceFields.DISPLAY_NAME,
|
||||||
EntityReferenceFields.DELETED,
|
EntityReferenceFields.DELETED,
|
||||||
];
|
];
|
||||||
|
|
||||||
|
export const SEMANTIC_OPERATORS = [
|
||||||
|
...(jsonLogicSearchClassBase.defaultSelectOperators ?? []),
|
||||||
|
'array_contains',
|
||||||
|
];
|
||||||
|
|||||||
@ -282,6 +282,7 @@
|
|||||||
"constraint-plural": "Einschränkungen",
|
"constraint-plural": "Einschränkungen",
|
||||||
"constraint-type": "Einschränkungstyp",
|
"constraint-type": "Einschränkungstyp",
|
||||||
"consumer-aligned": "Consumer-aligned",
|
"consumer-aligned": "Consumer-aligned",
|
||||||
|
"contain-plural": "Enthält",
|
||||||
"container": "Container",
|
"container": "Container",
|
||||||
"container-column": "Container Column",
|
"container-column": "Container Column",
|
||||||
"container-plural": "Container",
|
"container-plural": "Container",
|
||||||
|
|||||||
@ -282,6 +282,7 @@
|
|||||||
"constraint-plural": "Constraints",
|
"constraint-plural": "Constraints",
|
||||||
"constraint-type": "Constraint Type",
|
"constraint-type": "Constraint Type",
|
||||||
"consumer-aligned": "Consumer-aligned",
|
"consumer-aligned": "Consumer-aligned",
|
||||||
|
"contain-plural": "Contains",
|
||||||
"container": "Container",
|
"container": "Container",
|
||||||
"container-column": "Container Column",
|
"container-column": "Container Column",
|
||||||
"container-plural": "Containers",
|
"container-plural": "Containers",
|
||||||
|
|||||||
@ -282,6 +282,7 @@
|
|||||||
"constraint-plural": "Restricciones",
|
"constraint-plural": "Restricciones",
|
||||||
"constraint-type": "Tipo de restricción",
|
"constraint-type": "Tipo de restricción",
|
||||||
"consumer-aligned": "Alineado con el Consumidor",
|
"consumer-aligned": "Alineado con el Consumidor",
|
||||||
|
"contain-plural": "Contiene",
|
||||||
"container": "Contenedor",
|
"container": "Contenedor",
|
||||||
"container-column": "Contenedor Columna",
|
"container-column": "Contenedor Columna",
|
||||||
"container-plural": "Contenedores",
|
"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-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-om": "¡Bienvenido a OpenMetadata!",
|
||||||
"welcome-to-open-metadata": "¡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?",
|
"would-like-to-start-adding-some": "¿Te gustaría comenzar a agregar algunos?",
|
||||||
"write-your-announcement-lowercase": "escribe tu anuncio",
|
"write-your-announcement-lowercase": "escribe tu anuncio",
|
||||||
"write-your-description": "Escribe tu descripción",
|
"write-your-description": "Escribe tu descripción",
|
||||||
|
|||||||
@ -282,6 +282,7 @@
|
|||||||
"constraint-plural": "Constraints",
|
"constraint-plural": "Constraints",
|
||||||
"constraint-type": "Type de contrainte",
|
"constraint-type": "Type de contrainte",
|
||||||
"consumer-aligned": "Consumer-aligned",
|
"consumer-aligned": "Consumer-aligned",
|
||||||
|
"contain-plural": "Contient",
|
||||||
"container": "Conteneur",
|
"container": "Conteneur",
|
||||||
"container-column": "Container Column",
|
"container-column": "Container Column",
|
||||||
"container-plural": "Conteneurs",
|
"container-plural": "Conteneurs",
|
||||||
|
|||||||
@ -282,6 +282,7 @@
|
|||||||
"constraint-plural": "Restrición",
|
"constraint-plural": "Restrición",
|
||||||
"constraint-type": "Tipo de Restrición",
|
"constraint-type": "Tipo de Restrición",
|
||||||
"consumer-aligned": "Aliñado co consumidor",
|
"consumer-aligned": "Aliñado co consumidor",
|
||||||
|
"contain-plural": "Contains",
|
||||||
"container": "Contedor",
|
"container": "Contedor",
|
||||||
"container-column": "Columna do contedor",
|
"container-column": "Columna do contedor",
|
||||||
"container-plural": "Contedores",
|
"container-plural": "Contedores",
|
||||||
|
|||||||
@ -282,6 +282,7 @@
|
|||||||
"constraint-plural": "Constraints",
|
"constraint-plural": "Constraints",
|
||||||
"constraint-type": "סוג מגבלה",
|
"constraint-type": "סוג מגבלה",
|
||||||
"consumer-aligned": "מכוון לצרכן",
|
"consumer-aligned": "מכוון לצרכן",
|
||||||
|
"contain-plural": "Contains",
|
||||||
"container": "אחסון",
|
"container": "אחסון",
|
||||||
"container-column": "Container Column",
|
"container-column": "Container Column",
|
||||||
"container-plural": "אחסון (Storage)",
|
"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-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-team-transfer-message": "You cannot move to this team as Team Type {{dragTeam}} can't be {{dropTeam}} children",
|
||||||
"error-while-fetching-access-token": "שגיאה בעת קבלת טוקן גישה.",
|
"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.",
|
"explore-our-guide-here": "explore our guide here.",
|
||||||
"export-entity-help": "הורד את כל הישויות שלך בפורמט קובץ CSV ושתף עם הצוות שלך.",
|
"export-entity-help": "הורד את כל הישויות שלך בפורמט קובץ CSV ושתף עם הצוות שלך.",
|
||||||
"external-destination-selection": "Only external destinations can be tested.",
|
"external-destination-selection": "Only external destinations can be tested.",
|
||||||
@ -2347,8 +2348,8 @@
|
|||||||
"redirect-message": "אנא המתן בזמן שאתה מועבר.",
|
"redirect-message": "אנא המתן בזמן שאתה מועבר.",
|
||||||
"redirecting-to-home-page": "מועבר לדף הבית",
|
"redirecting-to-home-page": "מועבר לדף הבית",
|
||||||
"refer-to-our-doc": "זקוק.ה לעזרה נוספת? בקר.י ב-<0>{{doc}}</0> לקבלת מידע נוסף.",
|
"refer-to-our-doc": "זקוק.ה לעזרה נוספת? בקר.י ב-<0>{{doc}}</0> לקבלת מידע נוסף.",
|
||||||
"remove-default-persona-confirmation": "האם אתה בטוח שברצונך להסיר את {{persona}} כפרסונה ברירת מחדל? לא תוגדר פרסונה ברירת מחדל למשתמשים.",
|
"remove-default-persona-confirmation": "האם אתה בטוח שברצונך להסיר את {{persona}} כפרסונה ברירת מחדל? לא תוגדר פרסונה ברירת המחדל למשתמשים.",
|
||||||
"remove-default-persona-description": "לא תוגדר פרסונה ברירת מחדל למשתמשים",
|
"remove-default-persona-description": "לא תוגדר פרסונה ברירת המחדל למשתמשים",
|
||||||
"remove-edge-between-source-and-target": "האם אתה בטוח שברצונך להסיר את הקשת בין \"{{sourceDisplayName}} ו-{{targetDisplayName}}\"?",
|
"remove-edge-between-source-and-target": "האם אתה בטוח שברצונך להסיר את הקשת בין \"{{sourceDisplayName}} ו-{{targetDisplayName}}\"?",
|
||||||
"remove-lineage-edge": "הסר קישוריות",
|
"remove-lineage-edge": "הסר קישוריות",
|
||||||
"rename-entity": "שנה את השם והשם לתצוגה של {{entity}}.",
|
"rename-entity": "שנה את השם והשם לתצוגה של {{entity}}.",
|
||||||
|
|||||||
@ -282,6 +282,7 @@
|
|||||||
"constraint-plural": "制約",
|
"constraint-plural": "制約",
|
||||||
"constraint-type": "制約タイプ",
|
"constraint-type": "制約タイプ",
|
||||||
"consumer-aligned": "消費者に合わせる",
|
"consumer-aligned": "消費者に合わせる",
|
||||||
|
"contain-plural": "含む",
|
||||||
"container": "コンテナ",
|
"container": "コンテナ",
|
||||||
"container-column": "コンテナカラム",
|
"container-column": "コンテナカラム",
|
||||||
"container-plural": "コンテナ",
|
"container-plural": "コンテナ",
|
||||||
|
|||||||
@ -282,6 +282,7 @@
|
|||||||
"constraint-plural": "제약 조건",
|
"constraint-plural": "제약 조건",
|
||||||
"constraint-type": "제약 조건 유형",
|
"constraint-type": "제약 조건 유형",
|
||||||
"consumer-aligned": "소비자 중심",
|
"consumer-aligned": "소비자 중심",
|
||||||
|
"contain-plural": "포함",
|
||||||
"container": "컨테이너",
|
"container": "컨테이너",
|
||||||
"container-column": "컨테이너 열",
|
"container-column": "컨테이너 열",
|
||||||
"container-plural": "컨테이너들",
|
"container-plural": "컨테이너들",
|
||||||
|
|||||||
@ -282,6 +282,7 @@
|
|||||||
"constraint-plural": "Constraints",
|
"constraint-plural": "Constraints",
|
||||||
"constraint-type": "बंधन प्रकार",
|
"constraint-type": "बंधन प्रकार",
|
||||||
"consumer-aligned": "ग्राहक-संरेखित",
|
"consumer-aligned": "ग्राहक-संरेखित",
|
||||||
|
"contain-plural": "अंतर्भूत आहे",
|
||||||
"container": "कंटेनर",
|
"container": "कंटेनर",
|
||||||
"container-column": "कंटेनर स्तंभ",
|
"container-column": "कंटेनर स्तंभ",
|
||||||
"container-plural": "कंटेनर",
|
"container-plural": "कंटेनर",
|
||||||
|
|||||||
@ -282,6 +282,7 @@
|
|||||||
"constraint-plural": "Constraints",
|
"constraint-plural": "Constraints",
|
||||||
"constraint-type": "Beperkings type",
|
"constraint-type": "Beperkings type",
|
||||||
"consumer-aligned": "Consumentafgestemd",
|
"consumer-aligned": "Consumentafgestemd",
|
||||||
|
"contain-plural": "Bevat",
|
||||||
"container": "Container",
|
"container": "Container",
|
||||||
"container-column": "Container Column",
|
"container-column": "Container Column",
|
||||||
"container-plural": "Containers",
|
"container-plural": "Containers",
|
||||||
|
|||||||
@ -282,6 +282,7 @@
|
|||||||
"constraint-plural": "Constraints",
|
"constraint-plural": "Constraints",
|
||||||
"constraint-type": "نوع محدودیت",
|
"constraint-type": "نوع محدودیت",
|
||||||
"consumer-aligned": "هماهنگ با مصرفکننده",
|
"consumer-aligned": "هماهنگ با مصرفکننده",
|
||||||
|
"contain-plural": "Contém",
|
||||||
"container": "ظرف",
|
"container": "ظرف",
|
||||||
"container-column": "ستون ظرف",
|
"container-column": "ستون ظرف",
|
||||||
"container-plural": "ظرفها",
|
"container-plural": "ظرفها",
|
||||||
|
|||||||
@ -282,6 +282,7 @@
|
|||||||
"constraint-plural": "Restrições",
|
"constraint-plural": "Restrições",
|
||||||
"constraint-type": "Tipo de restrição",
|
"constraint-type": "Tipo de restrição",
|
||||||
"consumer-aligned": "Alinhado ao Consumidor",
|
"consumer-aligned": "Alinhado ao Consumidor",
|
||||||
|
"contain-plural": "Contém",
|
||||||
"container": "Contêiner",
|
"container": "Contêiner",
|
||||||
"container-column": "Coluna de contêiner",
|
"container-column": "Coluna de contêiner",
|
||||||
"container-plural": "Contêineres",
|
"container-plural": "Contêineres",
|
||||||
|
|||||||
@ -282,6 +282,7 @@
|
|||||||
"constraint-plural": "Constraints",
|
"constraint-plural": "Constraints",
|
||||||
"constraint-type": "Tipo de restrição",
|
"constraint-type": "Tipo de restrição",
|
||||||
"consumer-aligned": "Alinhado ao Consumidor",
|
"consumer-aligned": "Alinhado ao Consumidor",
|
||||||
|
"contain-plural": "Contém",
|
||||||
"container": "Contentor",
|
"container": "Contentor",
|
||||||
"container-column": "Container Column",
|
"container-column": "Container Column",
|
||||||
"container-plural": "Contentores",
|
"container-plural": "Contentores",
|
||||||
|
|||||||
@ -282,6 +282,7 @@
|
|||||||
"constraint-plural": "Ограничения",
|
"constraint-plural": "Ограничения",
|
||||||
"constraint-type": "Тип ограничения",
|
"constraint-type": "Тип ограничения",
|
||||||
"consumer-aligned": "Ориентированный на потребителя",
|
"consumer-aligned": "Ориентированный на потребителя",
|
||||||
|
"contain-plural": "Содержит",
|
||||||
"container": "Контейнер",
|
"container": "Контейнер",
|
||||||
"container-column": "Container Column",
|
"container-column": "Container Column",
|
||||||
"container-plural": "Контейнеры",
|
"container-plural": "Контейнеры",
|
||||||
|
|||||||
@ -282,6 +282,7 @@
|
|||||||
"constraint-plural": "ข้อจำกัดหลายรายการ",
|
"constraint-plural": "ข้อจำกัดหลายรายการ",
|
||||||
"constraint-type": "ประเภทข้อจำกัด",
|
"constraint-type": "ประเภทข้อจำกัด",
|
||||||
"consumer-aligned": "ตรงตามความต้องการของผู้ใช้",
|
"consumer-aligned": "ตรงตามความต้องการของผู้ใช้",
|
||||||
|
"contain-plural": "มีอยู่",
|
||||||
"container": "คอนเทนเนอร์",
|
"container": "คอนเทนเนอร์",
|
||||||
"container-column": "คอลัมน์คอนเทนเนอร์",
|
"container-column": "คอลัมน์คอนเทนเนอร์",
|
||||||
"container-plural": "คอนเทนเนอร์",
|
"container-plural": "คอนเทนเนอร์",
|
||||||
|
|||||||
@ -282,6 +282,7 @@
|
|||||||
"constraint-plural": "Kısıtlamalar",
|
"constraint-plural": "Kısıtlamalar",
|
||||||
"constraint-type": "Kısıtlama Türü",
|
"constraint-type": "Kısıtlama Türü",
|
||||||
"consumer-aligned": "Tüketici Odaklı",
|
"consumer-aligned": "Tüketici Odaklı",
|
||||||
|
"contain-plural": "İçerir",
|
||||||
"container": "Konteyner",
|
"container": "Konteyner",
|
||||||
"container-column": "Konteyner Sütunu",
|
"container-column": "Konteyner Sütunu",
|
||||||
"container-plural": "Konteynerler",
|
"container-plural": "Konteynerler",
|
||||||
|
|||||||
@ -282,6 +282,7 @@
|
|||||||
"constraint-plural": "Constraints",
|
"constraint-plural": "Constraints",
|
||||||
"constraint-type": "约束类型",
|
"constraint-type": "约束类型",
|
||||||
"consumer-aligned": "使用者对齐",
|
"consumer-aligned": "使用者对齐",
|
||||||
|
"contain-plural": "包含",
|
||||||
"container": "存储容器",
|
"container": "存储容器",
|
||||||
"container-column": "存储容器列",
|
"container-column": "存储容器列",
|
||||||
"container-plural": "存储容器",
|
"container-plural": "存储容器",
|
||||||
|
|||||||
@ -282,6 +282,7 @@
|
|||||||
"constraint-plural": "約束",
|
"constraint-plural": "約束",
|
||||||
"constraint-type": "約束類型",
|
"constraint-type": "約束類型",
|
||||||
"consumer-aligned": "消費者導向",
|
"consumer-aligned": "消費者導向",
|
||||||
|
"contain-plural": "包含",
|
||||||
"container": "容器",
|
"container": "容器",
|
||||||
"container-column": "容器欄位",
|
"container-column": "容器欄位",
|
||||||
"container-plural": "容器",
|
"container-plural": "容器",
|
||||||
|
|||||||
@ -26,6 +26,7 @@ import {
|
|||||||
RED_3,
|
RED_3,
|
||||||
YELLOW_2,
|
YELLOW_2,
|
||||||
} from '../../constants/Color.constants';
|
} from '../../constants/Color.constants';
|
||||||
|
import { SEMANTIC_OPERATORS } from '../../constants/DataContract.constants';
|
||||||
import { EntityReferenceFields } from '../../enums/AdvancedSearch.enum';
|
import { EntityReferenceFields } from '../../enums/AdvancedSearch.enum';
|
||||||
import { SearchIndex } from '../../enums/search.enum';
|
import { SearchIndex } from '../../enums/search.enum';
|
||||||
import { TestCaseType } from '../../enums/TestSuite.enum';
|
import { TestCaseType } from '../../enums/TestSuite.enum';
|
||||||
@ -239,7 +240,7 @@ export const getSematicRuleFields = () => {
|
|||||||
label: 'Tags',
|
label: 'Tags',
|
||||||
type: 'select',
|
type: 'select',
|
||||||
mainWidgetProps: jsonLogicSearchClassBase.mainWidgetProps,
|
mainWidgetProps: jsonLogicSearchClassBase.mainWidgetProps,
|
||||||
operators: jsonLogicSearchClassBase.defaultSelectOperators,
|
operators: SEMANTIC_OPERATORS,
|
||||||
fieldSettings: {
|
fieldSettings: {
|
||||||
asyncFetch: jsonLogicSearchClassBase.searchAutocomplete({
|
asyncFetch: jsonLogicSearchClassBase.searchAutocomplete({
|
||||||
searchIndex: SearchIndex.TAG,
|
searchIndex: SearchIndex.TAG,
|
||||||
@ -265,7 +266,7 @@ export const getSematicRuleFields = () => {
|
|||||||
label: 'Tags',
|
label: 'Tags',
|
||||||
type: 'select',
|
type: 'select',
|
||||||
mainWidgetProps: jsonLogicSearchClassBase.mainWidgetProps,
|
mainWidgetProps: jsonLogicSearchClassBase.mainWidgetProps,
|
||||||
operators: jsonLogicSearchClassBase.defaultSelectOperators,
|
operators: SEMANTIC_OPERATORS,
|
||||||
fieldSettings: {
|
fieldSettings: {
|
||||||
asyncFetch: jsonLogicSearchClassBase.searchAutocomplete({
|
asyncFetch: jsonLogicSearchClassBase.searchAutocomplete({
|
||||||
searchIndex: SearchIndex.GLOSSARY_TERM,
|
searchIndex: SearchIndex.GLOSSARY_TERM,
|
||||||
@ -280,13 +281,22 @@ export const getSematicRuleFields = () => {
|
|||||||
|
|
||||||
const tierField = {
|
const tierField = {
|
||||||
label: t('label.tier'),
|
label: t('label.tier'),
|
||||||
type: 'select',
|
type: '!group',
|
||||||
|
mode: 'some',
|
||||||
fieldName: 'tags',
|
fieldName: 'tags',
|
||||||
|
defaultField: 'tagFQN',
|
||||||
|
subfields: {
|
||||||
|
tagFQN: {
|
||||||
|
label: 'Tags',
|
||||||
|
type: 'multiselect',
|
||||||
mainWidgetProps: jsonLogicSearchClassBase.mainWidgetProps,
|
mainWidgetProps: jsonLogicSearchClassBase.mainWidgetProps,
|
||||||
operators: jsonLogicSearchClassBase.defaultSelectOperators,
|
operators: SEMANTIC_OPERATORS,
|
||||||
fieldSettings: {
|
fieldSettings: {
|
||||||
asyncFetch: jsonLogicSearchClassBase.autoCompleteTier,
|
asyncFetch: jsonLogicSearchClassBase.autoCompleteTier,
|
||||||
useAsyncSearch: true,
|
useAsyncSearch: true,
|
||||||
|
listValues: jsonLogicSearchClassBase.autoCompleteTier,
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -55,6 +55,13 @@ class JSONLogicSearchClassBase {
|
|||||||
text: {
|
text: {
|
||||||
operators: ['like', 'not_like', 'regexp'],
|
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
|
// Limits source to user input values, not other fields
|
||||||
valueSources: ['value'],
|
valueSources: ['value'],
|
||||||
@ -66,6 +73,12 @@ class JSONLogicSearchClassBase {
|
|||||||
text: {
|
text: {
|
||||||
operators: ['like', 'not_like', 'regexp'],
|
operators: ['like', 'not_like', 'regexp'],
|
||||||
},
|
},
|
||||||
|
select: {
|
||||||
|
operators: [
|
||||||
|
...(this.baseConfig.types.select?.widgets?.select?.operators || []),
|
||||||
|
'array_contains',
|
||||||
|
],
|
||||||
|
},
|
||||||
},
|
},
|
||||||
valueSources: ['value'],
|
valueSources: ['value'],
|
||||||
},
|
},
|
||||||
@ -100,7 +113,7 @@ class JSONLogicSearchClassBase {
|
|||||||
...this.baseConfig.widgets.text,
|
...this.baseConfig.widgets.text,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
configOperators = {
|
configOperators: Config['operators'] = {
|
||||||
...this.baseConfig.operators,
|
...this.baseConfig.operators,
|
||||||
like: {
|
like: {
|
||||||
...this.baseConfig.operators.like,
|
...this.baseConfig.operators.like,
|
||||||
@ -141,7 +154,6 @@ class JSONLogicSearchClassBase {
|
|||||||
label: t('label.is-entity', { entity: t('label.reviewer') }),
|
label: t('label.is-entity', { entity: t('label.reviewer') }),
|
||||||
labelForFormat: t('label.is-entity', { entity: t('label.reviewer') }),
|
labelForFormat: t('label.is-entity', { entity: t('label.reviewer') }),
|
||||||
cardinality: 0,
|
cardinality: 0,
|
||||||
unary: true,
|
|
||||||
jsonLogic: 'isReviewer',
|
jsonLogic: 'isReviewer',
|
||||||
sqlOp: 'IS REVIEWER',
|
sqlOp: 'IS REVIEWER',
|
||||||
},
|
},
|
||||||
@ -149,10 +161,17 @@ class JSONLogicSearchClassBase {
|
|||||||
label: t('label.is-entity', { entity: t('label.owner') }),
|
label: t('label.is-entity', { entity: t('label.owner') }),
|
||||||
labelForFormat: t('label.is-entity', { entity: t('label.owner') }),
|
labelForFormat: t('label.is-entity', { entity: t('label.owner') }),
|
||||||
cardinality: 0,
|
cardinality: 0,
|
||||||
unary: true,
|
|
||||||
jsonLogic: 'isOwner',
|
jsonLogic: 'isOwner',
|
||||||
sqlOp: 'IS OWNER',
|
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>;
|
mapFields: Record<string, FieldOrGroup>;
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user